From 1e7c4ef0bb451f939e5dc041b11d41f2d2ef6ca6 Mon Sep 17 00:00:00 2001 From: andreastanderen Date: Fri, 29 Nov 2024 15:30:33 +0100 Subject: [PATCH] use new getOptionList by optionListId --- .../AppContentLibrary.test.tsx | 16 +++-- .../appContentLibrary/AppContentLibrary.tsx | 25 ++++---- .../convertOptionListsToCodeLists.test.ts | 41 ------------- .../utils/convertOptionListsToCodeLists.ts | 13 ----- ...convertOptionsListToCodeListResult.test.ts | 58 +++++++++++++++++++ .../convertOptionsListToCodeListResult.ts | 14 +++++ frontend/language/src/nb.json | 2 +- .../mocks/mockPagesConfig.ts | 12 ++-- .../pages/CodeList/CodeList.test.tsx | 38 ++++-------- .../LibraryBody/pages/CodeList/CodeList.tsx | 27 +++++---- .../CodeList/CodeLists/CodeLists.test.tsx | 28 +++++++-- .../pages/CodeList/CodeLists/CodeLists.tsx | 27 ++++++--- .../LibraryBody/pages/CodeList/index.ts | 2 +- .../ContentLibrary/LibraryBody/pages/index.ts | 2 +- .../libs/studio-content-library/src/index.ts | 2 +- frontend/packages/shared/src/api/paths.js | 1 + frontend/packages/shared/src/api/queries.ts | 4 +- .../shared/src/hooks/queries/index.ts | 2 + .../hooks/queries/useGetOptionListQuery.ts | 16 +++++ .../hooks/queries/useOptionListQuery.test.ts | 31 ++++++++++ .../src/hooks/queries/useOptionListQuery.ts | 17 ++++++ .../packages/shared/src/mocks/queriesMock.ts | 3 +- .../shared/src/types/api/OptionsLists.ts | 4 +- .../EditOptionList/OptionListEditor.test.tsx | 29 +++++----- .../EditOptionList/OptionListEditor.tsx | 8 +-- 25 files changed, 262 insertions(+), 160 deletions(-) delete mode 100644 frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.test.ts delete mode 100644 frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.ts create mode 100644 frontend/app-development/features/appContentLibrary/utils/convertOptionsListToCodeListResult.test.ts create mode 100644 frontend/app-development/features/appContentLibrary/utils/convertOptionsListToCodeListResult.ts create mode 100644 frontend/packages/shared/src/hooks/queries/useGetOptionListQuery.ts create mode 100644 frontend/packages/shared/src/hooks/queries/useOptionListQuery.test.ts create mode 100644 frontend/packages/shared/src/hooks/queries/useOptionListQuery.ts diff --git a/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx b/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx index 42c6c0cdaaf..aa631aad892 100644 --- a/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx +++ b/frontend/app-development/features/appContentLibrary/AppContentLibrary.test.tsx @@ -40,15 +40,13 @@ jest.mock( }), ); -const optionListsMock: OptionsLists = { - list1: [{ label: 'label', value: 'value' }], -}; +const optionListIdsMock: string[] = ['list1']; describe('AppContentLibrary', () => { afterEach(jest.clearAllMocks); it('renders the AppContentLibrary with codeLists and images resources available in the content menu', () => { - renderAppContentLibrary(optionListsMock); + renderAppContentLibrary(optionListIdsMock); const libraryTitle = screen.getByRole('heading', { name: textMock('app_content_library.landing_page.title'), }); @@ -67,7 +65,7 @@ describe('AppContentLibrary', () => { it('calls onUploadOptionList when onUploadCodeList is triggered', async () => { const user = userEvent.setup(); - renderAppContentLibrary(optionListsMock); + renderAppContentLibrary(optionListIdsMock); await goToLibraryPage(user, 'code_lists'); const uploadCodeListButton = screen.getByRole('button', { name: uploadCodeListButtonTextMock }); await user.click(uploadCodeListButton); @@ -77,7 +75,7 @@ describe('AppContentLibrary', () => { it('calls onUpdateOptionList when onUpdateCodeList is triggered', async () => { const user = userEvent.setup(); - renderAppContentLibrary(optionListsMock); + renderAppContentLibrary(optionListIdsMock); await goToLibraryPage(user, 'code_lists'); const updateCodeListButton = screen.getByRole('button', { name: updateCodeListButtonTextMock }); await user.click(updateCodeListButton); @@ -99,10 +97,10 @@ const goToLibraryPage = async (user: UserEvent, libraryPage: string) => { await user.click(libraryPageNavTile); }; -const renderAppContentLibrary = (optionLists: OptionsLists = {}) => { +const renderAppContentLibrary = (optionListIds: string[] = []) => { const queryClientMock = createQueryClientMock(); - if (Object.keys(optionLists).length) { - queryClientMock.setQueryData([QueryKey.OptionLists, org, app], optionLists); + if (optionListIds.length) { + queryClientMock.setQueryData([QueryKey.OptionListIds, org, app], optionListIds); } renderWithProviders({}, queryClientMock)(); }; diff --git a/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx b/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx index 13746a6339f..322fca7ad78 100644 --- a/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx +++ b/frontend/app-development/features/appContentLibrary/AppContentLibrary.tsx @@ -1,28 +1,29 @@ -import type { CodeListWithMetadata } from '@studio/content-library'; +import type { CodeListWithMetadata, OnGetCodeListResult } from '@studio/content-library'; import { ResourceContentLibraryImpl } from '@studio/content-library'; import React from 'react'; -import { useOptionListsQuery } from 'app-shared/hooks/queries/useOptionListsQuery'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; -import { convertOptionListsToCodeLists } from './utils/convertOptionListsToCodeLists'; +import { convertOptionsListToCodeListResult } from './utils/convertOptionsListToCodeListResult'; import { StudioPageSpinner } from '@studio/components'; import { useTranslation } from 'react-i18next'; import { useAddOptionListMutation, useUpdateOptionListMutation } from 'app-shared/hooks/mutations'; +import { useOptionListIdsQuery } from '@altinn/ux-editor/hooks/queries/useOptionListIdsQuery'; +import { useGetOptionListQuery } from 'app-shared/hooks/queries'; export function AppContentLibrary(): React.ReactElement { const { org, app } = useStudioEnvironmentParams(); const { t } = useTranslation(); - const { - data: optionLists, - isPending: optionListsPending, - isError: optionListsError, - } = useOptionListsQuery(org, app); + const { data: optionListIds, isPending: optionListIdsPending } = useOptionListIdsQuery(org, app); + const getOptionList = useGetOptionListQuery(org, app); const { mutate: uploadOptionList } = useAddOptionListMutation(org, app); const { mutate: updateOptionList } = useUpdateOptionListMutation(org, app); - if (optionListsPending) + if (optionListIdsPending) return ; - const codeLists = convertOptionListsToCodeLists(optionLists); + const handleGetOptionList = (optionListId: string): OnGetCodeListResult => { + const { data: optionList, isError: optionListError } = getOptionList(optionListId); + return convertOptionsListToCodeListResult(optionListId, optionList, optionListError); + }; const handleUpload = (file: File) => { uploadOptionList(file); @@ -36,10 +37,10 @@ export function AppContentLibrary(): React.ReactElement { pages: { codeList: { props: { - codeLists: codeLists, + codeListIds: optionListIds, + onGetCodeList: handleGetOptionList, onUpdateCodeList: handleUpdate, onUploadCodeList: handleUpload, - fetchDataError: optionListsError, }, }, images: { diff --git a/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.test.ts b/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.test.ts deleted file mode 100644 index dbb6c5262db..00000000000 --- a/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CodeListWithMetadata } from '@studio/content-library'; -import { convertOptionListsToCodeLists } from './convertOptionListsToCodeLists'; -import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; - -describe('convertOptionListsToCodeLists', () => { - it('converts option lists map to code lists array correctly', () => { - const optionLists: OptionsLists = { - list1: [ - { label: 'Option 1', value: '1' }, - { label: 'Option 2', value: '2' }, - ], - list2: [ - { label: 'Option A', value: 'A' }, - { label: 'Option B', value: 'B' }, - ], - }; - const result: CodeListWithMetadata[] = convertOptionListsToCodeLists(optionLists); - expect(result).toEqual([ - { - title: 'list1', - codeList: [ - { label: 'Option 1', value: '1' }, - { label: 'Option 2', value: '2' }, - ], - }, - { - title: 'list2', - codeList: [ - { label: 'Option A', value: 'A' }, - { label: 'Option B', value: 'B' }, - ], - }, - ]); - }); - - it('returns an empty array when the input map is empty', () => { - const optionLists: OptionsLists = {}; - const result: CodeListWithMetadata[] = convertOptionListsToCodeLists(optionLists); - expect(result).toEqual([]); - }); -}); diff --git a/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.ts b/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.ts deleted file mode 100644 index 576e9fa0c0c..00000000000 --- a/frontend/app-development/features/appContentLibrary/utils/convertOptionListsToCodeLists.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { CodeListWithMetadata } from '@studio/content-library'; -import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; - -export function convertOptionListsToCodeLists(optionLists: OptionsLists): CodeListWithMetadata[] { - const codeLists = []; - Object.entries(optionLists).map((optionList) => - codeLists.push({ - codeList: optionList[1], - title: optionList[0], - }), - ); - return codeLists; -} diff --git a/frontend/app-development/features/appContentLibrary/utils/convertOptionsListToCodeListResult.test.ts b/frontend/app-development/features/appContentLibrary/utils/convertOptionsListToCodeListResult.test.ts new file mode 100644 index 00000000000..40ce5affcd6 --- /dev/null +++ b/frontend/app-development/features/appContentLibrary/utils/convertOptionsListToCodeListResult.test.ts @@ -0,0 +1,58 @@ +import type { OnGetCodeListResult } from '@studio/content-library'; +import { convertOptionsListToCodeListResult } from './convertOptionsListToCodeListResult'; +import type { OptionsList } from 'app-shared/types/api/OptionsLists'; + +describe('convertOptionsListToCodeListResult', () => { + it('converts option list to code list result correctly', () => { + const optionList: OptionsList = [ + { label: 'Option 1', value: '1' }, + { label: 'Option 2', value: '2' }, + ]; + const optionListId: string = 'optionListId'; + const result: OnGetCodeListResult = convertOptionsListToCodeListResult( + optionListId, + optionList, + false, + ); + expect(result).toEqual({ + codeListWithMetadata: { + title: optionListId, + codeList: [ + { label: 'Option 1', value: '1' }, + { label: 'Option 2', value: '2' }, + ], + }, + isError: false, + }); + }); + + it('sets isError to true in result when getOptionsList returns error', () => { + const optionListId: string = 'optionListId'; + const result: OnGetCodeListResult = convertOptionsListToCodeListResult( + optionListId, + undefined, + false, + ); + expect(result).toEqual({ + codeListWithMetadata: { + title: optionListId, + codeList: undefined, + }, + isError: false, + }); + }); + + it('returns a result with empty code list array when the input option list is empty', () => { + const optionList: OptionsList = []; + const optionListId: string = 'optionListId'; + const result: OnGetCodeListResult = convertOptionsListToCodeListResult( + optionListId, + optionList, + false, + ); + expect(result).toEqual({ + codeListWithMetadata: { title: optionListId, codeList: [] }, + isError: false, + }); + }); +}); diff --git a/frontend/app-development/features/appContentLibrary/utils/convertOptionsListToCodeListResult.ts b/frontend/app-development/features/appContentLibrary/utils/convertOptionsListToCodeListResult.ts new file mode 100644 index 00000000000..90a9d8c7fff --- /dev/null +++ b/frontend/app-development/features/appContentLibrary/utils/convertOptionsListToCodeListResult.ts @@ -0,0 +1,14 @@ +import type { OptionsList } from 'app-shared/types/api/OptionsLists'; +import type { OnGetCodeListResult } from '@studio/content-library'; + +export const convertOptionsListToCodeListResult = ( + optionListId: string, + optionsList: OptionsList, + isError: boolean, +): OnGetCodeListResult => { + const codeListWithMetadata = { title: optionListId, codeList: optionsList }; + return { + codeListWithMetadata: codeListWithMetadata, + isError: isError, + }; +}; diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index cf32f81e67c..f4161839676 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -19,7 +19,7 @@ "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.fetch_error": "Kunne ikke hente kodeliste 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", diff --git a/frontend/libs/studio-content-library/mocks/mockPagesConfig.ts b/frontend/libs/studio-content-library/mocks/mockPagesConfig.ts index 73ea05e2aaf..8fdf47c5908 100644 --- a/frontend/libs/studio-content-library/mocks/mockPagesConfig.ts +++ b/frontend/libs/studio-content-library/mocks/mockPagesConfig.ts @@ -1,15 +1,17 @@ import type { PagesConfig } from '../src/types/PagesProps'; +import type { OnGetCodeListResult } from '../src'; + +const onGetCodeListResult = (codeListId: string): OnGetCodeListResult => { + return { codeListWithMetadata: { title: codeListId, codeList: [] }, isError: false }; +}; export const mockPagesConfig: PagesConfig = { codeList: { props: { - codeLists: [ - { title: 'CodeList1', codeList: [] }, - { title: 'CodeList2', codeList: [] }, - ], + codeListIds: ['CodeList1', 'CodeList2'], + onGetCodeList: onGetCodeListResult, onUpdateCodeList: () => {}, onUploadCodeList: () => {}, - fetchDataError: false, }, }, images: { diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeList.test.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeList.test.tsx index 745d1f88c7d..3fc621d44c4 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeList.test.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeList.test.tsx @@ -1,16 +1,18 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import type { CodeListProps, CodeListWithMetadata } from './CodeList'; +import type { CodeListProps, OnGetCodeListResult } from './CodeList'; import { CodeList } from './CodeList'; import { textMock } from '@studio/testing/mocks/i18nMock'; const onUpdateCodeListMock = jest.fn(); const onUploadCodeListMock = jest.fn(); const codeListName = 'codeList'; -const codeListMock: CodeListWithMetadata = { - title: codeListName, - codeList: [{ value: 'value', label: 'label' }], -}; +const codeListMock = [{ value: 'value', label: 'label' }]; +const onGetCodeListMock: jest.Mock = jest.fn( + (codeListId: string) => { + return { codeListWithMetadata: { title: codeListId, codeList: codeListMock }, isError: false }; + }, +); describe('CodeList', () => { it('renders the codeList heading', () => { @@ -48,33 +50,15 @@ describe('CodeList', () => { const codeListAccordion = screen.getByRole('button', { name: codeListName }); expect(codeListAccordion).toBeInTheDocument(); }); - - it('renders error message if error fetching option lists occurred', () => { - renderCodeList({ fetchDataError: true }); - const errorMessage = screen.getByText(textMock('app_content_library.code_lists.fetch_error')); - expect(errorMessage).toBeInTheDocument(); - }); }); const defaultCodeListProps: CodeListProps = { - codeLists: [codeListMock], + codeListIds: [codeListName], + onGetCodeList: onGetCodeListMock, onUpdateCodeList: onUpdateCodeListMock, onUploadCodeList: onUploadCodeListMock, - fetchDataError: false, }; -const renderCodeList = ({ - codeLists, - onUpdateCodeList, - onUploadCodeList, - fetchDataError, -}: Partial = defaultCodeListProps) => { - render( - , - ); +const renderCodeList = (props: Partial = {}) => { + render(); }; 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 3838df10433..2c34fcf0496 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 @@ -1,5 +1,5 @@ import React from 'react'; -import { StudioHeading, StudioPageError } from '@studio/components'; +import { StudioHeading } from '@studio/components'; import type { CodeList as StudioComponentCodeList } from '@studio/components'; import { useTranslation } from 'react-i18next'; import { CodeListsActionsBar } from './CodeListsActionsBar'; @@ -12,32 +12,37 @@ export type CodeListWithMetadata = { title: string; }; +export type OnGetCodeListResult = { + codeListWithMetadata: CodeListWithMetadata | undefined; + isError: boolean; +}; + export type CodeListProps = { - codeLists: CodeListWithMetadata[]; + codeListIds: string[]; + onGetCodeList: (codeListId: string) => OnGetCodeListResult; onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void; onUploadCodeList: (uploadedCodeList: File) => void; - fetchDataError: boolean; }; export function CodeList({ - codeLists, + codeListIds, + onGetCodeList, onUpdateCodeList, onUploadCodeList, - fetchDataError, }: CodeListProps): React.ReactElement { const { t } = useTranslation(); - - if (fetchDataError) - return ; - return (
{t('app_content_library.code_lists.page_name')} - + - +
); } diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.test.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.test.tsx index 7f66073d566..14638238a09 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.test.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import type { CodeListsProps } from './CodeLists'; import { CodeLists } from './CodeLists'; import { textMock } from '@studio/testing/mocks/i18nMock'; -import type { CodeListWithMetadata } from '../CodeList'; +import type { CodeListWithMetadata, OnGetCodeListResult } from '../CodeList'; import userEvent from '@testing-library/user-event'; const codeListName = 'codeList'; @@ -11,6 +11,11 @@ const codeListMock: CodeListWithMetadata = { title: codeListName, codeList: [{ value: 'value', label: 'label' }], }; +const onGetCodeListMock: jest.Mock = jest.fn( + (codeListId: string) => { + return { codeListWithMetadata: { title: codeListId, codeList: codeListMock }, isError: false }; + }, +); describe('CodeLists', () => { it('renders the code list', () => { @@ -29,10 +34,23 @@ describe('CodeLists', () => { ); expect(placeholderAlert).toBeVisible(); }); + + it('renders error message if error fetching an option list occurred', () => { + const onGetCodeList = jest.fn((codeListId: string) => { + return { codeListWithMetadata: { title: codeListId, codeList: undefined }, isError: true }; + }); + renderCodeLists({ onGetCodeList }); + const errorMessage = screen.getByText(textMock('app_content_library.code_lists.fetch_error')); + expect(errorMessage).toBeInTheDocument(); + }); }); -const renderCodeLists = ( - { codeLists }: Partial = { codeLists: [codeListMock] }, -) => { - render(); +const defaultCodeListsProps: CodeListsProps = { + codeListIds: [codeListName], + onGetCodeList: onGetCodeListMock, + onUpdateCodeList: jest.fn(), +}; + +const renderCodeLists = (props: Partial = {}) => { + render(); }; diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.tsx b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.tsx index e25cd903039..4c8fab33bf0 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.tsx +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/CodeLists/CodeLists.tsx @@ -1,34 +1,43 @@ import React from 'react'; -import type { CodeListWithMetadata } from '../CodeList'; +import type { CodeListWithMetadata, OnGetCodeListResult } from '../CodeList'; import { Accordion, Alert } from '@digdir/designsystemet-react'; import { useTranslation } from 'react-i18next'; export type CodeListsProps = { - codeLists: CodeListWithMetadata[]; + codeListIds: string[]; + onGetCodeList: (codeListId: string) => OnGetCodeListResult; onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void; }; -export function CodeLists({ codeLists, onUpdateCodeList }: CodeListsProps) { - return codeLists.map((codeList) => ( - +export function CodeLists({ codeListIds, onGetCodeList, onUpdateCodeList }: CodeListsProps) { + return codeListIds.map((codeListId) => ( + )); } type CodeListProps = { - codeList: CodeListWithMetadata; + codeList: OnGetCodeListResult; onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void; }; function CodeList({ codeList, onUpdateCodeList }: CodeListProps) { const { t } = useTranslation(); + const codeListText = codeList.isError + ? t('app_content_library.code_lists.fetch_error') + : t('app_content_library.code_lists.edit_code_list_placeholder_text'); + return ( - {codeList.title} + {codeList.codeListWithMetadata.title} - - {t('app_content_library.code_lists.edit_code_list_placeholder_text')} + + {codeListText} diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/index.ts b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/index.ts index 12202f77851..7ac1e957dcb 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/index.ts +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeList/index.ts @@ -1,2 +1,2 @@ export { CodeList } from './CodeList'; -export type { CodeListWithMetadata, CodeListProps } from './CodeList'; +export type { CodeListWithMetadata, CodeListProps, OnGetCodeListResult } from './CodeList'; diff --git a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/index.ts b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/index.ts index 36261131269..5af55a7c923 100644 --- a/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/index.ts +++ b/frontend/libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/index.ts @@ -1 +1 @@ -export type { CodeListWithMetadata } from './CodeList'; +export type { CodeListWithMetadata, OnGetCodeListResult } from './CodeList'; diff --git a/frontend/libs/studio-content-library/src/index.ts b/frontend/libs/studio-content-library/src/index.ts index 7b2d6e43572..b4833887cd7 100644 --- a/frontend/libs/studio-content-library/src/index.ts +++ b/frontend/libs/studio-content-library/src/index.ts @@ -1,2 +1,2 @@ export { ResourceContentLibraryImpl } from './config/ContentResourceLibraryImpl'; -export type { CodeListWithMetadata } from './ContentLibrary/LibraryBody/pages'; +export type { CodeListWithMetadata, OnGetCodeListResult } from './ContentLibrary/LibraryBody/pages'; diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index e5198d24394..d9464237ce5 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -38,6 +38,7 @@ export const dataModelAddXsdFromRepoPath = (org, app, filePath) => `${basePath}/ // FormEditor export const ruleHandlerPath = (org, app, layoutSetName) => `${basePath}/${org}/${app}/app-development/rule-handler?${s({ layoutSetName })}`; // Get, Post export const widgetSettingsPath = (org, app) => `${basePath}/${org}/${app}/app-development/widget-settings`; // Get +export const optionListPath = (org, app, optionsListId) => `${basePath}/${org}/${app}/options/${optionsListId}`; // Get export const optionListsPath = (org, app) => `${basePath}/${org}/${app}/options/option-lists`; // Get export const optionListIdsPath = (org, app) => `${basePath}/${org}/${app}/app-development/option-list-ids`; // Get export const optionListUpdatePath = (org, app, optionsListId) => `${basePath}/${org}/${app}/options/${optionsListId}`; // Put diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index 57002072126..63caeb6f623 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -18,6 +18,7 @@ import { layoutSetsPath, layoutSettingsPath, optionListIdsPath, + optionListPath, optionListsPath, orgsListPath, accessListsPath, @@ -87,7 +88,7 @@ import type { Policy } from 'app-shared/types/Policy'; import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse'; import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse'; import type { MaskinportenScopes } from 'app-shared/types/MaskinportenScope'; -import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; +import type { OptionsLists, OptionsList } from 'app-shared/types/api/OptionsLists'; import type { LayoutSetsModel } from '../types/api/dto/LayoutSetsModel'; export const getIsLoggedInWithAnsattporten = () => get<{ isLoggedIn: boolean }>(authStatusAnsattporten()); @@ -114,6 +115,7 @@ export const getInstanceIdForPreview = (owner: string, app: string) => get get(layoutNamesPath(owner, app)); export const getLayoutSets = (owner: string, app: string) => get(layoutSetsPath(owner, app)); export const getLayoutSetsExtended = (owner: string, app: string) => get(layoutSetsPath(owner, app) + '/extended'); +export const getOptionList = (owner: string, app: string, optionListId: string) => get(optionListPath(owner, app, optionListId)); export const getOptionLists = (owner: string, app: string) => get(optionListsPath(owner, app)); export const getOptionListIds = (owner: string, app: string) => get(optionListIdsPath(owner, app)); export const getOrgList = () => get(orgListUrl()); diff --git a/frontend/packages/shared/src/hooks/queries/index.ts b/frontend/packages/shared/src/hooks/queries/index.ts index 1f3484f632b..09c6c6d0728 100644 --- a/frontend/packages/shared/src/hooks/queries/index.ts +++ b/frontend/packages/shared/src/hooks/queries/index.ts @@ -2,7 +2,9 @@ export { useAppMetadataQuery } from './useAppMetadataQuery'; export { useAppVersionQuery } from './useAppVersionQuery'; export { useDataModelsJsonQuery } from './useDataModelsJsonQuery'; export { useDataModelsXsdQuery } from './useDataModelsXsdQuery'; +export { useGetOptionListQuery } from './useGetOptionListQuery'; export { useInstanceIdQuery } from './useInstanceIdQuery'; +export { useOptionListQuery } from './useOptionListQuery'; export { useOptionListsQuery } from './useOptionListsQuery'; export { useRepoMetadataQuery } from './useRepoMetadataQuery'; export { useRepoPullQuery } from './useRepoPullQuery'; diff --git a/frontend/packages/shared/src/hooks/queries/useGetOptionListQuery.ts b/frontend/packages/shared/src/hooks/queries/useGetOptionListQuery.ts new file mode 100644 index 00000000000..f95be6522de --- /dev/null +++ b/frontend/packages/shared/src/hooks/queries/useGetOptionListQuery.ts @@ -0,0 +1,16 @@ +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { UseQueryResult, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { OptionsList } from 'app-shared/types/api/OptionsLists'; + +type GetOptionListResult = { data?: OptionsList; isError?: boolean }; +type UseGetOptionListQueryResult = (optionListId: string) => GetOptionListResult; + +export const useGetOptionListQuery = (org: string, app: string): UseGetOptionListQueryResult => { + const { getOptionList } = useServicesContext(); + return (optionListId: string) => + useQuery({ + queryKey: [QueryKey.OptionLists, org, app, optionListId], + queryFn: () => getOptionList(org, app, optionListId), + }); +}; diff --git a/frontend/packages/shared/src/hooks/queries/useOptionListQuery.test.ts b/frontend/packages/shared/src/hooks/queries/useOptionListQuery.test.ts new file mode 100644 index 00000000000..b7dd0c10632 --- /dev/null +++ b/frontend/packages/shared/src/hooks/queries/useOptionListQuery.test.ts @@ -0,0 +1,31 @@ +import { queriesMock } from 'app-shared/mocks/queriesMock'; +import { app, org } from '@studio/testing/testids'; +import { renderHookWithProviders } from 'app-shared/mocks/renderHookWithProviders'; +import { useOptionListQuery } from 'app-shared/hooks/queries/useOptionListQuery'; +import { waitFor } from '@testing-library/react'; +import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext'; +import type { OptionsList } from 'app-shared/types/api/OptionsLists'; + +const optionsListId = 'optionsListId'; + +describe('useOptionListQuery', () => { + it('calls getOptionList with the correct parameters', () => { + render(); + expect(queriesMock.getOptionList).toHaveBeenCalledWith(org, app, optionsListId); + }); + + it('getOptionList returns optionList as is', async () => { + const optionsList: OptionsList = [{ value: 'value', label: 'label' }]; + const getOptionList = jest.fn().mockImplementation(() => Promise.resolve(optionsList)); + const { current: currentResult } = await render({ getOptionList }); + expect(currentResult.data).toBe(optionsList); + }); +}); + +const render = async (queries: Partial = {}) => { + const { result } = renderHookWithProviders(() => useOptionListQuery(org, app, optionsListId), { + queries, + }); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + return result; +}; diff --git a/frontend/packages/shared/src/hooks/queries/useOptionListQuery.ts b/frontend/packages/shared/src/hooks/queries/useOptionListQuery.ts new file mode 100644 index 00000000000..ff2db725d25 --- /dev/null +++ b/frontend/packages/shared/src/hooks/queries/useOptionListQuery.ts @@ -0,0 +1,17 @@ +import { useServicesContext } from 'app-shared/contexts/ServicesContext'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; +import type { OptionsList } from 'app-shared/types/api/OptionsLists'; + +export const useOptionListQuery = ( + org: string, + app: string, + optionListId: string, +): UseQueryResult => { + const { getOptionList } = useServicesContext(); + return useQuery({ + queryKey: [QueryKey.OptionLists, org, app], + queryFn: () => getOptionList(org, app, optionListId), + }); +}; diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 6efd4b3f207..cc0957bfe1a 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -69,7 +69,7 @@ import type { DeploymentsResponse } from 'app-shared/types/api/DeploymentsRespon import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse'; import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse'; import type { MaskinportenScope } from 'app-shared/types/MaskinportenScope'; -import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; +import type { OptionsList, OptionsLists } from 'app-shared/types/api/OptionsLists'; import type { LayoutSetsModel } from '../types/api/dto/LayoutSetsModel'; import { layoutSetsExtendedMock } from '@altinn/ux-editor/testing/layoutSetsMock'; @@ -109,6 +109,7 @@ export const queriesMock: ServicesContextProps = { .fn() .mockImplementation(() => Promise.resolve(layoutSetsExtendedMock)), getOptionListIds: jest.fn().mockImplementation(() => Promise.resolve([])), + getOptionList: jest.fn().mockImplementation(() => Promise.resolve([])), getOptionLists: jest.fn().mockImplementation(() => Promise.resolve({})), getOrgList: jest.fn().mockImplementation(() => Promise.resolve(orgList)), getOrganizations: jest.fn().mockImplementation(() => Promise.resolve([])), diff --git a/frontend/packages/shared/src/types/api/OptionsLists.ts b/frontend/packages/shared/src/types/api/OptionsLists.ts index fa3f5d6d7da..35b59e98882 100644 --- a/frontend/packages/shared/src/types/api/OptionsLists.ts +++ b/frontend/packages/shared/src/types/api/OptionsLists.ts @@ -1,3 +1,5 @@ import type { Option } from 'app-shared/types/Option'; -export type OptionsLists = Record; +export type OptionsList = Option[]; + +export type OptionsLists = Record; diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/OptionListEditor.test.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/OptionListEditor.test.tsx index 66cd525659f..b1460f13cca 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/OptionListEditor.test.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/OptionListEditor.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; -import type { OptionsLists } from 'app-shared/types/api/OptionsLists'; -import type { Option } from 'app-shared/types/Option'; +import type { OptionsList, OptionsLists } from 'app-shared/types/api/OptionsLists'; import { OptionListEditor } from './OptionListEditor'; import { textMock } from '@studio/testing/mocks/i18nMock'; import { renderWithProviders } from '../../../../../../testing/mocks'; @@ -11,18 +10,16 @@ import { app, org } from '@studio/testing/testids'; // Test data: const mockComponentOptionsId = 'options'; -const getOptionLists = jest.fn().mockImplementation(() => Promise.resolve(apiResult)); +const getOptionList = jest.fn().mockImplementation(() => Promise.resolve(apiResult)); const updateOptionList = jest .fn() - .mockImplementation(() => Promise.resolve([{ value: '', label: '' }])); - -const apiResult: OptionsLists = { - options: [ - { value: 'test', label: 'label text', description: 'description', helpText: 'help text' }, - { value: 2, label: 'label number', description: null, helpText: null }, - { value: true, label: 'label boolean', description: null, helpText: null }, - ], -}; + .mockImplementation(() => Promise.resolve([{ value: '', label: '' }])); + +const apiResult: OptionsList = [ + { value: 'test', label: 'label text', description: 'description', helpText: 'help text' }, + { value: 2, label: 'label number', description: null, helpText: null }, + { value: true, label: 'label boolean', description: null, helpText: null }, +]; describe('OptionListEditor', () => { afterEach(jest.clearAllMocks); @@ -30,7 +27,7 @@ describe('OptionListEditor', () => { it('should render a spinner when there is no data', () => { renderOptionListEditor({ queries: { - getOptionLists: jest.fn().mockImplementation(() => Promise.resolve({})), + getOptionList: jest.fn().mockImplementation(() => Promise.resolve([])), }, }); @@ -42,7 +39,7 @@ describe('OptionListEditor', () => { it('should render an error message when api throws an error', async () => { await renderOptionListEditorAndWaitForSpinnerToBeRemoved({ queries: { - getOptionLists: jest.fn().mockRejectedValueOnce(new Error('Error')), + getOptionList: jest.fn().mockRejectedValueOnce(new Error('Error')), }, }); @@ -99,7 +96,7 @@ describe('OptionListEditor', () => { it('should call updateOptionList with correct parameters when closing Dialog', async () => { const user = userEvent.setup(); await renderOptionListEditorAndWaitForSpinnerToBeRemoved(); - const expectedResultAfterEdit: Option[] = [ + const expectedResultAfterEdit: OptionsList = [ { value: 'test', label: 'label text', description: 'description', helpText: 'help text' }, { value: 2, label: 'label number', description: 'test', helpText: null }, { value: true, label: 'label boolean', description: null, helpText: null }, @@ -130,7 +127,7 @@ const openModal = async (user: UserEvent) => { const renderOptionListEditor = ({ previewContextProps = {}, queries = {} } = {}) => { return renderWithProviders(, { - queries: { getOptionLists, updateOptionList, ...queries }, + queries: { getOptionList, updateOptionList, ...queries }, queryClient: createQueryClientMock(), previewContextProps, }); diff --git a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/OptionListEditor.tsx b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/OptionListEditor.tsx index 8642850f67a..95fbaf04003 100644 --- a/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/OptionListEditor.tsx +++ b/frontend/packages/ux-editor/src/components/config/editModal/EditOptions/OptionTabs/EditOptionList/OptionListEditor.tsx @@ -12,11 +12,11 @@ import { TableIcon } from '@studio/icons'; import { useDebounce } from '@studio/hooks'; import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams'; import { useUpdateOptionListMutation } from 'app-shared/hooks/mutations/useUpdateOptionListMutation'; -import { useOptionListsQuery } from 'app-shared/hooks/queries/useOptionListsQuery'; import { useOptionListEditorTexts } from '../hooks/useOptionListEditorTexts'; import { usePreviewContext } from 'app-development/contexts/PreviewContext'; import classes from './OptionListEditor.module.css'; import { AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS } from 'app-shared/constants'; +import { useOptionListQuery } from 'app-shared/hooks/queries'; type OptionListEditorProps = { optionsId: string; @@ -25,7 +25,7 @@ type OptionListEditorProps = { export function OptionListEditor({ optionsId }: OptionListEditorProps): React.ReactNode { const { t } = useTranslation(); const { org, app } = useStudioEnvironmentParams(); - const { data: optionsListMap, status } = useOptionListsQuery(org, app); + const { data: optionsList, status } = useOptionListQuery(org, app, optionsId); switch (status) { case 'pending': @@ -37,9 +37,7 @@ export function OptionListEditor({ optionsId }: OptionListEditorProps): React.Re {t('ux_editor.modal_properties_error_message')} ); case 'success': { - return ( - - ); + return ; } } }