From 3ab90869028ce162cfca44445bea46cadf21b7bb Mon Sep 17 00:00:00 2001 From: Nina Kylstad Date: Fri, 6 Dec 2024 09:32:15 +0100 Subject: [PATCH] feat(ui-editor): enable component poc and feedback form (#14210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gørild Døhl --- .../components/News/NewsContent/news.nb.json | 5 ++ .../src/FeedbackForm/FeedbackForm.module.css | 4 ++ .../src/FeedbackForm/FeedbackForm.test.tsx | 3 + .../src/FeedbackForm/FeedbackForm.tsx | 21 ++++++- .../FeedbackForm/Question/TextQuestion.tsx | 7 ++- .../FeedbackForm/Question/YesNoQuestion.tsx | 4 +- .../src/config/FeedbackFormImpl.test.tsx | 4 ++ .../src/config/FeedbackFormImpl.tsx | 14 ++++- frontend/packages/shared/src/api/paths.js | 3 + .../AddItem/AddItemModal.module.css | 6 +- .../AddItem/ComponentButton.module.css | 10 +-- .../DesignView/AddItem/ComponentButton.tsx | 2 +- .../DesignView/AddItem/FeedbackForm.test.tsx | 41 ++++++++++++ .../DesignView/AddItem/FeedbackForm.tsx | 63 +++++++++++++++++++ .../AddItem/ToggleAddComponentPoc.module.css | 5 ++ .../AddItem/ToggleAddComponentPoc.test.tsx | 10 +++ .../AddItem/ToggleAddComponentPoc.tsx | 57 +++++++++++++++++ .../src/containers/FormDesignerToolbar.tsx | 9 ++- .../ux-editor/src/data/formItemConfig.ts | 2 +- 19 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 frontend/packages/ux-editor/src/containers/DesignView/AddItem/FeedbackForm.test.tsx create mode 100644 frontend/packages/ux-editor/src/containers/DesignView/AddItem/FeedbackForm.tsx create mode 100644 frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.module.css create mode 100644 frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.test.tsx create mode 100644 frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.tsx diff --git a/frontend/app-development/features/overview/components/News/NewsContent/news.nb.json b/frontend/app-development/features/overview/components/News/NewsContent/news.nb.json index 63ee640e5f5..01b971e82fd 100644 --- a/frontend/app-development/features/overview/components/News/NewsContent/news.nb.json +++ b/frontend/app-development/features/overview/components/News/NewsContent/news.nb.json @@ -1,6 +1,11 @@ { "$schema": "news.schema.json", "news": [ + { + "date": "2024-11-29", + "title": "Prøv nytt design på Utforming!", + "content": "Vi tester en ny måte å legge til komponenter på Utforming-siden. Du kan velge det nye designet øverst til høyre på Utforming-siden og teste så mye du vil! Etterpå kan du gi tilbakemelding på hva du synes, ved å velge Gi tilbakemelding nederst til høyre." + }, { "date": "2024-10-25", "title": "Prosess har blitt voksen!", diff --git a/frontend/libs/studio-feedback-form/src/FeedbackForm/FeedbackForm.module.css b/frontend/libs/studio-feedback-form/src/FeedbackForm/FeedbackForm.module.css index 9dd716795a7..6b5dd06619d 100644 --- a/frontend/libs/studio-feedback-form/src/FeedbackForm/FeedbackForm.module.css +++ b/frontend/libs/studio-feedback-form/src/FeedbackForm/FeedbackForm.module.css @@ -5,6 +5,10 @@ z-index: 99999999; /* Make sure this is on top of everything */ } +.disclaimer { + margin-top: var(--fds-spacing-2); +} + .submit { margin-top: var(--fds-spacing-2); } diff --git a/frontend/libs/studio-feedback-form/src/FeedbackForm/FeedbackForm.test.tsx b/frontend/libs/studio-feedback-form/src/FeedbackForm/FeedbackForm.test.tsx index 4cd6ac174d6..b54757b4773 100644 --- a/frontend/libs/studio-feedback-form/src/FeedbackForm/FeedbackForm.test.tsx +++ b/frontend/libs/studio-feedback-form/src/FeedbackForm/FeedbackForm.test.tsx @@ -13,6 +13,7 @@ const buttonTexts: ButtonTexts = { }; const heading = 'Heading'; +const description = 'Description'; describe('FeedbackForm', () => { it('should render FeedbackForm', () => { @@ -102,8 +103,10 @@ const renderFeedbackForm = ({ render( ) => void; }; export function FeedbackForm({ + id, questions, buttonTexts, heading, + description, + disclaimer, position = 'inline', onSubmit, }: FeedbackFormProps): React.ReactElement { @@ -42,7 +48,10 @@ export function FeedbackForm({ }; const handleSubmit = () => { - onSubmit(answers); + onSubmit({ + ...answers, + feedbackFormId: id, + }); handleCloseModal(); }; @@ -77,9 +86,17 @@ export function FeedbackForm({ closeButtonTitle={buttonTexts.close} ref={modalRef} > + + {description} + {questions.map((question) => { return renderQuestion(question); })} + {disclaimer && ( + + {disclaimer} + + )} {buttonTexts.submit} diff --git a/frontend/libs/studio-feedback-form/src/FeedbackForm/Question/TextQuestion.tsx b/frontend/libs/studio-feedback-form/src/FeedbackForm/Question/TextQuestion.tsx index 40d2e7cf4ed..8770e5bce79 100644 --- a/frontend/libs/studio-feedback-form/src/FeedbackForm/Question/TextQuestion.tsx +++ b/frontend/libs/studio-feedback-form/src/FeedbackForm/Question/TextQuestion.tsx @@ -1,17 +1,18 @@ import React, { type ChangeEvent } from 'react'; import type { QuestionsProps } from '../../types/QuestionsProps'; -import { StudioTextfield } from '@studio/components'; +import { StudioTextarea } from '@studio/components'; import { useDebounce } from '@studio/hooks'; export function TextQuestion({ id, label, value, onChange }: QuestionsProps): React.ReactElement { const { debounce } = useDebounce({ debounceTimeInMs: 500 }); const debouncedOnChange = (newValue: string) => debounce(() => onChange(id, newValue)); return ( - ) => debouncedOnChange(e.target.value)} + onChange={(e: ChangeEvent) => debouncedOnChange(e.target.value)} /> ); } diff --git a/frontend/libs/studio-feedback-form/src/FeedbackForm/Question/YesNoQuestion.tsx b/frontend/libs/studio-feedback-form/src/FeedbackForm/Question/YesNoQuestion.tsx index 5b9a38ab6f5..d9941348115 100644 --- a/frontend/libs/studio-feedback-form/src/FeedbackForm/Question/YesNoQuestion.tsx +++ b/frontend/libs/studio-feedback-form/src/FeedbackForm/Question/YesNoQuestion.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StudioButton, StudioParagraph } from '@studio/components'; +import { StudioButton, StudioLabelAsParagraph } from '@studio/components'; import { ThumbDownFillIcon, ThumbDownIcon, ThumbUpFillIcon, ThumbUpIcon } from '@studio/icons'; import type { QuestionsProps } from '../../types/QuestionsProps'; import classes from './YesNoQuestion.module.css'; @@ -26,7 +26,7 @@ export function YesNoQuestion({ id, label, value, buttonLabels, onChange }: YesN return (
- {label} + {label}
{ it('should render FeedbackFormImpl', () => { const feedbackForm = new FeedbackFormImpl({ onSubmit: jest.fn(), + id: 'test', buttonTexts: { submit: 'Submit', trigger: 'Give feedback', close: 'Close', }, heading: 'Give feedback - heading', + description: 'Description', questions: mockQuestions, }); @@ -25,6 +27,7 @@ describe('FeedbackFormImpl', () => { it('should open form modal when trigger button is clicked', async () => { const user = userEvent.setup(); const feedbackForm = new FeedbackFormImpl({ + id: 'test', onSubmit: jest.fn(), buttonTexts: { submit: 'Submit', @@ -32,6 +35,7 @@ describe('FeedbackFormImpl', () => { close: 'Close', }, heading: 'Give feedback - heading', + description: 'Description', questions: mockQuestions, }); diff --git a/frontend/libs/studio-feedback-form/src/config/FeedbackFormImpl.tsx b/frontend/libs/studio-feedback-form/src/config/FeedbackFormImpl.tsx index 47d2da25fc5..e011459ee14 100644 --- a/frontend/libs/studio-feedback-form/src/config/FeedbackFormImpl.tsx +++ b/frontend/libs/studio-feedback-form/src/config/FeedbackFormImpl.tsx @@ -5,33 +5,45 @@ import { FeedbackForm } from '../FeedbackForm/FeedbackForm'; import type { AnswerType } from '../types/AnswerType'; export class FeedbackFormImpl { + private readonly id: string; private readonly buttonTexts: ButtonTexts; private readonly heading: string; + private readonly description: string; + private readonly disclaimer?: string; private readonly questions: QuestionConfig[]; private readonly position: 'inline' | 'fixed' = 'inline'; private readonly onSubmit: (answers: Record) => void; constructor(config: { + id: string; buttonTexts: ButtonTexts; heading: string; + description: string; + disclaimer?: string; questions: QuestionConfig[]; position?: 'inline' | 'fixed'; onSubmit: (answers: Record) => void; }) { + this.id = config.id; this.buttonTexts = config.buttonTexts; this.heading = config.heading; + this.description = config.description; + this.disclaimer = config.disclaimer; this.questions = config.questions; this.getFeedbackForm = this.getFeedbackForm.bind(this); this.position = config.position || 'inline'; this.onSubmit = config.onSubmit; } - public getFeedbackForm(): React.ReactNode { + public getFeedbackForm(): React.ReactElement { return ( `${basePath}/ // Deployment // See frontend/app-development/utils/urlHelper.ts Deployments +// Feedback form +export const submitFeedbackPath = (org, app) => `${basePath}/${org}/${app}/feedbackform/submit`; // Post + // FormEditor export const ruleHandlerPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/rule-handler?${s({ layoutSetName })}`; // Get, Post export const widgetSettingsPath = (org, app) => `${basePath}/${org}/${app}/app-development/widget-settings`; // Get diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/AddItemModal.module.css b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/AddItemModal.module.css index c714ea08127..2b74175bca0 100644 --- a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/AddItemModal.module.css +++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/AddItemModal.module.css @@ -1,6 +1,6 @@ .componentButtonInline { - height: 80px; - width: 140px; - margin: 8px; + height: 60px; + width: 120px; + margin: 4px; padding: 6px; } diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ComponentButton.module.css b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ComponentButton.module.css index b3306811c15..b3a83174dcd 100644 --- a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ComponentButton.module.css +++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ComponentButton.module.css @@ -1,14 +1,14 @@ .componentButton { margin: 4px; - width: 140px; - height: 100px; + width: 130px; + height: 80px; padding: 2px; overflow: wrap; } .componentButtonInline { - height: 80px; - width: 140px; - margin: 8px; + height: 60px; + width: 120px; + margin: 4px; padding: 2px; } diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ComponentButton.tsx b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ComponentButton.tsx index d9904f77e44..eec01d9c00c 100644 --- a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ComponentButton.tsx +++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ComponentButton.tsx @@ -20,7 +20,7 @@ export function ComponentButton({ ; + +describe('FeedbackForm', () => { + afterAll(() => { + jest.clearAllMocks(); + }); + + it('should render feedback form', () => { + renderFeedbackForm(); + expect(screen.getByRole('button', { name: 'Gi tilbakemelding' })).toBeInTheDocument(); + }); + + it('should open the feedback form when clicking trigger', async () => { + const user = userEvent.setup(); + renderFeedbackForm(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: 'Gi tilbakemelding' })); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + }); + + it('should close the feedback form when clicking send', async () => { + const user = userEvent.setup(); + mockedAxios.post.mockResolvedValueOnce({}); + renderFeedbackForm(); + await user.click(screen.getByRole('button', { name: 'Gi tilbakemelding' })); + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + await user.click(screen.getByRole('button', { name: 'Send' })); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); +}); + +const renderFeedbackForm = () => { + return render(); +}; diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/FeedbackForm.tsx b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/FeedbackForm.tsx new file mode 100644 index 00000000000..db5b2acec86 --- /dev/null +++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/FeedbackForm.tsx @@ -0,0 +1,63 @@ +import type { ReactElement } from 'react'; +import { submitFeedbackPath } from 'app-shared/api/paths'; +import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; +import { post } from 'app-shared/utils/networking'; +import { FeedbackFormImpl } from '@studio/feedback-form'; +import { toast } from 'react-toastify'; + +/** + * This is a feedback form to gather feedback on the new design for adding components. + * It uses the FeedbackForm component from the @studio/feedback-form package. + * The form is temporary, and will be removed once the new design is fully tested, or we decide to go in a different direction. + * As such, all texts are hardcoded in Norwegian, to avoid adding unnecessary translations. + * @returns The FeedbackForm component. + */ +export function FeedbackForm(): ReactElement { + const { org, app } = useStudioEnvironmentParams(); + + const submitFeedback = async (answers: Record) => { + try { + // Using regular axios post rather than a mutation hook, since we are not storing + // the feedback in the cache, nor are we updating any state. + await post(submitFeedbackPath(org, app), { answers: { ...answers } }); + toast.success('Takk for tilbakemeldingen!'); + } catch (error) { + console.error('Failed to submit feedback', error); + toast.error('Noe gikk galt. Prøv igjen senere.'); + } + }; + + const feedbackForm = new FeedbackFormImpl({ + id: 'add-component-poc-feedback', + onSubmit: submitFeedback, + buttonTexts: { + submit: 'Send', + trigger: 'Gi tilbakemelding', + close: 'Lukk', + }, + heading: 'Gi tilbakemelding', + description: + 'Hei! Så bra at du har testet det nye designet for å legge til komponenter! Vi vil gjerne høre hva du synes.', + disclaimer: + 'Merk at KI-verktøy kan bli brukt til å analysere svarene. Påse at du ikke inkluderer personopplysninger i dine svar.', + position: 'fixed', + questions: [ + { + id: 'bedreJaNei', + type: 'yesNo', + questionText: 'Likte du dette designet bedre?', + buttonLabels: { + yes: 'Ja', + no: 'Nei', + }, + }, + { + id: 'kommentar', + type: 'text', + questionText: 'Har du kommentarer eller forslag til forbedringer?', + }, + ], + }); + + return feedbackForm.getFeedbackForm(); +} diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.module.css b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.module.css new file mode 100644 index 00000000000..ba386190aa9 --- /dev/null +++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.module.css @@ -0,0 +1,5 @@ +.switchWrapper { + display: flex; + flex-direction: row; + gap: var(--fds-spacing-2); +} diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.test.tsx b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.test.tsx new file mode 100644 index 00000000000..fb9658b84a4 --- /dev/null +++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.test.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ToggleAddComponentPoc } from './ToggleAddComponentPoc'; + +describe('ToggleAddComponentPoc', () => { + it('should render the component', () => { + render(); + expect(screen.getByText('Prøv nytt design')).toBeInTheDocument(); + }); +}); diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.tsx b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.tsx new file mode 100644 index 00000000000..d2d333aa944 --- /dev/null +++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItem/ToggleAddComponentPoc.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { FeedbackForm } from './FeedbackForm'; +import { StudioParagraph, StudioSwitch } from '@studio/components'; +import { + addFeatureFlagToLocalStorage, + removeFeatureFlagFromLocalStorage, + shouldDisplayFeature, +} from 'app-shared/utils/featureToggleUtils'; +import { HelpText } from '@digdir/designsystemet-react'; +import classes from './ToggleAddComponentPoc.module.css'; + +/** + * Component that toggles the AddComponentModal POC and + * displays the feedback form if the feature flag is enabled + * NOTE: Since this is a poc, and the switch at some point will be + * removed, all texts are explicit in this file, rather than being + * fetched from the translation files. + * @returns The ToggleAddComponentPoc component + */ +export function ToggleAddComponentPoc(): React.ReactElement { + const toggleComponentModalPocAndReload = () => { + if (shouldDisplayFeature('addComponentModal')) { + removeFeatureFlagFromLocalStorage('addComponentModal'); + } else { + addFeatureFlagToLocalStorage('addComponentModal'); + } + window.location.reload(); + }; + return ( + <> +
+ + Prøv nytt design + + + + Vi jobber med brukeropplevelsen i Studio. Vil du prøve vårt forslag til nytt design for + å legge til komponenter? + + + Du kan fortelle oss hva du synes om det nye designet ved å trykke på "Gi + tilbakemelding" nederst til høyre på siden. + + +
+ {shouldDisplayFeature('addComponentModal') && } + + ); +} diff --git a/frontend/packages/ux-editor/src/containers/FormDesignerToolbar.tsx b/frontend/packages/ux-editor/src/containers/FormDesignerToolbar.tsx index 26a38a78a44..370773d5776 100644 --- a/frontend/packages/ux-editor/src/containers/FormDesignerToolbar.tsx +++ b/frontend/packages/ux-editor/src/containers/FormDesignerToolbar.tsx @@ -3,11 +3,18 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen import classes from './FormDesignerToolbar.module.css'; import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery'; import { LayoutSetsContainer } from '../components/Elements/LayoutSetsContainer'; +import { ToggleAddComponentPoc } from './DesignView/AddItem/ToggleAddComponentPoc'; export const FormDesignerToolbar = () => { const { org, app } = useStudioEnvironmentParams(); const layoutSetsQuery = useLayoutSetsQuery(org, app); const layoutSetNames = layoutSetsQuery?.data?.sets; - return
{layoutSetNames && }
; + return ( +
+ {layoutSetNames && } + {/* POC of new design for adding components*/} + +
+ ); }; diff --git a/frontend/packages/ux-editor/src/data/formItemConfig.ts b/frontend/packages/ux-editor/src/data/formItemConfig.ts index 878d3a335a4..a8e7c62e615 100644 --- a/frontend/packages/ux-editor/src/data/formItemConfig.ts +++ b/frontend/packages/ux-editor/src/data/formItemConfig.ts @@ -578,6 +578,7 @@ export const defaultComponents: ComponentType[] = [ export const allComponents: KeyValuePairs = { form: [ComponentType.Input, ComponentType.TextArea, ComponentType.Datepicker], + text: [ComponentType.Header, ComponentType.Paragraph, ComponentType.Panel, ComponentType.Alert], select: [ ComponentType.Checkboxes, ComponentType.RadioButtons, @@ -585,7 +586,6 @@ export const allComponents: KeyValuePairs = { ComponentType.MultipleSelect, ComponentType.Likert, ], - text: [ComponentType.Header, ComponentType.Paragraph, ComponentType.Panel, ComponentType.Alert], info: [ ComponentType.InstanceInformation, ComponentType.Image,