From f2f51013a2705d4caf7c3f605136dd61b748a095 Mon Sep 17 00:00:00 2001 From: andreastanderen <71079896+standeren@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:45:58 +0100 Subject: [PATCH] feat: Implement modal to create new code list (#14019) Co-authored-by: Tomas Engebretsen --- .../AppContentLibrary.test.tsx | 78 ++++++++-- .../appContentLibrary/AppContentLibrary.tsx | 14 +- .../convertOptionListsToCodeLists.test.ts | 5 +- frontend/language/src/nb.json | 3 + .../LibraryBody/pages/CodeList/CodeList.tsx | 5 +- .../CodeListsActionsBar.test.tsx | 4 +- .../CodeListsActionsBar.tsx | 14 +- .../CreateNewCodeListModal.module.css | 20 +++ .../CreateNewCodeListModal.test.tsx | 141 ++++++++++++++++++ .../CreateNewCodeListModal.tsx | 120 +++++++++++++++ .../CodeList/hooks/useCodeListEditorTexts.ts | 26 ++++ .../shared/src/hooks/mutations/index.ts | 1 + 12 files changed, 403 insertions(+), 28 deletions(-) create mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CreateNewCodeListModal/CreateNewCodeListModal.module.css create mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CreateNewCodeListModal/CreateNewCodeListModal.test.tsx create mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CreateNewCodeListModal/CreateNewCodeListModal.tsx create mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/hooks/useCodeListEditorTexts.ts diff --git a/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx b/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx index c74abe8306a..42c6c0cdaaf 100644 --- a/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx +++ b/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx @@ -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 = { +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) => ( +
+ + +
+ ), + }), +); + +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(); @@ -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 = {}) => { +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); diff --git a/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx b/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx index 4a4fdec7096..13746a6339f 100644 --- a/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx +++ b/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx @@ -1,3 +1,4 @@ +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'; @@ -5,7 +6,7 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen 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(); @@ -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 ; 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, }, }, diff --git a/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.test.ts b/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.test.ts index a5ad93b4638..dbb6c5262db 100644 --- a/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.test.ts +++ b/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.test.ts @@ -1,3 +1,4 @@ +import type { CodeListWithMetadata } from '@studio/content-library'; import { convertOptionListsToCodeLists } from './convertOptionListsToCodeLists'; import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; @@ -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', @@ -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([]); }); }); diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index ca5dadd6058..926e873bd1d 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -16,12 +16,15 @@ "app_content_library.code_lists.code_lists_count_info_plural": "Det finnes {{codeListsCount}} kodelister i biblioteket.", "app_content_library.code_lists.code_lists_count_info_single": "Det finnes 1 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.", diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeList.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeList.tsx index ad087121a02..3838df10433 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeList.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeList.tsx @@ -33,7 +33,10 @@ export function CodeList({
{t('app_content_library.code_lists.page_name')} - +
); diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CodeListsActionsBar.test.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CodeListsActionsBar.test.tsx index 3fc8eee69b2..d4b9b2643c7 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CodeListsActionsBar.test.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CodeListsActionsBar.test.tsx @@ -37,5 +37,7 @@ describe('CodeListsActionsBar', () => { }); const renderCodeListsActionsBar = () => { - render(); + render( + , + ); }; diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CodeListsActionsBar.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CodeListsActionsBar.tsx index 6aa289b12b2..6488e028bcb 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CodeListsActionsBar.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CodeListsActionsBar.tsx @@ -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 (
@@ -17,9 +23,7 @@ export function CodeListsActionsBar({ onUploadCodeList }: CodeListsActionsBarPro size='sm' placeholder={t('app_content_library.code_lists.search_placeholder')} /> - - {t('app_content_library.code_lists.create_new_code_list')} - + { + afterEach(jest.clearAllMocks); + + it('open dialog when clicking "create new code list" button', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + it('renders an empty textfield for inputting code list name', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + const codeListNameInput = screen.getByRole('textbox', { + name: textMock('app_content_library.code_lists.create_new_code_list_name'), + }); + expect(codeListNameInput).toHaveTextContent(''); + }); + + it('renders the code list editor without content', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + const codeListIsEmptyMessage = screen.getByText(textMock('code_list_editor.empty')); + expect(codeListIsEmptyMessage).toBeInTheDocument(); + }); + + it('renders a disabled button by default', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + const saveCodeListButton = screen.getByRole('button', { + name: textMock('app_content_library.code_lists.save_new_code_list'), + }); + expect(saveCodeListButton).toBeDisabled(); + }); + + it('enables the save button if only title is provided', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + await inputCodeListTitle(user); + const saveCodeListButton = screen.getByRole('button', { + name: textMock('app_content_library.code_lists.save_new_code_list'), + }); + expect(saveCodeListButton).toBeEnabled(); + }); + + it('keeps disabling the save button if only code list content is provided', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + await addCodeListItem(user); + const saveCodeListButton = screen.getByRole('button', { + name: textMock('app_content_library.code_lists.save_new_code_list'), + }); + expect(saveCodeListButton).toBeDisabled(); + }); + + it('disables the save button if code list content is invalid', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + await addDuplicatedCodeListValues(user); + const saveCodeListButton = screen.getByRole('button', { + name: textMock('app_content_library.code_lists.save_new_code_list'), + }); + expect(saveCodeListButton).toBeDisabled(); + }); + + it('enables the save button when title and valid code list content are provided', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + await inputCodeListTitle(user); + await addCodeListItem(user); + const saveCodeListButton = screen.getByRole('button', { + name: textMock('app_content_library.code_lists.save_new_code_list'), + }); + expect(saveCodeListButton).toBeEnabled(); + }); + + it('calls onUpdateCodeList and closes modal when save button is clicked', async () => { + const user = userEvent.setup(); + renderCreateNewCodeListModal(); + await openDialog(user); + await inputCodeListTitle(user); + await addCodeListItem(user); + const saveCodeListButton = screen.getByRole('button', { + name: textMock('app_content_library.code_lists.save_new_code_list'), + }); + await user.click(saveCodeListButton); + expect(onUpdateCodeListMock).toHaveBeenCalledTimes(1); + expect(onUpdateCodeListMock).toHaveBeenCalledWith({ + codeList: [{ label: '', value: '' }], + title: newCodeListTitleMock, + }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); +}); + +const openDialog = async (user: UserEvent) => { + const createNewButton = screen.getByRole('button', { + name: textMock('app_content_library.code_lists.create_new_code_list'), + }); + await user.click(createNewButton); +}; + +const inputCodeListTitle = async (user: UserEvent) => { + const codeListNameInput = screen.getByRole('textbox', { + name: textMock('app_content_library.code_lists.create_new_code_list_name'), + }); + await user.type(codeListNameInput, newCodeListTitleMock); +}; + +const addCodeListItem = async (user: UserEvent) => { + const addCodeListItemButton = screen.getByRole('button', { + name: textMock('code_list_editor.add_option'), + }); + await user.click(addCodeListItemButton); +}; + +const addDuplicatedCodeListValues = async (user: UserEvent) => { + await addCodeListItem(user); + await addCodeListItem(user); +}; + +const renderCreateNewCodeListModal = () => { + render(); +}; diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CreateNewCodeListModal/CreateNewCodeListModal.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CreateNewCodeListModal/CreateNewCodeListModal.tsx new file mode 100644 index 00000000000..579f5a066d2 --- /dev/null +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeListsActionsBar/CreateNewCodeListModal/CreateNewCodeListModal.tsx @@ -0,0 +1,120 @@ +import React, { createRef, useState } from 'react'; +import { + StudioButton, + StudioCodeListEditor, + StudioModal, + StudioTextfield, +} from '@studio/components'; +import type { CodeList, CodeListEditorTexts } from '@studio/components'; +import { useTranslation } from 'react-i18next'; +import { useOptionListEditorTexts } from '../../hooks/useCodeListEditorTexts'; +import { CheckmarkIcon } from '@studio/icons'; +import classes from './CreateNewCodeListModal.module.css'; +import type { CodeListWithMetadata } from '../../CodeList'; + +type CreateNewCodeListModalProps = { + onUpdateCodeList: (codeListWithMetadata: CodeListWithMetadata) => void; +}; + +export function CreateNewCodeListModal({ onUpdateCodeList }: CreateNewCodeListModalProps) { + const { t } = useTranslation(); + const modalRef = createRef(); + + const newCodeList: CodeList = []; + + const handleCloseModal = () => { + modalRef.current?.close(); + }; + + return ( + + + {t('app_content_library.code_lists.create_new_code_list')} + + + + + + ); +} + +type CreateNewCodeListProps = { + codeList: CodeList; + onUpdateCodeList: (codeListWithMetadata: CodeListWithMetadata) => void; + onCloseModal: () => void; +}; + +function CreateNewCodeList({ codeList, onUpdateCodeList, onCloseModal }: CreateNewCodeListProps) { + const { t } = useTranslation(); + const editorTexts: CodeListEditorTexts = useOptionListEditorTexts(); + const [isCodeListValid, setIsCodeListValid] = useState(true); + const [currentCodeListWithMetadata, setCurrentCodeListWithMetadata] = + useState({ + title: '', + codeList, + }); + + const handleSaveCodeList = () => { + onUpdateCodeList(currentCodeListWithMetadata); + onCloseModal(); + }; + + const handleCodeListTitleChange = (codeListTitle: string) => { + setCurrentCodeListWithMetadata({ + title: codeListTitle, + codeList: currentCodeListWithMetadata.codeList, + }); + }; + + const handleCodeListChange = (updatedCodeList: CodeList) => { + setIsCodeListValid(true); + setCurrentCodeListWithMetadata({ + title: currentCodeListWithMetadata.title, + codeList: updatedCodeList, + }); + }; + + const handleInvalidCodeList = () => { + setIsCodeListValid(false); + }; + + const isSaveButtonDisabled = !isCodeListValid || !currentCodeListWithMetadata.title; + + return ( +
+ handleCodeListTitleChange(event.target.value)} + /> +
+ +
+ } + onClick={handleSaveCodeList} + variant='secondary' + disabled={isSaveButtonDisabled} + > + {t('app_content_library.code_lists.save_new_code_list')} + +
+ ); +} diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/hooks/useCodeListEditorTexts.ts b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/hooks/useCodeListEditorTexts.ts new file mode 100644 index 00000000000..c56f5f583aa --- /dev/null +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/hooks/useCodeListEditorTexts.ts @@ -0,0 +1,26 @@ +import { useTranslation } from 'react-i18next'; +import type { CodeListEditorTexts } from '@studio/components'; + +export const useOptionListEditorTexts = (): CodeListEditorTexts => { + const { t } = useTranslation(); + + return { + add: t('code_list_editor.add_option'), + delete: t('code_list_editor.column_title_delete'), + value: t('code_list_editor.column_title_value'), + label: t('code_list_editor.column_title_label'), + description: t('code_list_editor.column_title_description'), + helpText: t('code_list_editor.column_title_help_text'), + deleteItem: (number: number) => t('code_list_editor.delete_code_list_item', { number }), + itemValue: (number: number) => t('code_list_editor.value_item', { number }), + itemLabel: (number: number) => t('code_list_editor.label_item', { number }), + itemDescription: (number: number) => t('code_list_editor.description_item', { number }), + itemHelpText: (number: number) => t('code_list_editor.help_text_item', { number }), + codeList: t('code_list_editor.legend'), + emptyCodeList: t('code_list_editor.empty'), + valueErrors: { + duplicateValue: t('code_list_editor.duplicate_values_error'), + }, + generalError: t('code_list_editor.general_error'), + }; +}; diff --git a/frontend/packages/shared/src/hooks/mutations/index.ts b/frontend/packages/shared/src/hooks/mutations/index.ts index 46063b5e8b2..719790e13c5 100644 --- a/frontend/packages/shared/src/hooks/mutations/index.ts +++ b/frontend/packages/shared/src/hooks/mutations/index.ts @@ -1,4 +1,5 @@ export { useAddOptionListMutation } from './useAddOptionListMutation'; +export { useUpdateOptionListMutation } from './useUpdateOptionListMutation'; export { useUpsertTextResourcesMutation } from './useUpsertTextResourcesMutation'; export { useUpsertTextResourceMutation } from './useUpsertTextResourceMutation'; export { useRepoCommitAndPushMutation } from './useRepoCommitAndPushMutation';