Skip to content

Commit

Permalink
feat: Implement modal to create new code list (#14019)
Browse files Browse the repository at this point in the history
Co-authored-by: Tomas Engebretsen <[email protected]>
  • Loading branch information
standeren and TomasEng authored Nov 22, 2024
1 parent 84df844 commit f2f5101
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,53 @@ import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { QueryKey } from 'app-shared/types/QueryKey';
import { app, org } from '@studio/testing/testids';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import type { UserEvent } from '@testing-library/user-event';
import userEvent from '@testing-library/user-event';
import type { Option } from 'app-shared/types/Option';
import type { OptionsLists } from 'app-shared/types/api/OptionsLists';
import type { CodeList } from '@studio/components';

const optionListsMock: Record<string, Option[]> = {
const uploadCodeListButtonTextMock = 'Upload Code List';
const updateCodeListButtonTextMock = 'Update Code List';
const codeListNameMock = 'codeListNameMock';
const codeListMock: CodeList = [{ value: '', label: '' }];
jest.mock(
'../../../libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList',
() => ({
CodeList: ({ onUpdateCodeList, onUploadCodeList }: any) => (
<div>
<button
onClick={() =>
onUploadCodeList(
new File(['test'], `${codeListNameMock}.json`, { type: 'application/json' }),
)
}
>
{uploadCodeListButtonTextMock}
</button>
<button
onClick={() => onUpdateCodeList({ title: codeListNameMock, codeList: codeListMock })}
>
{updateCodeListButtonTextMock}
</button>
</div>
),
}),
);

const optionListsMock: OptionsLists = {
list1: [{ label: 'label', value: 'value' }],
};

describe('AppContentLibrary', () => {
afterEach(jest.clearAllMocks);

it('renders the AppContentLibrary with codeLists and images resources', () => {
it('renders the AppContentLibrary with codeLists and images resources available in the content menu', () => {
renderAppContentLibrary(optionListsMock);
const libraryTitle = screen.getByRole('heading', {
name: textMock('app_content_library.landing_page.title'),
});
const codeListMenuElement = screen.getByText(
textMock('app_content_library.code_lists.page_name'),
);
const imagesMenuElement = screen.getByText(textMock('app_content_library.images.page_name'));
const codeListMenuElement = getLibraryPageTile('code_lists');
const imagesMenuElement = getLibraryPageTile('images');
expect(libraryTitle).toBeInTheDocument();
expect(codeListMenuElement).toBeInTheDocument();
expect(imagesMenuElement).toBeInTheDocument();
Expand All @@ -40,18 +68,38 @@ describe('AppContentLibrary', () => {
it('calls onUploadOptionList when onUploadCodeList is triggered', async () => {
const user = userEvent.setup();
renderAppContentLibrary(optionListsMock);
const codeListNavTitle = screen.getByText(textMock('app_content_library.code_lists.page_name'));
await user.click(codeListNavTitle);
const uploadCodeListButton = screen.getByLabelText(
textMock('app_content_library.code_lists.upload_code_list'),
);
const file = new File(['test'], 'fileNameMock.json', { type: 'application/json' });
await user.upload(uploadCodeListButton, file);
await goToLibraryPage(user, 'code_lists');
const uploadCodeListButton = screen.getByRole('button', { name: uploadCodeListButtonTextMock });
await user.click(uploadCodeListButton);
expect(queriesMock.uploadOptionList).toHaveBeenCalledTimes(1);
expect(queriesMock.uploadOptionList).toHaveBeenCalledWith(org, app, expect.any(FormData));
});

it('calls onUpdateOptionList when onUpdateCodeList is triggered', async () => {
const user = userEvent.setup();
renderAppContentLibrary(optionListsMock);
await goToLibraryPage(user, 'code_lists');
const updateCodeListButton = screen.getByRole('button', { name: updateCodeListButtonTextMock });
await user.click(updateCodeListButton);
expect(queriesMock.updateOptionList).toHaveBeenCalledTimes(1);
expect(queriesMock.updateOptionList).toHaveBeenCalledWith(
org,
app,
codeListNameMock,
codeListMock,
);
});
});

const renderAppContentLibrary = (optionLists: Record<string, Option[]> = {}) => {
const getLibraryPageTile = (libraryPage: string) =>
screen.getByText(textMock(`app_content_library.${libraryPage}.page_name`));

const goToLibraryPage = async (user: UserEvent, libraryPage: string) => {
const libraryPageNavTile = getLibraryPageTile(libraryPage);
await user.click(libraryPageNavTile);
};

const renderAppContentLibrary = (optionLists: OptionsLists = {}) => {
const queryClientMock = createQueryClientMock();
if (Object.keys(optionLists).length) {
queryClientMock.setQueryData([QueryKey.OptionLists, org, app], optionLists);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { CodeListWithMetadata } from '@studio/content-library';
import { ResourceContentLibraryImpl } from '@studio/content-library';
import React from 'react';
import { useOptionListsQuery } from 'app-shared/hooks/queries/useOptionListsQuery';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { convertOptionListsToCodeLists } from './utils/convertOptionListsToCodeLists';
import { StudioPageSpinner } from '@studio/components';
import { useTranslation } from 'react-i18next';
import { useAddOptionListMutation } from 'app-shared/hooks/mutations/useAddOptionListMutation';
import { useAddOptionListMutation, useUpdateOptionListMutation } from 'app-shared/hooks/mutations';

export function AppContentLibrary(): React.ReactElement {
const { org, app } = useStudioEnvironmentParams();
Expand All @@ -16,23 +17,28 @@ export function AppContentLibrary(): React.ReactElement {
isError: optionListsError,
} = useOptionListsQuery(org, app);
const { mutate: uploadOptionList } = useAddOptionListMutation(org, app);
const { mutate: updateOptionList } = useUpdateOptionListMutation(org, app);

if (optionListsPending)
return <StudioPageSpinner spinnerTitle={t('general.loading')}></StudioPageSpinner>;

const codeLists = convertOptionListsToCodeLists(optionLists);

const onSubmit = (file: File) => {
const handleUpload = (file: File) => {
uploadOptionList(file);
};

const handleUpdate = ({ title, codeList }: CodeListWithMetadata) => {
updateOptionList({ optionListId: title, optionsList: codeList });
};

const { getContentResourceLibrary } = new ResourceContentLibraryImpl({
pages: {
codeList: {
props: {
codeLists: codeLists,
onUpdateCodeList: () => {},
onUploadCodeList: onSubmit,
onUpdateCodeList: handleUpdate,
onUploadCodeList: handleUpload,
fetchDataError: optionListsError,
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CodeListWithMetadata } from '@studio/content-library';
import { convertOptionListsToCodeLists } from './convertOptionListsToCodeLists';
import type { OptionsLists } from 'app-shared/types/api/OptionsLists';

Expand All @@ -13,7 +14,7 @@ describe('convertOptionListsToCodeLists', () => {
{ label: 'Option B', value: 'B' },
],
};
const result = convertOptionListsToCodeLists(optionLists);
const result: CodeListWithMetadata[] = convertOptionListsToCodeLists(optionLists);
expect(result).toEqual([
{
title: 'list1',
Expand All @@ -34,7 +35,7 @@ describe('convertOptionListsToCodeLists', () => {

it('returns an empty array when the input map is empty', () => {
const optionLists: OptionsLists = {};
const result = convertOptionListsToCodeLists(optionLists);
const result: CodeListWithMetadata[] = convertOptionListsToCodeLists(optionLists);
expect(result).toEqual([]);
});
});
3 changes: 3 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
"app_content_library.code_lists.code_lists_count_info_plural": "Det finnes <bold>{{codeListsCount}}</bold> kodelister i biblioteket.",
"app_content_library.code_lists.code_lists_count_info_single": "Det finnes <bold>1</bold> kodeliste i biblioteket.",
"app_content_library.code_lists.create_new_code_list": "Lag en ny kodeliste",
"app_content_library.code_lists.create_new_code_list_modal_title": "Lag ny kodeliste",
"app_content_library.code_lists.create_new_code_list_name": "Navn",
"app_content_library.code_lists.edit_code_list_placeholder_text": "Her kommer det redigeringsmuligheter snart",
"app_content_library.code_lists.fetch_error": "Kunne ikke hente kodelister fra appen.",
"app_content_library.code_lists.info_box.description": "En kodeliste er en liste med strukturerte data. Den inneholder definerte alternativer som alle har en unik kode. For eksempel kan du ha en kodeliste med kommunenavn i skjemaet ditt, som brukerne kan velge fra en nedtrekksmeny. Brukerne ser bare navnet, ikke koden.",
"app_content_library.code_lists.info_box.title": "Hva er en kodeliste?",
"app_content_library.code_lists.no_content": "Dette biblioteket har ingen kodelister",
"app_content_library.code_lists.page_name": "Kodelister",
"app_content_library.code_lists.save_new_code_list": "Lagre",
"app_content_library.code_lists.search_placeholder": "Søk på kodelister",
"app_content_library.code_lists.upload_code_list": "Last opp din egen kodeliste",
"app_content_library.images.info_box.description": "Du kan bruke bildene i biblioteket til å legge inn bilder i skjemaet. Du kan også laste opp et bilde med organisasjonens logo, og legge det som logobilde i innstillingene for appen.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export function CodeList({
<div className={classes.codeListsContainer}>
<StudioHeading size='small'>{t('app_content_library.code_lists.page_name')}</StudioHeading>
<CodeListsCounterMessage codeListsCount={codeLists.length} />
<CodeListsActionsBar onUploadCodeList={onUploadCodeList} />
<CodeListsActionsBar
onUploadCodeList={onUploadCodeList}
onUpdateCodeList={onUpdateCodeList}
/>
<CodeLists codeLists={codeLists} onUpdateCodeList={onUpdateCodeList} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,7 @@ describe('CodeListsActionsBar', () => {
});

const renderCodeListsActionsBar = () => {
render(<CodeListsActionsBar onUploadCodeList={onUploadCodeListMock} />);
render(
<CodeListsActionsBar onUploadCodeList={onUploadCodeListMock} onUpdateCodeList={jest.fn()} />,
);
};
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from 'react';
import { Search } from '@digdir/designsystemet-react';
import { StudioButton, StudioFileUploader } from '@studio/components';
import { StudioFileUploader } from '@studio/components';
import classes from './CodeListsActionsBar.module.css';
import { useTranslation } from 'react-i18next';
import type { CodeListWithMetadata } from '../CodeList';
import { CreateNewCodeListModal } from './CreateNewCodeListModal/CreateNewCodeListModal';

type CodeListsActionsBarProps = {
onUploadCodeList: (updatedCodeList: File) => void;
onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void;
};

export function CodeListsActionsBar({ onUploadCodeList }: CodeListsActionsBarProps) {
export function CodeListsActionsBar({
onUploadCodeList,
onUpdateCodeList,
}: CodeListsActionsBarProps) {
const { t } = useTranslation();
return (
<div className={classes.actionsBar}>
Expand All @@ -17,9 +23,7 @@ export function CodeListsActionsBar({ onUploadCodeList }: CodeListsActionsBarPro
size='sm'
placeholder={t('app_content_library.code_lists.search_placeholder')}
/>
<StudioButton size='small' variant='secondary'>
{t('app_content_library.code_lists.create_new_code_list')}
</StudioButton>
<CreateNewCodeListModal onUpdateCodeList={onUpdateCodeList} />
<StudioFileUploader
accept='.json'
size='small'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.createNewCodeListModal[open] {
max-width: unset;
width: min(80vw, 64rem);
}

.createNewCodeList {
display: flex;
flex-direction: column;
gap: var(--fds-spacing-4);
align-items: flex-start;
height: 30rem;
}

.codeListTitle {
width: 30%;
}

.codeListEditor {
width: 100%;
}
Loading

0 comments on commit f2f5101

Please sign in to comment.