Skip to content

Commit

Permalink
[MDS-4971] In-app help (#3271)
Browse files Browse the repository at this point in the history
* add roles and packages

* BE: create model, migration, add new role to constants, add basic resource

* create reducer, add to root, add keys to routes

* create HelpGuide component. Copy in rich text editor from prototype, improve later

* add to CORE header. Refactor CORE navbar

* change MS Header to TS and add in help guide. Add in basic CSS rules for tooltips in editor

* fix bugs, add missing functionality

* extract HelpGuide interface

* fix a couple style things on MS, make sure delete is disabled properly

* fix a bug on MS, sanitize HTML, and test sanitization of HTML on MS

* BE testing

* MDS-4971 Updated webpack css chunk filename for MiniCssExtractPlugin to fix webpack bundle issue

---------

Co-authored-by: Simen Fivelstad Smaaberg <66635118+simensma-fresh@users.noreply.github.com>
  • Loading branch information
taraepp and simensma-fresh authored Oct 18, 2024
1 parent 626d094 commit ad29b1a
Show file tree
Hide file tree
Showing 58 changed files with 3,404 additions and 823 deletions.
24 changes: 24 additions & 0 deletions migrations/sql/V2024.10.03.13.03__create_help_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS help (
help_guid uuid DEFAULT gen_random_uuid() PRIMARY KEY,
help_key VARCHAR(30) NOT NULL,
system VARCHAR(9) NOT NULL,
page_tab VARCHAR(50) DEFAULT 'all_tabs',
content VARCHAR,
is_draft BOOLEAN DEFAULT false,
create_user VARCHAR(60) NOT NULL,
create_timestamp timestamp with time zone DEFAULT now() NOT NULL,
update_user VARCHAR(60) NOT NULL,
update_timestamp timestamp with time zone DEFAULT now() NOT NULL
);

INSERT INTO help (
help_key, system, page_tab, content, is_draft, create_user, update_user
) VALUES (
'default', 'CORE', 'all_tabs', '<p>Check back soon for updates. Thank you for your patience.</p>', false, 'system', 'system'
);

INSERT INTO help (
help_key, system, page_tab, content, is_draft, create_user, update_user
) VALUES (
'default', 'MineSpace', 'all_tabs', '<p>Check back soon for updates. Thank you for your patience.</p>', false, 'system', 'system'
);
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,10 @@
"tasks": {
"*.js": []
}
},
"dependencies": {
"dompurify": "^3.1.7",
"html-react-parser": "^5.1.17",
"react-quill": "^2.0.0"
}
}
5 changes: 4 additions & 1 deletion services/common/src/@Types/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { CoreRoute } from "../interfaces/common/route.interface";
import { ItemMap } from "../interfaces/common/itemMap.interface";

declare global {
const REQUEST_HEADER: {
createRequestHeader: (
Expand All @@ -10,7 +13,7 @@ declare global {
};
};
};
const GLOBAL_ROUTES: any;
const GLOBAL_ROUTES: ItemMap<CoreRoute>;
}

export {};
1 change: 1 addition & 0 deletions services/common/src/components/forms/BaseInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface BaseInputProps extends WrappedFieldProps {
loading?: boolean;
allowClear?: boolean;
help?: string;
showOptional?: boolean;
}

interface BaseViewInputProps {
Expand Down
4 changes: 3 additions & 1 deletion services/common/src/components/forms/RenderRadioButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ const RenderRadioButtons: FC<RenderRadioButtonsProps> = ({
id,
help,
customOptions,
labelSubtitle,
required = false,
optionType = "default",
isVertical = false,
showOptional = true,
}) => {
const { isEditMode } = useContext(FormContext);

Expand All @@ -49,7 +51,7 @@ const RenderRadioButtons: FC<RenderRadioButtonsProps> = ({
meta.touched &&
((meta.error && <span>{meta.error}</span>) || (meta.warning && <span>{meta.warning}</span>))
}
label={getFormItemLabel(label, required)}
label={getFormItemLabel(label, required, labelSubtitle, showOptional)}
>
<>
<Radio.Group
Expand Down
86 changes: 86 additions & 0 deletions services/common/src/components/forms/RenderRichTextEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { FC, useContext, useMemo } from "react";
import { Form } from "antd";
import { BaseInputProps, BaseViewInput, getFormItemLabel } from "./BaseInput";
import { FormContext } from "./FormWrapper";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import parse from "html-react-parser";
import DOMPurify from "dompurify";

const RenderRichTextEditor: FC<BaseInputProps> = ({
label,
labelSubtitle,
meta,
input,
disabled,
help,
required,
defaultValue,
id,
placeholder,
}) => {
const { isEditMode } = useContext(FormContext);

const handleAddImage = (data) => {
// just a click handler for the image button,
// everything else will have to be implemented
console.log("image data", data);
};
const colorOptions = ["#003366", "white", "#fcba19", "#d8292f", "#2e8540", "#313132", "black"];
const toolbarOptions = [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
["bold", "italic", "underline", "strike"],
["blockquote"],
[{ list: "ordered" }, { list: "bullet" }],
[{ color: colorOptions }, { background: colorOptions }, "font", "align"],
["link", "video"],
];

const modules = useMemo(
() => ({
toolbar: {
container: toolbarOptions,
// handlers: { image: handleAddImage },// TODO: add image to toolBarOptions and implement handler
},
}),
[]
);

const handleChange = (newValue) => {
input.onChange(newValue);
};

if (!isEditMode) {
return <BaseViewInput value={parse(DOMPurify.sanitize(input.value))} label={label} />;
}

return (
<Form.Item
id={id}
getValueProps={() => ({ value: input.value })}
name={input.name}
required={required}
validateStatus={meta.touched ? (meta.error && "error") || (meta.warning && "warning") : ""}
help={
meta.touched &&
((meta.error && <span>{meta.error}</span>) || (meta.warning && <span>{meta.warning}</span>))
}
label={getFormItemLabel(label, required, labelSubtitle)}
>
<>
<ReactQuill
readOnly={disabled}
defaultValue={defaultValue}
placeholder={placeholder}
theme="snow"
value={input.value}
onChange={handleChange}
modules={modules}
/>
{help && <div className={`form-item-help ${input.name}-form-help`}>{help}</div>}
</>
</Form.Item>
);
};

export default RenderRichTextEditor;
1 change: 0 additions & 1 deletion services/common/src/components/forms/RenderSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ interface SelectProps extends BaseInputProps {
onSelect?: (value, option) => void;
usedOptions: string[];
allowClear?: boolean;
showOptional?: boolean;
}

export const RenderSelect: FC<SelectProps> = ({
Expand Down
57 changes: 57 additions & 0 deletions services/common/src/components/help/HelpGuide-core.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import { ReduxWrapper } from "@mds/common/tests/utils/ReduxWrapper";
import { HelpGuideContent } from "./HelpGuide";
import { AUTHENTICATION } from "@mds/common/constants/reducerTypes";
import { SystemFlagEnum } from "@mds/common/constants";
import { HELP_GUIDE_CORE, USER_ACCESS_DATA } from "@mds/common/tests/mocks/dataMocks";
import { EMPTY_HELP_KEY, helpReducerType } from "@mds/common/redux/slices/helpSlice";

const coreState = {
[AUTHENTICATION]: {
systemFlag: SystemFlagEnum.core,
userAccessData: [...USER_ACCESS_DATA, "core_helpdesk"],
isAuthenticated: true,
},
[helpReducerType]: {
helpGuides: {
[EMPTY_HELP_KEY]: HELP_GUIDE_CORE.default,
},
},
};

function mockFunction() {
const original = jest.requireActual("react-router-dom");
return {
...original,
useParams: jest.fn().mockReturnValue({
tab: "overview",
}),
};
}

jest.mock("react-router-dom", () => mockFunction());

describe("HelpGuide", () => {
it("renders CORE properly", async () => {
const helpKey = "Not-Exists";

const { findByTestId, findByText } = render(
<ReduxWrapper initialState={coreState}>
<HelpGuideContent helpKey={helpKey} />
</ReduxWrapper>
);

const helpButton = await findByTestId("help-open");
fireEvent.click(helpButton);

const defaultContent = await findByText("CORE default content");
expect(defaultContent).toBeInTheDocument();

const editButton = await findByText("Edit Help Guide");
fireEvent.click(editButton);

const helpContent = await findByTestId("help-content");
expect(helpContent).toMatchSnapshot();
});
});
52 changes: 52 additions & 0 deletions services/common/src/components/help/HelpGuide-ms.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import { ReduxWrapper } from "@mds/common/tests/utils/ReduxWrapper";
import HelpGuide, { HelpGuideContent } from "./HelpGuide";
import { AUTHENTICATION } from "@mds/common/constants/reducerTypes";
import { SystemFlagEnum } from "@mds/common/constants";
import { MS_USER_ACCESS_DATA } from "@mds/common/tests/mocks/dataMocks";
import { helpReducerType } from "@mds/common/redux/slices/helpSlice";
import { BrowserRouter } from "react-router-dom";

const msState = {
[AUTHENTICATION]: {
systemFlag: SystemFlagEnum.ms,
userAccessData: MS_USER_ACCESS_DATA,
isAuthenticated: true,
},
[helpReducerType]: {
helpGuides: {},
},
};

function mockFunction() {
const original = jest.requireActual("react-router-dom");
return {
...original,
useParams: jest.fn().mockReturnValue({
tab: "overview",
}),
};
}

jest.mock("react-router-dom", () => mockFunction());

describe("HelpGuide", () => {
it("renders MS properly with default content", async () => {
const helpKey = "Not-Exists";
const { findByTestId } = render(
<BrowserRouter>
<ReduxWrapper initialState={msState}>
<HelpGuide />
<HelpGuideContent helpKey={helpKey} />
</ReduxWrapper>
</BrowserRouter>
);
const helpButton = await findByTestId("help-open");
fireEvent.click(helpButton);

const helpContent = await findByTestId("help-content");

expect(helpContent).toMatchSnapshot();
});
});
Loading

0 comments on commit ad29b1a

Please sign in to comment.