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

refactor: fetch only single needed option list in option list editor #14266

Merged
merged 7 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const submitFeedbackPath = (org, app) => `${basePath}/${org}/${app}/feedb
// 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
Expand Down
4 changes: 3 additions & 1 deletion frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
authStatusAnsattporten,
availableMaskinportenScopesPath,
selectedMaskinportenScopesPath,
optionListPath,
} from './paths';

import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api';
Expand Down Expand Up @@ -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 { OptionsList, OptionsLists } from 'app-shared/types/api/OptionsLists';
import type { LayoutSetsModel } from '../types/api/dto/LayoutSetsModel';

export const getIsLoggedInWithAnsattporten = () => get<{ isLoggedIn: boolean }>(authStatusAnsattporten());
Expand All @@ -114,6 +115,7 @@ export const getInstanceIdForPreview = (owner: string, app: string) => get<strin
export const getLayoutNames = (owner: string, app: string) => get<string[]>(layoutNamesPath(owner, app));
export const getLayoutSets = (owner: string, app: string) => get<LayoutSets>(layoutSetsPath(owner, app));
export const getLayoutSetsExtended = (owner: string, app: string) => get<LayoutSetsModel>(layoutSetsPath(owner, app) + '/extended');
export const getOptionList = (owner: string, app: string, optionsListId: string) => get<OptionsList>(optionListPath(owner, app, optionsListId));
export const getOptionLists = (owner: string, app: string) => get<OptionsLists>(optionListsPath(owner, app));
export const getOptionListIds = (owner: string, app: string) => get<string[]>(optionListIdsPath(owner, app));
export const getOrgList = () => get<OrgList>(orgListUrl());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ export const useAddOptionListMutation = (org: string, app: string, meta?: Mutati

return useMutation({
mutationFn,
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: [QueryKey.OptionListIds, org, app] }),
queryClient.invalidateQueries({ queryKey: [QueryKey.OptionLists, org, app] }),
]);
TomasEng marked this conversation as resolved.
Show resolved Hide resolved
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: [QueryKey.OptionListIds, org, app] });
void queryClient.invalidateQueries({ queryKey: [QueryKey.OptionLists, org, app] });
},
meta,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,7 @@ describe('useUpdateOptionListIdMutation', () => {
test('Invalidates the optionListIds query cache', async () => {
const queryClient = createQueryClientMock();
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
const oldData: OptionsLists = {
firstOptionList: optionListMock,
optionListId: optionListMock,
lastOptionList: optionListMock,
};
queryClient.setQueryData([QueryKey.OptionLists, org, app], oldData);
queryClient.setQueryData([QueryKey.OptionLists, org, app], []);
const renderUpdateOptionListMutationResult = renderHookWithProviders(
() => useUpdateOptionListIdMutation(org, app),
{ queryClient },
Expand All @@ -78,4 +73,19 @@ describe('useUpdateOptionListIdMutation', () => {
queryKey: [QueryKey.OptionListIds, org, app],
});
});

test('Removes the option list query cache for the old Id', async () => {
const queryClient = createQueryClientMock();
const removeQueriesSpy = jest.spyOn(queryClient, 'removeQueries');
queryClient.setQueryData([QueryKey.OptionLists, org, app], []);
const renderUpdateOptionListMutationResult = renderHookWithProviders(
() => useUpdateOptionListIdMutation(org, app),
{ queryClient },
).result;
await renderUpdateOptionListMutationResult.current.mutateAsync(args);
expect(removeQueriesSpy).toHaveBeenCalledTimes(1);
expect(removeQueriesSpy).toHaveBeenCalledWith({
queryKey: [QueryKey.OptionList, org, app, optionListId],
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export const useUpdateOptionListIdMutation = (org: string, app: string) => {
const oldData: OptionsLists = queryClient.getQueryData([QueryKey.OptionLists, org, app]);
const ascSortedData = changeIdAndSortCacheData(optionListId, newOptionListId, oldData);
queryClient.setQueryData([QueryKey.OptionLists, org, app], ascSortedData);
// Currently we only need to remove the old and not set the new, since mutating the Id only happens from the library which uses the large OptionLists cache
queryClient.removeQueries({ queryKey: [QueryKey.OptionList, org, app, optionListId] });
queryClient.invalidateQueries({ queryKey: [QueryKey.OptionListIds, org, app] });
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('useUpdateOptionListMutation', () => {
expect(queriesMock.updateOptionList).toHaveBeenCalledWith(org, app, optionListId, optionsList);
});

test('Sets the updated option list on the cache', async () => {
test('Sets the updated option list on the cache for all option lists', async () => {
const queryClient = createQueryClientMock();
const renderUpdateOptionListMutationResult = renderHookWithProviders(
() => useUpdateOptionListMutation(org, app),
Expand All @@ -38,4 +38,16 @@ describe('useUpdateOptionListMutation', () => {
test: updatedOptionsList,
});
});

test('Sets the updated option list on the cache for the single option list', async () => {
const queryClient = createQueryClientMock();
const renderUpdateOptionListMutationResult = renderHookWithProviders(
() => useUpdateOptionListMutation(org, app),
{ queries: { updateOptionList }, queryClient },
).result;
await renderUpdateOptionListMutationResult.current.mutateAsync(args);
expect(queryClient.getQueryData([QueryKey.OptionList, org, app, optionListId])).toEqual(
updatedOptionsList,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const useUpdateOptionListMutation = (org: string, app: string, meta?: Mut
const newData = { ...oldData };
newData[optionListId] = updatedOptionList;
queryClient.setQueryData([QueryKey.OptionLists, org, app], newData);
queryClient.setQueryData([QueryKey.OptionList, org, app, optionListId], updatedOptionList);
},
meta,
});
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/hooks/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { useAppVersionQuery } from './useAppVersionQuery';
export { useDataModelsJsonQuery } from './useDataModelsJsonQuery';
export { useDataModelsXsdQuery } from './useDataModelsXsdQuery';
export { useInstanceIdQuery } from './useInstanceIdQuery';
export { useOptionListQuery } from './useOptionListQuery';
export { useOptionListsQuery } from './useOptionListsQuery';
export { useRepoMetadataQuery } from './useRepoMetadataQuery';
export { useRepoPullQuery } from './useRepoPullQuery';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';

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<ServicesContextProps> = {}) => {
const queryClient = createQueryClientMock();
const { result } = renderHookWithProviders(() => useOptionListQuery(org, app, optionsListId), {
queries,
queryClient,
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
return result;
};
17 changes: 17 additions & 0 deletions frontend/packages/shared/src/hooks/queries/useOptionListQuery.ts
Original file line number Diff line number Diff line change
@@ -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<OptionsList> => {
const { getOptionList } = useServicesContext();
return useQuery<OptionsList>({
queryKey: [QueryKey.OptionList, org, app, optionListId],
queryFn: () => getOptionList(org, app, optionListId),
});
};
3 changes: 2 additions & 1 deletion frontend/packages/shared/src/mocks/queriesMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -109,6 +109,7 @@ export const queriesMock: ServicesContextProps = {
.fn()
.mockImplementation(() => Promise.resolve<LayoutSetsModel>(layoutSetsExtendedMock)),
getOptionListIds: jest.fn().mockImplementation(() => Promise.resolve<string[]>([])),
getOptionList: jest.fn().mockImplementation(() => Promise.resolve<OptionsList>([])),
getOptionLists: jest.fn().mockImplementation(() => Promise.resolve<OptionsLists>({})),
getOrgList: jest.fn().mockImplementation(() => Promise.resolve<OrgList>(orgList)),
getOrganizations: jest.fn().mockImplementation(() => Promise.resolve<Organization[]>([])),
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/types/QueryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum QueryKey {
LayoutSchema = 'LayoutSchema',
LayoutSets = 'LayoutSets',
LayoutSetsExtended = 'LayoutSetsExtended',
OptionList = 'OptionList',
OptionLists = 'OptionLists',
OptionListIds = 'OptionListIds',
OrgList = 'OrgList',
Expand Down
4 changes: 3 additions & 1 deletion frontend/packages/shared/src/types/api/OptionsLists.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Option } from 'app-shared/types/Option';

export type OptionsLists = Record<string, Option[]>;
export type OptionsList = Option[];

export type OptionsLists = Record<string, OptionsList>;
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,9 @@ function SelectedOptionList({
setComponentHasOptionList(false);
};

const label =
component.optionsId !== '' && component.optionsId !== undefined
? component.optionsId
: t('ux_editor.modal_properties_code_list_custom_list');

return (
<div aria-label={label} className={classes.chosenOptionContainer}>
<OptionListEditor
label={label}
optionsId={component.optionsId}
component={component}
handleComponentChange={handleComponentChange}
/>
<div className={classes.chosenOptionContainer}>
<OptionListEditor component={component} handleComponentChange={handleComponentChange} />
<div className={classes.deleteButtonContainer}>
<StudioDeleteButton
className={classes.deleteButton}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { createRef, useState } from 'react';
import type { Option } from 'app-shared/types/Option';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { useUpdateOptionListMutation } from 'app-shared/hooks/mutations';
import { useOptionListEditorTexts } from '../../../../../EditOptions/OptionTabs/hooks';
import classes from './LibraryOptionsEditor.module.css';
import { useTranslation } from 'react-i18next';
import type { CodeListEditorTexts } from '@studio/components';
import { StudioAlert, StudioCodeListEditor, StudioModal, StudioProperty } from '@studio/components';
import { usePreviewContext } from 'app-development/contexts/PreviewContext';

type LibraryOptionsEditorProps = {
optionsId: string;
optionsList: Option[];
};

export function LibraryOptionsEditor({
optionsId,
optionsList,
}: LibraryOptionsEditorProps): React.ReactNode {
const { t } = useTranslation();
const { org, app } = useStudioEnvironmentParams();
const { doReloadPreview } = usePreviewContext();
const { mutate: updateOptionList } = useUpdateOptionListMutation(org, app);
const [localOptionList, setLocalOptionList] = useState<Option[]>(optionsList);
const editorTexts: CodeListEditorTexts = useOptionListEditorTexts();
const modalRef = createRef<HTMLDialogElement>();

const optionListHasChanged = (options: Option[]): boolean =>
JSON.stringify(options) !== JSON.stringify(localOptionList);

const handleBlurAny = (options: Option[]) => {
if (optionListHasChanged(options)) {
updateOptionList({ optionListId: optionsId, optionsList: options });
setLocalOptionList(options);
doReloadPreview();
}
};

const handleClose = () => {
modalRef.current?.close();
};

return (
<>
<StudioProperty.Button
value={optionsId}
title={t('ux_editor.options.option_edit_text')}
property={t('ux_editor.modal_properties_code_list_button_title_library')}
onClick={() => modalRef.current.showModal()}
/>
<StudioModal.Dialog
ref={modalRef}
className={classes.editOptionTabModal}
contentClassName={classes.content}
closeButtonTitle={t('general.close')}
heading={t('ux_editor.modal_add_options_code_list')}
onInteractOutside={handleClose}
onBeforeClose={handleClose}
footer={
<StudioAlert severity={'warning'} size='sm'>
{t('ux_editor.modal_properties_code_list_alert_title')}
</StudioAlert>
}
>
<StudioCodeListEditor
codeList={localOptionList}
onBlurAny={handleBlurAny}
texts={editorTexts}
/>
</StudioModal.Dialog>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { LibraryOptionsEditor } from './LibraryOptionsEditor';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.editOptionTabModal[open] {
--code-list-modal-min-width: min(80rem, 100%);
--code-list-modal-height: min(45rem, 100%);

min-width: var(--code-list-modal-min-width);
height: var(--code-list-modal-height);
}

.content {
height: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useOptionListEditorTexts } from '@altinn/ux-editor/components/config/editModal/EditOptions/OptionTabs/hooks';
import type { Option } from 'app-shared/types/Option';
import classes from './ManualOptionsEditor.module.css';
import type { IGenericEditComponent } from '@altinn/ux-editor/components/config/componentConfig';
import type { SelectionComponentType } from '@altinn/ux-editor/types/FormComponent';
import { useTranslation } from 'react-i18next';
import React, { useRef } from 'react';
import { StudioCodeListEditor, StudioModal, StudioProperty } from '@studio/components';

type ManualOptionsEditorProps = Pick<
IGenericEditComponent<SelectionComponentType>,
'component' | 'handleComponentChange'
>;

export function ManualOptionsEditor({
component,
handleComponentChange,
}: ManualOptionsEditorProps): React.ReactNode {
const { t } = useTranslation();
const modalRef = useRef<HTMLDialogElement>(null);
const editorTexts = useOptionListEditorTexts();

const handleBlurAny = (options: Option[]) => {
if (component.optionsId) {
delete component.optionsId;
}
TomasEng marked this conversation as resolved.
Show resolved Hide resolved

handleComponentChange({
...component,
options,
});
};

return (
<>
<StudioProperty.Button
value={t('ux_editor.modal_properties_code_list_custom_list')}
title={t('ux_editor.options.option_edit_text')}
property={t('ux_editor.modal_properties_code_list_button_title_manual')}
onClick={() => modalRef.current.showModal()}
/>
<StudioModal.Dialog
ref={modalRef}
className={classes.editOptionTabModal}
contentClassName={classes.content}
closeButtonTitle={t('general.close')}
heading={t('ux_editor.modal_add_options_code_list')}
>
<StudioCodeListEditor
codeList={component.options ?? []}
onBlurAny={handleBlurAny}
texts={editorTexts}
/>
</StudioModal.Dialog>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ManualOptionsEditor } from './ManualOptionsEditor';
Loading
Loading