Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ The `editor-wc` tag accepts the following attributes, which must be provided as
- `output_panels`: Array of output panel names to display (defaults to `["text", "visual"]`)
- `output_split_view`: Start with split view in output panel (defaults to `false`, i.e. tabbed view)
- `project_name_editable`: Allow the user to edit the project name in the project bar (defaults to `false`)
- `preview`: Enable preview mode (Python, HTML, and Scratch) — upload and download stay available, but saving, renaming, autosave, remix, and local backup are disabled (defaults to `false`). If `read_only` is also set, read-only behaviour takes precedence and upload is hidden.
- `react_app_api_endpoint`: API endpoint to send project-related requests to
- `offline_enabled`: Show an offline indicator when the user's device loses connectivity (defaults to `false`). Requires the host page's service worker to broadcast `{ type: "OFFLINE" }` / `{ type: "ONLINE" }` messages - see [Offline support](#offline-support).
- `read_only`: Display the editor in read only mode (defaults to `false`)
Expand Down
5 changes: 4 additions & 1 deletion src/components/Menus/Sidebar/ProjectsPanel/ProjectsPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ import { useMediaQuery } from "react-responsive";
import SaveStatus from "../../../SaveStatus/SaveStatus";
import { navigateToProjectsPageEvent } from "../../../../events/WebComponentCustomEvents";
import DesignSystemButton from "../../../DesignSystemButton/DesignSystemButton";
import { usePreviewMode } from "../../../../hooks/usePreviewMode";

const ProjectsPanel = () => {
const { t } = useTranslation();

const isLoggedIn = useSelector((state) => state?.auth?.user);
const isMobile = useMediaQuery({ query: MOBILE_MEDIA_QUERY });
const readOnly = useSelector((state) => state.editor.readOnly);
const previewMode = usePreviewMode();
const canEditProjectName = !readOnly && !previewMode;

const saveOptions = (
<>
Expand Down Expand Up @@ -60,7 +63,7 @@ const ProjectsPanel = () => {
<ProjectName
showLabel={true}
className="projects-panel__item"
editable={!readOnly}
editable={canEditProjectName}
/>
<ProjectInfo className="projects-panel__item" />
<div className="projects-panel__button">
Expand Down
18 changes: 18 additions & 0 deletions src/components/Menus/Sidebar/ProjectsPanel/ProjectsPanel.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,22 @@ describe("Projects Panel", () => {
).not.toBeInTheDocument();
});
});

describe("When preview mode", () => {
beforeEach(() => {
renderProjectsPanel({
editor: {
...initialState.editor,
preview: true,
readOnly: false,
},
});
});

test("Project name is not editable", () => {
expect(
screen.queryByTitle("header.renameProject"),
).not.toBeInTheDocument();
});
});
});
9 changes: 6 additions & 3 deletions src/components/ProjectBar/ProjectBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ProjectName from "../ProjectName/ProjectName";
import DownloadButton from "../DownloadButton/DownloadButton";
import SaveButton from "../SaveButton/SaveButton";
import useIsOnline from "../../hooks/useIsOnline";
import { usePreviewMode } from "../../hooks/usePreviewMode";

import "../../assets/stylesheets/ProjectBar.scss?inline";
import { isOwner } from "../../utils/projectHelpers";
Expand All @@ -21,6 +22,8 @@ const ProjectBar = ({ nameEditable = true }) => {
const offlineEnabled = useSelector((state) => state.editor.offlineEnabled);
const projectOwner = isOwner(user, project);
const readOnly = useSelector((state) => state.editor.readOnly);
const previewMode = usePreviewMode();
const canSave = !readOnly && !previewMode;
Comment thread
DNR500 marked this conversation as resolved.
const isOnline = useIsOnline();

if (loading !== "success") {
Expand All @@ -29,7 +32,7 @@ const ProjectBar = ({ nameEditable = true }) => {

return (
<div className="project-bar" data-testid="default-project-bar">
<ProjectName editable={!readOnly && nameEditable} isHeading={true} />
<ProjectName editable={canSave && nameEditable} isHeading={true} />
<div className="project-bar__right">
<div className="project-bar__btn-wrapper">
<DownloadButton
Expand All @@ -39,13 +42,13 @@ const ProjectBar = ({ nameEditable = true }) => {
type="tertiary"
/>
</div>
{!projectOwner && !readOnly && (
{!projectOwner && canSave && (
<div className="project-bar__btn-wrapper">
<SaveButton className="project-bar__btn btn--save" />
</div>
)}
{user &&
!readOnly &&
canSave &&
(offlineEnabled && !isOnline
? projectOwner && (
<div className="project-bar__btn-wrapper">
Expand Down
32 changes: 32 additions & 0 deletions src/components/ProjectBar/ProjectBar.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,35 @@ describe("When read only", () => {
expect(screen.queryByText(/saveStatus.saved/)).not.toBeInTheDocument();
});
});

describe("When preview mode", () => {
beforeEach(() => {
renderProjectBar({
editor: {
project,
preview: true,
readOnly: false,
lastSavedTime: new Date().getTime(),
},
auth: {
user,
},
});
});

test("Project name is not editable", () => {
expect(screen.queryByTitle("header.renameProject")).not.toBeInTheDocument();
});

test("Save button is not shown", () => {
expect(screen.queryByText("header.save")).not.toBeInTheDocument();
});

test("Save status is not shown", () => {
expect(screen.queryByText(/saveStatus.saved/)).not.toBeInTheDocument();
});

test("Download button remains available", () => {
expect(screen.queryByText("header.download")).toBeInTheDocument();
});
});
7 changes: 5 additions & 2 deletions src/components/ProjectBar/ScratchProjectBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SaveStatus from "../SaveStatus/SaveStatus";
import "../../assets/stylesheets/ProjectBar.scss?inline";
import { setScratchLastSavedTime } from "../../redux/EditorSlice";
import { useScratchSave } from "../../hooks/useScratchSave";
import { usePreviewMode } from "../../hooks/usePreviewMode";

const getProjectLastSavedTime = (updatedAt) => {
const timestamp = Date.parse(updatedAt || "");
Expand All @@ -27,9 +28,11 @@ const ScratchProjectBar = ({ nameEditable = true }) => {
const loading = useSelector((state) => state.editor.loading);
const project = useSelector((state) => state.editor.project);
const readOnly = useSelector((state) => state.editor.readOnly);
const previewMode = usePreviewMode();
const saving = useSelector((state) => state.editor.saving);
const lastSavedTime = useSelector((state) => state.editor.lastSavedTime);
const canSave = Boolean(user && !readOnly);
const canSave = Boolean(user && !readOnly && !previewMode);
const canEditProjectName = !readOnly && !previewMode && nameEditable;
Comment thread
DNR500 marked this conversation as resolved.
const { saveScratchProject, shouldRemixOnSave } = useScratchSave({
enabled: canSave,
});
Expand Down Expand Up @@ -62,7 +65,7 @@ const ScratchProjectBar = ({ nameEditable = true }) => {

return (
<div className="project-bar" data-testid="scratch-project-bar">
<ProjectName editable={!readOnly && nameEditable} isHeading={true} />
<ProjectName editable={canEditProjectName} isHeading={true} />
<div className="project-bar__right">
{!readOnly && (
<div className="project-bar__btn-wrapper">
Expand Down
58 changes: 58 additions & 0 deletions src/components/ProjectBar/ScratchProjectBar.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,61 @@ describe("Additional Scratch manual save states", () => {
expect(screen.queryByText("header.loginToSave")).not.toBeInTheDocument();
});
});

describe("When preview mode", () => {
test("shows upload and download but not save, and project name is not editable", () => {
renderSignedInScratchProjectBar({
editor: {
preview: true,
},
});

expect(
screen.queryByRole("button", { name: "header.renameProject" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "header.upload" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "header.download" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "header.save" }),
).not.toBeInTheDocument();
});

test("does not auto-save after a Scratch project change", () => {
renderSignedInScratchProjectBar({
editor: {
preview: true,
},
});

dispatchScratchMessage("scratch-gui-project-changed");

act(() => {
vi.advanceTimersByTime(2000);
});

expect(postMessageToScratchIframe).not.toHaveBeenCalled();
});

test("read only overrides preview and hides upload", () => {
renderSignedInScratchProjectBar({
editor: {
preview: true,
readOnly: true,
},
});

expect(
screen.queryByRole("button", { name: "header.upload" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "header.download" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "header.save" }),
).not.toBeInTheDocument();
});
});
12 changes: 11 additions & 1 deletion src/components/SaveButton/SaveButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import OfflineBadge from "../OfflineBadge/OfflineBadge";
import SaveIcon from "../../assets/icons/save.svg";
import { triggerSave } from "../../redux/EditorSlice";
import useIsOnline from "../../hooks/useIsOnline";
import { usePreviewMode } from "../../hooks/usePreviewMode";

const SaveButton = ({ className, type, fill = false }) => {
const dispatch = useDispatch();
Expand All @@ -21,6 +22,8 @@ const SaveButton = ({ className, type, fill = false }) => {
const user = useSelector((state) => state.auth.user);
const project = useSelector((state) => state.editor.project);
const offlineEnabled = useSelector((state) => state.editor.offlineEnabled);
const readOnly = useSelector((state) => state.editor.readOnly);
const previewMode = usePreviewMode();
const isOnline = useIsOnline();

useEffect(() => {
Expand All @@ -39,7 +42,14 @@ const SaveButton = ({ className, type, fill = false }) => {

const projectOwner = isOwner(user, project);

if (loading !== "success" || projectOwner || !buttonType) return null;
if (
loading !== "success" ||
projectOwner ||
!buttonType ||
readOnly ||
previewMode
)
return null;

if (offlineEnabled && !isOnline) {
return <OfflineBadge className={className} />;
Expand Down
71 changes: 71 additions & 0 deletions src/components/SaveButton/SaveButton.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,77 @@ describe("When project is loaded", () => {
).not.toBeInTheDocument();
});
});

describe("in preview mode", () => {
beforeEach(() => {
const middlewares = [];
const mockStore = configureStore(middlewares);
const initialState = {
editor: {
loading: "success",
webComponent: true,
preview: true,
readOnly: false,
project: {
identifier: "hot-diggity-dog",
user_id: "some-other-user",
},
},
auth: {
user: {
profile: {
user: "some-dummy-user",
},
},
},
};
store = mockStore(initialState);
render(
<Provider store={store}>
<SaveButton />
</Provider>,
);
});

test("Does not render save button", () => {
expect(screen.queryByText("header.save")).not.toBeInTheDocument();
});
});

describe("in read only mode", () => {
beforeEach(() => {
const middlewares = [];
const mockStore = configureStore(middlewares);
const initialState = {
editor: {
loading: "success",
webComponent: true,
readOnly: true,
project: {
identifier: "hot-diggity-dog",
user_id: "some-other-user",
},
},
auth: {
user: {
profile: {
user: "some-dummy-user",
},
},
},
};
store = mockStore(initialState);
render(
<Provider store={store}>
<SaveButton />
</Provider>,
);
});

test("Does not render save button", () => {
expect(screen.queryByText("header.save")).not.toBeInTheDocument();
});
});
});

describe("Without a logged in user", () => {
Expand Down
6 changes: 6 additions & 0 deletions src/containers/WebComponentLoader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
setReactAppApiEndpoint,
setScratchApiEndpoint,
setReadOnly,
setPreview,
} from "../redux/EditorSlice";
import WebComponentProject from "../components/WebComponentProject/WebComponentProject";
import { useTranslation } from "react-i18next";
Expand Down Expand Up @@ -62,6 +63,7 @@ const WebComponentLoader = (props) => {
projectNameEditable = false,
reactAppApiEndpoint = process.env.REACT_APP_API_ENDPOINT,
scratchApiEndpoint = process.env.REACT_APP_API_ENDPOINT,
preview = false,
readOnly = false,
senseHatAlwaysEnabled = false,
friendlyErrorsEnabled = false,
Expand Down Expand Up @@ -201,6 +203,10 @@ const WebComponentLoader = (props) => {
dispatch(setReadOnly(readOnly));
}, [readOnly, dispatch]);

useEffect(() => {
dispatch(setPreview(preview));
}, [preview, dispatch]);

useEffect(() => {
dispatch(setOfflineEnabled(offlineEnabled));
}, [offlineEnabled, dispatch]);
Expand Down
Loading
Loading