@qoretechnologies/reqraft 0.10.55-pr.124.g77dd26c → 0.10.55
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 +273 -22
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +669 -192
- 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 +17 -2
- 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 +21 -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/number/Number.d.ts +12 -1
- package/dist/components/form/fields/number/Number.d.ts.map +1 -1
- package/dist/components/form/fields/number/Number.js +6 -3
- package/dist/components/form/fields/number/Number.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 +35 -8
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +162 -25
- 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 +10 -6
- 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 +1269 -211
- package/src/components/form/engine/FormEngine.tsx +922 -270
- 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 +17 -2
- 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 +102 -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/number/Number.tsx +25 -0
- 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 +203 -51
- 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,
|
|
@@ -97,15 +102,17 @@ import {
|
|
|
97
102
|
TCompactSort,
|
|
98
103
|
} from './compactToolbarContext';
|
|
99
104
|
import {
|
|
100
|
-
IConditionalFieldMessage,
|
|
101
105
|
OptionFieldMessages,
|
|
106
|
+
getAllowedValueAvailability,
|
|
102
107
|
getShownSchemaMessages,
|
|
103
108
|
} from './OptionFieldMessages';
|
|
109
|
+
import { OptionsContext } from './optionsContext';
|
|
104
110
|
import {
|
|
105
111
|
MarkdownRendererContext,
|
|
106
112
|
TMarkdownRenderer,
|
|
107
113
|
} from '../../Description/markdownRendererContext';
|
|
108
114
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
115
|
+
import { firstDeclaredType, isUntypedOptionType } from '../../../helpers/optionUiTypes';
|
|
109
116
|
import {
|
|
110
117
|
TReadFirstStatus,
|
|
111
118
|
findAllowedValueOption,
|
|
@@ -149,10 +156,26 @@ export type TOperatorValue = TQorusFormOperatorValue;
|
|
|
149
156
|
* either, the message is static and always shown, which is what every existing
|
|
150
157
|
* consumer gets.
|
|
151
158
|
*/
|
|
152
|
-
export
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
export type IOptionFieldMessage = IQorusFormFieldMessage;
|
|
160
|
+
/** `T` without `K`, member by member — `Omit` on a union collapses the union. */
|
|
161
|
+
type TDistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
162
|
+
/**
|
|
163
|
+
* One field of a form's schema: the shared descriptor, plus what this engine
|
|
164
|
+
* reads that the shared type does not carry — conditional `messages`, and
|
|
165
|
+
* `absorb_fields`. Declared, so a descriptor using them is checked rather than
|
|
166
|
+
* cast; still assignable wherever the shared descriptor is expected.
|
|
167
|
+
*/
|
|
168
|
+
export type IOptionsSchemaArg = TDistributiveOmit<TQorusFormFieldSchema, 'messages'> & {
|
|
169
|
+
messages?: IOptionFieldMessage[];
|
|
170
|
+
/**
|
|
171
|
+
* Siblings this field renders inside its own editor instead of on rows of
|
|
172
|
+
* their own — a code editor and its language, say.
|
|
173
|
+
*/
|
|
174
|
+
absorb_fields?: string[];
|
|
175
|
+
};
|
|
176
|
+
export interface IOptionsSchema {
|
|
177
|
+
[optionName: string]: IOptionsSchemaArg;
|
|
178
|
+
}
|
|
156
179
|
export interface IOperator extends IQorusFormOperator {}
|
|
157
180
|
export interface IOperatorsSchema extends IQorusFormOperatorsSchema {}
|
|
158
181
|
export interface IOptionsOnChangeMeta extends IQorusFormFieldOnChangeMeta {
|
|
@@ -214,6 +237,48 @@ const resolveInheritProps = (
|
|
|
214
237
|
return out;
|
|
215
238
|
};
|
|
216
239
|
|
|
240
|
+
/** One shared "nothing came down from above" bag. See `inheritedScopeFor`. */
|
|
241
|
+
const NO_INHERITED_SCOPE: Record<string, unknown> = Object.freeze({});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The inheritance bag ONE field forwards to any `arg_schema` sub-form it hosts:
|
|
245
|
+
* whatever the ancestor chain passed down, plus whatever this field's own
|
|
246
|
+
* `inherit_props` resolves to.
|
|
247
|
+
*
|
|
248
|
+
* Written as a function with a stable answer because it is a PROP, and a prop
|
|
249
|
+
* that is a new object on every render re-renders the field that receives it
|
|
250
|
+
* every time this form renders — for most fields, forever, over a value that
|
|
251
|
+
* never changed. It showed up in the console as `Template field kind Updated
|
|
252
|
+
* because: {inheritedFromParent}` repeating on a form nobody was touching:
|
|
253
|
+
* `kind` declares no `inherit_props` at all, so the bag it was handed was
|
|
254
|
+
* `{ ...(undefined ?? {}) }` — a fresh empty object each time, unequal to the
|
|
255
|
+
* last one, and `TemplateField` is memoised precisely so it does not re-render
|
|
256
|
+
* on a prop that did not change.
|
|
257
|
+
*
|
|
258
|
+
* So a field with nothing of its own to add forwards the bag it was HANDED,
|
|
259
|
+
* unchanged and unwrapped, and a form with no inherited scope at all forwards
|
|
260
|
+
* one shared frozen object rather than minting one per field per render.
|
|
261
|
+
*
|
|
262
|
+
* Mirrored in qorus-ide's `systemOptions.tsx` (which grew this treatment
|
|
263
|
+
* first); per the CLAUDE.md rule there, the forwarding logic is kept identical
|
|
264
|
+
* in both until FormEngine replaces that renderer.
|
|
265
|
+
*
|
|
266
|
+
* @param inheritProps the field's `inherit_props` map, if it declares one
|
|
267
|
+
* @param availableOptions this form's fields, where a sibling's value is read
|
|
268
|
+
* @param inheritedFromParent the bag threaded in from an outer form
|
|
269
|
+
*/
|
|
270
|
+
export const inheritedScopeFor = (
|
|
271
|
+
inheritProps: Record<string, string> | undefined,
|
|
272
|
+
availableOptions: TQorusForm | undefined,
|
|
273
|
+
inheritedFromParent: Record<string, unknown> | undefined
|
|
274
|
+
): Record<string, unknown> => {
|
|
275
|
+
const resolved = resolveInheritProps(inheritProps, availableOptions, inheritedFromParent);
|
|
276
|
+
if (!Object.keys(resolved).length) {
|
|
277
|
+
return inheritedFromParent ?? NO_INHERITED_SCOPE;
|
|
278
|
+
}
|
|
279
|
+
return { ...(inheritedFromParent ?? {}), ...resolved };
|
|
280
|
+
};
|
|
281
|
+
|
|
217
282
|
const NegativeColorEffect: any = {
|
|
218
283
|
gradient: {
|
|
219
284
|
colors: { 0: 'danger', 100: 'danger:darken:2' },
|
|
@@ -257,9 +322,9 @@ const StyledCompactWrap = styled.div<{ $ownScroll?: boolean }>`
|
|
|
257
322
|
toolbar silently stops pinning. \`clip\` keeps the horizontal guard without
|
|
258
323
|
that coercion. */
|
|
259
324
|
${({ $ownScroll }) =>
|
|
260
|
-
$ownScroll
|
|
261
|
-
|
|
262
|
-
|
|
325
|
+
$ownScroll ?
|
|
326
|
+
css`
|
|
327
|
+
/* 'own': we are the scroller. The sticky toolbar pins to whatever
|
|
263
328
|
scrolls, so a host whose scrollport carries top padding would
|
|
264
329
|
resolve sticky \`top: 0\` against its padding box and leave an
|
|
265
330
|
unblurred strip above the toolbar. Scrolling in this unpadded box
|
|
@@ -268,22 +333,22 @@ const StyledCompactWrap = styled.div<{ $ownScroll?: boolean }>`
|
|
|
268
333
|
|
|
269
334
|
\`min-height: 0\` is what lets a flex item shrink below its content
|
|
270
335
|
so there is something to scroll; it belongs to this branch only. */
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
336
|
+
min-height: 0;
|
|
337
|
+
max-height: 100%;
|
|
338
|
+
overflow-y: auto;
|
|
339
|
+
overflow-x: hidden;
|
|
340
|
+
`
|
|
341
|
+
: css`
|
|
342
|
+
/* \`min-height: auto\` is load-bearing, not a default: inside a column
|
|
278
343
|
flex parent the shrink allowance above collapses this box to zero
|
|
279
344
|
height while its rows paint outside it, so the form looks right but
|
|
280
345
|
contributes nothing to layout — the host cannot size or scroll to
|
|
281
346
|
it and anything below it stacks against nothing. */
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
347
|
+
min-height: auto;
|
|
348
|
+
max-height: none;
|
|
349
|
+
overflow-y: visible;
|
|
350
|
+
overflow-x: clip;
|
|
351
|
+
`}
|
|
287
352
|
|
|
288
353
|
/* Option logos (e.g. language images) render as <img> inside ReqoreIcon's
|
|
289
354
|
square box; constrain them so portrait PNGs don't overflow the row. */
|
|
@@ -318,7 +383,7 @@ export const getType = (
|
|
|
318
383
|
operator?: TOperatorValue
|
|
319
384
|
): TQorusType => {
|
|
320
385
|
const finalType = getTypeFromOperator(operators, fixOperatorValue(operator)) || type;
|
|
321
|
-
const resolvedType =
|
|
386
|
+
const resolvedType = firstDeclaredType(finalType);
|
|
322
387
|
|
|
323
388
|
// A value can be received before its server-driven option schema. Keep the
|
|
324
389
|
// renderer type-safe during that transition without guessing a concrete type.
|
|
@@ -355,17 +420,59 @@ const getOptionSchemaStorageType = (
|
|
|
355
420
|
: isRendererOnly(option?.ui_type) ? option?.type || option?.ui_type
|
|
356
421
|
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
357
422
|
|
|
358
|
-
|
|
423
|
+
/**
|
|
424
|
+
* Whether an option currently holds an expression, in EITHER shape it comes in.
|
|
425
|
+
*
|
|
426
|
+
* At runtime the editor writes the flag onto the option itself
|
|
427
|
+
* (`{ type, is_expression: true, value }`), but a value loaded from a saved
|
|
428
|
+
* draft carries the envelope NESTED (`{ type, value: { is_expression: true,
|
|
429
|
+
* value } }`). Same thing, two shapes, and only the flat one was ever checked —
|
|
430
|
+
* so a freshly typed expression behaved while a reloaded one did not, which is
|
|
431
|
+
* exactly how this survived several fixes: every test used the runtime shape.
|
|
432
|
+
*/
|
|
433
|
+
const optionHoldsExpression = (option: unknown): boolean => {
|
|
434
|
+
const o = option as { is_expression?: unknown; value?: { is_expression?: unknown } } | undefined;
|
|
435
|
+
return !!(o?.is_expression || o?.value?.is_expression);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Which type an option resolves to — the schema's, or the one stored on the
|
|
440
|
+
* value.
|
|
441
|
+
*
|
|
442
|
+
* Exported because this single decision is what every option field is rendered
|
|
443
|
+
* with, and what an expression's return type is checked against. It deserves
|
|
444
|
+
* to be assertable without standing up a form.
|
|
445
|
+
*/
|
|
446
|
+
export const getOptionFieldStorageType = (
|
|
359
447
|
optionName: string,
|
|
360
448
|
fieldType: TQorusType | TQorusType[] | undefined,
|
|
361
449
|
schema?: IQorusFormSchema,
|
|
362
450
|
operators?: IOperatorsSchema,
|
|
363
451
|
operatorData?: TOperatorValue,
|
|
364
|
-
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
452
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType,
|
|
453
|
+
/** Whether the stored value is an expression — see below. */
|
|
454
|
+
isExpression?: boolean
|
|
365
455
|
): TQorusType => {
|
|
366
456
|
const schemaOption = schema?.[optionName];
|
|
457
|
+
/* A STORED type does not describe an expression.
|
|
458
|
+
|
|
459
|
+
An expression value is `{ is_expression: true, value: {...} }`, so the type
|
|
460
|
+
recorded beside it describes that envelope — `hash` — and not what the
|
|
461
|
+
field holds. Letting it win means the SCHEMA's declared type is ignored and
|
|
462
|
+
the wrong answer is handed to the expression editor as the return type to
|
|
463
|
+
check against: `1 + 2` on a field declared `"type": "auto"` came back
|
|
464
|
+
"The expression returns int, and this field holds hash". A literal `1` was
|
|
465
|
+
fine, because a literal is stored as a plain value whose type matches.
|
|
466
|
+
|
|
467
|
+
`auto`/`any` accept anything, which is exactly why nothing should be
|
|
468
|
+
checked against them; a concrete declared type still is. The schema is the
|
|
469
|
+
only party that knows what the field accepts. */
|
|
367
470
|
const storedType =
|
|
368
|
-
|
|
471
|
+
(
|
|
472
|
+
!isExpression &&
|
|
473
|
+
fieldType &&
|
|
474
|
+
!(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType))
|
|
475
|
+
) ?
|
|
369
476
|
fieldType
|
|
370
477
|
: getOptionSchemaStorageType(schemaOption, isRendererOnly);
|
|
371
478
|
|
|
@@ -376,10 +483,10 @@ export const hasRequiredOptions = (options: IQorusFormSchema = {}) => {
|
|
|
376
483
|
return !!findKey(options, (option) => option.required);
|
|
377
484
|
};
|
|
378
485
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
486
|
+
/* Re-exported from the module that owns it so the existing import path keeps
|
|
487
|
+
working; see `optionsContext.ts` for why it is not declared here. */
|
|
488
|
+
export { OptionsContext } from './optionsContext';
|
|
489
|
+
export type { IOptionsContext } from './optionsContext';
|
|
383
490
|
|
|
384
491
|
export const fixOptions = (
|
|
385
492
|
value: TQorusForm | TQorusFlatForm = {},
|
|
@@ -557,6 +664,85 @@ const normalizeEmptyFieldValues = (fields: TQorusForm | TQorusFlatForm | undefin
|
|
|
557
664
|
{} as TQorusForm
|
|
558
665
|
);
|
|
559
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Does this rebuild say anything the parent does not already hold?
|
|
669
|
+
*
|
|
670
|
+
* `false` means the rebuild is a MIRROR: `fixOptions` found no default to
|
|
671
|
+
* materialise, so the result repeats the `value` the parent handed down. The
|
|
672
|
+
* compare is empty-normalized because `fixOptions` round-trips a required-empty
|
|
673
|
+
* field between `{ value: '' }` and no `value` key, and that cosmetic
|
|
674
|
+
* difference would otherwise read as new information forever.
|
|
675
|
+
*/
|
|
676
|
+
export const isMirrorOfValue = (
|
|
677
|
+
fixedValue: TQorusForm | TQorusFlatForm | undefined,
|
|
678
|
+
value: TQorusForm | TQorusFlatForm | undefined
|
|
679
|
+
): boolean => isEqual(normalizeEmptyFieldValues(fixedValue), normalizeEmptyFieldValues(value));
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* What this rebuild lets the form agree with the parent on, if anything.
|
|
683
|
+
*
|
|
684
|
+
* Returns the rebuild when it mirrors `value`, and `undefined` when it
|
|
685
|
+
* contributed something the parent has yet to learn.
|
|
686
|
+
*
|
|
687
|
+
* Adopting the parent's value IS agreement, and saying so matters as much as
|
|
688
|
+
* staying quiet about it. The sync-down skips only when the parent echoed what
|
|
689
|
+
* this form last sent; if adopting a value never updates that record, it points
|
|
690
|
+
* at an older state forever, the skip can never fire again, and the form
|
|
691
|
+
* rebuilds on every single change. Holding the mirror back WITHOUT recording
|
|
692
|
+
* the agreement was measured making the starvation worse, not better — 8 s
|
|
693
|
+
* became a 45 s timeout — which is why the two belong together.
|
|
694
|
+
*/
|
|
695
|
+
export const agreementFromRebuild = (
|
|
696
|
+
fixedValue: TQorusForm | TQorusFlatForm | undefined,
|
|
697
|
+
value: TQorusForm | TQorusFlatForm | undefined
|
|
698
|
+
): TQorusForm | TQorusFlatForm | undefined =>
|
|
699
|
+
isMirrorOfValue(fixedValue, value) ? fixedValue : undefined;
|
|
700
|
+
|
|
701
|
+
/** The state the emit decision is made against. */
|
|
702
|
+
export interface IEmitDecision {
|
|
703
|
+
/** The form's own copy of the value, as it stands now. */
|
|
704
|
+
localValue: TQorusForm | TQorusFlatForm | undefined;
|
|
705
|
+
/** The `value` prop, read at the moment the decision is made. */
|
|
706
|
+
value: TQorusForm | TQorusFlatForm | undefined;
|
|
707
|
+
/**
|
|
708
|
+
* The last rebuild that mirrored `value` without adding anything, if the most
|
|
709
|
+
* recent one did. `undefined` when the last rebuild contributed something.
|
|
710
|
+
*/
|
|
711
|
+
mirroredValue: TQorusForm | TQorusFlatForm | undefined;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Should the form report `localValue` to its parent as an answer?
|
|
716
|
+
*
|
|
717
|
+
* Extracted and pure because the situation it exists for cannot be reproduced
|
|
718
|
+
* through a rendered form: it needs the emit to be decided against a `value`
|
|
719
|
+
* NEWER than the `localValue` built from it, which only happens when two
|
|
720
|
+
* components' effects land in one React commit — and a test renderer flushes
|
|
721
|
+
* effects between updates, so the two never fall out of step. The states below
|
|
722
|
+
* were recorded from a live instrumented run instead, and this is what makes
|
|
723
|
+
* them assertable.
|
|
724
|
+
*
|
|
725
|
+
* Two ways the answer is no:
|
|
726
|
+
*
|
|
727
|
+
* 1. `localValue` is a mirror — a rebuild of the parent's own value that added
|
|
728
|
+
* nothing. Repeating it back is not an answer. **Checked first**, because
|
|
729
|
+
* the comparison in (2) reads whatever `value` has become by now, which,
|
|
730
|
+
* when a nested engine shares the parent value, is already that engine's
|
|
731
|
+
* newer write; comparing against it would find a difference this form never
|
|
732
|
+
* made and emit the older mirror over the newer write. That is the loop.
|
|
733
|
+
* 2. `localValue` already equals `value` — there is nothing to report.
|
|
734
|
+
*/
|
|
735
|
+
export const shouldEmitLocalValue = ({
|
|
736
|
+
localValue,
|
|
737
|
+
value,
|
|
738
|
+
mirroredValue,
|
|
739
|
+
}: IEmitDecision): boolean => {
|
|
740
|
+
if (mirroredValue !== undefined && isEqual(localValue, mirroredValue)) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
return !isEqual(localValue, value);
|
|
744
|
+
};
|
|
745
|
+
|
|
560
746
|
export const flattenOptions = (options: TQorusForm): TQorusFlatForm => {
|
|
561
747
|
return reduce(
|
|
562
748
|
options,
|
|
@@ -592,7 +778,7 @@ export const getTypeAndCanBeNull = (
|
|
|
592
778
|
return {
|
|
593
779
|
type: realType,
|
|
594
780
|
defaultType: realType,
|
|
595
|
-
defaultInternalType: realType
|
|
781
|
+
defaultInternalType: isUntypedOptionType(realType) ? undefined : realType,
|
|
596
782
|
canBeNull,
|
|
597
783
|
};
|
|
598
784
|
};
|
|
@@ -610,7 +796,7 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
610
796
|
name: string;
|
|
611
797
|
uniqueName?: string;
|
|
612
798
|
value?: TQorusForm | TQorusFlatForm;
|
|
613
|
-
options?:
|
|
799
|
+
options?: IOptionsSchema;
|
|
614
800
|
onChange?: (name: string, value?: TQorusForm, meta?: IOptionsOnChangeMeta) => void;
|
|
615
801
|
/**
|
|
616
802
|
* `'immediate'` (default): edits flow out via the debounced `onChange`.
|
|
@@ -652,7 +838,28 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
652
838
|
onOptionsLoaded?: (options: IQorusFormSchema) => void;
|
|
653
839
|
recordRequiresSearchOptions?: boolean;
|
|
654
840
|
readOnly?: boolean;
|
|
841
|
+
/**
|
|
842
|
+
* What a click on a READ-ONLY row does, when there is somewhere to send it.
|
|
843
|
+
*
|
|
844
|
+
* A read-only row opens nothing by default — it shows everything it has and
|
|
845
|
+
* answers a click with silence, which is right when the form is the only
|
|
846
|
+
* place the value exists. Where the same field IS editable somewhere else,
|
|
847
|
+
* that silence is a dead end: the reader clicking a value they want to
|
|
848
|
+
* change has said exactly what they want.
|
|
849
|
+
*
|
|
850
|
+
* Given this, each row becomes the way in to that place and announces it —
|
|
851
|
+
* it takes a button's role, keyboard handling and cursor. Omit it and
|
|
852
|
+
* nothing changes.
|
|
853
|
+
*/
|
|
854
|
+
onReadOnlyActivate?: (optionName: string) => void;
|
|
655
855
|
allowTemplates?: boolean;
|
|
856
|
+
/**
|
|
857
|
+
* The form's SHARED template vocabulary — config items, system properties —
|
|
858
|
+
* offered to every field that supports templates.
|
|
859
|
+
*
|
|
860
|
+
* A field may also declare `templates` of its own in the schema, and that
|
|
861
|
+
* narrower list wins for that field: see {@link TFieldWithOwnTemplates}.
|
|
862
|
+
*/
|
|
656
863
|
stringTemplates?: IReqoreFormTemplates;
|
|
657
864
|
/** Opt-in: fetch global templates from `system/getContextData` for this context. */
|
|
658
865
|
interfaceContext?: string;
|
|
@@ -727,13 +934,78 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
727
934
|
* the reader is choosing WHICH to open rather than reading this one, e.g. a
|
|
728
935
|
* template's per-action option forms stacked in a drawer.
|
|
729
936
|
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
937
|
+
* One escape is not negotiable: a box always opens while a search query is
|
|
938
|
+
* running (`ReqorePanel` unmounts collapsed content, so a match inside a
|
|
939
|
+
* closed box would be unreachable).
|
|
940
|
+
*
|
|
941
|
+
* A second applies only by DEFAULT: the Optional box stays open when every
|
|
942
|
+
* row is optional, because a form that collapsed its only box would render a
|
|
943
|
+
* card that looks empty and broken. Passing this prop explicitly waives it —
|
|
944
|
+
* a host that supplies its own heading and explanation around the form (a
|
|
945
|
+
* run-options panel, say) has already told the reader what the card is, and
|
|
946
|
+
* asked for the settings to start folded away.
|
|
735
947
|
*/
|
|
736
948
|
compactCollapsedGroups?: TFormEngineBoxKey[];
|
|
949
|
+
/**
|
|
950
|
+
* Compact mode only: rename the status boxes.
|
|
951
|
+
*
|
|
952
|
+
* The three are `Needs attention`, `Set` and `Optional`, which read correctly
|
|
953
|
+
* when the form is the page's subject. Inside a host that already named the
|
|
954
|
+
* subject — a panel headed "Change a setting for this run" — a bare
|
|
955
|
+
* `Optional` names a category rather than the things in it, and
|
|
956
|
+
* `Optional settings` is what the reader is looking at. Only the labels
|
|
957
|
+
* change; bucketing, icons and intents are untouched.
|
|
958
|
+
*/
|
|
959
|
+
compactBoxLabels?: Partial<Record<TFormEngineBoxKey, string>>;
|
|
960
|
+
/**
|
|
961
|
+
* Compact mode only: the three status-box headers stay pinned to the top of
|
|
962
|
+
* whatever scrolls while their own box is in view. Default `true`.
|
|
963
|
+
*
|
|
964
|
+
* A box header is the only thing that says which bucket a row is in — `Set`,
|
|
965
|
+
* `Needs attention`, `Optional`. On a form long enough to scroll it leaves
|
|
966
|
+
* the screen, and every row below it then reads as an undifferentiated list:
|
|
967
|
+
* the reader deep inside `Optional` has no way to tell it from `Set` except
|
|
968
|
+
* by scrolling back. Pinning the header is what keeps the answer on screen.
|
|
969
|
+
*
|
|
970
|
+
* ALL THREE behave identically, deliberately. They are one control rendered
|
|
971
|
+
* three times, differing only in label, tint and icon; a reader who has
|
|
972
|
+
* scrolled into the Optional box needs to know that exactly as much as one
|
|
973
|
+
* who has scrolled into Set, and a rule that pinned only some of them would
|
|
974
|
+
* make the absence of a pinned header mean two different things (this box has
|
|
975
|
+
* no header vs. you are above every box).
|
|
976
|
+
*
|
|
977
|
+
* A header can never outlive its own box: `position: sticky` is bounded by
|
|
978
|
+
* its containing block, so each header unpins as its box scrolls away and at
|
|
979
|
+
* most ONE box header is ever pinned at a time. The pin costs one header's
|
|
980
|
+
* height, not three.
|
|
981
|
+
*
|
|
982
|
+
* Off by construction for a nested (`arg_schema`) sub-form — it sits inside
|
|
983
|
+
* the parent form's card and owns no scroll context to pin against.
|
|
984
|
+
*
|
|
985
|
+
* On a NARROW form this also takes the pin off the form's own toolbar, so the
|
|
986
|
+
* two never cost a third of a phone's screen between them — see the note at
|
|
987
|
+
* the panel's `stickyHeader`.
|
|
988
|
+
*
|
|
989
|
+
* Turn it off where the form is one item in a scrolling LIST of forms: there
|
|
990
|
+
* the reader is choosing which form to read, and a box header pinned from the
|
|
991
|
+
* item they have scrolled past is chrome belonging to something they have
|
|
992
|
+
* left.
|
|
993
|
+
*/
|
|
994
|
+
compactStickyBoxHeaders?: boolean;
|
|
995
|
+
/**
|
|
996
|
+
* Compact mode only: how many pixels of the HOST's own sticky chrome sit
|
|
997
|
+
* above this form, inside the same scroll container.
|
|
998
|
+
*
|
|
999
|
+
* Sticky offsets do not compose by themselves — every pinned thing resolves
|
|
1000
|
+
* `top` against the same scrollport, so two of them with the same offset land
|
|
1001
|
+
* on top of each other. A host that pins a bar of its own (a page header
|
|
1002
|
+
* carrying a name and a Run control, say) measures it and passes the height
|
|
1003
|
+
* here; the form then pins its toolbar below that bar, and its box headers
|
|
1004
|
+
* below the toolbar, instead of underneath both.
|
|
1005
|
+
*
|
|
1006
|
+
* Default `0` — the form is the topmost pinned thing in its scroller.
|
|
1007
|
+
*/
|
|
1008
|
+
compactStickyOffset?: number;
|
|
737
1009
|
/**
|
|
738
1010
|
* Compact mode only: which parts of the form's toolbar to show. `true`
|
|
739
1011
|
* (default) shows all of it; `false` hides the whole thing.
|
|
@@ -781,6 +1053,37 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
781
1053
|
* a consumer injecting many actions can never blow out the row.
|
|
782
1054
|
*/
|
|
783
1055
|
optionActionsCollapse?: 'auto' | 'always' | 'never';
|
|
1056
|
+
/**
|
|
1057
|
+
* The HOST is still loading something this form needs.
|
|
1058
|
+
*
|
|
1059
|
+
* Declared because it was already read (`rest.skeleton`) and honoured by the
|
|
1060
|
+
* loading gate, but undeclared props are invisible to the people who need
|
|
1061
|
+
* them — and the reason to reach for this is not obvious.
|
|
1062
|
+
*
|
|
1063
|
+
* A host that resolves anything before it can render a form — an option list
|
|
1064
|
+
* fetched by `get_message`, a schema id resolved over the wire — has a wait of
|
|
1065
|
+
* its own that ends just as this component's begins. Drawing its own
|
|
1066
|
+
* placeholder for that first half and then handing over produces TWO
|
|
1067
|
+
* placeholders in series for one load, from two different components: the DOM
|
|
1068
|
+
* node is replaced, the geometry can differ, and the page relayouts in the
|
|
1069
|
+
* middle of waiting. Measured on the Qorus alert rule, whose three sections
|
|
1070
|
+
* each did this — six placeholder mounts where three would do.
|
|
1071
|
+
*
|
|
1072
|
+
* Passing the host's own wait in here instead makes ONE component own the
|
|
1073
|
+
* whole wait, from the host's first fetch to this form's last, with one
|
|
1074
|
+
* placeholder and one DOM node.
|
|
1075
|
+
*/
|
|
1076
|
+
skeleton?: boolean;
|
|
1077
|
+
/**
|
|
1078
|
+
* What the HOST is waiting for, when it hands its wait down via `skeleton`.
|
|
1079
|
+
*
|
|
1080
|
+
* Without it every host wait reads as the same word and the reading stops at
|
|
1081
|
+
* the package boundary — measured on the live alert rule, three sections all
|
|
1082
|
+
* reported `host` and the next step was guessing which of the host's own five
|
|
1083
|
+
* conditions had fired. Surfaced as `data-wait` on the placeholder.
|
|
1084
|
+
*/
|
|
1085
|
+
skeletonReason?: string;
|
|
1086
|
+
|
|
784
1087
|
/**
|
|
785
1088
|
* SEAM (reqraft): consumer-injected field editors for types reqraft doesn't
|
|
786
1089
|
* ship (IDE domain fields). Keyed by field `type`/`ui_type`; forwarded through
|
|
@@ -817,6 +1120,28 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
817
1120
|
* consumer's new editor no longer needs a reqraft release to be handled.
|
|
818
1121
|
*/
|
|
819
1122
|
rendererOnlyUiTypes?: string[];
|
|
1123
|
+
/**
|
|
1124
|
+
* The `ui_type` names whose own editor renders templates as chips INLINE.
|
|
1125
|
+
*
|
|
1126
|
+
* For these the template selector is suppressed and the field's editor is
|
|
1127
|
+
* shown instead — the selector would replace a control the author can type in
|
|
1128
|
+
* with one they can only pick from. Merged with reqraft's built-in list
|
|
1129
|
+
* (`richtext`), the same way `rendererOnlyUiTypes` is.
|
|
1130
|
+
*/
|
|
1131
|
+
templateAwareUiTypes?: string[];
|
|
1132
|
+
/**
|
|
1133
|
+
* Extra `ui_type` names that mean "a value of any type", for the purpose of
|
|
1134
|
+
* offering the per-type choices in the row's menu.
|
|
1135
|
+
*
|
|
1136
|
+
* A field that pins no concrete type offers "enter a value of type X" for
|
|
1137
|
+
* each type the form knows. That was gated on the schema saying
|
|
1138
|
+
* `ui_type: 'any'` alone, which misses two cases: an option that declares its
|
|
1139
|
+
* untypedness as `type: 'auto'` and carries no `ui_type` at all (the server
|
|
1140
|
+
* spells an assertion's Expected Value exactly that way), and an option whose
|
|
1141
|
+
* `ui_type` is a HOST's own untyped editor. Merged with reqraft's built-ins
|
|
1142
|
+
* (`any`, `auto`), the same way `templateAwareUiTypes` is.
|
|
1143
|
+
*/
|
|
1144
|
+
anyLikeUiTypes?: string[];
|
|
820
1145
|
|
|
821
1146
|
/**
|
|
822
1147
|
* Bag of values forwarded from an outer FormEngine scope, used as a
|
|
@@ -878,12 +1203,17 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
878
1203
|
* for the pane to exist at all — so a pasted or reloaded link lands on a
|
|
879
1204
|
* collapsed form.
|
|
880
1205
|
*
|
|
881
|
-
* Applied once, on the first render
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
1206
|
+
* Applied once, on the first render that actually CONTAINS one of the named
|
|
1207
|
+
* rows — not merely the first render with any rows — so a schema arriving in
|
|
1208
|
+
* pieces still gets its address applied rather than spending the one shot on
|
|
1209
|
+
* a form the field had not reached yet.
|
|
1210
|
+
*
|
|
1211
|
+
* It never re-expands a row the user has since collapsed, and it does not
|
|
1212
|
+
* participate in `expandMode: 'single'` accordion collapsing — the caller is
|
|
1213
|
+
* naming a starting point, not driving the state. That holds against the
|
|
1214
|
+
* engine's own openers too: `expandFirstRequired` / `autoFocusFirstRequired`
|
|
1215
|
+
* open their target ALONGSIDE these rows in single mode instead of replacing
|
|
1216
|
+
* them. Only a remount re-arms it. No-op in classic (non-compact) mode.
|
|
887
1217
|
*/
|
|
888
1218
|
initialExpandedOptions?: string[];
|
|
889
1219
|
|
|
@@ -925,6 +1255,7 @@ const FormEngineImpl = ({
|
|
|
925
1255
|
onOptionsLoaded,
|
|
926
1256
|
recordRequiresSearchOptions,
|
|
927
1257
|
readOnly,
|
|
1258
|
+
onReadOnlyActivate,
|
|
928
1259
|
allowTemplates = true,
|
|
929
1260
|
interfaceContext,
|
|
930
1261
|
templateFieldProps,
|
|
@@ -941,7 +1272,10 @@ const FormEngineImpl = ({
|
|
|
941
1272
|
compactNested = false,
|
|
942
1273
|
compactScroll = 'host',
|
|
943
1274
|
compactPanelProps,
|
|
944
|
-
compactCollapsedGroups
|
|
1275
|
+
compactCollapsedGroups: compactCollapsedGroupsProp,
|
|
1276
|
+
compactBoxLabels,
|
|
1277
|
+
compactStickyBoxHeaders = true,
|
|
1278
|
+
compactStickyOffset = 0,
|
|
945
1279
|
compactToolbar = true,
|
|
946
1280
|
commitMode = 'immediate',
|
|
947
1281
|
expandMode = 'single',
|
|
@@ -959,6 +1293,8 @@ const FormEngineImpl = ({
|
|
|
959
1293
|
// onto a DOM node
|
|
960
1294
|
markdownRenderer: _markdownRenderer, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
961
1295
|
rendererOnlyUiTypes,
|
|
1296
|
+
templateAwareUiTypes,
|
|
1297
|
+
anyLikeUiTypes,
|
|
962
1298
|
inheritedFromParent,
|
|
963
1299
|
autoFocusFirstRequired,
|
|
964
1300
|
expandFirstRequired,
|
|
@@ -977,17 +1313,27 @@ const FormEngineImpl = ({
|
|
|
977
1313
|
// from Reqore's context (one subscription for the whole app) rather than a
|
|
978
1314
|
// reqraft-local matchMedia hook.
|
|
979
1315
|
//
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
const isHoverCapable = useReqoreProperty('isHoverCapable')
|
|
1316
|
+
// Both are always set by `ReqoreProvider`, and the peer floor (>= 0.74.0) is
|
|
1317
|
+
// the version that sets them. A defensive `?? true` here would be dead code:
|
|
1318
|
+
// `useReqoreProperty` THROWS on a context that lacks the key, so an older
|
|
1319
|
+
// Reqore fails at the hook rather than degrading to a default.
|
|
1320
|
+
const isHoverCapable = useReqoreProperty('isHoverCapable');
|
|
985
1321
|
const isMobile = useReqoreProperty('isMobile');
|
|
986
1322
|
const collapseOptionActions =
|
|
987
1323
|
optionActionsCollapse === 'always' ? true
|
|
988
1324
|
: optionActionsCollapse === 'never' ? false
|
|
989
1325
|
: !isHoverCapable || !!isMobile;
|
|
990
|
-
|
|
1326
|
+
/* The schema, as the form should ASK it — not always as it was served.
|
|
1327
|
+
`resolveDegenerateRequiredGroups` turns a one-of group that has a single
|
|
1328
|
+
member in this schema back into a plain required field, because a choice
|
|
1329
|
+
with one option is not a choice. Applied here, at the one place the schema
|
|
1330
|
+
enters the component, so the header, the chips, the messages, the meter and
|
|
1331
|
+
the read-first summary cannot disagree about it: every one of them reads
|
|
1332
|
+
`options`, and none of them has to know the rule. */
|
|
1333
|
+
const [servedOptions, setOptions] = useState<IQorusFormSchema | undefined>(
|
|
1334
|
+
rest?.options || undefined
|
|
1335
|
+
);
|
|
1336
|
+
const options = useMemo(() => resolveDegenerateRequiredGroups(servedOptions), [servedOptions]);
|
|
991
1337
|
// optionsLoader lifecycle: loading feeds the skeleton gate, error the banner.
|
|
992
1338
|
const [optionsLoading, setOptionsLoading] = useState<boolean>(!!optionsLoader && !rest?.options);
|
|
993
1339
|
const [optionsError, setOptionsError] = useState<string | undefined>();
|
|
@@ -1079,6 +1425,67 @@ const FormEngineImpl = ({
|
|
|
1079
1425
|
},
|
|
1080
1426
|
[compactWrapRef]
|
|
1081
1427
|
);
|
|
1428
|
+
/* Declared here, not where it is first USED in the render: the sticky
|
|
1429
|
+
measurement below takes it as a dependency, and a dependency array is
|
|
1430
|
+
evaluated during render — a `const` declared further down would still be
|
|
1431
|
+
in its temporal dead zone. */
|
|
1432
|
+
const compactNarrow = !!compactWrapWidth && compactWrapWidth < 480;
|
|
1433
|
+
|
|
1434
|
+
/* How much pinned chrome sits above a status-box header.
|
|
1435
|
+
|
|
1436
|
+
Sticky offsets do not compose on their own: everything pinned in one
|
|
1437
|
+
scroller resolves `top` against the same scrollport, so a box header and
|
|
1438
|
+
this form's own toolbar both asking for 0 land on top of each other — and
|
|
1439
|
+
the toolbar is the loser, because it renders first and the box header wins
|
|
1440
|
+
on z-order. Covering it hides the search, which is the one control that
|
|
1441
|
+
forces a collapsed box open.
|
|
1442
|
+
|
|
1443
|
+
So the box headers pin BELOW the toolbar, at the host's own offset plus the
|
|
1444
|
+
toolbar's height. Measured, not assumed: the toolbar is a completion meter,
|
|
1445
|
+
a search row and a field picker that each hide themselves when they have
|
|
1446
|
+
nothing to say, and what is left wraps differently at every form width.
|
|
1447
|
+
Zero when this form draws no toolbar at all. */
|
|
1448
|
+
const [compactToolbarHeight, setCompactToolbarHeight] = useState(0);
|
|
1449
|
+
useLayoutEffect(() => {
|
|
1450
|
+
const wrap = compactWrapNode;
|
|
1451
|
+
if (!compact || !wrap || compactNested || !compactStickyBoxHeaders) {
|
|
1452
|
+
setCompactToolbarHeight(0);
|
|
1453
|
+
return undefined;
|
|
1454
|
+
}
|
|
1455
|
+
const header = wrap.querySelector<HTMLElement>(
|
|
1456
|
+
':scope > .reqore-panel > .reqore-panel-title'
|
|
1457
|
+
);
|
|
1458
|
+
if (!header) {
|
|
1459
|
+
setCompactToolbarHeight(0);
|
|
1460
|
+
return undefined;
|
|
1461
|
+
}
|
|
1462
|
+
const read = () =>
|
|
1463
|
+
setCompactToolbarHeight((prev) => {
|
|
1464
|
+
/* Only a toolbar that is ITSELF pinned takes room off the top. A host
|
|
1465
|
+
can switch the pin off (`compactPanelProps.stickyHeader`), and it
|
|
1466
|
+
does where the form is a couple of fields inside a longer page — and
|
|
1467
|
+
a box header offset by a toolbar that scrolls away would float a
|
|
1468
|
+
header's height below the top edge with nothing under it. Read from
|
|
1469
|
+
the element rather than from the props that produced it, so every
|
|
1470
|
+
route to that decision is accounted for. */
|
|
1471
|
+
const pinned = getComputedStyle(header).position === 'sticky';
|
|
1472
|
+
const next = pinned ? Math.round(header.getBoundingClientRect().height) : 0;
|
|
1473
|
+
return prev === next ? prev : next;
|
|
1474
|
+
});
|
|
1475
|
+
read();
|
|
1476
|
+
/* jsdom and older engines have no ResizeObserver; the one-shot read above
|
|
1477
|
+
still gives the right answer for a form whose toolbar never reflows. */
|
|
1478
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
1479
|
+
return undefined;
|
|
1480
|
+
}
|
|
1481
|
+
const observer = new ResizeObserver(read);
|
|
1482
|
+
observer.observe(header);
|
|
1483
|
+
return () => observer.disconnect();
|
|
1484
|
+
/* `compactNarrow` is a dependency, not decoration: crossing the width
|
|
1485
|
+
breakpoint takes the pin OFF the toolbar without changing its height, so
|
|
1486
|
+
the ResizeObserver never fires and a stale 124px would go on offsetting
|
|
1487
|
+
the box headers below a toolbar that is no longer there. */
|
|
1488
|
+
}, [compact, compactNested, compactStickyBoxHeaders, compactNarrow, compactWrapNode, options]);
|
|
1082
1489
|
|
|
1083
1490
|
// Global label-column sizing: size the label column to the WIDEST field label
|
|
1084
1491
|
// across the whole form, clamped to [MIN, MAX], and publish it as a CSS var on
|
|
@@ -1179,7 +1586,6 @@ const FormEngineImpl = ({
|
|
|
1179
1586
|
}
|
|
1180
1587
|
});
|
|
1181
1588
|
|
|
1182
|
-
const compactNarrow = !!compactWrapWidth && compactWrapWidth < 480;
|
|
1183
1589
|
// Info panels auto-open on Tier-1 content; the per-row user override sticks.
|
|
1184
1590
|
const [infoPanelOverrides, setInfoPanelOverrides] = useState<Record<string, boolean>>({});
|
|
1185
1591
|
// Toolbar filters affect the listed rows only — the meter reflects the full set.
|
|
@@ -1198,6 +1604,32 @@ const FormEngineImpl = ({
|
|
|
1198
1604
|
// Track the last value we emitted via onChange so we can skip re-applying fixOptions
|
|
1199
1605
|
// when the parent echoes it back as the new value prop (controlled component loop prevention)
|
|
1200
1606
|
const lastEmittedValue = useRef<TQorusForm | TQorusFlatForm | undefined>(value);
|
|
1607
|
+
/**
|
|
1608
|
+
* A `localValue` that is only a MIRROR of the incoming `value`, and so must
|
|
1609
|
+
* never be emitted back as though this form had answered something.
|
|
1610
|
+
*
|
|
1611
|
+
* `localValue` is derived state: the sync-down effect below rebuilds it from
|
|
1612
|
+
* the `value` prop. When that rebuild adds nothing (`fixOptions` had no
|
|
1613
|
+
* default to materialise), the result says exactly what the parent already
|
|
1614
|
+
* holds, and emitting it is not a change — it is this form repeating the
|
|
1615
|
+
* parent's own words back at it.
|
|
1616
|
+
*
|
|
1617
|
+
* That repetition is harmless in a form that owns its state outright and
|
|
1618
|
+
* fatal in one that shares a parent with a NESTED engine. The two write to
|
|
1619
|
+
* the same parent value, so each sees the other's write arrive as an external
|
|
1620
|
+
* change; `lastEmittedValue` is then always one step behind and its guard
|
|
1621
|
+
* never fires. The engines alternate between two states forever, several
|
|
1622
|
+
* times a second, and the newer write is repeatedly overwritten by the older
|
|
1623
|
+
* one. Measured in the test editor: the row's summary flipped between two
|
|
1624
|
+
* readings indefinitely and `setTimeout` was starved past a 45-second
|
|
1625
|
+
* deadline.
|
|
1626
|
+
*
|
|
1627
|
+
* Only a mirror that CHANGES nothing is held back. A rebuild that restores a
|
|
1628
|
+
* required or preselected field's default still emits, because that default
|
|
1629
|
+
* is genuinely new information the parent has to be told about — which is the
|
|
1630
|
+
* whole reason the sync-down runs `fixOptions` at all.
|
|
1631
|
+
*/
|
|
1632
|
+
const mirroredValue = useRef<TQorusForm | TQorusFlatForm | undefined>(undefined);
|
|
1201
1633
|
|
|
1202
1634
|
if (originalValue.current === undefined && size(value)) {
|
|
1203
1635
|
originalValue.current = localValue.fields;
|
|
@@ -1208,7 +1640,13 @@ const FormEngineImpl = ({
|
|
|
1208
1640
|
const templates = useTemplates(allowTemplates, rest.stringTemplates, interfaceContext);
|
|
1209
1641
|
|
|
1210
1642
|
useEffect(() => {
|
|
1211
|
-
if (
|
|
1643
|
+
if (
|
|
1644
|
+
!shouldEmitLocalValue({
|
|
1645
|
+
localValue: localValue.fields,
|
|
1646
|
+
value,
|
|
1647
|
+
mirroredValue: mirroredValue.current,
|
|
1648
|
+
})
|
|
1649
|
+
) {
|
|
1212
1650
|
return;
|
|
1213
1651
|
}
|
|
1214
1652
|
|
|
@@ -1371,9 +1809,12 @@ const FormEngineImpl = ({
|
|
|
1371
1809
|
// Note: compare fixedValue against value, not localValue.fields — localValue may have been
|
|
1372
1810
|
// updated by nested FormEngine emissions, so comparing against it would never skip.
|
|
1373
1811
|
const normalizedValue = normalizeEmptyFieldValues(value);
|
|
1812
|
+
/* What this rebuild lets us agree with the parent on — `undefined` when it
|
|
1813
|
+
contributed something the parent has yet to learn. */
|
|
1814
|
+
const agreed = agreementFromRebuild(fixedValue, value);
|
|
1374
1815
|
if (
|
|
1375
1816
|
isEqual(normalizedValue, normalizeEmptyFieldValues(lastEmittedValue.current)) &&
|
|
1376
|
-
|
|
1817
|
+
agreed !== undefined
|
|
1377
1818
|
) {
|
|
1378
1819
|
return;
|
|
1379
1820
|
}
|
|
@@ -1382,14 +1823,42 @@ const FormEngineImpl = ({
|
|
|
1382
1823
|
originalValue.current = fixedValue;
|
|
1383
1824
|
}
|
|
1384
1825
|
|
|
1826
|
+
/* Remember a mirror so the emit effect can tell one from an answer, and
|
|
1827
|
+
record the agreement it represents. A rebuild that DID add something
|
|
1828
|
+
leaves both alone — the parent has to learn about it. */
|
|
1829
|
+
mirroredValue.current = agreed;
|
|
1830
|
+
if (agreed !== undefined) {
|
|
1831
|
+
lastEmittedValue.current = agreed;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1385
1834
|
setLocalValue?.({ fields: fixedValue, meta: undefined });
|
|
1386
1835
|
}, [JSON.stringify(options), JSON.stringify(value), isRendererOnly]);
|
|
1387
1836
|
|
|
1837
|
+
/**
|
|
1838
|
+
* Fields the reader has actually edited in this instance.
|
|
1839
|
+
*
|
|
1840
|
+
* "This field is required" is an ERROR message, and an error is a report that
|
|
1841
|
+
* something went wrong. Under a field nobody has been in yet it reports
|
|
1842
|
+
* nothing — the form is empty because it is new — while the requirement is
|
|
1843
|
+
* already stated by the asterisk, by the Needs-attention box the row sits in,
|
|
1844
|
+
* and by the completion meter. A form whose first act is to accuse the reader
|
|
1845
|
+
* of a mistake they have not made teaches them to discount its warnings.
|
|
1846
|
+
*
|
|
1847
|
+
* So the message waits for a touch. Being IN the field and leaving it empty
|
|
1848
|
+
* is a real gap, and that one shows. A ref, not state: every edit re-renders
|
|
1849
|
+
* anyway, so the following render reads the current set with no second pass.
|
|
1850
|
+
*/
|
|
1851
|
+
const touchedOptionsRef = useRef<Set<string>>(new Set());
|
|
1852
|
+
|
|
1388
1853
|
const handleValueChange = useCallback(
|
|
1389
1854
|
(optionName: string, val?: any, _type?: string, isFunction?: boolean) => {
|
|
1855
|
+
// Every route into this handler is a person acting: typing, picking from a
|
|
1856
|
+
// menu, clearing the value, adding an optional field. Marked here rather
|
|
1857
|
+
// than inside the updater below, which React may invoke twice.
|
|
1858
|
+
touchedOptionsRef.current.add(optionName);
|
|
1390
1859
|
setLocalValue(({ fields = {} }) => {
|
|
1391
1860
|
const schemaType = getOptionSchemaStorageType(options?.[optionName], isRendererOnly);
|
|
1392
|
-
const isAnyLike = schemaType
|
|
1861
|
+
const isAnyLike = isUntypedOptionType(schemaType);
|
|
1393
1862
|
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
1394
1863
|
const resolvedSchemaType =
|
|
1395
1864
|
isAnyLike && (fields[optionName] as IQorusFormField)?.type ?
|
|
@@ -1441,7 +1910,19 @@ const FormEngineImpl = ({
|
|
|
1441
1910
|
},
|
|
1442
1911
|
};
|
|
1443
1912
|
|
|
1444
|
-
|
|
1913
|
+
/* The flag belongs to the VALUE, so a change that said nothing about it
|
|
1914
|
+
does not throw it away — `undefined` is no opinion, not a denial.
|
|
1915
|
+
Deleting it unconditionally left the AST on the row with nothing
|
|
1916
|
+
marking it as one: an assertion's Expected Value came back from a
|
|
1917
|
+
saved draft drawn as a two-field hash of `exp` and `args`, because
|
|
1918
|
+
merely opening the row and reverting had stripped the flag. */
|
|
1919
|
+
if (
|
|
1920
|
+
shouldMarkAsExpression(
|
|
1921
|
+
isFunction,
|
|
1922
|
+
val,
|
|
1923
|
+
((fields as TQorusForm)[optionName] as { is_expression?: boolean })?.is_expression
|
|
1924
|
+
)
|
|
1925
|
+
) {
|
|
1445
1926
|
(updatedValue[optionName] as { is_expression?: boolean }).is_expression = true;
|
|
1446
1927
|
} else {
|
|
1447
1928
|
delete updatedValue[optionName].is_expression;
|
|
@@ -1455,9 +1936,24 @@ const FormEngineImpl = ({
|
|
|
1455
1936
|
val !== (fields as TQorusForm)[optionName]?.value
|
|
1456
1937
|
) {
|
|
1457
1938
|
forEach(options, (option, depName) => {
|
|
1939
|
+
/* A dependency is `name`, `name=value` or `name!=value`, so the
|
|
1940
|
+
entry has to be PARSED to find which option it names. Compared as
|
|
1941
|
+
a whole string, only the bare spelling ever matched — so a field
|
|
1942
|
+
declared `depends_on: ['subject_iface_kind=workflow']` was never
|
|
1943
|
+
cleared when that option changed.
|
|
1944
|
+
|
|
1945
|
+
What that cost: a test whose subject was switched from a workflow
|
|
1946
|
+
to a service kept the workflow's `Subject Interface Version`.
|
|
1947
|
+
Services and jobs are versioned but only the latest is testable,
|
|
1948
|
+
so the field no longer applied, yet it stayed on the form as a
|
|
1949
|
+
locked control in front of a value the author could not clear —
|
|
1950
|
+
the value being there is exactly what stops the field being
|
|
1951
|
+
withheld outright. */
|
|
1458
1952
|
if (
|
|
1459
1953
|
option.depends_on &&
|
|
1460
|
-
flatten(option.depends_on).
|
|
1954
|
+
flatten(option.depends_on).some(
|
|
1955
|
+
(dependency) => parseDependency(dependency as string).name === optionName
|
|
1956
|
+
) &&
|
|
1461
1957
|
updatedValue[depName]
|
|
1462
1958
|
) {
|
|
1463
1959
|
updatedValue[depName].value = undefined;
|
|
@@ -1637,6 +2133,7 @@ const FormEngineImpl = ({
|
|
|
1637
2133
|
return newValue;
|
|
1638
2134
|
}
|
|
1639
2135
|
|
|
2136
|
+
|
|
1640
2137
|
const rendererType = getType(
|
|
1641
2138
|
(options[optionName].ui_type || options[optionName].type) as TQorusType,
|
|
1642
2139
|
operators,
|
|
@@ -1648,7 +2145,8 @@ const FormEngineImpl = ({
|
|
|
1648
2145
|
options,
|
|
1649
2146
|
operators,
|
|
1650
2147
|
(option as IQorusFormField)?.op,
|
|
1651
|
-
isRendererOnly
|
|
2148
|
+
isRendererOnly,
|
|
2149
|
+
optionHoldsExpression(option)
|
|
1652
2150
|
);
|
|
1653
2151
|
|
|
1654
2152
|
if (!isPlainObject(option)) {
|
|
@@ -1693,6 +2191,27 @@ const FormEngineImpl = ({
|
|
|
1693
2191
|
return { members, satisfiedBy };
|
|
1694
2192
|
}, [JSON.stringify(options), JSON.stringify(availableOptions)]);
|
|
1695
2193
|
|
|
2194
|
+
/* One bag per field, rebuilt only when something it is made of changes — the
|
|
2195
|
+
schema that declares the `inherit_props`, the sibling values they resolve
|
|
2196
|
+
against, or the scope handed in from an outer form. Held per field rather
|
|
2197
|
+
than per form because each field's bag is different: `inherit_props` is
|
|
2198
|
+
declared on the field. See `inheritedScopeFor` for what a churning bag
|
|
2199
|
+
costs the field that receives it. */
|
|
2200
|
+
const inheritedScopes = useMemo(() => {
|
|
2201
|
+
const scopes = new Map<string, Record<string, unknown>>();
|
|
2202
|
+
for (const optionName of Object.keys(options ?? {})) {
|
|
2203
|
+
scopes.set(
|
|
2204
|
+
optionName,
|
|
2205
|
+
inheritedScopeFor(
|
|
2206
|
+
options?.[optionName]?.inherit_props,
|
|
2207
|
+
availableOptions,
|
|
2208
|
+
inheritedFromParent
|
|
2209
|
+
)
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
return scopes;
|
|
2213
|
+
}, [options, availableOptions, inheritedFromParent]);
|
|
2214
|
+
|
|
1696
2215
|
// Dependency linkage (3a): when filling a dependency unlocks rows, flash
|
|
1697
2216
|
// them once — the form visibly "opens up" instead of silently changing.
|
|
1698
2217
|
const dependencyLockedNames = useMemo(() => {
|
|
@@ -1722,6 +2241,49 @@ const FormEngineImpl = ({
|
|
|
1722
2241
|
}
|
|
1723
2242
|
}, [dependencyLockedNames.join('|')]);
|
|
1724
2243
|
|
|
2244
|
+
/* The same thing one level down: a CHOICE that becomes available is as
|
|
2245
|
+
invisible as a field that becomes editable, and it is worse, because it
|
|
2246
|
+
opens up inside a control the reader may not have open. So the field that
|
|
2247
|
+
offers it flashes, which is the affordance that says "look here" without
|
|
2248
|
+
claiming the choice was made.
|
|
2249
|
+
|
|
2250
|
+
Keyed by field and by the value's identity, so a value that merely moves in
|
|
2251
|
+
the list is not mistaken for one that opened. Fields whose values gate on
|
|
2252
|
+
nothing never enter the set, which is nearly all of them. */
|
|
2253
|
+
const lockedAllowedValues = useMemo(() => {
|
|
2254
|
+
const locked: string[] = [];
|
|
2255
|
+
forEach(options || {}, (optionSchema, name) => {
|
|
2256
|
+
forEach(optionSchema?.allowed_values || [], (allowedValue) => {
|
|
2257
|
+
if (!getAllowedValueAvailability(allowedValue, availableOptions, options || {}).available) {
|
|
2258
|
+
locked.push(`${name}\u0000${JSON.stringify(allowedValue?.value?.value)}`);
|
|
2259
|
+
}
|
|
2260
|
+
});
|
|
2261
|
+
});
|
|
2262
|
+
return locked;
|
|
2263
|
+
}, [JSON.stringify(options), JSON.stringify(availableOptions)]);
|
|
2264
|
+
const previousLockedAllowedValues = useRef<string[] | null>(null);
|
|
2265
|
+
useEffect(() => {
|
|
2266
|
+
const previous = previousLockedAllowedValues.current;
|
|
2267
|
+
previousLockedAllowedValues.current = lockedAllowedValues;
|
|
2268
|
+
if (!previous) {
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
const opened = previous
|
|
2272
|
+
.filter((key) => !lockedAllowedValues.includes(key))
|
|
2273
|
+
.map((key) => key.split('\u0000')[0]);
|
|
2274
|
+
const fields = uniq(opened).filter(
|
|
2275
|
+
(name) =>
|
|
2276
|
+
!!options?.[name] &&
|
|
2277
|
+
!options?.[name]?.disabled &&
|
|
2278
|
+
// A field that is itself still locked flashes for its OWN unlocking, in
|
|
2279
|
+
// the effect above; two flashes for one change is a flicker, not a cue.
|
|
2280
|
+
!dependencyLockedNames.includes(name)
|
|
2281
|
+
);
|
|
2282
|
+
if (fields.length) {
|
|
2283
|
+
flashOptions(fields);
|
|
2284
|
+
}
|
|
2285
|
+
}, [lockedAllowedValues.join('|')]);
|
|
2286
|
+
|
|
1725
2287
|
// The not-yet-added optional fields: everything in the schema the form is not
|
|
1726
2288
|
// already showing a row for. This ONE list feeds all three ways a field gets
|
|
1727
2289
|
// added — the inline addable rows, the Fields menu, and its "Select all" — so
|
|
@@ -1793,7 +2355,8 @@ const FormEngineImpl = ({
|
|
|
1793
2355
|
options,
|
|
1794
2356
|
operators,
|
|
1795
2357
|
(option as IQorusFormField).op,
|
|
1796
|
-
isRendererOnly
|
|
2358
|
+
isRendererOnly,
|
|
2359
|
+
optionHoldsExpression(option)
|
|
1797
2360
|
);
|
|
1798
2361
|
const optionValue = (option as IQorusFormField).value;
|
|
1799
2362
|
|
|
@@ -1864,7 +2427,8 @@ const FormEngineImpl = ({
|
|
|
1864
2427
|
options,
|
|
1865
2428
|
operators,
|
|
1866
2429
|
(option as IQorusFormField).op,
|
|
1867
|
-
isRendererOnly
|
|
2430
|
+
isRendererOnly,
|
|
2431
|
+
optionHoldsExpression(option)
|
|
1868
2432
|
),
|
|
1869
2433
|
(option as IQorusFormField).value
|
|
1870
2434
|
)
|
|
@@ -1913,7 +2477,8 @@ const FormEngineImpl = ({
|
|
|
1913
2477
|
options,
|
|
1914
2478
|
operators,
|
|
1915
2479
|
(availableOptions as TQorusForm)?.[name]?.op,
|
|
1916
|
-
isRendererOnly
|
|
2480
|
+
isRendererOnly,
|
|
2481
|
+
optionHoldsExpression((availableOptions as TQorusForm)?.[name])
|
|
1917
2482
|
);
|
|
1918
2483
|
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1919
2484
|
// A schema-declared hash whose every entry is a materialised-but-unset
|
|
@@ -1988,18 +2553,6 @@ const FormEngineImpl = ({
|
|
|
1988
2553
|
setShowAllDescriptions((prev) => prev !== true);
|
|
1989
2554
|
}, []);
|
|
1990
2555
|
const handleToggleInvalidOnly = useCallback(() => setShowInvalidOptionsOnly((prev) => !prev), []);
|
|
1991
|
-
/**
|
|
1992
|
-
* Put every field back, whichever filter took them away.
|
|
1993
|
-
*
|
|
1994
|
-
* Both filters can hide the whole form at once, and a reader who has
|
|
1995
|
-
* lost the form does not care which one did it — so one control
|
|
1996
|
-
* undoes both rather than making them work out that the attention
|
|
1997
|
-
* filter is still on behind the text they just cleared.
|
|
1998
|
-
*/
|
|
1999
|
-
const handleClearCompactFilters = useCallback(() => {
|
|
2000
|
-
setCompactQuery('');
|
|
2001
|
-
setShowInvalidOptionsOnly(false);
|
|
2002
|
-
}, []);
|
|
2003
2556
|
const handleRevertChangesClick = useCallback(() => {
|
|
2004
2557
|
setLocalValue({
|
|
2005
2558
|
fields: originalValue.current,
|
|
@@ -2076,11 +2629,16 @@ const FormEngineImpl = ({
|
|
|
2076
2629
|
if (!names.length) {
|
|
2077
2630
|
return;
|
|
2078
2631
|
}
|
|
2079
|
-
hasAppliedInitialExpansionRef.current = true;
|
|
2080
2632
|
const toExpand = initialExpandedOptions.filter((name) => names.includes(name));
|
|
2081
2633
|
if (!toExpand.length) {
|
|
2634
|
+
// Not "there is no such row" — "not YET". A server-driven schema arrives
|
|
2635
|
+
// in pieces, and the row the caller named can be in a later one (a test's
|
|
2636
|
+
// `cases` field materialises once its step-kind catalogue lands). Latching
|
|
2637
|
+
// here would spend the one shot on a form that did not yet contain the
|
|
2638
|
+
// field, and the address would never be applied at all.
|
|
2082
2639
|
return;
|
|
2083
2640
|
}
|
|
2641
|
+
hasAppliedInitialExpansionRef.current = true;
|
|
2084
2642
|
setExpandedOptions((prev) => [...prev, ...toExpand.filter((name) => !prev.includes(name))]);
|
|
2085
2643
|
}, [initialExpandedOptions, compact, availableOptions]);
|
|
2086
2644
|
|
|
@@ -2101,15 +2659,13 @@ const FormEngineImpl = ({
|
|
|
2101
2659
|
// genuine remount (a fresh instance) auto-focuses again, which is the intended
|
|
2102
2660
|
// on-mount behaviour.
|
|
2103
2661
|
const hasAutoFocusedRef = useRef(false);
|
|
2104
|
-
// The field expanded programmatically for autofocus. A ref (not state) so
|
|
2105
|
-
// CompactRow's 60ms focus timer reads the current value regardless of render
|
|
2106
|
-
// batching; CompactRow focuses this one with `preventScroll` so an off-screen
|
|
2107
|
-
// (or below-the-fold) form is never scrolled into view on mount.
|
|
2108
|
-
const autoFocusNameRef = useRef<string | undefined>(undefined);
|
|
2109
2662
|
useEffect(() => {
|
|
2110
|
-
// Two decisions, one scan.
|
|
2111
|
-
//
|
|
2112
|
-
// the
|
|
2663
|
+
// Two decisions, one scan. Both open the row that needs attention;
|
|
2664
|
+
// `autoFocusFirstRequired` additionally promises the caret lands in it, and
|
|
2665
|
+
// so refuses to run at all while the reader holds the caret somewhere else
|
|
2666
|
+
// (the guard below). An opened row takes the caret either way — that is
|
|
2667
|
+
// CompactRow's tap-to-edit rule, and it declines just as firmly to take one
|
|
2668
|
+
// that is already somewhere outside the row.
|
|
2113
2669
|
const wantsFocus = !!autoFocusFirstRequired;
|
|
2114
2670
|
if ((!wantsFocus && !expandFirstRequired) || !compact || !options) {
|
|
2115
2671
|
return;
|
|
@@ -2163,15 +2719,16 @@ const FormEngineImpl = ({
|
|
|
2163
2719
|
hasAutoFocusedRef.current = true;
|
|
2164
2720
|
|
|
2165
2721
|
if (target) {
|
|
2166
|
-
// Set the ref before the state update so CompactRow's focus timer sees it.
|
|
2167
|
-
// Expand-only leaves it unset, which is what keeps the caret where it is.
|
|
2168
|
-
if (wantsFocus) {
|
|
2169
|
-
autoFocusNameRef.current = target;
|
|
2170
|
-
}
|
|
2171
2722
|
setExpandedOptions((prev) =>
|
|
2172
2723
|
prev.includes(target) ? prev
|
|
2173
2724
|
: expandMode === 'multi' ? [...prev, target]
|
|
2174
|
-
|
|
2725
|
+
// `expandMode: 'single'` is a rule about what a CLICK does: opening a
|
|
2726
|
+
// row closes the one the reader last opened. It is not a licence to
|
|
2727
|
+
// close a row the HOST named through `initialExpandedOptions`. That
|
|
2728
|
+
// is an address — a statement about what the page is FOR, made before
|
|
2729
|
+
// the reader did anything — and this scan is a convenience layered on
|
|
2730
|
+
// top of it. Keep the named rows; replace only what a click could.
|
|
2731
|
+
: [...prev.filter((name) => initialExpandedOptions?.includes(name)), target]
|
|
2175
2732
|
);
|
|
2176
2733
|
}
|
|
2177
2734
|
}, [
|
|
@@ -2184,13 +2741,18 @@ const FormEngineImpl = ({
|
|
|
2184
2741
|
dependencyLockedNames,
|
|
2185
2742
|
getOptionBucket,
|
|
2186
2743
|
expandMode,
|
|
2744
|
+
initialExpandedOptions,
|
|
2187
2745
|
]);
|
|
2188
2746
|
|
|
2189
2747
|
// Read-first completion summary (how many shown options have a value set),
|
|
2190
2748
|
// surfaced as a progress meter at the top of the compact form.
|
|
2191
2749
|
const readFirstCompletion = useMemo(
|
|
2192
|
-
() =>
|
|
2193
|
-
|
|
2750
|
+
() =>
|
|
2751
|
+
getReadFirstCompletion(
|
|
2752
|
+
availableOptions as Record<string, IQorusFormField | undefined>,
|
|
2753
|
+
(name) => getOptionBucket(name) === 'attention'
|
|
2754
|
+
),
|
|
2755
|
+
[JSON.stringify(availableOptions), getOptionBucket]
|
|
2194
2756
|
);
|
|
2195
2757
|
|
|
2196
2758
|
const optionalFields = useMemo(
|
|
@@ -2284,13 +2846,59 @@ const FormEngineImpl = ({
|
|
|
2284
2846
|
// schema has not arrived yet (`type` undefined) instead of crashing.
|
|
2285
2847
|
const optionSchema = options?.[optionName];
|
|
2286
2848
|
const schemaUiType = optionSchema?.ui_type as TQorusType;
|
|
2287
|
-
const uiTypeIsAnyLike = schemaUiType
|
|
2849
|
+
const uiTypeIsAnyLike = isUntypedOptionType(schemaUiType);
|
|
2850
|
+
/* An EXPRESSION's stored type is skipped in this chain.
|
|
2851
|
+
|
|
2852
|
+
The value is `{ is_expression: true, value: {...} }`, so the type saved
|
|
2853
|
+
beside it describes that envelope — `hash` — not what the field holds.
|
|
2854
|
+
With no `ui_type` to win first, that `hash` beat the schema's `auto`
|
|
2855
|
+
and was handed to the expression editor as the return type to check
|
|
2856
|
+
against: `1 + 2` on a field declared `auto` came back "This does not
|
|
2857
|
+
fit. The expression returns int, and this field holds hash". A literal
|
|
2858
|
+
`1` was fine, because a literal is stored as a plain value whose type
|
|
2859
|
+
matches.
|
|
2860
|
+
|
|
2861
|
+
Dropping the stored type here lets the schema decide, which is the
|
|
2862
|
+
only party that knows what the field accepts. This is the row
|
|
2863
|
+
renderer's OWN resolution and does not go through
|
|
2864
|
+
`getOptionFieldStorageType`, so guarding that one alone left this
|
|
2865
|
+
untouched. */
|
|
2866
|
+
const storedRowType = optionHoldsExpression(other) ? undefined : type;
|
|
2867
|
+
|
|
2868
|
+
/* A RELOADED expression is handed to the editor in the shape the editor
|
|
2869
|
+
was written against.
|
|
2870
|
+
|
|
2871
|
+
An expression is written as `{ type, is_expression: true, value: ast }`
|
|
2872
|
+
and saved as `{ type, value: { is_expression: true, value: ast } }` —
|
|
2873
|
+
the flag moves ONTO the value. The editor was only ever given
|
|
2874
|
+
`other.is_expression`, which is absent in the saved shape, so a
|
|
2875
|
+
reloaded expression was not recognised as one: an `auto` field then
|
|
2876
|
+
resolved its type from the value it could see, decided the envelope was
|
|
2877
|
+
a `hash`, and rendered `1 + 2` as a raw tree of `is_expression` /
|
|
2878
|
+
`exp` / `args` for the author to edit by hand.
|
|
2879
|
+
|
|
2880
|
+
Both halves have to move together. Flipping the flag alone would put
|
|
2881
|
+
the editor into expression mode and then hand it the ENVELOPE where it
|
|
2882
|
+
expects the expression, so the value is unwrapped here too — after
|
|
2883
|
+
which a saved expression is indistinguishable from one just typed,
|
|
2884
|
+
which is what every component downstream already assumes.
|
|
2885
|
+
|
|
2886
|
+
Reported as "after Run assertion the value shows as raw data", but the
|
|
2887
|
+
run is incidental: the field beside it, which no run touched, was
|
|
2888
|
+
equally raw. The trigger is a RELOAD, which is the only thing that puts
|
|
2889
|
+
an expression into its saved shape. */
|
|
2890
|
+
const nestedExpression = !!(other as { value?: { is_expression?: unknown } })?.value
|
|
2891
|
+
?.is_expression;
|
|
2892
|
+
const expressionAwareValue =
|
|
2893
|
+
nestedExpression ?
|
|
2894
|
+
(other as unknown as { value: { value: unknown } }).value.value
|
|
2895
|
+
: other.value;
|
|
2288
2896
|
const resolvedType =
|
|
2289
2897
|
(isFixedCompactAllowedValueOption(optionSchema) ?
|
|
2290
2898
|
getOptionSchemaStorageType(optionSchema, isRendererOnly)
|
|
2291
2899
|
: schemaUiType && !uiTypeIsAnyLike ? schemaUiType
|
|
2292
2900
|
: undefined) ||
|
|
2293
|
-
|
|
2901
|
+
storedRowType ||
|
|
2294
2902
|
schemaUiType ||
|
|
2295
2903
|
(optionSchema?.type as TQorusType) ||
|
|
2296
2904
|
'any';
|
|
@@ -2423,20 +3031,23 @@ const FormEngineImpl = ({
|
|
|
2423
3031
|
// `inherit_props: { language: 'language' }`, the list renderer
|
|
2424
3032
|
// forwards it into each row, and the row's body resolves against
|
|
2425
3033
|
// the accumulated bag.
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
3034
|
+
//
|
|
3035
|
+
// Read from the memoised map rather than built here: this is a
|
|
3036
|
+
// PROP, and one built inline is a new object on every render of
|
|
3037
|
+
// the form, which re-renders every field that receives it. The
|
|
3038
|
+
// `??` chain answers for a field the schema does not describe —
|
|
3039
|
+
// `inheritedScopeFor` returns the SAME references, so an
|
|
3040
|
+
// undescribed field is as stable as a described one.
|
|
3041
|
+
inheritedFromParent={
|
|
3042
|
+
inheritedScopes.get(optionName) ?? inheritedFromParent ?? NO_INHERITED_SCOPE
|
|
3043
|
+
}
|
|
2434
3044
|
// Propagate compact so an arg_schema field renders a COMPACT sub-form
|
|
2435
3045
|
// (consistent with the parent) rather than the classic FormEngine.
|
|
2436
3046
|
compact={compact}
|
|
2437
3047
|
// SEAM: forwarded through TemplateField's rest-spread to AutoFormField,
|
|
2438
3048
|
// which renders consumer-injected editors by field type/ui_type.
|
|
2439
3049
|
componentOverrides={componentOverrides}
|
|
3050
|
+
templateAwareUiTypes={templateAwareUiTypes}
|
|
2440
3051
|
allowTemplates={!!(allowTemplates && options?.[optionName]?.supports_templates)}
|
|
2441
3052
|
allowFunctions={!!options?.[optionName]?.supports_expressions}
|
|
2442
3053
|
// reqraft: form-level expression fields get the Visual/Text shell
|
|
@@ -2453,7 +3064,25 @@ const FormEngineImpl = ({
|
|
|
2453
3064
|
isFixedCompactAllowedValueOption(optionSchema) ||
|
|
2454
3065
|
(options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any')
|
|
2455
3066
|
}
|
|
2456
|
-
|
|
3067
|
+
// A field may declare a template list OF ITS OWN, and when it does
|
|
3068
|
+
// that list is the one to offer. The engine-wide `stringTemplates`
|
|
3069
|
+
// are the form's shared vocabulary - config items, system properties
|
|
3070
|
+
// - and every field draws on the same set. A per-field list answers a
|
|
3071
|
+
// question only that field asks: a test assertion's `$.` references
|
|
3072
|
+
// are the values ITS case captured, and no other field in the form
|
|
3073
|
+
// should be offering them.
|
|
3074
|
+
//
|
|
3075
|
+
// Passing the engine's list unconditionally overwrote the schema's,
|
|
3076
|
+
// so a field that declared templates had none by the time it
|
|
3077
|
+
// rendered. On a typed field that only cost the picker; on an
|
|
3078
|
+
// ANY-LIKE one it changed the question, because `TemplateField`
|
|
3079
|
+
// opens on the template selector only when there is something to
|
|
3080
|
+
// pick and otherwise falls back to the type picker. An untyped field
|
|
3081
|
+
// therefore asked the author to choose `string`/`int`/`hash` before
|
|
3082
|
+
// it would let them name a value they had already captured.
|
|
3083
|
+
templates={
|
|
3084
|
+
(optionSchema as TFieldWithOwnTemplates | undefined)?.templates ?? templates.value
|
|
3085
|
+
}
|
|
2457
3086
|
{...getTypeAndCanBeNull(
|
|
2458
3087
|
// The RENDERER type picks the editor — the storage type lives on
|
|
2459
3088
|
// the value envelope. Passing storage here rendered a `richtext`
|
|
@@ -2474,8 +3103,8 @@ const FormEngineImpl = ({
|
|
|
2474
3103
|
key={optionName}
|
|
2475
3104
|
arg_schema={options?.[optionName]?.arg_schema}
|
|
2476
3105
|
noSoft={!!rest?.options}
|
|
2477
|
-
value={
|
|
2478
|
-
isFunction={(other
|
|
3106
|
+
value={expressionAwareValue}
|
|
3107
|
+
isFunction={optionHoldsExpression(other)}
|
|
2479
3108
|
isDefaultFunction={options?.[optionName]?.default_view === 'expression'}
|
|
2480
3109
|
sensitive={options?.[optionName]?.sensitive}
|
|
2481
3110
|
default_value={getDefaultValue(options?.[optionName])}
|
|
@@ -2499,7 +3128,7 @@ const FormEngineImpl = ({
|
|
|
2499
3128
|
readOnly={readOnly}
|
|
2500
3129
|
size={editorSize || rest.size}
|
|
2501
3130
|
menuItems={
|
|
2502
|
-
(options?.[optionName]
|
|
3131
|
+
offersTypeChoices(options?.[optionName], anyLikeUiTypes) ?
|
|
2503
3132
|
getCustomMenuTemplateItems(optionName)
|
|
2504
3133
|
: undefined
|
|
2505
3134
|
}
|
|
@@ -2512,6 +3141,7 @@ const FormEngineImpl = ({
|
|
|
2512
3141
|
name={optionName}
|
|
2513
3142
|
option={{ type: resolvedType, ...other }}
|
|
2514
3143
|
getType={getTypeForOption}
|
|
3144
|
+
untouched={!touchedOptionsRef.current.has(optionName)}
|
|
2515
3145
|
/>
|
|
2516
3146
|
{operators && size(operators) && size(other.op) ?
|
|
2517
3147
|
<>
|
|
@@ -2561,6 +3191,8 @@ const FormEngineImpl = ({
|
|
|
2561
3191
|
handleRemoveOperator,
|
|
2562
3192
|
getCustomMenuTemplateItems,
|
|
2563
3193
|
getTypeForOption,
|
|
3194
|
+
inheritedScopes,
|
|
3195
|
+
inheritedFromParent,
|
|
2564
3196
|
]
|
|
2565
3197
|
);
|
|
2566
3198
|
|
|
@@ -2593,6 +3225,7 @@ const FormEngineImpl = ({
|
|
|
2593
3225
|
() => ({
|
|
2594
3226
|
templates: templates.value,
|
|
2595
3227
|
readOnly,
|
|
3228
|
+
onReadOnlyActivate,
|
|
2596
3229
|
commitMode,
|
|
2597
3230
|
expandMode,
|
|
2598
3231
|
options,
|
|
@@ -2602,7 +3235,6 @@ const FormEngineImpl = ({
|
|
|
2602
3235
|
codePreviewRenderer,
|
|
2603
3236
|
showAllDescriptions,
|
|
2604
3237
|
expandedOptions,
|
|
2605
|
-
autoFocusNameRef,
|
|
2606
3238
|
highlightedOptions,
|
|
2607
3239
|
flashedOptions,
|
|
2608
3240
|
infoPanelOverrides,
|
|
@@ -2650,7 +3282,6 @@ const FormEngineImpl = ({
|
|
|
2650
3282
|
codePreviewRenderer,
|
|
2651
3283
|
showAllDescriptions,
|
|
2652
3284
|
expandedOptions,
|
|
2653
|
-
autoFocusNameRef,
|
|
2654
3285
|
highlightedOptions,
|
|
2655
3286
|
flashedOptions,
|
|
2656
3287
|
infoPanelOverrides,
|
|
@@ -2710,7 +3341,21 @@ const FormEngineImpl = ({
|
|
|
2710
3341
|
fields: false,
|
|
2711
3342
|
help: false,
|
|
2712
3343
|
},
|
|
2713
|
-
|
|
3344
|
+
/* From the SCHEMA, not from the value.
|
|
3345
|
+
*
|
|
3346
|
+
* `availableOptions` is keyed off the form's VALUE, so it grows as data
|
|
3347
|
+
* arrives — and the search row is rendered only when this is true
|
|
3348
|
+
* (`CompactToolbar`). A form whose value lands after its first render
|
|
3349
|
+
* therefore ADDED a 38px row to its header a beat later, and everything
|
|
3350
|
+
* below it moved. Measured on the Qorus IDE's test editor, on the nested
|
|
3351
|
+
* sub-form of a case: 33 elements jumped down 51px in one frame, on every
|
|
3352
|
+
* cold load.
|
|
3353
|
+
*
|
|
3354
|
+
* How many fields a form has is a property of its schema. The two agree
|
|
3355
|
+
* once the value has arrived — `fixOptions` materialises an entry per
|
|
3356
|
+
* declared field — so this only changes the answer during the load, which
|
|
3357
|
+
* is the whole point. */
|
|
3358
|
+
hasMultipleOptions: size(options) > 1,
|
|
2714
3359
|
compactQuery,
|
|
2715
3360
|
setCompactQuery,
|
|
2716
3361
|
requiredOnly,
|
|
@@ -2731,6 +3376,7 @@ const FormEngineImpl = ({
|
|
|
2731
3376
|
}),
|
|
2732
3377
|
[
|
|
2733
3378
|
readOnly,
|
|
3379
|
+
onReadOnlyActivate,
|
|
2734
3380
|
validityData,
|
|
2735
3381
|
readFirstAttentionCount,
|
|
2736
3382
|
readFirstCompletion,
|
|
@@ -2961,6 +3607,13 @@ const FormEngineImpl = ({
|
|
|
2961
3607
|
const bucketCount = (b: TFormEngineBoxKey) =>
|
|
2962
3608
|
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
2963
3609
|
|
|
3610
|
+
/* The default is to collapse the Optional box, and an explicit pass is
|
|
3611
|
+
what waives the "never collapse the sole box" escape below. Resolving it
|
|
3612
|
+
here rather than in the parameter list is what makes the two
|
|
3613
|
+
distinguishable — `['optional']` as a default and `['optional']` from a
|
|
3614
|
+
caller mean different things. */
|
|
3615
|
+
const compactCollapsedGroups = compactCollapsedGroupsProp ?? ['optional'];
|
|
3616
|
+
const collapsedGroupsRequested = compactCollapsedGroupsProp !== undefined;
|
|
2964
3617
|
// "Is the Optional box the whole form?" — when nothing needs attention and
|
|
2965
3618
|
// nothing is set, collapsing it leaves a card with no visible content at all.
|
|
2966
3619
|
const onlyOptionalRows =
|
|
@@ -2997,15 +3650,28 @@ const FormEngineImpl = ({
|
|
|
2997
3650
|
const boxHasPreselectedRows = (boxKey: TFormEngineBoxKey) =>
|
|
2998
3651
|
boxKey === 'optional' &&
|
|
2999
3652
|
bucketGroups[boxKey].some((groupName) => groupHasPreselectedRow(boxKey, groupName));
|
|
3653
|
+
/* The pin, and where it lands. A nested sub-form sits inside the parent's
|
|
3654
|
+
card and owns no scroll context, so it never pins. */
|
|
3655
|
+
const stickyBoxHeaders = compactStickyBoxHeaders && !compactNested;
|
|
3656
|
+
const boxStickyOffset = compactStickyOffset + compactToolbarHeight;
|
|
3000
3657
|
const STATUS_BOXES: Array<{
|
|
3001
3658
|
key: TFormEngineBoxKey;
|
|
3002
3659
|
label: string;
|
|
3003
3660
|
intent?: 'warning' | 'success';
|
|
3004
3661
|
icon: IReqoreIconName;
|
|
3005
3662
|
}> = [
|
|
3006
|
-
{
|
|
3007
|
-
|
|
3008
|
-
|
|
3663
|
+
{
|
|
3664
|
+
key: 'attention',
|
|
3665
|
+
label: compactBoxLabels?.attention ?? 'Needs attention',
|
|
3666
|
+
intent: 'warning',
|
|
3667
|
+
icon: 'ErrorWarningLine',
|
|
3668
|
+
},
|
|
3669
|
+
{ key: 'set', label: compactBoxLabels?.set ?? 'Set', intent: 'success', icon: 'CheckLine' },
|
|
3670
|
+
{
|
|
3671
|
+
key: 'optional',
|
|
3672
|
+
label: compactBoxLabels?.optional ?? 'Optional',
|
|
3673
|
+
icon: 'CheckboxBlankCircleLine',
|
|
3674
|
+
},
|
|
3009
3675
|
];
|
|
3010
3676
|
|
|
3011
3677
|
// Build the rows for one group: contiguous required-group members are pulled
|
|
@@ -3149,7 +3815,7 @@ const FormEngineImpl = ({
|
|
|
3149
3815
|
// Mirrors the toolbar's own search-row gate: when no search
|
|
3150
3816
|
// row renders, the header is a thin strip and sits tight to
|
|
3151
3817
|
// the first status box (see StyledCompactPanel).
|
|
3152
|
-
$tightHeader={!(compactToolbarParts?.search && size(
|
|
3818
|
+
$tightHeader={!(compactToolbarParts?.search && size(options) > 1)}
|
|
3153
3819
|
// The top-level form scrolls, so its toolbar STICKS and carries a
|
|
3154
3820
|
// dark blurred backdrop so content ghosts cleanly beneath it. A
|
|
3155
3821
|
// nested (arg_schema) sub-form owns no scroll context — drop the
|
|
@@ -3157,6 +3823,9 @@ const FormEngineImpl = ({
|
|
|
3157
3823
|
// transparent inside the parent's card.
|
|
3158
3824
|
$headerBg={compactNested ? 'transparent' : headerBg}
|
|
3159
3825
|
$nested={compactNested}
|
|
3826
|
+
// Keeps the wrapper from clipping, which is what a pinned box
|
|
3827
|
+
// header inside it needs — see StyledCompactPanel.
|
|
3828
|
+
$stickyBoxes={compactStickyBoxHeaders && !compactNested}
|
|
3160
3829
|
flat
|
|
3161
3830
|
raised
|
|
3162
3831
|
minimal
|
|
@@ -3165,7 +3834,30 @@ const FormEngineImpl = ({
|
|
|
3165
3834
|
// parent's edit card) instead of stacking its own dark surface.
|
|
3166
3835
|
// The status boxes keep their own tints; the sticky toolbar keeps
|
|
3167
3836
|
// its blurred header via the $headerBg override.
|
|
3168
|
-
|
|
3837
|
+
// NARROW: the toolbar gives up its pin so the box headers
|
|
3838
|
+
// can keep theirs.
|
|
3839
|
+
//
|
|
3840
|
+
// Measured at 380px, a pinned toolbar is 124px and a pinned
|
|
3841
|
+
// box header is 48 — together a third of a phone's readable
|
|
3842
|
+
// body, before the form has drawn a line. Only one of them
|
|
3843
|
+
// can be afforded there, and the box header is the better
|
|
3844
|
+
// buy: the toolbar is chrome you GO to (a search, a field
|
|
3845
|
+
// picker, a meter — all of it still one scroll away at the
|
|
3846
|
+
// top of the form), while the box header is chrome you READ,
|
|
3847
|
+
// and it is the only thing that says which bucket the rows
|
|
3848
|
+
// under the pointer are in.
|
|
3849
|
+
//
|
|
3850
|
+
// Same shape of decision the engine already makes one level
|
|
3851
|
+
// down, where `optionActionsCollapse: 'auto'` folds a row's
|
|
3852
|
+
// actions into a menu because a narrow row has no width for
|
|
3853
|
+
// a button strip.
|
|
3854
|
+
//
|
|
3855
|
+
// A consumer can still force either way: `compactPanelProps`
|
|
3856
|
+
// spreads after this.
|
|
3857
|
+
stickyHeader={!compactNested && !(compactNarrow && compactStickyBoxHeaders)}
|
|
3858
|
+
// Below the host's own pinned chrome, if it has any — see
|
|
3859
|
+
// `compactStickyOffset`.
|
|
3860
|
+
stickyHeaderOffset={compactStickyOffset || undefined}
|
|
3169
3861
|
// Consumer overrides land last so they win over every default
|
|
3170
3862
|
// above; `actions` and `contentStyle` below merge rather than
|
|
3171
3863
|
// replace (see `compactPanelProps`).
|
|
@@ -3175,52 +3867,35 @@ const FormEngineImpl = ({
|
|
|
3175
3867
|
display: 'flex',
|
|
3176
3868
|
flexFlow: 'column',
|
|
3177
3869
|
gap: '10px',
|
|
3870
|
+
/* The panel body must not be a SCROLL CONTAINER.
|
|
3871
|
+
|
|
3872
|
+
`StyledPanelContent` ships `overflow: auto` so a
|
|
3873
|
+
height-capped panel can scroll its body. This one is never
|
|
3874
|
+
capped — in `compactScroll: 'host'` the page scrolls and in
|
|
3875
|
+
'own' the wrap above does — so the `auto` never engages and
|
|
3876
|
+
reads as inert. It is not: an `overflow: auto` box is a
|
|
3877
|
+
scrollport whether or not it scrolls, and every
|
|
3878
|
+
`position: sticky` inside resolves against IT. The status
|
|
3879
|
+
box headers (and the toolbar, in a nested host) would pin
|
|
3880
|
+
to a box that never moves, which looks exactly like sticky
|
|
3881
|
+
being ignored.
|
|
3882
|
+
|
|
3883
|
+
`overflow-x: clip`, NOT `hidden`, for the reason spelled
|
|
3884
|
+
out on `StyledCompactWrap`: CSS coerces an `overflow-y:
|
|
3885
|
+
visible` to `auto` unless the other axis is visible or
|
|
3886
|
+
clip, which would put the scrollport straight back. `clip`
|
|
3887
|
+
keeps the horizontal guard the row ellipsis relies on. */
|
|
3888
|
+
overflowY: 'visible',
|
|
3889
|
+
overflowX: 'clip',
|
|
3178
3890
|
...compactPanelProps?.contentStyle,
|
|
3179
3891
|
}}
|
|
3180
3892
|
>
|
|
3181
3893
|
{size(groupKeys) === 0 || (readOnly && !bucketCount('set')) ?
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
Nothing is set.
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
say what is behind it, and offer the way back.
|
|
3188
|
-
|
|
3189
|
-
This used to be one passive line — "No fields match
|
|
3190
|
-
the current filters." — with no control and no idea
|
|
3191
|
-
how much it was hiding, while the toolbar directly
|
|
3192
|
-
above it still read "2 need attention →". The form
|
|
3193
|
-
claimed two fields wanted the reader's attention and
|
|
3194
|
-
then showed them none, which reads as a broken form
|
|
3195
|
-
rather than a filter. At phone width that line plus
|
|
3196
|
-
the toolbar IS the whole form, and the input's own
|
|
3197
|
-
clear affordance is a ~20px target. */
|
|
3198
|
-
: <ReqoreMessage flat opaque={false} size='small' icon='Search2Line'>
|
|
3199
|
-
<ReqoreControlGroup vertical gapSize='small'>
|
|
3200
|
-
<span>
|
|
3201
|
-
{query ?
|
|
3202
|
-
`No field matches "${compactQuery.trim()}".`
|
|
3203
|
-
: 'No field needs attention right now.'}
|
|
3204
|
-
{/* The sentence that resolves the contradiction
|
|
3205
|
-
with the toolbar: the fields are still there,
|
|
3206
|
-
the filter is simply in front of them. */}
|
|
3207
|
-
{readFirstAttentionCount > 0 && query ?
|
|
3208
|
-
` ${readFirstAttentionCount} of the fields here still need attention.`
|
|
3209
|
-
: ''}
|
|
3210
|
-
</span>
|
|
3211
|
-
<ReqoreButton
|
|
3212
|
-
fixed
|
|
3213
|
-
flat
|
|
3214
|
-
minimal
|
|
3215
|
-
size='small'
|
|
3216
|
-
icon='FilterOffLine'
|
|
3217
|
-
className='options-readfirst-clear-filters'
|
|
3218
|
-
onClick={handleClearCompactFilters}
|
|
3219
|
-
>
|
|
3220
|
-
Show every field
|
|
3221
|
-
</ReqoreButton>
|
|
3222
|
-
</ReqoreControlGroup>
|
|
3223
|
-
</ReqoreMessage>
|
|
3894
|
+
<ReqoreMessage flat opaque={false} size='small'>
|
|
3895
|
+
{readOnly && !query ?
|
|
3896
|
+
'Nothing is set.'
|
|
3897
|
+
: 'No fields match the current filters.'}
|
|
3898
|
+
</ReqoreMessage>
|
|
3224
3899
|
: null}
|
|
3225
3900
|
|
|
3226
3901
|
{/* A read-only form is one flat list of what is set: no
|
|
@@ -3254,7 +3929,7 @@ const FormEngineImpl = ({
|
|
|
3254
3929
|
const wouldCollapse =
|
|
3255
3930
|
compactCollapsedGroups.includes(box.key) &&
|
|
3256
3931
|
!query &&
|
|
3257
|
-
!(box.key === 'optional' && onlyOptionalRows);
|
|
3932
|
+
!(box.key === 'optional' && onlyOptionalRows && !collapsedGroupsRequested);
|
|
3258
3933
|
// Only a box that was opened SOLELY to reveal a preselected
|
|
3259
3934
|
// row splits its body; a box that renders open anyway shows
|
|
3260
3935
|
// all of its groups as before.
|
|
@@ -3314,6 +3989,10 @@ const FormEngineImpl = ({
|
|
|
3314
3989
|
<StyledStatusBox
|
|
3315
3990
|
$accent={accent}
|
|
3316
3991
|
$bg={boxBg}
|
|
3992
|
+
// Only a box that PINS needs an opaque header (see
|
|
3993
|
+
// StyledStatusBox); passing it unconditionally would
|
|
3994
|
+
// repaint every form's boxes for nothing.
|
|
3995
|
+
$surface={stickyBoxHeaders ? getMainBackgroundColor(theme) : undefined}
|
|
3317
3996
|
key={box.key}
|
|
3318
3997
|
ref={(node: HTMLDivElement | null) => {
|
|
3319
3998
|
statusBoxRefs.current[box.key] = node;
|
|
@@ -3325,6 +4004,12 @@ const FormEngineImpl = ({
|
|
|
3325
4004
|
}
|
|
3326
4005
|
flat
|
|
3327
4006
|
minimal
|
|
4007
|
+
/* The header stays on screen while its own box is
|
|
4008
|
+
being read — see `compactStickyBoxHeaders`. Sticky
|
|
4009
|
+
is bounded by its containing block, so the header
|
|
4010
|
+
leaves with its box and only one is ever pinned. */
|
|
4011
|
+
stickyHeader={stickyBoxHeaders}
|
|
4012
|
+
stickyHeaderOffset={stickyBoxHeaders ? boxStickyOffset : undefined}
|
|
3328
4013
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
3329
4014
|
collapsible
|
|
3330
4015
|
// Which boxes start closed is the consumer's call
|
|
@@ -3495,37 +4180,25 @@ const FormEngineImpl = ({
|
|
|
3495
4180
|
);
|
|
3496
4181
|
};
|
|
3497
4182
|
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
<ReqoreControlGroup fixed fill={false}>
|
|
3518
|
-
<ReqoreSkeleton />
|
|
3519
|
-
<ReqoreSkeleton />
|
|
3520
|
-
<ReqoreSkeleton width='100%' />
|
|
3521
|
-
</ReqoreControlGroup>
|
|
3522
|
-
<ReqoreControlGroup vertical fill={false}>
|
|
3523
|
-
<ReqoreSkeleton width='100%' height='150px' />
|
|
3524
|
-
<ReqoreSkeleton width='100%' height='150px' />
|
|
3525
|
-
<ReqoreSkeleton width='100%' height='150px' />
|
|
3526
|
-
</ReqoreControlGroup>
|
|
3527
|
-
</ReqoreControlGroup>
|
|
3528
|
-
);
|
|
4183
|
+
/* Which of the seven waits is holding the form, in the order they are
|
|
4184
|
+
tested. Rendered as `data-wait` on the placeholder so a page can be asked
|
|
4185
|
+
what it is waiting FOR instead of only that it is waiting. */
|
|
4186
|
+
const waitReason =
|
|
4187
|
+
rest.skeleton ? (rest.skeletonReason ? `host:${rest.skeletonReason}` : 'host')
|
|
4188
|
+
: templates.loading ? 'templates'
|
|
4189
|
+
: typesLoading ? 'types'
|
|
4190
|
+
: optionsLoading ? 'options'
|
|
4191
|
+
: loading ? 'fetch'
|
|
4192
|
+
: operatorsUrl && !operators ? 'operators'
|
|
4193
|
+
: (url || customUrl) && !options ? 'schema'
|
|
4194
|
+
: undefined;
|
|
4195
|
+
|
|
4196
|
+
if (waitReason) {
|
|
4197
|
+
/* The shape of the FORM, not a generic block arrangement. This used to be
|
|
4198
|
+
three bars over three big panels, which resembles nothing this gate is
|
|
4199
|
+
waiting for and shared no vocabulary with the other waits on the same
|
|
4200
|
+
page — see `FormFieldsSkeleton`. */
|
|
4201
|
+
return <FormFieldsSkeleton fill className='options-loading-skeleton' reason={waitReason} />;
|
|
3529
4202
|
}
|
|
3530
4203
|
|
|
3531
4204
|
// A loader that rejected (and produced no usable schema) surfaces its error
|
|
@@ -3692,91 +4365,70 @@ const FormEngineImpl = ({
|
|
|
3692
4365
|
STRECHABLE_TYPES.has(options[optionName].type as TQorusType) ||
|
|
3693
4366
|
(options[optionName] as { stretch?: boolean }).stretch,
|
|
3694
4367
|
size: 'small',
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
4368
|
+
/* No floating bar. It is portalled over whatever sits above the
|
|
4369
|
+
row — on the first row, the form's own header buttons, which it
|
|
4370
|
+
made unclickable — and a bar that only appears on a hover is out
|
|
4371
|
+
of reach on a phone. The row's actions render in its own
|
|
4372
|
+
header: injected ones as buttons (all of them in the menu on
|
|
4373
|
+
touch or a narrow viewport), the row's own secondary ones in
|
|
4374
|
+
its ⋯ menu. */
|
|
4375
|
+
actions: optionRowActions({
|
|
4376
|
+
injected: resolveOptionActions(optionActions, {
|
|
3703
4377
|
name: optionName,
|
|
3704
4378
|
schema: options[optionName],
|
|
3705
4379
|
value: availableOptions?.[optionName] as TOption,
|
|
3706
|
-
}),
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
(
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
(
|
|
3730
|
-
!readOnly &&
|
|
3731
|
-
(other as any).value &&
|
|
3732
|
-
!isEqual((other as any).value, getDefaultValue(options[optionName])) &&
|
|
3733
|
-
!(options[optionName]?.disabled || (options[optionName] as any)?.readonly)
|
|
3734
|
-
) ?
|
|
3735
|
-
'hover'
|
|
3736
|
-
: false,
|
|
3737
|
-
onClick: () => handleValueChange(optionName, undefined),
|
|
3738
|
-
},
|
|
3739
|
-
{
|
|
3740
|
-
size: 'tiny',
|
|
3741
|
-
icon: 'HistoryLine',
|
|
3742
|
-
className: 'options-item-revert',
|
|
3743
|
-
tooltip: 'Revert Changes',
|
|
3744
|
-
show:
|
|
3745
|
-
!readOnly && hasOptionChanged((other as any).value, optionName) ?
|
|
3746
|
-
'hover'
|
|
3747
|
-
: false,
|
|
3748
|
-
onClick: () => {
|
|
3749
|
-
handleValueChange(
|
|
3750
|
-
optionName,
|
|
3751
|
-
originalValue.current?.[optionName]?.value,
|
|
3752
|
-
originalValue.current?.[optionName]?.type
|
|
3753
|
-
);
|
|
3754
|
-
},
|
|
3755
|
-
},
|
|
3756
|
-
{
|
|
3757
|
-
size: 'tiny',
|
|
3758
|
-
icon: 'DeleteBinLine',
|
|
3759
|
-
intent: 'danger',
|
|
3760
|
-
minimal: true,
|
|
3761
|
-
className: 'options-optional-remove',
|
|
3762
|
-
show:
|
|
3763
|
-
(
|
|
3764
|
-
!readOnly &&
|
|
3765
|
-
!options[optionName]?.preselected &&
|
|
3766
|
-
!options[optionName]?.required
|
|
3767
|
-
) ?
|
|
3768
|
-
'hover'
|
|
3769
|
-
: false,
|
|
3770
|
-
onClick: () => {
|
|
3771
|
-
confirmAction({
|
|
3772
|
-
title: 'Remove Selected Option',
|
|
3773
|
-
onConfirm: () => {
|
|
3774
|
-
removeSelectedOption(optionName);
|
|
4380
|
+
}).filter((action) => action.show !== false),
|
|
4381
|
+
collapse: collapseOptionActions,
|
|
4382
|
+
menu: [
|
|
4383
|
+
!readOnly &&
|
|
4384
|
+
type !== 'code-editor' &&
|
|
4385
|
+
((options[optionName] as any)?.ui_type || options[optionName]?.type) !==
|
|
4386
|
+
'code-editor' && {
|
|
4387
|
+
label: 'Focused editing',
|
|
4388
|
+
icon: 'FullscreenLine',
|
|
4389
|
+
className: 'options-item-fullscreen',
|
|
4390
|
+
onClick: () => setFocusedEditing(optionName),
|
|
4391
|
+
},
|
|
4392
|
+
!readOnly &&
|
|
4393
|
+
hasOptionChanged((other as any).value, optionName) && {
|
|
4394
|
+
label: 'Revert changes',
|
|
4395
|
+
icon: 'HistoryLine',
|
|
4396
|
+
className: 'options-item-revert',
|
|
4397
|
+
onClick: () => {
|
|
4398
|
+
handleValueChange(
|
|
4399
|
+
optionName,
|
|
4400
|
+
originalValue.current?.[optionName]?.value,
|
|
4401
|
+
originalValue.current?.[optionName]?.type
|
|
4402
|
+
);
|
|
3775
4403
|
},
|
|
3776
|
-
}
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
4404
|
+
},
|
|
4405
|
+
!readOnly &&
|
|
4406
|
+
(other as any).value &&
|
|
4407
|
+
!isEqual((other as any).value, getDefaultValue(options[optionName])) &&
|
|
4408
|
+
!(options[optionName]?.disabled || (options[optionName] as any)?.readonly) && {
|
|
4409
|
+
label: 'Remove value',
|
|
4410
|
+
icon: 'CloseLine',
|
|
4411
|
+
className: 'options-item-remove',
|
|
4412
|
+
onClick: () => handleValueChange(optionName, undefined),
|
|
4413
|
+
},
|
|
4414
|
+
!readOnly &&
|
|
4415
|
+
!options[optionName]?.preselected &&
|
|
4416
|
+
!options[optionName]?.required && {
|
|
4417
|
+
label: 'Remove option',
|
|
4418
|
+
icon: 'DeleteBinLine',
|
|
4419
|
+
intent: 'danger',
|
|
4420
|
+
className: 'options-optional-remove',
|
|
4421
|
+
onClick: () => {
|
|
4422
|
+
confirmAction({
|
|
4423
|
+
title: 'Remove Selected Option',
|
|
4424
|
+
onConfirm: () => {
|
|
4425
|
+
removeSelectedOption(optionName);
|
|
4426
|
+
},
|
|
4427
|
+
});
|
|
4428
|
+
},
|
|
4429
|
+
},
|
|
4430
|
+
],
|
|
4431
|
+
}),
|
|
3780
4432
|
content: (
|
|
3781
4433
|
<FocusedEditing
|
|
3782
4434
|
isFullscreen={focusedEditing === optionName}
|