diff --git a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx index 80cd830252f..579ffcadc0b 100644 --- a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx +++ b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx @@ -16,16 +16,27 @@ import { queriesMock } from 'app-shared/mocks/queriesMock'; import { SelectedContextType } from '../../context/HeaderContext'; import { Route, Routes } from 'react-router-dom'; -const deleteCodeListButtonTextMock = 'Delete Code List'; -const codeListNameMock = 'codeListNameMock'; +const uploadCodeListButtonTextMock: string = 'Upload Code List'; +const deleteCodeListButtonTextMock: string = 'Delete Code List'; +const codeListNameMock: string = 'codeListNameMock'; const codeListMock: CodeList = [{ value: '', label: '' }]; const codeListsDataMock: CodeListData[] = [{ title: codeListNameMock, data: codeListMock }]; +const mockOrgPath: string = '/testOrg'; jest.mock( '../../../libs/studio-content-library/src/ContentLibrary/LibraryBody/pages/CodeListPage', () => ({ - CodeListPage: ({ onDeleteCodeList }: any) => ( + CodeListPage: ({ onDeleteCodeList, onUploadCodeList }: any) => (
+ @@ -84,6 +95,58 @@ describe('OrgContentLibrary', () => { expect(codeListMenuElement).toBeInTheDocument(); }); + it('calls onUploadCodeList when onUploadCodeList is triggered', async () => { + const user = userEvent.setup(); + renderOrgContentLibraryWithCodeLists({ initialEntries: [mockOrgPath] }); + 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({ initialEntries: [mockOrgPath] }); + 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({ + initialEntries: [mockOrgPath], + 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(); + }); + + it('calls onUploadCodeList and hides default error when handleUpload is triggered', async () => { + const user = userEvent.setup(); + renderOrgContentLibraryWithCodeLists({ initialEntries: [mockOrgPath] }); + 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 deleteCodeListForOrg when onDeleteCodeList is triggered', async () => { const user = userEvent.setup(); renderOrgContentLibraryWithCodeLists({ initialEntries: ['/testOrg'] }); diff --git a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx index 1fb4a3fc497..78fbbc36843 100644 --- a/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx +++ b/frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx @@ -1,10 +1,15 @@ 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'; import { useDeleteOrgCodeListMutation } from 'app-shared/hooks/mutations/useDeleteOrgCodeListMutation'; import { StudioAlert, StudioCenter, StudioParagraph } from '@studio/components'; -import { useTranslation } from 'react-i18next'; import { isOrg } from './utils'; export function OrgContentLibrary(): ReactElement { @@ -19,6 +24,9 @@ export function OrgContentLibrary(): ReactElement { function OrgContentLibraryWithContext(): ReactElement { const selectedContext = useSelectedContext(); + + const handleUpload = useUploadCodeList(selectedContext); + const { mutate: deleteCodeList } = useDeleteOrgCodeListMutation(selectedContext); const { getContentResourceLibrary } = new ResourceContentLibraryImpl({ @@ -29,7 +37,7 @@ function OrgContentLibraryWithContext(): ReactElement { onDeleteCodeList: deleteCodeList, onUpdateCodeListId: () => {}, onUpdateCodeList: () => {}, - onUploadCodeList: () => {}, + onUploadCodeList: handleUpload, }, }, }, @@ -51,3 +59,25 @@ function ContextWithoutLibraryAccess(): ReactElement { ); } + +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 d37dd640fb4..5bfc04d5b9f 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -234,6 +234,8 @@ "dashboard.org_data_models": "{{orgName}}s datamodeller", "dashboard.org_library.alert_no_org_selected": "For å vise biblioteket, må du velge en organisasjon i menyen øverst til høyre.", "dashboard.org_library.alert_no_org_selected_no_access": "Hvis du ikke har tilgang til noen organisasjoner, må du be om tilgang fra en tjenesteeier.", + "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, }); };