Skip to content
Open
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
40 changes: 11 additions & 29 deletions src/core/contexts/queryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { UseQueryResult } from '@tanstack/react-query';
import type { AxiosError } from 'axios';

import { createContext } from 'src/core/contexts/context';
import { DisplayError as DefaultDisplayError } from 'src/core/errorHandling/DisplayError';
import { Loader as DefaultLoader } from 'src/core/loading/Loader';
import { DisplayError } from 'src/core/errorHandling/DisplayError';
import { Loader } from 'src/core/loading/Loader';
import type { LaxContextProps, StrictContextProps } from 'src/core/contexts/context';

type Err = Error | AxiosError;
Expand All @@ -18,17 +18,9 @@ type Query<Req extends boolean, QueryData> = () => Req extends true

type ContextProps<Ctx, Req extends boolean> = Req extends true ? StrictContextProps : LaxContextProps<Ctx>;

export type QueryContextProps<QueryData, Req extends boolean, ContextData = QueryData> = ContextProps<
ContextData,
Req
> & {
export type QueryContextProps<QueryData, Req extends boolean> = ContextProps<QueryData, Req> & {
query: Query<Req, QueryData>;

process?: (data: QueryData) => ContextData;
shouldDisplayError?: (error: Err) => boolean;

DisplayError?: React.ComponentType<{ error: Err }>;
Loader?: React.ComponentType<{ reason: string }>;
};

/**
Expand All @@ -38,26 +30,16 @@ export type QueryContextProps<QueryData, Req extends boolean, ContextData = Quer
* Remember to call this through a delayedContext() call to prevent problems with cyclic imports.
* @see delayedContext
*/
export function createQueryContext<QD, Req extends boolean, CD = QD>(props: QueryContextProps<QD, Req, CD>) {
const {
name,
required,
query,
process = (i: QD) => i as unknown as CD,
shouldDisplayError = () => true,
DisplayError = DefaultDisplayError,
Loader = DefaultLoader,
...rest
} = props;
export function createQueryContext<QD, Req extends boolean>(props: QueryContextProps<QD, Req>) {
const { name, required, query: useQuery, shouldDisplayError = () => true, ...rest } = props;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { Provider, useCtx, useLaxCtx, useHasProvider } = createContext<CD>({ name, required, ...(rest as any) });
const defaultValue = ('default' in rest ? rest.default : undefined) as CD;
const { Provider, useCtx, useLaxCtx, useHasProvider } = createContext<QD>({ name, required, ...(rest as any) });
const defaultValue = ('default' in rest ? rest.default : undefined) as QD;

const WrappingProvider = ({ children }: PropsWithChildren) => {
const { data, isPending, error, ...rest } = query();
function WrappingProvider({ children }: PropsWithChildren) {
const { data, isPending, error, ...rest } = useQuery();
const enabled = 'enabled' in rest && !required ? rest.enabled : true;

const value = useMemo(() => (typeof data !== 'undefined' ? process(data) : undefined), [data]);
const value = useMemo(() => data, [data]);

if (enabled && isPending) {
return <Loader reason={`query-${name}`} />;
Expand All @@ -68,7 +50,7 @@ export function createQueryContext<QD, Req extends boolean, CD = QD>(props: Quer
}

return <Provider value={enabled ? (value ?? defaultValue) : defaultValue}>{children}</Provider>;
};
}

return {
Provider: WrappingProvider,
Expand Down
16 changes: 14 additions & 2 deletions src/features/datamodel/DataModelsProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import type { PropsWithChildren } from 'react';

import { useQueryClient } from '@tanstack/react-query';
import { useMutationState, useQueryClient } from '@tanstack/react-query';
import deepEqual from 'fast-deep-equal';
import { createStore } from 'zustand';
import type { JSONSchema7 } from 'json-schema';
Expand Down Expand Up @@ -257,6 +257,18 @@ function BlockUntilLoaded({ children }: PropsWithChildren) {
const actualCurrentTask = useCurrentLayoutSetId();
const isPDF = useIsPdf();

const currentMutations = useMutationState({ filters: { status: 'pending', mutationKey: ['saveFormData'] } });
const hasPassedMutationCheck = useRef(false);
if (currentMutations.length > 0 && !hasPassedMutationCheck.current) {
// FormDataWrite automatically saves unsaved changes on unmount. If something happens above us in the render tree
// that causes FormDataWrite to be unmounted (forcing it to save) and re-mounts everything (including us), we
// should wait for that previously started save to complete. Otherwise, we'd end up saving outdated initial data
// and cause a 409 when patching later.
return <Loader reason='save-form-data' />;
}

hasPassedMutationCheck.current = true;

if (error) {
// Error trying to fetch data, if missing rights we display relevant page
if (isAxiosError(error) && error.response?.status === HttpStatusCodes.Forbidden) {
Expand Down
23 changes: 13 additions & 10 deletions src/features/form/layout/LayoutsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';

import { skipToken, useQuery } from '@tanstack/react-query';

Expand Down Expand Up @@ -57,15 +57,18 @@ function useLayoutQuery() {
utils.error && window.logError('Fetching form layout failed:\n', utils.error);
}, [utils.error]);

return utils.data
? {
...utils,
data: {
...utils.data,
lookups: makeLayoutLookups(utils.data.layouts),
},
}
: utils;
const data = useMemo(() => {
if (utils.data) {
return {
...utils.data,
lookups: makeLayoutLookups(utils.data.layouts),
};
}

return utils.data;
}, [utils.data]);

return { ...utils, data };
}
const { Provider, useCtx, useLaxCtx } = delayedContext(() =>
createQueryContext({
Expand Down
26 changes: 13 additions & 13 deletions src/features/form/layoutSets/LayoutSetsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@ export function useLayoutSetsQueryDef() {
const { fetchLayoutSets } = useAppQueries();
return {
queryKey: ['fetchLayoutSets'],
queryFn: fetchLayoutSets,
queryFn: async () => {
const layoutSets = await fetchLayoutSets();
if (layoutSets?.uiSettings?.taskNavigation) {
return {
...layoutSets,
uiSettings: {
...layoutSets.uiSettings,
taskNavigation: layoutSets.uiSettings.taskNavigation.map((g) => ({ ...g, id: uuidv4() })),
},
};
}
return layoutSets;
},
};
}

Expand All @@ -33,18 +45,6 @@ const { Provider, useCtx, useLaxCtx } = delayedContext(() =>
name: 'LayoutSets',
required: true,
query: useLayoutSetsQuery,
process: (layoutSets) => {
if (layoutSets?.uiSettings?.taskNavigation) {
return {
...layoutSets,
uiSettings: {
...layoutSets.uiSettings,
taskNavigation: layoutSets.uiSettings.taskNavigation.map((g) => ({ ...g, id: uuidv4() })),
},
};
}
return layoutSets;
},
}),
);

Expand Down
87 changes: 45 additions & 42 deletions src/features/form/layoutSettings/LayoutSettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import type { QueryDefinition } from 'src/core/queries/usePrefetchQuery';
import type { GlobalPageSettings, ILayoutSettings, NavigationPageGroup } from 'src/layout/common.generated';

// Also used for prefetching @see formPrefetcher.ts
export function useLayoutSettingsQueryDef(layoutSetId?: string): QueryDefinition<ILayoutSettings | null> {
export function useLayoutSettingsQueryDef(layoutSetId?: string): QueryDefinition<ProcessedLayoutSettings> {
const { fetchLayoutSettings } = useAppQueries();
return {
queryKey: ['layoutSettings', layoutSetId],
queryFn: () => (layoutSetId ? fetchLayoutSettings(layoutSetId) : null),
queryFn: async () => processData(layoutSetId ? await fetchLayoutSettings(layoutSetId) : null),
};
}

Expand All @@ -33,50 +33,53 @@ function useLayoutSettingsQuery() {
return query;
}

function processData(settings: ILayoutSettings | null): ProcessedLayoutSettings {
if (!settings) {
return {
order: [],
groups: [],
pageSettings: {},
pdfLayoutName: undefined,
};
}

if (!('order' in settings.pages) && !('groups' in settings.pages)) {
const msg = 'Missing page order, specify one of `pages.order` or `pages.groups` in Settings.json';
window.logError(msg);
throw new Error(msg);
}
if ('order' in settings.pages && 'groups' in settings.pages) {
const msg = 'Specify one of `pages.order` or `pages.groups` in Settings.json';
window.logError(msg);
throw new Error(msg);
}

const order: string[] =
'order' in settings.pages
? settings.pages.order
: settings.pages.groups.filter((group) => 'order' in group).flatMap((group) => group.order);

return {
order,
groups: 'groups' in settings.pages ? settings.pages.groups.map((g) => ({ ...g, id: uuidv4() })) : undefined,
pageSettings: omitUndefined({
autoSaveBehavior: settings.pages.autoSaveBehavior,
expandedWidth: settings.pages.expandedWidth,
hideCloseButton: settings.pages.hideCloseButton,
showExpandWidthButton: settings.pages.showExpandWidthButton,
showLanguageSelector: settings.pages.showLanguageSelector,
showProgress: settings.pages.showProgress,
taskNavigation: settings.pages.taskNavigation?.map((g) => ({ ...g, id: uuidv4() })),
}),
pdfLayoutName: settings.pages.pdfLayoutName,
};
}

const { Provider, useCtx, useLaxCtx } = delayedContext(() =>
createQueryContext<ILayoutSettings | null, true, ProcessedLayoutSettings>({
createQueryContext<ProcessedLayoutSettings, true>({
name: 'LayoutSettings',
required: true,
query: useLayoutSettingsQuery,
process: (settings) => {
if (!settings) {
return {
order: [],
groups: [],
pageSettings: {},
pdfLayoutName: undefined,
};
}

if (!('order' in settings.pages) && !('groups' in settings.pages)) {
window.logError('Missing page order, specify one of `pages.order` or `pages.groups` in Settings.json');
throw 'Missing page order, specify one of `pages.order` or `pages.groups` in Settings.json';
}
if ('order' in settings.pages && 'groups' in settings.pages) {
window.logError('Both `pages.order` and `pages.groups` was set in Settings.json');
throw 'Both `pages.order` and `pages.groups` was set in Settings.json';
}

const order: string[] =
'order' in settings.pages
? settings.pages.order
: settings.pages.groups.filter((group) => 'order' in group).flatMap((group) => group.order);

return {
order,
groups: 'groups' in settings.pages ? settings.pages.groups.map((g) => ({ ...g, id: uuidv4() })) : undefined,
pageSettings: omitUndefined({
autoSaveBehavior: settings.pages.autoSaveBehavior,
expandedWidth: settings.pages.expandedWidth,
hideCloseButton: settings.pages.hideCloseButton,
showExpandWidthButton: settings.pages.showExpandWidthButton,
showLanguageSelector: settings.pages.showLanguageSelector,
showProgress: settings.pages.showProgress,
taskNavigation: settings.pages.taskNavigation?.map((g) => ({ ...g, id: uuidv4() })),
}),
pdfLayoutName: settings.pages.pdfLayoutName,
};
},
}),
);

Expand Down
7 changes: 3 additions & 4 deletions src/features/language/textResources/TextResourcesProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ const useTextResourcesQuery = () => {
const enabled = useIsCurrentLanguageResolved();

const utils = {
...useQueryWithStaleData<ITextResourceResult, HttpClientError>({
...useQueryWithStaleData<TextResourceMap, HttpClientError>({
enabled,
queryKey: ['fetchTextResources', selectedLanguage],
queryFn: () => fetchTextResources(selectedLanguage),
queryFn: async () => convertResult(await fetchTextResources(selectedLanguage)),
}),
enabled,
};
Expand All @@ -38,12 +38,11 @@ const useTextResourcesQuery = () => {
};

const { Provider, useCtx, useHasProvider } = delayedContext(() =>
createQueryContext<ITextResourceResult, false, TextResourceMap>({
createQueryContext<TextResourceMap, false>({
name: 'TextResources',
required: false,
default: {},
query: useTextResourcesQuery,
process: convertResult,
}),
);

Expand Down
28 changes: 7 additions & 21 deletions src/layout/RepeatingGroup/Providers/RepeatingGroupContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,26 +357,16 @@ export function RepeatingGroupProvider({ baseComponentId, children }: PropsWithC

export const useRepeatingGroupComponentId = () => ZStore.useSelector((state) => state.baseComponentId);

function useDelayedSelector() {
return ZStore.useDelayedSelector({
mode: 'innerSelector',
makeArgs: (state) => [state],
});
}

function useMaybeValidateRow() {
const store = ZStore.useStore();
const baseComponentId = useRepeatingGroupComponentId();
const delayedSelector = useDelayedSelector();
const { validateOnSaveRow } = useExternalItem(baseComponentId, 'RepeatingGroup');
const onGroupCloseValidation = useOnGroupCloseValidation();
const getRows = RepGroupHooks.useGetFreshRowsWithButtons(baseComponentId);
const getState = () => produceStateFromRows(getRows() ?? []);

return () => {
const editingAll = delayedSelector((s) => s.editingAll, []);
const editingId = delayedSelector((s) => s.editingId, []);
const editingNone = delayedSelector((s) => s.editingNone, []);
const index = getState().editableRows.find((row) => row.uuid === editingId)?.index;
const { editingAll, editingId, editingNone } = store.getState();
const index = produceStateFromRows(getRows() ?? []).editableRows.find((row) => row.uuid === editingId)?.index;
if (!validateOnSaveRow || editingAll || editingNone || editingId === undefined || index === undefined) {
return Promise.resolve(false);
}
Expand Down Expand Up @@ -419,16 +409,16 @@ export const RepGroupContext = {
return ZStore.useSelector((state) => (uuid ? state.deletingIds.includes(uuid) : false));
},
useToggleEditing() {
const store = ZStore.useStore();
const rawOpenForEditing = ZStore.useStaticSelector((state) => state.openForEditing);
const rawCloseForEditing = ZStore.useStaticSelector((state) => state.closeForEditing);
const delayedSelector = useDelayedSelector();
const maybeValidateRow = useMaybeValidateRow();

return async (row: BaseRow) => {
if (await maybeValidateRow()) {
return;
}
const editingId = delayedSelector((s) => s.editingId, []);
const editingId = store.getState().editingId;
if (editingId === row.uuid) {
rawCloseForEditing(row);
} else {
Expand Down Expand Up @@ -481,20 +471,16 @@ export const RepGroupContext = {
};
},
useChangePageToRow() {
const store = ZStore.useStore();
const baseComponentId = useRepeatingGroupComponentId();
const rawChangePage = ZStore.useStaticSelector((state) => state.changePage);
const delayedSelector = useDelayedSelector();
const maybeValidateRow = useMaybeValidateRow();

const { pagination } = useExternalItem(baseComponentId, 'RepeatingGroup');
const getRows = RepGroupHooks.useGetFreshRowsWithButtons(baseComponentId);
const getState = () => produceStateFromRows(getRows() ?? []);
const getPaginationState = () =>
producePaginationState(
delayedSelector((s) => s.currentPage, []),
pagination,
getState().visibleRows,
);
producePaginationState(store.getState().currentPage, pagination, getState().visibleRows);

return async (row: BaseRow) => {
if (await maybeValidateRow()) {
Expand Down
Loading