diff --git a/src/openapi.yaml b/src/openapi.yaml index ef9ad80413..1d045b7eed 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -1133,6 +1133,7 @@ paths: required: true tags: - forms + - form-definitions security: - tokenAuth: [] - cookieAuth: [] diff --git a/src/openforms/forms/api/filters.py b/src/openforms/forms/api/filters.py index 63baba2865..627d9b837d 100644 --- a/src/openforms/forms/api/filters.py +++ b/src/openforms/forms/api/filters.py @@ -1,3 +1,5 @@ +import warnings + from django.db.models import Q from django.utils.translation import gettext_lazy as _ @@ -56,5 +58,9 @@ def filter_queryset(self, queryset): _or |= q_expr if _or: + warnings.warn( + "Using OR filters is deprecated and will be removed starting with Open Forms 3.0", + DeprecationWarning, + ) queryset = queryset.filter(_or) return super().filter_queryset(queryset) diff --git a/src/openforms/forms/api/viewsets.py b/src/openforms/forms/api/viewsets.py index 99aef7fe27..5a543cf68a 100644 --- a/src/openforms/forms/api/viewsets.py +++ b/src/openforms/forms/api/viewsets.py @@ -183,7 +183,7 @@ def get_serializer_class(self): @extend_schema( summary=_("Retrieve form definition JSON schema"), - tags=["forms"], + tags=["forms", "form-definitions"], responses=OpenApiTypes.OBJECT, ) @action(methods=("GET",), detail=True) diff --git a/src/openforms/js/components/admin/form_design/Context.js b/src/openforms/js/components/admin/form_design/Context.js index f1a6d60719..8e25d34159 100644 --- a/src/openforms/js/components/admin/form_design/Context.js +++ b/src/openforms/js/components/admin/form_design/Context.js @@ -11,6 +11,7 @@ const FormContext = React.createContext({ components: {}, formSteps: [], formDefinitions: [], + reusableFormDefinitionsLoaded: false, formVariables: {}, registrationBackends: [], plugins: {}, diff --git a/src/openforms/js/components/admin/form_design/NewStepFormDefinitionPicker.js b/src/openforms/js/components/admin/form_design/NewStepFormDefinitionPicker.js index e6167586c2..ce923a8dc9 100644 --- a/src/openforms/js/components/admin/form_design/NewStepFormDefinitionPicker.js +++ b/src/openforms/js/components/admin/form_design/NewStepFormDefinitionPicker.js @@ -3,6 +3,7 @@ import React, {useContext, useState} from 'react'; import {FormattedMessage, useIntl} from 'react-intl'; import FAIcon from 'components/admin/FAIcon'; +import Loader from 'components/admin/Loader'; import {SubmitAction} from 'components/admin/forms/ActionButton'; import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; @@ -109,37 +110,43 @@ const NewStepFormDefinitionPicker = ({onReplace}) => { /> } > - - - } - errors={validationErrors} - required - > - + + - - - + + + + + ) : ( + + )} ); diff --git a/src/openforms/js/components/admin/form_design/form-creation-form.js b/src/openforms/js/components/admin/form_design/form-creation-form.js index 82082c40d7..894ce84813 100644 --- a/src/openforms/js/components/admin/form_design/form-creation-form.js +++ b/src/openforms/js/components/admin/form_design/form-creation-form.js @@ -115,6 +115,7 @@ const initialFormState = { formSteps: [], errors: {}, formDefinitions: [], + reusableFormDefinitionsLoaded: false, availableRegistrationBackends: [], availableAuthPlugins: [], availablePrefillPlugins: [], @@ -187,7 +188,7 @@ function reducer(draft, action) { * Form-level actions */ case 'BACKEND_DATA_LOADED': { - const {formData, supportingData} = action.payload; + const {supportingData, formData} = action.payload; const {form, selectedAuthPlugins, steps, variables, logicRules, priceRules} = formData; for (const [stateVar, data] of Object.entries(supportingData)) { @@ -246,6 +247,16 @@ function reducer(draft, action) { ); break; } + case 'REUSABLE_FORM_DEFINITIONS_LOADED': { + const reusableFormDefinitions = action.payload; + const formDefinitionsIds = draft.formDefinitions.map(fd => fd.uuid); + draft.formDefinitions = [ + ...draft.formDefinitions, + ...reusableFormDefinitions.filter(fd => !formDefinitionsIds.includes(fd.uuid)), + ]; + draft.reusableFormDefinitionsLoaded = true; + break; + } case 'ADD_REGISTRATION': { const {key} = action.payload; draft.form.registrationBackends.push({ @@ -953,11 +964,6 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl}) => { const backendDataToLoad = [ {endpoint: LANGUAGE_INFO_ENDPOINT, stateVar: 'languageInfo'}, {endpoint: PAYMENT_PLUGINS_ENDPOINT, stateVar: 'availablePaymentBackends'}, - { - endpoint: FORM_DEFINITIONS_ENDPOINT, - query: {is_reusable: true, used_in: formUuid || ''}, - stateVar: 'formDefinitions', - }, {endpoint: REGISTRATION_BACKENDS_ENDPOINT, stateVar: 'availableRegistrationBackends'}, {endpoint: AUTH_PLUGINS_ENDPOINT, stateVar: 'availableAuthPlugins'}, {endpoint: CATEGORIES_ENDPOINT, stateVar: 'availableCategories'}, @@ -965,6 +971,17 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl}) => { {endpoint: STATIC_VARIABLES_ENDPOINT, stateVar: 'staticVariables'}, ]; + if (formUuid) { + // We only fetch FDs used in this form if it already exists, otherwise + // it will fetch all the FDs because the `used_in` query param will have no effect. + // Reusable FDs are fetched in the background afterwards to avoid long loading time. + backendDataToLoad.push({ + endpoint: FORM_DEFINITIONS_ENDPOINT, + query: {used_in: formUuid}, + stateVar: 'formDefinitions', + }); + } + const {loading} = useAsync(async () => { const promises = [loadFromBackend(backendDataToLoad), loadForm(formUuid)]; @@ -980,6 +997,20 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl}) => { }); }, []); + useAsync(async () => { + // Waiting for the last dispatch to be done to avoid state race conditions. + if (!loading) { + const responses = await loadFromBackend([ + {endpoint: FORM_DEFINITIONS_ENDPOINT, query: {is_reusable: true}}, + ]); + const [reusableFormDefinitions] = responses; + dispatch({ + type: 'REUSABLE_FORM_DEFINITIONS_LOADED', + payload: reusableFormDefinitions, + }); + } + }, [loading]); + /** * Functions for handling events */ @@ -1181,6 +1212,7 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl}) => { components: availableComponents, formSteps: state.formSteps, formDefinitions: state.formDefinitions, + reusableFormDefinitionsLoaded: state.reusableFormDefinitionsLoaded, formVariables: state.formVariables, staticVariables: state.staticVariables, plugins: {