Skip to content

Commit

Permalink
refactor: fetch only single needed option list in option list editor (#…
Browse files Browse the repository at this point in the history
…14266)

Co-authored-by: Erling Hauan <[email protected]>
  • Loading branch information
standeren and ErlingHauan authored Dec 20, 2024
1 parent 6714508 commit a578aab
Show file tree
Hide file tree
Showing 22 changed files with 324 additions and 246 deletions.
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] }),
]);
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;
}

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

0 comments on commit a578aab

Please sign in to comment.