Skip to content

Commit

Permalink
feat: Add text resources to app content library (#14722)
Browse files Browse the repository at this point in the history
Co-authored-by: William Thorenfeldt <[email protected]>
  • Loading branch information
TomasEng and wrt95 authored Feb 25, 2025
1 parent c66f25b commit c1d0036
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -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(() => <div data-testid={resourceLibraryTestId} />);
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', () => {
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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,
);
});
});

Expand All @@ -153,19 +179,18 @@ const renderAppContentLibrary = ({
renderWithProviders(queries, queryClient)(<AppContentLibrary />);
};

function renderAppContentLibraryWithOptionLists(
function renderAppContentLibraryWithData(
props?: Omit<RenderAppContentLibraryProps, 'queryClient'>,
): 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand All @@ -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':
Expand All @@ -51,6 +64,7 @@ export function AppContentLibrary(): React.ReactElement {
<AppContentLibraryWithData
optionListDataList={optionListDataList}
optionListUsages={optionListUsages}
textResources={textResources}
/>
);
}
Expand All @@ -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);
Expand All @@ -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: {
Expand All @@ -91,8 +116,10 @@ function AppContentLibraryWithData({
onDeleteCodeList: deleteOptionList,
onUpdateCodeListId: handleUpdateCodeListId,
onUpdateCodeList: handleUpdate,
onUpdateTextResource: handleUpdateTextResource,
onUploadCodeList: handleUpload,
codeListsUsages,
textResources,
},
},
images: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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];
Loading

0 comments on commit c1d0036

Please sign in to comment.