diff --git a/schemas/json/text-resources/text-resources.schema.v1.json b/schemas/json/text-resources/text-resources.schema.v1.json
index 48a811651b..b15318ea1a 100644
--- a/schemas/json/text-resources/text-resources.schema.v1.json
+++ b/schemas/json/text-resources/text-resources.schema.v1.json
@@ -79,7 +79,7 @@
"type": "string",
"title": "Variable lookup location",
"description": "Location of the variable in the data source",
- "pattern": "^[\\w\\p{L}.\\-_{}[\\]]+$"
+ "pattern": "^[\\w\\p{L}.\\-_{}\\[\\]]+$"
},
"dataSource": {
"title": "Variable data source",
diff --git a/src/App.tsx b/src/App.tsx
index fc91c3c0e8..a008ce8c16 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -8,7 +8,9 @@ import { UnknownError } from 'src/features/instantiate/containers/UnknownError';
import { QueueActions } from 'src/features/queue/queueSlice';
import { useApplicationMetadataQuery } from 'src/hooks/queries/useApplicationMetadataQuery';
import { useApplicationSettingsQuery } from 'src/hooks/queries/useApplicationSettingsQuery';
+import { useCurrentDataModelSchemaQuery } from 'src/hooks/queries/useCurrentDataModelSchemaQuery';
import { useFooterLayoutQuery } from 'src/hooks/queries/useFooterLayoutQuery';
+import { useFormDataQuery } from 'src/hooks/queries/useFormDataQuery';
import { useCurrentPartyQuery } from 'src/hooks/queries/useGetCurrentPartyQuery';
import { usePartiesQuery } from 'src/hooks/queries/useGetPartiesQuery';
import { useLayoutSetsQuery } from 'src/hooks/queries/useLayoutSetsQuery';
@@ -31,6 +33,7 @@ export const App = () => {
const { isError: hasLayoutSetError } = useLayoutSetsQuery();
const { isError: hasOrgsError } = useOrgsQuery();
useFooterLayoutQuery(!!applicationMetadata?.features?.footer);
+ useCurrentDataModelSchemaQuery();
const componentIsReady = applicationSettings && applicationMetadata;
const componentHasError =
@@ -59,10 +62,10 @@ type AppInternalProps = {
const AppInternal = ({ applicationSettings }: AppInternalProps): JSX.Element | null => {
const allowAnonymousSelector = makeGetAllowAnonymousSelector();
- const allowAnonymous: boolean = useAppSelector(allowAnonymousSelector);
+ const allowAnonymous = useAppSelector(allowAnonymousSelector);
- const { isError: hasProfileError } = useProfileQuery(allowAnonymous === false);
- const { isError: hasPartiesError } = usePartiesQuery(allowAnonymous === false);
+ const { isError: hasProfileError, isFetching: isProfileFetching } = useProfileQuery(allowAnonymous === false);
+ const { isError: hasPartiesError, isFetching: isPartiesFetching } = usePartiesQuery(allowAnonymous === false);
const alwaysPromptForParty = useAlwaysPromptForParty();
@@ -75,8 +78,10 @@ const AppInternal = ({ applicationSettings }: AppInternalProps): JSX.Element | n
useKeepAlive(applicationSettings.appOidcProvider, allowAnonymous);
useUpdatePdfState(allowAnonymous);
+ const { isFetching: isFormDataFetching } = useFormDataQuery();
const hasComponentError = hasProfileError || hasCurrentPartyError || hasPartiesError;
+ const isFetching = isProfileFetching || isPartiesFetching || isFormDataFetching;
// Set the title of the app
React.useEffect(() => {
@@ -104,7 +109,7 @@ const AppInternal = ({ applicationSettings }: AppInternalProps): JSX.Element | n
/>
}
+ element={}
/>
>
diff --git a/src/components/wrappers/ProcessWrapper.tsx b/src/components/wrappers/ProcessWrapper.tsx
index 80baeeb4d6..f82a87e7e1 100644
--- a/src/components/wrappers/ProcessWrapper.tsx
+++ b/src/components/wrappers/ProcessWrapper.tsx
@@ -24,7 +24,11 @@ import { ProcessTaskType } from 'src/types';
import { behavesLikeDataTask } from 'src/utils/formLayout';
import { checkIfAxiosError, HttpStatusCodes } from 'src/utils/network/networking';
-export const ProcessWrapper = () => {
+export interface IProcessWrapperProps {
+ isFetching?: boolean;
+}
+
+export const ProcessWrapper = ({ isFetching }: IProcessWrapperProps) => {
const instantiating = useAppSelector((state) => state.instantiation.instantiating);
const isLoading = useAppSelector((state) => state.isLoading.dataTask);
const layoutSets = useAppSelector((state) => state.formLayout.layoutsets);
@@ -87,7 +91,7 @@ export const ProcessWrapper = () => {
appOwner={appOwner}
type={taskType}
>
- {isLoading === false ? (
+ {isLoading === false && isFetching !== true ? (
<>
{taskType === ProcessTaskType.Data || behavesLikeDataTask(process.taskId, layoutSets) ? (
diff --git a/src/features/datamodel/datamodelSlice.ts b/src/features/datamodel/datamodelSlice.ts
index 6a96f61ec6..27f00eb84e 100644
--- a/src/features/datamodel/datamodelSlice.ts
+++ b/src/features/datamodel/datamodelSlice.ts
@@ -1,4 +1,3 @@
-import { watchFetchJsonSchemaSaga } from 'src/features/datamodel/fetchFormDatamodelSagas';
import { createSagaSlice } from 'src/redux/sagaSlice';
import type {
IDataModelState,
@@ -18,16 +17,13 @@ export const formDataModelSlice = () => {
name: 'formDataModel',
initialState,
actions: {
- fetchJsonSchema: mkAction({
- saga: () => watchFetchJsonSchemaSaga,
- }),
- fetchJsonSchemaFulfilled: mkAction({
+ fetchFulfilled: mkAction({
reducer: (state, action) => {
const { schema, id } = action.payload;
state.schemas[id] = schema;
},
}),
- fetchJsonSchemaRejected: mkAction({
+ fetchRejected: mkAction({
reducer: (state, action) => {
const { error } = action.payload;
state.error = error;
diff --git a/src/features/datamodel/fetchFormDatamodelSagas.ts b/src/features/datamodel/fetchFormDatamodelSagas.ts
deleted file mode 100644
index 6afd58f68a..0000000000
--- a/src/features/datamodel/fetchFormDatamodelSagas.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { all, call, put, select, take } from 'redux-saga/effects';
-import type { SagaIterator } from 'redux-saga';
-
-import { ApplicationMetadataActions } from 'src/features/applicationMetadata/applicationMetadataSlice';
-import { DataModelActions } from 'src/features/datamodel/datamodelSlice';
-import { InstanceDataActions } from 'src/features/instanceData/instanceDataSlice';
-import { FormLayoutActions } from 'src/features/layout/formLayoutSlice';
-import { QueueActions } from 'src/features/queue/queueSlice';
-import { getCurrentDataTypeForApplication, isStatelessApp } from 'src/utils/appMetadata';
-import { httpGet } from 'src/utils/network/networking';
-import { getJsonSchemaUrl } from 'src/utils/urls/appUrlHelper';
-import type { IApplicationMetadata } from 'src/features/applicationMetadata';
-import type { ILayoutSets, IRuntimeState } from 'src/types';
-import type { IInstance } from 'src/types/shared';
-
-const AppMetadataSelector: (state: IRuntimeState) => IApplicationMetadata | null = (state: IRuntimeState) =>
- state.applicationMetadata.applicationMetadata;
-const InstanceDataSelector = (state: IRuntimeState) => state.instanceData.instance;
-
-function* fetchJsonSchemaSaga(): SagaIterator {
- try {
- const url = getJsonSchemaUrl();
- const appMetadata: IApplicationMetadata | null = yield select(AppMetadataSelector);
- const instance: IInstance | null = yield select(InstanceDataSelector);
- const layoutSets: ILayoutSets | null = yield select((state: IRuntimeState) => state.formLayout.layoutsets);
-
- const dataTypeId = getCurrentDataTypeForApplication({
- application: appMetadata,
- instance,
- layoutSets,
- });
-
- if (dataTypeId) {
- const schema: any = yield call(httpGet, url + dataTypeId);
- yield put(DataModelActions.fetchJsonSchemaFulfilled({ schema, id: dataTypeId }));
- }
- } catch (error) {
- yield put(DataModelActions.fetchJsonSchemaRejected({ error }));
- yield put(QueueActions.dataTaskQueueError({ error }));
- window.logError('Fetching JSON schema failed:\n', error);
- }
-}
-
-export function* watchFetchJsonSchemaSaga(): SagaIterator {
- yield all([
- take(ApplicationMetadataActions.getFulfilled),
- take(FormLayoutActions.fetchSetsFulfilled),
- take(DataModelActions.fetchJsonSchema),
- ]);
- const application: IApplicationMetadata = yield select(
- (state: IRuntimeState) => state.applicationMetadata.applicationMetadata,
- );
- if (isStatelessApp(application)) {
- yield call(fetchJsonSchemaSaga);
- while (true) {
- yield take(DataModelActions.fetchJsonSchema);
- yield call(fetchJsonSchemaSaga);
- }
- } else {
- yield call(fetchJsonSchemaSaga);
- while (true) {
- yield all([take(InstanceDataActions.getFulfilled), take(DataModelActions.fetchJsonSchema)]);
- yield call(fetchJsonSchemaSaga);
- }
- }
-}
diff --git a/src/features/datamodel/index.d.ts b/src/features/datamodel/index.d.ts
index 776548f30d..8d8cd261df 100644
--- a/src/features/datamodel/index.d.ts
+++ b/src/features/datamodel/index.d.ts
@@ -1,5 +1,7 @@
+import type { JSONSchema7 } from 'json-schema';
+
export interface IJsonSchemas {
- [id: string]: object;
+ [id: string]: JSONSchema7;
}
export interface IDataModelState {
@@ -8,10 +10,10 @@ export interface IDataModelState {
}
export interface IFetchJsonSchemaFulfilled {
- schema: object;
+ schema: JSONSchema7;
id: string;
}
export interface IFetchJsonSchemaRejected {
- error: Error;
+ error: Error | null;
}
diff --git a/src/features/entrypoint/Entrypoint.test.tsx b/src/features/entrypoint/Entrypoint.test.tsx
index 40c0b00ba6..bf088092a3 100644
--- a/src/features/entrypoint/Entrypoint.test.tsx
+++ b/src/features/entrypoint/Entrypoint.test.tsx
@@ -8,6 +8,7 @@ import type { AxiosError } from 'axios';
import { getInitialStateMock } from 'src/__mocks__/initialStateMock';
import { Entrypoint } from 'src/features/entrypoint/Entrypoint';
import { renderWithProviders } from 'src/testUtils';
+import type { AppQueriesContext } from 'src/contexts/appQueriesContext';
import type { IApplicationMetadata } from 'src/features/applicationMetadata';
import type { IRuntimeState } from 'src/types';
import type { IApplicationLogic } from 'src/types/shared';
@@ -35,11 +36,12 @@ describe('Entrypoint', () => {
});
test('should show invalid party error if user has no valid parties', async () => {
- const doPartyValidation = () => Promise.resolve({ data: { valid: false, validParties: [], message: '' } });
- const queries = {
- doPartyValidation,
- };
- render({ store: mockStore, queries });
+ render({
+ store: mockStore,
+ queries: {
+ doPartyValidation: () => Promise.resolve({ valid: false, validParties: [], message: '' }),
+ },
+ });
await waitForElementToBeRemoved(screen.queryByText('Vent litt, vi henter det du trenger'));
@@ -60,10 +62,6 @@ describe('Entrypoint', () => {
});
test('should show loader while fetching data then start statelessQueue if stateless app', async () => {
- const doPartyValidation = () => Promise.resolve({ data: { valid: true, validParties: [], message: '' } });
- const queries = {
- doPartyValidation,
- };
const statelessApplication: IApplicationMetadata = {
...(mockInitialState.applicationMetadata.applicationMetadata as IApplicationMetadata),
onEntry: {
@@ -77,7 +75,13 @@ describe('Entrypoint', () => {
mockStore = createStore(mockReducer, mockStateWithStatelessApplication);
mockStore.dispatch = jest.fn();
- render({ store: mockStore, queries, allowAnonymous: false });
+ render({
+ store: mockStore,
+ queries: {
+ doPartyValidation: () => Promise.resolve({ valid: true, validParties: [], message: '' }),
+ },
+ allowAnonymous: false,
+ });
const contentLoader = await screen.findByText('Loading...');
expect(contentLoader).not.toBeNull();
@@ -131,25 +135,25 @@ describe('Entrypoint', () => {
mockStore = createStore(mockReducer, mockStateWithStatelessApplication);
mockStore.dispatch = jest.fn();
- const doPartyValidation = () => Promise.resolve({ data: { valid: true, validParties: [], message: '' } });
-
- const queries = {
- doPartyValidation,
- fetchActiveInstances: () =>
- Promise.resolve([
- {
- id: 'some-id-1',
- lastChanged: '28-01-1992',
- lastChangedBy: 'Navn Navnesen',
- },
- {
- id: 'some-id-2',
- lastChanged: '06-03-1974',
- lastChangedBy: 'Test Testesen',
- },
- ]),
- };
- render({ store: mockStore, queries });
+ render({
+ store: mockStore,
+ queries: {
+ doPartyValidation: () => Promise.resolve({ valid: true, validParties: [], message: '' }),
+ fetchActiveInstances: () =>
+ Promise.resolve([
+ {
+ id: 'some-id-1',
+ lastChanged: '28-01-1992',
+ lastChangedBy: 'Navn Navnesen',
+ },
+ {
+ id: 'some-id-2',
+ lastChanged: '06-03-1974',
+ lastChangedBy: 'Test Testesen',
+ },
+ ]),
+ },
+ });
await waitFor(async () => {
const selectInstanceText = await screen.findByText('Du har allerede startet å fylle ut dette skjemaet.');
@@ -172,7 +176,15 @@ describe('Entrypoint', () => {
expect(missingRolesText).not.toBeNull();
});
- function render({ store, allowAnonymous = false, queries }: { store: any; allowAnonymous?: boolean; queries?: any }) {
+ function render({
+ store,
+ allowAnonymous = false,
+ queries,
+ }: {
+ store: any;
+ allowAnonymous?: boolean;
+ queries?: Partial;
+ }) {
return renderWithProviders(
diff --git a/src/features/formData/fetch/fetchFormDataSagas.test.ts b/src/features/formData/fetch/fetchFormDataSagas.test.ts
deleted file mode 100644
index 98f66e94ba..0000000000
--- a/src/features/formData/fetch/fetchFormDataSagas.test.ts
+++ /dev/null
@@ -1,330 +0,0 @@
-import { call, select } from 'redux-saga/effects';
-import { expectSaga } from 'redux-saga-test-plan';
-import type { AxiosError, AxiosRequestHeaders } from 'axios';
-
-import { getInitialStateMock } from 'src/__mocks__/initialStateMock';
-import { DataModelActions } from 'src/features/datamodel/datamodelSlice';
-import {
- fetchFormDataInitialSaga,
- fetchFormDataSaga,
- watchFetchFormDataInitialSaga,
-} from 'src/features/formData/fetch/fetchFormDataSagas';
-import { FormDataActions } from 'src/features/formData/formDataSlice';
-import { InstanceDataActions } from 'src/features/instanceData/instanceDataSlice';
-import { QueueActions } from 'src/features/queue/queueSlice';
-import { makeGetAllowAnonymousSelector } from 'src/selectors/getAllowAnonymous';
-import {
- appMetaDataSelector,
- currentSelectedPartyIdSelector,
- instanceDataSelector,
- layoutSetsSelector,
-} from 'src/selectors/simpleSelectors';
-import { getCurrentTaskDataElementId, getDataTypeByLayoutSetId } from 'src/utils/appMetadata';
-import * as networking from 'src/utils/network/sharedNetworking';
-import * as appUrlHelper from 'src/utils/urls/appUrlHelper';
-import type { IApplicationMetadata } from 'src/features/applicationMetadata';
-import type { ILayoutSets } from 'src/types';
-import type { IApplication } from 'src/types/shared';
-
-describe('fetchFormDataSagas', () => {
- let mockInitialState;
- const mockFormData = {
- someField: 'test test',
- otherField: 'testing 123',
- group: {
- groupField: 'this is a field in a group',
- },
- };
- const flattenedFormData = {
- someField: 'test test',
- otherField: 'testing 123',
- 'group.groupField': 'this is a field in a group',
- };
-
- beforeEach(() => {
- mockInitialState = getInitialStateMock();
- });
- it('should fetch form data', () => {
- const appMetadata = appMetaDataSelector(mockInitialState);
- const instance = instanceDataSelector(mockInitialState);
- const layoutSets: ILayoutSets = { sets: [] };
- const taskId = getCurrentTaskDataElementId(appMetadata, instance, layoutSets) as string;
- const url = appUrlHelper.getFetchFormDataUrl(instance?.id || '', taskId);
-
- expectSaga(fetchFormDataSaga)
- .provide([
- [select(appMetaDataSelector), { ...mockInitialState.applicationMetadata.applicationMetadata }],
- [select(instanceDataSelector), { ...mockInitialState.instanceData.instance }],
- [call(networking.httpGet, url), mockFormData],
- ])
- .put(FormDataActions.fetchFulfilled({ formData: flattenedFormData }))
- .run();
- });
-
- it('should handle error in fetchFormData', () => {
- const appMetadata = appMetaDataSelector(mockInitialState);
- const instance = instanceDataSelector(mockInitialState);
- const error: AxiosError = {
- isAxiosError: true,
- message: 'error',
- name: 'AxiosError',
- toJSON: () => ({}),
- response: {
- config: { headers: {} as AxiosRequestHeaders },
- headers: {} as AxiosRequestHeaders,
- data: null,
- status: 500,
- statusText: 'error',
- },
- };
-
- jest.spyOn(networking, 'httpGet').mockImplementation(() => {
- throw error;
- });
-
- expectSaga(fetchFormDataSaga)
- .provide([
- [select(appMetaDataSelector), appMetadata],
- [select(instanceDataSelector), instance],
- ])
- .put(FormDataActions.fetchRejected({ error }))
- .run();
- });
-
- it('should fetch form data initial', () => {
- const appMetadata = appMetaDataSelector(mockInitialState);
- const instance = instanceDataSelector(mockInitialState);
- const layoutSets: ILayoutSets = { sets: [] };
- const taskId = getCurrentTaskDataElementId(appMetadata, instance, layoutSets) as string;
- const url = appUrlHelper.getFetchFormDataUrl(instance?.id || '', taskId);
-
- expectSaga(fetchFormDataInitialSaga)
- .provide([
- [select(appMetaDataSelector), { ...mockInitialState.applicationMetadata.applicationMetadata }],
- [select(instanceDataSelector), { ...mockInitialState.instanceData.instance }],
- [call(networking.httpGet, url), mockFormData],
- ])
- .put(FormDataActions.fetchFulfilled({ formData: flattenedFormData }))
- .run();
- });
-
- it('should handle error in fetchFormDataInitial', () => {
- const appMetadata = appMetaDataSelector(mockInitialState);
- const instance = instanceDataSelector(mockInitialState);
- const error: AxiosError = {
- isAxiosError: true,
- message: 'error',
- name: 'AxiosError',
- toJSON: () => ({}),
- response: {
- config: { headers: {} as AxiosRequestHeaders },
- headers: {} as AxiosRequestHeaders,
- data: null,
- status: 500,
- statusText: 'error',
- },
- };
-
- jest.spyOn(networking, 'httpGet').mockImplementation(() => {
- throw error;
- });
-
- expectSaga(fetchFormDataInitialSaga)
- .provide([
- [select(appMetaDataSelector), { ...appMetadata }],
- [select(instanceDataSelector), { ...instance }],
- ])
- .put(FormDataActions.fetchRejected({ error }))
- .put(QueueActions.dataTaskQueueError({ error }))
- .run();
- });
-
- it('should fetch form data initial for stateless app', () => {
- const appMetadata: IApplication = {
- ...(appMetaDataSelector(mockInitialState) as IApplicationMetadata),
- onEntry: {
- show: 'stateless',
- },
- };
- const mockLayoutSets: ILayoutSets = {
- sets: [
- {
- id: 'stateless',
- dataType: 'test-data-model',
- },
- ],
- };
-
- const dataType = getDataTypeByLayoutSetId('stateless', mockLayoutSets) as string;
- const url = appUrlHelper.getStatelessFormDataUrl(dataType);
- const options = {
- headers: {
- party: 'partyid:1234',
- },
- };
-
- expectSaga(fetchFormDataInitialSaga)
- .provide([
- [select(appMetaDataSelector), appMetadata],
- [select(layoutSetsSelector), mockLayoutSets],
- [select(makeGetAllowAnonymousSelector()), false],
- [select(currentSelectedPartyIdSelector), '1234'],
- [call(networking.httpGet, url, options), mockFormData],
- ])
- .put(FormDataActions.fetchFulfilled({ formData: flattenedFormData }))
- .run();
- });
-
- it('should fetch form data initial for stateless app with allowAnonymousOnStateless', () => {
- const appMetadata: IApplication = {
- ...(appMetaDataSelector(mockInitialState) as IApplicationMetadata),
- onEntry: {
- show: 'stateless',
- },
- };
-
- const mockLayoutSets: ILayoutSets = {
- sets: [
- {
- id: 'stateless',
- dataType: 'test-data-model',
- },
- ],
- };
-
- const dataType = getDataTypeByLayoutSetId('stateless', mockLayoutSets) as string;
- const url = appUrlHelper.getStatelessFormDataUrl(dataType);
- const options = {};
-
- expectSaga(fetchFormDataInitialSaga)
- .provide([
- [select(appMetaDataSelector), appMetadata],
- [select(layoutSetsSelector), mockLayoutSets],
- [select(makeGetAllowAnonymousSelector()), true],
- [call(networking.httpGet, url, options), mockFormData],
- ])
- .put(FormDataActions.fetchFulfilled({ formData: flattenedFormData }))
- .run();
- });
-
- it('should handle error in fetchFormDataStateless', () => {
- const appMetadata: IApplication = {
- ...(appMetaDataSelector(mockInitialState) as IApplicationMetadata),
- onEntry: {
- show: 'stateless',
- },
- };
-
- const mockLayoutSets: ILayoutSets = {
- sets: [
- {
- id: 'stateless',
- dataType: 'test-data-model',
- },
- ],
- };
-
- const error: AxiosError = {
- isAxiosError: true,
- message: 'error',
- name: 'AxiosError',
- toJSON: () => ({}),
- response: {
- config: { headers: {} as AxiosRequestHeaders },
- headers: {} as AxiosRequestHeaders,
- data: null,
- status: 500,
- statusText: 'error',
- },
- };
-
- jest.spyOn(networking, 'httpGet').mockImplementation(() => {
- throw error;
- });
-
- expectSaga(fetchFormDataInitialSaga)
- .provide([
- [select(appMetaDataSelector), appMetadata],
- [select(layoutSetsSelector), mockLayoutSets],
- [select(makeGetAllowAnonymousSelector()), true],
- ])
- .put(FormDataActions.fetchRejected({ error }))
- .put(QueueActions.dataTaskQueueError({ error }))
- .run();
- });
-
- it('should handle redirect to authentication in fetchFormDataStateless', () => {
- const appMetadata: IApplication = {
- ...(appMetaDataSelector(mockInitialState) as IApplicationMetadata),
- onEntry: {
- show: 'stateless',
- },
- };
-
- const mockLayoutSets: ILayoutSets = {
- sets: [
- {
- id: 'stateless',
- dataType: 'test-data-model',
- },
- ],
- };
-
- const error: AxiosError = {
- isAxiosError: true,
- message: 'error',
- name: 'AxiosError',
- toJSON: () => ({}),
- response: {
- config: { headers: {} as AxiosRequestHeaders },
- headers: {} as AxiosRequestHeaders,
- data: null,
- status: 403,
- statusText: 'error',
- },
- };
-
- jest.spyOn(networking, 'httpGet').mockImplementation(() => {
- throw error;
- });
-
- jest.spyOn(appUrlHelper, 'redirectToUpgrade').mockImplementation(() => undefined);
-
- expectSaga(fetchFormDataInitialSaga)
- .provide([
- [select(appMetaDataSelector), appMetadata],
- [select(layoutSetsSelector), mockLayoutSets],
- [select(makeGetAllowAnonymousSelector()), true],
- ])
- .call(appUrlHelper.redirectToUpgrade, 2)
- .run();
- });
-
- it('should trigger fetchFormDataInitialSaga', () => {
- const appMetadata = appMetaDataSelector(mockInitialState);
- const instance = instanceDataSelector(mockInitialState);
-
- expectSaga(watchFetchFormDataInitialSaga)
- .provide([
- [select(appMetaDataSelector), appMetadata],
- [select(instanceDataSelector), instance],
- ])
- .take(InstanceDataActions.getFulfilled)
- .take(DataModelActions.fetchJsonSchemaFulfilled)
- .call(fetchFormDataInitialSaga)
- .run();
- });
-
- it('should trigger fetchFormDataInitialSaga, stateless app', () => {
- const appMetadata = appMetaDataSelector(mockInitialState);
-
- expectSaga(watchFetchFormDataInitialSaga)
- .provide([
- [select(appMetaDataSelector), appMetadata],
- [select(makeGetAllowAnonymousSelector()), false],
- ])
- .take(DataModelActions.fetchJsonSchemaFulfilled)
- .call(fetchFormDataInitialSaga)
- .run();
- });
-});
diff --git a/src/features/formData/fetch/fetchFormDataSagas.ts b/src/features/formData/fetch/fetchFormDataSagas.ts
deleted file mode 100644
index 907bb26603..0000000000
--- a/src/features/formData/fetch/fetchFormDataSagas.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-import { all, call, put, select, take } from 'redux-saga/effects';
-import type { SagaIterator } from 'redux-saga';
-
-import { DataModelActions } from 'src/features/datamodel/datamodelSlice';
-import { FormDynamicsActions } from 'src/features/dynamics/formDynamicsSlice';
-import { FormDataActions } from 'src/features/formData/formDataSlice';
-import { FormRulesActions } from 'src/features/formRules/rulesSlice';
-import { InstanceDataActions } from 'src/features/instanceData/instanceDataSlice';
-import { QueueActions } from 'src/features/queue/queueSlice';
-import { makeGetAllowAnonymousSelector } from 'src/selectors/getAllowAnonymous';
-import {
- appMetaDataSelector,
- currentSelectedPartyIdSelector,
- instanceDataSelector,
- layoutSetsSelector,
- processStateSelector,
-} from 'src/selectors/simpleSelectors';
-import { getCurrentTaskDataElementId, getDataTypeByLayoutSetId, isStatelessApp } from 'src/utils/appMetadata';
-import { convertModelToDataBinding } from 'src/utils/databindings';
-import { putWithoutConfig } from 'src/utils/network/networking';
-import { httpGet } from 'src/utils/network/sharedNetworking';
-import { waitFor } from 'src/utils/sagas';
-import {
- getFetchFormDataUrl,
- getStatelessFormDataUrl,
- invalidateCookieUrl,
- redirectToUpgrade,
-} from 'src/utils/urls/appUrlHelper';
-import type { IApplicationMetadata } from 'src/features/applicationMetadata';
-import type { IProcessState } from 'src/features/process';
-import type { ILayoutSets } from 'src/types';
-import type { IInstance } from 'src/types/shared';
-
-export function* fetchFormDataSaga(): SagaIterator {
- try {
- // This is a temporary solution for the "one task - one datamodel - process"
- const applicationMetadata: IApplicationMetadata = yield select(appMetaDataSelector);
- const instance: IInstance = yield select(instanceDataSelector);
- const layoutSets: ILayoutSets = yield select(layoutSetsSelector);
- const currentTaskDataElementId = getCurrentTaskDataElementId(applicationMetadata, instance, layoutSets);
- if (currentTaskDataElementId) {
- const fetchedData: any = yield call(httpGet, getFetchFormDataUrl(instance.id, currentTaskDataElementId));
- const formData = convertModelToDataBinding(fetchedData);
- yield put(FormDataActions.fetchFulfilled({ formData }));
- } else {
- yield put(FormDataActions.fetchRejected({ error: null }));
- }
- } catch (error) {
- yield put(FormDataActions.fetchRejected({ error }));
- window.logError('Fetching form data failed:\n', error);
- }
-}
-
-export function* fetchFormDataInitialSaga(): SagaIterator {
- try {
- // This is a temporary solution for the "one task - one datamodel - process"
- const applicationMetadata: IApplicationMetadata = yield select(appMetaDataSelector);
- let fetchedData: any;
- if (isStatelessApp(applicationMetadata)) {
- // stateless app
- fetchedData = yield call(fetchFormDataStateless, applicationMetadata);
- } else {
- // app with instance
- const instance: IInstance = yield select(instanceDataSelector);
- const layoutSets: ILayoutSets = yield select(layoutSetsSelector);
- const currentTaskDataId = getCurrentTaskDataElementId(applicationMetadata, instance, layoutSets);
- if (currentTaskDataId) {
- fetchedData = yield call(httpGet, getFetchFormDataUrl(instance.id, currentTaskDataId));
- }
- }
-
- const formData = convertModelToDataBinding(fetchedData);
- yield put(FormDataActions.fetchFulfilled({ formData }));
- yield put(FormRulesActions.fetch());
- yield put(FormDynamicsActions.fetch());
- } catch (error) {
- yield put(FormDataActions.fetchRejected({ error }));
- yield put(QueueActions.dataTaskQueueError({ error }));
- if (error.message?.includes('403')) {
- window.logInfo('Current party is missing roles');
- } else {
- window.logError('Fetching form data failed:\n', error);
- }
- }
-}
-
-function* fetchFormDataStateless(applicationMetadata: IApplicationMetadata) {
- const layoutSets: ILayoutSets = yield select(layoutSetsSelector);
- const dataType = getDataTypeByLayoutSetId(applicationMetadata.onEntry?.show, layoutSets);
-
- const allowAnonymous = yield select(makeGetAllowAnonymousSelector());
-
- let options = {};
-
- if (!allowAnonymous) {
- const selectedPartyId = yield select(currentSelectedPartyIdSelector);
- options = {
- headers: {
- party: `partyid:${selectedPartyId}`,
- },
- };
- }
-
- if (!dataType) {
- return;
- }
-
- try {
- return yield call(httpGet, getStatelessFormDataUrl(dataType, allowAnonymous), options);
- } catch (error) {
- if (error?.response?.status === 403 && error.response.data) {
- const reqAuthLevel = error.response.data.RequiredAuthenticationLevel;
- if (reqAuthLevel) {
- putWithoutConfig(invalidateCookieUrl);
- yield call(redirectToUpgrade, reqAuthLevel);
- } else {
- throw error;
- }
- } else {
- throw error;
- }
- }
-}
-
-export function* watchFetchFormDataInitialSaga(): SagaIterator {
- while (true) {
- yield take(FormDataActions.fetchInitial);
- const processState: IProcessState = yield select(processStateSelector);
- const instance: IInstance = yield select(instanceDataSelector);
- const application: IApplicationMetadata = yield select(appMetaDataSelector);
- if (isStatelessApp(application)) {
- yield take(DataModelActions.fetchJsonSchemaFulfilled);
- const allowAnonymous = yield select(makeGetAllowAnonymousSelector());
- if (!allowAnonymous) {
- yield waitFor((state) => currentSelectedPartyIdSelector(state) !== undefined);
- }
- } else if (!processState || !instance || processState.taskId !== instance.process?.currentTask?.elementId) {
- yield all([take(InstanceDataActions.getFulfilled), take(DataModelActions.fetchJsonSchemaFulfilled)]);
- }
- yield call(fetchFormDataInitialSaga);
- }
-}
diff --git a/src/features/formData/formDataSlice.ts b/src/features/formData/formDataSlice.ts
index 81370f8344..535cb82905 100644
--- a/src/features/formData/formDataSlice.ts
+++ b/src/features/formData/formDataSlice.ts
@@ -1,7 +1,6 @@
import type { AnyAction } from 'redux';
import { checkIfDataListShouldRefetchSaga } from 'src/features/dataLists/fetchDataListsSaga';
-import { fetchFormDataSaga, watchFetchFormDataInitialSaga } from 'src/features/formData/fetch/fetchFormDataSagas';
import { autoSaveSaga, saveFormDataSaga, submitFormSaga } from 'src/features/formData/submit/submitFormDataSagas';
import { deleteAttachmentReferenceSaga, updateFormDataSaga } from 'src/features/formData/update/updateFormDataSagas';
import { checkIfRuleShouldRunSaga } from 'src/features/formRules/checkRulesSagas';
@@ -27,6 +26,7 @@ export const initialState: IFormDataState = {
saving: false,
submittingId: '',
error: null,
+ reFetch: false,
};
const isProcessAction = (action: AnyAction) =>
@@ -39,22 +39,23 @@ export const formDataSlice = () => {
initialState,
actions: {
fetch: mkAction({
- takeLatest: fetchFormDataSaga,
- }),
- fetchInitial: mkAction({
- saga: () => watchFetchFormDataInitialSaga,
+ reducer: (state) => {
+ state.reFetch = true;
+ },
}),
fetchFulfilled: mkAction({
reducer: (state, action) => {
const { formData } = action.payload;
state.formData = formData;
state.lastSavedFormData = formData;
+ state.reFetch = false;
},
}),
fetchRejected: mkAction({
reducer: (state, action) => {
const { error } = action.payload;
state.error = error;
+ state.reFetch = false;
},
}),
setFulfilled: mkAction({
diff --git a/src/features/formData/index.d.ts b/src/features/formData/index.d.ts
index 109393a543..c2af7ad694 100644
--- a/src/features/formData/index.d.ts
+++ b/src/features/formData/index.d.ts
@@ -21,6 +21,9 @@ export interface IFormDataState {
submittingId: string;
error: Error | null;
+
+ // Setting this to true will force a re-fetch of the form data.
+ reFetch?: boolean;
}
export interface IFormData {
diff --git a/src/features/isLoading/statelessIsLoadingSagas.ts b/src/features/isLoading/statelessIsLoadingSagas.ts
index 3c5a1ed81c..fe1049d444 100644
--- a/src/features/isLoading/statelessIsLoadingSagas.ts
+++ b/src/features/isLoading/statelessIsLoadingSagas.ts
@@ -1,7 +1,6 @@
import { all, put, take } from 'redux-saga/effects';
import type { SagaIterator } from 'redux-saga';
-import { DataModelActions } from 'src/features/datamodel/datamodelSlice';
import { FormDynamicsActions } from 'src/features/dynamics/formDynamicsSlice';
import { FormDataActions } from 'src/features/formData/formDataSlice';
import { FormRulesActions } from 'src/features/formRules/rulesSlice';
@@ -15,7 +14,6 @@ export function* watcherFinishStatelessIsLoadingSaga(): SagaIterator {
take(FormDataActions.fetchFulfilled),
take(FormLayoutActions.fetchFulfilled),
take(FormLayoutActions.fetchSettingsFulfilled),
- take(DataModelActions.fetchJsonSchemaFulfilled),
take(FormRulesActions.fetchFulfilled),
take(FormDynamicsActions.fetchFulfilled),
]);
diff --git a/src/features/layout/fetch/fetchFormLayoutSagas.ts b/src/features/layout/fetch/fetchFormLayoutSagas.ts
index 7af993d6e0..1d46081a94 100644
--- a/src/features/layout/fetch/fetchFormLayoutSagas.ts
+++ b/src/features/layout/fetch/fetchFormLayoutSagas.ts
@@ -119,11 +119,7 @@ export function* fetchLayoutSaga(): SagaIterator {
export function* watchFetchFormLayoutSaga(): SagaIterator {
while (true) {
- yield all([
- take(FormLayoutActions.fetch),
- take(FormDataActions.fetchInitial),
- take(FormDataActions.fetchFulfilled),
- ]);
+ yield all([take(FormLayoutActions.fetch), take(FormDataActions.fetchFulfilled)]);
yield call(fetchLayoutSaga);
}
}
diff --git a/src/features/queue/queueSlice.ts b/src/features/queue/queueSlice.ts
index af8107649a..7805268c56 100644
--- a/src/features/queue/queueSlice.ts
+++ b/src/features/queue/queueSlice.ts
@@ -2,8 +2,6 @@ import { put } from 'redux-saga/effects';
import type { SagaIterator } from 'redux-saga';
import { AttachmentActions } from 'src/features/attachments/attachmentSlice';
-import { DataModelActions } from 'src/features/datamodel/datamodelSlice';
-import { FormDataActions } from 'src/features/formData/formDataSlice';
import { IsLoadingActions } from 'src/features/isLoading/isLoadingSlice';
import { FormLayoutActions } from 'src/features/layout/formLayoutSlice';
import { PdfActions } from 'src/features/pdf/data/pdfSlice';
@@ -74,8 +72,6 @@ export const queueSlice = () => {
}),
startInitialDataTaskQueue: mkAction({
*takeEvery(): SagaIterator {
- yield put(FormDataActions.fetchInitial());
- yield put(DataModelActions.fetchJsonSchema());
yield put(FormLayoutActions.fetch());
yield put(FormLayoutActions.fetchSettings());
yield put(PdfActions.initial());
@@ -105,8 +101,6 @@ export const queueSlice = () => {
startInitialStatelessQueue: mkAction({
*takeLatest(): SagaIterator {
yield put(IsLoadingActions.startStatelessIsLoading());
- yield put(FormDataActions.fetchInitial());
- yield put(DataModelActions.fetchJsonSchema());
yield put(FormLayoutActions.fetch());
yield put(FormLayoutActions.fetchSettings());
yield put(QueueActions.startInitialStatelessQueueFulfilled());
diff --git a/src/hooks/mutations/usePartyValidationMutation.ts b/src/hooks/mutations/usePartyValidationMutation.ts
index dc0159bd93..3e591a5d55 100644
--- a/src/hooks/mutations/usePartyValidationMutation.ts
+++ b/src/hooks/mutations/usePartyValidationMutation.ts
@@ -5,7 +5,7 @@ import type { HttpClientError } from 'src/utils/network/sharedNetworking';
export const usePartyValidationMutation = () => {
const { doPartyValidation } = useAppQueriesContext();
- return useMutation((partyId: string) => doPartyValidation(partyId).then((response) => response.data), {
+ return useMutation((partyId: string) => doPartyValidation(partyId), {
onError: (error: HttpClientError) => {
console.warn(error);
throw new Error('Server did not respond with party validation');
diff --git a/src/hooks/queries/useActiveInstancesQuery.ts b/src/hooks/queries/useActiveInstancesQuery.ts
index 56b30f60ba..bb65e6c7e1 100644
--- a/src/hooks/queries/useActiveInstancesQuery.ts
+++ b/src/hooks/queries/useActiveInstancesQuery.ts
@@ -7,14 +7,10 @@ import { useAppDispatch } from 'src/hooks/useAppDispatch';
import type { ISimpleInstance } from 'src/types';
import type { HttpClientError } from 'src/utils/network/sharedNetworking';
-enum ServerStateCacheKey {
- GetActiveInstances = 'getActiveInstances',
-}
-
export const useActiveInstancesQuery = (partyId?: string, enabled?: boolean): UseQueryResult => {
const dispatch = useAppDispatch();
const { fetchActiveInstances } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.GetActiveInstances], () => fetchActiveInstances(partyId || ''), {
+ return useQuery(['getActiveInstances'], () => fetchActiveInstances(partyId || ''), {
enabled,
onSuccess: (instanceData) => {
// Sort array by last changed date
diff --git a/src/hooks/queries/useApplicationMetadataQuery.ts b/src/hooks/queries/useApplicationMetadataQuery.ts
index f50aaa5175..46ac18f465 100644
--- a/src/hooks/queries/useApplicationMetadataQuery.ts
+++ b/src/hooks/queries/useApplicationMetadataQuery.ts
@@ -8,14 +8,10 @@ import { useAppDispatch } from 'src/hooks/useAppDispatch';
import type { IApplicationMetadata } from 'src/features/applicationMetadata';
import type { HttpClientError } from 'src/utils/network/sharedNetworking';
-enum ServerStateCacheKey {
- ApplicationMetadata = 'fetchApplicationMetadata',
-}
-
export const useApplicationMetadataQuery = (): UseQueryResult => {
const dispatch = useAppDispatch();
const { fetchApplicationMetadata } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.ApplicationMetadata], fetchApplicationMetadata, {
+ return useQuery(['fetchApplicationMetadata'], fetchApplicationMetadata, {
onSuccess: (applicationMetadata) => {
// Update the Redux Store ensures that legacy code has access to the data without using the Tanstack Query Cache
dispatch(ApplicationMetadataActions.getFulfilled({ applicationMetadata }));
diff --git a/src/hooks/queries/useApplicationSettingsQuery.ts b/src/hooks/queries/useApplicationSettingsQuery.ts
index baba7ed34f..ecf58268c7 100644
--- a/src/hooks/queries/useApplicationSettingsQuery.ts
+++ b/src/hooks/queries/useApplicationSettingsQuery.ts
@@ -7,14 +7,10 @@ import { useAppDispatch } from 'src/hooks/useAppDispatch';
import type { IApplicationSettings } from 'src/types/shared';
import type { HttpClientError } from 'src/utils/network/sharedNetworking';
-enum ServerStateCacheKey {
- ApplicationSettings = 'fetchApplicationSettings',
-}
-
export const useApplicationSettingsQuery = (): UseQueryResult => {
const dispatch = useAppDispatch();
const { fetchApplicationSettings } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.ApplicationSettings], fetchApplicationSettings, {
+ return useQuery(['fetchApplicationSettings'], fetchApplicationSettings, {
onSuccess: (settings) => {
// Update the Redux Store ensures that legacy code has access to the data without using the Tanstack Query Cache
dispatch(ApplicationSettingsActions.fetchApplicationSettingsFulfilled({ settings }));
diff --git a/src/hooks/queries/useCurrentDataModelSchemaQuery.ts b/src/hooks/queries/useCurrentDataModelSchemaQuery.ts
new file mode 100644
index 0000000000..2db763dc2d
--- /dev/null
+++ b/src/hooks/queries/useCurrentDataModelSchemaQuery.ts
@@ -0,0 +1,30 @@
+import { useQuery } from '@tanstack/react-query';
+import type { UseQueryResult } from '@tanstack/react-query';
+import type { JSONSchema7 } from 'json-schema';
+
+import { useAppQueriesContext } from 'src/contexts/appQueriesContext';
+import { DataModelActions } from 'src/features/datamodel/datamodelSlice';
+import { useAppDispatch } from 'src/hooks/useAppDispatch';
+import { useCurrentDataModelName } from 'src/hooks/useDataModelSchema';
+import type { HttpClientError } from 'src/utils/network/sharedNetworking';
+
+export const useCurrentDataModelSchemaQuery = (): UseQueryResult => {
+ const dispatch = useAppDispatch();
+ const { fetchDataModelSchema } = useAppQueriesContext();
+ const dataModelName = useCurrentDataModelName();
+ return useQuery(['fetchDataModelSchemas', dataModelName], () => fetchDataModelSchema(dataModelName || ''), {
+ enabled: !!dataModelName,
+ onSuccess: (schema) => {
+ dispatch(DataModelActions.fetchFulfilled({ id: dataModelName || '', schema }));
+ },
+ onError: (error: HttpClientError) => {
+ if (error.status === 404) {
+ dispatch(DataModelActions.fetchRejected({ error: null }));
+ window.logWarn('Data model schema not found:\n', error);
+ } else {
+ dispatch(DataModelActions.fetchRejected({ error }));
+ window.logError('Data model schema request failed:\n', error);
+ }
+ },
+ });
+};
diff --git a/src/hooks/queries/useFooterLayoutQuery.ts b/src/hooks/queries/useFooterLayoutQuery.ts
index 17deaf2972..954622e5ce 100644
--- a/src/hooks/queries/useFooterLayoutQuery.ts
+++ b/src/hooks/queries/useFooterLayoutQuery.ts
@@ -12,14 +12,10 @@ const handleErrorAsSuccessful = (dispatch: AppDispatch): void => {
dispatch(FooterLayoutActions.fetchFulfilled({ footerLayout: null }));
};
-enum ServerStateCacheKey {
- FetchFooterLayout = 'fetchFooterLayout',
-}
-
export const useFooterLayoutQuery = (enabled?: boolean): UseQueryResult => {
const dispatch = useAppDispatch();
const { fetchFooterLayout } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.FetchFooterLayout], fetchFooterLayout, {
+ return useQuery(['fetchFooterLayout'], fetchFooterLayout, {
enabled,
onSuccess: (footerLayout) => {
// Update the Redux Store ensures that legacy code has access to the data without using the Tanstack Query Cache
diff --git a/src/hooks/queries/useFormDataQuery.ts b/src/hooks/queries/useFormDataQuery.ts
new file mode 100644
index 0000000000..26840d4763
--- /dev/null
+++ b/src/hooks/queries/useFormDataQuery.ts
@@ -0,0 +1,101 @@
+import { useQuery } from '@tanstack/react-query';
+import type { UseQueryResult } from '@tanstack/react-query';
+import type { AxiosRequestConfig } from 'axios';
+
+import { useAppQueriesContext } from 'src/contexts/appQueriesContext';
+import { FormDynamicsActions } from 'src/features/dynamics/formDynamicsSlice';
+import { FormDataActions } from 'src/features/formData/formDataSlice';
+import { FormRulesActions } from 'src/features/formRules/rulesSlice';
+import { QueueActions } from 'src/features/queue/queueSlice';
+import { useAppDispatch } from 'src/hooks/useAppDispatch';
+import { useAppSelector } from 'src/hooks/useAppSelector';
+import { makeGetAllowAnonymousSelector } from 'src/selectors/getAllowAnonymous';
+import { getCurrentTaskDataElementId, getDataTypeByLayoutSetId, isStatelessApp } from 'src/utils/appMetadata';
+import { convertModelToDataBinding } from 'src/utils/databindings';
+import { putWithoutConfig } from 'src/utils/network/networking';
+import {
+ getFetchFormDataUrl,
+ getStatelessFormDataUrl,
+ invalidateCookieUrl,
+ redirectToUpgrade,
+} from 'src/utils/urls/appUrlHelper';
+import type { IFormData } from 'src/features/formData';
+import type { HttpClientError } from 'src/utils/network/sharedNetworking';
+
+export function useFormDataQuery(): UseQueryResult {
+ const dispatch = useAppDispatch();
+ const reFetchActive = useAppSelector((state) => state.formData.reFetch);
+ const appMetaData = useAppSelector((state) => state.applicationMetadata.applicationMetadata);
+ const currentPartyId = useAppSelector((state) => state.party.selectedParty?.partyId);
+ const isDoneDataTask = useAppSelector((state) => state.queue.dataTask.isDone);
+ const isDoneStatelessTask = useAppSelector((state) => state.queue.stateless.isDone);
+ const allowAnonymousSelector = makeGetAllowAnonymousSelector();
+ const allowAnonymous = useAppSelector(allowAnonymousSelector);
+ const isStateless = isStatelessApp(appMetaData);
+
+ let isEnabled = isStateless ? isDoneStatelessTask !== null : isDoneDataTask !== null;
+ if (isStateless && !allowAnonymous && currentPartyId === undefined) {
+ isEnabled = false;
+ }
+
+ const instance = useAppSelector((state) => state.instanceData.instance);
+ const layoutSets = useAppSelector((state) => state.formLayout.layoutsets);
+ const statelessDataType = isStateless ? getDataTypeByLayoutSetId(appMetaData?.onEntry?.show, layoutSets) : undefined;
+ const currentTaskDataId = getCurrentTaskDataElementId(appMetaData, instance, layoutSets);
+
+ const url =
+ isStateless && statelessDataType
+ ? getStatelessFormDataUrl(statelessDataType, allowAnonymous)
+ : instance && currentTaskDataId
+ ? getFetchFormDataUrl(instance.id, currentTaskDataId)
+ : undefined;
+
+ const options: AxiosRequestConfig = {};
+ if (isStateless && !allowAnonymous && currentPartyId) {
+ options.headers = {
+ party: `partyid:${currentPartyId}`,
+ };
+ }
+
+ // We also add the current task id to the query key, so that the query is refetched when the task changes. This
+ // is needed because we have logic waiting for the form data to be fetched before we can continue (even if the
+ // data element used is the same one between two different tasks - in which case it could also have been changed
+ // on the server).
+ const currentTaskId = instance?.process?.currentTask?.elementId;
+
+ const { fetchFormData } = useAppQueriesContext();
+ const out = useQuery(['fetchFormData', url, currentTaskId], () => fetchFormData(url || '', options), {
+ enabled: isEnabled && url !== undefined,
+ onSuccess: (formDataAsObj) => {
+ const formData = convertModelToDataBinding(formDataAsObj);
+ dispatch(FormDataActions.fetchFulfilled({ formData }));
+ dispatch(FormRulesActions.fetch());
+ dispatch(FormDynamicsActions.fetch());
+ },
+ onError: async (error: HttpClientError) => {
+ dispatch(FormDataActions.fetchRejected({ error }));
+ dispatch(QueueActions.dataTaskQueueError({ error }));
+ if (error.message?.includes('403')) {
+ window.logInfo('Current party is missing roles');
+ } else {
+ window.logError('Fetching form data failed:\n', error);
+ }
+
+ if (isStateless && error?.response?.status === 403 && error.response.data) {
+ const reqAuthLevel = (error.response.data as any).RequiredAuthenticationLevel;
+ if (reqAuthLevel) {
+ await putWithoutConfig(invalidateCookieUrl);
+ redirectToUpgrade(reqAuthLevel);
+ } else {
+ throw error;
+ }
+ }
+ },
+ });
+
+ if (reFetchActive && !out.isFetching) {
+ out.refetch();
+ }
+
+ return out;
+}
diff --git a/src/hooks/queries/useGetCurrentPartyQuery.ts b/src/hooks/queries/useGetCurrentPartyQuery.ts
index 9c14d6036a..2f95e43497 100644
--- a/src/hooks/queries/useGetCurrentPartyQuery.ts
+++ b/src/hooks/queries/useGetCurrentPartyQuery.ts
@@ -7,16 +7,12 @@ import { useAppDispatch } from 'src/hooks/useAppDispatch';
import { useAppSelector } from 'src/hooks/useAppSelector';
import type { HttpClientError } from 'src/utils/network/sharedNetworking';
-enum ServerStateCacheKey {
- UseCurrentParty = 'fetchUseCurrentParty',
-}
-
export const useCurrentPartyQuery = (enabled: boolean) => {
const dispatch = useAppDispatch();
const parties = useAppSelector((state) => state.party.parties);
const { fetchCurrentParty } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.UseCurrentParty], fetchCurrentParty, {
+ return useQuery(['fetchUseCurrentParty'], fetchCurrentParty, {
enabled,
onSuccess: (currentParty) => {
// Update the Redux Store ensures that legacy code has access to the data without using the Tanstack Query Cache
diff --git a/src/hooks/queries/useGetPartiesQuery.ts b/src/hooks/queries/useGetPartiesQuery.ts
index 4170668b2f..0e4feb47b7 100644
--- a/src/hooks/queries/useGetPartiesQuery.ts
+++ b/src/hooks/queries/useGetPartiesQuery.ts
@@ -6,15 +6,11 @@ import { QueueActions } from 'src/features/queue/queueSlice';
import { useAppDispatch } from 'src/hooks/useAppDispatch';
import type { HttpClientError } from 'src/utils/network/sharedNetworking';
-enum ServerStateCacheKey {
- UseParties = 'fetchUseParties',
-}
-
export const usePartiesQuery = (enabled: boolean) => {
const dispatch = useAppDispatch();
const { fetchParties } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.UseParties], fetchParties, {
+ return useQuery(['fetchUseParties'], fetchParties, {
enabled,
onSuccess: (parties) => {
// Update the Redux Store ensures that legacy code has access to the data without using the Tanstack Query Cache
diff --git a/src/hooks/queries/useLayoutSetsQuery.ts b/src/hooks/queries/useLayoutSetsQuery.ts
index 0359ebb5d3..6bf4ee30fa 100644
--- a/src/hooks/queries/useLayoutSetsQuery.ts
+++ b/src/hooks/queries/useLayoutSetsQuery.ts
@@ -7,13 +7,10 @@ import { useAppDispatch } from 'src/hooks/useAppDispatch';
import type { ILayoutSets } from 'src/types';
import type { HttpClientError } from 'src/utils/network/sharedNetworking';
-enum ServerStateCacheKey {
- LayoutSets = 'fetchLayoutSets',
-}
export const useLayoutSetsQuery = (): UseQueryResult => {
const dispatch = useAppDispatch();
const { fetchLayoutSets } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.LayoutSets], fetchLayoutSets, {
+ return useQuery(['fetchLayoutSets'], fetchLayoutSets, {
onSuccess: (layoutSets) => {
// Update the Redux Store ensures that legacy code has access to the data without using the Tanstack Query Cache
dispatch(FormLayoutActions.fetchSetsFulfilled({ layoutSets }));
diff --git a/src/hooks/queries/useOrgsQuery.ts b/src/hooks/queries/useOrgsQuery.ts
index 6013c8089b..ddfc092cbc 100644
--- a/src/hooks/queries/useOrgsQuery.ts
+++ b/src/hooks/queries/useOrgsQuery.ts
@@ -7,16 +7,12 @@ import { useAppDispatch } from 'src/hooks/useAppDispatch';
import type { IAltinnOrgs } from 'src/types/shared';
import type { HttpClientError } from 'src/utils/network/sharedNetworking';
-enum ServerStateCacheKey {
- GetOrganizations = 'fetchOrganizations',
-}
-
const extractOrgsFromServerResponse = (response: { orgs: IAltinnOrgs }): IAltinnOrgs => response.orgs;
export const useOrgsQuery = (): UseQueryResult => {
const dispatch = useAppDispatch();
const { fetchOrgs } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.GetOrganizations], () => fetchOrgs().then(extractOrgsFromServerResponse), {
+ return useQuery(['fetchOrganizations'], () => fetchOrgs().then(extractOrgsFromServerResponse), {
onSuccess: (orgs) => {
// Update the Redux Store ensures that legacy code has access to the data without using the Tanstack Query Cache
dispatch(OrgsActions.fetchFulfilled({ orgs }));
diff --git a/src/hooks/queries/useProfileQuery.ts b/src/hooks/queries/useProfileQuery.ts
index 7df04dd1f1..4fd820c28b 100644
--- a/src/hooks/queries/useProfileQuery.ts
+++ b/src/hooks/queries/useProfileQuery.ts
@@ -8,14 +8,11 @@ import { useAppDispatch } from 'src/hooks/useAppDispatch';
import type { IProfile } from 'src/types/shared';
import type { HttpClientError } from 'src/utils/network/sharedNetworking';
-enum ServerStateCacheKey {
- GetUserProfile = 'fetchUserProfile',
-}
export const useProfileQuery = (enabled: boolean): UseQueryResult => {
const dispatch = useAppDispatch();
const { fetchUserProfile } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.GetUserProfile], fetchUserProfile, {
+ return useQuery(['fetchUserProfile'], fetchUserProfile, {
enabled,
onSuccess: (profile) => {
dispatch(ProfileActions.fetchFulfilled({ profile }));
diff --git a/src/hooks/queries/useRefreshJwtTokenQuery.ts b/src/hooks/queries/useRefreshJwtTokenQuery.ts
index dab49da80f..37c5265257 100644
--- a/src/hooks/queries/useRefreshJwtTokenQuery.ts
+++ b/src/hooks/queries/useRefreshJwtTokenQuery.ts
@@ -9,10 +9,6 @@ const redirectToLogin = (appOidcProvider: string | null): void => {
window.location.href = getEnvironmentLoginUrl(appOidcProvider);
};
-enum ServerStateCacheKey {
- RefreshJwtToken = 'refreshJwtToken',
-}
-
export const useRefreshJwtTokenQuery = (
appOidcProvider: string | null,
options: {
@@ -22,7 +18,7 @@ export const useRefreshJwtTokenQuery = (
},
): UseQueryResult => {
const { fetchRefreshJwtToken } = useAppQueriesContext();
- return useQuery([ServerStateCacheKey.RefreshJwtToken], fetchRefreshJwtToken, {
+ return useQuery(['refreshJwtToken'], fetchRefreshJwtToken, {
...options,
onError: (error: HttpClientError) => {
try {
diff --git a/src/hooks/useDataModelSchema.tsx b/src/hooks/useDataModelSchema.tsx
index cb9ecbbe49..3f4c291aa2 100644
--- a/src/hooks/useDataModelSchema.tsx
+++ b/src/hooks/useDataModelSchema.tsx
@@ -7,15 +7,19 @@ import { useAppSelector } from 'src/hooks/useAppSelector';
import { getCurrentDataTypeForApplication } from 'src/utils/appMetadata';
import { getRootElementPath } from 'src/utils/schemaUtils';
-function useDraft(enabled: boolean) {
- const dataModels = useAppSelector((state) => state.formDataModel.schemas);
- const currentDataModelName = useAppSelector((state) =>
+export function useCurrentDataModelName() {
+ return useAppSelector((state) =>
getCurrentDataTypeForApplication({
application: state.applicationMetadata.applicationMetadata,
instance: state.instanceData.instance,
layoutSets: state.formLayout.layoutsets,
}),
);
+}
+
+function useDraft(enabled: boolean) {
+ const dataModels = useAppSelector((state) => state.formDataModel.schemas);
+ const currentDataModelName = useCurrentDataModelName();
const currentModel =
dataModels && currentDataModelName && currentDataModelName in dataModels && dataModels[currentDataModelName];
diff --git a/src/queries/queries.ts b/src/queries/queries.ts
index dd7490f768..0bb8cf91cf 100644
--- a/src/queries/queries.ts
+++ b/src/queries/queries.ts
@@ -1,3 +1,6 @@
+import type { AxiosRequestConfig } from 'axios';
+import type { JSONSchema7 } from 'json-schema';
+
import { httpPost } from 'src/utils/network/networking';
import { httpGet } from 'src/utils/network/sharedNetworking';
import {
@@ -6,6 +9,7 @@ import {
currentPartyUrl,
getActiveInstancesUrl,
getFooterLayoutUrl,
+ getJsonSchemaUrl,
getLayoutSetsUrl,
getPartyValidationUrl,
profileApiUrl,
@@ -18,7 +22,7 @@ import type { IFooterLayout } from 'src/features/footer/types';
import type { ILayoutSets, ISimpleInstance } from 'src/types';
import type { IAltinnOrgs, IApplicationSettings, IProfile } from 'src/types/shared';
-export const doPartyValidation = (partyId: string) => httpPost(getPartyValidationUrl(partyId));
+export const doPartyValidation = async (partyId: string) => (await httpPost(getPartyValidationUrl(partyId))).data;
export const fetchActiveInstances = (partyId: string): Promise =>
httpGet(getActiveInstancesUrl(partyId));
@@ -43,3 +47,8 @@ export const fetchOrgs = (): Promise<{ orgs: IAltinnOrgs }> =>
export const fetchUserProfile = (): Promise => httpGet(profileApiUrl);
export const fetchRefreshJwtToken = () => httpGet(refreshJwtTokenUrl);
+
+export const fetchDataModelSchema = (dataTypeName: string): Promise =>
+ httpGet(getJsonSchemaUrl() + dataTypeName);
+
+export const fetchFormData = (url: string, options?: AxiosRequestConfig): Promise => httpGet(url, options);
diff --git a/src/selectors/getAllowAnonymous.ts b/src/selectors/getAllowAnonymous.ts
index dc975cc84a..2094e85be9 100644
--- a/src/selectors/getAllowAnonymous.ts
+++ b/src/selectors/getAllowAnonymous.ts
@@ -6,7 +6,7 @@ import type { IRuntimeState } from 'src/types';
const getApplicationMetadata = (state: IRuntimeState) => state.applicationMetadata?.applicationMetadata;
const getLayoutSets = (state: IRuntimeState) => state.formLayout.layoutsets;
-let selector: any = undefined;
+let selector: ((state: IRuntimeState) => boolean | undefined) | undefined = undefined;
const getAllowAnonymous = () => {
if (selector) {
return selector;
diff --git a/src/testUtils.tsx b/src/testUtils.tsx
index e33d303f05..ea693b5bb8 100644
--- a/src/testUtils.tsx
+++ b/src/testUtils.tsx
@@ -15,10 +15,13 @@ import { setupStore } from 'src/redux/store';
import { AltinnAppTheme } from 'src/theme/altinnAppTheme';
import { ExprContextWrapper, useResolvedNode } from 'src/utils/layout/ExprContext';
import type { AppQueriesContext } from 'src/contexts/appQueriesContext';
+import type { IApplicationMetadata } from 'src/features/applicationMetadata';
+import type { IFooterLayout } from 'src/features/footer/types';
import type { IComponentProps, PropsFromGenericComponent } from 'src/layout';
import type { CompExternalExact, CompTypes } from 'src/layout/layout';
import type { AppStore, RootState } from 'src/redux/store';
-import type { IRuntimeState } from 'src/types';
+import type { ILayoutSets, IRuntimeState } from 'src/types';
+import type { IProfile } from 'src/types/shared';
interface ExtendedRenderOptions extends Omit {
preloadedState?: PreloadedState;
@@ -33,18 +36,22 @@ export const renderWithProviders = (
function Wrapper({ children }: React.PropsWithChildren) {
const theme = createTheme(AltinnAppTheme);
- const mockedQueries = {
- doPartyValidation: () => Promise.resolve({ data: { isValid: true, validParties: [] } }),
+ const allMockedQueries = {
+ doPartyValidation: () => Promise.resolve({ isValid: true, validParties: [] }),
fetchActiveInstances: () => Promise.resolve([]),
- fetchApplicationMetadata: () => Promise.resolve({}),
+ fetchApplicationMetadata: () => Promise.resolve({} as unknown as IApplicationMetadata),
fetchCurrentParty: () => Promise.resolve({}),
fetchApplicationSettings: () => Promise.resolve({}),
- fetchFooterLayout: () => Promise.resolve({}),
- fetchLayoutSets: () => Promise.resolve([]),
- fetchOrgs: () => Promise.resolve({}),
- fetchUserProfile: () => Promise.resolve({}),
- ...queries,
+ fetchFooterLayout: () => Promise.resolve({} as unknown as IFooterLayout),
+ fetchLayoutSets: () => Promise.resolve({} as unknown as ILayoutSets),
+ fetchOrgs: () => Promise.resolve({ orgs: {} }),
+ fetchUserProfile: () => Promise.resolve({} as unknown as IProfile),
+ fetchDataModelSchema: () => Promise.resolve({}),
+ fetchParties: () => Promise.resolve({}),
+ fetchRefreshJwtToken: () => Promise.resolve({}),
+ fetchFormData: () => Promise.resolve({}),
} as AppQueriesContext;
+ const mockedQueries = { ...allMockedQueries, ...queries };
const client = new QueryClient({
logger: {