From c1d0036306e56f0ccd3db42b87ca6a1c4e785442 Mon Sep 17 00:00:00 2001 From: Tomas Engebretsen Date: Tue, 25 Feb 2025 08:16:27 +0100 Subject: [PATCH] feat: Add text resources to app content library (#14722) Co-authored-by: William Thorenfeldt <48119543+wrt95@users.noreply.github.com> --- .../AppContentLibrary.test.tsx | 89 ++++++++++++------- .../appContentLibrary/AppContentLibrary.tsx | 31 ++++++- .../test-data/optionListDataList.ts | 57 ++++++++++++ .../test-data/textResources.ts | 74 +++++++++++++++ .../convertTextResourceToMutationArgs.test.ts | 14 +++ .../convertTextResourceToMutationArgs.ts | 13 +++ 6 files changed, 244 insertions(+), 34 deletions(-) create mode 100644 frontend/app-development/features/appContentLibrary/test-data/optionListDataList.ts create mode 100644 frontend/app-development/features/appContentLibrary/test-data/textResources.ts create mode 100644 frontend/app-development/features/appContentLibrary/utils/convertTextResourceToMutationArgs.test.ts create mode 100644 frontend/app-development/features/appContentLibrary/utils/convertTextResourceToMutationArgs.ts diff --git a/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx b/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx index bad508a01f1..64c86b078a3 100644 --- a/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx +++ b/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx @@ -7,16 +7,18 @@ 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 { CodeList } from '@studio/components'; import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext'; -import type { OptionListData } from 'app-shared/types/OptionList'; import type { QueryClient } from '@tanstack/react-query'; import type { CodeListData, CodeListWithMetadata, PagesConfig, ResourceContentLibraryImpl, + TextResourceWithLanguage, } from '@studio/content-library'; +import { optionList1Data, optionListDataList } from './test-data/optionListDataList'; +import { label1ResourceNb, textResources } from './test-data/textResources'; +import type { ITextResourcesObjectFormat } from 'app-shared/types/global'; // Mocks: jest.mock('@studio/content-library', () => ({ @@ -32,23 +34,17 @@ function mockContentLibrary( } const mockConstructor = jest.fn(); -const getContentResourceLibrary = jest.fn(); - -// Test data: -const codeListName = 'codeListNameMock'; -const codeList: CodeList = [{ value: '', label: '' }]; -const codeListWithMetadata: CodeListWithMetadata = { - codeList, - title: codeListName, -}; -const optionListData: OptionListData = { title: codeListName, data: codeList }; +const getContentResourceLibrary = jest + .fn() + .mockImplementation(() =>
); +const resourceLibraryTestId = 'resource-library'; describe('AppContentLibrary', () => { afterEach(jest.clearAllMocks); it('Renders the content library', async () => { - renderAppContentLibraryWithOptionLists(); - expect(getContentResourceLibrary).toHaveBeenCalledTimes(1); + renderAppContentLibraryWithData(); + expect(screen.getByTestId(resourceLibraryTestId)).toBeInTheDocument(); }); it('renders a spinner when waiting for option lists', () => { @@ -66,16 +62,22 @@ describe('AppContentLibrary', () => { }); it('Renders with the given code lists', () => { - renderAppContentLibraryWithOptionLists(); + renderAppContentLibraryWithData(); const codeListDataList = retrieveConfig().codeList.props.codeListsData; - const expectedData: CodeListData[] = [{ title: codeListName, data: codeList }]; + const expectedData: CodeListData[] = optionListDataList; expect(codeListDataList).toEqual(expectedData); }); + it('Renders with the given text resources', () => { + renderAppContentLibraryWithData(); + const textResourcesData = retrieveConfig().codeList.props.textResources; + expect(textResourcesData).toEqual(textResources); + }); + it('calls uploadOptionList with correct data when onUploadCodeList is triggered', async () => { const uploadOptionList = jest.fn(); const file = new File([''], 'list.json'); - renderAppContentLibraryWithOptionLists({ queries: { uploadOptionList } }); + renderAppContentLibraryWithData({ queries: { uploadOptionList } }); retrieveConfig().codeList.props.onUploadCodeList(file); await waitFor(expect(uploadOptionList).toHaveBeenCalled); @@ -87,7 +89,7 @@ describe('AppContentLibrary', () => { }); it('renders success toast when onUploadOptionList is called successfully', async () => { - renderAppContentLibraryWithOptionLists(); + renderAppContentLibraryWithData(); const file = new File([''], 'list.json'); retrieveConfig().codeList.props.onUploadCodeList(file); @@ -100,7 +102,7 @@ describe('AppContentLibrary', () => { it('renders error toast when onUploadOptionList is rejected with unknown error code', async () => { const uploadOptionList = jest.fn().mockImplementation(() => Promise.reject({ response: {} })); const file = new File([''], 'list.json'); - renderAppContentLibraryWithOptionLists({ queries: { uploadOptionList } }); + renderAppContentLibraryWithData({ queries: { uploadOptionList } }); retrieveConfig().codeList.props.onUploadCodeList(file); await waitFor(expect(uploadOptionList).toHaveBeenCalled); @@ -110,34 +112,58 @@ describe('AppContentLibrary', () => { }); it('calls updateOptionList with correct data when onUpdateCodeList is triggered', async () => { - renderAppContentLibraryWithOptionLists(); + const { title, data: codeList } = optionList1Data; + const codeListWithMetadata: CodeListWithMetadata = { title, codeList }; + renderAppContentLibraryWithData(); retrieveConfig().codeList.props.onUpdateCodeList(codeListWithMetadata); await waitFor(expect(queriesMock.updateOptionList).toHaveBeenCalled); expect(queriesMock.updateOptionList).toHaveBeenCalledTimes(1); - expect(queriesMock.updateOptionList).toHaveBeenCalledWith(org, app, codeListName, codeList); + expect(queriesMock.updateOptionList).toHaveBeenCalledWith(org, app, title, codeList); }); it('calls updateOptionListId with correct data when onUpdateCodeListId is triggered', async () => { + const { title: currentName } = optionList1Data; const newName = 'newName'; - renderAppContentLibraryWithOptionLists(); + renderAppContentLibraryWithData(); - retrieveConfig().codeList.props.onUpdateCodeListId(codeListName, newName); + retrieveConfig().codeList.props.onUpdateCodeListId(currentName, newName); await waitFor(expect(queriesMock.updateOptionListId).toHaveBeenCalled); expect(queriesMock.updateOptionListId).toHaveBeenCalledTimes(1); - expect(queriesMock.updateOptionListId).toHaveBeenCalledWith(org, app, codeListName, newName); + expect(queriesMock.updateOptionListId).toHaveBeenCalledWith(org, app, currentName, newName); }); it('calls deleteOptionList with correct data when onDeleteCodeList is triggered', async () => { - renderAppContentLibraryWithOptionLists(); + renderAppContentLibraryWithData(); - retrieveConfig().codeList.props.onDeleteCodeList(codeListName); + retrieveConfig().codeList.props.onDeleteCodeList(optionList1Data.title); await waitFor(expect(queriesMock.deleteOptionList).toHaveBeenCalled); expect(queriesMock.deleteOptionList).toHaveBeenCalledTimes(1); - expect(queriesMock.deleteOptionList).toHaveBeenCalledWith(org, app, codeListName); + expect(queriesMock.deleteOptionList).toHaveBeenCalledWith(org, app, optionList1Data.title); + }); + + it('Calls upsertTextResource with correct data when onUpdateTextResource is triggered', async () => { + const language = 'nb'; + const textResource = label1ResourceNb; + const textResourceWithLanguage: TextResourceWithLanguage = { language, textResource }; + renderAppContentLibraryWithData(); + + retrieveConfig().codeList.props.onUpdateTextResource(textResourceWithLanguage); + await waitFor(expect(queriesMock.upsertTextResources).toHaveBeenCalled); + + expect(queriesMock.upsertTextResources).toHaveBeenCalledTimes(1); + const expectedPayload: ITextResourcesObjectFormat = { + [textResource.id]: textResource.value, + }; + expect(queriesMock.upsertTextResources).toHaveBeenCalledWith( + org, + app, + language, + expectedPayload, + ); }); }); @@ -153,19 +179,18 @@ const renderAppContentLibrary = ({ renderWithProviders(queries, queryClient)(); }; -function renderAppContentLibraryWithOptionLists( +function renderAppContentLibraryWithData( props?: Omit, ): void { - const queryClient = createQueryClientWithOptionsDataList([optionListData]); + const queryClient = createQueryClientWithData(); renderAppContentLibrary({ ...props, queryClient }); } -function createQueryClientWithOptionsDataList( - optionListDataList: OptionListData[] | undefined, -): QueryClient { +function createQueryClientWithData(): QueryClient { const queryClient = createQueryClientMock(); queryClient.setQueryData([QueryKey.OptionLists, org, app], optionListDataList); queryClient.setQueryData([QueryKey.OptionListsUsage, org, app], []); + queryClient.setQueryData([QueryKey.TextResources, org, app], textResources); return queryClient; } diff --git a/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx b/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx index 4b35abe90ac..e1d448bf0f3 100644 --- a/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx +++ b/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx @@ -2,12 +2,17 @@ import type { CodeListData, CodeListReference, CodeListWithMetadata, + TextResourceWithLanguage, } from '@studio/content-library'; import { ResourceContentLibraryImpl } from '@studio/content-library'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; -import { useOptionListsQuery, useOptionListsReferencesQuery } from 'app-shared/hooks/queries'; +import { + useOptionListsQuery, + useOptionListsReferencesQuery, + useTextResourcesQuery, +} from 'app-shared/hooks/queries'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { mapToCodeListDataList } from './utils/mapToCodeListDataList'; import { StudioPageError, StudioPageSpinner } from '@studio/components'; @@ -21,11 +26,14 @@ import { useUpdateOptionListMutation, useUpdateOptionListIdMutation, useDeleteOptionListMutation, + useUpsertTextResourceMutation, } from 'app-shared/hooks/mutations'; import { mapToCodeListUsages } from './utils/mapToCodeListUsages'; import type { OptionListData } from 'app-shared/types/OptionList'; import type { OptionListReferences } from 'app-shared/types/OptionListReferences'; import { mergeQueryStatuses } from 'app-shared/utils/tanstackQueryUtils'; +import type { ITextResources } from 'app-shared/types/global'; +import { convertTextResourceToMutationArgs } from './utils/convertTextResourceToMutationArgs'; export function AppContentLibrary(): React.ReactElement { const { org, app } = useStudioEnvironmentParams(); @@ -38,8 +46,13 @@ export function AppContentLibrary(): React.ReactElement { org, app, ); + const { data: textResources, status: textResourcesStatus } = useTextResourcesQuery(org, app); - const status = mergeQueryStatuses(optionListDataListStatus, optionListUsagesStatus); + const status = mergeQueryStatuses( + optionListDataListStatus, + optionListUsagesStatus, + textResourcesStatus, + ); switch (status) { case 'pending': @@ -51,6 +64,7 @@ export function AppContentLibrary(): React.ReactElement { ); } @@ -59,16 +73,19 @@ export function AppContentLibrary(): React.ReactElement { type AppContentLibraryWithDataProps = { optionListDataList: OptionListData[]; optionListUsages: OptionListReferences; + textResources: ITextResources; }; function AppContentLibraryWithData({ optionListDataList, optionListUsages, + textResources, }: AppContentLibraryWithDataProps): ReactElement { const { org, app } = useStudioEnvironmentParams(); const { mutate: updateOptionList } = useUpdateOptionListMutation(org, app); const { mutate: updateOptionListId } = useUpdateOptionListIdMutation(org, app); const { mutate: deleteOptionList } = useDeleteOptionListMutation(org, app); + const { mutate: updateTextResource } = useUpsertTextResourceMutation(org, app); const handleUpload = useUploadOptionList(org, app); const codeListDataList: CodeListData[] = mapToCodeListDataList(optionListDataList); @@ -83,6 +100,14 @@ function AppContentLibraryWithData({ updateOptionList({ optionListId: title, optionList: codeList }); }; + const handleUpdateTextResource = useCallback( + (textResourceWithLanguage: TextResourceWithLanguage): void => { + const mutationArgs = convertTextResourceToMutationArgs(textResourceWithLanguage); + updateTextResource(mutationArgs); + }, + [updateTextResource], + ); + const { getContentResourceLibrary } = new ResourceContentLibraryImpl({ pages: { codeList: { @@ -91,8 +116,10 @@ function AppContentLibraryWithData({ onDeleteCodeList: deleteOptionList, onUpdateCodeListId: handleUpdateCodeListId, onUpdateCodeList: handleUpdate, + onUpdateTextResource: handleUpdateTextResource, onUploadCodeList: handleUpload, codeListsUsages, + textResources, }, }, images: { diff --git a/frontend/app-development/features/appContentLibrary/test-data/optionListDataList.ts b/frontend/app-development/features/appContentLibrary/test-data/optionListDataList.ts new file mode 100644 index 00000000000..389dacc760c --- /dev/null +++ b/frontend/app-development/features/appContentLibrary/test-data/optionListDataList.ts @@ -0,0 +1,57 @@ +import { + description1ResourceNb, + description2ResourceNb, + description3ResourceNb, + helpText1ResourceNb, + helpText2ResourceNb, + helpText3ResourceNb, + label1ResourceNb, + label2ResourceNb, + label3ResourceNb, + label4ResourceNb, +} from './textResources'; +import type { OptionList, OptionListData } from 'app-shared/types/OptionList'; + +const optionList1: OptionList = [ + { + value: 'item1', + label: label1ResourceNb.id, + description: description1ResourceNb.id, + helpText: helpText1ResourceNb.id, + }, + { + value: 'item2', + label: label2ResourceNb.id, + description: description2ResourceNb.id, + helpText: helpText2ResourceNb.id, + }, + { + value: 'item3', + label: label3ResourceNb.id, + description: description3ResourceNb.id, + helpText: helpText3ResourceNb.id, + }, +]; +const optionList1Name = 'optionList1'; +export const optionList1Data: OptionListData = { + title: optionList1Name, + data: optionList1, +}; + +const optionList2: OptionList = [ + { + value: 'item1', + label: label1ResourceNb.id, + }, + { + value: 'item4', + label: label4ResourceNb.id, + }, +]; +const optionList2Name = 'optionList2'; +export const optionList2Data: OptionListData = { + title: optionList2Name, + data: optionList2, +}; + +export const optionListDataList = [optionList1Data, optionList2Data]; diff --git a/frontend/app-development/features/appContentLibrary/test-data/textResources.ts b/frontend/app-development/features/appContentLibrary/test-data/textResources.ts new file mode 100644 index 00000000000..95fd4c5fe32 --- /dev/null +++ b/frontend/app-development/features/appContentLibrary/test-data/textResources.ts @@ -0,0 +1,74 @@ +import type { ITextResource, ITextResources } from 'app-shared/types/global'; + +const label1Id = 'label1'; +const description1Id = 'description1'; +const helpText1Id = 'helpText1'; +const label2Id = 'label2'; +const description2Id = 'description2'; +const helpText2Id = 'helpText2'; +const label3Id = 'label3'; +const description3Id = 'description3'; +const helpText3Id = 'helpText3'; +const label4Id = 'label4'; +const description4Id = 'description4'; +const helpText4Id = 'helpText4'; + +export const label1ResourceNb: ITextResource = { id: label1Id, value: 'Ledetekst 1' }; +export const label1ResourceEn: ITextResource = { id: label1Id, value: 'Label 1' }; +export const description1ResourceNb: ITextResource = { id: description1Id, value: 'Beskrivelse 1' }; +export const description1ResourceEn: ITextResource = { id: description1Id, value: 'Description 1' }; +export const helpText1ResourceNb: ITextResource = { id: helpText1Id, value: 'Hjelpetekst 1' }; +export const helpText1ResourceEn: ITextResource = { id: helpText1Id, value: 'Help text 1' }; +export const label2ResourceNb: ITextResource = { id: label2Id, value: 'Ledetekst 2' }; +export const label2ResourceEn: ITextResource = { id: label2Id, value: 'Label 2' }; +export const description2ResourceNb: ITextResource = { id: description2Id, value: 'Beskrivelse 2' }; +export const description2ResourceEn: ITextResource = { id: description2Id, value: 'Description 2' }; +export const helpText2ResourceNb: ITextResource = { id: helpText2Id, value: 'Hjelpetekst 2' }; +export const helpText2ResourceEn: ITextResource = { id: helpText2Id, value: 'Help text 2' }; +export const label3ResourceNb: ITextResource = { id: label3Id, value: 'Ledetekst 3' }; +export const label3ResourceEn: ITextResource = { id: label3Id, value: 'Label 3' }; +export const description3ResourceNb: ITextResource = { id: description3Id, value: 'Beskrivelse 3' }; +export const description3ResourceEn: ITextResource = { id: description3Id, value: 'Description 3' }; +export const helpText3ResourceNb: ITextResource = { id: helpText3Id, value: 'Hjelpetekst 3' }; +export const helpText3ResourceEn: ITextResource = { id: helpText3Id, value: 'Help text 3' }; +export const label4ResourceNb: ITextResource = { id: label4Id, value: 'Ledetekst 4' }; +export const label4ResourceEn: ITextResource = { id: label4Id, value: 'Label 4' }; +export const description4ResourceNb: ITextResource = { id: description4Id, value: 'Beskrivelse 4' }; +export const description4ResourceEn: ITextResource = { id: description4Id, value: 'Description 4' }; +export const helpText4ResourceNb: ITextResource = { id: helpText4Id, value: 'Hjelpetekst 4' }; +export const helpText4ResourceEn: ITextResource = { id: helpText4Id, value: 'Help text 4' }; + +export const textResourcesNb: ITextResource[] = [ + label1ResourceNb, + description1ResourceNb, + helpText1ResourceNb, + label2ResourceNb, + description2ResourceNb, + helpText2ResourceNb, + label3ResourceNb, + description3ResourceNb, + helpText3ResourceNb, + label4ResourceNb, + description4ResourceNb, + helpText4ResourceNb, +]; + +export const textResourcesEn: ITextResource[] = [ + label1ResourceEn, + description1ResourceEn, + helpText1ResourceEn, + label2ResourceEn, + description2ResourceEn, + helpText2ResourceEn, + label3ResourceEn, + description3ResourceEn, + helpText3ResourceEn, + label4ResourceEn, + description4ResourceEn, + helpText4ResourceEn, +]; + +export const textResources: ITextResources = { + nb: textResourcesNb, + en: textResourcesEn, +}; diff --git a/frontend/app-development/features/appContentLibrary/utils/convertTextResourceToMutationArgs.test.ts b/frontend/app-development/features/appContentLibrary/utils/convertTextResourceToMutationArgs.test.ts new file mode 100644 index 00000000000..6e6c909762f --- /dev/null +++ b/frontend/app-development/features/appContentLibrary/utils/convertTextResourceToMutationArgs.test.ts @@ -0,0 +1,14 @@ +import type { TextResource, TextResourceWithLanguage } from '@studio/content-library'; +import { convertTextResourceToMutationArgs } from './convertTextResourceToMutationArgs'; + +describe('convertTextResourceToMutationArgs', () => { + it('Converts a TextResourceWithLanguage object to an UpsertTextResourceMutation object', () => { + const language = 'nn'; + const id = 'a_text'; + const value = 'Ein tekst'; + const textResource: TextResource = { id, value }; + const textResourceWithLanguage: TextResourceWithLanguage = { language, textResource }; + const result = convertTextResourceToMutationArgs(textResourceWithLanguage); + expect(result).toEqual({ textId: id, language, translation: value }); + }); +}); diff --git a/frontend/app-development/features/appContentLibrary/utils/convertTextResourceToMutationArgs.ts b/frontend/app-development/features/appContentLibrary/utils/convertTextResourceToMutationArgs.ts new file mode 100644 index 00000000000..80e434aefd7 --- /dev/null +++ b/frontend/app-development/features/appContentLibrary/utils/convertTextResourceToMutationArgs.ts @@ -0,0 +1,13 @@ +import type { TextResourceWithLanguage } from '@studio/content-library'; +import type { UpsertTextResourceMutation } from 'app-shared/hooks/mutations/useUpsertTextResourceMutation'; + +export function convertTextResourceToMutationArgs({ + textResource, + language, +}: TextResourceWithLanguage): UpsertTextResourceMutation { + return { + textId: textResource.id, + language, + translation: textResource.value, + }; +}