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: {