diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx
index 906debbc9b..63394a8fa5 100644
--- a/src/components/form/Form.tsx
+++ b/src/components/form/Form.tsx
@@ -91,10 +91,7 @@ export function FormPage({ currentPageId }: { currentPageId: string | undefined
return (
<>
-
+
>
);
}
diff --git a/src/components/presentation/Presentation.tsx b/src/components/presentation/Presentation.tsx
index 6bad9e14b5..6727ba2a20 100644
--- a/src/components/presentation/Presentation.tsx
+++ b/src/components/presentation/Presentation.tsx
@@ -11,6 +11,7 @@ import { Header } from 'src/components/presentation/Header';
import { NavBar } from 'src/components/presentation/NavBar';
import classes from 'src/components/presentation/Presentation.module.css';
import { Progress } from 'src/components/presentation/Progress';
+import { createContext } from 'src/core/contexts/context';
import { RenderStart } from 'src/core/ui/RenderStart';
import { Footer } from 'src/features/footer/Footer';
import { useUiConfigContext } from 'src/features/form/layout/UiConfigContext';
@@ -45,38 +46,40 @@ export const PresentationComponent = ({ header, type, children, renderNavBar = t
return (
-
-
-
- {isProcessStepsArchived && instanceStatus?.substatus && (
- }
- description={}
- />
- )}
- {renderNavBar && }
-
-
-
-
+
+
+
+
+ {isProcessStepsArchived && instanceStatus?.substatus && (
+ }
+ description={}
+ />
+ )}
+ {renderNavBar && }
+
+
+
+
+
);
};
@@ -98,3 +101,10 @@ function ProgressBar({ type }: { type: ProcessTaskType | PresentationType }) {
);
}
+
+const { Provider: PresentationProvider, useHasProvider } = createContext({
+ name: 'Presentation',
+ required: true,
+});
+
+export const useHasPresentation = () => useHasProvider();
diff --git a/src/components/wrappers/ProcessWrapper.tsx b/src/components/wrappers/ProcessWrapper.tsx
index fe81c0fe3e..a18da97c8e 100644
--- a/src/components/wrappers/ProcessWrapper.tsx
+++ b/src/components/wrappers/ProcessWrapper.tsx
@@ -5,7 +5,7 @@ import { Route, Routes } from 'react-router-dom';
import Grid from '@material-ui/core/Grid';
import { Button } from 'src/app-components/button/Button';
-import { Form, FormFirstPage } from 'src/components/form/Form';
+import { Form } from 'src/components/form/Form';
import { PresentationComponent } from 'src/components/presentation/Presentation';
import classes from 'src/components/wrappers/ProcessWrapper.module.css';
import { Loader } from 'src/core/loading/Loader';
@@ -187,7 +187,7 @@ export const ProcessWrapper = () => {
}
/>
@@ -196,10 +196,6 @@ export const ProcessWrapper = () => {
}
/>
- }
- />
);
diff --git a/src/core/contexts/context.tsx b/src/core/contexts/context.tsx
index c5b5b1c97a..ffe56ac92e 100644
--- a/src/core/contexts/context.tsx
+++ b/src/core/contexts/context.tsx
@@ -50,12 +50,12 @@ export function createContext({ name, required, ...rest }: CreateContextProps
});
Context.displayName = name;
- const useHasProvider = () => Boolean(useContext(Context).provided);
+ const useHasProvider = () => useContext(Context).provided;
const useCtx = (): T => {
- const hasProvider = useHasProvider();
- const value = useContext(Context).innerValue;
- if (!hasProvider) {
+ const ctx = useContext(Context);
+ const value = ctx.innerValue;
+ if (!ctx.provided) {
if (required) {
throw new Error(`${name} is missing`);
}
diff --git a/src/core/loading/Loader.tsx b/src/core/loading/Loader.tsx
index 022e28718a..7510c8b624 100644
--- a/src/core/loading/Loader.tsx
+++ b/src/core/loading/Loader.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { AltinnContentIconFormData } from 'src/components/atoms/AltinnContentIconFormData';
import { AltinnContentLoader } from 'src/components/molecules/AltinnContentLoader';
-import { PresentationComponent } from 'src/components/presentation/Presentation';
+import { PresentationComponent, useHasPresentation } from 'src/components/presentation/Presentation';
import { useTaskStore } from 'src/core/contexts/taskStoreContext';
import { LoadingProvider } from 'src/core/loading/LoadingContext';
import { Lang } from 'src/features/language/Lang';
@@ -11,12 +11,12 @@ import { ProcessTaskType } from 'src/types';
interface LoaderProps {
reason: string; // The reason is used by developers to identify the reason for the loader
details?: string;
- renderPresentation?: boolean;
}
-export const Loader = ({ renderPresentation = true, ...rest }: LoaderProps) => {
+export const Loader = (props: LoaderProps) => {
const overriddenDataModelUuid = useTaskStore((state) => state.overriddenDataModelUuid);
const overriddenTaskId = useTaskStore((state) => state.overriddenTaskId);
+ const hasPresentation = useHasPresentation();
if (overriddenDataModelUuid) {
return null;
@@ -25,23 +25,23 @@ export const Loader = ({ renderPresentation = true, ...rest }: LoaderProps) => {
if (overriddenTaskId) {
return null;
}
- if (renderPresentation) {
+ if (!hasPresentation) {
return (
-
+
}
type={ProcessTaskType.Unknown}
renderNavBar={false}
>
-
+
);
}
return (
-
-
+
+
);
};
diff --git a/src/features/formData/FormDataWrite.tsx b/src/features/formData/FormDataWrite.tsx
index 73e9cd368d..8d354d0e39 100644
--- a/src/features/formData/FormDataWrite.tsx
+++ b/src/features/formData/FormDataWrite.tsx
@@ -971,11 +971,4 @@ export const FD = {
return useCallback((dataElementId: string) => map[dataElementId], [map]);
},
-
- /**
- * This lets you set to a function that will be called as soon as the saving operation finishes.
- * Beware that this is not a subscription service, so you can easily overwrite an existing callback here. This
- * is only meant to be used in NodesContext.
- */
- useSetOnSaveFinished: () => useSelector((s) => s.setOnSaveFinished),
};
diff --git a/src/features/formData/FormDataWriteStateMachine.tsx b/src/features/formData/FormDataWriteStateMachine.tsx
index e1feb5c69e..258fba9e1f 100644
--- a/src/features/formData/FormDataWriteStateMachine.tsx
+++ b/src/features/formData/FormDataWriteStateMachine.tsx
@@ -80,11 +80,6 @@ type FormDataState = {
// This contains the validation issues we receive from the server last time we saved the data model.
validationIssues: BackendValidationIssueGroups | undefined;
- // This may contain a callback function that will be called whenever the save finishes.
- // Should only be set from NodesContext.
- onSaveFinished: ((result: FDSaveFinished) => void) | undefined;
- setOnSaveFinished: (callback: (result: FDSaveFinished) => void) => void;
-
// This is used to track which component is currently blocking the auto-saving feature. If this is set to a string
// value, auto-saving will be disabled, even if the autoSaving flag is set to true. This is useful when you want
// to temporarily disable auto-saving, for example when clicking a CustomButton and waiting for the server to
@@ -238,7 +233,6 @@ function makeActions(
const { validationIssues, savedData, newDataModels, instance } = toProcess;
state.manualSaveRequested = false;
state.validationIssues = validationIssues;
- state.onSaveFinished?.(toProcess);
if (instance && changeInstance) {
changeInstance(() => instance);
@@ -548,11 +542,6 @@ export const createFormDataWriteStore = (
debounceTimeout: DEFAULT_DEBOUNCE_TIMEOUT,
manualSaveRequested: false,
validationIssues: undefined,
- onSaveFinished: undefined,
- setOnSaveFinished: (callback) =>
- set((state) => {
- state.onSaveFinished = callback;
- }),
...actions,
};
}),
diff --git a/src/features/routing/AppRoutingContext.tsx b/src/features/routing/AppRoutingContext.tsx
index 8f54cd2524..47fe582ec4 100644
--- a/src/features/routing/AppRoutingContext.tsx
+++ b/src/features/routing/AppRoutingContext.tsx
@@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import { matchPath, useLocation, useNavigate as useNativeNavigate } from 'react-router-dom';
import type { MutableRefObject, PropsWithChildren } from 'react';
+import type { NavigateOptions } from 'react-router-dom';
import { createStore } from 'zustand';
@@ -30,17 +31,18 @@ interface Context {
updateHash: (hash: string) => void;
effectCallback: NavigationEffectCb | null;
setEffectCallback: (cb: NavigationEffectCb | null) => void;
- navigateRef: MutableRefObject>;
+ navigateRef: MutableRefObject;
}
+export type SimpleNavigate = (target: string, options?: NavigateOptions) => void;
+
function newStore({ initialLocation }: { initialLocation: string | undefined }) {
return createStore((set) => ({
hash: initialLocation ? initialLocation : `${window.location.hash}`,
updateHash: (hash: string) => set({ hash }),
effectCallback: null,
setEffectCallback: (effectCallback: NavigationEffectCb) => set({ effectCallback }),
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- navigateRef: { current: undefined as any },
+ navigateRef: { current: undefined },
}));
}
@@ -139,11 +141,13 @@ export const useIsSubformPage = () =>
useSelector((s) => {
const path = getPath(s.hash);
const matches = matchers.map((matcher) => matchPath(matcher, path));
- return !!paramFrom(matches, 'mainPageKey');
+ const mainPageKey = paramFrom(matches, 'mainPageKey');
+ const subformPageKey = paramFrom(matches, 'pageKey');
+ return !!(mainPageKey && subformPageKey);
});
// Use this instead of the native one to avoid re-rendering whenever the route changes
-export const useNavigate = () => useSelector((ctx) => ctx.navigateRef).current;
+export const useNavigate = () => useStaticSelector((ctx) => ctx.navigateRef).current!;
const matchers: string[] = [
'/instance/:partyId/:instanceGuid',
@@ -198,8 +202,20 @@ function UpdateHash() {
}
function UpdateNavigate() {
- const navigateRef = useSelector((ctx) => ctx.navigateRef);
- navigateRef.current = useNativeNavigate();
+ const store = useStore();
+ const navigateRef = useStaticSelector((ctx) => ctx.navigateRef);
+ const nativeNavigate = useNativeNavigate();
+
+ navigateRef.current = (target, options) => {
+ if (target && !target.startsWith('/')) {
+ // Used for relative navigation, e.g. navigating to a subform page
+ const currentPath = getPath(store.getState().hash).replace(/\/$/, '');
+ const newTarget = `${currentPath}/${target}`;
+ nativeNavigate(newTarget, options);
+ return;
+ }
+ nativeNavigate(target, options);
+ };
return null;
}
diff --git a/src/features/validation/StoreValidationsInNode.tsx b/src/features/validation/StoreValidationsInNode.tsx
index 9d2a445f73..dd6dcffa0d 100644
--- a/src/features/validation/StoreValidationsInNode.tsx
+++ b/src/features/validation/StoreValidationsInNode.tsx
@@ -10,7 +10,7 @@ import { GeneratorCondition, StageFormValidation } from 'src/utils/layout/genera
import { NodesInternal } from 'src/utils/layout/NodesContext';
import type { AnyValidation, AttachmentValidation } from 'src/features/validation/index';
import type { CompCategory } from 'src/layout/common';
-import type { TypesFromCategory } from 'src/layout/layout';
+import type { CompIntermediate, TypesFromCategory } from 'src/layout/layout';
import type { LayoutNode } from 'src/utils/layout/LayoutNode';
export function StoreValidationsInNode() {
@@ -29,9 +29,9 @@ type Node = LayoutNode !deepEqual(data.validations, validations));
@@ -56,15 +56,6 @@ function StoreValidationsInNodeWorker() {
visibilityToSet !== undefined,
);
- const shouldSetProcessedLast = NodesInternal.useNodeData(
- node,
- (data) => data.validationsProcessedLast !== processedLast,
- );
- NodesStateQueue.useSetNodeProp(
- { node, prop: 'validationsProcessedLast', value: processedLast, force: true },
- shouldSetProcessedLast,
- );
-
return null;
}
@@ -91,3 +82,7 @@ function useUpdatedValidations(validations: AnyValidation[], node: Node) {
return copy;
});
}
+
+export function shouldValidateNode(item: CompIntermediate): boolean {
+ return !('renderAsSummary' in item && item.renderAsSummary);
+}
diff --git a/src/features/validation/ValidationPlugin.tsx b/src/features/validation/ValidationPlugin.tsx
index d57d30dc70..088ed45c04 100644
--- a/src/features/validation/ValidationPlugin.tsx
+++ b/src/features/validation/ValidationPlugin.tsx
@@ -1,22 +1,16 @@
import { CG } from 'src/codegen/CG';
import { NodeDefPlugin } from 'src/utils/layout/plugins/NodeDefPlugin';
import type { ComponentConfig } from 'src/codegen/ComponentConfig';
-import type { ComponentValidation, ValidationsProcessedLast } from 'src/features/validation/index';
+import type { ComponentValidation } from 'src/features/validation/index';
import type { CompCategory } from 'src/layout/common';
import type { TypesFromCategory } from 'src/layout/layout';
-import type { NodesContext } from 'src/utils/layout/NodesContext';
-import type {
- DefPluginExtraState,
- DefPluginState,
- DefPluginStateFactoryProps,
-} from 'src/utils/layout/plugins/NodeDefPlugin';
+import type { DefPluginExtraState, DefPluginStateFactoryProps } from 'src/utils/layout/plugins/NodeDefPlugin';
interface Config {
componentType: TypesFromCategory;
extraState: {
validations: ComponentValidation[];
validationVisibility: number;
- validationsProcessedLast: ValidationsProcessedLast;
};
}
@@ -49,7 +43,6 @@ export class ValidationPlugin extends NodeDefPlugin {
return {
validations: [],
validationVisibility: 0,
- validationsProcessedLast: { initial: undefined, incremental: undefined },
};
}
@@ -61,11 +54,4 @@ export class ValidationPlugin extends NodeDefPlugin {
return `<${StoreValidationsInNode} />`;
}
-
- stateIsReady(state: DefPluginState, fullState: NodesContext): boolean {
- return (
- state.validationsProcessedLast.initial === fullState.validationsProcessedLast.initial &&
- state.validationsProcessedLast.incremental === fullState.validationsProcessedLast.incremental
- );
- }
}
diff --git a/src/features/validation/backendValidation/BackendValidation.tsx b/src/features/validation/backendValidation/BackendValidation.tsx
index 3edaa35f85..39dda82b88 100644
--- a/src/features/validation/backendValidation/BackendValidation.tsx
+++ b/src/features/validation/backendValidation/BackendValidation.tsx
@@ -13,7 +13,6 @@ import {
useShouldValidateInitial,
} from 'src/features/validation/backendValidation/backendValidationUtils';
import { Validation } from 'src/features/validation/validationContext';
-import { NodesStore } from 'src/utils/layout/NodesContext';
const emptyObject = {};
const emptyArray = [];
@@ -91,15 +90,3 @@ export function BackendValidation({ dataTypes }: { dataTypes: string[] }) {
return null;
}
-
-export function MaintainInitialValidationsInNodesContext() {
- const enabled = useShouldValidateInitial();
- const { data: initialValidations } = useBackendValidationQuery(enabled);
- const setInitialValidations = NodesStore.useSelector((state) => state.setLatestInitialValidations);
-
- useEffect(() => {
- setInitialValidations(initialValidations);
- }, [initialValidations, setInitialValidations]);
-
- return null;
-}
diff --git a/src/features/validation/nodeValidation/useNodeValidation.ts b/src/features/validation/nodeValidation/useNodeValidation.ts
index 910e2401a1..cc902348b4 100644
--- a/src/features/validation/nodeValidation/useNodeValidation.ts
+++ b/src/features/validation/nodeValidation/useNodeValidation.ts
@@ -1,119 +1,59 @@
-import { useMemo } from 'react';
-
-import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider';
-import { useAttachmentsSelector } from 'src/features/attachments/hooks';
-import { useLayoutSets } from 'src/features/form/layoutSets/LayoutSetsProvider';
-import { FD } from 'src/features/formData/FormDataWrite';
-import { useLaxDataElementsSelector } from 'src/features/instance/InstanceContext';
-import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
import { Validation } from 'src/features/validation/validationContext';
import { implementsValidateComponent, implementsValidateEmptyField } from 'src/layout';
+import { GeneratorInternal } from 'src/utils/layout/generator/GeneratorContext';
import { GeneratorData } from 'src/utils/layout/generator/GeneratorDataSources';
-import { NodesInternal } from 'src/utils/layout/NodesContext';
-import type {
- AnyValidation,
- BaseValidation,
- ValidationDataSources,
- ValidationsProcessedLast,
-} from 'src/features/validation';
+import type { AnyValidation, BaseValidation } from 'src/features/validation';
import type { CompDef, ValidationFilter } from 'src/layout';
import type { IDataModelReference } from 'src/layout/common.generated';
import type { LayoutNode } from 'src/utils/layout/LayoutNode';
import type { NodeDataSelector } from 'src/utils/layout/NodesContext';
+const emptyArray: AnyValidation[] = [];
+
/**
* Runs validations defined in the component classes. This runs from the node generator, and will collect all
* validations for a node and return them.
*/
-export function useNodeValidation(
- node: LayoutNode,
- shouldValidate: boolean,
-): { validations: AnyValidation[]; processedLast: ValidationsProcessedLast } {
- const dataModelSelector = Validation.useDataModelSelector();
- const validationDataSources = GeneratorData.useValidationDataSources();
- const nodeDataSelector = NodesInternal.useNodeDataSelector();
-
+export function useNodeValidation(node: LayoutNode, shouldValidate: boolean): AnyValidation[] {
+ const registry = GeneratorInternal.useRegistry();
+ const dataSources = GeneratorData.useValidationDataSources();
const getDataElementIdForDataType = GeneratorData.useGetDataElementIdForDataType();
- const processedLast = GeneratorData.useValidationsProcessedLast();
+ const dataModelBindings = GeneratorInternal.useIntermediateItem()?.dataModelBindings;
+ const bindings = Object.entries((dataModelBindings ?? {}) as Record);
- return {
- processedLast,
- validations: useMemo(() => {
- const validations: AnyValidation[] = [];
- if (!shouldValidate) {
- return validations;
- }
+ return Validation.useFullState((state) => {
+ if (!shouldValidate) {
+ return emptyArray;
+ }
- if (implementsValidateEmptyField(node.def)) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- validations.push(...node.def.runEmptyFieldValidation(node as any, validationDataSources));
- }
+ const validations: AnyValidation[] = [];
+ if (implementsValidateEmptyField(node.def)) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ validations.push(...node.def.runEmptyFieldValidation(node as any, dataSources));
+ }
- if (implementsValidateComponent(node.def)) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- validations.push(...node.def.runComponentValidation(node as any, validationDataSources));
- }
+ if (implementsValidateComponent(node.def)) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ validations.push(...node.def.runComponentValidation(node as any, dataSources));
+ }
- const dataModelBindings = validationDataSources.nodeDataSelector(
- (picker) => picker(node)?.layout.dataModelBindings,
- [node],
- );
- for (const [bindingKey, { dataType, field }] of Object.entries(
- (dataModelBindings ?? {}) as Record,
- )) {
- const dataElementId = getDataElementIdForDataType(dataType) ?? dataType; // stateless does not have dataElementId
- const fieldValidations = dataModelSelector(
- (dataModels) => dataModels[dataElementId]?.[field],
- [dataType, field],
- );
- if (fieldValidations) {
- validations.push(...fieldValidations.map((v) => ({ ...v, bindingKey })));
- }
+ for (const [bindingKey, { dataType, field }] of bindings) {
+ const dataElementId = getDataElementIdForDataType(dataType) ?? dataType; // stateless does not have dataElementId
+ const fieldValidations = state.state.dataModels[dataElementId]?.[field];
+ if (fieldValidations) {
+ validations.push(...fieldValidations.map((v) => ({ ...v, bindingKey })));
}
+ }
- return filter(validations, node, nodeDataSelector);
- }, [shouldValidate, node, validationDataSources, nodeDataSelector, getDataElementIdForDataType, dataModelSelector]),
- };
-}
+ const result = filter(validations, node, dataSources.nodeDataSelector);
+ registry.current.validationsProcessed[node.id] = state.processedLast;
-/**
- * Hook providing validation data sources
- */
-function useValidationDataSources(): ValidationDataSources {
- const formDataSelector = FD.useDebouncedSelector();
- const invalidDataSelector = FD.useInvalidDebouncedSelector();
- const attachmentsSelector = useAttachmentsSelector();
- const currentLanguage = useCurrentLanguage();
- const nodeSelector = NodesInternal.useNodeDataSelector();
- const applicationMetadata = useApplicationMetadata();
- const dataElementsSelector = useLaxDataElementsSelector();
- const layoutSets = useLayoutSets();
- const dataElementHasErrorsSelector = Validation.useDataElementHasErrorsSelector();
+ if (result.length === 0) {
+ return emptyArray;
+ }
- return useMemo(
- () => ({
- formDataSelector,
- invalidDataSelector,
- attachmentsSelector,
- currentLanguage,
- nodeDataSelector: nodeSelector,
- applicationMetadata,
- dataElementsSelector,
- layoutSets,
- dataElementHasErrorsSelector,
- }),
- [
- formDataSelector,
- invalidDataSelector,
- attachmentsSelector,
- currentLanguage,
- nodeSelector,
- applicationMetadata,
- dataElementsSelector,
- layoutSets,
- dataElementHasErrorsSelector,
- ],
- );
+ return result;
+ });
}
/**
diff --git a/src/features/validation/nodeValidation/waitForNodesToValidate.ts b/src/features/validation/nodeValidation/waitForNodesToValidate.ts
new file mode 100644
index 0000000000..09229564b7
--- /dev/null
+++ b/src/features/validation/nodeValidation/waitForNodesToValidate.ts
@@ -0,0 +1,54 @@
+import { useCallback } from 'react';
+
+import { shouldValidateNode } from 'src/features/validation/StoreValidationsInNode';
+import { GeneratorInternal } from 'src/utils/layout/generator/GeneratorContext';
+import { NodesStore } from 'src/utils/layout/NodesContext';
+import type { ValidationsProcessedLast } from 'src/features/validation';
+
+export function useWaitForNodesToValidate() {
+ const registry = GeneratorInternal.useRegistry();
+ const nodesStore = NodesStore.useStore();
+
+ return useCallback(
+ async (processedLast: ValidationsProcessedLast): Promise => {
+ let callbackId: ReturnType | undefined;
+ const request = window.requestIdleCallback || window.requestAnimationFrame;
+ const cancel = window.cancelIdleCallback || window.cancelAnimationFrame;
+
+ function check(): boolean {
+ const allNodeData = nodesStore.getState().nodeData;
+ const nodeIds = Object.keys(allNodeData);
+ for (const nodeId of nodeIds) {
+ const nodeData = allNodeData[nodeId];
+ if (!nodeData || !('validations' in nodeData) || !shouldValidateNode(nodeData.layout)) {
+ // Node does not support validation
+ continue;
+ }
+
+ const lastValidations = registry.current.validationsProcessed[nodeId];
+ const initialIsLatest = lastValidations?.initial === processedLast.initial;
+ const incrementalIsLatest = lastValidations?.incremental === processedLast.incremental;
+ if (!(lastValidations && initialIsLatest && incrementalIsLatest)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return new Promise((resolve) => {
+ function checkAndResolve() {
+ if (check()) {
+ resolve();
+ callbackId && cancel(callbackId);
+ } else {
+ callbackId = request(checkAndResolve);
+ }
+ }
+
+ checkAndResolve();
+ });
+ },
+ [nodesStore, registry],
+ );
+}
diff --git a/src/features/validation/validationContext.tsx b/src/features/validation/validationContext.tsx
index 0d52480958..c4dc098623 100644
--- a/src/features/validation/validationContext.tsx
+++ b/src/features/validation/validationContext.tsx
@@ -31,6 +31,7 @@ import {
useShouldValidateInitial,
} from 'src/features/validation/backendValidation/backendValidationUtils';
import { InvalidDataValidation } from 'src/features/validation/invalidDataValidation/InvalidDataValidation';
+import { useWaitForNodesToValidate } from 'src/features/validation/nodeValidation/waitForNodesToValidate';
import { SchemaValidation } from 'src/features/validation/schemaValidation/SchemaValidation';
import { hasValidationErrors, mergeFieldValidations, selectValidations } from 'src/features/validation/utils';
import { useAsRef } from 'src/hooks/useAsRef';
@@ -146,6 +147,7 @@ function initialCreateStore() {
const {
Provider,
useSelector,
+ useMemoSelector,
useLaxShallowSelector,
useSelectorAsRef,
useStore,
@@ -188,6 +190,7 @@ function useWaitForValidation(): WaitForValidation {
const hasWritableDataTypes = !!DataModels.useWritableDataTypes()?.length;
const enabled = useShouldValidateInitial();
const getCachedInitialValidations = useGetCachedInitialValidations();
+ const waitForNodesToValidate = useWaitForNodesToValidate();
return useCallback(
async (forceSave = true) => {
@@ -201,21 +204,22 @@ function useWaitForValidation(): WaitForValidation {
await waitForNodesReady();
const validationsFromSave = await waitForSave(forceSave);
// If validationsFromSave is not defined, we check if initial validations are done processing
- const lastInitialValidations = await waitForState((state, setReturnValue) => {
+ await waitForState(async (state) => {
const { isFetching, cachedInitialValidations } = getCachedInitialValidations();
- if (
+ const validationsReady =
state.processedLast.incremental === validationsFromSave &&
state.processedLast.initial === cachedInitialValidations &&
- !isFetching
- ) {
- setReturnValue(cachedInitialValidations);
+ !isFetching;
+
+ if (validationsReady) {
+ await waitForNodesToValidate(state.processedLast);
return true;
}
return false;
});
- await waitForNodesReady({ initial: lastInitialValidations, incremental: validationsFromSave });
+ await waitForNodesReady();
},
[
enabled,
@@ -223,6 +227,7 @@ function useWaitForValidation(): WaitForValidation {
hasWritableDataTypes,
waitForAttachments,
waitForNodesReady,
+ waitForNodesToValidate,
waitForSave,
waitForState,
],
@@ -334,8 +339,6 @@ export type ValidationDataModelSelector = ReturnType;
export const Validation = {
- useFullStateRef: () => useSelectorAsRef((state) => state.state),
-
// Selectors. These are memoized, so they won't cause a re-render unless the selected fields change.
useSelector: () => useDS((state) => state),
useDataModelSelector: () => useDS((state) => state.state.dataModels),
@@ -377,8 +380,12 @@ export const Validation = {
useUpdateDataModelValidations: () => useSelector((state) => state.updateDataModelValidations),
useUpdateBackendValidations: () => useSelector((state) => state.updateBackendValidations),
- useProcessedLast: () => useSelector((state) => state.processedLast),
- useProcessedLastRef: () => useSelectorAsRef((state) => state.processedLast),
+ useFullState: (selector: (state: ValidationContext & Internals) => U): U =>
+ useMemoSelector((state) => selector(state)),
+ useGetProcessedLast: () => {
+ const store = useStore();
+ return useCallback(() => store.getState().processedLast, [store]);
+ },
useRef: () => useSelectorAsRef((state) => state),
useLaxRef: () => useLaxSelectorAsRef((state) => state),
diff --git a/src/hooks/useWaitForState.ts b/src/hooks/useWaitForState.ts
index 349d860492..d6a6d8b819 100644
--- a/src/hooks/useWaitForState.ts
+++ b/src/hooks/useWaitForState.ts
@@ -6,9 +6,9 @@ import type { StoreApi } from 'zustand';
import { ContextNotProvided } from 'src/core/contexts/context';
export type WaitForState = (callback: Callback) => Promise;
-type Callback = (state: T, setReturnValue: (val: RetVal) => void) => boolean;
+type Callback = (state: T, setReturnValue: (val: RetVal) => void) => boolean | Promise;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type Subscriber = (state: any) => boolean;
+type Subscriber = (state: any) => boolean | Promise;
// If ContextNotProvided is a valid state, it will possibly be provided as a direct
// input (not wrapped in a ref or store)
@@ -27,8 +27,13 @@ export function useWaitForState(state: ValidInputs[0]): WaitForSta
// Call subscribers on every re-render/state change if state is a ref
if (isRef(state)) {
for (const subscriber of subscribersRef.current) {
- if (subscriber(state.current)) {
+ const result = subscriber(state.current);
+ if (result === true) {
subscribersRef.current.delete(subscriber);
+ } else if (result instanceof Promise) {
+ result.then((res) => {
+ res && subscribersRef.current.delete(subscriber);
+ });
}
}
}
@@ -36,9 +41,9 @@ export function useWaitForState(state: ValidInputs[0]): WaitForSta
useEffect(() => {
// Call subscribers on every state change if state is a zustand store
if (isStore(state)) {
- return state.subscribe((state) => {
+ return state.subscribe(async (state) => {
for (const subscriber of subscribersRef.current) {
- if (subscriber(state)) {
+ if (await subscriber(state)) {
subscribersRef.current.delete(subscriber);
}
}
@@ -56,8 +61,8 @@ export function useWaitForState(state: ValidInputs[0]): WaitForSta
}
if (state === ContextNotProvided) {
- subscribersRef.current.add((state) => {
- if (callback(state, setReturnValue)) {
+ subscribersRef.current.add(async (state) => {
+ if (await callback(state, setReturnValue)) {
resolve(returnValue as RetVal);
return true;
}
@@ -69,13 +74,20 @@ export function useWaitForState(state: ValidInputs[0]): WaitForSta
const currentState = isRef(state) ? state.current : state.getState();
// If state is already correct, resolve immediately
- if (callback(currentState, setReturnValue)) {
+ const result = callback(currentState, setReturnValue);
+
+ if (result === true) {
resolve(returnValue as RetVal);
return;
}
+ if (result instanceof Promise) {
+ result.then((res) => {
+ res && resolve(returnValue as RetVal);
+ });
+ }
- subscribersRef.current.add((state) => {
- if (callback(state, setReturnValue)) {
+ subscribersRef.current.add(async (state) => {
+ if (await callback(state, setReturnValue)) {
resolve(returnValue as RetVal);
return true;
}
diff --git a/src/layout/Subform/SubformComponent.tsx b/src/layout/Subform/SubformComponent.tsx
index 5db47207f5..48de463d6d 100644
--- a/src/layout/Subform/SubformComponent.tsx
+++ b/src/layout/Subform/SubformComponent.tsx
@@ -1,5 +1,4 @@
import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
import { Spinner, Table } from '@digdir/designsystemet-react';
import { Grid } from '@material-ui/core';
@@ -14,7 +13,7 @@ import { useFormDataQuery } from 'src/features/formData/useFormDataQuery';
import { useStrictDataElements, useStrictInstanceId } from 'src/features/instance/InstanceContext';
import { Lang } from 'src/features/language/Lang';
import { useLanguage } from 'src/features/language/useLanguage';
-import { useIsSubformPage } from 'src/features/routing/AppRoutingContext';
+import { useIsSubformPage, useNavigate } from 'src/features/routing/AppRoutingContext';
import { useAddEntryMutation, useDeleteEntryMutation } from 'src/features/subformData/useSubformMutations';
import { isSubformValidation } from 'src/features/validation';
import { useComponentValidationsForNode } from 'src/features/validation/selectors/componentValidationsForNode';
diff --git a/src/layout/Subform/SubformWrapper.tsx b/src/layout/Subform/SubformWrapper.tsx
index 57086eb08b..7a773cecd4 100644
--- a/src/layout/Subform/SubformWrapper.tsx
+++ b/src/layout/Subform/SubformWrapper.tsx
@@ -1,11 +1,11 @@
import React, { useEffect } from 'react';
-import { Form, FormFirstPage } from 'src/components/form/Form';
+import { Form } from 'src/components/form/Form';
import { useTaskStore } from 'src/core/contexts/taskStoreContext';
import { Loader } from 'src/core/loading/Loader';
import { FormProvider } from 'src/features/form/FormContext';
import { useDataTypeFromLayoutSet } from 'src/features/form/layout/LayoutsContext';
-import { useIsCurrentView, useNavigationParam } from 'src/features/routing/AppRoutingContext';
+import { useNavigationParam } from 'src/features/routing/AppRoutingContext';
import { useNavigatePage } from 'src/hooks/useNavigatePage';
import { useNodeItem } from 'src/utils/layout/useNodeItem';
import type { LayoutNode } from 'src/utils/layout/LayoutNode';
@@ -19,21 +19,10 @@ export function SubformWrapper({ node }: { node: LayoutNode<'Subform'> }) {
return (
-
+
);
}
-
-function SubformForm() {
- const hasFormPage = !useIsCurrentView(undefined);
-
- if (hasFormPage) {
- return ;
- }
-
- return ;
-}
-
export const RedirectBackToMainForm = () => {
const mainPageKey = useNavigationParam('mainPageKey');
const { navigateToPage } = useNavigatePage();
diff --git a/src/layout/Subform/Summary/SubformSummaryTable.tsx b/src/layout/Subform/Summary/SubformSummaryTable.tsx
index 75e5b37d15..9d2cb2a1c4 100644
--- a/src/layout/Subform/Summary/SubformSummaryTable.tsx
+++ b/src/layout/Subform/Summary/SubformSummaryTable.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import { useNavigate } from 'react-router-dom';
import { Paragraph, Spinner, Table } from '@digdir/designsystemet-react';
import { Grid } from '@material-ui/core';
@@ -13,7 +12,7 @@ import { useStrictDataElements, useStrictInstanceId } from 'src/features/instanc
import { Lang } from 'src/features/language/Lang';
import { useLanguage } from 'src/features/language/useLanguage';
import { usePdfModeActive } from 'src/features/pdf/PDFWrapper';
-import { useIsSubformPage } from 'src/features/routing/AppRoutingContext';
+import { useIsSubformPage, useNavigate } from 'src/features/routing/AppRoutingContext';
import { isSubformValidation } from 'src/features/validation';
import { useComponentValidationsForNode } from 'src/features/validation/selectors/componentValidationsForNode';
import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper';
diff --git a/src/utils/layout/LayoutPage.ts b/src/utils/layout/LayoutPage.ts
index 14688e9e60..237f0585cb 100644
--- a/src/utils/layout/LayoutPage.ts
+++ b/src/utils/layout/LayoutPage.ts
@@ -12,19 +12,34 @@ export class LayoutPage implements LayoutObject {
public layoutSet: LayoutPages;
public pageKey: string;
- private allChildren: Map = new Map();
+ private allChildren: LayoutNode[] = [];
+ private allChildIds = new Set();
+
+ private _directChildren: LayoutNode[] = [];
/**
* Adds a child to the collection. For internal use only.
*/
public _addChild(child: LayoutNode) {
- this.allChildren.set(child.id, child);
- this.layoutSet.registerNode(child);
+ if (!this.allChildIds.has(child.id)) {
+ this.layoutSet.registerNode(child);
+ this.allChildIds.add(child.id);
+ this.allChildren.push(child);
+
+ // Direct children of a layout page are always static.
+ // Only children of components like repeating groups are dynamic
+ if (child.parent === this) {
+ this._directChildren.push(child);
+ }
+ }
}
public _removeChild(child: LayoutNode) {
- this.allChildren.delete(child.id);
- this.layoutSet.unregisterNode(child);
+ if (this.allChildIds.has(child.id)) {
+ this.layoutSet.unregisterNode(child);
+ this.allChildIds.delete(child.id);
+ this.allChildren.splice(this.allChildren.indexOf(child), 1);
+ }
}
/**
@@ -47,7 +62,7 @@ export class LayoutPage implements LayoutObject {
}
protected directChildren(_task: TraversalTask): LayoutNode[] {
- return [...this.allChildren.values()].filter((node) => node.parent === this);
+ return this._directChildren;
}
public firstChild(task: TraversalTask): LayoutNode | undefined {
@@ -61,7 +76,7 @@ export class LayoutPage implements LayoutObject {
}
private firstDeepChild(task: TraversalTask): LayoutNode | undefined {
- for (const node of this.allChildren.values()) {
+ for (const node of this.allChildren) {
if (task.passes(node)) {
return node;
}
@@ -86,7 +101,7 @@ export class LayoutPage implements LayoutObject {
}
public flat(task?: TraversalTask): LayoutNode[] {
- return task ? [...this.allChildren.values()].filter((n) => task.passes(n)) : [...this.allChildren.values()];
+ return task ? this.allChildren.filter((n) => task.passes(n)) : this.allChildren;
}
public isRegisteredInCollection(layoutSet: LayoutPages): boolean {
@@ -98,7 +113,7 @@ export class LayoutPage implements LayoutObject {
this.layoutSet = layoutSet;
layoutSet.replacePage(this);
- for (const node of this.allChildren.values()) {
+ for (const node of this.allChildren) {
layoutSet.registerNode(node);
}
}
diff --git a/src/utils/layout/NodesContext.tsx b/src/utils/layout/NodesContext.tsx
index d844ceef6f..dd4d06e902 100644
--- a/src/utils/layout/NodesContext.tsx
+++ b/src/utils/layout/NodesContext.tsx
@@ -17,11 +17,8 @@ import { UpdateAttachmentsForCypress } from 'src/features/attachments/UpdateAtta
import { HiddenComponentsProvider } from 'src/features/form/dynamics/HiddenComponentsProvider';
import { useLayouts } from 'src/features/form/layout/LayoutsContext';
import { useLaxLayoutSettings, useLayoutSettings } from 'src/features/form/layoutSettings/LayoutSettingsContext';
-import { FD } from 'src/features/formData/FormDataWrite';
import { OptionsStorePlugin } from 'src/features/options/OptionsStorePlugin';
import { useIsCurrentView } from 'src/features/routing/AppRoutingContext';
-import { MaintainInitialValidationsInNodesContext } from 'src/features/validation/backendValidation/BackendValidation';
-import { useGetCachedInitialValidations } from 'src/features/validation/backendValidation/backendValidationQuery';
import { ExpressionValidation } from 'src/features/validation/expressionValidation/ExpressionValidation';
import {
LoadingBlockerWaitForValidation,
@@ -51,7 +48,6 @@ import { LayoutPages } from 'src/utils/layout/LayoutPages';
import { RepeatingChildrenStorePlugin } from 'src/utils/layout/plugins/RepeatingChildrenStorePlugin';
import { TraversalTask } from 'src/utils/layout/useNodeTraversal';
import type { AttachmentsStorePluginConfig } from 'src/features/attachments/AttachmentsStorePlugin';
-import type { FDSaveFinished } from 'src/features/formData/FormDataWriteStateMachine';
import type { OptionsStorePluginConfig } from 'src/features/options/OptionsStorePlugin';
import type { ValidationsProcessedLast } from 'src/features/validation';
import type { ValidationStorePluginConfig } from 'src/features/validation/ValidationStorePlugin';
@@ -129,7 +125,6 @@ export interface SetPagePropRequest {
export enum NodesReadiness {
Ready = 'READY',
NotReady = 'NOT READY',
- WaitingUntilLastSaveHasProcessed = 'WAIT FOR SAVE',
}
export type NodesContext = {
@@ -144,7 +139,6 @@ export type NodesContext = {
prevNodeData: { [key: string]: NodeData } | undefined; // Earlier node data from before the state became non-ready
hiddenViaRules: { [key: string]: true | undefined };
hiddenViaRulesRan: boolean;
- validationsProcessedLast: ValidationsProcessedLast;
layouts: ILayouts | undefined; // Used to detect if the layouts have changed
stages: GeneratorStagesContext;
@@ -158,8 +152,6 @@ export type NodesContext = {
addPage: (pageKey: string) => void;
setPageProps: (requests: SetPagePropRequest[]) => void;
markReady: (reason: string, readiness?: NodesReadiness) => void;
- onSaveFinished: (result: FDSaveFinished) => void;
- setLatestInitialValidations: (validations: ValidationsProcessedLast['initial']) => void;
reset: (layouts: ILayouts, validationsProcessedLast: ValidationsProcessedLast) => void;
@@ -349,28 +341,6 @@ export function createNodesDataStore({ registry, validationsProcessedLast }: Cre
}),
markReady: (reason, readiness = NodesReadiness.Ready) =>
set((state) => setReadiness({ state, target: readiness, reason })),
- onSaveFinished: (result) =>
- set((state) => {
- if (state.readiness !== NodesReadiness.WaitingUntilLastSaveHasProcessed) {
- generatorLog('logReadiness', `Marking state as NOT READY: processing save results`);
- }
-
- return {
- readiness: NodesReadiness.WaitingUntilLastSaveHasProcessed,
- prevNodeData: state.nodeData,
- validationsProcessedLast: {
- ...state.validationsProcessedLast,
- incremental: result.validationIssues,
- },
- };
- }),
- setLatestInitialValidations: (validations) =>
- set((state) => ({
- validationsProcessedLast: {
- ...state.validationsProcessedLast,
- initial: validations,
- },
- })),
reset: (layouts, validationsProcessedLast: ValidationsProcessedLast) =>
set(() => {
@@ -518,12 +488,12 @@ const {
export const NodesProvider = ({ children }: React.PropsWithChildren) => {
const registry = useRegistry();
- const processedLast = Validation.useProcessedLastRef();
+ const getProcessedLast = Validation.useGetProcessedLast();
return (
@@ -551,7 +521,7 @@ function ProvideGlobalContext({ children, registry }: PropsWithChildren<{ regist
const layouts = Store.useSelector((s) => s.layouts);
const markNotReady = NodesInternal.useMarkNotReady();
const reset = Store.useSelector((s) => s.reset);
- const processedLast = Validation.useProcessedLastRef();
+ const getProcessedLast = Validation.useGetProcessedLast();
const pagesRef = useRef();
if (!pagesRef.current) {
pagesRef.current = new LayoutPages();
@@ -561,9 +531,9 @@ function ProvideGlobalContext({ children, registry }: PropsWithChildren<{ regist
if (layouts !== latestLayouts) {
markNotReady('new layouts');
pagesRef.current = new LayoutPages();
- reset(latestLayouts, processedLast.current);
+ reset(latestLayouts, getProcessedLast());
}
- }, [latestLayouts, layouts, markNotReady, reset, processedLast]);
+ }, [latestLayouts, layouts, markNotReady, reset, getProcessedLast]);
const layoutMap = useMemo(() => {
const out: { [id: string]: CompExternal } = {};
@@ -647,16 +617,6 @@ function IndicateReadiness() {
);
}
-function MarkAsReady() {
- return (
- <>
-
-
-
- >
- );
-}
-
/**
* Some selectors (like NodeTraversal) only re-runs when the data store is 'ready', and when nodes start being added
* or removed, the store is marked as not ready. This component will mark the store as ready when all nodes are added,
@@ -665,49 +625,27 @@ function MarkAsReady() {
* This causes the node traversal selectors to re-run only when all nodes in a new repeating group row (and similar)
* have been added.
*/
-function InnerMarkAsReady() {
+function MarkAsReady() {
+ const store = Store.useStore();
const markReady = Store.useSelector((s) => s.markReady);
const readiness = Store.useSelector((s) => s.readiness);
const hiddenViaRulesRan = Store.useSelector((s) => s.hiddenViaRulesRan);
const stagesFinished = GeneratorStages.useIsFinished();
- const hasUnsavedChanges = FD.useHasUnsavedChanges();
const registry = GeneratorInternal.useRegistry();
- const getCachedInitialValidations = useGetCachedInitialValidations();
// Even though the getAwaitingCommits() function works on refs in the GeneratorStages context, the effects of such
// commits always changes the NodesContext. Thus our useSelector() re-runs and re-renders this components when
// commits are done.
const getAwaitingCommits = useGetAwaitingCommits();
- const savingOk = readiness === NodesReadiness.WaitingUntilLastSaveHasProcessed ? !hasUnsavedChanges : true;
- const checkNodeStates = stagesFinished && savingOk && hiddenViaRulesRan && readiness !== NodesReadiness.Ready;
+ const checkNodeStates = stagesFinished && hiddenViaRulesRan && readiness !== NodesReadiness.Ready;
const nodeStateReady = Store.useSelector((state) => {
if (!checkNodeStates) {
return false;
}
- const { cachedInitialValidations, isFetching } = getCachedInitialValidations();
- if (isFetching || cachedInitialValidations !== state.validationsProcessedLast.initial) {
- generatorLog('logReadiness', `Initial validations are still being fetched, waiting...`);
- return false;
- }
-
- for (const nodeData of Object.values(state.nodeData)) {
- const def = getComponentDef(nodeData.layout.type) as LayoutComponent;
- const nodeReady = def.stateIsReady(nodeData);
- const pluginsReady = def.pluginStateIsReady(nodeData, state);
- if (!nodeReady || !pluginsReady) {
- generatorLog(
- 'logReadiness',
- `Node ${nodeData.layout.id} is not ready yet because of ` +
- `${nodeReady ? 'plugins' : pluginsReady ? 'node' : 'both node and plugins'}`,
- );
- return false;
- }
- }
-
- return true;
+ return areAllNodesReady(state);
});
const maybeReady = checkNodeStates && nodeStateReady;
@@ -719,22 +657,40 @@ function InnerMarkAsReady() {
// isn't ready.
return setIdleInterval(registry, () => {
const awaiting = getAwaitingCommits();
- if (awaiting === 0) {
- markReady('idle and nothing to commit');
- return true;
+ if (awaiting > 0) {
+ generatorLog('logReadiness', `Not quite ready yet (waiting for ${awaiting} commits)`);
+ return false;
}
- generatorLog('logReadiness', `Not quite ready yet (waiting for ${awaiting} commits)`);
- return false;
+ markReady('idle, nothing to commit');
+ return true;
});
}
return () => undefined;
- }, [maybeReady, getAwaitingCommits, markReady, registry]);
+ }, [maybeReady, getAwaitingCommits, markReady, registry, store]);
return null;
}
+function areAllNodesReady(state: NodesContext) {
+ for (const nodeData of Object.values(state.nodeData)) {
+ const def = getComponentDef(nodeData.layout.type) as LayoutComponent;
+ const nodeReady = def.stateIsReady(nodeData);
+ const pluginsReady = def.pluginStateIsReady(nodeData, state);
+ if (!nodeReady || !pluginsReady) {
+ generatorLog(
+ 'logReadiness',
+ `Node ${nodeData.layout.id} is not ready yet because of ` +
+ `${nodeReady ? 'plugins' : pluginsReady ? 'node' : 'both node and plugins'}`,
+ );
+ return false;
+ }
+ }
+
+ return true;
+}
+
const IDLE_COUNTDOWN = 3;
/**
@@ -775,19 +731,6 @@ function setIdleInterval(registry: MutableRefObject, fn: () => boolean
return () => (id === undefined ? undefined : cancel(id));
}
-function RegisterOnSaveFinished() {
- const setOnSaveFinished = FD.useSetOnSaveFinished();
- const ourOnSaveFinished = Store.useSelector((s) => s.onSaveFinished);
-
- useEffect(() => {
- setOnSaveFinished((result) => {
- ourOnSaveFinished(result);
- });
- }, [ourOnSaveFinished, setOnSaveFinished]);
-
- return null;
-}
-
function BlockUntilLoaded({ children }: PropsWithChildren) {
const hasBeenReady = useRef(false);
const ready = Store.useSelector((state) => {
@@ -1147,29 +1090,17 @@ export const NodesInternal = {
const store = Store.useLaxStore();
const waitForState = useWaitForState(store);
const waitForCommits = Store.useSelector((s) => s.waitForCommits);
- return useCallback(
- async (lastValidations?: ValidationsProcessedLast) => {
- await waitForState((state) => {
- if (state === ContextNotProvided) {
- return true;
- }
- const initialIsLatest =
- lastValidations?.initial === undefined ||
- lastValidations.initial === state.validationsProcessedLast.initial;
- const incrementalIsLatest =
- lastValidations?.incremental === undefined ||
- lastValidations.incremental === state.validationsProcessedLast.incremental;
- if (!incrementalIsLatest || !initialIsLatest) {
- return false;
- }
- return state.readiness === NodesReadiness.Ready && state.hiddenViaRulesRan;
- });
- if (waitForCommits) {
- await waitForCommits();
+ return useCallback(async () => {
+ await waitForState((state) => {
+ if (state === ContextNotProvided) {
+ return true;
}
- },
- [waitForState, waitForCommits],
- );
+ return state.readiness === NodesReadiness.Ready && state.hiddenViaRulesRan;
+ });
+ if (waitForCommits) {
+ await waitForCommits();
+ }
+ }, [waitForState, waitForCommits]);
},
useMarkNotReady() {
const markReady = Store.useSelector((s) => s.markReady);
diff --git a/src/utils/layout/generator/GeneratorDataSources.tsx b/src/utils/layout/generator/GeneratorDataSources.tsx
index aaae8f43d1..7c00edf8db 100644
--- a/src/utils/layout/generator/GeneratorDataSources.tsx
+++ b/src/utils/layout/generator/GeneratorDataSources.tsx
@@ -29,10 +29,7 @@ const { Provider, hooks } = createHookContext({
useReadableDataTypes: () => DataModels.useReadableDataTypes(),
useExternalApis: () => useExternalApis(useApplicationMetadata().externalApiIds ?? []),
useIsForcedVisibleByDevTools: () => useDevToolsStore((state) => state.isOpen && state.hiddenComponents !== 'hide'),
-
useGetDataElementIdForDataType: () => DataModels.useGetDataElementIdForDataType(),
- useValidationsProcessedLast: () => Validation.useProcessedLast(),
-
useCommitWhenFinished: () => useCommitWhenFinished(),
});
@@ -42,10 +39,7 @@ export const GeneratorData = {
useValidationDataSources,
useDefaultDataType: hooks.useDefaultDataType,
useIsForcedVisibleByDevTools: hooks.useIsForcedVisibleByDevTools,
-
useGetDataElementIdForDataType: hooks.useGetDataElementIdForDataType,
- useValidationsProcessedLast: hooks.useValidationsProcessedLast,
-
useCommitWhenFinished: hooks.useCommitWhenFinished,
};
diff --git a/src/utils/layout/generator/GeneratorStages.tsx b/src/utils/layout/generator/GeneratorStages.tsx
index 43643ad3f6..dd7d2b89c7 100644
--- a/src/utils/layout/generator/GeneratorStages.tsx
+++ b/src/utils/layout/generator/GeneratorStages.tsx
@@ -53,7 +53,9 @@ export type Registry = {
toCommit: RegistryCommitQueues;
toCommitCount: number;
commitTimeout: ReturnType | null;
- validations: ValidationsProcessedLast;
+ validationsProcessed: {
+ [nodeId: string]: ValidationsProcessedLast;
+ };
};
/**
@@ -238,10 +240,7 @@ export function useRegistry() {
setPageProps: [],
},
commitTimeout: null,
- validations: {
- initial: undefined,
- incremental: undefined,
- },
+ validationsProcessed: {},
});
}
diff --git a/src/utils/layout/generator/NodeRepeatingChildren.tsx b/src/utils/layout/generator/NodeRepeatingChildren.tsx
index 8745b4b0dc..2068add44f 100644
--- a/src/utils/layout/generator/NodeRepeatingChildren.tsx
+++ b/src/utils/layout/generator/NodeRepeatingChildren.tsx
@@ -150,8 +150,7 @@ interface ResolveRowProps {
function ResolveRowExpressions({ plugin }: ResolveRowProps) {
const node = GeneratorInternal.useParent() as LayoutNode;
const rowIndex = GeneratorInternal.useRowIndex()!;
- const nodeChildren = useNodeDirectChildren(node, rowIndex);
- const firstChild = nodeChildren ? nodeChildren[0] : undefined;
+ const firstChild = useNodeDirectChildren(node, rowIndex).at(0);
const internal = NodesInternal.useNodeData(
node,
diff --git a/src/utils/layout/plugins/RepeatingChildrenPlugin.tsx b/src/utils/layout/plugins/RepeatingChildrenPlugin.tsx
index 1c60ebe1da..3291ec61bf 100644
--- a/src/utils/layout/plugins/RepeatingChildrenPlugin.tsx
+++ b/src/utils/layout/plugins/RepeatingChildrenPlugin.tsx
@@ -218,24 +218,21 @@ export class RepeatingChildrenPlugin>, restriction?: TraversalRestriction): string[] {
- const out: string[] = [];
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- const rows = (state.item as any)[this.settings.internalProp] as Row[];
+ const rows = (state.item as any)[this.settings.internalProp] as (Row | undefined)[];
if (!rows) {
- return out;
+ return emptyArray;
}
- for (const row of rows) {
- if (!row || (restriction !== undefined && row.index !== restriction)) {
- continue;
- }
-
- for (const child of row.itemIds || []) {
- child && out.push(child);
- }
+ if (restriction !== undefined) {
+ return rows[restriction]?.itemIds ?? emptyArray;
}
+ const out: string[] = [];
+ for (let i = 0; i < rows.length; i++) {
+ const row = rows[i];
+ row && out.push(...row.itemIds);
+ }
return out;
}
@@ -255,3 +252,5 @@ export class RepeatingChildrenPlugin row && row.uuid !== undefined && row.itemIds !== undefined) ?? false;
}
}
+
+const emptyArray = [];
diff --git a/src/utils/layout/plugins/RepeatingChildrenStorePlugin.tsx b/src/utils/layout/plugins/RepeatingChildrenStorePlugin.tsx
index 75096f6ff7..97eafc431a 100644
--- a/src/utils/layout/plugins/RepeatingChildrenStorePlugin.tsx
+++ b/src/utils/layout/plugins/RepeatingChildrenStorePlugin.tsx
@@ -36,6 +36,10 @@ export class RepeatingChildrenStorePlugin extends NodeDataPlugin {
let changes = false;
const nodeData = { ...state.nodeData };
+ const newPartialItems: {
+ [nodeId: string]: { [internalProp: string]: (RepChildrenRow | undefined)[] | undefined } | undefined;
+ } = {};
+
for (const { node, rowIndex, plugin, extras } of requests) {
if (typeof extras !== 'object' || !extras) {
throw new Error('Extras must be an object');
@@ -43,22 +47,30 @@ export class RepeatingChildrenStorePlugin extends NodeDataPlugin