diff --git a/src/App.tsx b/src/App.tsx
index 31464dc7ec..235dabde84 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,12 +6,14 @@ import { useAppSelector } from 'src/common/hooks/useAppSelector';
import { Entrypoint } from 'src/features/entrypoint/Entrypoint';
import { PartySelection } from 'src/features/instantiate/containers/PartySelection';
import { UnknownError } from 'src/features/instantiate/containers/UnknownError';
+import { PdfActions } from 'src/features/pdf/data/pdfSlice';
import { makeGetAllowAnonymousSelector } from 'src/selectors/getAllowAnonymous';
import { makeGetHasErrorsSelector } from 'src/selectors/getErrors';
import { selectAppName, selectAppOwner } from 'src/selectors/language';
import { ProcessWrapper } from 'src/shared/containers/ProcessWrapper';
import { QueueActions } from 'src/shared/resources/queue/queueSlice';
import { httpGet } from 'src/utils/network/networking';
+import { shouldGeneratePdf } from 'src/utils/pdf';
import { getEnvironmentLoginUrl, refreshJwtTokenUrl } from 'src/utils/urls/appUrlHelper';
// 1 minute = 60.000ms
@@ -47,6 +49,7 @@ export const App = () => {
window.addEventListener('scroll', refreshJwtToken);
window.addEventListener('onfocus', refreshJwtToken);
window.addEventListener('keydown', refreshJwtToken);
+ window.addEventListener('hashchange', setPdfState);
}
function removeEventListeners() {
@@ -54,6 +57,13 @@ export const App = () => {
window.removeEventListener('scroll', refreshJwtToken);
window.removeEventListener('onfocus', refreshJwtToken);
window.removeEventListener('keydown', refreshJwtToken);
+ window.removeEventListener('hashchange', setPdfState);
+ }
+
+ function setPdfState() {
+ if (shouldGeneratePdf()) {
+ dispatch(PdfActions.pdfStateChanged());
+ }
}
function refreshJwtToken() {
diff --git a/src/__mocks__/initialStateMock.ts b/src/__mocks__/initialStateMock.ts
index d4e0ee917c..4886f91a7a 100644
--- a/src/__mocks__/initialStateMock.ts
+++ b/src/__mocks__/initialStateMock.ts
@@ -80,6 +80,12 @@ export function getInitialStateMock(customStates?: Partial): IRun
parties: [partyMock],
selectedParty: partyMock,
},
+ pdf: {
+ readyForPrint: false,
+ pdfFormat: null,
+ method: null,
+ error: null,
+ },
process: {
error: null,
taskType: null,
@@ -135,8 +141,6 @@ export function getInitialStateMock(customStates?: Partial): IRun
optionState: {
options: {},
error: null,
- optionsCount: 0,
- optionsLoadedCount: 0,
loading: false,
},
dataListState: {
diff --git a/src/components/summary/SummaryComponent.tsx b/src/components/summary/SummaryComponent.tsx
index 19a5a1910d..7dae0e8396 100644
--- a/src/components/summary/SummaryComponent.tsx
+++ b/src/components/summary/SummaryComponent.tsx
@@ -207,7 +207,7 @@ export function SummaryComponent(_props: ISummaryComponent) {
lg={displayGrid?.lg || false}
xl={displayGrid?.xl || false}
data-testid={`summary-${id}`}
- className={cn(pageBreakStyles(summaryComponent ?? formComponent))}
+ className={cn(pageBreakStyles(summaryComponent?.pageBreak ?? formComponent?.pageBreak))}
>
(false);
+ const classes = useStyles();
const handlePopoverClick = (event: React.MouseEvent): void => {
event.stopPropagation();
@@ -39,6 +50,7 @@ export function HelpTextContainer({ language, helpText }: IHelpTextContainerProp
({
+ reducer: (state, action) => {
+ state.layouts = { ...state.layouts, ...action.payload };
+ },
+ }),
},
}));
diff --git a/src/features/form/layout/update/updateFormLayoutSagas.ts b/src/features/form/layout/update/updateFormLayoutSagas.ts
index 0946a9d842..b8430b6b48 100644
--- a/src/features/form/layout/update/updateFormLayoutSagas.ts
+++ b/src/features/form/layout/update/updateFormLayoutSagas.ts
@@ -549,6 +549,8 @@ export function* watchInitialCalculatePageOrderAndMoveToNextPageSaga(): SagaIter
skipMoveToNext: true,
}),
);
+ } else {
+ yield put(FormLayoutActions.calculatePageOrderAndMoveToNextPageRejected({ error: null }));
}
}
}
diff --git a/src/features/pdf/AutomaticPDFLayout.tsx b/src/features/pdf/AutomaticPDFLayout.tsx
deleted file mode 100644
index f0b370569b..0000000000
--- a/src/features/pdf/AutomaticPDFLayout.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import React from 'react';
-
-import type { IPdfFormat } from '.';
-
-import { SummaryComponent } from 'src/components/summary/SummaryComponent';
-import css from 'src/features/pdf/PDFView.module.css';
-import { ComponentType } from 'src/layout';
-import { GenericComponent } from 'src/layout/GenericComponent';
-import { getLayoutComponentObject } from 'src/layout/LayoutComponent';
-import type { ILayoutCompInstanceInformation } from 'src/layout/InstanceInformation/types';
-import type { ComponentExceptGroupAndSummary, RenderableGenericComponent } from 'src/layout/layout';
-import type { LayoutNode, LayoutRootNodeCollection } from 'src/utils/layout/hierarchy';
-
-interface IAutomaticPDFLayout {
- layouts: LayoutRootNodeCollection<'resolved'>;
- pdfFormat: IPdfFormat | null;
- pageOrder: string[];
- hidden: string[];
-}
-
-const AutomaticPDFSummaryComponent = ({
- node,
- pageRef,
- excludedChildren,
-}: {
- node: LayoutNode<'resolved'>;
- pageRef: string;
- excludedChildren: string[];
-}) => {
- const layoutComponent = getLayoutComponentObject(node.item.type as ComponentExceptGroupAndSummary);
-
- if (node.item.type === 'Group' || layoutComponent?.getComponentType() === ComponentType.Form) {
- return (
-
- );
- }
- if (layoutComponent?.getComponentType() === ComponentType.Presentation) {
- return (
-
- );
- }
- return null;
-};
-
-export const AutomaticPDFLayout = ({ layouts, pdfFormat, pageOrder, hidden }: IAutomaticPDFLayout) => {
- const excludedPages = new Set(pdfFormat?.excludedPages);
- const excludedComponents = new Set(pdfFormat?.excludedComponents);
- const hiddenPages = new Set(hidden);
-
- const pdfLayouts = Object.entries(layouts.all())
- .filter(([pageRef]) => !excludedPages.has(pageRef))
- .filter(([pageRef]) => !hiddenPages.has(pageRef))
- .filter(([pageRef]) => pageOrder.includes(pageRef))
- .sort(([pA], [pB]) => pageOrder.indexOf(pA) - pageOrder.indexOf(pB));
-
- const instanceInformationProps: ILayoutCompInstanceInformation = {
- id: '__pdf__instance-information',
- type: 'InstanceInformation',
- elements: {
- dateSent: true,
- sender: true,
- receiver: true,
- referenceNumber: true,
- },
- pageBreak: {
- breakAfter: 'always',
- },
- };
-
- return (
- <>
-
-
-
- {pdfLayouts.map(([pageRef, layout]) => {
- return layout.children().map((node) => {
- if (excludedComponents.has(node.item.id)) {
- return null;
- }
-
- return (
-
- );
- });
- })}
- >
- );
-};
diff --git a/src/features/pdf/CustomPDFLayout.tsx b/src/features/pdf/CustomPDFLayout.tsx
deleted file mode 100644
index 8f39225fbf..0000000000
--- a/src/features/pdf/CustomPDFLayout.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React from 'react';
-
-import { SummaryComponent } from 'src/components/summary/SummaryComponent';
-import { DisplayGroupContainer } from 'src/features/form/containers/DisplayGroupContainer';
-import { mapGroupComponents } from 'src/features/form/containers/formUtils';
-import css from 'src/features/pdf/PDFView.module.css';
-import { ComponentType } from 'src/layout';
-import { GenericComponent } from 'src/layout/GenericComponent';
-import { getLayoutComponentObject } from 'src/layout/LayoutComponent';
-import { topLevelComponents } from 'src/utils/formLayout';
-import type { ComponentExceptGroupAndSummary, ILayout, ILayoutComponentOrGroup } from 'src/layout/layout';
-
-interface ICustomPDFLayout {
- layout: ILayout;
-}
-
-const CustomPDFSummaryComponent = ({ component, layout }: { component: ILayoutComponentOrGroup; layout: ILayout }) => {
- const layoutComponent = getLayoutComponentObject(component.type as ComponentExceptGroupAndSummary);
-
- if (component.type === 'Group') {
- return (
- (
-
- )}
- />
- );
- } else if (component.type === 'Summary') {
- return (
-
- );
- } else if (layoutComponent?.getComponentType() === ComponentType.Presentation) {
- return (
-
- );
- } else {
- console.warn(`Type: "${component.type}" is not allowed in PDF.`);
- return null;
- }
-};
-
-export const CustomPDFLayout = ({ layout }: ICustomPDFLayout) => (
- <>
- {topLevelComponents(layout).map((component) => (
-
-
-
- ))}
- >
-);
diff --git a/src/features/pdf/PDFView.tsx b/src/features/pdf/PDFView.tsx
index d0962f2eed..d368ffb0e6 100644
--- a/src/features/pdf/PDFView.tsx
+++ b/src/features/pdf/PDFView.tsx
@@ -2,92 +2,70 @@ import React from 'react';
import cn from 'classnames';
-import type { IPdfFormat } from '.';
-
import { useAppSelector } from 'src/common/hooks/useAppSelector';
-import { AutomaticPDFLayout } from 'src/features/pdf/AutomaticPDFLayout';
-import { CustomPDFLayout } from 'src/features/pdf/CustomPDFLayout';
+import { SummaryComponent } from 'src/components/summary/SummaryComponent';
+import { DisplayGroupContainer } from 'src/features/form/containers/DisplayGroupContainer';
+import { mapGroupComponents } from 'src/features/form/containers/formUtils';
+import { PDF_LAYOUT_NAME } from 'src/features/pdf/data/pdfSlice';
import css from 'src/features/pdf/PDFView.module.css';
+import { ComponentType } from 'src/layout';
+import { GenericComponent } from 'src/layout/GenericComponent';
+import { getLayoutComponentObject } from 'src/layout/LayoutComponent';
import { ReadyForPrint } from 'src/shared/components/ReadyForPrint';
-import { getCurrentTaskDataElementId } from 'src/utils/appMetadata';
-import { useExprContext } from 'src/utils/layout/ExprContext';
-import { httpGet } from 'src/utils/network/networking';
-import { getPdfFormatUrl } from 'src/utils/urls/appUrlHelper';
+import { topLevelComponents } from 'src/utils/formLayout';
+import type { ComponentExceptGroupAndSummary, ILayout, ILayoutComponentOrGroup } from 'src/layout/layout';
interface PDFViewProps {
appName: string;
appOwner?: string;
}
-export const PDFView = ({ appName, appOwner }: PDFViewProps) => {
- const layouts = useAppSelector((state) => state.formLayout.layouts);
- const layoutSets = useAppSelector((state) => state.formLayout.layoutsets);
- const excludedPages = useAppSelector((state) => state.formLayout.uiConfig.excludePageFromPdf);
- const excludedComponents = useAppSelector((state) => state.formLayout.uiConfig.excludeComponentFromPdf);
- const pageOrder = useAppSelector((state) => state.formLayout.uiConfig.tracks.order);
- const hidden = useAppSelector((state) => state.formLayout.uiConfig.tracks.hidden);
- const pdfLayoutName = useAppSelector((state) => state.formLayout.uiConfig.pdfLayoutName);
- const optionsLoading = useAppSelector((state) => state.optionState.loading);
- const dataListLoading = useAppSelector((state) => state.dataListState.loading);
- const repeatingGroups = useAppSelector((state) => state.formLayout.uiConfig.repeatingGroups);
- const applicationSettings = useAppSelector((state) => state.applicationSettings.applicationSettings);
- const applicationMetadata = useAppSelector((state) => state.applicationMetadata.applicationMetadata);
- const formData = useAppSelector((state) => state.formData.formData);
- const unsavedChanges = useAppSelector((state) => state.formData.unsavedChanges);
- const hiddenFields = useAppSelector((state) => state.formLayout.uiConfig.hiddenFields);
- const parties = useAppSelector((state) => state.party.parties);
- const language = useAppSelector((state) => state.language.language);
- const textResources = useAppSelector((state) => state.textResources.resources);
- const instance = useAppSelector((state) => state.instanceData.instance);
- const allOrgs = useAppSelector((state) => state.organisationMetaData.allOrgs);
- const profile = useAppSelector((state) => state.profile.profile);
- const nodeLayouts = useExprContext();
+const PDFComponent = ({ component, layout }: { component: ILayoutComponentOrGroup; layout: ILayout }) => {
+ const layoutComponent = getLayoutComponentObject(component.type as ComponentExceptGroupAndSummary);
- // Custom pdf layout
- const pdfLayout = pdfLayoutName && layouts ? layouts[pdfLayoutName] : undefined;
+ if (component.type === 'Group') {
+ return (
+ (
+
+ )}
+ />
+ );
+ } else if (component.type === 'Summary') {
+ return (
+
+ );
+ } else if (layoutComponent?.getComponentType() === ComponentType.Presentation) {
+ return (
+
+ );
+ } else {
+ console.warn(`Type: "${component.type}" is not allowed in PDF.`);
+ return null;
+ }
+};
- // Fetch PdfFormat from backend
- const [pdfFormat, setPdfFormat] = React.useState(null);
- React.useEffect(() => {
- if (
- applicationMetadata &&
- instance &&
- (instance?.data?.length === 1 || layoutSets) &&
- excludedPages &&
- excludedComponents &&
- !pdfLayout &&
- !unsavedChanges
- ) {
- const dataGuid = getCurrentTaskDataElementId(applicationMetadata, instance, layoutSets);
- if (typeof dataGuid === 'string') {
- const url = getPdfFormatUrl(instance.id, dataGuid);
- httpGet(url)
- .then((pdfFormat: IPdfFormat) => setPdfFormat(pdfFormat))
- .catch(() => setPdfFormat({ excludedPages, excludedComponents }));
- } else {
- setPdfFormat({ excludedPages, excludedComponents });
- }
- }
- }, [applicationMetadata, excludedComponents, excludedPages, instance, layoutSets, pdfLayout, unsavedChanges]);
+export const PDFView = ({ appName, appOwner }: PDFViewProps) => {
+ const { readyForPrint, method } = useAppSelector((state) => state.pdf);
+ const { layouts, uiConfig } = useAppSelector((state) => state.formLayout);
+
+ const pdfLayoutName = method === 'custom' ? uiConfig.pdfLayoutName : method === 'auto' ? PDF_LAYOUT_NAME : undefined;
+ const pdfLayout = pdfLayoutName && layouts?.[pdfLayoutName];
- if (
- optionsLoading ||
- dataListLoading ||
- !layouts ||
- !hidden ||
- !repeatingGroups ||
- !applicationSettings ||
- !formData ||
- !hiddenFields ||
- !parties ||
- !language ||
- !textResources ||
- !allOrgs ||
- !profile ||
- !pageOrder ||
- (!pdfFormat && !pdfLayout) ||
- !nodeLayouts
- ) {
+ if (!readyForPrint || !pdfLayout) {
return null;
}
@@ -102,16 +80,17 @@ export const PDFView = ({ appName, appOwner }: PDFViewProps) => {
{appOwner}
)}
- {typeof pdfLayout !== 'undefined' ? (
-
- ) : (
-
- )}
+ {topLevelComponents(pdfLayout).map((component) => (
+
+ ))}
);
diff --git a/src/features/pdf/data/generatePdfSagas.ts b/src/features/pdf/data/generatePdfSagas.ts
new file mode 100644
index 0000000000..c5db025c62
--- /dev/null
+++ b/src/features/pdf/data/generatePdfSagas.ts
@@ -0,0 +1,207 @@
+import { all, call, put, race, select, take } from 'redux-saga/effects';
+import type { SagaIterator } from 'redux-saga';
+
+import { FormDataActions } from 'src/features/form/data/formDataSlice';
+import { FormLayoutActions } from 'src/features/form/layout/formLayoutSlice';
+import { PDF_LAYOUT_NAME, PdfActions } from 'src/features/pdf/data/pdfSlice';
+import { ComponentType } from 'src/layout';
+import { getLayoutComponentObject } from 'src/layout/LayoutComponent';
+import { DataListsActions } from 'src/shared/resources/dataLists/dataListsSlice';
+import { InstanceDataActions } from 'src/shared/resources/instanceData/instanceDataSlice';
+import { IsLoadingActions } from 'src/shared/resources/isLoading/isLoadingSlice';
+import { LanguageActions } from 'src/shared/resources/language/languageSlice';
+import { OptionsActions } from 'src/shared/resources/options/optionsSlice';
+import { OrgsActions } from 'src/shared/resources/orgs/orgsSlice';
+import { PartyActions } from 'src/shared/resources/party/partySlice';
+import { QueueActions } from 'src/shared/resources/queue/queueSlice';
+import { TextResourcesActions } from 'src/shared/resources/textResources/textResourcesSlice';
+import { getCurrentTaskDataElementId } from 'src/utils/appMetadata';
+import { httpGet } from 'src/utils/network/networking';
+import { pdfPreviewMode, shouldGeneratePdf } from 'src/utils/pdf';
+import { getPdfFormatUrl } from 'src/utils/urls/appUrlHelper';
+import type { IPdfFormat, IPdfMethod } from 'src/features/pdf/data/types';
+import type { ILayoutCompInstanceInformation } from 'src/layout/InstanceInformation/types';
+import type { ComponentExceptGroupAndSummary, ILayout, ILayoutComponentOrGroup, ILayouts } from 'src/layout/layout';
+import type { ILayoutCompSummary } from 'src/layout/Summary/types.d';
+import type { IApplicationMetadata } from 'src/shared/resources/applicationMetadata';
+import type { ILayoutSets, IRuntimeState, IUiConfig } from 'src/types';
+import type { IInstance } from 'src/types/shared';
+
+const layoutSetsSelector = (state: IRuntimeState) => state.formLayout.layoutsets;
+const layoutsSelector = (state: IRuntimeState) => state.formLayout.layouts;
+const uiConfigSelector = (state: IRuntimeState) => state.formLayout.uiConfig;
+const instanceSelector = (state: IRuntimeState) => state.instanceData.instance;
+const applicationMetadataSelector = (state: IRuntimeState) => state.applicationMetadata.applicationMetadata;
+const pdfFormatSelector = (state: IRuntimeState) => state.pdf.pdfFormat;
+const pdfMethodSelector = (state: IRuntimeState) => state.pdf.method;
+
+function generateAutomaticLayout(pdfFormat: IPdfFormat, uiConfig: IUiConfig, layouts: ILayouts): ILayout {
+ const automaticPdfLayout: ILayout = [];
+
+ const instanceInformation: ILayoutCompInstanceInformation = {
+ id: '__pdf__instance-information',
+ type: 'InstanceInformation',
+ elements: {
+ dateSent: true,
+ sender: true,
+ receiver: true,
+ referenceNumber: true,
+ },
+ pageBreak: {
+ breakAfter: 'always',
+ },
+ };
+ automaticPdfLayout.push(instanceInformation);
+
+ const excludedPages = new Set(pdfFormat?.excludedPages);
+ const excludedComponents = new Set(pdfFormat?.excludedComponents);
+ const hiddenPages = new Set(uiConfig.tracks.hidden);
+ const pageOrder = uiConfig.tracks.order;
+
+ Object.entries(layouts)
+ .filter(([pageRef]) => !excludedPages.has(pageRef))
+ .filter(([pageRef]) => !hiddenPages.has(pageRef))
+ .filter(([pageRef]) => pageOrder?.includes(pageRef))
+ .sort(([pA], [pB]) => (pageOrder ? pageOrder.indexOf(pA) - pageOrder.indexOf(pB) : 0))
+ .flatMap(
+ ([pageRef, components]) =>
+ components?.map((component) => [pageRef, component]) as [string, ILayoutComponentOrGroup][],
+ )
+ .filter(([_, component]) => !excludedComponents.has(component.id))
+ .map(([pageRef, component]) => {
+ const layoutComponent = getLayoutComponentObject(component.type as ComponentExceptGroupAndSummary);
+
+ if (component.type === 'Group' || layoutComponent?.getComponentType() === ComponentType.Form) {
+ return {
+ id: `__pdf__${component.id}`,
+ type: 'Summary',
+ componentRef: component.id,
+ pageRef,
+ excludedChildren: pdfFormat?.excludedComponents,
+ } as ILayoutCompSummary;
+ }
+ if (layoutComponent?.getComponentType() === ComponentType.Presentation) {
+ return {
+ ...component,
+ id: `__pdf__${component.id}`,
+ };
+ }
+ return null;
+ })
+ .forEach((summaryComponent) => {
+ if (summaryComponent !== null) {
+ automaticPdfLayout.push(summaryComponent);
+ }
+ });
+
+ return automaticPdfLayout;
+}
+
+function* generatePdfSaga(): SagaIterator {
+ try {
+ const layouts: ILayouts = yield select(layoutsSelector);
+ const uiConfig: IUiConfig = yield select(uiConfigSelector);
+ const pdfFormat: IPdfFormat = yield select(pdfFormatSelector);
+ const method: IPdfMethod = yield select(pdfMethodSelector);
+
+ if (method == 'auto') {
+ // Automatic layout
+ const pdfLayout = generateAutomaticLayout(pdfFormat, uiConfig, layouts);
+ yield put(FormLayoutActions.updateLayout({ [PDF_LAYOUT_NAME]: pdfLayout }));
+ }
+
+ yield put(PdfActions.generateFulfilled());
+ } catch (error) {
+ yield put(PdfActions.generateRejected({ error }));
+ }
+}
+
+function* fetchPdfFormatSaga(): SagaIterator {
+ const layoutSets: ILayoutSets = yield select(layoutSetsSelector);
+ const uiConfig: IUiConfig = yield select(uiConfigSelector);
+ const instance: IInstance = yield select(instanceSelector);
+ const applicationMetadata: IApplicationMetadata = yield select(applicationMetadataSelector);
+
+ const dataGuid = getCurrentTaskDataElementId(applicationMetadata, instance, layoutSets);
+ let pdfFormat: IPdfFormat;
+ if (typeof dataGuid === 'string') {
+ try {
+ pdfFormat = yield call(httpGet, getPdfFormatUrl(instance.id, dataGuid));
+ } catch {
+ pdfFormat = {
+ excludedPages: uiConfig.excludePageFromPdf ?? [],
+ excludedComponents: uiConfig.excludeComponentFromPdf ?? [],
+ };
+ }
+ } else {
+ pdfFormat = {
+ excludedPages: uiConfig.excludePageFromPdf ?? [],
+ excludedComponents: uiConfig.excludeComponentFromPdf ?? [],
+ };
+ }
+ yield put(PdfActions.pdfFormatFulfilled({ pdfFormat }));
+}
+
+/**
+ * Watches for changes in formdata and calls fetchPdfFormat and regenerates pdf layout if the method is set to automatic
+ */
+export function* watchPdfPreviewSaga(): SagaIterator {
+ while (true) {
+ yield race([take(FormDataActions.submitFulfilled), take(PdfActions.pdfStateChanged)]);
+ const method: IPdfMethod = yield select(pdfMethodSelector);
+ if (method == 'auto' && pdfPreviewMode()) {
+ yield call(fetchPdfFormatSaga);
+ yield call(generatePdfSaga);
+ }
+ }
+}
+
+/**
+ * Checks if all necessary data is loaded before signaling that pdf-generation is ready
+ */
+export function* watchPdfReadySaga(): SagaIterator {
+ yield all([
+ take(PartyActions.getPartiesFulfilled),
+ take(LanguageActions.fetchLanguageFulfilled),
+ take(TextResourcesActions.fetchFulfilled),
+ take(OrgsActions.fetchFulfilled),
+ take(OptionsActions.loaded),
+ take(DataListsActions.loaded),
+ take(IsLoadingActions.finishDataTaskIsLoading),
+ race([
+ take(FormLayoutActions.calculatePageOrderAndMoveToNextPageRejected),
+ take(FormLayoutActions.calculatePageOrderAndMoveToNextPageFulfilled),
+ ]),
+ ]);
+
+ yield put(PdfActions.pdfReady());
+}
+
+export function* watchInitialPdfSaga(): SagaIterator {
+ while (true) {
+ yield race([
+ all([
+ take(QueueActions.startInitialDataTaskQueueFulfilled),
+ take(FormLayoutActions.fetchFulfilled),
+ take(FormLayoutActions.fetchSettingsFulfilled),
+ take(InstanceDataActions.getFulfilled),
+ ]),
+ take(PdfActions.pdfStateChanged),
+ ]);
+ if (shouldGeneratePdf()) {
+ const layouts: ILayouts = yield select(layoutsSelector);
+ const uiConfig: IUiConfig = yield select(uiConfigSelector);
+ const customPdfLayout = uiConfig.pdfLayoutName ? layouts[uiConfig.pdfLayoutName] : undefined;
+ const method = customPdfLayout ? 'custom' : 'auto';
+ yield put(
+ PdfActions.methodFulfilled({
+ method,
+ }),
+ );
+ if (method == 'auto') {
+ yield call(fetchPdfFormatSaga);
+ }
+ yield call(generatePdfSaga);
+ }
+ }
+}
diff --git a/src/features/pdf/data/pdfSlice.ts b/src/features/pdf/data/pdfSlice.ts
new file mode 100644
index 0000000000..483cb03514
--- /dev/null
+++ b/src/features/pdf/data/pdfSlice.ts
@@ -0,0 +1,51 @@
+import { watchInitialPdfSaga, watchPdfPreviewSaga, watchPdfReadySaga } from 'src/features/pdf/data/generatePdfSagas';
+import { createSagaSlice } from 'src/shared/resources/utils/sagaSlice';
+import type { IPdfActionRejected, IPdfFormatFulfilled, IPdfState } from 'src/features/pdf/data/types';
+import type { IPdfMethodFulfilled } from 'src/features/pdf/data/types.d';
+import type { MkActionType } from 'src/shared/resources/utils/sagaSlice';
+
+export const initialState: IPdfState = {
+ readyForPrint: false,
+ pdfFormat: null,
+ method: null,
+ error: null,
+};
+
+export const pdfSlice = createSagaSlice((mkAction: MkActionType) => ({
+ name: 'pdf',
+ initialState,
+ extraSagas: [watchPdfReadySaga, watchPdfPreviewSaga],
+ actions: {
+ initial: mkAction({
+ saga: () => watchInitialPdfSaga,
+ }),
+ generateFulfilled: mkAction({}),
+ pdfReady: mkAction({
+ reducer: (state) => {
+ state.readyForPrint = true;
+ },
+ }),
+ generateRejected: mkAction({
+ reducer: (state, action) => {
+ const { error } = action.payload;
+ state.error = error;
+ },
+ }),
+ methodFulfilled: mkAction({
+ reducer: (state, action) => {
+ const { method } = action.payload;
+ state.method = method;
+ },
+ }),
+ pdfFormatFulfilled: mkAction({
+ reducer: (state, action) => {
+ const { pdfFormat } = action.payload;
+ state.pdfFormat = pdfFormat;
+ },
+ }),
+ pdfStateChanged: mkAction({}),
+ },
+}));
+
+export const PdfActions = pdfSlice.actions;
+export const PDF_LAYOUT_NAME = '__pdf__';
diff --git a/src/features/pdf/data/types.d.ts b/src/features/pdf/data/types.d.ts
new file mode 100644
index 0000000000..b883f3d464
--- /dev/null
+++ b/src/features/pdf/data/types.d.ts
@@ -0,0 +1,25 @@
+export type IPdfMethod = 'auto' | 'custom';
+
+export interface IPdfState {
+ readyForPrint: boolean;
+ pdfFormat: IPdfFormat | null;
+ method: IPdfMethod | null;
+ error: Error | null;
+}
+
+export interface IPdfActionRejected {
+ error: Error | null;
+}
+
+export interface IPdfMethodFulfilled {
+ method: IPdfMethod;
+}
+
+export interface IPdfFormatFulfilled {
+ pdfFormat: IPdfFormat;
+}
+
+export interface IPdfFormat {
+ excludedPages: string[];
+ excludedComponents: string[];
+}
diff --git a/src/features/pdf/index.d.ts b/src/features/pdf/index.d.ts
deleted file mode 100644
index 4002c6038b..0000000000
--- a/src/features/pdf/index.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export interface IPdfFormat {
- excludedPages: string[];
- excludedComponents: string[];
-}
diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.test.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.test.tsx
index b96b7f899f..ce00932f84 100644
--- a/src/layout/Checkboxes/CheckboxesContainerComponent.test.tsx
+++ b/src/layout/Checkboxes/CheckboxesContainerComponent.test.tsx
@@ -61,8 +61,6 @@ const render = (props: Partial = {}, customState: Prelo
name: '',
message: '',
},
- optionsCount: 2,
- optionsLoadedCount: 1,
loading: true,
},
...customState,
diff --git a/src/layout/Dropdown/DropdownComponent.test.tsx b/src/layout/Dropdown/DropdownComponent.test.tsx
index 33009e7806..566fd0e145 100644
--- a/src/layout/Dropdown/DropdownComponent.test.tsx
+++ b/src/layout/Dropdown/DropdownComponent.test.tsx
@@ -55,8 +55,6 @@ const render = (props: Partial = {}, customState: PreloadedState
name: '',
message: '',
},
- optionsCount: 2,
- optionsLoadedCount: 1,
loading: true,
},
},
diff --git a/src/layout/FileUploadWithTag/AttachmentWithTagSummaryComponent.test.tsx b/src/layout/FileUploadWithTag/AttachmentWithTagSummaryComponent.test.tsx
index cb0e7e7481..9f91cdc43b 100644
--- a/src/layout/FileUploadWithTag/AttachmentWithTagSummaryComponent.test.tsx
+++ b/src/layout/FileUploadWithTag/AttachmentWithTagSummaryComponent.test.tsx
@@ -104,8 +104,6 @@ describe('AttachmentWithTagSummaryComponent', () => {
],
},
},
- optionsCount: 4,
- optionsLoadedCount: 4,
loading: false,
},
};
diff --git a/src/layout/GenericComponent.tsx b/src/layout/GenericComponent.tsx
index 3a6cb12509..cc35a1194e 100644
--- a/src/layout/GenericComponent.tsx
+++ b/src/layout/GenericComponent.tsx
@@ -309,7 +309,7 @@ export function GenericComponent(
'a-form-group',
classes.container,
gridToClasses(props.grid?.labelGrid, classes),
- pageBreakStyles(evaluatedProps),
+ pageBreakStyles(evaluatedProps?.pageBreak),
)}
alignItems='baseline'
>
diff --git a/src/layout/Likert/GroupContainerLikertTestUtils.tsx b/src/layout/Likert/GroupContainerLikertTestUtils.tsx
index fa419308d3..0c45ad2597 100644
--- a/src/layout/Likert/GroupContainerLikertTestUtils.tsx
+++ b/src/layout/Likert/GroupContainerLikertTestUtils.tsx
@@ -242,8 +242,6 @@ export const render = ({
},
},
error: null,
- optionsCount: 1,
- optionsLoadedCount: 1,
loading: false,
},
});
diff --git a/src/layout/RadioButtons/RadioButtonsContainerComponent.test.tsx b/src/layout/RadioButtons/RadioButtonsContainerComponent.test.tsx
index 8c09a47dc8..3bd0c8d81d 100644
--- a/src/layout/RadioButtons/RadioButtonsContainerComponent.test.tsx
+++ b/src/layout/RadioButtons/RadioButtonsContainerComponent.test.tsx
@@ -60,8 +60,6 @@ const render = (props: Partial = {}, customState: P
name: '',
message: '',
},
- optionsCount: 2,
- optionsLoadedCount: 1,
loading: true,
},
...customState,
diff --git a/src/reducers/index.ts b/src/reducers/index.ts
index 9764d8a93d..8dd2008383 100644
--- a/src/reducers/index.ts
+++ b/src/reducers/index.ts
@@ -8,6 +8,7 @@ import { formLayoutSlice } from 'src/features/form/layout/formLayoutSlice';
import { formRulesSlice } from 'src/features/form/rules/rulesSlice';
import { validationSlice } from 'src/features/form/validation/validationSlice';
import { instantiationSlice } from 'src/features/instantiate/instantiation/instantiationSlice';
+import { pdfSlice } from 'src/features/pdf/data/pdfSlice';
import { appApi } from 'src/services/AppApi';
import { applicationMetadataSlice } from 'src/shared/resources/applicationMetadata/applicationMetadataSlice';
import { applicationSettingsSlice } from 'src/shared/resources/applicationSettings/applicationSettingsSlice';
@@ -40,6 +41,7 @@ const reducers = {
[languageSlice.name]: languageSlice.reducer,
[orgsSlice.name]: orgsSlice.reducer,
[partySlice.name]: partySlice.reducer,
+ [pdfSlice.name]: pdfSlice.reducer,
[processSlice.name]: processSlice.reducer,
[profileSlice.name]: profileSlice.reducer,
[queueSlice.name]: queueSlice.reducer,
diff --git a/src/selectors/getErrors.ts b/src/selectors/getErrors.ts
index 121961fa41..0d0a690384 100644
--- a/src/selectors/getErrors.ts
+++ b/src/selectors/getErrors.ts
@@ -31,6 +31,7 @@ const getHasErrorsSelector = (state: IRuntimeState) => {
state.optionState.error ||
state.attachments.error ||
state.dataListState.error ||
+ state.pdf.error ||
// we have a few special cases where we allow 404 status codes but not other errors
exceptIfIncludes(state.applicationSettings.error, '404') ||
exceptIfIncludes(state.textResources.error, '404') ||
diff --git a/src/shared/resources/dataLists/dataListsSlice.ts b/src/shared/resources/dataLists/dataListsSlice.ts
index e523b18f1f..1f66bc3328 100644
--- a/src/shared/resources/dataLists/dataListsSlice.ts
+++ b/src/shared/resources/dataLists/dataListsSlice.ts
@@ -1,8 +1,7 @@
-import { fetchDataListsSaga } from 'src/shared/resources/dataLists/fetchDataListsSaga';
+import { fetchDataListsSaga, watchFinishedLoadingSaga } from 'src/shared/resources/dataLists/fetchDataListsSaga';
import { createSagaSlice } from 'src/shared/resources/utils/sagaSlice';
import type {
IDataListsState,
- IFetchDataListCountFulfilledAction,
IFetchDataListsFulfilledAction,
IFetchDataListsRejectedAction,
IFetchingDataListsAction,
@@ -26,18 +25,14 @@ const initialState: IDataListsState = {
export const dataListsSlice = createSagaSlice((mkAction: MkActionType) => ({
name: 'dataListState',
initialState,
+ extraSagas: [watchFinishedLoadingSaga],
actions: {
fetch: mkAction({
takeEvery: fetchDataListsSaga,
}),
- dataListCountFulfilled: mkAction({
- reducer: (state, action) => {
- const { count } = action.payload;
- if (count <= 0) {
- state.loading = false;
- } else {
- state.dataListCount = count;
- }
+ loaded: mkAction({
+ reducer: (state) => {
+ state.loading = false;
},
}),
fetchFulfilled: mkAction({
@@ -46,12 +41,6 @@ export const dataListsSlice = createSagaSlice((mkAction: MkActionType({
@@ -59,12 +48,6 @@ export const dataListsSlice = createSagaSlice((mkAction: MkActionType({
diff --git a/src/shared/resources/dataLists/fetchDataListsSaga.ts b/src/shared/resources/dataLists/fetchDataListsSaga.ts
index 669de302a4..e16096144f 100644
--- a/src/shared/resources/dataLists/fetchDataListsSaga.ts
+++ b/src/shared/resources/dataLists/fetchDataListsSaga.ts
@@ -1,5 +1,5 @@
import { SortDirection } from '@altinn/altinn-design-system';
-import { call, fork, put, select } from 'redux-saga/effects';
+import { call, fork, put, race, select, take } from 'redux-saga/effects';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { SagaIterator } from 'redux-saga';
@@ -29,12 +29,28 @@ export const dataListsWithIndexIndicatorsSelector = (state: IRuntimeState) =>
export const instanceIdSelector = (state: IRuntimeState): string | undefined => state.instanceData.instance?.id;
export const repeatingGroupsSelector = (state: IRuntimeState) => state.formLayout?.uiConfig.repeatingGroups;
+export function* watchFinishedLoadingSaga(): SagaIterator {
+ let dataListCount = 0;
+ let fulfilledCount = 0;
+ while (true) {
+ const [fetch, fulfilled] = yield race([take(DataListsActions.fetching), take(DataListsActions.fetchFulfilled)]);
+ if (fetch) {
+ dataListCount++;
+ }
+ if (fulfilled) {
+ fulfilledCount++;
+ }
+ if (dataListCount === fulfilledCount) {
+ yield put(DataListsActions.loaded());
+ }
+ }
+}
+
export function* fetchDataListsSaga(): SagaIterator {
const layouts: ILayouts = yield selectNotNull(formLayoutSelector);
const repeatingGroups: IRepeatingGroups = yield selectNotNull(repeatingGroupsSelector);
const fetchedDataLists: string[] = [];
const dataListsWithIndexIndicators: IDataListsMetaData[] = [];
- let count = 0;
for (const layoutId of Object.keys(layouts)) {
for (const element of layouts[layoutId] || []) {
if (element.type !== 'List' || !element.id) {
@@ -69,13 +85,14 @@ export function* fetchDataListsSaga(): SagaIterator {
secure,
paginationDefaultValue: paginationDefault,
});
- count++;
fetchedDataLists.push(lookupKey);
}
}
}
}
- yield put(DataListsActions.dataListCountFulfilled({ count }));
+ if (fetchedDataLists.length == 0) {
+ yield put(DataListsActions.loaded());
+ }
yield put(
DataListsActions.setDataListsWithIndexIndicators({
dataListsWithIndexIndicators,
diff --git a/src/shared/resources/options/fetch/fetchOptionsSagas.ts b/src/shared/resources/options/fetch/fetchOptionsSagas.ts
index e2a815cb8f..f251ba67bc 100644
--- a/src/shared/resources/options/fetch/fetchOptionsSagas.ts
+++ b/src/shared/resources/options/fetch/fetchOptionsSagas.ts
@@ -1,4 +1,4 @@
-import { call, fork, put, select } from 'redux-saga/effects';
+import { call, fork, put, race, select, take } from 'redux-saga/effects';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { SagaIterator } from 'redux-saga';
@@ -34,12 +34,28 @@ export const optionsWithIndexIndicatorsSelector = (state: IRuntimeState) =>
export const instanceIdSelector = (state: IRuntimeState): string | undefined => state.instanceData.instance?.id;
export const repeatingGroupsSelector = (state: IRuntimeState) => state.formLayout?.uiConfig.repeatingGroups;
+export function* watchFinishedLoadingSaga(): SagaIterator {
+ let optionCount = 0;
+ let fulfilledCount = 0;
+ while (true) {
+ const [fetch, fulfilled] = yield race([take(OptionsActions.fetching), take(OptionsActions.fetchFulfilled)]);
+ if (fetch) {
+ optionCount++;
+ }
+ if (fulfilled) {
+ fulfilledCount++;
+ }
+ if (optionCount === fulfilledCount) {
+ yield put(OptionsActions.loaded());
+ }
+ }
+}
+
export function* fetchOptionsSaga(): SagaIterator {
const layouts: ILayouts = yield selectNotNull(formLayoutSelector);
const repeatingGroups: IRepeatingGroups = yield selectNotNull(repeatingGroupsSelector);
const fetchedOptions: string[] = [];
const optionsWithIndexIndicators: IOptionsMetaData[] = [];
- let count = 0;
for (const layoutId of Object.keys(layouts)) {
for (const element of layouts[layoutId] || []) {
const { optionsId, mapping, secure } = element as ISelectionComponentProps;
@@ -72,13 +88,14 @@ export function* fetchOptionsSaga(): SagaIterator {
dataMapping: mapping,
secure,
});
- count++;
fetchedOptions.push(lookupKey);
}
}
}
}
- yield put(OptionsActions.optionCountFulfilled({ count }));
+ if (fetchedOptions.length == 0) {
+ yield put(OptionsActions.loaded());
+ }
yield put(
OptionsActions.setOptionsWithIndexIndicators({
optionsWithIndexIndicators,
diff --git a/src/shared/resources/options/index.d.ts b/src/shared/resources/options/index.d.ts
index cb6a7fdf79..2bea55c42a 100644
--- a/src/shared/resources/options/index.d.ts
+++ b/src/shared/resources/options/index.d.ts
@@ -4,8 +4,6 @@ export interface IOptionsState {
error: Error | null;
options: IOptions;
optionsWithIndexIndicators?: IOptionsMetaData[];
- optionsCount: number;
- optionsLoadedCount: number;
loading: boolean;
}
diff --git a/src/shared/resources/options/optionsSlice.ts b/src/shared/resources/options/optionsSlice.ts
index 54482c8d12..7899a7db82 100644
--- a/src/shared/resources/options/optionsSlice.ts
+++ b/src/shared/resources/options/optionsSlice.ts
@@ -1,8 +1,7 @@
-import { fetchOptionsSaga } from 'src/shared/resources/options/fetch/fetchOptionsSagas';
+import { fetchOptionsSaga, watchFinishedLoadingSaga } from 'src/shared/resources/options/fetch/fetchOptionsSagas';
import { createSagaSlice } from 'src/shared/resources/utils/sagaSlice';
import type {
IFetchingOptionsAction,
- IFetchOptionsCountFulfilledAction,
IFetchOptionsFulfilledAction,
IFetchOptionsRejectedAction,
IOptionsState,
@@ -15,26 +14,20 @@ const initialState: IOptionsState = {
options: {},
optionsWithIndexIndicators: [],
error: null,
- optionsCount: 0,
- optionsLoadedCount: 0,
loading: true,
};
export const optionsSlice = createSagaSlice((mkAction: MkActionType) => ({
name: 'optionState',
initialState,
+ extraSagas: [watchFinishedLoadingSaga],
actions: {
fetch: mkAction({
takeEvery: fetchOptionsSaga,
}),
- optionCountFulfilled: mkAction({
- reducer: (state, action) => {
- const { count } = action.payload;
- if (count <= 0) {
- state.loading = false;
- } else {
- state.optionsCount = count;
- }
+ loaded: mkAction({
+ reducer: (state) => {
+ state.loading = false;
},
}),
fetchFulfilled: mkAction({
@@ -45,12 +38,6 @@ export const optionsSlice = createSagaSlice((mkAction: MkActionType({
@@ -61,12 +48,6 @@ export const optionsSlice = createSagaSlice((mkAction: MkActionType({
diff --git a/src/shared/resources/queue/queueSlice.ts b/src/shared/resources/queue/queueSlice.ts
index b4d29bdb5e..3d5034bad3 100644
--- a/src/shared/resources/queue/queueSlice.ts
+++ b/src/shared/resources/queue/queueSlice.ts
@@ -5,6 +5,7 @@ import { FooterLayoutActions } from 'src/features/footer/data/footerLayoutSlice'
import { FormDataActions } from 'src/features/form/data/formDataSlice';
import { DataModelActions } from 'src/features/form/datamodel/datamodelSlice';
import { FormLayoutActions } from 'src/features/form/layout/formLayoutSlice';
+import { PdfActions } from 'src/features/pdf/data/pdfSlice';
import { ApplicationMetadataActions } from 'src/shared/resources/applicationMetadata/applicationMetadataSlice';
import { ApplicationSettingsActions } from 'src/shared/resources/applicationSettings/applicationSettingsSlice';
import { AttachmentActions } from 'src/shared/resources/attachments/attachmentSlice';
@@ -104,6 +105,7 @@ export const queueSlice = createSagaSlice((mkAction: MkActionType)
yield put(DataModelActions.fetchJsonSchema());
yield put(FormLayoutActions.fetch());
yield put(FormLayoutActions.fetchSettings());
+ yield put(PdfActions.initial());
yield put(AttachmentActions.mapAttachments());
yield put(QueueActions.startInitialDataTaskQueueFulfilled());
},
diff --git a/src/styles/tjenester3.css b/src/styles/tjenester3.css
index 78f674e0e0..0a0ff55f81 100644
--- a/src/styles/tjenester3.css
+++ b/src/styles/tjenester3.css
@@ -12481,10 +12481,6 @@ a.text-dark:focus {
box-shadow: none !important;
}
- a:not(.btn) {
- text-decoration: underline;
- }
-
abbr[title]::after {
content: ' (' attr(title) ')';
}
diff --git a/src/utils/formComponentUtils.ts b/src/utils/formComponentUtils.ts
index 9a9da0a8b5..425e885900 100644
--- a/src/utils/formComponentUtils.ts
+++ b/src/utils/formComponentUtils.ts
@@ -13,6 +13,7 @@ import { getTextFromAppOrDefault } from 'src/utils/textResource';
import type { IFormData } from 'src/features/form/data';
import type { ILayoutGroup } from 'src/layout/Group/types';
import type { IDataModelBindings, IGridStyling, ILayoutComponent, ISelectionComponentProps } from 'src/layout/layout';
+import type { IPageBreak } from 'src/layout/layout.d';
import type { IAttachment, IAttachments } from 'src/shared/resources/attachments';
import type {
IComponentValidations,
@@ -24,7 +25,6 @@ import type {
IValidations,
} from 'src/types';
import type { ILanguage } from 'src/types/shared';
-import type { AnyItem } from 'src/utils/layout/hierarchy.types';
export const componentHasValidationMessages = (componentValidations: IComponentValidations | undefined) => {
if (!componentValidations) {
@@ -546,17 +546,17 @@ export const gridBreakpoints = (grid?: IGridStyling) => {
};
};
-export const pageBreakStyles = (component: AnyItem<'resolved'> | undefined) => {
- if (!component?.pageBreak) {
+export const pageBreakStyles = (pageBreak: IPageBreak | undefined) => {
+ if (!pageBreak) {
return {};
}
return {
- [printStyles['break-before-auto']]: component.pageBreak.breakBefore === 'auto',
- [printStyles['break-before-always']]: component.pageBreak.breakBefore === 'always',
- [printStyles['break-before-avoid']]: component.pageBreak.breakBefore === 'avoid',
- [printStyles['break-after-auto']]: component.pageBreak.breakAfter === 'auto',
- [printStyles['break-after-always']]: component.pageBreak.breakAfter === 'always',
- [printStyles['break-after-avoid']]: component.pageBreak.breakAfter === 'avoid',
+ [printStyles['break-before-auto']]: pageBreak.breakBefore === 'auto',
+ [printStyles['break-before-always']]: pageBreak.breakBefore === 'always',
+ [printStyles['break-before-avoid']]: pageBreak.breakBefore === 'avoid',
+ [printStyles['break-after-auto']]: pageBreak.breakAfter === 'auto',
+ [printStyles['break-after-always']]: pageBreak.breakAfter === 'always',
+ [printStyles['break-after-avoid']]: pageBreak.breakAfter === 'avoid',
};
};
diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts
new file mode 100644
index 0000000000..f64d51add7
--- /dev/null
+++ b/src/utils/pdf.ts
@@ -0,0 +1,13 @@
+function getPdfQueryParam(): string | null {
+ const params = new URLSearchParams(window.location.hash.split('?')[1]);
+ return params.get('pdf');
+}
+
+export function pdfPreviewMode(): boolean {
+ return getPdfQueryParam() === 'preview';
+}
+
+export function shouldGeneratePdf(): boolean {
+ const pdfQueryParam = getPdfQueryParam();
+ return pdfQueryParam === '1' || pdfQueryParam === 'preview';
+}