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(ui-editor): enable component poc and feedback form #14210

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const buttonTexts: ButtonTexts = {
};

const heading = 'Heading';
const description = 'Description';

describe('FeedbackForm', () => {
it('should render FeedbackForm', () => {
Expand Down Expand Up @@ -102,8 +103,10 @@ const renderFeedbackForm = ({
render(
<FeedbackFormContext.Provider value={{ answers: {}, setAnswers: setAnswers || jest.fn() }}>
<FeedbackForm
id='test'
buttonTexts={buttonTexts}
heading={heading}
description={description}
questions={questions}
position={position || 'inline'}
onSubmit={jest.fn()}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useRef } from 'react';
import { StudioButton, StudioModal } from '@studio/components';
import { StudioButton, StudioModal, StudioParagraph } from '@studio/components';
import type { ButtonTexts, QuestionConfig, QuestionsProps } from '../types/QuestionsProps';
import { YesNoQuestion } from './Question/YesNoQuestion';
import { useFeedbackFormContext } from '../contexts/FeedbackFormContext';
Expand All @@ -9,17 +9,21 @@ import { getDefaultAnswerValueForQuestion } from '../utils/questionUtils';
import type { AnswerType } from '../types/AnswerType';

type FeedbackFormProps = {
id: string;
buttonTexts: ButtonTexts;
heading: string;
description: string;
questions: QuestionConfig[];
position?: 'inline' | 'fixed';
onSubmit: (answers: Record<string, AnswerType>) => void;
};

export function FeedbackForm({
id,
questions,
buttonTexts,
heading,
description,
position = 'inline',
onSubmit,
}: FeedbackFormProps): React.ReactElement {
Expand All @@ -42,7 +46,10 @@ export function FeedbackForm({
};

const handleSubmit = () => {
onSubmit(answers);
onSubmit({
...answers,
feedbackFormId: id,
});
handleCloseModal();
};

Expand Down Expand Up @@ -77,6 +84,9 @@ export function FeedbackForm({
closeButtonTitle={buttonTexts.close}
ref={modalRef}
>
wrt95 marked this conversation as resolved.
Show resolved Hide resolved
<StudioParagraph size='sm' spacing={true}>
{description}
</StudioParagraph>
{questions.map((question) => {
return renderQuestion(question);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<StudioTextfield
<StudioTextarea
size='sm'
id={id}
label={label}
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) => debouncedOnChange(e.target.value)}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => debouncedOnChange(e.target.value)}
/>
);
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -26,7 +26,7 @@ export function YesNoQuestion({ id, label, value, buttonLabels, onChange }: YesN

return (
<div>
<StudioParagraph>{label}</StudioParagraph>
<StudioLabelAsParagraph size='sm'>{label}</StudioLabelAsParagraph>
<div className={classes.buttons}>
<StudioButton
variant='tertiary'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ describe('FeedbackFormImpl', () => {
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,
});

Expand All @@ -25,13 +27,15 @@ 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',
trigger: 'Give feedback',
close: 'Close',
},
heading: 'Give feedback - heading',
description: 'Description',
questions: mockQuestions,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,41 @@ 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 questions: QuestionConfig[];
private readonly position: 'inline' | 'fixed' = 'inline';
private readonly onSubmit: (answers: Record<string, any>) => void;

constructor(config: {
id: string;
buttonTexts: ButtonTexts;
heading: string;
description: string;
questions: QuestionConfig[];
position?: 'inline' | 'fixed';
onSubmit: (answers: Record<string, AnswerType>) => void;
}) {
this.id = config.id;
this.buttonTexts = config.buttonTexts;
this.heading = config.heading;
this.description = config.description;
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 (
<FeedbackFormContextProvider>
<FeedbackForm
id={this.id}
buttonTexts={this.buttonTexts}
heading={this.heading}
description={this.description}
questions={this.questions}
position={this.position}
onSubmit={this.onSubmit}
Expand Down
3 changes: 3 additions & 0 deletions frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const dataModelAddXsdFromRepoPath = (org, app, filePath) => `${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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.componentButtonInline {
height: 80px;
width: 140px;
margin: 8px;
height: 60px;
width: 120px;
margin: 4px;
padding: 6px;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function ComponentButton({
<StudioButton
variant={selected ? 'primary' : 'secondary'}
onClick={onClick}
size='sm'
size='xs'
aria-label={tooltipContent}
className={inline ? classes.componentButtonInline : classes.componentButton}
title={tooltipContent}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { getByText, render, screen, waitFor } from '@testing-library/react';

Check failure on line 2 in frontend/packages/ux-editor/src/containers/DesignView/AddItem/FeedbackForm.test.tsx

View workflow job for this annotation

GitHub Actions / Typechecking and linting

'getByText' is declared but its value is never read.
Fixed Show fixed Hide fixed
nkylstad marked this conversation as resolved.
Show resolved Hide resolved
import userEvent from '@testing-library/user-event';
import { FeedbackForm } from './FeedbackForm';

describe('FeedbackForm', () => {
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();
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();
});

it('should call axios post when clicking send', async () => {
const user = userEvent.setup();
const postMock = jest.fn().mockImplementation(() => Promise.resolve({}));
jest.mock('app-shared/utils/networking', () => ({
post: postMock,
}));
renderFeedbackForm();
await user.click(screen.getByRole('button', { name: 'Gi tilbakemelding' }));
await user.click(screen.getByRole('button', { name: 'Send' }));
waitFor(() => expect(postMock).toHaveBeenCalledTimes(1));
});

it('should show success toast after clicking send', async () => {
const user = userEvent.setup();
const postMock = jest.fn().mockImplementation(() => Promise.resolve({}));
jest.mock('app-shared/utils/networking', () => ({
post: postMock,
}));
renderFeedbackForm();
await user.click(screen.getByRole('button', { name: 'Gi tilbakemelding' }));
await user.click(screen.getByRole('button', { name: 'Send' }));
waitFor(() => expect(screen.getByText('Takk for tilbakemeldingen!')).toBeInTheDocument());
});
});

const renderFeedbackForm = () => {
return render(<FeedbackForm />);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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<string, string>) => {
wrt95 marked this conversation as resolved.
Show resolved Hide resolved
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!');
wrt95 marked this conversation as resolved.
Show resolved Hide resolved
nkylstad marked this conversation as resolved.
Show resolved Hide resolved
} 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! Vi ser du tester et nytt design for å legge til komponenter og vil gjerne høre hva du synes!',
nkylstad marked this conversation as resolved.
Show resolved Hide resolved
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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.switchWrapper {
display: flex;
flex-direction: row;
gap: var(--fds-spacing-2);
}
Original file line number Diff line number Diff line change
@@ -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(<ToggleAddComponentPoc />);
expect(screen.getByText('Prøv nytt design')).toBeInTheDocument();
});
});
Loading
Loading