Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 14508/upload code list for org #14629

Merged
merged 7 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,47 @@ import { screen } from '@testing-library/react';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { ProviderData } from '../../testing/mocks';
import { renderWithProviders } from '../../testing/mocks';
import userEvent, { type UserEvent } from '@testing-library/user-event';
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';
import { SelectedContextType } from '../../context/HeaderContext';
import { Route, Routes } from 'react-router-dom';

const uploadCodeListButtonTextMock = 'Upload Code List';
const codeListNameMock = '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: ({ onUploadCodeList }: any) => (
<div>
<button
onClick={() =>
onUploadCodeList(
new File(['test'], `${codeListNameMock}.json`, { type: 'application/json' }),
)
}
>
{uploadCodeListButtonTextMock}
</button>
</div>
),
}),
);

jest.mock('react-router-dom', () => jest.requireActual('react-router-dom')); // Todo: Remove this when we have removed the global mock: https://github.com/Altinn/altinn-studio/issues/14597

describe('OrgContentLibrary', () => {
afterEach(jest.clearAllMocks);

it.each([SelectedContextType.None, SelectedContextType.All, SelectedContextType.Self])(
'renders alert and omits library content when context is %s',
(selectedContext) => {
Expand Down Expand Up @@ -55,8 +90,68 @@ 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();
});
});

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);
};

function renderOrgContentLibrary(providerData: ProviderData): RenderResult {
return renderWithProviders(
<Routes>
Expand All @@ -65,3 +160,16 @@ function renderOrgContentLibrary(providerData: ProviderData): RenderResult {
providerData,
);
}

function renderOrgContentLibraryWithCodeLists(providerData: ProviderData): void {
const queryClient = createQueryClientWithOptionsDataList(codeListsDataMock);
renderOrgContentLibrary({ ...providerData, queryClient });
}

function createQueryClientWithOptionsDataList(
codeListDataList: CodeListData[] | undefined,
): QueryClient {
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.OrgCodeLists, org], codeListDataList);
return queryClient;
}
36 changes: 33 additions & 3 deletions frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
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 { StudioAlert, StudioCenter, StudioParagraph } from '@studio/components';
import { useTranslation } from 'react-i18next';
import { isOrg } from './utils';

export function OrgContentLibrary(): ReactElement {
Expand All @@ -16,6 +21,9 @@
}

function OrgContentLibraryWithContext(): ReactElement {
const selectedContext = useSelectedContext();
const handleUpload = useUploadCodeList(selectedContext);

const { getContentResourceLibrary } = new ResourceContentLibraryImpl({
pages: {
codeList: {
Expand All @@ -24,7 +32,7 @@
onDeleteCodeList: () => {},
onUpdateCodeListId: () => {},
onUpdateCodeList: () => {},
onUploadCodeList: () => {},
onUploadCodeList: handleUpload,
},
},
},
Expand All @@ -46,3 +54,25 @@
</StudioCenter>
);
}

function useUploadCodeList(org: string): (file: File) => void {
const { mutate: uploadCodeList } = useUploadOrgCodeListMutation(org, {
hideDefaultError: (error: AxiosError<ApiError>) => isErrorUnknown(error),

Check warning on line 60 in frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/dashboard/pages/OrgContentLibrary/OrgContentLibrary.tsx#L60

Added line #L60 was not covered by tests
});
const { t } = useTranslation();

return useCallback(
(file: File) =>
uploadCodeList(file, {
onSuccess: () => {
toast.success(t('dashboard.org_library.code_list_upload_success'));
},
onError: (error: AxiosError<ApiError>) => {
if (isErrorUnknown(error)) {
toast.error(t('dashboard.org_library.code_list_upload_generic_error'));
}
},
}),
[uploadCodeList, t],
);
}
2 changes: 2 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
ErlingHauan marked this conversation as resolved.
Show resolved Hide resolved
"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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -18,5 +19,6 @@ export const useUploadOrgCodeListMutation = (org: string) => {
onSuccess: (newData: CodeListsResponse) => {
queryClient.setQueryData([QueryKey.OrgCodeLists, org], newData);
},
meta,
});
};