@qoretechnologies/reqraft 0.10.54 → 0.10.55-pr.115.g37abdfc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +25 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
- package/README.md +1 -1
- package/design/FORM_ENGINE_COMPACT.md +34 -0
- package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
- package/design/FORM_ENGINE_FIELD_MIGRATION.md +155 -1
- package/design/IDE_INTEGRATION.md +13 -0
- package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
- package/design/SELECT_DIALOG_FLOOR.md +86 -0
- package/design/SMART_EDITOR_UX.md +21 -0
- package/design/UNAVAILABLE_ALLOWED_VALUES.md +226 -0
- package/dist/components/composer/Composer.d.ts +8 -3
- package/dist/components/composer/Composer.d.ts.map +1 -1
- package/dist/components/composer/Composer.js.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
- package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
- package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
- package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlTags.js +5 -1
- package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
- package/dist/components/dpqlEditor/index.d.ts +4 -0
- package/dist/components/dpqlEditor/index.d.ts.map +1 -1
- package/dist/components/dpqlEditor/index.js +5 -1
- package/dist/components/dpqlEditor/index.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +42 -2
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/dpqlEditor/types.js +0 -4
- package/dist/components/dpqlEditor/types.js.map +1 -1
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
- package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
- package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
- package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +414 -128
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +5 -2
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +272 -19
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +666 -161
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js +41 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
- package/dist/components/form/engine/OptionFieldMessages.d.ts +108 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +203 -13
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +68 -5
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +15 -1
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +55 -29
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/optionActions.d.ts +37 -1
- package/dist/components/form/engine/optionActions.d.ts.map +1 -1
- package/dist/components/form/engine/optionActions.js +71 -1
- package/dist/components/form/engine/optionActions.js.map +1 -1
- package/dist/components/form/engine/optionsContext.d.ts +22 -0
- package/dist/components/form/engine/optionsContext.d.ts.map +1 -0
- package/dist/components/form/engine/optionsContext.js +20 -0
- package/dist/components/form/engine/optionsContext.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +42 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +76 -8
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rendererTypes.d.ts +42 -3
- package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
- package/dist/components/form/engine/rendererTypes.js +3 -1
- package/dist/components/form/engine/rendererTypes.js.map +1 -1
- package/dist/components/form/engine/rowFocus.d.ts +30 -0
- package/dist/components/form/engine/rowFocus.d.ts.map +1 -0
- package/dist/components/form/engine/rowFocus.js +93 -0
- package/dist/components/form/engine/rowFocus.js.map +1 -0
- package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
- package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
- package/dist/components/form/engine/rowMenuContext.js +103 -0
- package/dist/components/form/engine/rowMenuContext.js.map +1 -0
- package/dist/components/form/engine/rowOpenPicker.d.ts +27 -0
- package/dist/components/form/engine/rowOpenPicker.d.ts.map +1 -0
- package/dist/components/form/engine/rowOpenPicker.js +30 -0
- package/dist/components/form/engine/rowOpenPicker.js.map +1 -0
- package/dist/components/form/engine/typeChoices.d.ts +42 -0
- package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
- package/dist/components/form/engine/typeChoices.js +33 -0
- package/dist/components/form/engine/typeChoices.js.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.js +53 -0
- package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
- package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +203 -45
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/argumentPresence.d.ts +84 -0
- package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
- package/dist/components/form/expressions/argumentPresence.js +217 -0
- package/dist/components/form/expressions/argumentPresence.js.map +1 -0
- package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
- package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts +15 -2
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +21 -65
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
- package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +8 -4
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +95 -19
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/types.d.ts +7 -0
- package/dist/components/form/expressions/types.d.ts.map +1 -1
- package/dist/components/form/expressions/useExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/useExpressions.js +5 -2
- package/dist/components/form/expressions/useExpressions.js.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +50 -126
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts +18 -0
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +54 -8
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +52 -8
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts +7 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +91 -28
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/date/Date.d.ts +1 -1
- package/dist/components/form/fields/date/Date.d.ts.map +1 -1
- package/dist/components/form/fields/date/Date.js +16 -2
- package/dist/components/form/fields/date/Date.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +14 -2
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js +50 -4
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +47 -2
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +153 -12
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/select/Select.d.ts.map +1 -1
- package/dist/components/form/fields/select/Select.js +120 -20
- package/dist/components/form/fields/select/Select.js.map +1 -1
- package/dist/components/form/fields/select/SelectCollection.d.ts +166 -9
- package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
- package/dist/components/form/fields/select/SelectCollection.js +200 -7
- package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts +34 -2
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +606 -58
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/TemplateText.d.ts +36 -0
- package/dist/components/form/fields/template/TemplateText.d.ts.map +1 -0
- package/dist/components/form/fields/template/TemplateText.js +80 -0
- package/dist/components/form/fields/template/TemplateText.js.map +1 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +4 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
- package/dist/components/smartEditor/SmartEditor.js +29 -8
- package/dist/components/smartEditor/SmartEditor.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +5 -1
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +16 -2
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.d.ts +8 -4
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +7 -3
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/dpqlDetection.d.ts +58 -0
- package/dist/helpers/dpqlDetection.d.ts.map +1 -0
- package/dist/helpers/dpqlDetection.js +164 -0
- package/dist/helpers/dpqlDetection.js.map +1 -0
- package/dist/helpers/optionUiTypes.d.ts +31 -3
- package/dist/helpers/optionUiTypes.d.ts.map +1 -1
- package/dist/helpers/optionUiTypes.js +36 -1
- package/dist/helpers/optionUiTypes.js.map +1 -1
- package/dist/helpers/options.d.ts +57 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +97 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/templateItems.d.ts +47 -0
- package/dist/helpers/templateItems.d.ts.map +1 -0
- package/dist/helpers/templateItems.js +88 -0
- package/dist/helpers/templateItems.js.map +1 -0
- package/dist/helpers/templateText.d.ts +80 -0
- package/dist/helpers/templateText.d.ts.map +1 -0
- package/dist/helpers/templateText.js +291 -0
- package/dist/helpers/templateText.js.map +1 -0
- package/dist/helpers/templates.d.ts +30 -12
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +91 -33
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +17 -8
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +117 -22
- package/dist/helpers/validations.js.map +1 -1
- package/dist/hooks/useArgSchema.d.ts +2 -1
- package/dist/hooks/useArgSchema.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.d.ts +3 -1
- package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.js +47 -9
- package/dist/hooks/useFetch/useFetch.js.map +1 -1
- package/dist/hooks/useTemplates.d.ts +20 -0
- package/dist/hooks/useTemplates.d.ts.map +1 -1
- package/dist/hooks/useTemplates.js +34 -2
- package/dist/hooks/useTemplates.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/fetch.d.ts +151 -1
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +256 -44
- package/dist/utils/fetch.js.map +1 -1
- package/dist/utils/lspClient.d.ts +6 -0
- package/dist/utils/lspClient.d.ts.map +1 -1
- package/dist/utils/lspClient.js +62 -11
- package/dist/utils/lspClient.js.map +1 -1
- package/dist/utils/websocket.d.ts +6 -0
- package/dist/utils/websocket.d.ts.map +1 -1
- package/dist/utils/websocket.js +15 -7
- package/dist/utils/websocket.js.map +1 -1
- package/package.json +8 -4
- package/src/components/composer/Composer.tsx +7 -2
- package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
- package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
- package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
- package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
- package/src/components/dpqlEditor/dpqlTags.ts +13 -0
- package/src/components/dpqlEditor/index.ts +4 -0
- package/src/components/dpqlEditor/types.ts +38 -2
- package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
- package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
- package/src/components/form/engine/CompactRow.tsx +487 -160
- package/src/components/form/engine/CompactToolbar.tsx +10 -3
- package/src/components/form/engine/FormEngine.stories.tsx +1294 -161
- package/src/components/form/engine/FormEngine.tsx +916 -213
- package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +331 -0
- package/src/components/form/engine/FormFieldsSkeleton.tsx +77 -0
- package/src/components/form/engine/OptionFieldMessages.tsx +300 -14
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +94 -3
- package/src/components/form/engine/compactRowContext.ts +15 -6
- package/src/components/form/engine/compactRowStyles.ts +56 -2
- package/src/components/form/engine/compactToolbarContext.ts +1 -1
- package/src/components/form/engine/optionActions.ts +89 -1
- package/src/components/form/engine/optionsContext.ts +24 -0
- package/src/components/form/engine/readFirst.ts +88 -8
- package/src/components/form/engine/rendererTypes.ts +48 -4
- package/src/components/form/engine/rowFocus.ts +101 -0
- package/src/components/form/engine/rowMenuContext.ts +159 -0
- package/src/components/form/engine/rowOpenPicker.ts +27 -0
- package/src/components/form/engine/typeChoices.ts +63 -0
- package/src/components/form/expressions/DpqlRendering.tsx +85 -0
- package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
- package/src/components/form/expressions/ExpressionField.tsx +269 -55
- package/src/components/form/expressions/argumentPresence.ts +213 -0
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +38 -21
- package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
- package/src/components/form/expressions/builder/index.tsx +39 -81
- package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
- package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
- package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
- package/src/components/form/expressions/mockExpressions.ts +8 -4
- package/src/components/form/expressions/renderExpressionToText.ts +112 -22
- package/src/components/form/expressions/types.ts +7 -0
- package/src/components/form/expressions/useExpressions.ts +5 -1
- package/src/components/form/expressions/useRenderExpression.ts +43 -127
- package/src/components/form/fields/Field.tsx +99 -20
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +106 -44
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +85 -18
- package/src/components/form/fields/auto/AutoFormField.tsx +97 -21
- package/src/components/form/fields/date/Date.tsx +17 -1
- package/src/components/form/fields/long-string/LongString.tsx +15 -3
- package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +65 -6
- package/src/components/form/fields/rich-text/RichText.tsx +248 -30
- package/src/components/form/fields/select/Select.stories.tsx +392 -0
- package/src/components/form/fields/select/Select.tsx +164 -27
- package/src/components/form/fields/select/SelectCollection.tsx +350 -6
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
- package/src/components/form/fields/template/TemplateField.stories.tsx +93 -14
- package/src/components/form/fields/template/TemplateField.tsx +825 -131
- package/src/components/form/fields/template/TemplateText.tsx +115 -0
- package/src/components/form/index.tsx +4 -0
- package/src/components/log/Log.stories.tsx +2 -1
- package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +3 -3
- package/src/components/smartEditor/SmartEditor.tsx +41 -9
- package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
- package/src/components/smartEditor/useLspSession.ts +21 -3
- package/src/components/supportTicket/TicketReplyBox.tsx +8 -4
- package/src/components/ticketThread/TicketThread.stories.tsx +1 -0
- package/src/helpers/common.ts +7 -3
- package/src/helpers/dpqlDetection.ts +209 -0
- package/src/helpers/optionUiTypes.ts +38 -0
- package/src/helpers/options.ts +96 -0
- package/src/helpers/templateItems.ts +81 -0
- package/src/helpers/templateText.ts +350 -0
- package/src/helpers/templates.ts +94 -41
- package/src/helpers/validations.ts +138 -39
- package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
- package/src/hooks/useFetch/useFetch.tsx +53 -7
- package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
- package/src/hooks/useTemplates.ts +41 -0
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
- package/src/index.tsx +14 -1
- package/src/stores/currentUser/currentUser.stories.tsx +2 -1
- package/src/stories/storyNetwork.ts +92 -0
- package/src/utils/fetch.ts +311 -21
- package/src/utils/lspClient.ts +68 -11
- package/src/utils/websocket.ts +20 -9
- package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts +0 -73
- package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts.map +0 -1
- package/dist/components/form/engine/__fixtures__/basicFormOptions.js +0 -317
- package/dist/components/form/engine/__fixtures__/basicFormOptions.js.map +0 -1
- package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts +0 -41
- package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts.map +0 -1
- package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js +0 -45
- package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js.map +0 -1
- package/dist/components/form/expressions/dpqlMockLsp.d.ts +0 -3
- package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +0 -1
- package/dist/components/form/expressions/dpqlMockLsp.js +0 -91
- package/dist/components/form/expressions/dpqlMockLsp.js.map +0 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +0 -2
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +0 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +0 -9
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +0 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +0 -55
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +0 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js +0 -126
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +0 -1
- package/dist/stories/Data/fileFieldArgSchema.json +0 -86
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
ReqoreIcon,
|
|
7
7
|
ReqoreMessage,
|
|
8
8
|
ReqoreP,
|
|
9
|
-
ReqoreSkeleton,
|
|
10
9
|
ReqoreTag,
|
|
11
10
|
ReqoreTagGroup,
|
|
12
11
|
ReqoreVerticalSpacer,
|
|
@@ -17,6 +16,7 @@ import { IReqoreCollectionProps } from '@qoretechnologies/reqore/dist/components
|
|
|
17
16
|
import { IReqoreCollectionItemProps } from '@qoretechnologies/reqore/dist/components/Collection/item';
|
|
18
17
|
import { IReqorePanelProps } from '@qoretechnologies/reqore/dist/components/Panel';
|
|
19
18
|
import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
19
|
+
import { TFieldWithOwnTemplates } from './rendererTypes';
|
|
20
20
|
import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
|
|
21
21
|
import {
|
|
22
22
|
changeDarkness,
|
|
@@ -38,27 +38,31 @@ import {
|
|
|
38
38
|
TQorusFormOperatorValue,
|
|
39
39
|
TQorusType,
|
|
40
40
|
} from '@qoretechnologies/ts-toolkit';
|
|
41
|
-
import {
|
|
41
|
+
import { shouldMarkAsExpression } from '../expressions/argumentPresence';
|
|
42
|
+
import { offersTypeChoices } from './typeChoices';
|
|
43
|
+
import { optionRowActions, resolveOptionActions, TOptionActions } from './optionActions';
|
|
42
44
|
import { createRendererOnlyUiTypeCheck, isRendererOnlyUiType } from './rendererTypes';
|
|
43
|
-
import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last } from 'lodash';
|
|
44
|
-
import isArray from 'lodash/isArray';
|
|
45
|
+
import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last, uniq } from 'lodash';
|
|
45
46
|
import map from 'lodash/map';
|
|
46
47
|
import reduce from 'lodash/reduce';
|
|
47
48
|
import size from 'lodash/size';
|
|
48
49
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
49
50
|
import { useMeasure, useMount, useUpdateEffect } from 'react-use';
|
|
50
51
|
import styled, { css } from 'styled-components';
|
|
51
|
-
import { createContext } from 'use-context-selector';
|
|
52
52
|
import {
|
|
53
53
|
fixOperatorValue,
|
|
54
54
|
getDefaultValue,
|
|
55
55
|
insertAtIndex,
|
|
56
56
|
richtextToString,
|
|
57
57
|
} from '../../../helpers/common';
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
getRequiredOptionMessage,
|
|
60
|
+
resolveDegenerateRequiredGroups,
|
|
61
|
+
} from '../../../helpers/options';
|
|
59
62
|
import {
|
|
60
63
|
IValidationResult,
|
|
61
64
|
hasAllDependenciesFullfilled,
|
|
65
|
+
parseDependency,
|
|
62
66
|
validateField,
|
|
63
67
|
validateFieldWithResult,
|
|
64
68
|
} from '../../../helpers/validations';
|
|
@@ -75,6 +79,7 @@ import {
|
|
|
75
79
|
isValueTemplate,
|
|
76
80
|
} from '../fields/template/TemplateField';
|
|
77
81
|
import { CompactRow } from './CompactRow';
|
|
82
|
+
import { FormFieldsSkeleton } from './FormFieldsSkeleton';
|
|
78
83
|
import { CompactRowContext, ICompactRowContext, TCodePreviewRenderer } from './compactRowContext';
|
|
79
84
|
import {
|
|
80
85
|
GROUP_INDENT,
|
|
@@ -99,13 +104,16 @@ import {
|
|
|
99
104
|
import {
|
|
100
105
|
IConditionalFieldMessage,
|
|
101
106
|
OptionFieldMessages,
|
|
107
|
+
getAllowedValueAvailability,
|
|
102
108
|
getShownSchemaMessages,
|
|
103
109
|
} from './OptionFieldMessages';
|
|
110
|
+
import { OptionsContext } from './optionsContext';
|
|
104
111
|
import {
|
|
105
112
|
MarkdownRendererContext,
|
|
106
113
|
TMarkdownRenderer,
|
|
107
114
|
} from '../../Description/markdownRendererContext';
|
|
108
115
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
116
|
+
import { firstDeclaredType, isUntypedOptionType } from '../../../helpers/optionUiTypes';
|
|
109
117
|
import {
|
|
110
118
|
TReadFirstStatus,
|
|
111
119
|
findAllowedValueOption,
|
|
@@ -151,8 +159,25 @@ export type TOperatorValue = TQorusFormOperatorValue;
|
|
|
151
159
|
*/
|
|
152
160
|
export interface IOptionFieldMessage
|
|
153
161
|
extends IQorusFormFieldMessage, Pick<IConditionalFieldMessage, 'when' | 'unless'> {}
|
|
154
|
-
|
|
155
|
-
|
|
162
|
+
/** `T` without `K`, member by member — `Omit` on a union collapses the union. */
|
|
163
|
+
type TDistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
164
|
+
/**
|
|
165
|
+
* One field of a form's schema: the shared descriptor, plus what this engine
|
|
166
|
+
* reads that the shared type does not carry — conditional `messages`, and
|
|
167
|
+
* `absorb_fields`. Declared, so a descriptor using them is checked rather than
|
|
168
|
+
* cast; still assignable wherever the shared descriptor is expected.
|
|
169
|
+
*/
|
|
170
|
+
export type IOptionsSchemaArg = TDistributiveOmit<TQorusFormFieldSchema, 'messages'> & {
|
|
171
|
+
messages?: IOptionFieldMessage[];
|
|
172
|
+
/**
|
|
173
|
+
* Siblings this field renders inside its own editor instead of on rows of
|
|
174
|
+
* their own — a code editor and its language, say.
|
|
175
|
+
*/
|
|
176
|
+
absorb_fields?: string[];
|
|
177
|
+
};
|
|
178
|
+
export interface IOptionsSchema {
|
|
179
|
+
[optionName: string]: IOptionsSchemaArg;
|
|
180
|
+
}
|
|
156
181
|
export interface IOperator extends IQorusFormOperator {}
|
|
157
182
|
export interface IOperatorsSchema extends IQorusFormOperatorsSchema {}
|
|
158
183
|
export interface IOptionsOnChangeMeta extends IQorusFormFieldOnChangeMeta {
|
|
@@ -214,6 +239,48 @@ const resolveInheritProps = (
|
|
|
214
239
|
return out;
|
|
215
240
|
};
|
|
216
241
|
|
|
242
|
+
/** One shared "nothing came down from above" bag. See `inheritedScopeFor`. */
|
|
243
|
+
const NO_INHERITED_SCOPE: Record<string, unknown> = Object.freeze({});
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The inheritance bag ONE field forwards to any `arg_schema` sub-form it hosts:
|
|
247
|
+
* whatever the ancestor chain passed down, plus whatever this field's own
|
|
248
|
+
* `inherit_props` resolves to.
|
|
249
|
+
*
|
|
250
|
+
* Written as a function with a stable answer because it is a PROP, and a prop
|
|
251
|
+
* that is a new object on every render re-renders the field that receives it
|
|
252
|
+
* every time this form renders — for most fields, forever, over a value that
|
|
253
|
+
* never changed. It showed up in the console as `Template field kind Updated
|
|
254
|
+
* because: {inheritedFromParent}` repeating on a form nobody was touching:
|
|
255
|
+
* `kind` declares no `inherit_props` at all, so the bag it was handed was
|
|
256
|
+
* `{ ...(undefined ?? {}) }` — a fresh empty object each time, unequal to the
|
|
257
|
+
* last one, and `TemplateField` is memoised precisely so it does not re-render
|
|
258
|
+
* on a prop that did not change.
|
|
259
|
+
*
|
|
260
|
+
* So a field with nothing of its own to add forwards the bag it was HANDED,
|
|
261
|
+
* unchanged and unwrapped, and a form with no inherited scope at all forwards
|
|
262
|
+
* one shared frozen object rather than minting one per field per render.
|
|
263
|
+
*
|
|
264
|
+
* Mirrored in qorus-ide's `systemOptions.tsx` (which grew this treatment
|
|
265
|
+
* first); per the CLAUDE.md rule there, the forwarding logic is kept identical
|
|
266
|
+
* in both until FormEngine replaces that renderer.
|
|
267
|
+
*
|
|
268
|
+
* @param inheritProps the field's `inherit_props` map, if it declares one
|
|
269
|
+
* @param availableOptions this form's fields, where a sibling's value is read
|
|
270
|
+
* @param inheritedFromParent the bag threaded in from an outer form
|
|
271
|
+
*/
|
|
272
|
+
export const inheritedScopeFor = (
|
|
273
|
+
inheritProps: Record<string, string> | undefined,
|
|
274
|
+
availableOptions: TQorusForm | undefined,
|
|
275
|
+
inheritedFromParent: Record<string, unknown> | undefined
|
|
276
|
+
): Record<string, unknown> => {
|
|
277
|
+
const resolved = resolveInheritProps(inheritProps, availableOptions, inheritedFromParent);
|
|
278
|
+
if (!Object.keys(resolved).length) {
|
|
279
|
+
return inheritedFromParent ?? NO_INHERITED_SCOPE;
|
|
280
|
+
}
|
|
281
|
+
return { ...(inheritedFromParent ?? {}), ...resolved };
|
|
282
|
+
};
|
|
283
|
+
|
|
217
284
|
const NegativeColorEffect: any = {
|
|
218
285
|
gradient: {
|
|
219
286
|
colors: { 0: 'danger', 100: 'danger:darken:2' },
|
|
@@ -257,9 +324,9 @@ const StyledCompactWrap = styled.div<{ $ownScroll?: boolean }>`
|
|
|
257
324
|
toolbar silently stops pinning. \`clip\` keeps the horizontal guard without
|
|
258
325
|
that coercion. */
|
|
259
326
|
${({ $ownScroll }) =>
|
|
260
|
-
$ownScroll
|
|
261
|
-
|
|
262
|
-
|
|
327
|
+
$ownScroll ?
|
|
328
|
+
css`
|
|
329
|
+
/* 'own': we are the scroller. The sticky toolbar pins to whatever
|
|
263
330
|
scrolls, so a host whose scrollport carries top padding would
|
|
264
331
|
resolve sticky \`top: 0\` against its padding box and leave an
|
|
265
332
|
unblurred strip above the toolbar. Scrolling in this unpadded box
|
|
@@ -268,22 +335,22 @@ const StyledCompactWrap = styled.div<{ $ownScroll?: boolean }>`
|
|
|
268
335
|
|
|
269
336
|
\`min-height: 0\` is what lets a flex item shrink below its content
|
|
270
337
|
so there is something to scroll; it belongs to this branch only. */
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
338
|
+
min-height: 0;
|
|
339
|
+
max-height: 100%;
|
|
340
|
+
overflow-y: auto;
|
|
341
|
+
overflow-x: hidden;
|
|
342
|
+
`
|
|
343
|
+
: css`
|
|
344
|
+
/* \`min-height: auto\` is load-bearing, not a default: inside a column
|
|
278
345
|
flex parent the shrink allowance above collapses this box to zero
|
|
279
346
|
height while its rows paint outside it, so the form looks right but
|
|
280
347
|
contributes nothing to layout — the host cannot size or scroll to
|
|
281
348
|
it and anything below it stacks against nothing. */
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
349
|
+
min-height: auto;
|
|
350
|
+
max-height: none;
|
|
351
|
+
overflow-y: visible;
|
|
352
|
+
overflow-x: clip;
|
|
353
|
+
`}
|
|
287
354
|
|
|
288
355
|
/* Option logos (e.g. language images) render as <img> inside ReqoreIcon's
|
|
289
356
|
square box; constrain them so portrait PNGs don't overflow the row. */
|
|
@@ -318,7 +385,7 @@ export const getType = (
|
|
|
318
385
|
operator?: TOperatorValue
|
|
319
386
|
): TQorusType => {
|
|
320
387
|
const finalType = getTypeFromOperator(operators, fixOperatorValue(operator)) || type;
|
|
321
|
-
const resolvedType =
|
|
388
|
+
const resolvedType = firstDeclaredType(finalType);
|
|
322
389
|
|
|
323
390
|
// A value can be received before its server-driven option schema. Keep the
|
|
324
391
|
// renderer type-safe during that transition without guessing a concrete type.
|
|
@@ -355,17 +422,59 @@ const getOptionSchemaStorageType = (
|
|
|
355
422
|
: isRendererOnly(option?.ui_type) ? option?.type || option?.ui_type
|
|
356
423
|
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
357
424
|
|
|
358
|
-
|
|
425
|
+
/**
|
|
426
|
+
* Whether an option currently holds an expression, in EITHER shape it comes in.
|
|
427
|
+
*
|
|
428
|
+
* At runtime the editor writes the flag onto the option itself
|
|
429
|
+
* (`{ type, is_expression: true, value }`), but a value loaded from a saved
|
|
430
|
+
* draft carries the envelope NESTED (`{ type, value: { is_expression: true,
|
|
431
|
+
* value } }`). Same thing, two shapes, and only the flat one was ever checked —
|
|
432
|
+
* so a freshly typed expression behaved while a reloaded one did not, which is
|
|
433
|
+
* exactly how this survived several fixes: every test used the runtime shape.
|
|
434
|
+
*/
|
|
435
|
+
const optionHoldsExpression = (option: unknown): boolean => {
|
|
436
|
+
const o = option as { is_expression?: unknown; value?: { is_expression?: unknown } } | undefined;
|
|
437
|
+
return !!(o?.is_expression || o?.value?.is_expression);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Which type an option resolves to — the schema's, or the one stored on the
|
|
442
|
+
* value.
|
|
443
|
+
*
|
|
444
|
+
* Exported because this single decision is what every option field is rendered
|
|
445
|
+
* with, and what an expression's return type is checked against. It deserves
|
|
446
|
+
* to be assertable without standing up a form.
|
|
447
|
+
*/
|
|
448
|
+
export const getOptionFieldStorageType = (
|
|
359
449
|
optionName: string,
|
|
360
450
|
fieldType: TQorusType | TQorusType[] | undefined,
|
|
361
451
|
schema?: IQorusFormSchema,
|
|
362
452
|
operators?: IOperatorsSchema,
|
|
363
453
|
operatorData?: TOperatorValue,
|
|
364
|
-
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
454
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType,
|
|
455
|
+
/** Whether the stored value is an expression — see below. */
|
|
456
|
+
isExpression?: boolean
|
|
365
457
|
): TQorusType => {
|
|
366
458
|
const schemaOption = schema?.[optionName];
|
|
459
|
+
/* A STORED type does not describe an expression.
|
|
460
|
+
|
|
461
|
+
An expression value is `{ is_expression: true, value: {...} }`, so the type
|
|
462
|
+
recorded beside it describes that envelope — `hash` — and not what the
|
|
463
|
+
field holds. Letting it win means the SCHEMA's declared type is ignored and
|
|
464
|
+
the wrong answer is handed to the expression editor as the return type to
|
|
465
|
+
check against: `1 + 2` on a field declared `"type": "auto"` came back
|
|
466
|
+
"The expression returns int, and this field holds hash". A literal `1` was
|
|
467
|
+
fine, because a literal is stored as a plain value whose type matches.
|
|
468
|
+
|
|
469
|
+
`auto`/`any` accept anything, which is exactly why nothing should be
|
|
470
|
+
checked against them; a concrete declared type still is. The schema is the
|
|
471
|
+
only party that knows what the field accepts. */
|
|
367
472
|
const storedType =
|
|
368
|
-
|
|
473
|
+
(
|
|
474
|
+
!isExpression &&
|
|
475
|
+
fieldType &&
|
|
476
|
+
!(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType))
|
|
477
|
+
) ?
|
|
369
478
|
fieldType
|
|
370
479
|
: getOptionSchemaStorageType(schemaOption, isRendererOnly);
|
|
371
480
|
|
|
@@ -376,10 +485,10 @@ export const hasRequiredOptions = (options: IQorusFormSchema = {}) => {
|
|
|
376
485
|
return !!findKey(options, (option) => option.required);
|
|
377
486
|
};
|
|
378
487
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
488
|
+
/* Re-exported from the module that owns it so the existing import path keeps
|
|
489
|
+
working; see `optionsContext.ts` for why it is not declared here. */
|
|
490
|
+
export { OptionsContext } from './optionsContext';
|
|
491
|
+
export type { IOptionsContext } from './optionsContext';
|
|
383
492
|
|
|
384
493
|
export const fixOptions = (
|
|
385
494
|
value: TQorusForm | TQorusFlatForm = {},
|
|
@@ -557,6 +666,85 @@ const normalizeEmptyFieldValues = (fields: TQorusForm | TQorusFlatForm | undefin
|
|
|
557
666
|
{} as TQorusForm
|
|
558
667
|
);
|
|
559
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Does this rebuild say anything the parent does not already hold?
|
|
671
|
+
*
|
|
672
|
+
* `false` means the rebuild is a MIRROR: `fixOptions` found no default to
|
|
673
|
+
* materialise, so the result repeats the `value` the parent handed down. The
|
|
674
|
+
* compare is empty-normalized because `fixOptions` round-trips a required-empty
|
|
675
|
+
* field between `{ value: '' }` and no `value` key, and that cosmetic
|
|
676
|
+
* difference would otherwise read as new information forever.
|
|
677
|
+
*/
|
|
678
|
+
export const isMirrorOfValue = (
|
|
679
|
+
fixedValue: TQorusForm | TQorusFlatForm | undefined,
|
|
680
|
+
value: TQorusForm | TQorusFlatForm | undefined
|
|
681
|
+
): boolean => isEqual(normalizeEmptyFieldValues(fixedValue), normalizeEmptyFieldValues(value));
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* What this rebuild lets the form agree with the parent on, if anything.
|
|
685
|
+
*
|
|
686
|
+
* Returns the rebuild when it mirrors `value`, and `undefined` when it
|
|
687
|
+
* contributed something the parent has yet to learn.
|
|
688
|
+
*
|
|
689
|
+
* Adopting the parent's value IS agreement, and saying so matters as much as
|
|
690
|
+
* staying quiet about it. The sync-down skips only when the parent echoed what
|
|
691
|
+
* this form last sent; if adopting a value never updates that record, it points
|
|
692
|
+
* at an older state forever, the skip can never fire again, and the form
|
|
693
|
+
* rebuilds on every single change. Holding the mirror back WITHOUT recording
|
|
694
|
+
* the agreement was measured making the starvation worse, not better — 8 s
|
|
695
|
+
* became a 45 s timeout — which is why the two belong together.
|
|
696
|
+
*/
|
|
697
|
+
export const agreementFromRebuild = (
|
|
698
|
+
fixedValue: TQorusForm | TQorusFlatForm | undefined,
|
|
699
|
+
value: TQorusForm | TQorusFlatForm | undefined
|
|
700
|
+
): TQorusForm | TQorusFlatForm | undefined =>
|
|
701
|
+
isMirrorOfValue(fixedValue, value) ? fixedValue : undefined;
|
|
702
|
+
|
|
703
|
+
/** The state the emit decision is made against. */
|
|
704
|
+
export interface IEmitDecision {
|
|
705
|
+
/** The form's own copy of the value, as it stands now. */
|
|
706
|
+
localValue: TQorusForm | TQorusFlatForm | undefined;
|
|
707
|
+
/** The `value` prop, read at the moment the decision is made. */
|
|
708
|
+
value: TQorusForm | TQorusFlatForm | undefined;
|
|
709
|
+
/**
|
|
710
|
+
* The last rebuild that mirrored `value` without adding anything, if the most
|
|
711
|
+
* recent one did. `undefined` when the last rebuild contributed something.
|
|
712
|
+
*/
|
|
713
|
+
mirroredValue: TQorusForm | TQorusFlatForm | undefined;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Should the form report `localValue` to its parent as an answer?
|
|
718
|
+
*
|
|
719
|
+
* Extracted and pure because the situation it exists for cannot be reproduced
|
|
720
|
+
* through a rendered form: it needs the emit to be decided against a `value`
|
|
721
|
+
* NEWER than the `localValue` built from it, which only happens when two
|
|
722
|
+
* components' effects land in one React commit — and a test renderer flushes
|
|
723
|
+
* effects between updates, so the two never fall out of step. The states below
|
|
724
|
+
* were recorded from a live instrumented run instead, and this is what makes
|
|
725
|
+
* them assertable.
|
|
726
|
+
*
|
|
727
|
+
* Two ways the answer is no:
|
|
728
|
+
*
|
|
729
|
+
* 1. `localValue` is a mirror — a rebuild of the parent's own value that added
|
|
730
|
+
* nothing. Repeating it back is not an answer. **Checked first**, because
|
|
731
|
+
* the comparison in (2) reads whatever `value` has become by now, which,
|
|
732
|
+
* when a nested engine shares the parent value, is already that engine's
|
|
733
|
+
* newer write; comparing against it would find a difference this form never
|
|
734
|
+
* made and emit the older mirror over the newer write. That is the loop.
|
|
735
|
+
* 2. `localValue` already equals `value` — there is nothing to report.
|
|
736
|
+
*/
|
|
737
|
+
export const shouldEmitLocalValue = ({
|
|
738
|
+
localValue,
|
|
739
|
+
value,
|
|
740
|
+
mirroredValue,
|
|
741
|
+
}: IEmitDecision): boolean => {
|
|
742
|
+
if (mirroredValue !== undefined && isEqual(localValue, mirroredValue)) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
return !isEqual(localValue, value);
|
|
746
|
+
};
|
|
747
|
+
|
|
560
748
|
export const flattenOptions = (options: TQorusForm): TQorusFlatForm => {
|
|
561
749
|
return reduce(
|
|
562
750
|
options,
|
|
@@ -592,7 +780,7 @@ export const getTypeAndCanBeNull = (
|
|
|
592
780
|
return {
|
|
593
781
|
type: realType,
|
|
594
782
|
defaultType: realType,
|
|
595
|
-
defaultInternalType: realType
|
|
783
|
+
defaultInternalType: isUntypedOptionType(realType) ? undefined : realType,
|
|
596
784
|
canBeNull,
|
|
597
785
|
};
|
|
598
786
|
};
|
|
@@ -610,7 +798,7 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
610
798
|
name: string;
|
|
611
799
|
uniqueName?: string;
|
|
612
800
|
value?: TQorusForm | TQorusFlatForm;
|
|
613
|
-
options?:
|
|
801
|
+
options?: IOptionsSchema;
|
|
614
802
|
onChange?: (name: string, value?: TQorusForm, meta?: IOptionsOnChangeMeta) => void;
|
|
615
803
|
/**
|
|
616
804
|
* `'immediate'` (default): edits flow out via the debounced `onChange`.
|
|
@@ -652,7 +840,28 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
652
840
|
onOptionsLoaded?: (options: IQorusFormSchema) => void;
|
|
653
841
|
recordRequiresSearchOptions?: boolean;
|
|
654
842
|
readOnly?: boolean;
|
|
843
|
+
/**
|
|
844
|
+
* What a click on a READ-ONLY row does, when there is somewhere to send it.
|
|
845
|
+
*
|
|
846
|
+
* A read-only row opens nothing by default — it shows everything it has and
|
|
847
|
+
* answers a click with silence, which is right when the form is the only
|
|
848
|
+
* place the value exists. Where the same field IS editable somewhere else,
|
|
849
|
+
* that silence is a dead end: the reader clicking a value they want to
|
|
850
|
+
* change has said exactly what they want.
|
|
851
|
+
*
|
|
852
|
+
* Given this, each row becomes the way in to that place and announces it —
|
|
853
|
+
* it takes a button's role, keyboard handling and cursor. Omit it and
|
|
854
|
+
* nothing changes.
|
|
855
|
+
*/
|
|
856
|
+
onReadOnlyActivate?: (optionName: string) => void;
|
|
655
857
|
allowTemplates?: boolean;
|
|
858
|
+
/**
|
|
859
|
+
* The form's SHARED template vocabulary — config items, system properties —
|
|
860
|
+
* offered to every field that supports templates.
|
|
861
|
+
*
|
|
862
|
+
* A field may also declare `templates` of its own in the schema, and that
|
|
863
|
+
* narrower list wins for that field: see {@link TFieldWithOwnTemplates}.
|
|
864
|
+
*/
|
|
656
865
|
stringTemplates?: IReqoreFormTemplates;
|
|
657
866
|
/** Opt-in: fetch global templates from `system/getContextData` for this context. */
|
|
658
867
|
interfaceContext?: string;
|
|
@@ -727,13 +936,78 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
727
936
|
* the reader is choosing WHICH to open rather than reading this one, e.g. a
|
|
728
937
|
* template's per-action option forms stacked in a drawer.
|
|
729
938
|
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
939
|
+
* One escape is not negotiable: a box always opens while a search query is
|
|
940
|
+
* running (`ReqorePanel` unmounts collapsed content, so a match inside a
|
|
941
|
+
* closed box would be unreachable).
|
|
942
|
+
*
|
|
943
|
+
* A second applies only by DEFAULT: the Optional box stays open when every
|
|
944
|
+
* row is optional, because a form that collapsed its only box would render a
|
|
945
|
+
* card that looks empty and broken. Passing this prop explicitly waives it —
|
|
946
|
+
* a host that supplies its own heading and explanation around the form (a
|
|
947
|
+
* run-options panel, say) has already told the reader what the card is, and
|
|
948
|
+
* asked for the settings to start folded away.
|
|
735
949
|
*/
|
|
736
950
|
compactCollapsedGroups?: TFormEngineBoxKey[];
|
|
951
|
+
/**
|
|
952
|
+
* Compact mode only: rename the status boxes.
|
|
953
|
+
*
|
|
954
|
+
* The three are `Needs attention`, `Set` and `Optional`, which read correctly
|
|
955
|
+
* when the form is the page's subject. Inside a host that already named the
|
|
956
|
+
* subject — a panel headed "Change a setting for this run" — a bare
|
|
957
|
+
* `Optional` names a category rather than the things in it, and
|
|
958
|
+
* `Optional settings` is what the reader is looking at. Only the labels
|
|
959
|
+
* change; bucketing, icons and intents are untouched.
|
|
960
|
+
*/
|
|
961
|
+
compactBoxLabels?: Partial<Record<TFormEngineBoxKey, string>>;
|
|
962
|
+
/**
|
|
963
|
+
* Compact mode only: the three status-box headers stay pinned to the top of
|
|
964
|
+
* whatever scrolls while their own box is in view. Default `true`.
|
|
965
|
+
*
|
|
966
|
+
* A box header is the only thing that says which bucket a row is in — `Set`,
|
|
967
|
+
* `Needs attention`, `Optional`. On a form long enough to scroll it leaves
|
|
968
|
+
* the screen, and every row below it then reads as an undifferentiated list:
|
|
969
|
+
* the reader deep inside `Optional` has no way to tell it from `Set` except
|
|
970
|
+
* by scrolling back. Pinning the header is what keeps the answer on screen.
|
|
971
|
+
*
|
|
972
|
+
* ALL THREE behave identically, deliberately. They are one control rendered
|
|
973
|
+
* three times, differing only in label, tint and icon; a reader who has
|
|
974
|
+
* scrolled into the Optional box needs to know that exactly as much as one
|
|
975
|
+
* who has scrolled into Set, and a rule that pinned only some of them would
|
|
976
|
+
* make the absence of a pinned header mean two different things (this box has
|
|
977
|
+
* no header vs. you are above every box).
|
|
978
|
+
*
|
|
979
|
+
* A header can never outlive its own box: `position: sticky` is bounded by
|
|
980
|
+
* its containing block, so each header unpins as its box scrolls away and at
|
|
981
|
+
* most ONE box header is ever pinned at a time. The pin costs one header's
|
|
982
|
+
* height, not three.
|
|
983
|
+
*
|
|
984
|
+
* Off by construction for a nested (`arg_schema`) sub-form — it sits inside
|
|
985
|
+
* the parent form's card and owns no scroll context to pin against.
|
|
986
|
+
*
|
|
987
|
+
* On a NARROW form this also takes the pin off the form's own toolbar, so the
|
|
988
|
+
* two never cost a third of a phone's screen between them — see the note at
|
|
989
|
+
* the panel's `stickyHeader`.
|
|
990
|
+
*
|
|
991
|
+
* Turn it off where the form is one item in a scrolling LIST of forms: there
|
|
992
|
+
* the reader is choosing which form to read, and a box header pinned from the
|
|
993
|
+
* item they have scrolled past is chrome belonging to something they have
|
|
994
|
+
* left.
|
|
995
|
+
*/
|
|
996
|
+
compactStickyBoxHeaders?: boolean;
|
|
997
|
+
/**
|
|
998
|
+
* Compact mode only: how many pixels of the HOST's own sticky chrome sit
|
|
999
|
+
* above this form, inside the same scroll container.
|
|
1000
|
+
*
|
|
1001
|
+
* Sticky offsets do not compose by themselves — every pinned thing resolves
|
|
1002
|
+
* `top` against the same scrollport, so two of them with the same offset land
|
|
1003
|
+
* on top of each other. A host that pins a bar of its own (a page header
|
|
1004
|
+
* carrying a name and a Run control, say) measures it and passes the height
|
|
1005
|
+
* here; the form then pins its toolbar below that bar, and its box headers
|
|
1006
|
+
* below the toolbar, instead of underneath both.
|
|
1007
|
+
*
|
|
1008
|
+
* Default `0` — the form is the topmost pinned thing in its scroller.
|
|
1009
|
+
*/
|
|
1010
|
+
compactStickyOffset?: number;
|
|
737
1011
|
/**
|
|
738
1012
|
* Compact mode only: which parts of the form's toolbar to show. `true`
|
|
739
1013
|
* (default) shows all of it; `false` hides the whole thing.
|
|
@@ -781,6 +1055,37 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
781
1055
|
* a consumer injecting many actions can never blow out the row.
|
|
782
1056
|
*/
|
|
783
1057
|
optionActionsCollapse?: 'auto' | 'always' | 'never';
|
|
1058
|
+
/**
|
|
1059
|
+
* The HOST is still loading something this form needs.
|
|
1060
|
+
*
|
|
1061
|
+
* Declared because it was already read (`rest.skeleton`) and honoured by the
|
|
1062
|
+
* loading gate, but undeclared props are invisible to the people who need
|
|
1063
|
+
* them — and the reason to reach for this is not obvious.
|
|
1064
|
+
*
|
|
1065
|
+
* A host that resolves anything before it can render a form — an option list
|
|
1066
|
+
* fetched by `get_message`, a schema id resolved over the wire — has a wait of
|
|
1067
|
+
* its own that ends just as this component's begins. Drawing its own
|
|
1068
|
+
* placeholder for that first half and then handing over produces TWO
|
|
1069
|
+
* placeholders in series for one load, from two different components: the DOM
|
|
1070
|
+
* node is replaced, the geometry can differ, and the page relayouts in the
|
|
1071
|
+
* middle of waiting. Measured on the Qorus alert rule, whose three sections
|
|
1072
|
+
* each did this — six placeholder mounts where three would do.
|
|
1073
|
+
*
|
|
1074
|
+
* Passing the host's own wait in here instead makes ONE component own the
|
|
1075
|
+
* whole wait, from the host's first fetch to this form's last, with one
|
|
1076
|
+
* placeholder and one DOM node.
|
|
1077
|
+
*/
|
|
1078
|
+
skeleton?: boolean;
|
|
1079
|
+
/**
|
|
1080
|
+
* What the HOST is waiting for, when it hands its wait down via `skeleton`.
|
|
1081
|
+
*
|
|
1082
|
+
* Without it every host wait reads as the same word and the reading stops at
|
|
1083
|
+
* the package boundary — measured on the live alert rule, three sections all
|
|
1084
|
+
* reported `host` and the next step was guessing which of the host's own five
|
|
1085
|
+
* conditions had fired. Surfaced as `data-wait` on the placeholder.
|
|
1086
|
+
*/
|
|
1087
|
+
skeletonReason?: string;
|
|
1088
|
+
|
|
784
1089
|
/**
|
|
785
1090
|
* SEAM (reqraft): consumer-injected field editors for types reqraft doesn't
|
|
786
1091
|
* ship (IDE domain fields). Keyed by field `type`/`ui_type`; forwarded through
|
|
@@ -817,6 +1122,28 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
817
1122
|
* consumer's new editor no longer needs a reqraft release to be handled.
|
|
818
1123
|
*/
|
|
819
1124
|
rendererOnlyUiTypes?: string[];
|
|
1125
|
+
/**
|
|
1126
|
+
* The `ui_type` names whose own editor renders templates as chips INLINE.
|
|
1127
|
+
*
|
|
1128
|
+
* For these the template selector is suppressed and the field's editor is
|
|
1129
|
+
* shown instead — the selector would replace a control the author can type in
|
|
1130
|
+
* with one they can only pick from. Merged with reqraft's built-in list
|
|
1131
|
+
* (`richtext`), the same way `rendererOnlyUiTypes` is.
|
|
1132
|
+
*/
|
|
1133
|
+
templateAwareUiTypes?: string[];
|
|
1134
|
+
/**
|
|
1135
|
+
* Extra `ui_type` names that mean "a value of any type", for the purpose of
|
|
1136
|
+
* offering the per-type choices in the row's menu.
|
|
1137
|
+
*
|
|
1138
|
+
* A field that pins no concrete type offers "enter a value of type X" for
|
|
1139
|
+
* each type the form knows. That was gated on the schema saying
|
|
1140
|
+
* `ui_type: 'any'` alone, which misses two cases: an option that declares its
|
|
1141
|
+
* untypedness as `type: 'auto'` and carries no `ui_type` at all (the server
|
|
1142
|
+
* spells an assertion's Expected Value exactly that way), and an option whose
|
|
1143
|
+
* `ui_type` is a HOST's own untyped editor. Merged with reqraft's built-ins
|
|
1144
|
+
* (`any`, `auto`), the same way `templateAwareUiTypes` is.
|
|
1145
|
+
*/
|
|
1146
|
+
anyLikeUiTypes?: string[];
|
|
820
1147
|
|
|
821
1148
|
/**
|
|
822
1149
|
* Bag of values forwarded from an outer FormEngine scope, used as a
|
|
@@ -878,12 +1205,17 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
878
1205
|
* for the pane to exist at all — so a pasted or reloaded link lands on a
|
|
879
1206
|
* collapsed form.
|
|
880
1207
|
*
|
|
881
|
-
* Applied once, on the first render
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
1208
|
+
* Applied once, on the first render that actually CONTAINS one of the named
|
|
1209
|
+
* rows — not merely the first render with any rows — so a schema arriving in
|
|
1210
|
+
* pieces still gets its address applied rather than spending the one shot on
|
|
1211
|
+
* a form the field had not reached yet.
|
|
1212
|
+
*
|
|
1213
|
+
* It never re-expands a row the user has since collapsed, and it does not
|
|
1214
|
+
* participate in `expandMode: 'single'` accordion collapsing — the caller is
|
|
1215
|
+
* naming a starting point, not driving the state. That holds against the
|
|
1216
|
+
* engine's own openers too: `expandFirstRequired` / `autoFocusFirstRequired`
|
|
1217
|
+
* open their target ALONGSIDE these rows in single mode instead of replacing
|
|
1218
|
+
* them. Only a remount re-arms it. No-op in classic (non-compact) mode.
|
|
887
1219
|
*/
|
|
888
1220
|
initialExpandedOptions?: string[];
|
|
889
1221
|
|
|
@@ -925,6 +1257,7 @@ const FormEngineImpl = ({
|
|
|
925
1257
|
onOptionsLoaded,
|
|
926
1258
|
recordRequiresSearchOptions,
|
|
927
1259
|
readOnly,
|
|
1260
|
+
onReadOnlyActivate,
|
|
928
1261
|
allowTemplates = true,
|
|
929
1262
|
interfaceContext,
|
|
930
1263
|
templateFieldProps,
|
|
@@ -941,7 +1274,10 @@ const FormEngineImpl = ({
|
|
|
941
1274
|
compactNested = false,
|
|
942
1275
|
compactScroll = 'host',
|
|
943
1276
|
compactPanelProps,
|
|
944
|
-
compactCollapsedGroups
|
|
1277
|
+
compactCollapsedGroups: compactCollapsedGroupsProp,
|
|
1278
|
+
compactBoxLabels,
|
|
1279
|
+
compactStickyBoxHeaders = true,
|
|
1280
|
+
compactStickyOffset = 0,
|
|
945
1281
|
compactToolbar = true,
|
|
946
1282
|
commitMode = 'immediate',
|
|
947
1283
|
expandMode = 'single',
|
|
@@ -959,6 +1295,8 @@ const FormEngineImpl = ({
|
|
|
959
1295
|
// onto a DOM node
|
|
960
1296
|
markdownRenderer: _markdownRenderer, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
961
1297
|
rendererOnlyUiTypes,
|
|
1298
|
+
templateAwareUiTypes,
|
|
1299
|
+
anyLikeUiTypes,
|
|
962
1300
|
inheritedFromParent,
|
|
963
1301
|
autoFocusFirstRequired,
|
|
964
1302
|
expandFirstRequired,
|
|
@@ -977,17 +1315,27 @@ const FormEngineImpl = ({
|
|
|
977
1315
|
// from Reqore's context (one subscription for the whole app) rather than a
|
|
978
1316
|
// reqraft-local matchMedia hook.
|
|
979
1317
|
//
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
const isHoverCapable = useReqoreProperty('isHoverCapable')
|
|
1318
|
+
// Both are always set by `ReqoreProvider`, and the peer floor (>= 0.74.0) is
|
|
1319
|
+
// the version that sets them. A defensive `?? true` here would be dead code:
|
|
1320
|
+
// `useReqoreProperty` THROWS on a context that lacks the key, so an older
|
|
1321
|
+
// Reqore fails at the hook rather than degrading to a default.
|
|
1322
|
+
const isHoverCapable = useReqoreProperty('isHoverCapable');
|
|
985
1323
|
const isMobile = useReqoreProperty('isMobile');
|
|
986
1324
|
const collapseOptionActions =
|
|
987
1325
|
optionActionsCollapse === 'always' ? true
|
|
988
1326
|
: optionActionsCollapse === 'never' ? false
|
|
989
1327
|
: !isHoverCapable || !!isMobile;
|
|
990
|
-
|
|
1328
|
+
/* The schema, as the form should ASK it — not always as it was served.
|
|
1329
|
+
`resolveDegenerateRequiredGroups` turns a one-of group that has a single
|
|
1330
|
+
member in this schema back into a plain required field, because a choice
|
|
1331
|
+
with one option is not a choice. Applied here, at the one place the schema
|
|
1332
|
+
enters the component, so the header, the chips, the messages, the meter and
|
|
1333
|
+
the read-first summary cannot disagree about it: every one of them reads
|
|
1334
|
+
`options`, and none of them has to know the rule. */
|
|
1335
|
+
const [servedOptions, setOptions] = useState<IQorusFormSchema | undefined>(
|
|
1336
|
+
rest?.options || undefined
|
|
1337
|
+
);
|
|
1338
|
+
const options = useMemo(() => resolveDegenerateRequiredGroups(servedOptions), [servedOptions]);
|
|
991
1339
|
// optionsLoader lifecycle: loading feeds the skeleton gate, error the banner.
|
|
992
1340
|
const [optionsLoading, setOptionsLoading] = useState<boolean>(!!optionsLoader && !rest?.options);
|
|
993
1341
|
const [optionsError, setOptionsError] = useState<string | undefined>();
|
|
@@ -1079,6 +1427,67 @@ const FormEngineImpl = ({
|
|
|
1079
1427
|
},
|
|
1080
1428
|
[compactWrapRef]
|
|
1081
1429
|
);
|
|
1430
|
+
/* Declared here, not where it is first USED in the render: the sticky
|
|
1431
|
+
measurement below takes it as a dependency, and a dependency array is
|
|
1432
|
+
evaluated during render — a `const` declared further down would still be
|
|
1433
|
+
in its temporal dead zone. */
|
|
1434
|
+
const compactNarrow = !!compactWrapWidth && compactWrapWidth < 480;
|
|
1435
|
+
|
|
1436
|
+
/* How much pinned chrome sits above a status-box header.
|
|
1437
|
+
|
|
1438
|
+
Sticky offsets do not compose on their own: everything pinned in one
|
|
1439
|
+
scroller resolves `top` against the same scrollport, so a box header and
|
|
1440
|
+
this form's own toolbar both asking for 0 land on top of each other — and
|
|
1441
|
+
the toolbar is the loser, because it renders first and the box header wins
|
|
1442
|
+
on z-order. Covering it hides the search, which is the one control that
|
|
1443
|
+
forces a collapsed box open.
|
|
1444
|
+
|
|
1445
|
+
So the box headers pin BELOW the toolbar, at the host's own offset plus the
|
|
1446
|
+
toolbar's height. Measured, not assumed: the toolbar is a completion meter,
|
|
1447
|
+
a search row and a field picker that each hide themselves when they have
|
|
1448
|
+
nothing to say, and what is left wraps differently at every form width.
|
|
1449
|
+
Zero when this form draws no toolbar at all. */
|
|
1450
|
+
const [compactToolbarHeight, setCompactToolbarHeight] = useState(0);
|
|
1451
|
+
useLayoutEffect(() => {
|
|
1452
|
+
const wrap = compactWrapNode;
|
|
1453
|
+
if (!compact || !wrap || compactNested || !compactStickyBoxHeaders) {
|
|
1454
|
+
setCompactToolbarHeight(0);
|
|
1455
|
+
return undefined;
|
|
1456
|
+
}
|
|
1457
|
+
const header = wrap.querySelector<HTMLElement>(
|
|
1458
|
+
':scope > .reqore-panel > .reqore-panel-title'
|
|
1459
|
+
);
|
|
1460
|
+
if (!header) {
|
|
1461
|
+
setCompactToolbarHeight(0);
|
|
1462
|
+
return undefined;
|
|
1463
|
+
}
|
|
1464
|
+
const read = () =>
|
|
1465
|
+
setCompactToolbarHeight((prev) => {
|
|
1466
|
+
/* Only a toolbar that is ITSELF pinned takes room off the top. A host
|
|
1467
|
+
can switch the pin off (`compactPanelProps.stickyHeader`), and it
|
|
1468
|
+
does where the form is a couple of fields inside a longer page — and
|
|
1469
|
+
a box header offset by a toolbar that scrolls away would float a
|
|
1470
|
+
header's height below the top edge with nothing under it. Read from
|
|
1471
|
+
the element rather than from the props that produced it, so every
|
|
1472
|
+
route to that decision is accounted for. */
|
|
1473
|
+
const pinned = getComputedStyle(header).position === 'sticky';
|
|
1474
|
+
const next = pinned ? Math.round(header.getBoundingClientRect().height) : 0;
|
|
1475
|
+
return prev === next ? prev : next;
|
|
1476
|
+
});
|
|
1477
|
+
read();
|
|
1478
|
+
/* jsdom and older engines have no ResizeObserver; the one-shot read above
|
|
1479
|
+
still gives the right answer for a form whose toolbar never reflows. */
|
|
1480
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
1481
|
+
return undefined;
|
|
1482
|
+
}
|
|
1483
|
+
const observer = new ResizeObserver(read);
|
|
1484
|
+
observer.observe(header);
|
|
1485
|
+
return () => observer.disconnect();
|
|
1486
|
+
/* `compactNarrow` is a dependency, not decoration: crossing the width
|
|
1487
|
+
breakpoint takes the pin OFF the toolbar without changing its height, so
|
|
1488
|
+
the ResizeObserver never fires and a stale 124px would go on offsetting
|
|
1489
|
+
the box headers below a toolbar that is no longer there. */
|
|
1490
|
+
}, [compact, compactNested, compactStickyBoxHeaders, compactNarrow, compactWrapNode, options]);
|
|
1082
1491
|
|
|
1083
1492
|
// Global label-column sizing: size the label column to the WIDEST field label
|
|
1084
1493
|
// across the whole form, clamped to [MIN, MAX], and publish it as a CSS var on
|
|
@@ -1179,7 +1588,6 @@ const FormEngineImpl = ({
|
|
|
1179
1588
|
}
|
|
1180
1589
|
});
|
|
1181
1590
|
|
|
1182
|
-
const compactNarrow = !!compactWrapWidth && compactWrapWidth < 480;
|
|
1183
1591
|
// Info panels auto-open on Tier-1 content; the per-row user override sticks.
|
|
1184
1592
|
const [infoPanelOverrides, setInfoPanelOverrides] = useState<Record<string, boolean>>({});
|
|
1185
1593
|
// Toolbar filters affect the listed rows only — the meter reflects the full set.
|
|
@@ -1198,6 +1606,32 @@ const FormEngineImpl = ({
|
|
|
1198
1606
|
// Track the last value we emitted via onChange so we can skip re-applying fixOptions
|
|
1199
1607
|
// when the parent echoes it back as the new value prop (controlled component loop prevention)
|
|
1200
1608
|
const lastEmittedValue = useRef<TQorusForm | TQorusFlatForm | undefined>(value);
|
|
1609
|
+
/**
|
|
1610
|
+
* A `localValue` that is only a MIRROR of the incoming `value`, and so must
|
|
1611
|
+
* never be emitted back as though this form had answered something.
|
|
1612
|
+
*
|
|
1613
|
+
* `localValue` is derived state: the sync-down effect below rebuilds it from
|
|
1614
|
+
* the `value` prop. When that rebuild adds nothing (`fixOptions` had no
|
|
1615
|
+
* default to materialise), the result says exactly what the parent already
|
|
1616
|
+
* holds, and emitting it is not a change — it is this form repeating the
|
|
1617
|
+
* parent's own words back at it.
|
|
1618
|
+
*
|
|
1619
|
+
* That repetition is harmless in a form that owns its state outright and
|
|
1620
|
+
* fatal in one that shares a parent with a NESTED engine. The two write to
|
|
1621
|
+
* the same parent value, so each sees the other's write arrive as an external
|
|
1622
|
+
* change; `lastEmittedValue` is then always one step behind and its guard
|
|
1623
|
+
* never fires. The engines alternate between two states forever, several
|
|
1624
|
+
* times a second, and the newer write is repeatedly overwritten by the older
|
|
1625
|
+
* one. Measured in the test editor: the row's summary flipped between two
|
|
1626
|
+
* readings indefinitely and `setTimeout` was starved past a 45-second
|
|
1627
|
+
* deadline.
|
|
1628
|
+
*
|
|
1629
|
+
* Only a mirror that CHANGES nothing is held back. A rebuild that restores a
|
|
1630
|
+
* required or preselected field's default still emits, because that default
|
|
1631
|
+
* is genuinely new information the parent has to be told about — which is the
|
|
1632
|
+
* whole reason the sync-down runs `fixOptions` at all.
|
|
1633
|
+
*/
|
|
1634
|
+
const mirroredValue = useRef<TQorusForm | TQorusFlatForm | undefined>(undefined);
|
|
1201
1635
|
|
|
1202
1636
|
if (originalValue.current === undefined && size(value)) {
|
|
1203
1637
|
originalValue.current = localValue.fields;
|
|
@@ -1208,7 +1642,13 @@ const FormEngineImpl = ({
|
|
|
1208
1642
|
const templates = useTemplates(allowTemplates, rest.stringTemplates, interfaceContext);
|
|
1209
1643
|
|
|
1210
1644
|
useEffect(() => {
|
|
1211
|
-
if (
|
|
1645
|
+
if (
|
|
1646
|
+
!shouldEmitLocalValue({
|
|
1647
|
+
localValue: localValue.fields,
|
|
1648
|
+
value,
|
|
1649
|
+
mirroredValue: mirroredValue.current,
|
|
1650
|
+
})
|
|
1651
|
+
) {
|
|
1212
1652
|
return;
|
|
1213
1653
|
}
|
|
1214
1654
|
|
|
@@ -1371,9 +1811,12 @@ const FormEngineImpl = ({
|
|
|
1371
1811
|
// Note: compare fixedValue against value, not localValue.fields — localValue may have been
|
|
1372
1812
|
// updated by nested FormEngine emissions, so comparing against it would never skip.
|
|
1373
1813
|
const normalizedValue = normalizeEmptyFieldValues(value);
|
|
1814
|
+
/* What this rebuild lets us agree with the parent on — `undefined` when it
|
|
1815
|
+
contributed something the parent has yet to learn. */
|
|
1816
|
+
const agreed = agreementFromRebuild(fixedValue, value);
|
|
1374
1817
|
if (
|
|
1375
1818
|
isEqual(normalizedValue, normalizeEmptyFieldValues(lastEmittedValue.current)) &&
|
|
1376
|
-
|
|
1819
|
+
agreed !== undefined
|
|
1377
1820
|
) {
|
|
1378
1821
|
return;
|
|
1379
1822
|
}
|
|
@@ -1382,14 +1825,42 @@ const FormEngineImpl = ({
|
|
|
1382
1825
|
originalValue.current = fixedValue;
|
|
1383
1826
|
}
|
|
1384
1827
|
|
|
1828
|
+
/* Remember a mirror so the emit effect can tell one from an answer, and
|
|
1829
|
+
record the agreement it represents. A rebuild that DID add something
|
|
1830
|
+
leaves both alone — the parent has to learn about it. */
|
|
1831
|
+
mirroredValue.current = agreed;
|
|
1832
|
+
if (agreed !== undefined) {
|
|
1833
|
+
lastEmittedValue.current = agreed;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1385
1836
|
setLocalValue?.({ fields: fixedValue, meta: undefined });
|
|
1386
1837
|
}, [JSON.stringify(options), JSON.stringify(value), isRendererOnly]);
|
|
1387
1838
|
|
|
1839
|
+
/**
|
|
1840
|
+
* Fields the reader has actually edited in this instance.
|
|
1841
|
+
*
|
|
1842
|
+
* "This field is required" is an ERROR message, and an error is a report that
|
|
1843
|
+
* something went wrong. Under a field nobody has been in yet it reports
|
|
1844
|
+
* nothing — the form is empty because it is new — while the requirement is
|
|
1845
|
+
* already stated by the asterisk, by the Needs-attention box the row sits in,
|
|
1846
|
+
* and by the completion meter. A form whose first act is to accuse the reader
|
|
1847
|
+
* of a mistake they have not made teaches them to discount its warnings.
|
|
1848
|
+
*
|
|
1849
|
+
* So the message waits for a touch. Being IN the field and leaving it empty
|
|
1850
|
+
* is a real gap, and that one shows. A ref, not state: every edit re-renders
|
|
1851
|
+
* anyway, so the following render reads the current set with no second pass.
|
|
1852
|
+
*/
|
|
1853
|
+
const touchedOptionsRef = useRef<Set<string>>(new Set());
|
|
1854
|
+
|
|
1388
1855
|
const handleValueChange = useCallback(
|
|
1389
1856
|
(optionName: string, val?: any, _type?: string, isFunction?: boolean) => {
|
|
1857
|
+
// Every route into this handler is a person acting: typing, picking from a
|
|
1858
|
+
// menu, clearing the value, adding an optional field. Marked here rather
|
|
1859
|
+
// than inside the updater below, which React may invoke twice.
|
|
1860
|
+
touchedOptionsRef.current.add(optionName);
|
|
1390
1861
|
setLocalValue(({ fields = {} }) => {
|
|
1391
1862
|
const schemaType = getOptionSchemaStorageType(options?.[optionName], isRendererOnly);
|
|
1392
|
-
const isAnyLike = schemaType
|
|
1863
|
+
const isAnyLike = isUntypedOptionType(schemaType);
|
|
1393
1864
|
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
1394
1865
|
const resolvedSchemaType =
|
|
1395
1866
|
isAnyLike && (fields[optionName] as IQorusFormField)?.type ?
|
|
@@ -1441,7 +1912,19 @@ const FormEngineImpl = ({
|
|
|
1441
1912
|
},
|
|
1442
1913
|
};
|
|
1443
1914
|
|
|
1444
|
-
|
|
1915
|
+
/* The flag belongs to the VALUE, so a change that said nothing about it
|
|
1916
|
+
does not throw it away — `undefined` is no opinion, not a denial.
|
|
1917
|
+
Deleting it unconditionally left the AST on the row with nothing
|
|
1918
|
+
marking it as one: an assertion's Expected Value came back from a
|
|
1919
|
+
saved draft drawn as a two-field hash of `exp` and `args`, because
|
|
1920
|
+
merely opening the row and reverting had stripped the flag. */
|
|
1921
|
+
if (
|
|
1922
|
+
shouldMarkAsExpression(
|
|
1923
|
+
isFunction,
|
|
1924
|
+
val,
|
|
1925
|
+
((fields as TQorusForm)[optionName] as { is_expression?: boolean })?.is_expression
|
|
1926
|
+
)
|
|
1927
|
+
) {
|
|
1445
1928
|
(updatedValue[optionName] as { is_expression?: boolean }).is_expression = true;
|
|
1446
1929
|
} else {
|
|
1447
1930
|
delete updatedValue[optionName].is_expression;
|
|
@@ -1455,9 +1938,24 @@ const FormEngineImpl = ({
|
|
|
1455
1938
|
val !== (fields as TQorusForm)[optionName]?.value
|
|
1456
1939
|
) {
|
|
1457
1940
|
forEach(options, (option, depName) => {
|
|
1941
|
+
/* A dependency is `name`, `name=value` or `name!=value`, so the
|
|
1942
|
+
entry has to be PARSED to find which option it names. Compared as
|
|
1943
|
+
a whole string, only the bare spelling ever matched — so a field
|
|
1944
|
+
declared `depends_on: ['subject_iface_kind=workflow']` was never
|
|
1945
|
+
cleared when that option changed.
|
|
1946
|
+
|
|
1947
|
+
What that cost: a test whose subject was switched from a workflow
|
|
1948
|
+
to a service kept the workflow's `Subject Interface Version`.
|
|
1949
|
+
Services and jobs are versioned but only the latest is testable,
|
|
1950
|
+
so the field no longer applied, yet it stayed on the form as a
|
|
1951
|
+
locked control in front of a value the author could not clear —
|
|
1952
|
+
the value being there is exactly what stops the field being
|
|
1953
|
+
withheld outright. */
|
|
1458
1954
|
if (
|
|
1459
1955
|
option.depends_on &&
|
|
1460
|
-
flatten(option.depends_on).
|
|
1956
|
+
flatten(option.depends_on).some(
|
|
1957
|
+
(dependency) => parseDependency(dependency as string).name === optionName
|
|
1958
|
+
) &&
|
|
1461
1959
|
updatedValue[depName]
|
|
1462
1960
|
) {
|
|
1463
1961
|
updatedValue[depName].value = undefined;
|
|
@@ -1637,6 +2135,7 @@ const FormEngineImpl = ({
|
|
|
1637
2135
|
return newValue;
|
|
1638
2136
|
}
|
|
1639
2137
|
|
|
2138
|
+
|
|
1640
2139
|
const rendererType = getType(
|
|
1641
2140
|
(options[optionName].ui_type || options[optionName].type) as TQorusType,
|
|
1642
2141
|
operators,
|
|
@@ -1648,7 +2147,8 @@ const FormEngineImpl = ({
|
|
|
1648
2147
|
options,
|
|
1649
2148
|
operators,
|
|
1650
2149
|
(option as IQorusFormField)?.op,
|
|
1651
|
-
isRendererOnly
|
|
2150
|
+
isRendererOnly,
|
|
2151
|
+
optionHoldsExpression(option)
|
|
1652
2152
|
);
|
|
1653
2153
|
|
|
1654
2154
|
if (!isPlainObject(option)) {
|
|
@@ -1693,6 +2193,27 @@ const FormEngineImpl = ({
|
|
|
1693
2193
|
return { members, satisfiedBy };
|
|
1694
2194
|
}, [JSON.stringify(options), JSON.stringify(availableOptions)]);
|
|
1695
2195
|
|
|
2196
|
+
/* One bag per field, rebuilt only when something it is made of changes — the
|
|
2197
|
+
schema that declares the `inherit_props`, the sibling values they resolve
|
|
2198
|
+
against, or the scope handed in from an outer form. Held per field rather
|
|
2199
|
+
than per form because each field's bag is different: `inherit_props` is
|
|
2200
|
+
declared on the field. See `inheritedScopeFor` for what a churning bag
|
|
2201
|
+
costs the field that receives it. */
|
|
2202
|
+
const inheritedScopes = useMemo(() => {
|
|
2203
|
+
const scopes = new Map<string, Record<string, unknown>>();
|
|
2204
|
+
for (const optionName of Object.keys(options ?? {})) {
|
|
2205
|
+
scopes.set(
|
|
2206
|
+
optionName,
|
|
2207
|
+
inheritedScopeFor(
|
|
2208
|
+
options?.[optionName]?.inherit_props,
|
|
2209
|
+
availableOptions,
|
|
2210
|
+
inheritedFromParent
|
|
2211
|
+
)
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
return scopes;
|
|
2215
|
+
}, [options, availableOptions, inheritedFromParent]);
|
|
2216
|
+
|
|
1696
2217
|
// Dependency linkage (3a): when filling a dependency unlocks rows, flash
|
|
1697
2218
|
// them once — the form visibly "opens up" instead of silently changing.
|
|
1698
2219
|
const dependencyLockedNames = useMemo(() => {
|
|
@@ -1722,6 +2243,49 @@ const FormEngineImpl = ({
|
|
|
1722
2243
|
}
|
|
1723
2244
|
}, [dependencyLockedNames.join('|')]);
|
|
1724
2245
|
|
|
2246
|
+
/* The same thing one level down: a CHOICE that becomes available is as
|
|
2247
|
+
invisible as a field that becomes editable, and it is worse, because it
|
|
2248
|
+
opens up inside a control the reader may not have open. So the field that
|
|
2249
|
+
offers it flashes, which is the affordance that says "look here" without
|
|
2250
|
+
claiming the choice was made.
|
|
2251
|
+
|
|
2252
|
+
Keyed by field and by the value's identity, so a value that merely moves in
|
|
2253
|
+
the list is not mistaken for one that opened. Fields whose values gate on
|
|
2254
|
+
nothing never enter the set, which is nearly all of them. */
|
|
2255
|
+
const lockedAllowedValues = useMemo(() => {
|
|
2256
|
+
const locked: string[] = [];
|
|
2257
|
+
forEach(options || {}, (optionSchema, name) => {
|
|
2258
|
+
forEach(optionSchema?.allowed_values || [], (allowedValue) => {
|
|
2259
|
+
if (!getAllowedValueAvailability(allowedValue, availableOptions, options || {}).available) {
|
|
2260
|
+
locked.push(`${name}\u0000${JSON.stringify(allowedValue?.value?.value)}`);
|
|
2261
|
+
}
|
|
2262
|
+
});
|
|
2263
|
+
});
|
|
2264
|
+
return locked;
|
|
2265
|
+
}, [JSON.stringify(options), JSON.stringify(availableOptions)]);
|
|
2266
|
+
const previousLockedAllowedValues = useRef<string[] | null>(null);
|
|
2267
|
+
useEffect(() => {
|
|
2268
|
+
const previous = previousLockedAllowedValues.current;
|
|
2269
|
+
previousLockedAllowedValues.current = lockedAllowedValues;
|
|
2270
|
+
if (!previous) {
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const opened = previous
|
|
2274
|
+
.filter((key) => !lockedAllowedValues.includes(key))
|
|
2275
|
+
.map((key) => key.split('\u0000')[0]);
|
|
2276
|
+
const fields = uniq(opened).filter(
|
|
2277
|
+
(name) =>
|
|
2278
|
+
!!options?.[name] &&
|
|
2279
|
+
!options?.[name]?.disabled &&
|
|
2280
|
+
// A field that is itself still locked flashes for its OWN unlocking, in
|
|
2281
|
+
// the effect above; two flashes for one change is a flicker, not a cue.
|
|
2282
|
+
!dependencyLockedNames.includes(name)
|
|
2283
|
+
);
|
|
2284
|
+
if (fields.length) {
|
|
2285
|
+
flashOptions(fields);
|
|
2286
|
+
}
|
|
2287
|
+
}, [lockedAllowedValues.join('|')]);
|
|
2288
|
+
|
|
1725
2289
|
// The not-yet-added optional fields: everything in the schema the form is not
|
|
1726
2290
|
// already showing a row for. This ONE list feeds all three ways a field gets
|
|
1727
2291
|
// added — the inline addable rows, the Fields menu, and its "Select all" — so
|
|
@@ -1793,7 +2357,8 @@ const FormEngineImpl = ({
|
|
|
1793
2357
|
options,
|
|
1794
2358
|
operators,
|
|
1795
2359
|
(option as IQorusFormField).op,
|
|
1796
|
-
isRendererOnly
|
|
2360
|
+
isRendererOnly,
|
|
2361
|
+
optionHoldsExpression(option)
|
|
1797
2362
|
);
|
|
1798
2363
|
const optionValue = (option as IQorusFormField).value;
|
|
1799
2364
|
|
|
@@ -1864,7 +2429,8 @@ const FormEngineImpl = ({
|
|
|
1864
2429
|
options,
|
|
1865
2430
|
operators,
|
|
1866
2431
|
(option as IQorusFormField).op,
|
|
1867
|
-
isRendererOnly
|
|
2432
|
+
isRendererOnly,
|
|
2433
|
+
optionHoldsExpression(option)
|
|
1868
2434
|
),
|
|
1869
2435
|
(option as IQorusFormField).value
|
|
1870
2436
|
)
|
|
@@ -1913,7 +2479,8 @@ const FormEngineImpl = ({
|
|
|
1913
2479
|
options,
|
|
1914
2480
|
operators,
|
|
1915
2481
|
(availableOptions as TQorusForm)?.[name]?.op,
|
|
1916
|
-
isRendererOnly
|
|
2482
|
+
isRendererOnly,
|
|
2483
|
+
optionHoldsExpression((availableOptions as TQorusForm)?.[name])
|
|
1917
2484
|
);
|
|
1918
2485
|
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1919
2486
|
// A schema-declared hash whose every entry is a materialised-but-unset
|
|
@@ -2064,11 +2631,16 @@ const FormEngineImpl = ({
|
|
|
2064
2631
|
if (!names.length) {
|
|
2065
2632
|
return;
|
|
2066
2633
|
}
|
|
2067
|
-
hasAppliedInitialExpansionRef.current = true;
|
|
2068
2634
|
const toExpand = initialExpandedOptions.filter((name) => names.includes(name));
|
|
2069
2635
|
if (!toExpand.length) {
|
|
2636
|
+
// Not "there is no such row" — "not YET". A server-driven schema arrives
|
|
2637
|
+
// in pieces, and the row the caller named can be in a later one (a test's
|
|
2638
|
+
// `cases` field materialises once its step-kind catalogue lands). Latching
|
|
2639
|
+
// here would spend the one shot on a form that did not yet contain the
|
|
2640
|
+
// field, and the address would never be applied at all.
|
|
2070
2641
|
return;
|
|
2071
2642
|
}
|
|
2643
|
+
hasAppliedInitialExpansionRef.current = true;
|
|
2072
2644
|
setExpandedOptions((prev) => [...prev, ...toExpand.filter((name) => !prev.includes(name))]);
|
|
2073
2645
|
}, [initialExpandedOptions, compact, availableOptions]);
|
|
2074
2646
|
|
|
@@ -2089,15 +2661,13 @@ const FormEngineImpl = ({
|
|
|
2089
2661
|
// genuine remount (a fresh instance) auto-focuses again, which is the intended
|
|
2090
2662
|
// on-mount behaviour.
|
|
2091
2663
|
const hasAutoFocusedRef = useRef(false);
|
|
2092
|
-
// The field expanded programmatically for autofocus. A ref (not state) so
|
|
2093
|
-
// CompactRow's 60ms focus timer reads the current value regardless of render
|
|
2094
|
-
// batching; CompactRow focuses this one with `preventScroll` so an off-screen
|
|
2095
|
-
// (or below-the-fold) form is never scrolled into view on mount.
|
|
2096
|
-
const autoFocusNameRef = useRef<string | undefined>(undefined);
|
|
2097
2664
|
useEffect(() => {
|
|
2098
|
-
// Two decisions, one scan.
|
|
2099
|
-
//
|
|
2100
|
-
// the
|
|
2665
|
+
// Two decisions, one scan. Both open the row that needs attention;
|
|
2666
|
+
// `autoFocusFirstRequired` additionally promises the caret lands in it, and
|
|
2667
|
+
// so refuses to run at all while the reader holds the caret somewhere else
|
|
2668
|
+
// (the guard below). An opened row takes the caret either way — that is
|
|
2669
|
+
// CompactRow's tap-to-edit rule, and it declines just as firmly to take one
|
|
2670
|
+
// that is already somewhere outside the row.
|
|
2101
2671
|
const wantsFocus = !!autoFocusFirstRequired;
|
|
2102
2672
|
if ((!wantsFocus && !expandFirstRequired) || !compact || !options) {
|
|
2103
2673
|
return;
|
|
@@ -2151,15 +2721,16 @@ const FormEngineImpl = ({
|
|
|
2151
2721
|
hasAutoFocusedRef.current = true;
|
|
2152
2722
|
|
|
2153
2723
|
if (target) {
|
|
2154
|
-
// Set the ref before the state update so CompactRow's focus timer sees it.
|
|
2155
|
-
// Expand-only leaves it unset, which is what keeps the caret where it is.
|
|
2156
|
-
if (wantsFocus) {
|
|
2157
|
-
autoFocusNameRef.current = target;
|
|
2158
|
-
}
|
|
2159
2724
|
setExpandedOptions((prev) =>
|
|
2160
2725
|
prev.includes(target) ? prev
|
|
2161
2726
|
: expandMode === 'multi' ? [...prev, target]
|
|
2162
|
-
|
|
2727
|
+
// `expandMode: 'single'` is a rule about what a CLICK does: opening a
|
|
2728
|
+
// row closes the one the reader last opened. It is not a licence to
|
|
2729
|
+
// close a row the HOST named through `initialExpandedOptions`. That
|
|
2730
|
+
// is an address — a statement about what the page is FOR, made before
|
|
2731
|
+
// the reader did anything — and this scan is a convenience layered on
|
|
2732
|
+
// top of it. Keep the named rows; replace only what a click could.
|
|
2733
|
+
: [...prev.filter((name) => initialExpandedOptions?.includes(name)), target]
|
|
2163
2734
|
);
|
|
2164
2735
|
}
|
|
2165
2736
|
}, [
|
|
@@ -2172,13 +2743,18 @@ const FormEngineImpl = ({
|
|
|
2172
2743
|
dependencyLockedNames,
|
|
2173
2744
|
getOptionBucket,
|
|
2174
2745
|
expandMode,
|
|
2746
|
+
initialExpandedOptions,
|
|
2175
2747
|
]);
|
|
2176
2748
|
|
|
2177
2749
|
// Read-first completion summary (how many shown options have a value set),
|
|
2178
2750
|
// surfaced as a progress meter at the top of the compact form.
|
|
2179
2751
|
const readFirstCompletion = useMemo(
|
|
2180
|
-
() =>
|
|
2181
|
-
|
|
2752
|
+
() =>
|
|
2753
|
+
getReadFirstCompletion(
|
|
2754
|
+
availableOptions as Record<string, IQorusFormField | undefined>,
|
|
2755
|
+
(name) => getOptionBucket(name) === 'attention'
|
|
2756
|
+
),
|
|
2757
|
+
[JSON.stringify(availableOptions), getOptionBucket]
|
|
2182
2758
|
);
|
|
2183
2759
|
|
|
2184
2760
|
const optionalFields = useMemo(
|
|
@@ -2272,13 +2848,59 @@ const FormEngineImpl = ({
|
|
|
2272
2848
|
// schema has not arrived yet (`type` undefined) instead of crashing.
|
|
2273
2849
|
const optionSchema = options?.[optionName];
|
|
2274
2850
|
const schemaUiType = optionSchema?.ui_type as TQorusType;
|
|
2275
|
-
const uiTypeIsAnyLike = schemaUiType
|
|
2851
|
+
const uiTypeIsAnyLike = isUntypedOptionType(schemaUiType);
|
|
2852
|
+
/* An EXPRESSION's stored type is skipped in this chain.
|
|
2853
|
+
|
|
2854
|
+
The value is `{ is_expression: true, value: {...} }`, so the type saved
|
|
2855
|
+
beside it describes that envelope — `hash` — not what the field holds.
|
|
2856
|
+
With no `ui_type` to win first, that `hash` beat the schema's `auto`
|
|
2857
|
+
and was handed to the expression editor as the return type to check
|
|
2858
|
+
against: `1 + 2` on a field declared `auto` came back "This does not
|
|
2859
|
+
fit. The expression returns int, and this field holds hash". A literal
|
|
2860
|
+
`1` was fine, because a literal is stored as a plain value whose type
|
|
2861
|
+
matches.
|
|
2862
|
+
|
|
2863
|
+
Dropping the stored type here lets the schema decide, which is the
|
|
2864
|
+
only party that knows what the field accepts. This is the row
|
|
2865
|
+
renderer's OWN resolution and does not go through
|
|
2866
|
+
`getOptionFieldStorageType`, so guarding that one alone left this
|
|
2867
|
+
untouched. */
|
|
2868
|
+
const storedRowType = optionHoldsExpression(other) ? undefined : type;
|
|
2869
|
+
|
|
2870
|
+
/* A RELOADED expression is handed to the editor in the shape the editor
|
|
2871
|
+
was written against.
|
|
2872
|
+
|
|
2873
|
+
An expression is written as `{ type, is_expression: true, value: ast }`
|
|
2874
|
+
and saved as `{ type, value: { is_expression: true, value: ast } }` —
|
|
2875
|
+
the flag moves ONTO the value. The editor was only ever given
|
|
2876
|
+
`other.is_expression`, which is absent in the saved shape, so a
|
|
2877
|
+
reloaded expression was not recognised as one: an `auto` field then
|
|
2878
|
+
resolved its type from the value it could see, decided the envelope was
|
|
2879
|
+
a `hash`, and rendered `1 + 2` as a raw tree of `is_expression` /
|
|
2880
|
+
`exp` / `args` for the author to edit by hand.
|
|
2881
|
+
|
|
2882
|
+
Both halves have to move together. Flipping the flag alone would put
|
|
2883
|
+
the editor into expression mode and then hand it the ENVELOPE where it
|
|
2884
|
+
expects the expression, so the value is unwrapped here too — after
|
|
2885
|
+
which a saved expression is indistinguishable from one just typed,
|
|
2886
|
+
which is what every component downstream already assumes.
|
|
2887
|
+
|
|
2888
|
+
Reported as "after Run assertion the value shows as raw data", but the
|
|
2889
|
+
run is incidental: the field beside it, which no run touched, was
|
|
2890
|
+
equally raw. The trigger is a RELOAD, which is the only thing that puts
|
|
2891
|
+
an expression into its saved shape. */
|
|
2892
|
+
const nestedExpression = !!(other as { value?: { is_expression?: unknown } })?.value
|
|
2893
|
+
?.is_expression;
|
|
2894
|
+
const expressionAwareValue =
|
|
2895
|
+
nestedExpression ?
|
|
2896
|
+
(other as unknown as { value: { value: unknown } }).value.value
|
|
2897
|
+
: other.value;
|
|
2276
2898
|
const resolvedType =
|
|
2277
2899
|
(isFixedCompactAllowedValueOption(optionSchema) ?
|
|
2278
2900
|
getOptionSchemaStorageType(optionSchema, isRendererOnly)
|
|
2279
2901
|
: schemaUiType && !uiTypeIsAnyLike ? schemaUiType
|
|
2280
2902
|
: undefined) ||
|
|
2281
|
-
|
|
2903
|
+
storedRowType ||
|
|
2282
2904
|
schemaUiType ||
|
|
2283
2905
|
(optionSchema?.type as TQorusType) ||
|
|
2284
2906
|
'any';
|
|
@@ -2411,20 +3033,23 @@ const FormEngineImpl = ({
|
|
|
2411
3033
|
// `inherit_props: { language: 'language' }`, the list renderer
|
|
2412
3034
|
// forwards it into each row, and the row's body resolves against
|
|
2413
3035
|
// the accumulated bag.
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
3036
|
+
//
|
|
3037
|
+
// Read from the memoised map rather than built here: this is a
|
|
3038
|
+
// PROP, and one built inline is a new object on every render of
|
|
3039
|
+
// the form, which re-renders every field that receives it. The
|
|
3040
|
+
// `??` chain answers for a field the schema does not describe —
|
|
3041
|
+
// `inheritedScopeFor` returns the SAME references, so an
|
|
3042
|
+
// undescribed field is as stable as a described one.
|
|
3043
|
+
inheritedFromParent={
|
|
3044
|
+
inheritedScopes.get(optionName) ?? inheritedFromParent ?? NO_INHERITED_SCOPE
|
|
3045
|
+
}
|
|
2422
3046
|
// Propagate compact so an arg_schema field renders a COMPACT sub-form
|
|
2423
3047
|
// (consistent with the parent) rather than the classic FormEngine.
|
|
2424
3048
|
compact={compact}
|
|
2425
3049
|
// SEAM: forwarded through TemplateField's rest-spread to AutoFormField,
|
|
2426
3050
|
// which renders consumer-injected editors by field type/ui_type.
|
|
2427
3051
|
componentOverrides={componentOverrides}
|
|
3052
|
+
templateAwareUiTypes={templateAwareUiTypes}
|
|
2428
3053
|
allowTemplates={!!(allowTemplates && options?.[optionName]?.supports_templates)}
|
|
2429
3054
|
allowFunctions={!!options?.[optionName]?.supports_expressions}
|
|
2430
3055
|
// reqraft: form-level expression fields get the Visual/Text shell
|
|
@@ -2441,7 +3066,25 @@ const FormEngineImpl = ({
|
|
|
2441
3066
|
isFixedCompactAllowedValueOption(optionSchema) ||
|
|
2442
3067
|
(options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any')
|
|
2443
3068
|
}
|
|
2444
|
-
|
|
3069
|
+
// A field may declare a template list OF ITS OWN, and when it does
|
|
3070
|
+
// that list is the one to offer. The engine-wide `stringTemplates`
|
|
3071
|
+
// are the form's shared vocabulary - config items, system properties
|
|
3072
|
+
// - and every field draws on the same set. A per-field list answers a
|
|
3073
|
+
// question only that field asks: a test assertion's `$.` references
|
|
3074
|
+
// are the values ITS case captured, and no other field in the form
|
|
3075
|
+
// should be offering them.
|
|
3076
|
+
//
|
|
3077
|
+
// Passing the engine's list unconditionally overwrote the schema's,
|
|
3078
|
+
// so a field that declared templates had none by the time it
|
|
3079
|
+
// rendered. On a typed field that only cost the picker; on an
|
|
3080
|
+
// ANY-LIKE one it changed the question, because `TemplateField`
|
|
3081
|
+
// opens on the template selector only when there is something to
|
|
3082
|
+
// pick and otherwise falls back to the type picker. An untyped field
|
|
3083
|
+
// therefore asked the author to choose `string`/`int`/`hash` before
|
|
3084
|
+
// it would let them name a value they had already captured.
|
|
3085
|
+
templates={
|
|
3086
|
+
(optionSchema as TFieldWithOwnTemplates | undefined)?.templates ?? templates.value
|
|
3087
|
+
}
|
|
2445
3088
|
{...getTypeAndCanBeNull(
|
|
2446
3089
|
// The RENDERER type picks the editor — the storage type lives on
|
|
2447
3090
|
// the value envelope. Passing storage here rendered a `richtext`
|
|
@@ -2462,8 +3105,8 @@ const FormEngineImpl = ({
|
|
|
2462
3105
|
key={optionName}
|
|
2463
3106
|
arg_schema={options?.[optionName]?.arg_schema}
|
|
2464
3107
|
noSoft={!!rest?.options}
|
|
2465
|
-
value={
|
|
2466
|
-
isFunction={(other
|
|
3108
|
+
value={expressionAwareValue}
|
|
3109
|
+
isFunction={optionHoldsExpression(other)}
|
|
2467
3110
|
isDefaultFunction={options?.[optionName]?.default_view === 'expression'}
|
|
2468
3111
|
sensitive={options?.[optionName]?.sensitive}
|
|
2469
3112
|
default_value={getDefaultValue(options?.[optionName])}
|
|
@@ -2487,7 +3130,7 @@ const FormEngineImpl = ({
|
|
|
2487
3130
|
readOnly={readOnly}
|
|
2488
3131
|
size={editorSize || rest.size}
|
|
2489
3132
|
menuItems={
|
|
2490
|
-
(options?.[optionName]
|
|
3133
|
+
offersTypeChoices(options?.[optionName], anyLikeUiTypes) ?
|
|
2491
3134
|
getCustomMenuTemplateItems(optionName)
|
|
2492
3135
|
: undefined
|
|
2493
3136
|
}
|
|
@@ -2500,6 +3143,7 @@ const FormEngineImpl = ({
|
|
|
2500
3143
|
name={optionName}
|
|
2501
3144
|
option={{ type: resolvedType, ...other }}
|
|
2502
3145
|
getType={getTypeForOption}
|
|
3146
|
+
untouched={!touchedOptionsRef.current.has(optionName)}
|
|
2503
3147
|
/>
|
|
2504
3148
|
{operators && size(operators) && size(other.op) ?
|
|
2505
3149
|
<>
|
|
@@ -2549,6 +3193,8 @@ const FormEngineImpl = ({
|
|
|
2549
3193
|
handleRemoveOperator,
|
|
2550
3194
|
getCustomMenuTemplateItems,
|
|
2551
3195
|
getTypeForOption,
|
|
3196
|
+
inheritedScopes,
|
|
3197
|
+
inheritedFromParent,
|
|
2552
3198
|
]
|
|
2553
3199
|
);
|
|
2554
3200
|
|
|
@@ -2581,6 +3227,7 @@ const FormEngineImpl = ({
|
|
|
2581
3227
|
() => ({
|
|
2582
3228
|
templates: templates.value,
|
|
2583
3229
|
readOnly,
|
|
3230
|
+
onReadOnlyActivate,
|
|
2584
3231
|
commitMode,
|
|
2585
3232
|
expandMode,
|
|
2586
3233
|
options,
|
|
@@ -2590,7 +3237,6 @@ const FormEngineImpl = ({
|
|
|
2590
3237
|
codePreviewRenderer,
|
|
2591
3238
|
showAllDescriptions,
|
|
2592
3239
|
expandedOptions,
|
|
2593
|
-
autoFocusNameRef,
|
|
2594
3240
|
highlightedOptions,
|
|
2595
3241
|
flashedOptions,
|
|
2596
3242
|
infoPanelOverrides,
|
|
@@ -2638,7 +3284,6 @@ const FormEngineImpl = ({
|
|
|
2638
3284
|
codePreviewRenderer,
|
|
2639
3285
|
showAllDescriptions,
|
|
2640
3286
|
expandedOptions,
|
|
2641
|
-
autoFocusNameRef,
|
|
2642
3287
|
highlightedOptions,
|
|
2643
3288
|
flashedOptions,
|
|
2644
3289
|
infoPanelOverrides,
|
|
@@ -2698,7 +3343,21 @@ const FormEngineImpl = ({
|
|
|
2698
3343
|
fields: false,
|
|
2699
3344
|
help: false,
|
|
2700
3345
|
},
|
|
2701
|
-
|
|
3346
|
+
/* From the SCHEMA, not from the value.
|
|
3347
|
+
*
|
|
3348
|
+
* `availableOptions` is keyed off the form's VALUE, so it grows as data
|
|
3349
|
+
* arrives — and the search row is rendered only when this is true
|
|
3350
|
+
* (`CompactToolbar`). A form whose value lands after its first render
|
|
3351
|
+
* therefore ADDED a 38px row to its header a beat later, and everything
|
|
3352
|
+
* below it moved. Measured on the Qorus IDE's test editor, on the nested
|
|
3353
|
+
* sub-form of a case: 33 elements jumped down 51px in one frame, on every
|
|
3354
|
+
* cold load.
|
|
3355
|
+
*
|
|
3356
|
+
* How many fields a form has is a property of its schema. The two agree
|
|
3357
|
+
* once the value has arrived — `fixOptions` materialises an entry per
|
|
3358
|
+
* declared field — so this only changes the answer during the load, which
|
|
3359
|
+
* is the whole point. */
|
|
3360
|
+
hasMultipleOptions: size(options) > 1,
|
|
2702
3361
|
compactQuery,
|
|
2703
3362
|
setCompactQuery,
|
|
2704
3363
|
requiredOnly,
|
|
@@ -2719,6 +3378,7 @@ const FormEngineImpl = ({
|
|
|
2719
3378
|
}),
|
|
2720
3379
|
[
|
|
2721
3380
|
readOnly,
|
|
3381
|
+
onReadOnlyActivate,
|
|
2722
3382
|
validityData,
|
|
2723
3383
|
readFirstAttentionCount,
|
|
2724
3384
|
readFirstCompletion,
|
|
@@ -2949,6 +3609,13 @@ const FormEngineImpl = ({
|
|
|
2949
3609
|
const bucketCount = (b: TFormEngineBoxKey) =>
|
|
2950
3610
|
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
2951
3611
|
|
|
3612
|
+
/* The default is to collapse the Optional box, and an explicit pass is
|
|
3613
|
+
what waives the "never collapse the sole box" escape below. Resolving it
|
|
3614
|
+
here rather than in the parameter list is what makes the two
|
|
3615
|
+
distinguishable — `['optional']` as a default and `['optional']` from a
|
|
3616
|
+
caller mean different things. */
|
|
3617
|
+
const compactCollapsedGroups = compactCollapsedGroupsProp ?? ['optional'];
|
|
3618
|
+
const collapsedGroupsRequested = compactCollapsedGroupsProp !== undefined;
|
|
2952
3619
|
// "Is the Optional box the whole form?" — when nothing needs attention and
|
|
2953
3620
|
// nothing is set, collapsing it leaves a card with no visible content at all.
|
|
2954
3621
|
const onlyOptionalRows =
|
|
@@ -2985,15 +3652,28 @@ const FormEngineImpl = ({
|
|
|
2985
3652
|
const boxHasPreselectedRows = (boxKey: TFormEngineBoxKey) =>
|
|
2986
3653
|
boxKey === 'optional' &&
|
|
2987
3654
|
bucketGroups[boxKey].some((groupName) => groupHasPreselectedRow(boxKey, groupName));
|
|
3655
|
+
/* The pin, and where it lands. A nested sub-form sits inside the parent's
|
|
3656
|
+
card and owns no scroll context, so it never pins. */
|
|
3657
|
+
const stickyBoxHeaders = compactStickyBoxHeaders && !compactNested;
|
|
3658
|
+
const boxStickyOffset = compactStickyOffset + compactToolbarHeight;
|
|
2988
3659
|
const STATUS_BOXES: Array<{
|
|
2989
3660
|
key: TFormEngineBoxKey;
|
|
2990
3661
|
label: string;
|
|
2991
3662
|
intent?: 'warning' | 'success';
|
|
2992
3663
|
icon: IReqoreIconName;
|
|
2993
3664
|
}> = [
|
|
2994
|
-
{
|
|
2995
|
-
|
|
2996
|
-
|
|
3665
|
+
{
|
|
3666
|
+
key: 'attention',
|
|
3667
|
+
label: compactBoxLabels?.attention ?? 'Needs attention',
|
|
3668
|
+
intent: 'warning',
|
|
3669
|
+
icon: 'ErrorWarningLine',
|
|
3670
|
+
},
|
|
3671
|
+
{ key: 'set', label: compactBoxLabels?.set ?? 'Set', intent: 'success', icon: 'CheckLine' },
|
|
3672
|
+
{
|
|
3673
|
+
key: 'optional',
|
|
3674
|
+
label: compactBoxLabels?.optional ?? 'Optional',
|
|
3675
|
+
icon: 'CheckboxBlankCircleLine',
|
|
3676
|
+
},
|
|
2997
3677
|
];
|
|
2998
3678
|
|
|
2999
3679
|
// Build the rows for one group: contiguous required-group members are pulled
|
|
@@ -3137,7 +3817,7 @@ const FormEngineImpl = ({
|
|
|
3137
3817
|
// Mirrors the toolbar's own search-row gate: when no search
|
|
3138
3818
|
// row renders, the header is a thin strip and sits tight to
|
|
3139
3819
|
// the first status box (see StyledCompactPanel).
|
|
3140
|
-
$tightHeader={!(compactToolbarParts?.search && size(
|
|
3820
|
+
$tightHeader={!(compactToolbarParts?.search && size(options) > 1)}
|
|
3141
3821
|
// The top-level form scrolls, so its toolbar STICKS and carries a
|
|
3142
3822
|
// dark blurred backdrop so content ghosts cleanly beneath it. A
|
|
3143
3823
|
// nested (arg_schema) sub-form owns no scroll context — drop the
|
|
@@ -3145,6 +3825,9 @@ const FormEngineImpl = ({
|
|
|
3145
3825
|
// transparent inside the parent's card.
|
|
3146
3826
|
$headerBg={compactNested ? 'transparent' : headerBg}
|
|
3147
3827
|
$nested={compactNested}
|
|
3828
|
+
// Keeps the wrapper from clipping, which is what a pinned box
|
|
3829
|
+
// header inside it needs — see StyledCompactPanel.
|
|
3830
|
+
$stickyBoxes={compactStickyBoxHeaders && !compactNested}
|
|
3148
3831
|
flat
|
|
3149
3832
|
raised
|
|
3150
3833
|
minimal
|
|
@@ -3153,7 +3836,30 @@ const FormEngineImpl = ({
|
|
|
3153
3836
|
// parent's edit card) instead of stacking its own dark surface.
|
|
3154
3837
|
// The status boxes keep their own tints; the sticky toolbar keeps
|
|
3155
3838
|
// its blurred header via the $headerBg override.
|
|
3156
|
-
|
|
3839
|
+
// NARROW: the toolbar gives up its pin so the box headers
|
|
3840
|
+
// can keep theirs.
|
|
3841
|
+
//
|
|
3842
|
+
// Measured at 380px, a pinned toolbar is 124px and a pinned
|
|
3843
|
+
// box header is 48 — together a third of a phone's readable
|
|
3844
|
+
// body, before the form has drawn a line. Only one of them
|
|
3845
|
+
// can be afforded there, and the box header is the better
|
|
3846
|
+
// buy: the toolbar is chrome you GO to (a search, a field
|
|
3847
|
+
// picker, a meter — all of it still one scroll away at the
|
|
3848
|
+
// top of the form), while the box header is chrome you READ,
|
|
3849
|
+
// and it is the only thing that says which bucket the rows
|
|
3850
|
+
// under the pointer are in.
|
|
3851
|
+
//
|
|
3852
|
+
// Same shape of decision the engine already makes one level
|
|
3853
|
+
// down, where `optionActionsCollapse: 'auto'` folds a row's
|
|
3854
|
+
// actions into a menu because a narrow row has no width for
|
|
3855
|
+
// a button strip.
|
|
3856
|
+
//
|
|
3857
|
+
// A consumer can still force either way: `compactPanelProps`
|
|
3858
|
+
// spreads after this.
|
|
3859
|
+
stickyHeader={!compactNested && !(compactNarrow && compactStickyBoxHeaders)}
|
|
3860
|
+
// Below the host's own pinned chrome, if it has any — see
|
|
3861
|
+
// `compactStickyOffset`.
|
|
3862
|
+
stickyHeaderOffset={compactStickyOffset || undefined}
|
|
3157
3863
|
// Consumer overrides land last so they win over every default
|
|
3158
3864
|
// above; `actions` and `contentStyle` below merge rather than
|
|
3159
3865
|
// replace (see `compactPanelProps`).
|
|
@@ -3163,6 +3869,26 @@ const FormEngineImpl = ({
|
|
|
3163
3869
|
display: 'flex',
|
|
3164
3870
|
flexFlow: 'column',
|
|
3165
3871
|
gap: '10px',
|
|
3872
|
+
/* The panel body must not be a SCROLL CONTAINER.
|
|
3873
|
+
|
|
3874
|
+
`StyledPanelContent` ships `overflow: auto` so a
|
|
3875
|
+
height-capped panel can scroll its body. This one is never
|
|
3876
|
+
capped — in `compactScroll: 'host'` the page scrolls and in
|
|
3877
|
+
'own' the wrap above does — so the `auto` never engages and
|
|
3878
|
+
reads as inert. It is not: an `overflow: auto` box is a
|
|
3879
|
+
scrollport whether or not it scrolls, and every
|
|
3880
|
+
`position: sticky` inside resolves against IT. The status
|
|
3881
|
+
box headers (and the toolbar, in a nested host) would pin
|
|
3882
|
+
to a box that never moves, which looks exactly like sticky
|
|
3883
|
+
being ignored.
|
|
3884
|
+
|
|
3885
|
+
`overflow-x: clip`, NOT `hidden`, for the reason spelled
|
|
3886
|
+
out on `StyledCompactWrap`: CSS coerces an `overflow-y:
|
|
3887
|
+
visible` to `auto` unless the other axis is visible or
|
|
3888
|
+
clip, which would put the scrollport straight back. `clip`
|
|
3889
|
+
keeps the horizontal guard the row ellipsis relies on. */
|
|
3890
|
+
overflowY: 'visible',
|
|
3891
|
+
overflowX: 'clip',
|
|
3166
3892
|
...compactPanelProps?.contentStyle,
|
|
3167
3893
|
}}
|
|
3168
3894
|
>
|
|
@@ -3205,7 +3931,7 @@ const FormEngineImpl = ({
|
|
|
3205
3931
|
const wouldCollapse =
|
|
3206
3932
|
compactCollapsedGroups.includes(box.key) &&
|
|
3207
3933
|
!query &&
|
|
3208
|
-
!(box.key === 'optional' && onlyOptionalRows);
|
|
3934
|
+
!(box.key === 'optional' && onlyOptionalRows && !collapsedGroupsRequested);
|
|
3209
3935
|
// Only a box that was opened SOLELY to reveal a preselected
|
|
3210
3936
|
// row splits its body; a box that renders open anyway shows
|
|
3211
3937
|
// all of its groups as before.
|
|
@@ -3265,6 +3991,10 @@ const FormEngineImpl = ({
|
|
|
3265
3991
|
<StyledStatusBox
|
|
3266
3992
|
$accent={accent}
|
|
3267
3993
|
$bg={boxBg}
|
|
3994
|
+
// Only a box that PINS needs an opaque header (see
|
|
3995
|
+
// StyledStatusBox); passing it unconditionally would
|
|
3996
|
+
// repaint every form's boxes for nothing.
|
|
3997
|
+
$surface={stickyBoxHeaders ? getMainBackgroundColor(theme) : undefined}
|
|
3268
3998
|
key={box.key}
|
|
3269
3999
|
ref={(node: HTMLDivElement | null) => {
|
|
3270
4000
|
statusBoxRefs.current[box.key] = node;
|
|
@@ -3276,6 +4006,12 @@ const FormEngineImpl = ({
|
|
|
3276
4006
|
}
|
|
3277
4007
|
flat
|
|
3278
4008
|
minimal
|
|
4009
|
+
/* The header stays on screen while its own box is
|
|
4010
|
+
being read — see `compactStickyBoxHeaders`. Sticky
|
|
4011
|
+
is bounded by its containing block, so the header
|
|
4012
|
+
leaves with its box and only one is ever pinned. */
|
|
4013
|
+
stickyHeader={stickyBoxHeaders}
|
|
4014
|
+
stickyHeaderOffset={stickyBoxHeaders ? boxStickyOffset : undefined}
|
|
3279
4015
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
3280
4016
|
collapsible
|
|
3281
4017
|
// Which boxes start closed is the consumer's call
|
|
@@ -3446,37 +4182,25 @@ const FormEngineImpl = ({
|
|
|
3446
4182
|
);
|
|
3447
4183
|
};
|
|
3448
4184
|
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
<ReqoreControlGroup fixed fill={false}>
|
|
3469
|
-
<ReqoreSkeleton />
|
|
3470
|
-
<ReqoreSkeleton />
|
|
3471
|
-
<ReqoreSkeleton width='100%' />
|
|
3472
|
-
</ReqoreControlGroup>
|
|
3473
|
-
<ReqoreControlGroup vertical fill={false}>
|
|
3474
|
-
<ReqoreSkeleton width='100%' height='150px' />
|
|
3475
|
-
<ReqoreSkeleton width='100%' height='150px' />
|
|
3476
|
-
<ReqoreSkeleton width='100%' height='150px' />
|
|
3477
|
-
</ReqoreControlGroup>
|
|
3478
|
-
</ReqoreControlGroup>
|
|
3479
|
-
);
|
|
4185
|
+
/* Which of the seven waits is holding the form, in the order they are
|
|
4186
|
+
tested. Rendered as `data-wait` on the placeholder so a page can be asked
|
|
4187
|
+
what it is waiting FOR instead of only that it is waiting. */
|
|
4188
|
+
const waitReason =
|
|
4189
|
+
rest.skeleton ? (rest.skeletonReason ? `host:${rest.skeletonReason}` : 'host')
|
|
4190
|
+
: templates.loading ? 'templates'
|
|
4191
|
+
: typesLoading ? 'types'
|
|
4192
|
+
: optionsLoading ? 'options'
|
|
4193
|
+
: loading ? 'fetch'
|
|
4194
|
+
: operatorsUrl && !operators ? 'operators'
|
|
4195
|
+
: (url || customUrl) && !options ? 'schema'
|
|
4196
|
+
: undefined;
|
|
4197
|
+
|
|
4198
|
+
if (waitReason) {
|
|
4199
|
+
/* The shape of the FORM, not a generic block arrangement. This used to be
|
|
4200
|
+
three bars over three big panels, which resembles nothing this gate is
|
|
4201
|
+
waiting for and shared no vocabulary with the other waits on the same
|
|
4202
|
+
page — see `FormFieldsSkeleton`. */
|
|
4203
|
+
return <FormFieldsSkeleton fill className='options-loading-skeleton' reason={waitReason} />;
|
|
3480
4204
|
}
|
|
3481
4205
|
|
|
3482
4206
|
// A loader that rejected (and produced no usable schema) surfaces its error
|
|
@@ -3643,91 +4367,70 @@ const FormEngineImpl = ({
|
|
|
3643
4367
|
STRECHABLE_TYPES.has(options[optionName].type as TQorusType) ||
|
|
3644
4368
|
(options[optionName] as { stretch?: boolean }).stretch,
|
|
3645
4369
|
size: 'small',
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
4370
|
+
/* No floating bar. It is portalled over whatever sits above the
|
|
4371
|
+
row — on the first row, the form's own header buttons, which it
|
|
4372
|
+
made unclickable — and a bar that only appears on a hover is out
|
|
4373
|
+
of reach on a phone. The row's actions render in its own
|
|
4374
|
+
header: injected ones as buttons (all of them in the menu on
|
|
4375
|
+
touch or a narrow viewport), the row's own secondary ones in
|
|
4376
|
+
its ⋯ menu. */
|
|
4377
|
+
actions: optionRowActions({
|
|
4378
|
+
injected: resolveOptionActions(optionActions, {
|
|
3654
4379
|
name: optionName,
|
|
3655
4380
|
schema: options[optionName],
|
|
3656
4381
|
value: availableOptions?.[optionName] as TOption,
|
|
3657
|
-
}),
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
(
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
(
|
|
3681
|
-
!readOnly &&
|
|
3682
|
-
(other as any).value &&
|
|
3683
|
-
!isEqual((other as any).value, getDefaultValue(options[optionName])) &&
|
|
3684
|
-
!(options[optionName]?.disabled || (options[optionName] as any)?.readonly)
|
|
3685
|
-
) ?
|
|
3686
|
-
'hover'
|
|
3687
|
-
: false,
|
|
3688
|
-
onClick: () => handleValueChange(optionName, undefined),
|
|
3689
|
-
},
|
|
3690
|
-
{
|
|
3691
|
-
size: 'tiny',
|
|
3692
|
-
icon: 'HistoryLine',
|
|
3693
|
-
className: 'options-item-revert',
|
|
3694
|
-
tooltip: 'Revert Changes',
|
|
3695
|
-
show:
|
|
3696
|
-
!readOnly && hasOptionChanged((other as any).value, optionName) ?
|
|
3697
|
-
'hover'
|
|
3698
|
-
: false,
|
|
3699
|
-
onClick: () => {
|
|
3700
|
-
handleValueChange(
|
|
3701
|
-
optionName,
|
|
3702
|
-
originalValue.current?.[optionName]?.value,
|
|
3703
|
-
originalValue.current?.[optionName]?.type
|
|
3704
|
-
);
|
|
3705
|
-
},
|
|
3706
|
-
},
|
|
3707
|
-
{
|
|
3708
|
-
size: 'tiny',
|
|
3709
|
-
icon: 'DeleteBinLine',
|
|
3710
|
-
intent: 'danger',
|
|
3711
|
-
minimal: true,
|
|
3712
|
-
className: 'options-optional-remove',
|
|
3713
|
-
show:
|
|
3714
|
-
(
|
|
3715
|
-
!readOnly &&
|
|
3716
|
-
!options[optionName]?.preselected &&
|
|
3717
|
-
!options[optionName]?.required
|
|
3718
|
-
) ?
|
|
3719
|
-
'hover'
|
|
3720
|
-
: false,
|
|
3721
|
-
onClick: () => {
|
|
3722
|
-
confirmAction({
|
|
3723
|
-
title: 'Remove Selected Option',
|
|
3724
|
-
onConfirm: () => {
|
|
3725
|
-
removeSelectedOption(optionName);
|
|
4382
|
+
}).filter((action) => action.show !== false),
|
|
4383
|
+
collapse: collapseOptionActions,
|
|
4384
|
+
menu: [
|
|
4385
|
+
!readOnly &&
|
|
4386
|
+
type !== 'code-editor' &&
|
|
4387
|
+
((options[optionName] as any)?.ui_type || options[optionName]?.type) !==
|
|
4388
|
+
'code-editor' && {
|
|
4389
|
+
label: 'Focused editing',
|
|
4390
|
+
icon: 'FullscreenLine',
|
|
4391
|
+
className: 'options-item-fullscreen',
|
|
4392
|
+
onClick: () => setFocusedEditing(optionName),
|
|
4393
|
+
},
|
|
4394
|
+
!readOnly &&
|
|
4395
|
+
hasOptionChanged((other as any).value, optionName) && {
|
|
4396
|
+
label: 'Revert changes',
|
|
4397
|
+
icon: 'HistoryLine',
|
|
4398
|
+
className: 'options-item-revert',
|
|
4399
|
+
onClick: () => {
|
|
4400
|
+
handleValueChange(
|
|
4401
|
+
optionName,
|
|
4402
|
+
originalValue.current?.[optionName]?.value,
|
|
4403
|
+
originalValue.current?.[optionName]?.type
|
|
4404
|
+
);
|
|
3726
4405
|
},
|
|
3727
|
-
}
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
4406
|
+
},
|
|
4407
|
+
!readOnly &&
|
|
4408
|
+
(other as any).value &&
|
|
4409
|
+
!isEqual((other as any).value, getDefaultValue(options[optionName])) &&
|
|
4410
|
+
!(options[optionName]?.disabled || (options[optionName] as any)?.readonly) && {
|
|
4411
|
+
label: 'Remove value',
|
|
4412
|
+
icon: 'CloseLine',
|
|
4413
|
+
className: 'options-item-remove',
|
|
4414
|
+
onClick: () => handleValueChange(optionName, undefined),
|
|
4415
|
+
},
|
|
4416
|
+
!readOnly &&
|
|
4417
|
+
!options[optionName]?.preselected &&
|
|
4418
|
+
!options[optionName]?.required && {
|
|
4419
|
+
label: 'Remove option',
|
|
4420
|
+
icon: 'DeleteBinLine',
|
|
4421
|
+
intent: 'danger',
|
|
4422
|
+
className: 'options-optional-remove',
|
|
4423
|
+
onClick: () => {
|
|
4424
|
+
confirmAction({
|
|
4425
|
+
title: 'Remove Selected Option',
|
|
4426
|
+
onConfirm: () => {
|
|
4427
|
+
removeSelectedOption(optionName);
|
|
4428
|
+
},
|
|
4429
|
+
});
|
|
4430
|
+
},
|
|
4431
|
+
},
|
|
4432
|
+
],
|
|
4433
|
+
}),
|
|
3731
4434
|
content: (
|
|
3732
4435
|
<FocusedEditing
|
|
3733
4436
|
isFullscreen={focusedEditing === optionName}
|