Skip to content

Commit 10a479b

Browse files
committed
feat(offline): Ensure offline UI displays for logged-in users
ie. replace the "Saved X secs ago" UI with the offline UI Moved the OfflineBadge into a new component, so that it can be shared / used across both the SaveButton and the SaveStatus components
1 parent 411e35f commit 10a479b

6 files changed

Lines changed: 101 additions & 32 deletions

File tree

src/components/Mobile/MobileProjectBar/MobileProjectBar.jsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import "../../../assets/stylesheets/MobileProjectBar.scss";
22
import SaveStatus from "../../SaveStatus/SaveStatus";
3+
import OfflineBadge from "../../OfflineBadge/OfflineBadge";
34
import { useSelector } from "react-redux";
5+
import useIsOnline from "../../../hooks/useIsOnline";
46
import React from "react";
57

68
const MobileProjectBar = () => {
79
const projectName = useSelector((state) => state.editor.project.name);
810
const lastSavedTime = useSelector((state) => state.editor.lastSavedTime);
11+
const offlineEnabled = useSelector((state) => state.editor.offlineEnabled);
912
const readOnly = useSelector((state) => state.editor.readOnly);
13+
const isOnline = useIsOnline();
1014

1115
return (
1216
<div className="mobile-project-bar">
1317
<p className="mobile-project-bar__name">{projectName}</p>
14-
{lastSavedTime && !readOnly ? <SaveStatus isMobile={true} /> : null}
18+
{!readOnly && (
19+
offlineEnabled && !isOnline
20+
? <OfflineBadge />
21+
: lastSavedTime && <SaveStatus isMobile={true} />
22+
)}
1523
</div>
1624
);
1725
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react";
2+
import { useTranslation } from "react-i18next";
3+
import classNames from "classnames";
4+
import OfflineIcon from "../../assets/icons/offline.svg";
5+
6+
const OfflineBadge = ({ className }) => {
7+
const { t } = useTranslation();
8+
9+
return (
10+
<div
11+
className={classNames(className, "offline-badge")}
12+
tabIndex={0}
13+
aria-describedby="offline-badge-tooltip"
14+
>
15+
<OfflineIcon />
16+
<span>{t("header.offline")}</span>
17+
<div
18+
id="offline-badge-tooltip"
19+
className="offline-badge__tooltip"
20+
role="tooltip"
21+
>
22+
<p>{t("header.offlineTooltipDevice")}</p>
23+
<p>{t("header.offlineTooltipContinue")}</p>
24+
</div>
25+
</div>
26+
);
27+
};
28+
29+
export default OfflineBadge;

src/components/ProjectBar/ProjectBar.jsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import React from "react";
22
import { useSelector } from "react-redux";
33
import { useTranslation } from "react-i18next";
44
import SaveStatus from "../SaveStatus/SaveStatus";
5+
import OfflineBadge from "../OfflineBadge/OfflineBadge";
56
import DownloadIcon from "../../assets/icons/download.svg";
67
import ProjectName from "../ProjectName/ProjectName";
78
import DownloadButton from "../DownloadButton/DownloadButton";
89
import SaveButton from "../SaveButton/SaveButton";
10+
import useIsOnline from "../../hooks/useIsOnline";
911

1012
import "../../assets/stylesheets/ProjectBar.scss";
1113
import { isOwner } from "../../utils/projectHelpers";
@@ -15,10 +17,11 @@ const ProjectBar = ({ nameEditable = true }) => {
1517
const project = useSelector((state) => state.editor.project);
1618
const user = useSelector((state) => state.auth.user);
1719
const loading = useSelector((state) => state.editor.loading);
18-
const saving = useSelector((state) => state.editor.saving);
1920
const lastSavedTime = useSelector((state) => state.editor.lastSavedTime);
21+
const offlineEnabled = useSelector((state) => state.editor.offlineEnabled);
2022
const projectOwner = isOwner(user, project);
2123
const readOnly = useSelector((state) => state.editor.readOnly);
24+
const isOnline = useIsOnline();
2225

2326
if (loading !== "success") {
2427
return null;
@@ -41,8 +44,14 @@ const ProjectBar = ({ nameEditable = true }) => {
4144
<SaveButton className="project-bar__btn btn--save" />
4245
</div>
4346
)}
44-
{lastSavedTime && user && !readOnly && (
45-
<SaveStatus saving={saving} lastSavedTime={lastSavedTime} />
47+
{user && !readOnly && (
48+
offlineEnabled && !isOnline
49+
? projectOwner && (
50+
<div className="project-bar__btn-wrapper">
51+
<OfflineBadge className="project-bar__btn" />
52+
</div>
53+
)
54+
: lastSavedTime && <SaveStatus />
4655
)}
4756
</div>
4857
</div>

src/components/ProjectBar/ProjectBar.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { Provider } from "react-redux";
44
import configureStore from "redux-mock-store";
55
import { MemoryRouter } from "react-router-dom";
66
import ProjectBar from "./ProjectBar";
7+
import useIsOnline from "../../hooks/useIsOnline";
78

89
jest.mock("axios");
10+
jest.mock("../../hooks/useIsOnline");
911

1012
jest.mock("react-router-dom", () => ({
1113
...jest.requireActual("react-router-dom"),
@@ -27,6 +29,10 @@ const user = {
2729
},
2830
};
2931

32+
beforeEach(() => {
33+
useIsOnline.mockReturnValue(true);
34+
});
35+
3036
const renderProjectBar = (state) => {
3137
const middlewares = [];
3238
const mockStore = configureStore(middlewares);
@@ -184,6 +190,45 @@ describe("When no project loaded", () => {
184190
});
185191
});
186192

193+
describe("offline badge", () => {
194+
beforeEach(() => {
195+
useIsOnline.mockReturnValue(false);
196+
});
197+
198+
test("shows offline badge for project owner when offline", () => {
199+
renderProjectBar({
200+
editor: { project, offlineEnabled: true, lastSavedTime: Date.now() },
201+
auth: { user },
202+
});
203+
expect(screen.queryByText("header.offline")).toBeInTheDocument();
204+
});
205+
206+
test("shows offline badge for project owner on fresh load with no save history", () => {
207+
renderProjectBar({
208+
editor: { project, offlineEnabled: true },
209+
auth: { user },
210+
});
211+
expect(screen.queryByText("header.offline")).toBeInTheDocument();
212+
});
213+
214+
test("does not show a second offline badge for non-owners (SaveButton handles it)", () => {
215+
const nonOwner = { ...user, profile: { user: "someone-else" } };
216+
renderProjectBar({
217+
editor: { project, offlineEnabled: true, lastSavedTime: Date.now() },
218+
auth: { user: nonOwner },
219+
});
220+
expect(screen.queryAllByText("header.offline")).toHaveLength(1);
221+
});
222+
223+
test("does not show offline badge when offlineEnabled is false", () => {
224+
renderProjectBar({
225+
editor: { project, offlineEnabled: false },
226+
auth: { user },
227+
});
228+
expect(screen.queryByText("header.offline")).not.toBeInTheDocument();
229+
});
230+
});
231+
187232
describe("When read only", () => {
188233
beforeEach(() => {
189234
renderProjectBar({

src/components/SaveButton/SaveButton.jsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { logInEvent } from "../../events/WebComponentCustomEvents";
77
import { isOwner } from "../../utils/projectHelpers";
88

99
import DesignSystemButton from "../DesignSystemButton/DesignSystemButton";
10+
import OfflineBadge from "../OfflineBadge/OfflineBadge";
1011
import SaveIcon from "../../assets/icons/save.svg";
11-
import OfflineIcon from "../../assets/icons/offline.svg";
1212
import { triggerSave } from "../../redux/EditorSlice";
1313
import useIsOnline from "../../hooks/useIsOnline";
1414

@@ -43,24 +43,7 @@ const SaveButton = ({ className, type, fill = false }) => {
4343
if (loading !== "success" || projectOwner || !buttonType) return null;
4444

4545
if (offlineEnabled && !isOnline) {
46-
return (
47-
<div
48-
className={classNames(className, "offline-badge")}
49-
tabIndex={0}
50-
aria-describedby="offline-badge-tooltip"
51-
>
52-
<OfflineIcon />
53-
<span>{t("header.offline")}</span>
54-
<div
55-
id="offline-badge-tooltip"
56-
className="offline-badge__tooltip"
57-
role="tooltip"
58-
>
59-
<p>{t("header.offlineTooltipDevice")}</p>
60-
<p>{t("header.offlineTooltipContinue")}</p>
61-
</div>
62-
</div>
63-
);
46+
return <OfflineBadge className={className} />;
6447
}
6548

6649
return (

src/components/SaveStatus/SaveStatus.test.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,24 @@ const project = {
1111
};
1212

1313
let store;
14-
let saveStatus;
1514

1615
describe("With a save button", () => {
1716
beforeEach(() => {
18-
const middlewares = [];
19-
const mockStore = configureStore(middlewares);
20-
const initialState = {
17+
store = configureStore([])({
2118
editor: {
2219
project: project,
2320
loading: "success",
2421
lastSavedTime: Date.now(),
2522
},
26-
};
27-
store = mockStore(initialState);
23+
});
2824
render(
2925
<Provider store={store}>
3026
<SaveStatus />
3127
</Provider>,
3228
);
33-
saveStatus = screen.queryByText("saveStatus.saved now");
3429
});
3530

36-
test("Renders save button", () => {
37-
expect(saveStatus).toBeInTheDocument();
31+
test("Renders save status", () => {
32+
expect(screen.queryByText("saveStatus.saved now")).toBeInTheDocument();
3833
});
3934
});

0 commit comments

Comments
 (0)