From 1ab24c876dff5198eb3b601c7abfeb8107b84977 Mon Sep 17 00:00:00 2001 From: wrt95 Date: Tue, 11 Feb 2025 11:28:13 +0100 Subject: [PATCH 1/3] feat: upload code list for org --- .../OrgContentLibrary.test.tsx | 117 ++++++++++++++++++ .../OrgContentLibrary/OrgContentLibrary.tsx | 37 +++++- frontend/language/src/nb.json | 2 + .../mutations/useUploadOrgCodeListMutation.ts | 4 +- 4 files changed, 157 insertions(+), 3 deletions(-) diff --git a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx index 3e72b7702be..6a912654e23 100644 --- a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx +++ b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx @@ -3,8 +3,50 @@ import { OrgContentLibrary } from './OrgContentLibrary'; import { screen } from '@testing-library/react'; import { textMock } from '@studio/testing/mocks/i18nMock'; import { renderWithProviders } from '../../testing/mocks'; +import userEvent, { type UserEvent } from '@testing-library/user-event'; +import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext'; +import type { QueryClient } from '@tanstack/react-query'; +import { createQueryClientMock } from 'app-shared/mocks/queryClientMock'; +import { QueryKey } from 'app-shared/types/QueryKey'; +import { org } from '@studio/testing/testids'; +import type { CodeListData } from '@studio/content-library'; +import type { CodeList } from '@studio/components'; +import { queriesMock } from 'app-shared/mocks/queriesMock'; + +const uploadCodeListButtonTextMock = 'Upload Code List'; +const codeListNameMock = 'codeListNameMock'; +const codeListMock: CodeList = [{ value: '', label: '' }]; +const codeListsDataMock: CodeListData[] = [{ title: codeListNameMock, data: codeListMock }]; + +jest.mock( + '../../../libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage', + () => ({ + CodeListPage: ({ onUploadCodeList }: any) => ( +
+ +
+ ), + }), +); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + selectedContext: 'testOrg', + }), +})); describe('OrgContentLibrary', () => { + afterEach(jest.clearAllMocks); + it('renders the library title', () => { renderWithProviders(); const libraryTitle = screen.getByRole('heading', { @@ -32,4 +74,79 @@ describe('OrgContentLibrary', () => { }); expect(codeListMenuElement).toBeInTheDocument(); }); + + it('calls onUploadCodeList when onUploadCodeList is triggered', async () => { + const user = userEvent.setup(); + renderOrgContentLibraryWithCodeLists(); + await goToLibraryPage(user, 'code_lists'); + const uploadCodeListButton = screen.getByRole('button', { name: uploadCodeListButtonTextMock }); + await user.click(uploadCodeListButton); + expect(queriesMock.uploadCodeListForOrg).toHaveBeenCalledTimes(1); + expect(queriesMock.uploadCodeListForOrg).toHaveBeenCalledWith(org, expect.any(FormData)); + }); + + it('renders success toast when onUploadCodeList is called successfully', async () => { + const user = userEvent.setup(); + renderOrgContentLibraryWithCodeLists(); + await goToLibraryPage(user, 'code_lists'); + const uploadCodeListButton = screen.getByRole('button', { name: uploadCodeListButtonTextMock }); + await user.click(uploadCodeListButton); + const successToastMessage = screen.getByText( + textMock('dashboard.org_library.code_list_upload_success'), + ); + expect(successToastMessage).toBeInTheDocument(); + }); + + it('renders error toast when onUploadCodeList is rejected with unknown error code', async () => { + const user = userEvent.setup(); + const uploadCodeListForOrg = jest + .fn() + .mockImplementation(() => Promise.reject({ response: {} })); + renderOrgContentLibraryWithCodeLists({ queries: { uploadCodeListForOrg } }); + await goToLibraryPage(user, 'code_lists'); + const uploadCodeListButton = screen.getByRole('button', { name: uploadCodeListButtonTextMock }); + await user.click(uploadCodeListButton); + const errorToastMessage = screen.getByText( + textMock('dashboard.org_library.code_list_upload_generic_error'), + ); + expect(errorToastMessage).toBeInTheDocument(); + }); }); + +const getLibraryPageTile = (libraryPage: string) => + screen.getByText(textMock(`app_content_library.${libraryPage}.page_name`)); + +const goToLibraryPage = async (user: UserEvent, libraryPage: string) => { + const libraryPageNavTile = getLibraryPageTile(libraryPage); + await user.click(libraryPageNavTile); +}; + +type RenderOrgContentLibraryProps = { + queries?: Partial; + queryClient?: QueryClient; +}; + +const renderAppContentLibrary = ({ + queries = {}, + queryClient = createQueryClientMock(), +}: RenderOrgContentLibraryProps = {}): void => { + renderWithProviders(, { + queries, + queryClient, + }); +}; + +function renderOrgContentLibraryWithCodeLists( + props?: Omit, +): void { + const queryClient = createQueryClientWithOptionsDataList(codeListsDataMock); + renderAppContentLibrary({ ...props, queryClient }); +} + +function createQueryClientWithOptionsDataList( + codeListDataList: CodeListData[] | undefined, +): QueryClient { + const queryClient = createQueryClientMock(); + queryClient.setQueryData([QueryKey.OrgCodeLists, org], codeListDataList); + return queryClient; +} diff --git a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx index 5c35f293937..a4a192c3235 100644 --- a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx +++ b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx @@ -1,8 +1,19 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useCallback } from 'react'; import { ResourceContentLibraryImpl } from '@studio/content-library'; +import { useTranslation } from 'react-i18next'; +import { isErrorUnknown } from 'app-shared/utils/ApiErrorUtils'; +import type { ApiError } from 'app-shared/types/api/ApiError'; +import { useUploadOrgCodeListMutation } from 'app-shared/hooks/mutations/useUploadOrgCodeListMutation'; +import { toast } from 'react-toastify'; +import type { AxiosError } from 'axios'; +import { useSelectedContext } from '../../hooks/useSelectedContext'; export function OrgContentLibrary(): ReactElement { + const org = useSelectedContext(); + + const handleUpload = useUploadCodeList(org); + const { getContentResourceLibrary } = new ResourceContentLibraryImpl({ pages: { codeList: { @@ -11,7 +22,7 @@ export function OrgContentLibrary(): ReactElement { onDeleteCodeList: () => {}, onUpdateCodeListId: () => {}, onUpdateCodeList: () => {}, - onUploadCodeList: () => {}, + onUploadCodeList: handleUpload, }, }, }, @@ -19,3 +30,25 @@ export function OrgContentLibrary(): ReactElement { return
{getContentResourceLibrary()}
; } + +function useUploadCodeList(org: string): (file: File) => void { + const { mutate: uploadCodeList } = useUploadOrgCodeListMutation(org, { + hideDefaultError: (error: AxiosError) => isErrorUnknown(error), + }); + const { t } = useTranslation(); + + return useCallback( + (file: File) => + uploadCodeList(file, { + onSuccess: () => { + toast.success(t('dashboard.org_library.code_list_upload_success')); + }, + onError: (error: AxiosError) => { + if (isErrorUnknown(error)) { + toast.error(t('dashboard.org_library.code_list_upload_generic_error')); + } + }, + }), + [uploadCodeList, t], + ); +} diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index 5973c8be3d4..4613b364e1c 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -232,6 +232,8 @@ "dashboard.open_repository": "Åpne repository", "dashboard.org_apps": "{{orgName}}s apper", "dashboard.org_data_models": "{{orgName}}s datamodeller", + "dashboard.org_library.code_list_upload_success": "Filen ble lastet opp.", + "dashboard.org_library.fetch_error": "Det har oppstått et problem ved henting av data til biblioteket.", "dashboard.org_resources": "{{orgName}}s ressurser", "dashboard.resource_contact_description": "Se oversikt over hvordan du kommer i kontakt med oss i Altinn Studio.", "dashboard.resource_contact_label": "Kontakt oss", diff --git a/frontend/packages/shared/src/hooks/mutations/useUploadOrgCodeListMutation.ts b/frontend/packages/shared/src/hooks/mutations/useUploadOrgCodeListMutation.ts index 4333880bb50..13197f015cb 100644 --- a/frontend/packages/shared/src/hooks/mutations/useUploadOrgCodeListMutation.ts +++ b/frontend/packages/shared/src/hooks/mutations/useUploadOrgCodeListMutation.ts @@ -1,10 +1,11 @@ +import type { MutationMeta } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useServicesContext } from '../../contexts/ServicesContext'; import { QueryKey } from '../../types/QueryKey'; import type { CodeListsResponse } from '../../types/api/CodeListsResponse'; import { FileUtils } from '@studio/pure-functions'; -export const useUploadOrgCodeListMutation = (org: string) => { +export const useUploadOrgCodeListMutation = (org: string, meta?: MutationMeta) => { const queryClient = useQueryClient(); const { uploadCodeListForOrg } = useServicesContext(); @@ -18,5 +19,6 @@ export const useUploadOrgCodeListMutation = (org: string) => { onSuccess: (newData: CodeListsResponse) => { queryClient.setQueryData([QueryKey.OrgCodeLists, org], newData); }, + meta, }); }; From 25b3767f97a5e0b083660b7ac87e6533e5a86a09 Mon Sep 17 00:00:00 2001 From: wrt95 Date: Tue, 11 Feb 2025 11:48:01 +0100 Subject: [PATCH 2/3] feat: adding another test --- .../OrgContentLibrary/OrgContentLibrary.test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx index 6a912654e23..980abb15c13 100644 --- a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx +++ b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx @@ -113,6 +113,18 @@ describe('OrgContentLibrary', () => { }); }); +it('calls onUploadCodeList and hides default error when handleUpload is triggered', async () => { + const user = userEvent.setup(); + renderOrgContentLibraryWithCodeLists(); + await goToLibraryPage(user, 'code_lists'); + const uploadCodeListButton = screen.getByRole('button', { name: uploadCodeListButtonTextMock }); + await user.click(uploadCodeListButton); + expect(queriesMock.uploadCodeListForOrg).toHaveBeenCalledTimes(1); + expect(queriesMock.uploadCodeListForOrg).toHaveBeenCalledWith(org, expect.any(FormData)); + const hideDefaultError = screen.queryByText(textMock('dashboard.org_library.default_error')); + expect(hideDefaultError).not.toBeInTheDocument(); +}); + const getLibraryPageTile = (libraryPage: string) => screen.getByText(textMock(`app_content_library.${libraryPage}.page_name`)); From f0904897dc7c1e56559741a7e9399561fa16bf6a Mon Sep 17 00:00:00 2001 From: wrt95 Date: Tue, 11 Feb 2025 12:35:11 +0100 Subject: [PATCH 3/3] fead: adding more tests --- .../OrgContentLibrary.test.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx index 980abb15c13..1c6fca2398f 100644 --- a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx +++ b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx @@ -111,18 +111,18 @@ describe('OrgContentLibrary', () => { ); expect(errorToastMessage).toBeInTheDocument(); }); -}); -it('calls onUploadCodeList and hides default error when handleUpload is triggered', async () => { - const user = userEvent.setup(); - renderOrgContentLibraryWithCodeLists(); - await goToLibraryPage(user, 'code_lists'); - const uploadCodeListButton = screen.getByRole('button', { name: uploadCodeListButtonTextMock }); - await user.click(uploadCodeListButton); - expect(queriesMock.uploadCodeListForOrg).toHaveBeenCalledTimes(1); - expect(queriesMock.uploadCodeListForOrg).toHaveBeenCalledWith(org, expect.any(FormData)); - const hideDefaultError = screen.queryByText(textMock('dashboard.org_library.default_error')); - expect(hideDefaultError).not.toBeInTheDocument(); + it('calls onUploadCodeList and hides default error when handleUpload is triggered', async () => { + const user = userEvent.setup(); + renderOrgContentLibraryWithCodeLists(); + await goToLibraryPage(user, 'code_lists'); + const uploadCodeListButton = screen.getByRole('button', { name: uploadCodeListButtonTextMock }); + await user.click(uploadCodeListButton); + expect(queriesMock.uploadCodeListForOrg).toHaveBeenCalledTimes(1); + expect(queriesMock.uploadCodeListForOrg).toHaveBeenCalledWith(org, expect.any(FormData)); + const hideDefaultError = screen.queryByText(textMock('dashboard.org_library.default_error')); + expect(hideDefaultError).not.toBeInTheDocument(); + }); }); const getLibraryPageTile = (libraryPage: string) =>