From 12f54e994bff61176e4ca7a49fca46e9b8155b61 Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 11 Feb 2025 15:26:33 +0100 Subject: [PATCH 1/3] fix: Add text resources to create code list dialog --- .../StudioTextResourceInput.tsx | 2 +- .../pages/CodeListPage/CodeListPage.test.tsx | 76 +++++++- .../pages/CodeListPage/CodeListPage.tsx | 31 +++- .../CodeListPage/CodeLists/CodeLists.tsx | 9 +- .../CodeLists/EditCodeList/EditCodeList.tsx | 33 +--- .../CodeLists/EditCodeList/utils.test.ts | 32 ---- .../CodeLists/EditCodeList/utils.ts | 13 -- .../CodeListsActionsBar.tsx | 12 +- .../CreateNewCodeListModal.tsx | 14 +- .../utils/codeListPageUtils.test.ts | 141 -------------- .../pages/CodeListPage/utils/index.ts | 1 + .../pages/CodeListPage/utils/utils.test.ts | 175 ++++++++++++++++++ .../utils/{codeListPageUtils.ts => utils.ts} | 13 ++ .../libs/studio-content-library/src/index.ts | 3 + 14 files changed, 329 insertions(+), 226 deletions(-) delete mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/utils.test.ts delete mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/utils.ts delete mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/codeListPageUtils.test.ts create mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/index.ts create mode 100644 frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/utils.test.ts rename frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/{codeListPageUtils.ts => utils.ts} (70%) diff --git a/frontend/libs/studio-components/src/components/StudioTextResourceInput/StudioTextResourceInput.tsx b/frontend/libs/studio-components/src/components/StudioTextResourceInput/StudioTextResourceInput.tsx index f7d9788c35e..e2840077440 100644 --- a/frontend/libs/studio-components/src/components/StudioTextResourceInput/StudioTextResourceInput.tsx +++ b/frontend/libs/studio-components/src/components/StudioTextResourceInput/StudioTextResourceInput.tsx @@ -27,7 +27,7 @@ type TextResourceInputPropsBase = { currentIdClass?: string; inputClass?: string; onChangeCurrentId: (id: string | null) => void; - onChangeTextResource: (textResource: TextResource) => void; + onChangeTextResource?: (textResource: TextResource) => void; textResources: TextResource[]; texts: TextResourceInputTexts; toggleClass?: string; diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx index 8fe2954d7e3..dc0887e535b 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx @@ -13,6 +13,7 @@ import { } from '../../../../test-data/codeListDataList'; import { ArrayUtils } from '@studio/pure-functions'; import { label1ResourceNb, textResources } from '../../../../test-data/textResources'; +import type { TextResource } from '../../../../types/TextResource'; import type { TextResourceWithLanguage } from '../../../../types/TextResourceWithLanguage'; const onDeleteCodeList = jest.fn(); @@ -182,6 +183,41 @@ describe('CodeListPage', () => { expect(onUpdateTextResource).toHaveBeenCalledTimes(newLabel.length); expect(onUpdateTextResource).toHaveBeenLastCalledWith(expectedObject); }); + + it('Renders with text resources in the input fields of the create dialog when given', async () => { + const user = userEvent.setup(); + + renderCodeListPage({ textResources }); + const dialog = await openCreateDialog(user); + await addCodeListItem(user, dialog); + await openSearchModeForFirstLabel(user, dialog); + await openFirstLabelCombobox(user, dialog); + + expect(getTextResourceOption(label1ResourceNb, dialog)).toBeInTheDocument(); + }); + + it('Calls onUpdateTextResource with the new text resource and the default language when a text resource is changed in the create dialog', async () => { + const user = userEvent.setup(); + const onUpdateTextResource = jest.fn(); + const newLabel = 'Ny ledetekst'; + + renderCodeListPage({ textResources, onUpdateTextResource }); + const dialog = await openCreateDialog(user); + await addCodeListItem(user, dialog); + await openSearchModeForFirstLabel(user, dialog); + await openFirstLabelCombobox(user, dialog); + await user.click(getTextResourceOption(label1ResourceNb, dialog)); + await openEditModeForFirstLabel(user, dialog); + await user.type(getFirstLabelField(dialog), newLabel); + + const expectedLanguage = 'nb'; + const expectedObject: TextResourceWithLanguage = { + language: expectedLanguage, + textResource: { ...label1ResourceNb, value: newLabel }, + }; + expect(onUpdateTextResource).toHaveBeenCalledTimes(newLabel.length); + expect(onUpdateTextResource).toHaveBeenLastCalledWith(expectedObject); + }); }); const uploadCodeList = async (user: UserEvent, fileName: string): Promise => { @@ -198,8 +234,12 @@ const openAndGetFirstLabelField = async ( ): Promise => { await user.click(getCodeListHeading(codeListTitle)); const accordion = getCodeListAccordion(codeListTitle); + return getFirstLabelField(accordion); +}; + +const getFirstLabelField = (area: HTMLElement): HTMLElement => { const labelFieldLabel = textMock('code_list_editor.text_resource.label.value', { number: 1 }); - return within(accordion).getByRole('textbox', { name: labelFieldLabel }); + return within(area).getByRole('textbox', { name: labelFieldLabel }); }; const getCodeListAccordion = (codeListTitle: string): HTMLElement => @@ -215,3 +255,37 @@ const queryCodeListHeading = (codeListTitle: string): HTMLElement => const renderCodeListPage = (props: Partial = {}): RenderResult => render(); + +const openCreateDialog = async (user: UserEvent): Promise => { + const createButtonLabel = textMock('app_content_library.code_lists.create_new_code_list'); + await user.click(screen.getByRole('button', { name: createButtonLabel })); + return screen.getByRole('dialog'); +}; + +const addCodeListItem = async (user: UserEvent, area: HTMLElement): Promise => { + const addButtonLabel = textMock('code_list_editor.add_option'); + await user.click(within(area).getByRole('button', { name: addButtonLabel })); +}; + +const openSearchModeForFirstLabel = async (user: UserEvent, area: HTMLElement): Promise => { + const radioLabel = textMock('code_list_editor.text_resource.label.search_mode', { number: 1 }); + const radio = within(area).getByRole('radio', { name: radioLabel }); + await user.click(radio); +}; + +const openEditModeForFirstLabel = async (user: UserEvent, area: HTMLElement): Promise => { + const radioLabel = textMock('code_list_editor.text_resource.label.edit_mode', { number: 1 }); + const radio = await within(area).findByRole('radio', { name: radioLabel }); + await user.click(radio); +}; + +const openFirstLabelCombobox = async (user: UserEvent, area: HTMLElement): Promise => { + const comboboxLabel = textMock('code_list_editor.text_resource.label.select', { number: 1 }); + const combobox = within(area).getByRole('combobox', { name: comboboxLabel }); + await user.click(combobox); +}; + +const getTextResourceOption = (textResource: TextResource, area: HTMLElement): HTMLElement => + within(area).getByRole('option', { name: retrieveOptionName(textResource) }); + +const retrieveOptionName = ({ value, id }: TextResource): string => `${value} ${id}`; diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx index f996ddeceee..3629c2c4d67 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx @@ -1,6 +1,6 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import type { CodeList, TextResource } from '@studio/components'; import { StudioHeading } from '@studio/components'; -import type { CodeList } from '@studio/components'; import { useTranslation } from 'react-i18next'; import { CodeListsActionsBar } from './CodeListsActionsBar'; import { CodeLists } from './CodeLists'; @@ -8,7 +8,11 @@ import { CodeListsCounterMessage } from './CodeListsCounterMessage'; import classes from './CodeListPage.module.css'; import { ArrayUtils, FileNameUtils } from '@studio/pure-functions'; import type { CodeListReference } from './types/CodeListReference'; -import { filterCodeLists } from './utils/codeListPageUtils'; +import { + filterCodeLists, + getTextResourcesForLanguage, + createTextResourceWithLanguage, +} from './utils'; import type { TextResourceWithLanguage } from '../../../../types/TextResourceWithLanguage'; import type { TextResources } from '../../../../types/TextResources'; @@ -53,6 +57,19 @@ export function CodeListPage({ [codeListsData, searchString], ); + const textResourcesForLanguage = useMemo( + () => getTextResourcesForLanguage(language, textResources), + [textResources], + ); + + const handleChangeTextResource = useCallback( + (textResource: TextResource) => { + const updatedTextResource = createTextResourceWithLanguage('nb', textResource); + onUpdateTextResource?.(updatedTextResource); + }, + [onUpdateTextResource], + ); + const codeListTitles = ArrayUtils.mapByKey(codeListsData, 'title'); const handleUploadCodeList = (uploadedCodeList: File) => { @@ -70,22 +87,26 @@ export function CodeListPage({ {t('app_content_library.code_lists.page_name')} ); } + +const language: string = 'nb'; // Todo: Let the user choose the language: https://github.com/Altinn/altinn-studio/issues/14572 diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.tsx index a4ddc5ca138..05ad580a165 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/CodeLists.tsx @@ -6,20 +6,19 @@ import { EditCodeList } from './EditCodeList/EditCodeList'; import { Trans, useTranslation } from 'react-i18next'; import type { CodeListIdSource, CodeListReference } from '../types/CodeListReference'; import classes from './CodeLists.module.css'; -import { getCodeListSourcesById, getCodeListUsageCount } from '../utils/codeListPageUtils'; -import type { TextResourceWithLanguage } from '../../../../../types/TextResourceWithLanguage'; -import type { TextResources } from '../../../../../types/TextResources'; +import { getCodeListSourcesById, getCodeListUsageCount } from '../utils'; +import type { TextResource } from '@studio/components'; export type CodeListsProps = { codeListsData: CodeListData[]; + onChangeTextResource?: (textResource: TextResource) => void; onDeleteCodeList: (codeListId: string) => void; onUpdateCodeListId: (codeListId: string, newCodeListId: string) => void; onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void; - onUpdateTextResource?: (textResource: TextResourceWithLanguage) => void; codeListInEditMode: string | undefined; codeListNames: string[]; codeListsUsages: CodeListReference[]; - textResources?: TextResources; + textResources?: TextResource[]; }; export function CodeLists({ diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.tsx index 81f9b2bfd24..7eca46fe8da 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/EditCodeList.tsx @@ -1,4 +1,4 @@ -import type { CodeList, CodeListEditorTexts } from '@studio/components'; +import type { CodeList, CodeListEditorTexts, TextResource } from '@studio/components'; import { StudioDeleteButton, StudioModal, @@ -6,7 +6,7 @@ import { StudioCodeListEditor, StudioToggleableTextfield, } from '@studio/components'; -import React, { useCallback, useMemo } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import type { CodeListWithMetadata } from '../../CodeListPage'; import { useCodeListEditorTexts } from '../../hooks/useCodeListEditorTexts'; @@ -16,43 +16,32 @@ import { useInputCodeListNameErrorMessage } from '../../hooks/useInputCodeListNa import classes from './EditCodeList.module.css'; import type { CodeListIdSource } from '../../types/CodeListReference'; import { CodeListUsages } from './CodeListUsages/CodeListUsages'; -import type { TextResources } from '../../../../../../types/TextResources'; -import type { TextResource } from '../../../../../../types/TextResource'; -import type { TextResourceWithLanguage } from '../../../../../../types/TextResourceWithLanguage'; -import { createTextResourceWithLanguage, getTextResourcesForLanguage } from './utils'; export type EditCodeListProps = { codeList: CodeList; codeListTitle: string; + onChangeTextResource?: (textResource: TextResource) => void; onDeleteCodeList: (codeListId: string) => void; onUpdateCodeListId: (codeListId: string, newCodeListId: string) => void; onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void; - onUpdateTextResource?: (textResource: TextResourceWithLanguage) => void; codeListNames: string[]; codeListSources: CodeListIdSource[]; - textResources?: TextResources; + textResources?: TextResource[]; }; -const language: string = 'nb'; // Todo: Let the user choose the language: https://github.com/Altinn/altinn-studio/issues/14572 - export function EditCodeList({ codeList, codeListTitle, + onChangeTextResource, onDeleteCodeList, onUpdateCodeListId, onUpdateCodeList, - onUpdateTextResource, codeListNames, codeListSources, textResources, }: EditCodeListProps): React.ReactElement { const editorTexts: CodeListEditorTexts = useCodeListEditorTexts(); - const textResourcesForLanguage = useMemo( - () => getTextResourcesForLanguage(language, textResources), - [textResources], - ); - const handleCodeListChange = (updatedCodeList: CodeList): void => { const updatedCodeListWithMetadata = updateCodeListWithMetadata( { title: codeListTitle, codeList: codeList }, @@ -63,14 +52,6 @@ export function EditCodeList({ const handleDeleteCodeList = (): void => onDeleteCodeList(codeListTitle); - const handleChangeTextResource = useCallback( - (textResource: TextResource) => { - const updatedTextResource = createTextResourceWithLanguage(language, textResource); - onUpdateTextResource?.(updatedTextResource); - }, - [onUpdateTextResource], - ); - const codeListHasUsages = codeListSources.length > 0; const isCodeListEditable = codeListSources.length === 0; @@ -86,9 +67,9 @@ export function EditCodeList({ codeList={codeList} onAddOrDeleteItem={handleCodeListChange} onBlurAny={handleCodeListChange} - onChangeTextResource={handleChangeTextResource} + onChangeTextResource={onChangeTextResource} texts={editorTexts} - textResources={textResourcesForLanguage} + textResources={textResources} /> { - describe('getTextResourcesForLanguage', () => { - it('Returns the list of text resources for the given language', () => { - expect(getTextResourcesForLanguage('nb', textResources)).toEqual(textResourcesNb); - }); - - it('Returns undefined when the language does not exist', () => { - expect(getTextResourcesForLanguage('eo', textResources)).toBeUndefined(); - }); - - it('Returns undefined when the textResources parameter is undefined', () => { - expect(getTextResourcesForLanguage('nb', undefined)).toBeUndefined(); - }); - }); - - describe('createTextResourceWithLanguage', () => { - it('Creates a TextResourceWithLanguage object from the parameters', () => { - const language = 'nb'; - const textResource = label1ResourceNb; - const expectedResult: TextResourceWithLanguage = { language, textResource }; - expect(createTextResourceWithLanguage(language, textResource)).toEqual(expectedResult); - }); - }); -}); diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/utils.ts b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/utils.ts deleted file mode 100644 index af555d9e755..00000000000 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeLists/EditCodeList/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { TextResource } from '../../../../../../types/TextResource'; -import type { TextResources } from '../../../../../../types/TextResources'; -import type { TextResourceWithLanguage } from '../../../../../../types/TextResourceWithLanguage'; - -export const getTextResourcesForLanguage = ( - language: string, - textResources?: TextResources, -): TextResource[] | undefined => textResources?.[language]; - -export const createTextResourceWithLanguage = ( - language: string, - textResource: TextResource, -): TextResourceWithLanguage => ({ language, textResource }); diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListsActionsBar/CodeListsActionsBar.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListsActionsBar/CodeListsActionsBar.tsx index e827fddc4af..a3327896956 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListsActionsBar/CodeListsActionsBar.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListsActionsBar/CodeListsActionsBar.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type { TextResource } from '@studio/components'; import { StudioFileUploader, StudioSearch } from '@studio/components'; import type { ChangeEvent } from 'react'; import classes from './CodeListsActionsBar.module.css'; @@ -10,17 +11,21 @@ import { useUploadCodeListNameErrorMessage } from '../hooks/useUploadCodeListNam import { toast } from 'react-toastify'; export type CodeListsActionsBarProps = { + onChangeTextResource?: (textResource: TextResource) => void; onUploadCodeList: (updatedCodeList: File) => void; onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void; codeListNames: string[]; onSetSearchString: (searchString: string) => void; + textResources?: TextResource[]; }; export function CodeListsActionsBar({ + onChangeTextResource, onUploadCodeList, onUpdateCodeList, codeListNames, onSetSearchString, + textResources, }: CodeListsActionsBarProps) { const { t } = useTranslation(); const getInvalidUploadFileNameErrorMessage = useUploadCodeListNameErrorMessage(); @@ -49,7 +54,12 @@ export function CodeListsActionsBar({ clearButtonLabel={t('app_content_library.code_lists.clear_search_button_label')} onClear={handleClearSearch} /> - + void; onUpdateCodeList: (codeListWithMetadata: CodeListWithMetadata) => void; codeListNames: string[]; + textResources?: TextResource[]; }; export function CreateNewCodeListModal({ + onChangeTextResource, onUpdateCodeList, codeListNames, + textResources, }: CreateNewCodeListModalProps) { const { t } = useTranslation(); const modalRef = createRef(); @@ -46,8 +50,10 @@ export function CreateNewCodeListModal({ @@ -57,15 +63,19 @@ export function CreateNewCodeListModal({ type CreateNewCodeListProps = { codeList: CodeList; codeListNames: string[]; + onChangeTextResource?: (textResource: TextResource) => void; onUpdateCodeList: (codeListWithMetadata: CodeListWithMetadata) => void; onCloseModal: () => void; + textResources?: TextResource[]; }; function CreateNewCodeList({ codeList, codeListNames, + onChangeTextResource, onUpdateCodeList, onCloseModal, + textResources, }: CreateNewCodeListProps) { const { t } = useTranslation(); const editorTexts: CodeListEditorTexts = useCodeListEditorTexts(); @@ -125,8 +135,10 @@ function CreateNewCodeList({ { - it('returns an array of CodeListSources if given Id is present in codeListsUsages array', () => { - const codeListUsages: CodeListReference[] = [ - { codeListId: codeListId1, codeListIdSources: codeListIdSources1 }, - { codeListId: codeListId2, codeListIdSources: codeListIdSources2 }, - ]; - const codeListSources = getCodeListSourcesById(codeListUsages, codeListId1); - - expect(codeListSources).toBe(codeListIdSources1); - expect(codeListSources).not.toBe(codeListIdSources2); - }); - - it('returns an empty array if given Id is not present in codeListsUsages array', () => { - const codeListUsages: CodeListReference[] = [ - { codeListId: codeListId2, codeListIdSources: codeListIdSources2 }, - ]; - const codeListSources = getCodeListSourcesById(codeListUsages, codeListId1); - expect(codeListSources).toEqual([]); - }); - - it('returns an empty array if codeListsUsages array is empty', () => { - const codeListSources = getCodeListSourcesById([], codeListId1); - expect(codeListSources).toEqual([]); - }); -}); -describe('getCodeListUsageCount', () => { - it('returns the total count of all component IDs across all codeListSources', () => { - const codeListSources: CodeListIdSource[] = [ - { ...codeListIdSource, componentIds: ['id1', 'id2', 'id3'] }, - { ...codeListIdSource, componentIds: ['id4', 'id5'] }, - ]; - - const usageCount = getCodeListUsageCount(codeListSources); - - expect(usageCount).toBe(5); - }); - - it('returns 0 if codeListSources array is empty', () => { - const codeListSources: CodeListIdSource[] = []; - - const usageCount = getCodeListUsageCount(codeListSources); - - expect(usageCount).toBe(0); - }); - - it('returns 0 if all componentIds arrays are empty', () => { - const codeListSources: CodeListIdSource[] = [ - { ...codeListIdSource, componentIds: [] }, - { ...codeListIdSource, componentIds: [] }, - ]; - - const usageCount = getCodeListUsageCount(codeListSources); - - expect(usageCount).toBe(0); - }); - - it('handles a single codeListSource with multiple component IDs', () => { - const codeListSources: CodeListIdSource[] = [ - { ...codeListIdSource, componentIds: ['id1', 'id2'] }, - ]; - - const usageCount = getCodeListUsageCount(codeListSources); - - expect(usageCount).toBe(2); - }); - - it('handles a single codeListSource with no component IDs', () => { - const codeListSources: CodeListIdSource[] = [{ ...codeListIdSource, componentIds: [] }]; - - const usageCount = getCodeListUsageCount(codeListSources); - - expect(usageCount).toBe(0); - }); -}); - -describe('filterCodeLists', () => { - const codeLists: CodeListData[] = [ - { title: 'Fruits' }, - { title: 'Vegetables' }, - { title: 'Dairy Products' }, - { title: 'Frozen Foods' }, - ]; - - it('should return all code lists when the search string is empty', () => { - const result = filterCodeLists(codeLists, ''); - expect(result).toEqual(codeLists); - }); - - it('should filter code lists by exact match', () => { - const result = filterCodeLists(codeLists, 'Fruits'); - expect(result).toEqual([{ title: 'Fruits' }]); - }); - - it('should filter code lists by partial match', () => { - const result = filterCodeLists(codeLists, 'Food'); - expect(result).toEqual([{ title: 'Frozen Foods' }]); - }); - - it('should filter code lists case-insensitively', () => { - const result = filterCodeLists(codeLists, 'fruits'); - expect(result).toEqual([{ title: 'Fruits' }]); - }); - - it('should return an empty array when no matches are found', () => { - const result = filterCodeLists(codeLists, 'Meat'); - expect(result).toEqual([]); - }); - - it('should support an empty code list array', () => { - const result = filterCodeLists([], 'Fruits'); - expect(result).toEqual([]); - }); - - it('should support special characters in search strings', () => { - const specialCharacterCodeLists: CodeListData[] = [ - { title: 'Cakes & Cookies' }, - { title: 'Ice-Cream' }, - ]; - const result = filterCodeLists(specialCharacterCodeLists, '&'); - expect(result).toEqual([{ title: 'Cakes & Cookies' }]); - }); -}); diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/index.ts b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/index.ts new file mode 100644 index 00000000000..04bca77e0de --- /dev/null +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/index.ts @@ -0,0 +1 @@ +export * from './utils'; diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/utils.test.ts b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/utils.test.ts new file mode 100644 index 00000000000..ef52cb45fa7 --- /dev/null +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/utils.test.ts @@ -0,0 +1,175 @@ +import type { CodeListIdSource, CodeListReference } from '../types/CodeListReference'; +import { + createTextResourceWithLanguage, + filterCodeLists, + getCodeListSourcesById, + getCodeListUsageCount, + getTextResourcesForLanguage, +} from './'; +import type { CodeListData } from '../CodeListPage'; +import { + label1ResourceNb, + textResources, + textResourcesNb, +} from '../../../../../test-data/textResources'; +import type { TextResourceWithLanguage } from '../../../../../types/TextResourceWithLanguage'; + +const codeListId1: string = 'codeListId1'; +const codeListId2: string = 'codeListId2'; +const componentIds: string[] = ['componentId1', 'componentId2']; +const codeListIdSource: CodeListIdSource = { + layoutSetId: 'layoutSetId', + layoutName: 'layoutName', + componentIds, +}; +const codeListIdSources1: CodeListIdSource[] = [codeListIdSource]; +const codeListIdSources2: CodeListIdSource[] = [...codeListIdSources1]; + +describe('utils', () => { + describe('getCodeListSourcesById', () => { + it('returns an array of CodeListSources if given Id is present in codeListsUsages array', () => { + const codeListUsages: CodeListReference[] = [ + { codeListId: codeListId1, codeListIdSources: codeListIdSources1 }, + { codeListId: codeListId2, codeListIdSources: codeListIdSources2 }, + ]; + const codeListSources = getCodeListSourcesById(codeListUsages, codeListId1); + + expect(codeListSources).toBe(codeListIdSources1); + expect(codeListSources).not.toBe(codeListIdSources2); + }); + + it('returns an empty array if given Id is not present in codeListsUsages array', () => { + const codeListUsages: CodeListReference[] = [ + { codeListId: codeListId2, codeListIdSources: codeListIdSources2 }, + ]; + const codeListSources = getCodeListSourcesById(codeListUsages, codeListId1); + expect(codeListSources).toEqual([]); + }); + + it('returns an empty array if codeListsUsages array is empty', () => { + const codeListSources = getCodeListSourcesById([], codeListId1); + expect(codeListSources).toEqual([]); + }); + }); + + describe('getCodeListUsageCount', () => { + it('returns the total count of all component IDs across all codeListSources', () => { + const codeListSources: CodeListIdSource[] = [ + { ...codeListIdSource, componentIds: ['id1', 'id2', 'id3'] }, + { ...codeListIdSource, componentIds: ['id4', 'id5'] }, + ]; + + const usageCount = getCodeListUsageCount(codeListSources); + + expect(usageCount).toBe(5); + }); + + it('returns 0 if codeListSources array is empty', () => { + const codeListSources: CodeListIdSource[] = []; + + const usageCount = getCodeListUsageCount(codeListSources); + + expect(usageCount).toBe(0); + }); + + it('returns 0 if all componentIds arrays are empty', () => { + const codeListSources: CodeListIdSource[] = [ + { ...codeListIdSource, componentIds: [] }, + { ...codeListIdSource, componentIds: [] }, + ]; + + const usageCount = getCodeListUsageCount(codeListSources); + + expect(usageCount).toBe(0); + }); + + it('handles a single codeListSource with multiple component IDs', () => { + const codeListSources: CodeListIdSource[] = [ + { ...codeListIdSource, componentIds: ['id1', 'id2'] }, + ]; + + const usageCount = getCodeListUsageCount(codeListSources); + + expect(usageCount).toBe(2); + }); + + it('handles a single codeListSource with no component IDs', () => { + const codeListSources: CodeListIdSource[] = [{ ...codeListIdSource, componentIds: [] }]; + + const usageCount = getCodeListUsageCount(codeListSources); + + expect(usageCount).toBe(0); + }); + }); + + describe('filterCodeLists', () => { + const codeLists: CodeListData[] = [ + { title: 'Fruits' }, + { title: 'Vegetables' }, + { title: 'Dairy Products' }, + { title: 'Frozen Foods' }, + ]; + + it('should return all code lists when the search string is empty', () => { + const result = filterCodeLists(codeLists, ''); + expect(result).toEqual(codeLists); + }); + + it('should filter code lists by exact match', () => { + const result = filterCodeLists(codeLists, 'Fruits'); + expect(result).toEqual([{ title: 'Fruits' }]); + }); + + it('should filter code lists by partial match', () => { + const result = filterCodeLists(codeLists, 'Food'); + expect(result).toEqual([{ title: 'Frozen Foods' }]); + }); + + it('should filter code lists case-insensitively', () => { + const result = filterCodeLists(codeLists, 'fruits'); + expect(result).toEqual([{ title: 'Fruits' }]); + }); + + it('should return an empty array when no matches are found', () => { + const result = filterCodeLists(codeLists, 'Meat'); + expect(result).toEqual([]); + }); + + it('should support an empty code list array', () => { + const result = filterCodeLists([], 'Fruits'); + expect(result).toEqual([]); + }); + + it('should support special characters in search strings', () => { + const specialCharacterCodeLists: CodeListData[] = [ + { title: 'Cakes & Cookies' }, + { title: 'Ice-Cream' }, + ]; + const result = filterCodeLists(specialCharacterCodeLists, '&'); + expect(result).toEqual([{ title: 'Cakes & Cookies' }]); + }); + }); + + describe('getTextResourcesForLanguage', () => { + it('Returns the list of text resources for the given language', () => { + expect(getTextResourcesForLanguage('nb', textResources)).toEqual(textResourcesNb); + }); + + it('Returns undefined when the language does not exist', () => { + expect(getTextResourcesForLanguage('eo', textResources)).toBeUndefined(); + }); + + it('Returns undefined when the textResources parameter is undefined', () => { + expect(getTextResourcesForLanguage('nb', undefined)).toBeUndefined(); + }); + }); + + describe('createTextResourceWithLanguage', () => { + it('Creates a TextResourceWithLanguage object from the parameters', () => { + const language = 'nb'; + const textResource = label1ResourceNb; + const expectedResult: TextResourceWithLanguage = { language, textResource }; + expect(createTextResourceWithLanguage(language, textResource)).toEqual(expectedResult); + }); + }); +}); diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/codeListPageUtils.ts b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/utils.ts similarity index 70% rename from frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/codeListPageUtils.ts rename to frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/utils.ts index f36ffa5b310..c57a51aa619 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/codeListPageUtils.ts +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/utils/utils.ts @@ -1,5 +1,8 @@ import type { CodeListIdSource, CodeListReference } from '../types/CodeListReference'; import type { CodeListData } from '../CodeListPage'; +import type { TextResources } from '../../../../../types/TextResources'; +import type { TextResource } from '@studio/components'; +import type { TextResourceWithLanguage } from '../../../../../types/TextResourceWithLanguage'; export const getCodeListSourcesById = ( codeListsUsages: CodeListReference[], @@ -33,3 +36,13 @@ function caseInsensitiveMatch(target: string, searchString: string): boolean { const lowerCaseSearchString = searchString.toLowerCase(); return lowerCaseTarget.includes(lowerCaseSearchString); } + +export const getTextResourcesForLanguage = ( + language: string, + textResources?: TextResources, +): TextResource[] | undefined => textResources?.[language]; + +export const createTextResourceWithLanguage = ( + language: string, + textResource: TextResource, +): TextResourceWithLanguage => ({ language, textResource }); diff --git a/frontend/libs/studio-content-library/src/index.ts b/frontend/libs/studio-content-library/src/index.ts index d06bfbe240b..ac53e2becd0 100644 --- a/frontend/libs/studio-content-library/src/index.ts +++ b/frontend/libs/studio-content-library/src/index.ts @@ -5,3 +5,6 @@ export type { CodeListIdSource, CodeListReference, } from './ContentLibrary/LibraryBody/pages'; +export type { TextResource } from './types/TextResource'; +export type { TextResourceWithLanguage } from './types/TextResourceWithLanguage'; +export type { TextResources } from './types/TextResources'; From 9fd63fdb515d1af15628911a71ee2c59380a0bc4 Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 12 Feb 2025 15:02:42 +0100 Subject: [PATCH 2/3] Fix test --- .../StudioTextResourceInput/StudioTextResourceInput.tsx | 2 +- .../LibraryBody/pages/CodeListPage/CodeListPage.test.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/libs/studio-components/src/components/StudioTextResourceInput/StudioTextResourceInput.tsx b/frontend/libs/studio-components/src/components/StudioTextResourceInput/StudioTextResourceInput.tsx index e2840077440..254fceca28c 100644 --- a/frontend/libs/studio-components/src/components/StudioTextResourceInput/StudioTextResourceInput.tsx +++ b/frontend/libs/studio-components/src/components/StudioTextResourceInput/StudioTextResourceInput.tsx @@ -62,7 +62,7 @@ export const StudioTextResourceInput = forwardRef { const newList = changeTextResourceInList(textResources, newTextResource); setTextResources(newList); - onChangeTextResource(newTextResource); + onChangeTextResource?.(newTextResource); }; const rootClass = cn(givenClass, classes.container); diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx index dc0887e535b..b9697ae9bf5 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.test.tsx @@ -193,7 +193,7 @@ describe('CodeListPage', () => { await openSearchModeForFirstLabel(user, dialog); await openFirstLabelCombobox(user, dialog); - expect(getTextResourceOption(label1ResourceNb, dialog)).toBeInTheDocument(); + expect(getTextResourceOption(label1ResourceNb)).toBeInTheDocument(); }); it('Calls onUpdateTextResource with the new text resource and the default language when a text resource is changed in the create dialog', async () => { @@ -206,7 +206,7 @@ describe('CodeListPage', () => { await addCodeListItem(user, dialog); await openSearchModeForFirstLabel(user, dialog); await openFirstLabelCombobox(user, dialog); - await user.click(getTextResourceOption(label1ResourceNb, dialog)); + await user.click(getTextResourceOption(label1ResourceNb)); await openEditModeForFirstLabel(user, dialog); await user.type(getFirstLabelField(dialog), newLabel); @@ -285,7 +285,7 @@ const openFirstLabelCombobox = async (user: UserEvent, area: HTMLElement): Promi await user.click(combobox); }; -const getTextResourceOption = (textResource: TextResource, area: HTMLElement): HTMLElement => - within(area).getByRole('option', { name: retrieveOptionName(textResource) }); +const getTextResourceOption = (textResource: TextResource): HTMLElement => + screen.getByRole('option', { name: retrieveOptionName(textResource) }); const retrieveOptionName = ({ value, id }: TextResource): string => `${value} ${id}`; From 35431d374ee9a40bfcdac429e4abe96fc089f9c6 Mon Sep 17 00:00:00 2001 From: Tomas Date: Thu, 13 Feb 2025 07:53:58 +0100 Subject: [PATCH 3/3] Remove magic string --- .../LibraryBody/pages/CodeListPage/CodeListPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx index 3629c2c4d67..c89dc35262b 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage/CodeListPage.tsx @@ -64,7 +64,7 @@ export function CodeListPage({ const handleChangeTextResource = useCallback( (textResource: TextResource) => { - const updatedTextResource = createTextResourceWithLanguage('nb', textResource); + const updatedTextResource = createTextResourceWithLanguage(language, textResource); onUpdateTextResource?.(updatedTextResource); }, [onUpdateTextResource],