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
16 changes: 9 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,15 @@ export const App = () => (
<Route
path=':taskId'
element={
<FixWrongReceiptType>
<ProcessWrapper>
<FormProvider>
<Outlet />
</FormProvider>
</ProcessWrapper>
</FixWrongReceiptType>
<PdfWrapper isTaskRoute>
<FixWrongReceiptType>
<ProcessWrapper>
<FormProvider>
<Outlet />
</FormProvider>
</ProcessWrapper>
</FixWrongReceiptType>
</PdfWrapper>
}
>
<Route
Expand Down
16 changes: 8 additions & 8 deletions src/features/form/FormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ import { FormPrefetcher } from 'src/queries/formPrefetcher';
import { NodesProvider } from 'src/utils/layout/NodesContext';

export interface FormContext {
// Set this if this form context is provided somewhere it's not expected we should write data to the data model.
// By setting this to true, no effects like 'preselectedOptionIndex' runs (which might try to change the data model).
// This should always be set to true when summarizing a previous task. It's important to note that it doesn't
// prevent any write operations from happening in case components inside try to write new form data, but it will
// prevent automatic effects from happening.
// Suppress automatic component effects and prevent writes to form data.
// Use this when rendering a summary or an immutable PDF preview.
readOnly?: boolean;
}

Expand All @@ -40,8 +37,11 @@ export function useIsInFormContext() {
/**
* This helper-context provider is used to provide all the contexts needed for forms to work
*/
export function FormProvider({ children, readOnly = false }: React.PropsWithChildren<FormContext>) {
const isEmbedded = useIsInFormContext();
export function FormProvider({ children, readOnly: ownReadOnly = false }: React.PropsWithChildren<FormContext>) {
const parent = useLaxCtx();
const isEmbedded = parent !== ContextNotProvided;
// Embedded subforms must preserve a read-only summary or preview context.
const readOnly = ownReadOnly || (parent !== ContextNotProvided && parent.readOnly === true);
const instanceOwnerPartyId = useNavigationParam('instanceOwnerPartyId');
const instanceGuid = useNavigationParam('instanceGuid');
const hasProcess = !!(instanceOwnerPartyId && instanceGuid);
Expand All @@ -56,7 +56,7 @@ export function FormProvider({ children, readOnly = false }: React.PropsWithChil
<PageNavigationProvider>
<DynamicsProvider>
<MaybeRulesProvider isEmbedded={isEmbedded}>
<FormDataWriteProvider>
<FormDataWriteProvider readOnly={readOnly}>
<ValidationProvider>
<NodesProvider
readOnly={readOnly}
Expand Down
4 changes: 2 additions & 2 deletions src/features/formData/FormDataWrite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ function useIsSavingFormData() {
);
}

export function FormDataWriteProvider({ children }: PropsWithChildren) {
export function FormDataWriteProvider({ children, readOnly = false }: PropsWithChildren<{ readOnly?: boolean }>) {
const proxies = useFormDataWriteProxies();
const ruleConnections = useRuleConnections();
const allDataTypes = DataModels.useReadableDataTypes();
Expand All @@ -394,7 +394,7 @@ export function FormDataWriteProvider({ children }: PropsWithChildren) {
lastSavedData: initialData[dt],
hasUnsavedChanges: false,
dataElementId: dataElementIds[dt],
readonly: !writableDataTypes.includes(dt),
readonly: readOnly || !writableDataTypes.includes(dt),
isDefault: dt === defaultDataType,
};
return dm;
Expand Down
41 changes: 41 additions & 0 deletions src/features/formData/FormDataWriteStateMachine.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { jest } from '@jest/globals';

import { createFormDataWriteStore } from 'src/features/formData/FormDataWriteStateMachine';
import { makeFormDataMethodProxies } from 'src/test/renderWithProviders';
import type { DataModelState } from 'src/features/formData/FormDataWriteStateMachine';

describe('read-only PDF data', () => {
it.each([true, false])('runs legacy calculations only on writable data (readonly=%s)', (readonly) => {
const calculate = jest.fn(() => 'calculated');
window.ruleHandlerObject = { calculate };
const currentData = { input: 'new', output: 'original' };
const debouncedData = { input: 'old', output: 'original' };
const model: DataModelState = {
currentData,
debouncedCurrentData: debouncedData,
lastSavedData: debouncedData,
invalidCurrentData: {},
invalidDebouncedCurrentData: {},
dataElementId: 'element',
readonly,
isDefault: true,
};
const { proxies } = makeFormDataMethodProxies({ current: true });
const store = createFormDataWriteStore(
{ model },
true,
proxies,
{ rule: { selectedFunction: 'calculate', inputParams: { input: 'input' }, outParams: { outParam0: 'output' } } },
{},
jest.fn(),
);

store.getState().debounce('beforeSave');

expect(calculate).toHaveBeenCalledTimes(readonly ? 0 : 1);
expect(store.getState().dataModels.model.currentData).toEqual({
input: 'new',
output: readonly ? 'original' : 'calculated',
});
});
});
4 changes: 2 additions & 2 deletions src/features/formData/FormDataWriteStateMachine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ function makeActions(

// Run rules again, against current data. Now that we have updates from the backend, some rules may
// have caused data to change.
if (isDefault) {
if (isDefault && !state.dataModels[dataType].readonly) {
const ruleResults = runLegacyRules(
ruleConnections,
savedData[dataType],
Expand Down Expand Up @@ -332,7 +332,7 @@ function makeActions(
continue;
}

if (isDefault) {
if (isDefault && !state.dataModels[dataType].readonly) {
const ruleChanges = runLegacyRules(
ruleConnections,
state.dataModels[dataType].debouncedCurrentData,
Expand Down
4 changes: 2 additions & 2 deletions src/features/instance/useProcessQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ export function useGetTaskTypeById() {

return (taskId: string | undefined) => {
const task =
(processData?.processTasks?.find((t) => t.elementId === taskId) ?? processData?.currentTask?.elementId === taskId)
processData?.currentTask?.elementId === taskId
? processData?.currentTask
: undefined;
: processData?.processTasks?.find((t) => t.elementId === taskId);

if (isStateless || taskId === TaskKeys.CustomReceipt || behavesLikeDataTask(taskId, layoutSets)) {
// Stateless apps only have data tasks. As soon as they start creating an instance from that stateless step,
Expand Down
38 changes: 31 additions & 7 deletions src/features/pdf/PdfWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
import React, { useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import type { PropsWithChildren } from 'react';

import cn from 'classnames';

import { TaskOverrides } from 'src/core/contexts/TaskOverrides';
import { useDevToolsStore } from 'src/features/devtools/data/DevToolsStore';
import { useGetTaskTypeById } from 'src/features/instance/useProcessQuery';
import { FormProvider } from 'src/features/form/FormContext';
import { useCurrentLayoutSet } from 'src/features/form/layoutSets/useCurrentLayoutSet';
import { useInstanceDataElements } from 'src/features/instance/InstanceContext';
import { PdfForServiceTask, PdfFromLayout } from 'src/features/pdf/PdfFromLayout';
import classes from 'src/features/pdf/PDFView.module.css';
import { useNavigationParam } from 'src/hooks/navigation';
import { useIsPdf } from 'src/hooks/useIsPdf';
import { ProcessTaskType } from 'src/types';

export const usePdfModeActive = (): boolean => {
const previewPDF = useDevToolsStore((state) => state.pdfPreview);
const pdfIsSetInUrl = useIsPdf();
return pdfIsSetInUrl || previewPDF;
};

export function PdfWrapper({ children }: PropsWithChildren) {
export function PdfWrapper({ children, isTaskRoute = false }: PropsWithChildren<{ isTaskRoute?: boolean }>) {
const previewPDF = useDevToolsStore((state) => state.pdfPreview);
const setPdfPreview = useDevToolsStore((state) => state.actions.setPdfPreview);
const renderInstead = useIsPdf();

const componentId = useNavigationParam('componentId');
const taskId = useNavigationParam('taskId');
const taskType = useGetTaskTypeById()(taskId);
const layoutSet = useCurrentLayoutSet(taskId);
const requestedElementId = useNavigationParam('dataElementId');
const dataElementId = useInstanceDataElements(layoutSet?.dataType).find((e) => e.id === requestedElementId)?.id;

useEffect(() => {
if (previewPDF) {
if (previewPDF && !isTaskRoute) {
waitForPrint().then((success) => {
if (success) {
window.print();
Expand All @@ -36,14 +42,32 @@ export function PdfWrapper({ children }: PropsWithChildren) {
setPdfPreview(false);
});
}
}, [previewPDF, setPdfPreview]);
}, [previewPDF, setPdfPreview, isTaskRoute]);

const PdfComponent = taskType === ProcessTaskType.Service ? PdfForServiceTask : PdfFromLayout;
const PdfComponent = layoutSet ? PdfFromLayout : PdfForServiceTask;

if (renderInstead) {
if (isTaskRoute && layoutSet) {
// PDF requests select their rendering context independently of the active process task.
// Subforms keep using component routing to select their layout and data element.
return (
<TaskOverrides
taskId={taskId}
dataModelType={layoutSet?.dataType}
dataModelElementId={dataElementId}
>
<FormProvider readOnly>{componentId ? <Outlet /> : <PdfComponent />}</FormProvider>
Comment thread
olemartinorg marked this conversation as resolved.
</TaskOverrides>
);
}
return <PdfComponent />;
}

// The task-route wrapper only intercepts PDF requests. Developer previews render inside the form.
if (isTaskRoute) {
return children;
}

return (
<>
<div className={cn({ [classes.hideInPrint]: previewPDF })}>{children}</div>
Expand Down
10 changes: 9 additions & 1 deletion src/features/pdf/usePdfFormatQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { useAppQueries } from 'src/core/contexts/AppQueriesProvider';
import { type QueryDefinition } from 'src/core/queries/usePrefetchQuery';
import { useCurrentDataModelDataElementId } from 'src/features/datamodel/useBindingSchema';
import { useLaxInstanceId } from 'src/features/instance/InstanceContext';
import { useGetAltinnTaskType, useProcessQuery } from 'src/features/instance/useProcessQuery';
import { useProcessTaskId } from 'src/features/instance/useProcessTaskId';
import { useIsPdf } from 'src/hooks/useIsPdf';
import type { IPdfFormat } from 'src/features/pdf/types';

// Also used for prefetching @see formPrefetcher.ts
Expand All @@ -16,10 +19,15 @@ export function usePdfFormatQueryDef(
dataElementId?: string,
): QueryDefinition<IPdfFormat> {
const { fetchPdfFormat } = useAppQueries();
const taskId = useProcessTaskId();
const currentTaskId = useProcessQuery().data?.currentTask?.elementId;
const taskType = useGetAltinnTaskType()(taskId);
// The obsolete formatter supports current data-task PDFs only.
const isNewPdf = useIsPdf() && (taskId !== currentTaskId || taskType === 'pdf' || taskType === 'subformPdf');
return {
queryKey: ['fetchPdfFormat', { instanceId, dataElementId }],
queryFn: instanceId && dataElementId ? () => fetchPdfFormat(instanceId, dataElementId) : skipToken,
enabled: enabled && !!instanceId && !!dataElementId,
enabled: enabled && !isNewPdf && !!instanceId && !!dataElementId,
gcTime: 0,
};
}
Expand Down
6 changes: 6 additions & 0 deletions src/utils/layout/NodesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@ function ProvideGlobalContext({ children, registry }: PropsWithChildren<{ regist
function useIsInTaskTransition() {
const currentTask = useProcessQuery().data?.currentTask?.elementId;
const taskIdFromUrl = useNavigationParam('taskId');
const readOnly = Store.useSelector((state) => state.readOnly);

// Read-only forms can intentionally render another process task.
if (readOnly) {
return false;
}

if ([TaskKeys.ProcessEnd, TaskKeys.CustomReceipt].includes(taskIdFromUrl as TaskKeys) && !currentTask) {
// Receipt indicates that the process ended - it cannot be compared directly with the taskId
Expand Down
Loading
Loading