Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove onDelete for Placeholder tag for non Platform admiins #3959

Merged
merged 11 commits into from
Nov 20, 2024
35 changes: 17 additions & 18 deletions editor.planx.uk/src/@planx/components/Checklist/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Delete from "@mui/icons-material/Delete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import FormControlLabel from "@mui/material/FormControlLabel";
import IconButton from "@mui/material/IconButton";
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { FormikErrors, FormikValues, useFormik } from "formik";
Expand Down Expand Up @@ -378,19 +377,19 @@ export const ChecklistComponent: React.FC<ChecklistProps> = (props) => {
/>
</InputRow>
<InputRow>
<Switch
checked={!!formik.values.groupedOptions}
onChange={() =>
formik.setValues({
...formik.values,
...toggleExpandableChecklist({
options: formik.values.options,
groupedOptions: formik.values.groupedOptions,
}),
})
}
label="Expandable"
/>
<Switch
checked={!!formik.values.groupedOptions}
onChange={() =>
formik.setValues({
...formik.values,
...toggleExpandableChecklist({
options: formik.values.options,
groupedOptions: formik.values.groupedOptions,
}),
})
}
label="Expandable"
/>
</InputRow>
<InputRow>
<Switch
Expand All @@ -401,16 +400,16 @@ export const ChecklistComponent: React.FC<ChecklistProps> = (props) => {
!formik.values.allRequired,
)
}
label="All required"
/>
label="All required"
/>
</InputRow>
<InputRow>
<Switch
checked={formik.values.neverAutoAnswer}
onChange={() =>
onChange={() =>
formik.setFieldValue(
"neverAutoAnswer",
!formik.values.neverAutoAnswer
!formik.values.neverAutoAnswer,
)
}
label="Always put to user (forgo automation)"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import Box from "@mui/material/Box";
import { Palette, useTheme } from "@mui/material/styles";
import { NodeTag } from "@opensystemslab/planx-core/types";
import { NodeTag, Role } from "@opensystemslab/planx-core/types";
import { useStore } from "pages/FlowEditor/lib/store";
import React from "react";
import { getContrastTextColor } from "styleUtils";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";

export const TAG_DISPLAY_VALUES: Record<
NodeTag,
{ color: keyof Palette["nodeTag"]; displayName: string }
{ color: keyof Palette["nodeTag"]; displayName: string; editableBy?: Role[] }
> = {
placeholder: {
color: "blocking",
displayName: "Placeholder",
editableBy: ["platformAdmin"],
},
toReview: {
color: "nonBlocking",
Expand Down
119 changes: 119 additions & 0 deletions editor.planx.uk/src/ui/editor/ComponentTagSelect.test.tsx
RODO94 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import ChecklistComponent from "@planx/components/Checklist/Editor";
import { within } from "@testing-library/react";
import { TAG_DISPLAY_VALUES } from "pages/FlowEditor/components/Flow/components/Tag";
import { useStore } from "pages/FlowEditor/lib/store";
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { act } from "react-dom/test-utils";
import { setup } from "testUtils";
import { it } from "vitest";

const { setState } = useStore;

const mockUser = {
id: 200,
firstName: "Testy",
lastName: "McTester",
email: "[email protected]",
teams: [],
};

describe("Checklist Component for a Platform Admin", () => {
beforeEach(() =>
act(() =>
setState({
user: {
...mockUser,
isPlatformAdmin: true,
},
teamSlug: "team",
}),
),
);

it("renders all tags with none selected", async () => {
const { getByRole, user } = setup(
<DndProvider backend={HTML5Backend}>
<ChecklistComponent text="" />
</DndProvider>,
);
const tagSelect = getByRole("combobox", { name: /tag this component/i });

await user.click(tagSelect);

const optionsList = getByRole("listbox", { name: /tag this component/i });
const options = within(optionsList).getAllByRole("option");

const tagDisplayNames = Object.values(TAG_DISPLAY_VALUES).map(
(tag) => tag.displayName,
);
const optionTexts = options.map((option) => option.textContent);

expect(optionTexts).toEqual(expect.arrayContaining(tagDisplayNames));
});

it("renders all tags with Placeholder selected as a button", async () => {
const { queryByTestId, queryByRole } = setup(
<DndProvider backend={HTML5Backend}>
<ChecklistComponent
text=""
node={{ data: { text: "", tags: ["placeholder"] } }}
/>
</DndProvider>,
);

const placeholderChip = queryByTestId("placeholder-chip");
const placeholderButton = queryByRole("button", { name: /placeholder/i });

expect(placeholderChip).toBeInTheDocument();
expect(placeholderButton).toBeInTheDocument();
});
});

describe("Checklist Component for a non Platform Admin", () => {
beforeEach(() =>
act(() =>
setState({
user: {
...mockUser,
isPlatformAdmin: false,
},
}),
),
);

it("renders all tags except Placeholder with none selected", async () => {
const { getByRole, user } = setup(
<DndProvider backend={HTML5Backend}>
<ChecklistComponent text="" />
</DndProvider>,
);
const tagSelect = getByRole("combobox", { name: /tag this component/i });

await user.click(tagSelect);

const optionsList = getByRole("listbox", { name: /tag this component/i });
const options = within(optionsList).getAllByRole("option");
const optionTexts = options.map((option) => option.textContent);

expect(optionTexts).not.toContain(/placeholder/i);
});

it("renders all tags with static Placeholder selected", async () => {
const { getByTestId, queryByRole } = setup(
<DndProvider backend={HTML5Backend}>
<ChecklistComponent
text=""
node={{ data: { text: "", tags: ["placeholder"] } }}
/>
</DndProvider>,
);

const placeholderChip = getByTestId("placeholder-chip");
const placeholderButton = queryByRole("button", { name: /placeholder/i });

expect(placeholderChip).toBeInTheDocument();
expect(placeholderButton).not.toBeInTheDocument();
});
});
64 changes: 43 additions & 21 deletions editor.planx.uk/src/ui/editor/ComponentTagSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import BookmarksIcon from "@mui/icons-material/Bookmarks";
import { AutocompleteProps } from "@mui/material/Autocomplete";
import Chip from "@mui/material/Chip";
import ListItem from "@mui/material/ListItem";
import { NODE_TAGS, NodeTag } from "@opensystemslab/planx-core/types";
import { NODE_TAGS, NodeTag, Role } from "@opensystemslab/planx-core/types";
import { TAG_DISPLAY_VALUES } from "pages/FlowEditor/components/Flow/components/Tag";
import { useStore } from "pages/FlowEditor/lib/store";
import React from "react";
import { getContrastTextColor } from "styleUtils";
import ModalSection from "ui/editor/ModalSection";
Expand All @@ -16,18 +17,29 @@ interface Props {
onChange: (values: NodeTag[]) => void;
}

const skipTag = (role?: Role) => {
const userRole = useStore.getState().getUserRoleForCurrentTeam();
return role === userRole ? false : true;
};

const renderOption: AutocompleteProps<
NodeTag,
true,
true,
false,
"div"
>["renderOption"] = (props, tag, { selected }) => (
<ListItem {...props}>
<CustomCheckbox aria-hidden="true" className={selected ? "selected" : ""} />
{TAG_DISPLAY_VALUES[tag].displayName}
</ListItem>
);
>["renderOption"] = (props, tag, { selected }) => {
if (TAG_DISPLAY_VALUES[tag].editableBy?.some(skipTag)) return null;
return (
<ListItem {...props}>
<CustomCheckbox
aria-hidden="true"
className={selected ? "selected" : ""}
/>
{TAG_DISPLAY_VALUES[tag].displayName}
</ListItem>
);
};

const renderTags: AutocompleteProps<
NodeTag,
Expand All @@ -36,20 +48,30 @@ const renderTags: AutocompleteProps<
false,
"div"
>["renderTags"] = (value, getTagProps) =>
value.map((tag, index) => (
<Chip
{...getTagProps({ index })}
key={tag}
label={TAG_DISPLAY_VALUES[tag].displayName}
sx={(theme) => ({
backgroundColor: theme.palette.nodeTag[TAG_DISPLAY_VALUES[tag].color],
color: getContrastTextColor(
theme.palette.nodeTag[TAG_DISPLAY_VALUES[tag].color],
"#FFF",
),
})}
/>
));
value.map((tag, index) => {
return (
<Chip
{...getTagProps({ index })}
data-testid={
TAG_DISPLAY_VALUES[tag].displayName.toLowerCase() + "-chip"
}
key={tag}
label={TAG_DISPLAY_VALUES[tag].displayName}
sx={(theme) => ({
backgroundColor: theme.palette.nodeTag[TAG_DISPLAY_VALUES[tag].color],
color: getContrastTextColor(
theme.palette.nodeTag[TAG_DISPLAY_VALUES[tag].color],
"#FFF",
),
})}
onDelete={
TAG_DISPLAY_VALUES[tag].editableBy?.some(skipTag)
? undefined
: getTagProps({ index }).onDelete
}
/>
);
});

export const ComponentTagSelect: React.FC<Props> = ({ value, onChange }) => {
return (
Expand Down
Loading