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: query/mutation hooks for org code lists #14540

Merged
merged 20 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b828f76
create useOrgCodeListsQuery
ErlingHauan Jan 28, 2025
bbf8ac5
create useCreateOrgCodeListMutation
ErlingHauan Jan 28, 2025
b6cffc6
Merge branch 'org-library-mvp' of https://github.com/Altinn/altinn-st…
ErlingHauan Jan 28, 2025
b15cfa2
create useCreateOrgCodeListMutation
ErlingHauan Jan 28, 2025
1663ea1
create useUpdateOrgCodeListMutation
ErlingHauan Jan 28, 2025
46d20ab
create useUploadOrgCodeListMutation
ErlingHauan Jan 28, 2025
d70ccf0
create useDeleteOrgCodeListMutation
ErlingHauan Jan 28, 2025
334b7a5
Merge branch 'org-library-mvp' into 14505-react-query-hooks-for-code-…
TomasEng Jan 29, 2025
c99f3e2
Merge branch 'org-library-mvp' of https://github.com/Altinn/altinn-st…
ErlingHauan Jan 30, 2025
a7c0f2b
fix some PR comments (correct import paths, clearAllMocks, destructur…
ErlingHauan Jan 31, 2025
97d8c5d
Merge branch '14505-react-query-hooks-for-code-lists' of https://gith…
ErlingHauan Jan 31, 2025
82e709d
improve caching for rest of mutations
ErlingHauan Jan 31, 2025
19110ba
Make mutations expect to receive all codelists in org as response
ErlingHauan Feb 3, 2025
40a613e
Remove unused ArrayUtil class
ErlingHauan Feb 3, 2025
30e8eda
Add test and index.ts for FileUtils
ErlingHauan Feb 3, 2025
2e55691
upload mutation test: convert data to string before creating file
ErlingHauan Feb 4, 2025
3755a90
fix import paths
ErlingHauan Feb 4, 2025
209d7ce
Merge branch 'org-library-mvp' into 14505-react-query-hooks-for-code-…
ErlingHauan Feb 5, 2025
f4b6569
fix PR comments
ErlingHauan Feb 6, 2025
aa398fd
Merge branch '14505-react-query-hooks-for-code-lists' of https://gith…
ErlingHauan Feb 6, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FileUtils } from './FileUtils';

describe('FileUtils', () => {
describe('convertToFormData', () => {
it('should append the file to FormData under the key "file"', () => {
const fileContent = 'Test file contents';
const fileName = 'test.txt';
const fileType = 'text/plain';
const file = new File([fileContent], fileName, { type: fileType });

const formData = FileUtils.convertToFormData(file);

const retrievedFile = formData.get('file');
expect(retrievedFile).toBe(file);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class FileUtils {
static convertToFormData = (file: File): FormData => {
const formData = new FormData();
formData.append('file', file);
return formData;
};
}
1 change: 1 addition & 0 deletions frontend/libs/studio-pure-functions/src/FileUtils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FileUtils } from './FileUtils';
1 change: 1 addition & 0 deletions frontend/libs/studio-pure-functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './ArrayUtils';
export * from './BlobDownloader';
export * from './DateUtils';
export * from './FileNameUtils';
export * from './FileUtils';
export * from './NumberUtils';
export * from './ObjectUtils';
export * from './ScopedStorage';
Expand Down
13 changes: 7 additions & 6 deletions frontend/packages/shared/src/api/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import {
dataTypePath,
optionListPath,
undeployAppFromEnvPath,
orgCodeListPath,
orgCodeListUploadPath,
} from 'app-shared/api/paths';
import type { AddLanguagePayload } from 'app-shared/types/api/AddLanguagePayload';
import type { AddRepoParams } from 'app-shared/types/api';
Expand All @@ -75,8 +77,8 @@ import type { FormLayoutRequest } from 'app-shared/types/api/FormLayoutRequest';
import type { Option } from 'app-shared/types/Option';
import type { MaskinportenScopes } from 'app-shared/types/MaskinportenScope';
import type { DataType } from '../types/DataType';
import type { CodeListData } from 'app-shared/types/CodeListData';
import type { CodeList } from 'app-shared/types/CodeList';
import type { CodeListsResponse } from 'app-shared/types/api/CodeListsResponse';
import { textResourcesMock } from 'app-shared/mocks/textResourcesMock';

const headers = {
Expand Down Expand Up @@ -172,11 +174,10 @@ export const updateProcessDataTypes = (org: string, app: string, dataTypesChange
export const updateSelectedMaskinportenScopes = (org: string, app: string, appScopesUpsertRequest: MaskinportenScopes) => put(selectedMaskinportenScopesPath(org, app), appScopesUpsertRequest);

// Organisation library code lists:
// Todo: Replace these with real API calls when endpoints are ready. https://github.com/Altinn/altinn-studio/issues/14505
export const createCodeListForOrg = async (org: string, payload: CodeListData): Promise<void> => Promise.resolve();
export const updateCodeListForOrg = async (org: string, codeListId: string, payload: CodeList): Promise<void> => Promise.resolve();
export const deleteCodeListForOrg = async (org: string, codeListId: string): Promise<void> => Promise.resolve();
export const uploadCodeListForOrg = async (org: string, app: string, payload: FormData): Promise<void> => Promise.resolve();
export const createCodeListForOrg = async (org: string, codeListId: string, payload: CodeList): Promise<CodeListsResponse> => post(orgCodeListPath(org, codeListId), payload);
export const updateCodeListForOrg = async (org: string, codeListId: string, payload: CodeList): Promise<CodeListsResponse> => put(orgCodeListPath(org, codeListId), payload);
export const deleteCodeListForOrg = async (org: string, codeListId: string): Promise<CodeListsResponse> => del(orgCodeListPath(org, codeListId));
export const uploadCodeListForOrg = async (org: string, payload: FormData): Promise<CodeListsResponse> => post(orgCodeListUploadPath(org), payload);

// Organisation text resources:
// Todo: Replace these with real API calls when endpoints are ready. https://github.com/Altinn/altinn-studio/issues/14503
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const languagesPath = (org, app) => `${basePath}/${org}/${app}/languages`

// Library - org-level
export const orgCodeListsPath = (org) => `${basePath}/${org}/code-lists`; // Get
export const orgCodeListPath = (org, optionsListId) => `${basePath}/${org}/code-lists/${optionsListId}`; // Post, Put, Delete
export const orgCodeListPath = (org, codeListId) => `${basePath}/${org}/code-lists/${codeListId}`; // Post, Put, Delete
export const orgCodeListUploadPath = (org) => `${basePath}/${org}/code-lists/upload`; // Post

// Organizations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { queriesMock } from 'app-shared/mocks/queriesMock';
import { app, org } from '@studio/testing/testids';
import { useAddOptionListMutation } from './useAddOptionListMutation';
import { renderHookWithProviders } from 'app-shared/mocks/renderHookWithProviders';
import { FileUtils } from '@studio/pure-functions';

// Test data:
const file = new File(['hello'], 'hello.json', { type: 'text/json' });
const formData = new FormData();
formData.append('file', file);
const formData = FileUtils.convertToFormData(file);

describe('useAddOptionsMutation', () => {
afterEach(jest.clearAllMocks);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { MutationMeta } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
import { FileUtils } from '@studio/pure-functions';

export const useAddOptionListMutation = (org: string, app: string, meta?: MutationMeta) => {
const queryClient = useQueryClient();
const { uploadOptionList } = useServicesContext();

const mutationFn = (file: File) => {
const formData = createFormDataWithFile(file);
const formData = FileUtils.convertToFormData(file);
return uploadOptionList(org, app, formData);
};

Expand All @@ -21,9 +22,3 @@ export const useAddOptionListMutation = (org: string, app: string, meta?: Mutati
meta,
});
};

const createFormDataWithFile = (file: File): FormData => {
const formData = new FormData();
formData.append('file', file);
return formData;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { renderHookWithProviders } from '../../mocks/renderHookWithProviders';
import { org } from '@studio/testing/testids';
import { queriesMock } from '../../mocks/queriesMock';
import { useCreateOrgCodeListMutation } from './useCreateOrgCodeListMutation';
import type { CodeList } from '../../types/CodeList';
import type { CodeListData } from '../../types/CodeListData';
import { createQueryClientMock } from '../../mocks/queryClientMock';
import { QueryKey } from '../../types/QueryKey';
import type { CodeListsResponse } from '../../types/api/CodeListsResponse';

// Test data:
const codeList: CodeList = [
{
value: 'test-value',
label: 'test-label',
},
];

const newCodeList: CodeListData = {
title: 'new-title',
data: codeList,
};

const existingCodeList: CodeListData = {
title: 'existing-title',
data: codeList,
};

describe('useCreateOrgCodeListMutation', () => {
beforeEach(jest.clearAllMocks);

it('Calls createCodeListForOrg with correct parameters', async () => {
const { result } = renderHookWithProviders(() => useCreateOrgCodeListMutation(org));

await result.current.mutateAsync(newCodeList);

expect(queriesMock.createCodeListForOrg).toHaveBeenCalledTimes(1);
expect(queriesMock.createCodeListForOrg).toHaveBeenCalledWith(
org,
newCodeList.title,
newCodeList.data,
);
});

it('Replaces cache with api response', async () => {
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.OrgCodeLists, org], [existingCodeList]);
const createCodeListForOrg = jest.fn(() => Promise.resolve([existingCodeList, newCodeList]));
const { result } = renderHookWithProviders(() => useCreateOrgCodeListMutation(org), {
queryClient,
queries: { createCodeListForOrg },
});

await result.current.mutateAsync(newCodeList);

const expectedUpdatedData: CodeListsResponse = [existingCodeList, newCodeList];
const updatedData = queryClient.getQueryData([QueryKey.OrgCodeLists, org]);
expect(updatedData).toEqual(expectedUpdatedData);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 type { CodeListData } from '../../types/CodeListData';

type CreateOrgCodeListMutationArgs = Pick<CodeListData, 'title' | 'data'>;

export const useCreateOrgCodeListMutation = (org: string) => {
const queryClient = useQueryClient();
const { createCodeListForOrg } = useServicesContext();

const mutationFn = ({ title, data }: CreateOrgCodeListMutationArgs) =>
createCodeListForOrg(org, title, data);

return useMutation({
mutationFn,
onSuccess: (newData: CodeListsResponse) => {
queryClient.setQueryData([QueryKey.OrgCodeLists, org], newData);
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { renderHookWithProviders } from '../../mocks/renderHookWithProviders';
import { org } from '@studio/testing/testids';
import { queriesMock } from '../../mocks/queriesMock';
import { useDeleteOrgCodeListMutation } from '../../hooks/mutations/useDeleteOrgCodeListMutation';
import { createQueryClientMock } from '../../mocks/queryClientMock';
import { QueryKey } from '../../types/QueryKey';
import { useCreateOrgCodeListMutation } from '../../hooks/mutations/useCreateOrgCodeListMutation';
import type { CodeListsResponse } from '../../types/api/CodeListsResponse';
import type { CodeListData } from '../../types/CodeListData';
import type { CodeList } from '../../types/CodeList';

// Test data:
const title = 'testId';

const codeList: CodeList = [
{
value: 'test-value',
label: 'test-label',
},
];

const codeListToDelete: CodeListData = {
title: 'deleted-title',
data: codeList,
};

const otherCodeList: CodeListData = {
title: 'other-title',
data: codeList,
};

describe('useDeleteOrgCodeListMutation', () => {
beforeEach(jest.clearAllMocks);

it('Calls deleteCodeListForOrg with correct parameters', async () => {
const { result } = renderHookWithProviders(() => useDeleteOrgCodeListMutation(org));
await result.current.mutateAsync({ title });
expect(queriesMock.deleteCodeListForOrg).toHaveBeenCalledTimes(1);
expect(queriesMock.deleteCodeListForOrg).toHaveBeenCalledWith(org, title);
});

it('Replaces cache with api response', async () => {
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.OrgCodeLists, org], [codeListToDelete, otherCodeList]);
const createCodeListForOrg = jest.fn(() => Promise.resolve([otherCodeList]));
const { result } = renderHookWithProviders(() => useCreateOrgCodeListMutation(org), {
queryClient,
queries: { createCodeListForOrg },
});

await result.current.mutateAsync({ title: codeListToDelete.title });

const expectedUpdatedData: CodeListsResponse = [otherCodeList];
const updatedData = queryClient.getQueryData([QueryKey.OrgCodeLists, org]);
expect(updatedData).toEqual(expectedUpdatedData);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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 type { CodeListData } from '../../types/CodeListData';

type DeleteOrgCodeListMutationArgs = Pick<CodeListData, 'title'>;

export const useDeleteOrgCodeListMutation = (org: string) => {
const queryClient = useQueryClient();
const { deleteCodeListForOrg } = useServicesContext();

const mutationFn = ({ title }: DeleteOrgCodeListMutationArgs) => deleteCodeListForOrg(org, title);

return useMutation({
mutationFn,
onSuccess: (newData: CodeListsResponse) => {
queryClient.setQueryData([QueryKey.OrgCodeLists, org], newData);
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { renderHookWithProviders } from '../../mocks/renderHookWithProviders';
import { org } from '@studio/testing/testids';
import { queriesMock } from '../../mocks/queriesMock';
import type { CodeList } from '../../types/CodeList';
import { useUpdateOrgCodeListMutation } from './useUpdateOrgCodeListMutation';
import { createQueryClientMock } from '../../mocks/queryClientMock';
import { QueryKey } from '../../types/QueryKey';
import type { CodeListsResponse } from '../../types/api/CodeListsResponse';
import type { CodeListData } from '../../types/CodeListData';

// Test data:
const codeList: CodeList = [
{
value: 'test-value',
label: 'test-label',
},
];

const oldCodeList: CodeListData = {
title: 'old-title',
data: codeList,
};

const updatedCodeList: CodeListData = {
title: 'updated-title',
data: codeList,
};

describe('useUpdateOrgCodeListMutation', () => {
beforeEach(jest.clearAllMocks);

it('Calls updateCodeListForOrg with correct parameters', async () => {
const { result } = renderHookWithProviders(() => useUpdateOrgCodeListMutation(org));
await result.current.mutateAsync(updatedCodeList);
expect(queriesMock.updateCodeListForOrg).toHaveBeenCalledTimes(1);
expect(queriesMock.updateCodeListForOrg).toHaveBeenCalledWith(
org,
updatedCodeList.title,
updatedCodeList.data,
);
});

it('Replaces cache with api response', async () => {
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.OrgCodeLists, org], [oldCodeList]);
const updateCodeListForOrg = jest.fn(() => Promise.resolve([updatedCodeList]));
const { result } = renderHookWithProviders(() => useUpdateOrgCodeListMutation(org), {
queryClient,
queries: { updateCodeListForOrg },
});

await result.current.mutateAsync(updatedCodeList);

const expectedUpdatedData: CodeListsResponse = [updatedCodeList];
const updatedData = queryClient.getQueryData([QueryKey.OrgCodeLists, org]);
expect(updatedData).toEqual(expectedUpdatedData);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useServicesContext } from '../../contexts/ServicesContext';
import { QueryKey } from '../../types/QueryKey';
import type { CodeListData } from '../../types/CodeListData';
import type { CodeListsResponse } from '../../types/api/CodeListsResponse';

type UpdateOrgCodeListMutationArgs = Pick<CodeListData, 'title' | 'data'>;

export const useUpdateOrgCodeListMutation = (org: string) => {
const queryClient = useQueryClient();
const { updateCodeListForOrg } = useServicesContext();

const mutationFn = ({ title, data }: UpdateOrgCodeListMutationArgs) =>
updateCodeListForOrg(org, title, data);

return useMutation({
mutationFn,
onSuccess: (newData: CodeListsResponse) => {
queryClient.setQueryData([QueryKey.OrgCodeLists, org], newData);
},
});
};
Loading