@qoretechnologies/reqraft 0.10.66-pr.152.g3349989 → 0.10.66
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/README.md +53 -1
- package/dist/components/Description/AllowedValuesHelp.d.ts +20 -0
- package/dist/components/Description/AllowedValuesHelp.d.ts.map +1 -0
- package/dist/components/Description/AllowedValuesHelp.js +86 -0
- package/dist/components/Description/AllowedValuesHelp.js.map +1 -0
- package/dist/components/Description/describedControl.d.ts +54 -0
- package/dist/components/Description/describedControl.d.ts.map +1 -0
- package/dist/components/Description/describedControl.js +81 -0
- package/dist/components/Description/describedControl.js.map +1 -0
- package/dist/components/Description/index.d.ts.map +1 -1
- package/dist/components/Description/index.js +3 -2
- package/dist/components/Description/index.js.map +1 -1
- package/dist/components/colorValue/colorValue.d.ts +59 -0
- package/dist/components/colorValue/colorValue.d.ts.map +1 -0
- package/dist/components/colorValue/colorValue.js +245 -0
- package/dist/components/colorValue/colorValue.js.map +1 -0
- package/dist/components/colorValue/index.d.ts +34 -0
- package/dist/components/colorValue/index.d.ts.map +1 -0
- package/dist/components/colorValue/index.js +136 -0
- package/dist/components/colorValue/index.js.map +1 -0
- package/dist/components/colorValue/labels.d.ts +12 -0
- package/dist/components/colorValue/labels.d.ts.map +1 -0
- package/dist/components/colorValue/labels.js +11 -0
- package/dist/components/colorValue/labels.js.map +1 -0
- package/dist/components/composer/Composer.d.ts +1 -1
- package/dist/components/composer/Composer.d.ts.map +1 -1
- package/dist/components/composer/Composer.js +2 -2
- 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 +156 -10
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlCompletions.d.ts +39 -0
- package/dist/components/dpqlEditor/dpqlCompletions.d.ts.map +1 -0
- package/dist/components/dpqlEditor/dpqlCompletions.js +294 -0
- package/dist/components/dpqlEditor/dpqlCompletions.js.map +1 -0
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts +54 -5
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.js +420 -63
- package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlInserter.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlInserter.js +67 -29
- package/dist/components/dpqlEditor/dpqlInserter.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlTags.d.ts +17 -7
- package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlTags.js +44 -17
- package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +94 -8
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/dpqlEditor/useDpqlSession.d.ts +7 -2
- package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
- package/dist/components/dpqlEditor/useDpqlSession.js +49 -17
- 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 +322 -124
- 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 +29 -3
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +66 -11
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +570 -176
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/HiddenFieldsNote.d.ts +28 -0
- package/dist/components/form/engine/HiddenFieldsNote.d.ts.map +1 -0
- package/dist/components/form/engine/HiddenFieldsNote.js +32 -0
- package/dist/components/form/engine/HiddenFieldsNote.js.map +1 -0
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +25 -3
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/OptionsHelpDialog.d.ts.map +1 -1
- package/dist/components/form/engine/OptionsHelpDialog.js +19 -2
- package/dist/components/form/engine/OptionsHelpDialog.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts +2 -2
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +12 -6
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +30 -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 +59 -22
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +102 -55
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarColors.d.ts +16 -0
- package/dist/components/form/engine/compactToolbarColors.d.ts.map +1 -0
- package/dist/components/form/engine/compactToolbarColors.js +21 -0
- package/dist/components/form/engine/compactToolbarColors.js.map +1 -0
- package/dist/components/form/engine/inheritProps.d.ts +29 -0
- package/dist/components/form/engine/inheritProps.d.ts.map +1 -0
- package/dist/components/form/engine/inheritProps.js +43 -0
- package/dist/components/form/engine/inheritProps.js.map +1 -0
- package/dist/components/form/engine/operatorSummary.d.ts +26 -0
- package/dist/components/form/engine/operatorSummary.d.ts.map +1 -0
- package/dist/components/form/engine/operatorSummary.js +35 -0
- package/dist/components/form/engine/operatorSummary.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +24 -8
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +77 -109
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/readFirstColors.d.ts +30 -0
- package/dist/components/form/engine/readFirstColors.d.ts.map +1 -0
- package/dist/components/form/engine/readFirstColors.js +51 -0
- package/dist/components/form/engine/readFirstColors.js.map +1 -0
- package/dist/components/form/engine/rowFocus.d.ts +2 -0
- package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
- package/dist/components/form/engine/rowFocus.js +26 -3
- package/dist/components/form/engine/rowFocus.js.map +1 -1
- package/dist/components/form/engine/typeChoices.d.ts +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.js +2 -1
- package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -1
- package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -1
- package/dist/components/form/engine/variants/VariantCards.js +2 -1
- package/dist/components/form/engine/variants/VariantCards.js.map +1 -1
- package/dist/components/form/engine/variants/VariantFocus.js +1 -1
- package/dist/components/form/engine/variants/VariantFocus.js.map +1 -1
- package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -1
- package/dist/components/form/engine/variants/VariantMinimal.js +2 -1
- package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -1
- package/dist/components/form/engine/variants/variantParts.d.ts +1 -1
- package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -1
- package/dist/components/form/engine/variants/variantParts.js +25 -17
- package/dist/components/form/engine/variants/variantParts.js.map +1 -1
- package/dist/components/form/expressions/DpqlRendering.d.ts +1 -1
- package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -1
- package/dist/components/form/expressions/DpqlRendering.js +7 -6
- package/dist/components/form/expressions/DpqlRendering.js.map +1 -1
- package/dist/components/form/expressions/ExpressionField.d.ts +67 -3
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +536 -114
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/argumentPresence.d.ts +14 -0
- package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -1
- package/dist/components/form/expressions/argumentPresence.js +16 -1
- package/dist/components/form/expressions/argumentPresence.js.map +1 -1
- package/dist/components/form/expressions/builder/argumentDetail.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/argumentDetail.js +4 -1
- package/dist/components/form/expressions/builder/argumentDetail.js.map +1 -1
- package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/argumentLabel.js +30 -11
- package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
- package/dist/components/form/expressions/builder/argumentWrapper.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/argumentWrapper.js +3 -1
- package/dist/components/form/expressions/builder/argumentWrapper.js.map +1 -1
- package/dist/components/form/expressions/builder/confirmMismatchedTypesModal.d.ts +9 -0
- package/dist/components/form/expressions/builder/confirmMismatchedTypesModal.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/confirmMismatchedTypesModal.js +30 -27
- package/dist/components/form/expressions/builder/confirmMismatchedTypesModal.js.map +1 -1
- package/dist/components/form/expressions/builder/confirmUnsupportedTypeModal.d.ts +1 -1
- package/dist/components/form/expressions/builder/confirmUnsupportedTypeModal.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/confirmUnsupportedTypeModal.js +7 -5
- package/dist/components/form/expressions/builder/confirmUnsupportedTypeModal.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts +34 -2
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +170 -80
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/builder/item.d.ts +6 -2
- package/dist/components/form/expressions/builder/item.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/item.js +19 -5
- package/dist/components/form/expressions/builder/item.js.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.js +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
- package/dist/components/form/expressions/dpqlMockRecordPath.d.ts +11 -0
- package/dist/components/form/expressions/dpqlMockRecordPath.d.ts.map +1 -0
- package/dist/components/form/expressions/dpqlMockRecordPath.js +254 -0
- package/dist/components/form/expressions/dpqlMockRecordPath.js.map +1 -0
- package/dist/components/form/expressions/expressionOperands.d.ts +16 -0
- package/dist/components/form/expressions/expressionOperands.d.ts.map +1 -0
- package/dist/components/form/expressions/expressionOperands.js +97 -0
- package/dist/components/form/expressions/expressionOperands.js.map +1 -0
- package/dist/components/form/expressions/fieldPath.d.ts +24 -0
- package/dist/components/form/expressions/fieldPath.d.ts.map +1 -0
- package/dist/components/form/expressions/fieldPath.js +152 -0
- package/dist/components/form/expressions/fieldPath.js.map +1 -0
- package/dist/components/form/expressions/loneValue.d.ts +17 -0
- package/dist/components/form/expressions/loneValue.d.ts.map +1 -0
- package/dist/components/form/expressions/loneValue.js +33 -0
- package/dist/components/form/expressions/loneValue.js.map +1 -0
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +22 -5
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +2 -0
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +43 -10
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/textOfExpression.d.ts +18 -0
- package/dist/components/form/expressions/textOfExpression.d.ts.map +1 -0
- package/dist/components/form/expressions/textOfExpression.js +49 -0
- package/dist/components/form/expressions/textOfExpression.js.map +1 -0
- package/dist/components/form/expressions/types.d.ts +12 -0
- package/dist/components/form/expressions/types.d.ts.map +1 -1
- package/dist/components/form/expressions/types.js +6 -1
- package/dist/components/form/expressions/types.js.map +1 -1
- package/dist/components/form/expressions/useExpressionType.d.ts +26 -0
- package/dist/components/form/expressions/useExpressionType.d.ts.map +1 -0
- package/dist/components/form/expressions/useExpressionType.js +181 -0
- package/dist/components/form/expressions/useExpressionType.js.map +1 -0
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +13 -9
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/expressions/wildcardExpressions.d.ts +27 -0
- package/dist/components/form/expressions/wildcardExpressions.d.ts.map +1 -0
- package/dist/components/form/expressions/wildcardExpressions.js +101 -0
- package/dist/components/form/expressions/wildcardExpressions.js.map +1 -0
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +43 -20
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +39 -6
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/allowed-values/choicePicture.d.ts +37 -0
- package/dist/components/form/fields/allowed-values/choicePicture.d.ts.map +1 -0
- package/dist/components/form/fields/allowed-values/choicePicture.js +64 -0
- package/dist/components/form/fields/allowed-values/choicePicture.js.map +1 -0
- package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +24 -0
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.js +36 -13
- package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts +30 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +119 -62
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/auto/hostEditorValue.d.ts +30 -0
- package/dist/components/form/fields/auto/hostEditorValue.d.ts.map +1 -0
- package/dist/components/form/fields/auto/hostEditorValue.js +53 -0
- package/dist/components/form/fields/auto/hostEditorValue.js.map +1 -0
- package/dist/components/form/fields/binary/Binary.d.ts.map +1 -1
- package/dist/components/form/fields/binary/Binary.js +23 -3
- package/dist/components/form/fields/binary/Binary.js.map +1 -1
- package/dist/components/form/fields/boolean/Boolean.d.ts +5 -0
- package/dist/components/form/fields/boolean/Boolean.d.ts.map +1 -1
- package/dist/components/form/fields/boolean/Boolean.js +9 -1
- package/dist/components/form/fields/boolean/Boolean.js.map +1 -1
- package/dist/components/form/fields/color/Color.d.ts +103 -16
- package/dist/components/form/fields/color/Color.d.ts.map +1 -1
- package/dist/components/form/fields/color/Color.js +616 -16
- package/dist/components/form/fields/color/Color.js.map +1 -1
- package/dist/components/form/fields/emittedValues.d.ts +34 -3
- package/dist/components/form/fields/emittedValues.d.ts.map +1 -1
- package/dist/components/form/fields/emittedValues.js +24 -1
- package/dist/components/form/fields/emittedValues.js.map +1 -1
- package/dist/components/form/fields/file/File.d.ts +1 -1
- package/dist/components/form/fields/file/File.d.ts.map +1 -1
- package/dist/components/form/fields/file/File.js +4 -2
- package/dist/components/form/fields/file/File.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 +29 -8
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/markdown/MarkdownView.js +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js +13 -9
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
- package/dist/components/form/fields/number/Number.d.ts +6 -0
- package/dist/components/form/fields/number/Number.d.ts.map +1 -1
- package/dist/components/form/fields/number/Number.js +41 -6
- package/dist/components/form/fields/number/Number.js.map +1 -1
- package/dist/components/form/fields/object/Object.d.ts +1 -1
- package/dist/components/form/fields/object/Object.d.ts.map +1 -1
- package/dist/components/form/fields/object/Object.js +4 -2
- package/dist/components/form/fields/object/Object.js.map +1 -1
- package/dist/components/form/fields/pendingEdit.d.ts +52 -0
- package/dist/components/form/fields/pendingEdit.d.ts.map +1 -0
- package/dist/components/form/fields/pendingEdit.js +79 -0
- package/dist/components/form/fields/pendingEdit.js.map +1 -0
- package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.js +4 -3
- package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +20 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +426 -91
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/rich-text/useTemplateTags.d.ts +18 -0
- package/dist/components/form/fields/rich-text/useTemplateTags.d.ts.map +1 -0
- package/dist/components/form/fields/rich-text/useTemplateTags.js +80 -0
- package/dist/components/form/fields/rich-text/useTemplateTags.js.map +1 -0
- package/dist/components/form/fields/schema-definition/CatalogLeafForm.d.ts.map +1 -1
- package/dist/components/form/fields/schema-definition/CatalogLeafForm.js +4 -1
- package/dist/components/form/fields/schema-definition/CatalogLeafForm.js.map +1 -1
- package/dist/components/form/fields/schema-definition/CatalogNodeEditor.d.ts.map +1 -1
- package/dist/components/form/fields/schema-definition/CatalogNodeEditor.js +20 -5
- package/dist/components/form/fields/schema-definition/CatalogNodeEditor.js.map +1 -1
- package/dist/components/form/fields/schema-definition/TableEditor.d.ts.map +1 -1
- package/dist/components/form/fields/schema-definition/TableEditor.js +10 -2
- package/dist/components/form/fields/schema-definition/TableEditor.js.map +1 -1
- package/dist/components/form/fields/schema-definition/index.d.ts.map +1 -1
- package/dist/components/form/fields/schema-definition/index.js +8 -4
- package/dist/components/form/fields/schema-definition/index.js.map +1 -1
- package/dist/components/form/fields/select/Select.d.ts +10 -1
- package/dist/components/form/fields/select/Select.d.ts.map +1 -1
- package/dist/components/form/fields/select/Select.js +28 -14
- package/dist/components/form/fields/select/Select.js.map +1 -1
- package/dist/components/form/fields/select/SelectCollection.d.ts +5 -0
- package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
- package/dist/components/form/fields/select/SelectCollection.js +25 -12
- package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
- package/dist/components/form/fields/select/unavailableRow.d.ts +23 -0
- package/dist/components/form/fields/select/unavailableRow.d.ts.map +1 -0
- package/dist/components/form/fields/select/unavailableRow.js +55 -0
- package/dist/components/form/fields/select/unavailableRow.js.map +1 -0
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +11 -5
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +21 -5
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateBrowser.d.ts +16 -0
- package/dist/components/form/fields/template/TemplateBrowser.d.ts.map +1 -0
- package/dist/components/form/fields/template/TemplateBrowser.js +89 -0
- package/dist/components/form/fields/template/TemplateBrowser.js.map +1 -0
- package/dist/components/form/fields/template/TemplateField.d.ts +105 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +678 -107
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/TemplatePicker.d.ts +54 -0
- package/dist/components/form/fields/template/TemplatePicker.d.ts.map +1 -0
- package/dist/components/form/fields/template/TemplatePicker.js +459 -0
- package/dist/components/form/fields/template/TemplatePicker.js.map +1 -0
- package/dist/components/form/fields/template/TemplateText.d.ts +0 -17
- package/dist/components/form/fields/template/TemplateText.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateText.js +22 -16
- package/dist/components/form/fields/template/TemplateText.js.map +1 -1
- package/dist/components/form/fields/template/referenceNames.d.ts +63 -0
- package/dist/components/form/fields/template/referenceNames.d.ts.map +1 -0
- package/dist/components/form/fields/template/referenceNames.js +69 -0
- package/dist/components/form/fields/template/referenceNames.js.map +1 -0
- package/dist/components/form/fields/template/writtenAsText.d.ts +61 -0
- package/dist/components/form/fields/template/writtenAsText.d.ts.map +1 -0
- package/dist/components/form/fields/template/writtenAsText.js +204 -0
- package/dist/components/form/fields/template/writtenAsText.js.map +1 -0
- package/dist/components/form/index.d.ts +12 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +25 -1
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/typeSentenceForm.d.ts +17 -0
- package/dist/components/form/typeSentenceForm.d.ts.map +1 -0
- package/dist/components/form/typeSentenceForm.js +54 -0
- package/dist/components/form/typeSentenceForm.js.map +1 -0
- package/dist/components/imageLightbox/ImageLightbox.d.ts.map +1 -1
- package/dist/components/imageLightbox/ImageLightbox.js +19 -7
- package/dist/components/imageLightbox/ImageLightbox.js.map +1 -1
- package/dist/components/log/Log.d.ts.map +1 -1
- package/dist/components/log/Log.js +20 -8
- package/dist/components/log/Log.js.map +1 -1
- package/dist/components/menu/Menu.d.ts.map +1 -1
- package/dist/components/menu/Menu.js +19 -10
- package/dist/components/menu/Menu.js.map +1 -1
- package/dist/components/smartEditor/CompletionMenu.d.ts +53 -0
- package/dist/components/smartEditor/CompletionMenu.d.ts.map +1 -0
- package/dist/components/smartEditor/CompletionMenu.js +302 -0
- package/dist/components/smartEditor/CompletionMenu.js.map +1 -0
- package/dist/components/smartEditor/MarkdownDoc.d.ts +1 -1
- package/dist/components/smartEditor/MarkdownDoc.js +3 -3
- package/dist/components/smartEditor/MarkdownDoc.js.map +1 -1
- package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
- package/dist/components/smartEditor/SmartEditor.js +616 -190
- package/dist/components/smartEditor/SmartEditor.js.map +1 -1
- package/dist/components/smartEditor/completionStyle.d.ts +28 -0
- package/dist/components/smartEditor/completionStyle.d.ts.map +1 -0
- package/dist/components/smartEditor/completionStyle.js +79 -0
- package/dist/components/smartEditor/completionStyle.js.map +1 -0
- package/dist/components/smartEditor/contextTitle.d.ts +10 -0
- package/dist/components/smartEditor/contextTitle.d.ts.map +1 -0
- package/dist/components/smartEditor/contextTitle.js +21 -0
- package/dist/components/smartEditor/contextTitle.js.map +1 -0
- package/dist/components/smartEditor/editingAids.d.ts +64 -0
- package/dist/components/smartEditor/editingAids.d.ts.map +1 -0
- package/dist/components/smartEditor/editingAids.js +213 -0
- package/dist/components/smartEditor/editingAids.js.map +1 -0
- package/dist/components/smartEditor/index.d.ts +6 -1
- package/dist/components/smartEditor/index.d.ts.map +1 -1
- package/dist/components/smartEditor/index.js +19 -1
- package/dist/components/smartEditor/index.js.map +1 -1
- package/dist/components/smartEditor/templateCompletions.d.ts +98 -0
- package/dist/components/smartEditor/templateCompletions.d.ts.map +1 -0
- package/dist/components/smartEditor/templateCompletions.js +380 -0
- package/dist/components/smartEditor/templateCompletions.js.map +1 -0
- package/dist/components/smartEditor/typedReferences.d.ts +34 -0
- package/dist/components/smartEditor/typedReferences.d.ts.map +1 -0
- package/dist/components/smartEditor/typedReferences.js +179 -0
- package/dist/components/smartEditor/typedReferences.js.map +1 -0
- package/dist/components/smartEditor/types.d.ts +116 -2
- package/dist/components/smartEditor/types.d.ts.map +1 -1
- package/dist/components/smartEditor/useEditingAids.d.ts +38 -0
- package/dist/components/smartEditor/useEditingAids.d.ts.map +1 -0
- package/dist/components/smartEditor/useEditingAids.js +392 -0
- package/dist/components/smartEditor/useEditingAids.js.map +1 -0
- package/dist/components/smartEditor/useLspAutocomplete.d.ts +106 -3
- package/dist/components/smartEditor/useLspAutocomplete.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspAutocomplete.js +592 -149
- package/dist/components/smartEditor/useLspAutocomplete.js.map +1 -1
- package/dist/components/smartEditor/useLspHover.d.ts +33 -7
- package/dist/components/smartEditor/useLspHover.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspHover.js +364 -35
- package/dist/components/smartEditor/useLspHover.js.map +1 -1
- package/dist/components/smartEditor/useLspSemanticTokens.js +1 -1
- package/dist/components/smartEditor/useLspSemanticTokens.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +6 -0
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +7 -6
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/components/smartEditor/useLspSignatureHelp.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSignatureHelp.js +6 -11
- package/dist/components/smartEditor/useLspSignatureHelp.js.map +1 -1
- package/dist/components/smartEditor/useTabCameIn.d.ts +10 -0
- package/dist/components/smartEditor/useTabCameIn.d.ts.map +1 -0
- package/dist/components/smartEditor/useTabCameIn.js +42 -0
- package/dist/components/smartEditor/useTabCameIn.js.map +1 -0
- package/dist/components/supportTicket/AttachmentChips.d.ts.map +1 -1
- package/dist/components/supportTicket/AttachmentChips.js +15 -5
- package/dist/components/supportTicket/AttachmentChips.js.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.d.ts +9 -2
- package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.js +27 -6
- package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
- package/dist/components/supportTicket/TicketHeader.d.ts +3 -4
- package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketHeader.js +20 -2
- package/dist/components/supportTicket/TicketHeader.js.map +1 -1
- package/dist/components/supportTicket/TicketMetaTags.d.ts +1 -1
- package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -1
- package/dist/components/ticketReferences/TicketReferences.js +9 -4
- package/dist/components/ticketReferences/TicketReferences.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +38 -13
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/dist/helpers/allowedValues.d.ts +21 -0
- package/dist/helpers/allowedValues.d.ts.map +1 -1
- package/dist/helpers/allowedValues.js +40 -1
- package/dist/helpers/allowedValues.js.map +1 -1
- package/dist/helpers/descriptionTooltip.d.ts +46 -0
- package/dist/helpers/descriptionTooltip.d.ts.map +1 -0
- package/dist/helpers/descriptionTooltip.js +76 -0
- package/dist/helpers/descriptionTooltip.js.map +1 -0
- package/dist/helpers/domProps.d.ts +5 -0
- package/dist/helpers/domProps.d.ts.map +1 -0
- package/dist/helpers/domProps.js +47 -0
- package/dist/helpers/domProps.js.map +1 -0
- package/dist/helpers/expressions.d.ts +115 -4
- package/dist/helpers/expressions.d.ts.map +1 -1
- package/dist/helpers/expressions.js +247 -29
- package/dist/helpers/expressions.js.map +1 -1
- package/dist/helpers/hoveringPointer.d.ts +26 -0
- package/dist/helpers/hoveringPointer.d.ts.map +1 -0
- package/dist/helpers/hoveringPointer.js +29 -0
- package/dist/helpers/hoveringPointer.js.map +1 -0
- package/dist/helpers/markdownText.d.ts +14 -0
- package/dist/helpers/markdownText.d.ts.map +1 -0
- package/dist/helpers/markdownText.js +160 -0
- package/dist/helpers/markdownText.js.map +1 -0
- package/dist/helpers/noDescription.d.ts +9 -0
- package/dist/helpers/noDescription.d.ts.map +1 -0
- package/dist/helpers/noDescription.js +13 -0
- package/dist/helpers/noDescription.js.map +1 -0
- package/dist/helpers/optionUiTypes.d.ts +2 -2
- package/dist/helpers/qorusTypeCatalogue.d.ts +147 -0
- package/dist/helpers/qorusTypeCatalogue.d.ts.map +1 -0
- package/dist/helpers/qorusTypeCatalogue.js +505 -0
- package/dist/helpers/qorusTypeCatalogue.js.map +1 -0
- package/dist/helpers/readableColors.d.ts +36 -0
- package/dist/helpers/readableColors.d.ts.map +1 -0
- package/dist/helpers/readableColors.js +116 -0
- package/dist/helpers/readableColors.js.map +1 -0
- package/dist/helpers/recordFieldReference.d.ts +15 -0
- package/dist/helpers/recordFieldReference.d.ts.map +1 -0
- package/dist/helpers/recordFieldReference.js +89 -0
- package/dist/helpers/recordFieldReference.js.map +1 -0
- package/dist/helpers/templateCatalogue.d.ts +104 -0
- package/dist/helpers/templateCatalogue.d.ts.map +1 -0
- package/dist/helpers/templateCatalogue.js +116 -0
- package/dist/helpers/templateCatalogue.js.map +1 -0
- package/dist/helpers/templateCatalogueContext.d.ts +19 -0
- package/dist/helpers/templateCatalogueContext.d.ts.map +1 -0
- package/dist/helpers/templateCatalogueContext.js +31 -0
- package/dist/helpers/templateCatalogueContext.js.map +1 -0
- package/dist/helpers/templateItems.d.ts +2 -2
- package/dist/helpers/templateItems.d.ts.map +1 -1
- package/dist/helpers/templateItems.js +6 -4
- package/dist/helpers/templateItems.js.map +1 -1
- package/dist/helpers/templateText.d.ts +1 -0
- package/dist/helpers/templateText.d.ts.map +1 -1
- package/dist/helpers/templateText.js +7 -3
- package/dist/helpers/templateText.js.map +1 -1
- package/dist/helpers/templateTree.d.ts +248 -0
- package/dist/helpers/templateTree.d.ts.map +1 -0
- package/dist/helpers/templateTree.js +892 -0
- package/dist/helpers/templateTree.js.map +1 -0
- package/dist/helpers/templateType.d.ts +28 -0
- package/dist/helpers/templateType.d.ts.map +1 -0
- package/dist/helpers/templateType.js +30 -0
- package/dist/helpers/templateType.js.map +1 -0
- package/dist/helpers/templates.d.ts +86 -16
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +305 -45
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +27 -0
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +169 -39
- package/dist/helpers/validations.js.map +1 -1
- package/dist/helpers/webLink.d.ts +8 -0
- package/dist/helpers/webLink.d.ts.map +1 -0
- package/dist/helpers/webLink.js +29 -0
- package/dist/helpers/webLink.js.map +1 -0
- package/dist/hooks/useQorusTypes.d.ts +12 -17
- package/dist/hooks/useQorusTypes.d.ts.map +1 -1
- package/dist/hooks/useQorusTypes.js +44 -172
- package/dist/hooks/useQorusTypes.js.map +1 -1
- package/dist/hooks/useReadableIntentShade.d.ts +9 -0
- package/dist/hooks/useReadableIntentShade.d.ts.map +1 -0
- package/dist/hooks/useReadableIntentShade.js +23 -0
- package/dist/hooks/useReadableIntentShade.js.map +1 -0
- package/dist/hooks/useReadableOpacity.d.ts +6 -0
- package/dist/hooks/useReadableOpacity.d.ts.map +1 -0
- package/dist/hooks/useReadableOpacity.js +17 -0
- package/dist/hooks/useReadableOpacity.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/tracking/ui/ExperimentCard.d.ts +1 -1
- package/dist/tracking/ui/ExperimentCard.d.ts.map +1 -1
- package/dist/tracking/ui/ExperimentCard.js +26 -10
- package/dist/tracking/ui/ExperimentCard.js.map +1 -1
- package/dist/tracking/ui/card.d.ts +2 -2
- package/dist/tracking/ui/card.d.ts.map +1 -1
- package/dist/tracking/ui/card.js +2 -2
- package/dist/tracking/ui/card.js.map +1 -1
- package/dist/tracking/ui/readable.d.ts +14 -0
- package/dist/tracking/ui/readable.d.ts.map +1 -0
- package/dist/tracking/ui/readable.js +60 -0
- package/dist/tracking/ui/readable.js.map +1 -0
- package/dist/utils/fetch.d.ts +9 -2
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +6 -5
- package/dist/utils/fetch.js.map +1 -1
- package/dist/utils/lspClient.types.d.ts +2 -0
- package/dist/utils/lspClient.types.d.ts.map +1 -1
- package/dist/utils/websocket.d.ts +65 -2
- package/dist/utils/websocket.d.ts.map +1 -1
- package/dist/utils/websocket.js +276 -44
- package/dist/utils/websocket.js.map +1 -1
- package/package.json +14 -6
- package/.claude/CLAUDE.md +0 -341
- package/.tasks/EXPORT_EXPRESSION_BUILDER.md +0 -38
- package/.tasks/EXPRESSION_BUILDER_REPORT.md +0 -149
- package/.tasks/EXPRESSION_FIELD_SEAMS.md +0 -66
- package/.tasks/FIELD_STACK_REPORT.md +0 -234
- package/.tasks/FORM_ENGINE_COMPACT_READ_FIRST.md +0 -558
- package/.tasks/FORM_ENGINE_EXPRESSIONS.md +0 -197
- package/.tasks/FORM_ENGINE_OPTIONS_SYNC.md +0 -95
- package/.tasks/FORM_FIELD_EXTRAS.md +0 -72
- package/.tasks/INDEX.md +0 -111
- package/.tasks/LSP_SHARED_CONNECTION.md +0 -75
- package/.tasks/QONSOLE_ASSIST_FEATURES.md +0 -306
- package/.tasks/RENDER_EXPRESSION_LSP.md +0 -159
- package/.tasks/REQRAFT_AUTO_FIELD.md +0 -176
- package/.tasks/SCHEMA_DEFINITION_FIELD.md +0 -250
- package/.tasks/SMART_EDITOR_CONTEXT_AND_POLISH.md +0 -340
- package/.tasks/SMART_EDITOR_LSP_FEATURES.md +0 -299
- package/.tasks/SMART_EDITOR_REVIEW_FOLLOWUPS.md +0 -280
- package/.tasks/SMART_EDITOR_UX_POLISH.md +0 -383
- package/.tasks/SMART_EDITOR_VISUAL_POLISH.md +0 -246
- package/.tasks/STORY_NETWORK_ISOLATION.md +0 -98
- package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +0 -72
- package/CODE_OF_CONDUCT.md +0 -105
- package/CONTRIBUTING.MD +0 -78
- package/SECURITY.md +0 -3
- package/chromatic.config.json +0 -5
- package/design/COMPACT_ENGINE_REDESIGN.md +0 -156
- package/design/COMPACT_INFO_DISPLAY.md +0 -87
- package/design/COMPACT_PARITY.md +0 -85
- package/design/EXPRESSION_BUILDER_REPORT_STRATEGY.md +0 -92
- package/design/FORM_ENGINE_COMPACT.md +0 -300
- package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +0 -353
- package/design/FORM_ENGINE_EXPRESSIONS.md +0 -257
- package/design/FORM_ENGINE_FIELD_MIGRATION.md +0 -419
- package/design/IDE_INTEGRATION.md +0 -64
- package/design/RENDER_EXPRESSION_TRANSPORT.md +0 -156
- package/design/SELECT_DIALOG_FLOOR.md +0 -86
- package/design/SMART_EDITOR_UX.md +0 -438
- package/design/TRACKING.md +0 -290
- package/design/UNAVAILABLE_ALLOWED_VALUES.md +0 -226
- package/src/components/Description/index.tsx +0 -254
- package/src/components/Description/markdownRendererContext.tsx +0 -29
- package/src/components/FocusedEditing/index.tsx +0 -50
- package/src/components/codeSize/index.tsx +0 -79
- package/src/components/composer/Composer.stories.tsx +0 -238
- package/src/components/composer/Composer.tsx +0 -554
- package/src/components/composer/helpers.ts +0 -74
- package/src/components/composer/index.ts +0 -2
- package/src/components/dpqlEditor/DpqlEditor.stories.tsx +0 -1183
- package/src/components/dpqlEditor/DpqlEditor.tsx +0 -219
- package/src/components/dpqlEditor/dpqlHelpers.ts +0 -287
- package/src/components/dpqlEditor/dpqlInserter.ts +0 -121
- package/src/components/dpqlEditor/dpqlProbe.ts +0 -165
- package/src/components/dpqlEditor/dpqlTags.ts +0 -114
- package/src/components/dpqlEditor/index.ts +0 -41
- package/src/components/dpqlEditor/types.ts +0 -203
- package/src/components/dpqlEditor/useDpqlProbe.ts +0 -33
- package/src/components/dpqlEditor/useDpqlSession.ts +0 -411
- package/src/components/form/engine/CompactRow.tsx +0 -2620
- package/src/components/form/engine/CompactToolbar.tsx +0 -330
- package/src/components/form/engine/FormEngine.stories.tsx +0 -10091
- package/src/components/form/engine/FormEngine.tsx +0 -4569
- package/src/components/form/engine/FormEngineRemote.stories.tsx +0 -688
- package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +0 -334
- package/src/components/form/engine/FormFieldsSkeleton.tsx +0 -82
- package/src/components/form/engine/OptionFieldMessages.tsx +0 -477
- package/src/components/form/engine/OptionsHelpDialog.tsx +0 -29
- package/src/components/form/engine/__fixtures__/basicFormOptions.ts +0 -309
- package/src/components/form/engine/__fixtures__/fieldChromeOptions.ts +0 -45
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +0 -242
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +0 -742
- package/src/components/form/engine/_structuredData/StructuredDataView.stories.tsx +0 -85
- package/src/components/form/engine/_structuredData/StructuredDataView.tsx +0 -75
- package/src/components/form/engine/_structuredData/structuredData.ts +0 -145
- package/src/components/form/engine/compactRowContext.ts +0 -148
- package/src/components/form/engine/compactRowStyles.ts +0 -898
- package/src/components/form/engine/compactToolbarContext.ts +0 -69
- package/src/components/form/engine/optionActions.ts +0 -128
- package/src/components/form/engine/optionsContext.ts +0 -24
- package/src/components/form/engine/readFirst.ts +0 -1077
- package/src/components/form/engine/rendererTypes.ts +0 -107
- package/src/components/form/engine/revealRow.ts +0 -123
- package/src/components/form/engine/rowFocus.ts +0 -101
- package/src/components/form/engine/rowMenuContext.ts +0 -159
- package/src/components/form/engine/rowOpenPicker.ts +0 -27
- package/src/components/form/engine/typeChoices.ts +0 -63
- package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +0 -159
- package/src/components/form/engine/variants/VariantCalmTable.tsx +0 -244
- package/src/components/form/engine/variants/VariantCards.tsx +0 -212
- package/src/components/form/engine/variants/VariantFocus.tsx +0 -382
- package/src/components/form/engine/variants/VariantMinimal.tsx +0 -170
- package/src/components/form/engine/variants/focusDemo.ts +0 -145
- package/src/components/form/engine/variants/variantModel.ts +0 -223
- package/src/components/form/engine/variants/variantParts.tsx +0 -313
- package/src/components/form/expressions/DpqlRendering.tsx +0 -85
- package/src/components/form/expressions/ExpressionField.stories.tsx +0 -1395
- package/src/components/form/expressions/ExpressionField.tsx +0 -503
- package/src/components/form/expressions/argumentPresence.ts +0 -213
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +0 -1747
- package/src/components/form/expressions/builder/argumentDetail.tsx +0 -17
- package/src/components/form/expressions/builder/argumentLabel.tsx +0 -146
- package/src/components/form/expressions/builder/argumentSlot.tsx +0 -101
- package/src/components/form/expressions/builder/argumentWrapper.tsx +0 -243
- package/src/components/form/expressions/builder/confirmMismatchedTypesModal.tsx +0 -120
- package/src/components/form/expressions/builder/confirmUnsupportedTypeModal.tsx +0 -69
- package/src/components/form/expressions/builder/index.tsx +0 -1545
- package/src/components/form/expressions/builder/item.tsx +0 -89
- package/src/components/form/expressions/builder/moveToPositionStrip.tsx +0 -156
- package/src/components/form/expressions/builder/renderTemplate.tsx +0 -42
- package/src/components/form/expressions/dpqlMockLanguage.ts +0 -566
- package/src/components/form/expressions/dpqlMockLsp.ts +0 -126
- package/src/components/form/expressions/mockExpressions.ts +0 -317
- package/src/components/form/expressions/renderExpressionToText.ts +0 -137
- package/src/components/form/expressions/types.ts +0 -110
- package/src/components/form/expressions/useExpressions.ts +0 -101
- package/src/components/form/expressions/useRenderExpression.ts +0 -165
- package/src/components/form/fields/Field.stories.tsx +0 -143
- package/src/components/form/fields/Field.tsx +0 -732
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +0 -297
- package/src/components/form/fields/array/ArrayAuto.tsx +0 -237
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +0 -484
- package/src/components/form/fields/array/ArrayAutoField.tsx +0 -367
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +0 -825
- package/src/components/form/fields/auto/AutoFormField.tsx +0 -1164
- package/src/components/form/fields/binary/Binary.stories.tsx +0 -128
- package/src/components/form/fields/binary/Binary.tsx +0 -91
- package/src/components/form/fields/boolean/Boolean.stories.tsx +0 -128
- package/src/components/form/fields/boolean/Boolean.tsx +0 -34
- package/src/components/form/fields/byte-size/ByteSize.stories.tsx +0 -87
- package/src/components/form/fields/byte-size/ByteSize.tsx +0 -58
- package/src/components/form/fields/color/Color.stories.tsx +0 -108
- package/src/components/form/fields/color/Color.tsx +0 -46
- package/src/components/form/fields/cron/Cron.stories.tsx +0 -68
- package/src/components/form/fields/cron/Cron.tsx +0 -80
- package/src/components/form/fields/date/Date.stories.tsx +0 -80
- package/src/components/form/fields/date/Date.tsx +0 -55
- package/src/components/form/fields/emittedValues.ts +0 -70
- package/src/components/form/fields/file/File.stories.tsx +0 -167
- package/src/components/form/fields/file/File.tsx +0 -227
- package/src/components/form/fields/long-string/LongString.stories.tsx +0 -123
- package/src/components/form/fields/long-string/LongString.tsx +0 -130
- package/src/components/form/fields/markdown/Markdown.stories.tsx +0 -76
- package/src/components/form/fields/markdown/Markdown.tsx +0 -81
- package/src/components/form/fields/markdown/MarkdownView.tsx +0 -222
- package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +0 -139
- package/src/components/form/fields/number/Number.stories.tsx +0 -165
- package/src/components/form/fields/number/Number.tsx +0 -160
- package/src/components/form/fields/object/Object.stories.tsx +0 -410
- package/src/components/form/fields/object/Object.tsx +0 -255
- package/src/components/form/fields/radio-group/RadioGroup.stories.tsx +0 -95
- package/src/components/form/fields/radio-group/RadioGroup.tsx +0 -46
- package/src/components/form/fields/radio-group/images/java-96x128.png +0 -0
- package/src/components/form/fields/radio-group/images/python-129x128.png +0 -0
- package/src/components/form/fields/radio-group/images/qore-106x128.png +0 -0
- package/src/components/form/fields/rich-text/RichText.stories.tsx +0 -260
- package/src/components/form/fields/rich-text/RichText.tsx +0 -537
- package/src/components/form/fields/schema-definition/CatalogLeafForm.tsx +0 -242
- package/src/components/form/fields/schema-definition/CatalogNodeEditor.tsx +0 -758
- package/src/components/form/fields/schema-definition/Hint.tsx +0 -22
- package/src/components/form/fields/schema-definition/InlineValidationBanner.tsx +0 -138
- package/src/components/form/fields/schema-definition/Loader.tsx +0 -20
- package/src/components/form/fields/schema-definition/MigrationsTab.tsx +0 -187
- package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +0 -292
- package/src/components/form/fields/schema-definition/TableEditor.tsx +0 -155
- package/src/components/form/fields/schema-definition/TablesTab.tsx +0 -138
- package/src/components/form/fields/schema-definition/catalog.ts +0 -237
- package/src/components/form/fields/schema-definition/helpers.ts +0 -90
- package/src/components/form/fields/schema-definition/index.tsx +0 -418
- package/src/components/form/fields/schema-definition/mockCatalog.ts +0 -325
- package/src/components/form/fields/schema-definition/mockDefinition.ts +0 -111
- package/src/components/form/fields/schema-definition/referenceSuggestions.tsx +0 -152
- package/src/components/form/fields/schema-definition/starterTemplate.ts +0 -68
- package/src/components/form/fields/schema-definition/types.ts +0 -185
- package/src/components/form/fields/schema-definition/useSchemaOptionCatalog.ts +0 -49
- package/src/components/form/fields/select/Select.stories.tsx +0 -890
- package/src/components/form/fields/select/Select.tsx +0 -624
- package/src/components/form/fields/select/SelectCollection.tsx +0 -594
- package/src/components/form/fields/string/String.stories.tsx +0 -86
- package/src/components/form/fields/string/String.tsx +0 -43
- package/src/components/form/fields/template/ExampleValueModal.stories.tsx +0 -114
- package/src/components/form/fields/template/ExampleValueModal.tsx +0 -99
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +0 -47
- package/src/components/form/fields/template/TemplateField.stories.tsx +0 -1121
- package/src/components/form/fields/template/TemplateField.tsx +0 -1717
- package/src/components/form/fields/template/TemplateText.tsx +0 -115
- package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +0 -7
- package/src/components/form/fields/template/__fixtures__/multiLevelTemplates.json +0 -147
- package/src/components/form/fields/template/__fixtures__/templates.json +0 -83
- package/src/components/form/fields/timeout/Timeout.stories.tsx +0 -246
- package/src/components/form/fields/timeout/Timeout.tsx +0 -87
- package/src/components/form/fields/url/Url.stories.tsx +0 -97
- package/src/components/form/fields/url/Url.tsx +0 -85
- package/src/components/form/index.tsx +0 -47
- package/src/components/imageLightbox/ImageLightbox.stories.tsx +0 -241
- package/src/components/imageLightbox/ImageLightbox.tsx +0 -236
- package/src/components/imageLightbox/index.ts +0 -1
- package/src/components/log/Log.stories.tsx +0 -348
- package/src/components/log/Log.tsx +0 -285
- package/src/components/menu/Menu.stories.tsx +0 -158
- package/src/components/menu/Menu.tsx +0 -312
- package/src/components/qonsoleSmartInput/QonsoleSmartInput.stories.tsx +0 -797
- package/src/components/qonsoleSmartInput/QonsoleSmartInput.tsx +0 -105
- package/src/components/qonsoleSmartInput/index.ts +0 -16
- package/src/components/qonsoleSmartInput/qonsoleInserter.ts +0 -77
- package/src/components/qonsoleSmartInput/types.ts +0 -133
- package/src/components/qonsoleSmartInput/useQonsoleSession.ts +0 -124
- package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +0 -62
- package/src/components/searchFilterBar/SearchFilterBar.tsx +0 -144
- package/src/components/searchFilterBar/index.ts +0 -1
- package/src/components/smartEditor/MarkdownDoc.tsx +0 -190
- package/src/components/smartEditor/SmartEditor.stories.tsx +0 -475
- package/src/components/smartEditor/SmartEditor.tsx +0 -808
- package/src/components/smartEditor/__fixtures__/mockLspServer.ts +0 -205
- package/src/components/smartEditor/helpers.ts +0 -217
- package/src/components/smartEditor/index.ts +0 -35
- package/src/components/smartEditor/styling.ts +0 -45
- package/src/components/smartEditor/types.ts +0 -211
- package/src/components/smartEditor/useLspAutocomplete.ts +0 -758
- package/src/components/smartEditor/useLspDiagnosticDecorations.ts +0 -125
- package/src/components/smartEditor/useLspHover.ts +0 -230
- package/src/components/smartEditor/useLspSemanticTokens.ts +0 -298
- package/src/components/smartEditor/useLspSession.ts +0 -231
- package/src/components/smartEditor/useLspSignatureHelp.ts +0 -198
- package/src/components/supportTicket/AttachmentChips.stories.tsx +0 -174
- package/src/components/supportTicket/AttachmentChips.tsx +0 -147
- package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +0 -260
- package/src/components/supportTicket/InterfaceReferenceTags.tsx +0 -129
- package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -590
- package/src/components/supportTicket/ReferencePicker.tsx +0 -563
- package/src/components/supportTicket/TicketConversation.stories.tsx +0 -121
- package/src/components/supportTicket/TicketHeader.stories.tsx +0 -186
- package/src/components/supportTicket/TicketHeader.tsx +0 -247
- package/src/components/supportTicket/TicketMetaTags.stories.tsx +0 -109
- package/src/components/supportTicket/TicketMetaTags.tsx +0 -85
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +0 -357
- package/src/components/supportTicket/TicketReplyBox.tsx +0 -150
- package/src/components/supportTicket/__fixtures__/kindMarks.ts +0 -28
- package/src/components/supportTicket/attachmentAccess.ts +0 -97
- package/src/components/supportTicket/fileToAttachment.ts +0 -24
- package/src/components/supportTicket/index.ts +0 -10
- package/src/components/supportTicket/meta.ts +0 -206
- package/src/components/supportTicket/ticketModel.ts +0 -131
- package/src/components/supportTicket/useLoadedImages.ts +0 -73
- package/src/components/ticketReferences/TicketReferences.stories.tsx +0 -318
- package/src/components/ticketReferences/TicketReferences.tsx +0 -906
- package/src/components/ticketReferences/index.ts +0 -2
- package/src/components/ticketThread/TicketThread.stories.tsx +0 -844
- package/src/components/ticketThread/TicketThread.tsx +0 -719
- package/src/components/ticketThread/index.ts +0 -6
- package/src/contexts/FetchContext.tsx +0 -25
- package/src/contexts/ReqraftContext.tsx +0 -9
- package/src/contexts/StorageContext.tsx +0 -33
- package/src/global.d.ts +0 -4
- package/src/helpers/allowedValues.ts +0 -249
- package/src/helpers/choiceChip.ts +0 -227
- package/src/helpers/common.ts +0 -326
- package/src/helpers/dpqlDetection.ts +0 -209
- package/src/helpers/expressions.ts +0 -140
- package/src/helpers/optionUiTypes.ts +0 -199
- package/src/helpers/options.ts +0 -150
- package/src/helpers/providerValue.ts +0 -125
- package/src/helpers/scrollbar.ts +0 -32
- package/src/helpers/singleLineString.ts +0 -61
- package/src/helpers/templateItems.ts +0 -81
- package/src/helpers/templateText.ts +0 -350
- package/src/helpers/templates.ts +0 -602
- package/src/helpers/validations.ts +0 -2085
- package/src/hooks/useArgSchema.ts +0 -37
- package/src/hooks/useCoarsePointer.ts +0 -30
- package/src/hooks/useFetch/useFetch.stories.tsx +0 -388
- package/src/hooks/useFetch/useFetch.tsx +0 -154
- package/src/hooks/usePhoneViewport.ts +0 -34
- package/src/hooks/useQorusTypes.ts +0 -233
- package/src/hooks/useReqraftProperty.ts +0 -16
- package/src/hooks/useStorage/useStorage.stories.tsx +0 -177
- package/src/hooks/useStorage/useStorage.ts +0 -35
- package/src/hooks/useTemplates.ts +0 -123
- package/src/hooks/useValidation.ts +0 -9
- package/src/hooks/useWebSocket/useWebSocket.ts +0 -224
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +0 -675
- package/src/hooks/useWhyDidYouUpdate.ts +0 -38
- package/src/index.tsx +0 -86
- package/src/providers/FetchProvider.tsx +0 -40
- package/src/providers/ReqraftProvider.tsx +0 -52
- package/src/providers/StorageProvider.tsx +0 -131
- package/src/stores/currentUser/currentUser.stories.tsx +0 -100
- package/src/stores/currentUser/currentUser.tsx +0 -94
- package/src/stories/Data/fileFieldArgSchema.json +0 -86
- package/src/stories/Tests/utils.ts +0 -320
- package/src/stories/storyNetwork.ts +0 -92
- package/src/stories/tracking.tsx +0 -206
- package/src/tracking/browser/attach.ts +0 -186
- package/src/tracking/browser/env.ts +0 -78
- package/src/tracking/core/consent.ts +0 -37
- package/src/tracking/core/engagement.ts +0 -47
- package/src/tracking/core/experiments.ts +0 -258
- package/src/tracking/core/hash.ts +0 -26
- package/src/tracking/core/ids.ts +0 -25
- package/src/tracking/core/observable.ts +0 -23
- package/src/tracking/core/queue.ts +0 -131
- package/src/tracking/core/sections.ts +0 -166
- package/src/tracking/core/storage.ts +0 -69
- package/src/tracking/core/tracker.ts +0 -417
- package/src/tracking/core/types.ts +0 -162
- package/src/tracking/core/utm.ts +0 -23
- package/src/tracking/index.ts +0 -72
- package/src/tracking/markup.ts +0 -17
- package/src/tracking/react/Experiment.tsx +0 -44
- package/src/tracking/react/TrackingProvider.tsx +0 -56
- package/src/tracking/react/hooks.ts +0 -106
- package/src/tracking/react/index.ts +0 -13
- package/src/tracking/testing.ts +0 -108
- package/src/tracking/ui/ExperimentCard.stories.tsx +0 -327
- package/src/tracking/ui/ExperimentCard.tsx +0 -667
- package/src/tracking/ui/ExperimentsOverlay.stories.tsx +0 -157
- package/src/tracking/ui/ExperimentsOverlay.tsx +0 -437
- package/src/tracking/ui/anchor.ts +0 -180
- package/src/tracking/ui/card.ts +0 -464
- package/src/tracking/ui/client.ts +0 -117
- package/src/tracking/ui/index.ts +0 -77
- package/src/tracking/ui/types.ts +0 -110
- package/src/tracking/ui/useExperimentsAdmin.ts +0 -103
- package/src/types/Form.ts +0 -29
- package/src/types.ts +0 -14
- package/src/utils/datetime.ts +0 -10
- package/src/utils/fetch.ts +0 -457
- package/src/utils/lspClient.ts +0 -711
- package/src/utils/lspClient.types.ts +0 -211
- package/src/utils/queryUnlessCancelled.ts +0 -28
- package/src/utils/websocket.ts +0 -293
- package/vite.config.ts +0 -58
- package/vitest.config.ts +0 -69
|
@@ -1,4569 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ReqoreButton,
|
|
3
|
-
ReqoreCollection,
|
|
4
|
-
ReqoreControlGroup,
|
|
5
|
-
ReqoreErrorBoundary,
|
|
6
|
-
ReqoreIcon,
|
|
7
|
-
ReqoreMessage,
|
|
8
|
-
ReqoreP,
|
|
9
|
-
ReqoreTag,
|
|
10
|
-
ReqoreTagGroup,
|
|
11
|
-
ReqoreVerticalSpacer,
|
|
12
|
-
useReqoreProperty,
|
|
13
|
-
useReqoreTheme,
|
|
14
|
-
} from '@qoretechnologies/reqore';
|
|
15
|
-
import { IReqoreCollectionProps } from '@qoretechnologies/reqore/dist/components/Collection';
|
|
16
|
-
import { IReqoreCollectionItemProps } from '@qoretechnologies/reqore/dist/components/Collection/item';
|
|
17
|
-
import { IReqorePanelProps } from '@qoretechnologies/reqore/dist/components/Panel';
|
|
18
|
-
import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
19
|
-
import { TFieldWithOwnTemplates } from './rendererTypes';
|
|
20
|
-
import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
|
|
21
|
-
import {
|
|
22
|
-
changeDarkness,
|
|
23
|
-
getMainBackgroundColor,
|
|
24
|
-
getReadableColor,
|
|
25
|
-
percentToHexAlpha,
|
|
26
|
-
} from '@qoretechnologies/reqore/dist/helpers/colors';
|
|
27
|
-
import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
|
|
28
|
-
import {
|
|
29
|
-
IQorusFormField,
|
|
30
|
-
IQorusFormFieldMessage,
|
|
31
|
-
IQorusFormFieldOnChangeMeta,
|
|
32
|
-
IQorusFormOperator,
|
|
33
|
-
IQorusFormOperatorsSchema,
|
|
34
|
-
IQorusFormSchema,
|
|
35
|
-
TQorusFlatForm,
|
|
36
|
-
TQorusForm,
|
|
37
|
-
TQorusFormFieldSchema,
|
|
38
|
-
TQorusFormOperatorValue,
|
|
39
|
-
TQorusType,
|
|
40
|
-
} from '@qoretechnologies/ts-toolkit';
|
|
41
|
-
import { shouldMarkAsExpression } from '../expressions/argumentPresence';
|
|
42
|
-
import { offersTypeChoices } from './typeChoices';
|
|
43
|
-
import { optionRowActions, resolveOptionActions, TOptionActions } from './optionActions';
|
|
44
|
-
import { createRendererOnlyUiTypeCheck, isRendererOnlyUiType } from './rendererTypes';
|
|
45
|
-
import { revealRow } from './revealRow';
|
|
46
|
-
import { useEmittedValues } from '../fields/emittedValues';
|
|
47
|
-
import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last, uniq } from 'lodash';
|
|
48
|
-
import map from 'lodash/map';
|
|
49
|
-
import reduce from 'lodash/reduce';
|
|
50
|
-
import size from 'lodash/size';
|
|
51
|
-
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
52
|
-
import { useMeasure, useMount, useUpdateEffect } from 'react-use';
|
|
53
|
-
import styled, { css } from 'styled-components';
|
|
54
|
-
import {
|
|
55
|
-
fixOperatorValue,
|
|
56
|
-
getDefaultValue,
|
|
57
|
-
insertAtIndex,
|
|
58
|
-
richtextToString,
|
|
59
|
-
} from '../../../helpers/common';
|
|
60
|
-
import {
|
|
61
|
-
getRequiredOptionMessage,
|
|
62
|
-
resolveDegenerateRequiredGroups,
|
|
63
|
-
} from '../../../helpers/options';
|
|
64
|
-
import {
|
|
65
|
-
IValidationResult,
|
|
66
|
-
hasAllDependenciesFullfilled,
|
|
67
|
-
parseDependency,
|
|
68
|
-
validateField,
|
|
69
|
-
validateFieldWithResult,
|
|
70
|
-
} from '../../../helpers/validations';
|
|
71
|
-
import { useQorusTypes } from '../../../hooks/useQorusTypes';
|
|
72
|
-
import { useTemplates } from '../../../hooks/useTemplates';
|
|
73
|
-
import { isQueryCancelled } from '../../../utils/fetch';
|
|
74
|
-
import { queryUnlessCancelled } from '../../../utils/queryUnlessCancelled';
|
|
75
|
-
import { Description } from '../../Description';
|
|
76
|
-
import { FocusedEditing } from '../../FocusedEditing';
|
|
77
|
-
import { SelectFormField } from '../fields/select/Select';
|
|
78
|
-
import {
|
|
79
|
-
ITemplateFieldProps,
|
|
80
|
-
TCustomTemplateItems,
|
|
81
|
-
TemplateField,
|
|
82
|
-
} from '../fields/template/TemplateField';
|
|
83
|
-
import { CompactRow } from './CompactRow';
|
|
84
|
-
import { FormFieldsSkeleton } from './FormFieldsSkeleton';
|
|
85
|
-
import { CompactRowContext, ICompactRowContext, TCodePreviewRenderer } from './compactRowContext';
|
|
86
|
-
import {
|
|
87
|
-
GROUP_INDENT,
|
|
88
|
-
LABEL_AFFORDANCE_WIDTH,
|
|
89
|
-
LABEL_COL_MAX,
|
|
90
|
-
LABEL_COL_MIN,
|
|
91
|
-
LABEL_COL_VAR,
|
|
92
|
-
StyledCompactPanel,
|
|
93
|
-
StyledGroupBody,
|
|
94
|
-
StyledGroupHeader,
|
|
95
|
-
StyledRequiredClusterBox,
|
|
96
|
-
StyledRequiredClusterHeader,
|
|
97
|
-
StyledStatusBox,
|
|
98
|
-
StyledStatusBoxGroupLabel,
|
|
99
|
-
} from './compactRowStyles';
|
|
100
|
-
import { CompactToolbar } from './CompactToolbar';
|
|
101
|
-
import {
|
|
102
|
-
CompactToolbarContext,
|
|
103
|
-
ICompactToolbarContext,
|
|
104
|
-
TCompactSort,
|
|
105
|
-
} from './compactToolbarContext';
|
|
106
|
-
import {
|
|
107
|
-
OptionFieldMessages,
|
|
108
|
-
getAllowedValueAvailability,
|
|
109
|
-
getShownSchemaMessages,
|
|
110
|
-
} from './OptionFieldMessages';
|
|
111
|
-
import { OptionsContext } from './optionsContext';
|
|
112
|
-
import {
|
|
113
|
-
MarkdownRendererContext,
|
|
114
|
-
TMarkdownRenderer,
|
|
115
|
-
} from '../../Description/markdownRendererContext';
|
|
116
|
-
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
117
|
-
import { firstDeclaredType, isUntypedOptionType } from '../../../helpers/optionUiTypes';
|
|
118
|
-
import {
|
|
119
|
-
TReadFirstStatus,
|
|
120
|
-
getFirstAttentionOptionName,
|
|
121
|
-
getOptionGroup,
|
|
122
|
-
getOptionGroupLabel,
|
|
123
|
-
getReadFirstBucket,
|
|
124
|
-
getReadFirstCompletion,
|
|
125
|
-
getReadFirstStatus,
|
|
126
|
-
isOptionValueEmpty,
|
|
127
|
-
isFixedCompactAllowedValueOption,
|
|
128
|
-
shouldAutoCollapseCompactOption,
|
|
129
|
-
isUnsetSchemaHash,
|
|
130
|
-
} from './readFirst';
|
|
131
|
-
|
|
132
|
-
// Re-export types for consumers
|
|
133
|
-
/** The compact form's three status boxes. */
|
|
134
|
-
export type TFormEngineBoxKey = 'attention' | 'set' | 'optional';
|
|
135
|
-
|
|
136
|
-
/** Which parts of the compact toolbar to render — see `compactToolbar`. */
|
|
137
|
-
export interface IFormEngineToolbarParts {
|
|
138
|
-
/** The "Ready / 4 of 6 set / 67%" line and its meter. */
|
|
139
|
-
completion?: boolean;
|
|
140
|
-
/** The "Filter fields…" input. */
|
|
141
|
-
search?: boolean;
|
|
142
|
-
/** The field-picker dropdown (add optional fields, reset, revert). */
|
|
143
|
-
fields?: boolean;
|
|
144
|
-
/** The help button that opens the options dialog. */
|
|
145
|
-
help?: boolean;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export type IOptions = TQorusForm;
|
|
149
|
-
export type TFlatOptions = TQorusFlatForm;
|
|
150
|
-
export type TOption = IQorusFormField;
|
|
151
|
-
export type TOperatorValue = TQorusFormOperatorValue;
|
|
152
|
-
/**
|
|
153
|
-
* A field message, optionally conditional on the form's current values.
|
|
154
|
-
*
|
|
155
|
-
* `when` / `unless` use the `depends_on` grammar and are evaluated against the
|
|
156
|
-
* same form the field belongs to — see `isConditionalMessageShown`. Without
|
|
157
|
-
* either, the message is static and always shown, which is what every existing
|
|
158
|
-
* consumer gets.
|
|
159
|
-
*/
|
|
160
|
-
export type IOptionFieldMessage = IQorusFormFieldMessage;
|
|
161
|
-
/** `T` without `K`, member by member — `Omit` on a union collapses the union. */
|
|
162
|
-
type TDistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
163
|
-
/**
|
|
164
|
-
* One field of a form's schema: the shared descriptor, plus what this engine
|
|
165
|
-
* reads that the shared type does not carry — conditional `messages`, and
|
|
166
|
-
* `absorb_fields`. Declared, so a descriptor using them is checked rather than
|
|
167
|
-
* cast; still assignable wherever the shared descriptor is expected.
|
|
168
|
-
*/
|
|
169
|
-
export type IOptionsSchemaArg = TDistributiveOmit<TQorusFormFieldSchema, 'messages'> & {
|
|
170
|
-
messages?: IOptionFieldMessage[];
|
|
171
|
-
/**
|
|
172
|
-
* Siblings this field renders inside its own editor instead of on rows of
|
|
173
|
-
* their own — a code editor and its language, say.
|
|
174
|
-
*/
|
|
175
|
-
absorb_fields?: string[];
|
|
176
|
-
};
|
|
177
|
-
export interface IOptionsSchema {
|
|
178
|
-
[optionName: string]: IOptionsSchemaArg;
|
|
179
|
-
}
|
|
180
|
-
export interface IOperator extends IQorusFormOperator {}
|
|
181
|
-
export interface IOperatorsSchema extends IQorusFormOperatorsSchema {}
|
|
182
|
-
export interface IOptionsOnChangeMeta extends IQorusFormFieldOnChangeMeta {
|
|
183
|
-
/** `commitMode='batched'`: the emitted value is staged (a draft), not committed. */
|
|
184
|
-
draft?: boolean;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export interface IFormFieldValidityData {
|
|
188
|
-
fieldName: string;
|
|
189
|
-
type: TQorusType;
|
|
190
|
-
value: any;
|
|
191
|
-
validation: IValidationResult;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export interface IFormValidityData {
|
|
195
|
-
isValid: boolean;
|
|
196
|
-
fields: IFormFieldValidityData[];
|
|
197
|
-
invalidFields: IFormFieldValidityData[];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Resolve a field's `inherit_props` map against the current available
|
|
202
|
-
* options (plus any values inherited from an outer FormEngine scope),
|
|
203
|
-
* producing a `{ propName: siblingValue }` hash suitable for spreading
|
|
204
|
-
* onto the field's renderer.
|
|
205
|
-
*
|
|
206
|
-
* For each entry `<prop-name-on-renderer> -> <sibling-field-name>`, the
|
|
207
|
-
* sibling's current value is forwarded under the receiving prop name.
|
|
208
|
-
* Lookup order:
|
|
209
|
-
* 1. `availableOptions[siblingName]?.value` — the field's local scope.
|
|
210
|
-
* 2. `inheritedFromParent[siblingName]` — the bag threaded in by an
|
|
211
|
-
* outer FormEngine (see the `inheritedFromParent` prop). Used when
|
|
212
|
-
* a composite field's `arg_schema` sub-form needs to reach a value
|
|
213
|
-
* on an ancestor scope — e.g. a service-method row's `body` picking
|
|
214
|
-
* up `language` from the parent service form.
|
|
215
|
-
* Missing siblings emit `undefined`, which the renderer's prop type can
|
|
216
|
-
* treat as "no hint".
|
|
217
|
-
*
|
|
218
|
-
* Designed to be JSON-pure: no closures, no transformations. Renderers
|
|
219
|
-
* decide how to use the forwarded value (e.g. the consumer-injected
|
|
220
|
-
* `code-editor` renderer maps a `language: "qore"` prop to its
|
|
221
|
-
* highlighter mode).
|
|
222
|
-
*
|
|
223
|
-
* Mirrored in qorus-ide `src/components/Field/systemOptions.tsx`; keep
|
|
224
|
-
* the two in sync (see qorus-ide's `.claude/CLAUDE.md`).
|
|
225
|
-
*/
|
|
226
|
-
const resolveInheritProps = (
|
|
227
|
-
inheritProps: Record<string, string> | undefined,
|
|
228
|
-
availableOptions: TQorusForm | undefined,
|
|
229
|
-
inheritedFromParent?: Record<string, unknown>
|
|
230
|
-
): Record<string, unknown> => {
|
|
231
|
-
if (!inheritProps) return {};
|
|
232
|
-
const out: Record<string, unknown> = {};
|
|
233
|
-
for (const propName in inheritProps) {
|
|
234
|
-
const siblingName = inheritProps[propName];
|
|
235
|
-
const localValue = (availableOptions?.[siblingName] as IQorusFormField | undefined)?.value;
|
|
236
|
-
out[propName] = localValue !== undefined ? localValue : inheritedFromParent?.[siblingName];
|
|
237
|
-
}
|
|
238
|
-
return out;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
/** One shared "nothing came down from above" bag. See `inheritedScopeFor`. */
|
|
242
|
-
const NO_INHERITED_SCOPE: Record<string, unknown> = Object.freeze({});
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* The inheritance bag ONE field forwards to any `arg_schema` sub-form it hosts:
|
|
246
|
-
* whatever the ancestor chain passed down, plus whatever this field's own
|
|
247
|
-
* `inherit_props` resolves to.
|
|
248
|
-
*
|
|
249
|
-
* Written as a function with a stable answer because it is a PROP, and a prop
|
|
250
|
-
* that is a new object on every render re-renders the field that receives it
|
|
251
|
-
* every time this form renders — for most fields, forever, over a value that
|
|
252
|
-
* never changed. It showed up in the console as `Template field kind Updated
|
|
253
|
-
* because: {inheritedFromParent}` repeating on a form nobody was touching:
|
|
254
|
-
* `kind` declares no `inherit_props` at all, so the bag it was handed was
|
|
255
|
-
* `{ ...(undefined ?? {}) }` — a fresh empty object each time, unequal to the
|
|
256
|
-
* last one, and `TemplateField` is memoised precisely so it does not re-render
|
|
257
|
-
* on a prop that did not change.
|
|
258
|
-
*
|
|
259
|
-
* So a field with nothing of its own to add forwards the bag it was HANDED,
|
|
260
|
-
* unchanged and unwrapped, and a form with no inherited scope at all forwards
|
|
261
|
-
* one shared frozen object rather than minting one per field per render.
|
|
262
|
-
*
|
|
263
|
-
* Mirrored in qorus-ide's `systemOptions.tsx` (which grew this treatment
|
|
264
|
-
* first); per the CLAUDE.md rule there, the forwarding logic is kept identical
|
|
265
|
-
* in both until FormEngine replaces that renderer.
|
|
266
|
-
*
|
|
267
|
-
* @param inheritProps the field's `inherit_props` map, if it declares one
|
|
268
|
-
* @param availableOptions this form's fields, where a sibling's value is read
|
|
269
|
-
* @param inheritedFromParent the bag threaded in from an outer form
|
|
270
|
-
*/
|
|
271
|
-
export const inheritedScopeFor = (
|
|
272
|
-
inheritProps: Record<string, string> | undefined,
|
|
273
|
-
availableOptions: TQorusForm | undefined,
|
|
274
|
-
inheritedFromParent: Record<string, unknown> | undefined
|
|
275
|
-
): Record<string, unknown> => {
|
|
276
|
-
const resolved = resolveInheritProps(inheritProps, availableOptions, inheritedFromParent);
|
|
277
|
-
if (!Object.keys(resolved).length) {
|
|
278
|
-
return inheritedFromParent ?? NO_INHERITED_SCOPE;
|
|
279
|
-
}
|
|
280
|
-
return { ...(inheritedFromParent ?? {}), ...resolved };
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const NegativeColorEffect: any = {
|
|
284
|
-
gradient: {
|
|
285
|
-
colors: { 0: 'danger', 100: 'danger:darken:2' },
|
|
286
|
-
},
|
|
287
|
-
color: '#ffffff',
|
|
288
|
-
glow: 'danger',
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const PositiveColorEffect: any = {
|
|
292
|
-
gradient: {
|
|
293
|
-
colors: { 0: 'success', 100: 'success:darken:2' },
|
|
294
|
-
},
|
|
295
|
-
color: '#ffffff',
|
|
296
|
-
glow: 'success',
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Compact (read-first) layout: flat label | value | action rows in collapsible
|
|
300
|
-
// group panels; colours come in as props so the layout follows the Reqore theme.
|
|
301
|
-
|
|
302
|
-
const StyledCompactWrap = styled.div<{ $ownScroll?: boolean }>`
|
|
303
|
-
display: flex;
|
|
304
|
-
flex-flow: column;
|
|
305
|
-
gap: 10px;
|
|
306
|
-
width: 100%;
|
|
307
|
-
/* Allow the wrap to shrink inside flex/grid parents so its rows' ellipsis can
|
|
308
|
-
engage instead of overflowing the container horizontally. */
|
|
309
|
-
min-width: 0;
|
|
310
|
-
max-width: 100%;
|
|
311
|
-
|
|
312
|
-
/* Scroll ownership — see \`compactScroll\`.
|
|
313
|
-
|
|
314
|
-
'host' (the default): the page or panel around us already scrolls, so we
|
|
315
|
-
must not cap our height and scroll our own body, or the reader gets two
|
|
316
|
-
scrollbars in two styles and cannot tell which moves what.
|
|
317
|
-
|
|
318
|
-
\`overflow-x: clip\`, NOT \`hidden\`: CSS coerces an \`overflow-y: visible\`
|
|
319
|
-
to \`auto\` whenever the other axis is neither visible nor clip, which would
|
|
320
|
-
leave this box a scroll container — the element stops scrolling only
|
|
321
|
-
because \`max-height: none\` means nothing overflows, while \`position:
|
|
322
|
-
sticky\` inside it still resolves against US instead of the host, so the
|
|
323
|
-
toolbar silently stops pinning. \`clip\` keeps the horizontal guard without
|
|
324
|
-
that coercion. */
|
|
325
|
-
${({ $ownScroll }) =>
|
|
326
|
-
$ownScroll ?
|
|
327
|
-
css`
|
|
328
|
-
/* 'own': we are the scroller. The sticky toolbar pins to whatever
|
|
329
|
-
scrolls, so a host whose scrollport carries top padding would
|
|
330
|
-
resolve sticky \`top: 0\` against its padding box and leave an
|
|
331
|
-
unblurred strip above the toolbar. Scrolling in this unpadded box
|
|
332
|
-
pins it flush. Cap at the host's available height, but do not grow
|
|
333
|
-
to fill it — short forms still hug their content.
|
|
334
|
-
|
|
335
|
-
\`min-height: 0\` is what lets a flex item shrink below its content
|
|
336
|
-
so there is something to scroll; it belongs to this branch only. */
|
|
337
|
-
min-height: 0;
|
|
338
|
-
max-height: 100%;
|
|
339
|
-
overflow-y: auto;
|
|
340
|
-
overflow-x: hidden;
|
|
341
|
-
`
|
|
342
|
-
: css`
|
|
343
|
-
/* \`min-height: auto\` is load-bearing, not a default: inside a column
|
|
344
|
-
flex parent the shrink allowance above collapses this box to zero
|
|
345
|
-
height while its rows paint outside it, so the form looks right but
|
|
346
|
-
contributes nothing to layout — the host cannot size or scroll to
|
|
347
|
-
it and anything below it stacks against nothing. */
|
|
348
|
-
min-height: auto;
|
|
349
|
-
max-height: none;
|
|
350
|
-
overflow-y: visible;
|
|
351
|
-
overflow-x: clip;
|
|
352
|
-
`}
|
|
353
|
-
|
|
354
|
-
/* Option logos (e.g. language images) render as <img> inside ReqoreIcon's
|
|
355
|
-
square box; constrain them so portrait PNGs don't overflow the row. */
|
|
356
|
-
.reqore-icon img {
|
|
357
|
-
width: 100%;
|
|
358
|
-
height: 100%;
|
|
359
|
-
object-fit: contain;
|
|
360
|
-
}
|
|
361
|
-
`;
|
|
362
|
-
|
|
363
|
-
// Batched-commit dock: a floating Save/Discard card pinned bottom-right. Sticky
|
|
364
|
-
// (not fixed) so it stays within the form's own scroll bounds when the engine
|
|
365
|
-
// renders inside a drawer/panel; align-self pushes it to the right edge, and it
|
|
366
|
-
// elevates above the rows with an opaque surface + border + shadow.
|
|
367
|
-
const StyledCommitDock = styled.div<{ $bg: string; $border: string }>`
|
|
368
|
-
position: sticky;
|
|
369
|
-
bottom: 8px;
|
|
370
|
-
z-index: 6;
|
|
371
|
-
align-self: flex-end;
|
|
372
|
-
width: fit-content;
|
|
373
|
-
max-width: 100%;
|
|
374
|
-
padding: 8px 10px;
|
|
375
|
-
background: ${({ $bg }) => $bg};
|
|
376
|
-
border: 1px solid ${({ $border }) => $border};
|
|
377
|
-
border-radius: 8px;
|
|
378
|
-
box-shadow: 0 4px 16px ${({ $bg }) => `${changeDarkness($bg, 0.4)}${percentToHexAlpha(55)}`};
|
|
379
|
-
`;
|
|
380
|
-
|
|
381
|
-
export const getType = (
|
|
382
|
-
type?: TQorusType | TQorusType[],
|
|
383
|
-
operators?: IOperatorsSchema,
|
|
384
|
-
operator?: TOperatorValue
|
|
385
|
-
): TQorusType => {
|
|
386
|
-
const finalType = getTypeFromOperator(operators, fixOperatorValue(operator)) || type;
|
|
387
|
-
const resolvedType = firstDeclaredType(finalType);
|
|
388
|
-
|
|
389
|
-
// A value can be received before its server-driven option schema. Keep the
|
|
390
|
-
// renderer type-safe during that transition without guessing a concrete type.
|
|
391
|
-
return (resolvedType || 'any') as TQorusType;
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const getTypeFromOperator = (
|
|
395
|
-
operators?: IOperatorsSchema,
|
|
396
|
-
operatorData?: (string | null | undefined)[]
|
|
397
|
-
): TQorusType | null => {
|
|
398
|
-
if (!operators || !operatorData || !size(operatorData) || !last(operatorData)) {
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
return (operators[last(operatorData) as string]?.type as TQorusType) || null;
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
/** Re-exported from `helpers/common` so existing importers keep working; it
|
|
405
|
-
* lives there because `helpers/validations` needs it and cannot import this
|
|
406
|
-
* module (this module imports the validators). */
|
|
407
|
-
export { fixOperatorValue } from '../../../helpers/common';
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* The renderer-only predicate. Defaults to reqraft's built-ins; a FormEngine
|
|
411
|
-
* instance passes its own (built-ins + the `rendererOnlyUiTypes` prop) so a
|
|
412
|
-
* consumer's injected editors are recognised too. See `./rendererTypes`.
|
|
413
|
-
*/
|
|
414
|
-
type TRendererOnlyCheck = (type?: TQorusType | TQorusType[]) => boolean;
|
|
415
|
-
|
|
416
|
-
const getOptionSchemaStorageType = (
|
|
417
|
-
option?: TQorusFormFieldSchema,
|
|
418
|
-
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
419
|
-
): TQorusType =>
|
|
420
|
-
((isFixedCompactAllowedValueOption(option) ? option?.type || option?.ui_type
|
|
421
|
-
: isRendererOnly(option?.ui_type) ? option?.type || option?.ui_type
|
|
422
|
-
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Whether an option currently holds an expression, in EITHER shape it comes in.
|
|
426
|
-
*
|
|
427
|
-
* At runtime the editor writes the flag onto the option itself
|
|
428
|
-
* (`{ type, is_expression: true, value }`), but a value loaded from a saved
|
|
429
|
-
* draft carries the envelope NESTED (`{ type, value: { is_expression: true,
|
|
430
|
-
* value } }`). Same thing, two shapes, and only the flat one was ever checked —
|
|
431
|
-
* so a freshly typed expression behaved while a reloaded one did not, which is
|
|
432
|
-
* exactly how this survived several fixes: every test used the runtime shape.
|
|
433
|
-
*/
|
|
434
|
-
const optionHoldsExpression = (option: unknown): boolean => {
|
|
435
|
-
const o = option as { is_expression?: unknown; value?: { is_expression?: unknown } } | undefined;
|
|
436
|
-
return !!(o?.is_expression || o?.value?.is_expression);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Which type an option resolves to — the schema's, or the one stored on the
|
|
441
|
-
* value.
|
|
442
|
-
*
|
|
443
|
-
* Exported because this single decision is what every option field is rendered
|
|
444
|
-
* with, and what an expression's return type is checked against. It deserves
|
|
445
|
-
* to be assertable without standing up a form.
|
|
446
|
-
*/
|
|
447
|
-
export const getOptionFieldStorageType = (
|
|
448
|
-
optionName: string,
|
|
449
|
-
fieldType: TQorusType | TQorusType[] | undefined,
|
|
450
|
-
schema?: IQorusFormSchema,
|
|
451
|
-
operators?: IOperatorsSchema,
|
|
452
|
-
operatorData?: TOperatorValue,
|
|
453
|
-
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType,
|
|
454
|
-
/** Whether the stored value is an expression — see below. */
|
|
455
|
-
isExpression?: boolean
|
|
456
|
-
): TQorusType => {
|
|
457
|
-
const schemaOption = schema?.[optionName];
|
|
458
|
-
/* A STORED type does not describe an expression.
|
|
459
|
-
|
|
460
|
-
An expression value is `{ is_expression: true, value: {...} }`, so the type
|
|
461
|
-
recorded beside it describes that envelope — `hash` — and not what the
|
|
462
|
-
field holds. Letting it win means the SCHEMA's declared type is ignored and
|
|
463
|
-
the wrong answer is handed to the expression editor as the return type to
|
|
464
|
-
check against: `1 + 2` on a field declared `"type": "auto"` came back
|
|
465
|
-
"The expression returns int, and this field holds hash". A literal `1` was
|
|
466
|
-
fine, because a literal is stored as a plain value whose type matches.
|
|
467
|
-
|
|
468
|
-
`auto`/`any` accept anything, which is exactly why nothing should be
|
|
469
|
-
checked against them; a concrete declared type still is. The schema is the
|
|
470
|
-
only party that knows what the field accepts. */
|
|
471
|
-
const storedType =
|
|
472
|
-
(
|
|
473
|
-
!isExpression &&
|
|
474
|
-
fieldType &&
|
|
475
|
-
!(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType))
|
|
476
|
-
) ?
|
|
477
|
-
fieldType
|
|
478
|
-
: getOptionSchemaStorageType(schemaOption, isRendererOnly);
|
|
479
|
-
|
|
480
|
-
return getType(storedType as TQorusType, operators, operatorData);
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
export const hasRequiredOptions = (options: IQorusFormSchema = {}) => {
|
|
484
|
-
return !!findKey(options, (option) => option.required);
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
/* Re-exported from the module that owns it so the existing import path keeps
|
|
488
|
-
working; see `optionsContext.ts` for why it is not declared here. */
|
|
489
|
-
export { OptionsContext } from './optionsContext';
|
|
490
|
-
export type { IOptionsContext } from './optionsContext';
|
|
491
|
-
|
|
492
|
-
export const fixOptions = (
|
|
493
|
-
value: TQorusForm | TQorusFlatForm = {},
|
|
494
|
-
options?: IQorusFormSchema,
|
|
495
|
-
operators?: IOperatorsSchema,
|
|
496
|
-
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
497
|
-
): TQorusForm => {
|
|
498
|
-
const fixedValue = cloneDeep(value);
|
|
499
|
-
|
|
500
|
-
// Server-driven schemas can arrive after the persisted value. Preserve that
|
|
501
|
-
// value verbatim until its schema is available instead of manufacturing
|
|
502
|
-
// partially typed form fields that can crash the following render.
|
|
503
|
-
if (!size(options)) {
|
|
504
|
-
return fixedValue as TQorusForm;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
forEach(options, (option, name) => {
|
|
508
|
-
if (
|
|
509
|
-
option.value ||
|
|
510
|
-
option.required_groups ||
|
|
511
|
-
((option.required || option.preselected) && !fixedValue[name]) ||
|
|
512
|
-
((option.required || option.preselected) && option.default_value && !option.value)
|
|
513
|
-
) {
|
|
514
|
-
let obj: IQorusFormField;
|
|
515
|
-
const type = getType(
|
|
516
|
-
getOptionSchemaStorageType(option, isRendererOnly),
|
|
517
|
-
operators,
|
|
518
|
-
(fixedValue[name] as IQorusFormField)?.op
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
if (
|
|
522
|
-
(option.default_value as IQorusFormField)?.is_expression &&
|
|
523
|
-
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
524
|
-
) {
|
|
525
|
-
obj = option.default_value as IQorusFormField;
|
|
526
|
-
} else {
|
|
527
|
-
// A field declared with `default_view: 'expression'` is NOT seeded with
|
|
528
|
-
// an empty expression here. This runs on every load, so a seed written
|
|
529
|
-
// here changed a saved object merely by opening it: an existing Qog
|
|
530
|
-
// state gained `{args: []}` the moment its panel opened, and the editor
|
|
531
|
-
// autosaved a draft nobody made. The renderer already opens such a
|
|
532
|
-
// field in expression mode from the schema (`isDefaultFunction`), the
|
|
533
|
-
// first edit stores `is_expression`, and a host that creates a NEW
|
|
534
|
-
// object seeds the expression itself, at creation.
|
|
535
|
-
obj = {
|
|
536
|
-
type,
|
|
537
|
-
value:
|
|
538
|
-
(typeof fixedValue[name] === 'object' ?
|
|
539
|
-
(fixedValue[name] as IQorusFormField)?.value
|
|
540
|
-
: fixedValue[name]) ??
|
|
541
|
-
option.value ??
|
|
542
|
-
getDefaultValue(option),
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
if ((fixedValue[name] as IQorusFormField)?.is_expression) {
|
|
546
|
-
obj.is_expression = true;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
fixedValue[name] = obj;
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
const res = reduce(
|
|
555
|
-
fixedValue,
|
|
556
|
-
(newValue, option, optionName) => {
|
|
557
|
-
let newOption = option as IQorusFormField;
|
|
558
|
-
|
|
559
|
-
if (!isPlainObject(newOption) || !(newOption as IQorusFormField)?.type) {
|
|
560
|
-
const isUntypedEnvelope = isPlainObject(newOption) && 'value' in newOption;
|
|
561
|
-
const untypedFieldValue =
|
|
562
|
-
isUntypedEnvelope ? (newOption as IQorusFormField).value : newOption;
|
|
563
|
-
// Spread the original envelope first so every key it carried survives —
|
|
564
|
-
// rebuilding from just {type, value} silently dropped `op`, `is_expression`
|
|
565
|
-
// and anything else a field needs to render, which emptied the card.
|
|
566
|
-
const fixedOption: IQorusFormField = {
|
|
567
|
-
...(isPlainObject(newOption) ? (newOption as IQorusFormField) : {}),
|
|
568
|
-
type: getType(
|
|
569
|
-
getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
|
|
570
|
-
operators,
|
|
571
|
-
(newOption as IQorusFormField)?.op
|
|
572
|
-
),
|
|
573
|
-
value: untypedFieldValue,
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
newOption = fixedOption;
|
|
577
|
-
} else if (
|
|
578
|
-
newOption.type === options?.[optionName]?.ui_type &&
|
|
579
|
-
isRendererOnly(newOption.type)
|
|
580
|
-
) {
|
|
581
|
-
newOption = {
|
|
582
|
-
...newOption,
|
|
583
|
-
type: getType(
|
|
584
|
-
getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
|
|
585
|
-
operators,
|
|
586
|
-
newOption.op
|
|
587
|
-
),
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// A value that is not one of the declared choices is KEPT. It used to be
|
|
592
|
-
// dropped here, on load, and the dropped value was three times a real
|
|
593
|
-
// one — a bare-declared choice, a document naming a choice, a value whose
|
|
594
|
-
// choices had not arrived yet — that the emptied form then autosaved, so
|
|
595
|
-
// it was gone for good. Whether a value is one of the choices is now the
|
|
596
|
-
// validator's question (`getUnlistedChoiceReason`, asked through
|
|
597
|
-
// `validateFieldWithResult`): such a field is invalid, sits under "Needs
|
|
598
|
-
// attention" saying which value is not a choice, and the form reports
|
|
599
|
-
// itself invalid so the host blocks submitting it.
|
|
600
|
-
|
|
601
|
-
if (
|
|
602
|
-
newOption.value &&
|
|
603
|
-
options?.[optionName]?.readonly &&
|
|
604
|
-
options?.[optionName]?.default_value &&
|
|
605
|
-
getDefaultValue(options?.[optionName]) !== newOption.value
|
|
606
|
-
) {
|
|
607
|
-
newOption.value = getDefaultValue(options[optionName]);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
return { ...newValue, [optionName]: newOption };
|
|
611
|
-
},
|
|
612
|
-
{} as TQorusForm
|
|
613
|
-
);
|
|
614
|
-
|
|
615
|
-
return res;
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
// A required field with no value can arrive as `{ value: '' }` OR with no `value`
|
|
619
|
-
// key at all — fixOptions emits either depending on the field's default. They mean
|
|
620
|
-
// the same "empty", so drop the key before comparing: a value that differs ONLY by
|
|
621
|
-
// that representation must read as unchanged, otherwise the controlled value-sync
|
|
622
|
-
// effect re-fixes → re-emits → re-fixes forever (the echo loop).
|
|
623
|
-
const normalizeEmptyFieldValues = (fields: TQorusForm | TQorusFlatForm | undefined): TQorusForm =>
|
|
624
|
-
reduce(
|
|
625
|
-
(fields as TQorusForm) || {},
|
|
626
|
-
(acc, field, name) => {
|
|
627
|
-
const fieldValue = (field as IQorusFormField)?.value;
|
|
628
|
-
if (field && typeof field === 'object' && (fieldValue === '' || fieldValue === undefined)) {
|
|
629
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
630
|
-
const { value: _dropped, ...rest } = field as IQorusFormField;
|
|
631
|
-
acc[name] = rest as IQorusFormField;
|
|
632
|
-
} else {
|
|
633
|
-
acc[name] = field as IQorusFormField;
|
|
634
|
-
}
|
|
635
|
-
return acc;
|
|
636
|
-
},
|
|
637
|
-
{} as TQorusForm
|
|
638
|
-
);
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Does this rebuild say anything the parent does not already hold?
|
|
642
|
-
*
|
|
643
|
-
* `false` means the rebuild is a MIRROR: `fixOptions` found no default to
|
|
644
|
-
* materialise, so the result repeats the `value` the parent handed down. The
|
|
645
|
-
* compare is empty-normalized because `fixOptions` round-trips a required-empty
|
|
646
|
-
* field between `{ value: '' }` and no `value` key, and that cosmetic
|
|
647
|
-
* difference would otherwise read as new information forever.
|
|
648
|
-
*/
|
|
649
|
-
export const isMirrorOfValue = (
|
|
650
|
-
fixedValue: TQorusForm | TQorusFlatForm | undefined,
|
|
651
|
-
value: TQorusForm | TQorusFlatForm | undefined
|
|
652
|
-
): boolean => isEqual(normalizeEmptyFieldValues(fixedValue), normalizeEmptyFieldValues(value));
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* What this rebuild lets the form agree with the parent on, if anything.
|
|
656
|
-
*
|
|
657
|
-
* Returns the rebuild when it mirrors `value`, and `undefined` when it
|
|
658
|
-
* contributed something the parent has yet to learn.
|
|
659
|
-
*
|
|
660
|
-
* Adopting the parent's value IS agreement, and saying so matters as much as
|
|
661
|
-
* staying quiet about it. The sync-down skips only when the parent echoed what
|
|
662
|
-
* this form last sent; if adopting a value never updates that record, it points
|
|
663
|
-
* at an older state forever, the skip can never fire again, and the form
|
|
664
|
-
* rebuilds on every single change. Holding the mirror back WITHOUT recording
|
|
665
|
-
* the agreement was measured making the starvation worse, not better — 8 s
|
|
666
|
-
* became a 45 s timeout — which is why the two belong together.
|
|
667
|
-
*/
|
|
668
|
-
export const agreementFromRebuild = (
|
|
669
|
-
fixedValue: TQorusForm | TQorusFlatForm | undefined,
|
|
670
|
-
value: TQorusForm | TQorusFlatForm | undefined
|
|
671
|
-
): TQorusForm | TQorusFlatForm | undefined =>
|
|
672
|
-
isMirrorOfValue(fixedValue, value) ? fixedValue : undefined;
|
|
673
|
-
|
|
674
|
-
/** The state the emit decision is made against. */
|
|
675
|
-
export interface IEmitDecision {
|
|
676
|
-
/** The form's own copy of the value, as it stands now. */
|
|
677
|
-
localValue: TQorusForm | TQorusFlatForm | undefined;
|
|
678
|
-
/** The `value` prop, read at the moment the decision is made. */
|
|
679
|
-
value: TQorusForm | TQorusFlatForm | undefined;
|
|
680
|
-
/**
|
|
681
|
-
* The last rebuild that mirrored `value` without adding anything, if the most
|
|
682
|
-
* recent one did. `undefined` when the last rebuild contributed something.
|
|
683
|
-
*/
|
|
684
|
-
mirroredValue: TQorusForm | TQorusFlatForm | undefined;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Should the form report `localValue` to its parent as an answer?
|
|
689
|
-
*
|
|
690
|
-
* Extracted and pure because the situation it exists for cannot be reproduced
|
|
691
|
-
* through a rendered form: it needs the emit to be decided against a `value`
|
|
692
|
-
* NEWER than the `localValue` built from it, which only happens when two
|
|
693
|
-
* components' effects land in one React commit — and a test renderer flushes
|
|
694
|
-
* effects between updates, so the two never fall out of step. The states below
|
|
695
|
-
* were recorded from a live instrumented run instead, and this is what makes
|
|
696
|
-
* them assertable.
|
|
697
|
-
*
|
|
698
|
-
* Two ways the answer is no:
|
|
699
|
-
*
|
|
700
|
-
* 1. `localValue` is a mirror — a rebuild of the parent's own value that added
|
|
701
|
-
* nothing. Repeating it back is not an answer. **Checked first**, because
|
|
702
|
-
* the comparison in (2) reads whatever `value` has become by now, which,
|
|
703
|
-
* when a nested engine shares the parent value, is already that engine's
|
|
704
|
-
* newer write; comparing against it would find a difference this form never
|
|
705
|
-
* made and emit the older mirror over the newer write. That is the loop.
|
|
706
|
-
* 2. `localValue` already equals `value` — there is nothing to report.
|
|
707
|
-
*/
|
|
708
|
-
export const shouldEmitLocalValue = ({
|
|
709
|
-
localValue,
|
|
710
|
-
value,
|
|
711
|
-
mirroredValue,
|
|
712
|
-
}: IEmitDecision): boolean => {
|
|
713
|
-
if (mirroredValue !== undefined && isEqual(localValue, mirroredValue)) {
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
return !isEqual(localValue, value);
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
export const flattenOptions = (options: TQorusForm): TQorusFlatForm => {
|
|
720
|
-
return reduce(
|
|
721
|
-
options,
|
|
722
|
-
(newOptions, option, optionName) => {
|
|
723
|
-
return {
|
|
724
|
-
...newOptions,
|
|
725
|
-
[optionName]: typeof option === 'object' ? (option as IQorusFormField)?.value : option,
|
|
726
|
-
};
|
|
727
|
-
},
|
|
728
|
-
{} as TQorusFlatForm
|
|
729
|
-
);
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
export const getTypeAndCanBeNull = (
|
|
733
|
-
type?: TQorusType | TQorusType[],
|
|
734
|
-
allowed_values?: any[],
|
|
735
|
-
operatorData?: TOperatorValue,
|
|
736
|
-
operators?: IOperatorsSchema
|
|
737
|
-
) => {
|
|
738
|
-
let canBeNull = false;
|
|
739
|
-
let realType = getType(type, operators, operatorData);
|
|
740
|
-
|
|
741
|
-
realType = realType.replace('soft', '') as TQorusType;
|
|
742
|
-
realType = realType.replace(/<[^>]*>/g, '') as TQorusType;
|
|
743
|
-
|
|
744
|
-
if (realType?.startsWith('*')) {
|
|
745
|
-
realType = realType.replace('*', '') as TQorusType;
|
|
746
|
-
canBeNull = true;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
realType = (realType === 'string' && allowed_values ? 'string' : realType) as TQorusType;
|
|
750
|
-
|
|
751
|
-
return {
|
|
752
|
-
type: realType,
|
|
753
|
-
defaultType: realType,
|
|
754
|
-
defaultInternalType: isUntypedOptionType(realType) ? undefined : realType,
|
|
755
|
-
canBeNull,
|
|
756
|
-
};
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
/** Display metadata for a read-first group, keyed by the raw `group` string;
|
|
760
|
-
* omissions fall back to a title-cased key, no icon, and schema order. */
|
|
761
|
-
export interface IFormEngineGroup {
|
|
762
|
-
label?: string;
|
|
763
|
-
icon?: IReqoreIconName;
|
|
764
|
-
subtitle?: string;
|
|
765
|
-
sort?: number;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange'> {
|
|
769
|
-
name: string;
|
|
770
|
-
uniqueName?: string;
|
|
771
|
-
value?: TQorusForm | TQorusFlatForm;
|
|
772
|
-
options?: IOptionsSchema;
|
|
773
|
-
onChange?: (name: string, value?: TQorusForm, meta?: IOptionsOnChangeMeta) => void;
|
|
774
|
-
/**
|
|
775
|
-
* `'immediate'` (default): edits flow out via the debounced `onChange`.
|
|
776
|
-
* `'batched'`: edits stage locally (Draft chips + Save/Discard bar); Save
|
|
777
|
-
* fires `onCommit`, gated on validity; staged `onChange` carries `meta.draft`.
|
|
778
|
-
*/
|
|
779
|
-
commitMode?: 'immediate' | 'batched';
|
|
780
|
-
/**
|
|
781
|
-
* How many read-first rows can be expanded (editing) at once. `'single'`
|
|
782
|
-
* (default) collapses the previously-open row when another is opened — the
|
|
783
|
-
* accordion model that keeps the read-first list scannable. `'multi'` lets
|
|
784
|
-
* several stay open (e.g. filling an empty form top-to-bottom).
|
|
785
|
-
*/
|
|
786
|
-
expandMode?: 'single' | 'multi';
|
|
787
|
-
/** `commitMode='batched'` only: the user applied the staged changes. */
|
|
788
|
-
onCommit?: (name: string, value?: TQorusForm, meta?: IOptionsOnChangeMeta) => void;
|
|
789
|
-
/**
|
|
790
|
-
* Operator schema (`WHERE <field> IS <op>`): adds the operator selector rows;
|
|
791
|
-
* operator-bearing fields always card-edit in compact mode.
|
|
792
|
-
*/
|
|
793
|
-
operators?: IOperatorsSchema;
|
|
794
|
-
onSingleOptionsChange?: (name: string, value: TOption) => void;
|
|
795
|
-
onDependableOptionChange?: (
|
|
796
|
-
name: string,
|
|
797
|
-
value: TOption,
|
|
798
|
-
options: TQorusForm,
|
|
799
|
-
schema: IQorusFormSchema
|
|
800
|
-
) => void;
|
|
801
|
-
placeholder?: string;
|
|
802
|
-
noValueString?: string;
|
|
803
|
-
isValid?: boolean;
|
|
804
|
-
/** Fetch the options schema from `options/{url}` (IDE Options parity). */
|
|
805
|
-
url?: string;
|
|
806
|
-
/** Fetch the options schema from a custom endpoint; wins over `url`. */
|
|
807
|
-
customUrl?: string;
|
|
808
|
-
/** Fetch the search-operators schema (enables the per-option operator UI). */
|
|
809
|
-
operatorsUrl?: string;
|
|
810
|
-
/** Fired with the fetched schema after a successful `url`/`customUrl` load. */
|
|
811
|
-
onOptionsLoaded?: (options: IQorusFormSchema) => void;
|
|
812
|
-
recordRequiresSearchOptions?: boolean;
|
|
813
|
-
readOnly?: boolean;
|
|
814
|
-
/**
|
|
815
|
-
* What a click on a READ-ONLY row does, when there is somewhere to send it.
|
|
816
|
-
*
|
|
817
|
-
* A read-only row opens nothing by default — it shows everything it has and
|
|
818
|
-
* answers a click with silence, which is right when the form is the only
|
|
819
|
-
* place the value exists. Where the same field IS editable somewhere else,
|
|
820
|
-
* that silence is a dead end: the reader clicking a value they want to
|
|
821
|
-
* change has said exactly what they want.
|
|
822
|
-
*
|
|
823
|
-
* Given this, each row becomes the way in to that place and announces it —
|
|
824
|
-
* it takes a button's role, keyboard handling and cursor. Omit it and
|
|
825
|
-
* nothing changes.
|
|
826
|
-
*/
|
|
827
|
-
onReadOnlyActivate?: (optionName: string) => void;
|
|
828
|
-
allowTemplates?: boolean;
|
|
829
|
-
/**
|
|
830
|
-
* The form's SHARED template vocabulary — config items, system properties —
|
|
831
|
-
* offered to every field that supports templates.
|
|
832
|
-
*
|
|
833
|
-
* A field may also declare `templates` of its own in the schema, and that
|
|
834
|
-
* narrower list wins for that field: see {@link TFieldWithOwnTemplates}.
|
|
835
|
-
*/
|
|
836
|
-
stringTemplates?: IReqoreFormTemplates;
|
|
837
|
-
/** Opt-in: fetch global templates from `system/getContextData` for this context. */
|
|
838
|
-
interfaceContext?: string;
|
|
839
|
-
templateFieldProps?: Partial<ITemplateFieldProps>;
|
|
840
|
-
/**
|
|
841
|
-
* Force single-value selectors to use inline dropdowns even when their
|
|
842
|
-
* allowed values carry descriptions.
|
|
843
|
-
*/
|
|
844
|
-
forceDropdown?: boolean;
|
|
845
|
-
showTypeToggle?: boolean;
|
|
846
|
-
/**
|
|
847
|
-
* Compact (read-first) mode: each option renders as a row with its formatted
|
|
848
|
-
* value; clicking expands the real editor inline, Done collapses it.
|
|
849
|
-
*/
|
|
850
|
-
compact?: boolean;
|
|
851
|
-
/**
|
|
852
|
-
* Compact mode only: drop the read-first wrap's horizontal gutter (the 12px
|
|
853
|
-
* breathing room) so the form sits flush to its container's edges. For embeds
|
|
854
|
-
* that own their own gutters — e.g. the SchemaDefinition tab body, where the
|
|
855
|
-
* form should line up with the section description above it. Default `false`.
|
|
856
|
-
*
|
|
857
|
-
* @deprecated No-op. The wrap has never had the gutter this prop claims to
|
|
858
|
-
* drop — the value reached a styled template with no interpolation slot — so
|
|
859
|
-
* every form already renders flush. Kept so existing call sites keep
|
|
860
|
-
* compiling; do not add new ones.
|
|
861
|
-
*/
|
|
862
|
-
compactFlush?: boolean;
|
|
863
|
-
/**
|
|
864
|
-
* Compact mode only: this form is an EMBEDDED sub-form (e.g. an arg_schema
|
|
865
|
-
* field's nested form) rather than the top-level scroller. It doesn't own a
|
|
866
|
-
* scroll context, so the toolbar isn't sticky and its header drops the dark
|
|
867
|
-
* blurred backdrop (and the stacking context that goes with it) — it sits
|
|
868
|
-
* transparently inside the parent's edit card. Default `false`.
|
|
869
|
-
*
|
|
870
|
-
* Implies `compactScroll: 'host'` — a nested sub-form never owns a scroller.
|
|
871
|
-
*/
|
|
872
|
-
compactNested?: boolean;
|
|
873
|
-
/**
|
|
874
|
-
* Compact mode only: who owns the form's vertical scroll.
|
|
875
|
-
*
|
|
876
|
-
* - `'host'` (default) — the surrounding page, panel or drawer scrolls, and
|
|
877
|
-
* the form grows to its content. This is right almost everywhere: a form
|
|
878
|
-
* inside something that already scrolls must not cap its own height, or the
|
|
879
|
-
* reader gets two scrollbars side by side in two different styles and no
|
|
880
|
-
* way to tell which one moves what.
|
|
881
|
-
* - `'own'` — the form caps at the host's available height and scrolls its
|
|
882
|
-
* own body. Reach for this only when the host has a definite height and is
|
|
883
|
-
* NOT itself a scroll container (it would clip the form instead of
|
|
884
|
-
* scrolling it), or when the host's scrollport is padded and the sticky
|
|
885
|
-
* toolbar must pin flush against an unpadded box.
|
|
886
|
-
*
|
|
887
|
-
* Default `'host'`.
|
|
888
|
-
*/
|
|
889
|
-
compactScroll?: 'own' | 'host';
|
|
890
|
-
/**
|
|
891
|
-
* Compact mode only: props forwarded to the read-first form's outer
|
|
892
|
-
* `ReqorePanel` — the panel that carries the sticky toolbar header and holds
|
|
893
|
-
* the status boxes. Spread AFTER the engine's own defaults, so anything set
|
|
894
|
-
* here wins (`flat`, `raised`, `minimal`, `stickyHeader`, `label`, `icon`,
|
|
895
|
-
* `intent`, `padded`, `size`, …). Two props merge instead of replacing, so a
|
|
896
|
-
* consumer can add to the panel without dismantling the form:
|
|
897
|
-
* - `actions` — appended after the engine's toolbar action.
|
|
898
|
-
* - `contentStyle` — merged over the engine's flex-column layout.
|
|
899
|
-
* No-op in classic (non-compact) mode.
|
|
900
|
-
*/
|
|
901
|
-
compactPanelProps?: IReqorePanelProps;
|
|
902
|
-
/**
|
|
903
|
-
* Compact mode only: which status boxes start collapsed. Defaults to
|
|
904
|
-
* `['optional']` — a form opens on what is in use.
|
|
905
|
-
*
|
|
906
|
-
* Pass `['set', 'optional']` where the form is one item in a longer list and
|
|
907
|
-
* the reader is choosing WHICH to open rather than reading this one, e.g. a
|
|
908
|
-
* template's per-action option forms stacked in a drawer.
|
|
909
|
-
*
|
|
910
|
-
* One escape is not negotiable: a box always opens while a search query is
|
|
911
|
-
* running (`ReqorePanel` unmounts collapsed content, so a match inside a
|
|
912
|
-
* closed box would be unreachable).
|
|
913
|
-
*
|
|
914
|
-
* A second applies only by DEFAULT: the Optional box stays open when every
|
|
915
|
-
* row is optional, because a form that collapsed its only box would render a
|
|
916
|
-
* card that looks empty and broken. Passing this prop explicitly waives it —
|
|
917
|
-
* a host that supplies its own heading and explanation around the form (a
|
|
918
|
-
* run-options panel, say) has already told the reader what the card is, and
|
|
919
|
-
* asked for the settings to start folded away.
|
|
920
|
-
*/
|
|
921
|
-
compactCollapsedGroups?: TFormEngineBoxKey[];
|
|
922
|
-
/**
|
|
923
|
-
* Compact mode only: rename the status boxes.
|
|
924
|
-
*
|
|
925
|
-
* The three are `Needs attention`, `Set` and `Optional`, which read correctly
|
|
926
|
-
* when the form is the page's subject. Inside a host that already named the
|
|
927
|
-
* subject — a panel headed "Change a setting for this run" — a bare
|
|
928
|
-
* `Optional` names a category rather than the things in it, and
|
|
929
|
-
* `Optional settings` is what the reader is looking at. Only the labels
|
|
930
|
-
* change; bucketing, icons and intents are untouched.
|
|
931
|
-
*/
|
|
932
|
-
compactBoxLabels?: Partial<Record<TFormEngineBoxKey, string>>;
|
|
933
|
-
/**
|
|
934
|
-
* Compact mode only: the three status-box headers stay pinned to the top of
|
|
935
|
-
* whatever scrolls while their own box is in view. Default `true`.
|
|
936
|
-
*
|
|
937
|
-
* A box header is the only thing that says which bucket a row is in — `Set`,
|
|
938
|
-
* `Needs attention`, `Optional`. On a form long enough to scroll it leaves
|
|
939
|
-
* the screen, and every row below it then reads as an undifferentiated list:
|
|
940
|
-
* the reader deep inside `Optional` has no way to tell it from `Set` except
|
|
941
|
-
* by scrolling back. Pinning the header is what keeps the answer on screen.
|
|
942
|
-
*
|
|
943
|
-
* ALL THREE behave identically, deliberately. They are one control rendered
|
|
944
|
-
* three times, differing only in label, tint and icon; a reader who has
|
|
945
|
-
* scrolled into the Optional box needs to know that exactly as much as one
|
|
946
|
-
* who has scrolled into Set, and a rule that pinned only some of them would
|
|
947
|
-
* make the absence of a pinned header mean two different things (this box has
|
|
948
|
-
* no header vs. you are above every box).
|
|
949
|
-
*
|
|
950
|
-
* A header can never outlive its own box: `position: sticky` is bounded by
|
|
951
|
-
* its containing block, so each header unpins as its box scrolls away and at
|
|
952
|
-
* most ONE box header is ever pinned at a time. The pin costs one header's
|
|
953
|
-
* height, not three.
|
|
954
|
-
*
|
|
955
|
-
* Off by construction for a nested (`arg_schema`) sub-form — it sits inside
|
|
956
|
-
* the parent form's card and owns no scroll context to pin against.
|
|
957
|
-
*
|
|
958
|
-
* On a NARROW form this also takes the pin off the form's own toolbar, so the
|
|
959
|
-
* two never cost a third of a phone's screen between them — see the note at
|
|
960
|
-
* the panel's `stickyHeader`.
|
|
961
|
-
*
|
|
962
|
-
* Turn it off where the form is one item in a scrolling LIST of forms: there
|
|
963
|
-
* the reader is choosing which form to read, and a box header pinned from the
|
|
964
|
-
* item they have scrolled past is chrome belonging to something they have
|
|
965
|
-
* left.
|
|
966
|
-
*/
|
|
967
|
-
compactStickyBoxHeaders?: boolean;
|
|
968
|
-
/**
|
|
969
|
-
* Compact mode only: how many pixels of the HOST's own sticky chrome sit
|
|
970
|
-
* above this form, inside the same scroll container.
|
|
971
|
-
*
|
|
972
|
-
* Sticky offsets do not compose by themselves — every pinned thing resolves
|
|
973
|
-
* `top` against the same scrollport, so two of them with the same offset land
|
|
974
|
-
* on top of each other. A host that pins a bar of its own (a page header
|
|
975
|
-
* carrying a name and a Run control, say) measures it and passes the height
|
|
976
|
-
* here; the form then pins its toolbar below that bar, and its box headers
|
|
977
|
-
* below the toolbar, instead of underneath both.
|
|
978
|
-
*
|
|
979
|
-
* Default `0` — the form is the topmost pinned thing in its scroller.
|
|
980
|
-
*/
|
|
981
|
-
compactStickyOffset?: number;
|
|
982
|
-
/**
|
|
983
|
-
* Compact mode only: which parts of the form's toolbar to show. `true`
|
|
984
|
-
* (default) shows all of it; `false` hides the whole thing.
|
|
985
|
-
*
|
|
986
|
-
* The parts already hide themselves when they have nothing to say — the
|
|
987
|
-
* completion meter needs at least one counted option, and the search and
|
|
988
|
-
* field-picker need more than one option to pick between — so reach for this
|
|
989
|
-
* only to hide something that WOULD have rendered. A per-action form asking
|
|
990
|
-
* for one value, say, has a meter reading 0/1 that tells the reader nothing
|
|
991
|
-
* they cannot see in the row below it.
|
|
992
|
-
*
|
|
993
|
-
* Hiding `search` removes the only way to run a query, which is what forces
|
|
994
|
-
* a collapsed box open; `compactCollapsedGroups` then fully determines what
|
|
995
|
-
* is open.
|
|
996
|
-
*/
|
|
997
|
-
compactToolbar?: boolean | IFormEngineToolbarParts;
|
|
998
|
-
/** Compact mode only: per-group display metadata (label / icon / subtitle /
|
|
999
|
-
* order) — the server only sends the bare group key. */
|
|
1000
|
-
groups?: Record<string, IFormEngineGroup>;
|
|
1001
|
-
/**
|
|
1002
|
-
* Async schema source (used when `options` is absent): transport-agnostic,
|
|
1003
|
-
* called on mount and on identity change (memoize it); the engine owns the
|
|
1004
|
-
* loading/error lifecycle and fires `onOptionsLoaded` on success.
|
|
1005
|
-
*/
|
|
1006
|
-
optionsLoader?: () => Promise<IQorusFormSchema>;
|
|
1007
|
-
onValidityChange?: (isValid: boolean, data: IFormValidityData) => void;
|
|
1008
|
-
/**
|
|
1009
|
-
* SEAM (reqraft): extra hover actions prepended to each option row — where
|
|
1010
|
-
* the IDE renders its `allowAi` AI-assist button. A factory receives the
|
|
1011
|
-
* option's name/schema/value (the context the IDE's `AiAssistanceAction`
|
|
1012
|
-
* captures); the consumer injects the button, reqraft stays AI-free.
|
|
1013
|
-
*/
|
|
1014
|
-
optionActions?: TOptionActions;
|
|
1015
|
-
/**
|
|
1016
|
-
* Whether per-option injected actions collapse into the row's overflow menu
|
|
1017
|
-
* instead of rendering as inline buttons.
|
|
1018
|
-
*
|
|
1019
|
-
* `'auto'` (default) collapses when the device cannot hover or the viewport is
|
|
1020
|
-
* narrow — on a phone a hover-gated action would otherwise be unreachable, and
|
|
1021
|
-
* a row has no width for a button strip. `'always'` / `'never'` force it, which
|
|
1022
|
-
* consumers can use for a known-mobile surface and stories use to capture the
|
|
1023
|
-
* collapsed state deterministically.
|
|
1024
|
-
*
|
|
1025
|
-
* Actions beyond `MAX_INLINE_OPTION_ACTIONS` always overflow into the menu, so
|
|
1026
|
-
* a consumer injecting many actions can never blow out the row.
|
|
1027
|
-
*/
|
|
1028
|
-
optionActionsCollapse?: 'auto' | 'always' | 'never';
|
|
1029
|
-
/**
|
|
1030
|
-
* The HOST is still loading something this form needs.
|
|
1031
|
-
*
|
|
1032
|
-
* Declared because it was already read (`rest.skeleton`) and honoured by the
|
|
1033
|
-
* loading gate, but undeclared props are invisible to the people who need
|
|
1034
|
-
* them — and the reason to reach for this is not obvious.
|
|
1035
|
-
*
|
|
1036
|
-
* A host that resolves anything before it can render a form — an option list
|
|
1037
|
-
* fetched by `get_message`, a schema id resolved over the wire — has a wait of
|
|
1038
|
-
* its own that ends just as this component's begins. Drawing its own
|
|
1039
|
-
* placeholder for that first half and then handing over produces TWO
|
|
1040
|
-
* placeholders in series for one load, from two different components: the DOM
|
|
1041
|
-
* node is replaced, the geometry can differ, and the page relayouts in the
|
|
1042
|
-
* middle of waiting. Measured on the Qorus alert rule, whose three sections
|
|
1043
|
-
* each did this — six placeholder mounts where three would do.
|
|
1044
|
-
*
|
|
1045
|
-
* Passing the host's own wait in here instead makes ONE component own the
|
|
1046
|
-
* whole wait, from the host's first fetch to this form's last, with one
|
|
1047
|
-
* placeholder and one DOM node.
|
|
1048
|
-
*/
|
|
1049
|
-
skeleton?: boolean;
|
|
1050
|
-
/**
|
|
1051
|
-
* What the HOST is waiting for, when it hands its wait down via `skeleton`.
|
|
1052
|
-
*
|
|
1053
|
-
* Without it every host wait reads as the same word and the reading stops at
|
|
1054
|
-
* the package boundary — measured on the live alert rule, three sections all
|
|
1055
|
-
* reported `host` and the next step was guessing which of the host's own five
|
|
1056
|
-
* conditions had fired. Surfaced as `data-wait` on the placeholder.
|
|
1057
|
-
*/
|
|
1058
|
-
skeletonReason?: string;
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* SEAM (reqraft): consumer-injected field editors for types reqraft doesn't
|
|
1062
|
-
* ship (IDE domain fields). Keyed by field `type`/`ui_type`; forwarded through
|
|
1063
|
-
* `TemplateField` to the `AutoFormField` override seam.
|
|
1064
|
-
*/
|
|
1065
|
-
componentOverrides?: Record<string, React.FC<any>>;
|
|
1066
|
-
/**
|
|
1067
|
-
* Draws the read-first preview of a `code-editor` value.
|
|
1068
|
-
*
|
|
1069
|
-
* The built-in preview is a plain monospace block, because this package
|
|
1070
|
-
* cannot ship a syntax highlighter -- a code editor is the very dependency it
|
|
1071
|
-
* keeps out, which is why the *editor* for `code-editor` also arrives through
|
|
1072
|
-
* `componentOverrides`. A host that already has a highlighter supplies one
|
|
1073
|
-
* here; everyone else keeps the plain block.
|
|
1074
|
-
*/
|
|
1075
|
-
codePreviewRenderer?: TCodePreviewRenderer;
|
|
1076
|
-
/**
|
|
1077
|
-
* Draws every markdown description this form shows -- the inline row
|
|
1078
|
-
* description, the focused-editing header, and the field help dialog.
|
|
1079
|
-
*
|
|
1080
|
-
* A host with field descriptions has markdown elsewhere too, on its object
|
|
1081
|
-
* pages and in its catalogues. Without this the same description renders one
|
|
1082
|
-
* way inside a form and another way outside it, and the built-in dialect --
|
|
1083
|
-
* no GFM, no host heading scale -- is the one nobody chose. Supplying a
|
|
1084
|
-
* renderer here makes the form agree with the rest of the application.
|
|
1085
|
-
*/
|
|
1086
|
-
markdownRenderer?: TMarkdownRenderer;
|
|
1087
|
-
/**
|
|
1088
|
-
* The `ui_type` names among `componentOverrides` that select a bespoke editor
|
|
1089
|
-
* but store their value as the schema's plainer `type` (a `cron` editor stores
|
|
1090
|
-
* a string). Declaring them here keeps the field's stored `type` correct —
|
|
1091
|
-
* without it the renderer name is written into the value and validation and
|
|
1092
|
-
* round-tripping both break. Merged with reqraft's own built-in list, so a
|
|
1093
|
-
* consumer's new editor no longer needs a reqraft release to be handled.
|
|
1094
|
-
*/
|
|
1095
|
-
rendererOnlyUiTypes?: string[];
|
|
1096
|
-
/**
|
|
1097
|
-
* The `ui_type` names whose own editor renders templates as chips INLINE.
|
|
1098
|
-
*
|
|
1099
|
-
* For these the template selector is suppressed and the field's editor is
|
|
1100
|
-
* shown instead — the selector would replace a control the author can type in
|
|
1101
|
-
* with one they can only pick from. Merged with reqraft's built-in list
|
|
1102
|
-
* (`richtext`), the same way `rendererOnlyUiTypes` is.
|
|
1103
|
-
*/
|
|
1104
|
-
templateAwareUiTypes?: string[];
|
|
1105
|
-
/**
|
|
1106
|
-
* Extra `ui_type` names that mean "a value of any type", for the purpose of
|
|
1107
|
-
* offering the per-type choices in the row's menu.
|
|
1108
|
-
*
|
|
1109
|
-
* A field that pins no concrete type offers "enter a value of type X" for
|
|
1110
|
-
* each type the form knows. That was gated on the schema saying
|
|
1111
|
-
* `ui_type: 'any'` alone, which misses two cases: an option that declares its
|
|
1112
|
-
* untypedness as `type: 'auto'` and carries no `ui_type` at all (the server
|
|
1113
|
-
* spells an assertion's Expected Value exactly that way), and an option whose
|
|
1114
|
-
* `ui_type` is a HOST's own untyped editor. Merged with reqraft's built-ins
|
|
1115
|
-
* (`any`, `auto`), the same way `templateAwareUiTypes` is.
|
|
1116
|
-
*/
|
|
1117
|
-
anyLikeUiTypes?: string[];
|
|
1118
|
-
|
|
1119
|
-
/**
|
|
1120
|
-
* Bag of values forwarded from an outer FormEngine scope, used as a
|
|
1121
|
-
* fallback when a field's `inherit_props` names a sibling that isn't in
|
|
1122
|
-
* this form's own `availableOptions`. Populated automatically when
|
|
1123
|
-
* FormEngine renders a nested `arg_schema` sub-form (through
|
|
1124
|
-
* AutoFormField's hash / list mount sites) so that each level accumulates
|
|
1125
|
-
* its ancestors' inherited props. Consumers rarely set this by hand — it's
|
|
1126
|
-
* plumbing for the inherit_props scope-forwarding contract.
|
|
1127
|
-
*/
|
|
1128
|
-
inheritedFromParent?: Record<string, unknown>;
|
|
1129
|
-
|
|
1130
|
-
/**
|
|
1131
|
-
* Opt-in: on mount, drop the user straight into the first field they must
|
|
1132
|
-
* fill — the first empty, focusable field (in schema/sort order) that is
|
|
1133
|
-
* `required` or a member of a still-unsatisfied one-of `required_groups`
|
|
1134
|
-
* group. Disabled, readonly, read-only-form, and dependency-locked fields are
|
|
1135
|
-
* skipped. In compact (read-first) mode the target row is expanded through
|
|
1136
|
-
* the engine's own `expandedOptions` state, so its editor gains focus with no
|
|
1137
|
-
* DOM scraping or synthetic clicks. Strictly one-shot: it fires the first
|
|
1138
|
-
* time the form has focusable content (so an async-loaded schema is covered)
|
|
1139
|
-
* and then never again — a later value edit, server-driven field update, or
|
|
1140
|
-
* in-place schema reload will not re-focus; only a remount re-arms it. It also
|
|
1141
|
-
* never steals focus from a control the user has already moved into. No-op in
|
|
1142
|
-
* classic (non-compact) mode. Default: off.
|
|
1143
|
-
*/
|
|
1144
|
-
autoFocusFirstRequired?: boolean;
|
|
1145
|
-
|
|
1146
|
-
/**
|
|
1147
|
-
* Opt-in: on mount, OPEN the first row that needs attention — without taking
|
|
1148
|
-
* focus.
|
|
1149
|
-
*
|
|
1150
|
-
* `autoFocusFirstRequired` fuses two decisions that are not the same: which
|
|
1151
|
-
* row is open (layout) and where the caret is (focus). Focus is the half that
|
|
1152
|
-
* can be stolen, so that flag waits for focus to be free — and a form mounted
|
|
1153
|
-
* BY a click never sees free focus, because the button that mounted it still
|
|
1154
|
-
* has it. Adding a list item is exactly that case: the row the author must
|
|
1155
|
-
* fill stayed shut behind a second click, and the flag that exists to prevent
|
|
1156
|
-
* it could not fire.
|
|
1157
|
-
*
|
|
1158
|
-
* Opening a row takes nothing from the user, so this half needs no guard. Use
|
|
1159
|
-
* it where the mount is already the answer to a deliberate action; use
|
|
1160
|
-
* `autoFocusFirstRequired` where the form is the destination and the caret
|
|
1161
|
-
* should land in it. Setting both keeps the focusing behaviour. Same one-shot
|
|
1162
|
-
* contract: it fires on the first render with focusable content and then never
|
|
1163
|
-
* again for the life of the instance. No-op in classic mode. Default: off.
|
|
1164
|
-
*/
|
|
1165
|
-
expandFirstRequired?: boolean;
|
|
1166
|
-
|
|
1167
|
-
/**
|
|
1168
|
-
* Names of read-first rows to open on mount, in addition to whatever the
|
|
1169
|
-
* user opens afterwards.
|
|
1170
|
-
*
|
|
1171
|
-
* Which rows are expanded is otherwise private to the engine, which is
|
|
1172
|
-
* fine while expansion is only ever a click. It stops being fine when the
|
|
1173
|
-
* expanded row is part of an address: a consumer whose field renders its
|
|
1174
|
-
* own routable surface (a table whose rows open panes with their own URLs)
|
|
1175
|
-
* can restore the pane from the URL, but not the row that has to be open
|
|
1176
|
-
* for the pane to exist at all — so a pasted or reloaded link lands on a
|
|
1177
|
-
* collapsed form.
|
|
1178
|
-
*
|
|
1179
|
-
* Applied once, on the first render that actually CONTAINS one of the named
|
|
1180
|
-
* rows — not merely the first render with any rows — so a schema arriving in
|
|
1181
|
-
* pieces still gets its address applied rather than spending the one shot on
|
|
1182
|
-
* a form the field had not reached yet.
|
|
1183
|
-
*
|
|
1184
|
-
* It never re-expands a row the user has since collapsed, and it does not
|
|
1185
|
-
* participate in `expandMode: 'single'` accordion collapsing — the caller is
|
|
1186
|
-
* naming a starting point, not driving the state. That holds against the
|
|
1187
|
-
* engine's own openers too: `expandFirstRequired` / `autoFocusFirstRequired`
|
|
1188
|
-
* open their target ALONGSIDE these rows in single mode instead of replacing
|
|
1189
|
-
* them. Only a remount re-arms it. No-op in classic (non-compact) mode.
|
|
1190
|
-
*/
|
|
1191
|
-
initialExpandedOptions?: string[];
|
|
1192
|
-
|
|
1193
|
-
/**
|
|
1194
|
-
* Cap how many rows a status box shows before the rest fold behind a
|
|
1195
|
-
* "N more fields" control. Applies to every box — Needs attention, Set and
|
|
1196
|
-
* Optional alike — so a long form stays scannable instead of making the
|
|
1197
|
-
* reader page past one box to reach the next.
|
|
1198
|
-
*
|
|
1199
|
-
* Optional keeps its own rule on top of this: a box that would have
|
|
1200
|
-
* collapsed but holds preselected rows shows those rows rather than an
|
|
1201
|
-
* arbitrary first-N. The cap then applies to that set, so
|
|
1202
|
-
* `maxFieldsShown: 5` over 10 preselected fields shows 5 — and opening the
|
|
1203
|
-
* control reveals EVERY row in the box, not just the other 5 preselected
|
|
1204
|
-
* ones, because once the reader has asked to see more, hiding the rest is
|
|
1205
|
-
* the same defect one level down.
|
|
1206
|
-
*
|
|
1207
|
-
* Unset (the default) leaves every box uncapped.
|
|
1208
|
-
*/
|
|
1209
|
-
maxFieldsShown?: number;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// Option types rendered full-width (IDE Options parity, commit 8e6b7781).
|
|
1213
|
-
const STRECHABLE_TYPES = new Set<TQorusType>(['tool-catalog' as TQorusType]);
|
|
1214
|
-
|
|
1215
|
-
const FormEngineImpl = ({
|
|
1216
|
-
name,
|
|
1217
|
-
uniqueName,
|
|
1218
|
-
value,
|
|
1219
|
-
onChange,
|
|
1220
|
-
onSingleOptionsChange,
|
|
1221
|
-
onDependableOptionChange,
|
|
1222
|
-
placeholder,
|
|
1223
|
-
noValueString, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
1224
|
-
isValid, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
1225
|
-
url,
|
|
1226
|
-
customUrl,
|
|
1227
|
-
operatorsUrl,
|
|
1228
|
-
onOptionsLoaded,
|
|
1229
|
-
recordRequiresSearchOptions,
|
|
1230
|
-
readOnly,
|
|
1231
|
-
onReadOnlyActivate,
|
|
1232
|
-
allowTemplates = true,
|
|
1233
|
-
interfaceContext,
|
|
1234
|
-
templateFieldProps,
|
|
1235
|
-
forceDropdown,
|
|
1236
|
-
showTypeToggle = true,
|
|
1237
|
-
compact,
|
|
1238
|
-
// Destructured only to keep it out of `rest` (and so off the DOM). The gutter
|
|
1239
|
-
// this prop documents was never implemented: it was passed to the wrap as
|
|
1240
|
-
// `$flush`, whose styled template had no interpolation slot, so it has always
|
|
1241
|
-
// been a no-op. Removing it from the API is a separate, breaking change —
|
|
1242
|
-
// every consumer passing it today already gets the flush layout by default.
|
|
1243
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1244
|
-
compactFlush = false,
|
|
1245
|
-
compactNested = false,
|
|
1246
|
-
compactScroll = 'host',
|
|
1247
|
-
compactPanelProps,
|
|
1248
|
-
compactCollapsedGroups: compactCollapsedGroupsProp,
|
|
1249
|
-
compactBoxLabels,
|
|
1250
|
-
compactStickyBoxHeaders = true,
|
|
1251
|
-
compactStickyOffset = 0,
|
|
1252
|
-
compactToolbar = true,
|
|
1253
|
-
commitMode = 'immediate',
|
|
1254
|
-
expandMode = 'single',
|
|
1255
|
-
onCommit,
|
|
1256
|
-
operators: operatorsProp,
|
|
1257
|
-
groups,
|
|
1258
|
-
optionsLoader,
|
|
1259
|
-
onValidityChange,
|
|
1260
|
-
optionActions,
|
|
1261
|
-
optionActionsCollapse = 'auto',
|
|
1262
|
-
componentOverrides,
|
|
1263
|
-
codePreviewRenderer,
|
|
1264
|
-
// consumed by the wrapper below, which publishes it to every description this
|
|
1265
|
-
// form draws; destructured here only so it cannot reach `rest` and be spread
|
|
1266
|
-
// onto a DOM node
|
|
1267
|
-
markdownRenderer: _markdownRenderer, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
1268
|
-
rendererOnlyUiTypes,
|
|
1269
|
-
templateAwareUiTypes,
|
|
1270
|
-
anyLikeUiTypes,
|
|
1271
|
-
inheritedFromParent,
|
|
1272
|
-
autoFocusFirstRequired,
|
|
1273
|
-
expandFirstRequired,
|
|
1274
|
-
initialExpandedOptions,
|
|
1275
|
-
maxFieldsShown,
|
|
1276
|
-
...rest
|
|
1277
|
-
}: IFormEngineProps) => {
|
|
1278
|
-
// Built-ins + whatever the consumer declared for its own injected editors.
|
|
1279
|
-
const isRendererOnly = useMemo(
|
|
1280
|
-
() => createRendererOnlyUiTypeCheck(rendererOnlyUiTypes),
|
|
1281
|
-
[JSON.stringify(rendererOnlyUiTypes)]
|
|
1282
|
-
);
|
|
1283
|
-
// Pointer CAPABILITY and viewport WIDTH are different questions and both
|
|
1284
|
-
// matter here: a hover-gated action is unreachable without a hover, and a
|
|
1285
|
-
// phone-width row has no space for a button strip either way. Both now come
|
|
1286
|
-
// from Reqore's context (one subscription for the whole app) rather than a
|
|
1287
|
-
// reqraft-local matchMedia hook.
|
|
1288
|
-
//
|
|
1289
|
-
// Both are always set by `ReqoreProvider`, and the peer floor (>= 0.74.0) is
|
|
1290
|
-
// the version that sets them. A defensive `?? true` here would be dead code:
|
|
1291
|
-
// `useReqoreProperty` THROWS on a context that lacks the key, so an older
|
|
1292
|
-
// Reqore fails at the hook rather than degrading to a default.
|
|
1293
|
-
const isHoverCapable = useReqoreProperty('isHoverCapable');
|
|
1294
|
-
const isMobile = useReqoreProperty('isMobile');
|
|
1295
|
-
const collapseOptionActions =
|
|
1296
|
-
optionActionsCollapse === 'always' ? true
|
|
1297
|
-
: optionActionsCollapse === 'never' ? false
|
|
1298
|
-
: !isHoverCapable || !!isMobile;
|
|
1299
|
-
/* The schema, as the form should ASK it — not always as it was served.
|
|
1300
|
-
`resolveDegenerateRequiredGroups` turns a one-of group that has a single
|
|
1301
|
-
member in this schema back into a plain required field, because a choice
|
|
1302
|
-
with one option is not a choice. Applied here, at the one place the schema
|
|
1303
|
-
enters the component, so the header, the chips, the messages, the meter and
|
|
1304
|
-
the read-first summary cannot disagree about it: every one of them reads
|
|
1305
|
-
`options`, and none of them has to know the rule. */
|
|
1306
|
-
const [servedOptions, setOptions] = useState<IQorusFormSchema | undefined>(
|
|
1307
|
-
rest?.options || undefined
|
|
1308
|
-
);
|
|
1309
|
-
const options = useMemo(() => resolveDegenerateRequiredGroups(servedOptions), [servedOptions]);
|
|
1310
|
-
// optionsLoader lifecycle: loading feeds the skeleton gate, error the banner.
|
|
1311
|
-
const [optionsLoading, setOptionsLoading] = useState<boolean>(!!optionsLoader && !rest?.options);
|
|
1312
|
-
const [optionsError, setOptionsError] = useState<string | undefined>();
|
|
1313
|
-
// Operators: prop-provided (compact) or fetched via operatorsUrl (dpql,
|
|
1314
|
-
// ported from IDE Options) — the fetch overrides the seeded prop value.
|
|
1315
|
-
const [operators, setOperators] = useState<IOperatorsSchema | undefined>(operatorsProp);
|
|
1316
|
-
// Remote-fetch loading (ported from IDE Options); only relevant when one of
|
|
1317
|
-
// the fetch urls is set — schema-as-props consumers never see the skeleton.
|
|
1318
|
-
const [loading, setLoading] = useState<boolean>(!!(url || customUrl || operatorsUrl));
|
|
1319
|
-
const confirmAction = useReqoreProperty('confirmAction');
|
|
1320
|
-
const theme = useReqoreTheme();
|
|
1321
|
-
const [focusedEditing, setFocusedEditing] = useState<string>();
|
|
1322
|
-
const [showFieldTypes, setShowFieldTypes] = useState<boolean>(false);
|
|
1323
|
-
// Global toggle (toolbar ⓘ): reveal the short-description info panel on every
|
|
1324
|
-
// field that has a short_desc. Per-row ⓘ overrides still win over it.
|
|
1325
|
-
// Global field-info visibility, tri-state: `undefined` = default (critical
|
|
1326
|
-
// messages auto-open, the rest closed); `true` = show all; `false` = hide all
|
|
1327
|
-
// (even message fields). The toolbar ⓘ drives it.
|
|
1328
|
-
const [showAllDescriptions, setShowAllDescriptions] = useState<boolean | undefined>(undefined);
|
|
1329
|
-
const [showHelpForOption, setShowHelpForOption] = useState<string | undefined>();
|
|
1330
|
-
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
1331
|
-
// Which options are expanded into their editor (several can be open at once).
|
|
1332
|
-
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
1333
|
-
/** Status boxes whose "N more fields" control the reader has opened. Keyed by
|
|
1334
|
-
box so opening Optional does not also unfold Set. */
|
|
1335
|
-
const [expandedMoreBoxes, setExpandedMoreBoxes] = useState<TFormEngineBoxKey[]>([]);
|
|
1336
|
-
|
|
1337
|
-
// --- Reveal a status box's content when it is opened ----------------------
|
|
1338
|
-
//
|
|
1339
|
-
// The status boxes stack, so the last one ("Optional", which holds every
|
|
1340
|
-
// not-yet-added field) sits at the bottom of the form — frequently at the
|
|
1341
|
-
// bottom of the scroll container too. Opening it mounts its rows BELOW the
|
|
1342
|
-
// fold: the box's own header is all that stays on screen, the click appears to
|
|
1343
|
-
// have done nothing, and the fields it just revealed have to be hunted for by
|
|
1344
|
-
// scrolling.
|
|
1345
|
-
//
|
|
1346
|
-
// The box that was just opened, read by the layout effect below. Held as state
|
|
1347
|
-
// rather than acted on inside the collapse handler because the handler runs
|
|
1348
|
-
// BEFORE React re-renders — at that moment the rows do not exist yet, so the
|
|
1349
|
-
// box has not grown and there is nothing to scroll to. A layout effect runs
|
|
1350
|
-
// after the DOM is updated and before paint, which makes the reveal
|
|
1351
|
-
// deterministic instead of a guessed delay.
|
|
1352
|
-
const [openedStatusBox, setOpenedStatusBox] = useState<string | undefined>(undefined);
|
|
1353
|
-
const statusBoxRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
1354
|
-
useLayoutEffect(() => {
|
|
1355
|
-
if (!openedStatusBox) {
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
const element = statusBoxRefs.current[openedStatusBox];
|
|
1359
|
-
// Consume it either way: a box that has gone (a search narrowed it out of
|
|
1360
|
-
// existence) must not leave a pending reveal armed for a later render.
|
|
1361
|
-
setOpenedStatusBox(undefined);
|
|
1362
|
-
// `scrollIntoView` is not implemented in jsdom, and a missing reveal must
|
|
1363
|
-
// never break a form.
|
|
1364
|
-
if (!element?.scrollIntoView) {
|
|
1365
|
-
return;
|
|
1366
|
-
}
|
|
1367
|
-
// 'nearest' scrolls the MINIMUM needed: nothing at all when the box is
|
|
1368
|
-
// already fully visible, and a top-alignment when it is taller than the
|
|
1369
|
-
// viewport (header plus the first rows — the right answer for a long
|
|
1370
|
-
// Optional list). Anything else moves the page when it did not need to.
|
|
1371
|
-
element.scrollIntoView({
|
|
1372
|
-
block: 'nearest',
|
|
1373
|
-
behavior:
|
|
1374
|
-
window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth',
|
|
1375
|
-
});
|
|
1376
|
-
}, [openedStatusBox]);
|
|
1377
|
-
// Remembers each row's last settled status box, so an actively-edited field
|
|
1378
|
-
// stays put when its status flips (e.g. becomes valid) instead of jumping to
|
|
1379
|
-
// another box mid-edit and stealing focus. Keyed by option name.
|
|
1380
|
-
const settledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
|
|
1381
|
-
// Measured form width (not viewport — the form lives in drawers/panels of
|
|
1382
|
-
// arbitrary width) drives the stacked narrow layout.
|
|
1383
|
-
const [compactWrapRef, { width: compactWrapWidth }] = useMeasure<HTMLDivElement>();
|
|
1384
|
-
// Own handle on the scroll wrap (useMeasure's ref is a callback, no `.current`)
|
|
1385
|
-
// so the label-column measurement can publish its CSS var on the element. It's
|
|
1386
|
-
// STATE, not a ref: the wrap mounts only after the loading-skeleton gate
|
|
1387
|
-
// resolves, so the measurement effect must re-run when the node appears — a
|
|
1388
|
-
// ref wouldn't retrigger it.
|
|
1389
|
-
const [compactWrapNode, setCompactWrapNode] = useState<HTMLDivElement | null>(null);
|
|
1390
|
-
// Ref mirror of the wrap node for callbacks that must not re-create when it
|
|
1391
|
-
// mounts (flashOptions reads it inside a rAF).
|
|
1392
|
-
const compactWrapNodeRef = useRef<HTMLDivElement | null>(null);
|
|
1393
|
-
const setCompactWrap = useCallback(
|
|
1394
|
-
(node: HTMLDivElement | null) => {
|
|
1395
|
-
compactWrapRef(node);
|
|
1396
|
-
compactWrapNodeRef.current = node;
|
|
1397
|
-
setCompactWrapNode((prev) => (prev === node ? prev : node));
|
|
1398
|
-
},
|
|
1399
|
-
[compactWrapRef]
|
|
1400
|
-
);
|
|
1401
|
-
/* Declared here, not where it is first USED in the render: the sticky
|
|
1402
|
-
measurement below takes it as a dependency, and a dependency array is
|
|
1403
|
-
evaluated during render — a `const` declared further down would still be
|
|
1404
|
-
in its temporal dead zone. */
|
|
1405
|
-
const compactNarrow = !!compactWrapWidth && compactWrapWidth < 480;
|
|
1406
|
-
|
|
1407
|
-
/* How much pinned chrome sits above a status-box header.
|
|
1408
|
-
|
|
1409
|
-
Sticky offsets do not compose on their own: everything pinned in one
|
|
1410
|
-
scroller resolves `top` against the same scrollport, so a box header and
|
|
1411
|
-
this form's own toolbar both asking for 0 land on top of each other — and
|
|
1412
|
-
the toolbar is the loser, because it renders first and the box header wins
|
|
1413
|
-
on z-order. Covering it hides the search, which is the one control that
|
|
1414
|
-
forces a collapsed box open.
|
|
1415
|
-
|
|
1416
|
-
So the box headers pin BELOW the toolbar, at the host's own offset plus the
|
|
1417
|
-
toolbar's height. Measured, not assumed: the toolbar is a completion meter,
|
|
1418
|
-
a search row and a field picker that each hide themselves when they have
|
|
1419
|
-
nothing to say, and what is left wraps differently at every form width.
|
|
1420
|
-
Zero when this form draws no toolbar at all. */
|
|
1421
|
-
const [compactToolbarHeight, setCompactToolbarHeight] = useState(0);
|
|
1422
|
-
useLayoutEffect(() => {
|
|
1423
|
-
const wrap = compactWrapNode;
|
|
1424
|
-
if (!compact || !wrap || compactNested || !compactStickyBoxHeaders) {
|
|
1425
|
-
setCompactToolbarHeight(0);
|
|
1426
|
-
return undefined;
|
|
1427
|
-
}
|
|
1428
|
-
const header = wrap.querySelector<HTMLElement>(
|
|
1429
|
-
':scope > .reqore-panel > .reqore-panel-title'
|
|
1430
|
-
);
|
|
1431
|
-
if (!header) {
|
|
1432
|
-
setCompactToolbarHeight(0);
|
|
1433
|
-
return undefined;
|
|
1434
|
-
}
|
|
1435
|
-
const read = () =>
|
|
1436
|
-
setCompactToolbarHeight((prev) => {
|
|
1437
|
-
/* Only a toolbar that is ITSELF pinned takes room off the top. A host
|
|
1438
|
-
can switch the pin off (`compactPanelProps.stickyHeader`), and it
|
|
1439
|
-
does where the form is a couple of fields inside a longer page — and
|
|
1440
|
-
a box header offset by a toolbar that scrolls away would float a
|
|
1441
|
-
header's height below the top edge with nothing under it. Read from
|
|
1442
|
-
the element rather than from the props that produced it, so every
|
|
1443
|
-
route to that decision is accounted for. */
|
|
1444
|
-
const pinned = getComputedStyle(header).position === 'sticky';
|
|
1445
|
-
const next = pinned ? Math.round(header.getBoundingClientRect().height) : 0;
|
|
1446
|
-
return prev === next ? prev : next;
|
|
1447
|
-
});
|
|
1448
|
-
read();
|
|
1449
|
-
/* jsdom and older engines have no ResizeObserver; the one-shot read above
|
|
1450
|
-
still gives the right answer for a form whose toolbar never reflows. */
|
|
1451
|
-
if (typeof ResizeObserver === 'undefined') {
|
|
1452
|
-
return undefined;
|
|
1453
|
-
}
|
|
1454
|
-
const observer = new ResizeObserver(read);
|
|
1455
|
-
observer.observe(header);
|
|
1456
|
-
return () => observer.disconnect();
|
|
1457
|
-
/* `compactNarrow` is a dependency, not decoration: crossing the width
|
|
1458
|
-
breakpoint takes the pin OFF the toolbar without changing its height, so
|
|
1459
|
-
the ResizeObserver never fires and a stale 124px would go on offsetting
|
|
1460
|
-
the box headers below a toolbar that is no longer there. */
|
|
1461
|
-
}, [compact, compactNested, compactStickyBoxHeaders, compactNarrow, compactWrapNode, options]);
|
|
1462
|
-
|
|
1463
|
-
// Global label-column sizing: size the label column to the WIDEST field label
|
|
1464
|
-
// across the whole form, clamped to [MIN, MAX], and publish it as a CSS var on
|
|
1465
|
-
// the scroll wrap. Both the grid column and the value-surface offsets read the
|
|
1466
|
-
// var, so the surface stays glued to the (now variable) column edge. Measured
|
|
1467
|
-
// off-DOM from the option labels — stable regardless of filtering/scroll, so it
|
|
1468
|
-
// never reflows as rows come and go.
|
|
1469
|
-
useLayoutEffect(() => {
|
|
1470
|
-
const wrap = compactWrapNode;
|
|
1471
|
-
if (!compact || !wrap) {
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
const family = getComputedStyle(wrap).fontFamily || 'sans-serif';
|
|
1475
|
-
const measurer = document.createElement('span');
|
|
1476
|
-
// Match StyledRowLabel's typography (font-weight 600, 13px) so the measured
|
|
1477
|
-
// width matches what the row actually renders.
|
|
1478
|
-
measurer.style.cssText = `position:absolute;left:-9999px;top:-9999px;visibility:hidden;white-space:nowrap;font:600 13px ${family};`;
|
|
1479
|
-
document.body.appendChild(measurer);
|
|
1480
|
-
let widest = 0;
|
|
1481
|
-
forEach(options || {}, (schema, name) => {
|
|
1482
|
-
measurer.textContent = (schema?.display_name as string) || name;
|
|
1483
|
-
widest = Math.max(widest, measurer.offsetWidth);
|
|
1484
|
-
});
|
|
1485
|
-
document.body.removeChild(measurer);
|
|
1486
|
-
// The affordances are added AFTER the clamp, not inside it.
|
|
1487
|
-
//
|
|
1488
|
-
// They used to be inside — `min(MAX, widest + 28)` — which quietly spends the
|
|
1489
|
-
// allowance the moment the labels are long enough to reach the ceiling. Past
|
|
1490
|
-
// that point the column is exactly MAX and the text is free to use all of it,
|
|
1491
|
-
// so the trailing `?` has nowhere to sit on the last line and wraps onto a
|
|
1492
|
-
// line of its own. That is what an auth profile's authorization block showed:
|
|
1493
|
-
// "Require All Of These Permissions" filling the column with a lone `?`
|
|
1494
|
-
// beneath it.
|
|
1495
|
-
//
|
|
1496
|
-
// Clamping the TEXT and then adding the affordance means the reserved space
|
|
1497
|
-
// survives at every label length. MAX still bounds how much room the NAME may
|
|
1498
|
-
// take, which is what it is for; the asterisk and the `?` are chrome that has
|
|
1499
|
-
// to fit beside it either way.
|
|
1500
|
-
const text = Math.max(LABEL_COL_MIN, Math.min(LABEL_COL_MAX, Math.round(widest)));
|
|
1501
|
-
wrap.style.setProperty(LABEL_COL_VAR, `${text + LABEL_AFFORDANCE_WIDTH}px`);
|
|
1502
|
-
}, [compact, options, theme, compactWrapNode]);
|
|
1503
|
-
// Editing rows pin min-height to the measured read row they replace, so the
|
|
1504
|
-
// toggle never shifts neighbours (height varies with chrome — measure it).
|
|
1505
|
-
const readRowHeights = useRef<Record<string, number>>({});
|
|
1506
|
-
// Required-groups linkage: hovering a group chip highlights every member row;
|
|
1507
|
-
// clicking a sibling in the chip's popover scrolls to it and flashes it.
|
|
1508
|
-
const [highlightedOptions, setHighlightedOptions] = useState<string[]>([]);
|
|
1509
|
-
const [flashedOptions, setFlashedOptions] = useState<string[]>([]);
|
|
1510
|
-
const flashTimeout = useRef<ReturnType<typeof setTimeout>>();
|
|
1511
|
-
const flashOptions = useCallback((optionNames: string[], scrollToFirst = false) => {
|
|
1512
|
-
if (scrollToFirst && optionNames[0]) {
|
|
1513
|
-
// Defer to the next frame: when this fires for a field that just changed
|
|
1514
|
-
// panels, its row has only just re-mounted in the new box — scrolling in the
|
|
1515
|
-
// same tick targets the stale (pre-move) layout, so the page doesn't budge.
|
|
1516
|
-
// A rAF lets the new position settle first.
|
|
1517
|
-
requestAnimationFrame(() => {
|
|
1518
|
-
// Scoped to THIS engine's wrap: several compact engines can be mounted at
|
|
1519
|
-
// once with colliding field names (e.g. one form per state in the qog
|
|
1520
|
-
// template drawer, each with `guild`/`channel`), and a document-wide
|
|
1521
|
-
// lookup grabs the first match in the DOM — scrolling the container to a
|
|
1522
|
-
// DIFFERENT form's row. No document fallback for the same reason.
|
|
1523
|
-
const target = compactWrapNodeRef.current?.querySelector<HTMLElement>(
|
|
1524
|
-
`.readfirst-row[data-field="${optionNames[0]}"]`
|
|
1525
|
-
);
|
|
1526
|
-
// Only when it is not already in view, and never under a pinned box
|
|
1527
|
-
// header: `block: 'center'` scrolled a row that was in plain sight and
|
|
1528
|
-
// centred it against the whole scrollport, so the header pinned at the
|
|
1529
|
-
// top was drawn over it (see `revealRow`).
|
|
1530
|
-
if (target) {
|
|
1531
|
-
revealRow(
|
|
1532
|
-
target,
|
|
1533
|
-
window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth'
|
|
1534
|
-
);
|
|
1535
|
-
}
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
setFlashedOptions(optionNames);
|
|
1539
|
-
clearTimeout(flashTimeout.current);
|
|
1540
|
-
flashTimeout.current = setTimeout(() => setFlashedOptions([]), 1400);
|
|
1541
|
-
}, []);
|
|
1542
|
-
const flashOption = useCallback(
|
|
1543
|
-
(optionName: string) => flashOptions([optionName], true),
|
|
1544
|
-
[flashOptions]
|
|
1545
|
-
);
|
|
1546
|
-
useEffect(() => () => clearTimeout(flashTimeout.current), []);
|
|
1547
|
-
|
|
1548
|
-
// Follow a field across panels: when its status bucket changes — e.g. you fill
|
|
1549
|
-
// an optional field and it jumps to Set / Needs attention — scroll to its new
|
|
1550
|
-
// row and flash it so it's easy to keep track of. `settledBucket` holds each
|
|
1551
|
-
// field's current panel (frozen while the field is being edited, it re-buckets
|
|
1552
|
-
// on collapse), so diffing it after every render catches the move the instant it
|
|
1553
|
-
// lands in the new panel. Runs every render; the diff is cheap and only fires a
|
|
1554
|
-
// scroll on an ACTUAL move of a non-expanded field.
|
|
1555
|
-
const prevSettledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
|
|
1556
|
-
useEffect(() => {
|
|
1557
|
-
if (!compact) return;
|
|
1558
|
-
const cur = settledBucket.current;
|
|
1559
|
-
const prev = prevSettledBucket.current;
|
|
1560
|
-
const moved = Object.keys(cur).find(
|
|
1561
|
-
(name) => prev[name] && prev[name] !== cur[name] && !expandedOptions.includes(name)
|
|
1562
|
-
);
|
|
1563
|
-
prevSettledBucket.current = { ...cur };
|
|
1564
|
-
if (moved) {
|
|
1565
|
-
flashOptions([moved], true);
|
|
1566
|
-
}
|
|
1567
|
-
});
|
|
1568
|
-
|
|
1569
|
-
// Info panels auto-open on Tier-1 content; the per-row user override sticks.
|
|
1570
|
-
const [infoPanelOverrides, setInfoPanelOverrides] = useState<Record<string, boolean>>({});
|
|
1571
|
-
// Toolbar filters affect the listed rows only — the meter reflects the full set.
|
|
1572
|
-
const [requiredOnly, setRequiredOnly] = useState<boolean>(false);
|
|
1573
|
-
const [compactQuery, setCompactQuery] = useState<string>('');
|
|
1574
|
-
// Compact field sort (Fields menu → "Sort by"); 'schema' = declared order.
|
|
1575
|
-
const [compactSort, setCompactSort] = useState<TCompactSort>('schema');
|
|
1576
|
-
const [localValue, setLocalValue] = useState<{
|
|
1577
|
-
fields: TQorusForm | TQorusFlatForm;
|
|
1578
|
-
meta?: IOptionsOnChangeMeta;
|
|
1579
|
-
}>(() => ({
|
|
1580
|
-
fields: fixOptions(value, options || {}, undefined, isRendererOnly),
|
|
1581
|
-
meta: undefined,
|
|
1582
|
-
}));
|
|
1583
|
-
const originalValue = useRef<any>();
|
|
1584
|
-
// Track the last value we emitted via onChange so we can skip re-applying fixOptions
|
|
1585
|
-
// when the parent echoes it back as the new value prop (controlled component loop prevention)
|
|
1586
|
-
const lastEmittedValue = useRef<TQorusForm | TQorusFlatForm | undefined>(value);
|
|
1587
|
-
/**
|
|
1588
|
-
* A `localValue` that is only a MIRROR of the incoming `value`, and so must
|
|
1589
|
-
* never be emitted back as though this form had answered something.
|
|
1590
|
-
*
|
|
1591
|
-
* `localValue` is derived state: the sync-down effect below rebuilds it from
|
|
1592
|
-
* the `value` prop. When that rebuild adds nothing (`fixOptions` had no
|
|
1593
|
-
* default to materialise), the result says exactly what the parent already
|
|
1594
|
-
* holds, and emitting it is not a change — it is this form repeating the
|
|
1595
|
-
* parent's own words back at it.
|
|
1596
|
-
*
|
|
1597
|
-
* That repetition is harmless in a form that owns its state outright and
|
|
1598
|
-
* fatal in one that shares a parent with a NESTED engine. The two write to
|
|
1599
|
-
* the same parent value, so each sees the other's write arrive as an external
|
|
1600
|
-
* change; `lastEmittedValue` is then always one step behind and its guard
|
|
1601
|
-
* never fires. The engines alternate between two states forever, several
|
|
1602
|
-
* times a second, and the newer write is repeatedly overwritten by the older
|
|
1603
|
-
* one. Measured in the test editor: the row's summary flipped between two
|
|
1604
|
-
* readings indefinitely and `setTimeout` was starved past a 45-second
|
|
1605
|
-
* deadline.
|
|
1606
|
-
*
|
|
1607
|
-
* Only a mirror that CHANGES nothing is held back. A rebuild that restores a
|
|
1608
|
-
* required or preselected field's default still emits, because that default
|
|
1609
|
-
* is genuinely new information the parent has to be told about — which is the
|
|
1610
|
-
* whole reason the sync-down runs `fixOptions` at all.
|
|
1611
|
-
*/
|
|
1612
|
-
const mirroredValue = useRef<TQorusForm | TQorusFlatForm | undefined>(undefined);
|
|
1613
|
-
/**
|
|
1614
|
-
* Every emit the parent has yet to feed back, not only the last one. A parent that takes the value back
|
|
1615
|
-
* late (a store, a debounced owner, a slow render) echoes an older emit after this form has emitted a newer
|
|
1616
|
-
* one. Compared with `lastEmittedValue` alone, that echo read as the parent's own change, and the sync-down
|
|
1617
|
-
* reset the form to it: the field under the cursor went back a step and the keys typed since were lost
|
|
1618
|
-
* ("I have value" came out as "I he value" in a story under load).
|
|
1619
|
-
*/
|
|
1620
|
-
const inFlight = useEmittedValues<TQorusForm | TQorusFlatForm | undefined>((a, b) =>
|
|
1621
|
-
isEqual(normalizeEmptyFieldValues(a), normalizeEmptyFieldValues(b))
|
|
1622
|
-
);
|
|
1623
|
-
|
|
1624
|
-
if (originalValue.current === undefined && size(value)) {
|
|
1625
|
-
originalValue.current = localValue.fields;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
const unavailableOptionsCount = useRef(0);
|
|
1629
|
-
const { compactValue, loading: typesLoading } = useQorusTypes();
|
|
1630
|
-
const templates = useTemplates(allowTemplates, rest.stringTemplates, interfaceContext);
|
|
1631
|
-
|
|
1632
|
-
useEffect(() => {
|
|
1633
|
-
if (
|
|
1634
|
-
!shouldEmitLocalValue({
|
|
1635
|
-
localValue: localValue.fields,
|
|
1636
|
-
value,
|
|
1637
|
-
mirroredValue: mirroredValue.current,
|
|
1638
|
-
})
|
|
1639
|
-
) {
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
const toEmit = size(localValue.fields) ? (localValue.fields as TQorusForm) : undefined;
|
|
1644
|
-
lastEmittedValue.current = toEmit;
|
|
1645
|
-
inFlight.record(toEmit);
|
|
1646
|
-
const meta = size(localValue.meta) ? localValue.meta : undefined;
|
|
1647
|
-
// Batched mode still emits every staged change (consumers may want to
|
|
1648
|
-
// live-validate), but flags it as a draft — persistence waits for Save.
|
|
1649
|
-
onChange?.(name, toEmit, commitMode === 'batched' ? { ...(meta || {}), draft: true } : meta);
|
|
1650
|
-
}, [JSON.stringify(localValue)]);
|
|
1651
|
-
|
|
1652
|
-
useUpdateEffect(() => {
|
|
1653
|
-
// When a loader owns the schema, ignore controlled `options` syncs so a
|
|
1654
|
-
// late/undefined `options` prop can't clobber the loaded schema.
|
|
1655
|
-
if (optionsLoader) {
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
1658
|
-
setOptions(rest.options);
|
|
1659
|
-
}, [JSON.stringify(rest.options)]);
|
|
1660
|
-
|
|
1661
|
-
// Fetch the schema on mount and on loader identity change.
|
|
1662
|
-
useEffect(() => {
|
|
1663
|
-
if (!optionsLoader) {
|
|
1664
|
-
return undefined;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
let cancelled = false;
|
|
1668
|
-
setOptionsLoading(true);
|
|
1669
|
-
setOptionsError(undefined);
|
|
1670
|
-
|
|
1671
|
-
// `Promise.resolve().then(...)` so a synchronous throw in the loader is
|
|
1672
|
-
// funnelled into the same rejection path as an async failure.
|
|
1673
|
-
Promise.resolve()
|
|
1674
|
-
.then(() => optionsLoader())
|
|
1675
|
-
.then((loaded) => {
|
|
1676
|
-
if (cancelled) {
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
setOptions(loaded || {});
|
|
1680
|
-
onOptionsLoaded?.(loaded || {});
|
|
1681
|
-
setOptionsLoading(false);
|
|
1682
|
-
})
|
|
1683
|
-
.catch((error) => {
|
|
1684
|
-
if (cancelled) {
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
|
-
/* A loader built on `query` rejects with a cancel when its request is
|
|
1688
|
-
abandoned (the query cache was cleared mid-flight). The schema was not
|
|
1689
|
-
refused, so it is not an error to show: the form just stops waiting. */
|
|
1690
|
-
if (isQueryCancelled(error)) {
|
|
1691
|
-
setOptionsLoading(false);
|
|
1692
|
-
return;
|
|
1693
|
-
}
|
|
1694
|
-
setOptionsError(
|
|
1695
|
-
error instanceof Error ? error.message : String(error ?? 'Failed to load options')
|
|
1696
|
-
);
|
|
1697
|
-
setOptionsLoading(false);
|
|
1698
|
-
});
|
|
1699
|
-
|
|
1700
|
-
return () => {
|
|
1701
|
-
cancelled = true;
|
|
1702
|
-
};
|
|
1703
|
-
}, [optionsLoader]);
|
|
1704
|
-
|
|
1705
|
-
// --- Remote schema fetching, ported from IDE Options (systemOptions.tsx
|
|
1706
|
-
// lines 394-462 + 504-522). reqraft's `query()` replaces the IDE's
|
|
1707
|
-
// `fetchData` — same `{ data, ok, error }` shape and `api/latest/` prefix;
|
|
1708
|
-
// the IDE's leading-slash URLs are normalised away.
|
|
1709
|
-
const getFetchUrl = useCallback(() => customUrl || `options/${url}`, [customUrl, url]);
|
|
1710
|
-
|
|
1711
|
-
useMount(() => {
|
|
1712
|
-
if (url || customUrl) {
|
|
1713
|
-
(async () => {
|
|
1714
|
-
setOptions(undefined);
|
|
1715
|
-
setLoading(true);
|
|
1716
|
-
const data = await queryUnlessCancelled<IQorusFormSchema>({ url: getFetchUrl() });
|
|
1717
|
-
|
|
1718
|
-
// abandoned: nobody is waiting for this schema any more
|
|
1719
|
-
if (!data) {
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
if (!data.ok || data.data === null) {
|
|
1724
|
-
setLoading(false);
|
|
1725
|
-
setOptions({});
|
|
1726
|
-
return;
|
|
1727
|
-
}
|
|
1728
|
-
setLocalValue({
|
|
1729
|
-
fields: fixOptions(value, data.data, undefined, isRendererOnly),
|
|
1730
|
-
meta: undefined,
|
|
1731
|
-
});
|
|
1732
|
-
if (!operatorsUrl) {
|
|
1733
|
-
setLoading(false);
|
|
1734
|
-
}
|
|
1735
|
-
setOptions(data.data);
|
|
1736
|
-
onOptionsLoaded?.(data.data);
|
|
1737
|
-
})();
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
if (operatorsUrl) {
|
|
1741
|
-
(async () => {
|
|
1742
|
-
setOperators(undefined);
|
|
1743
|
-
setLoading(true);
|
|
1744
|
-
const data = await queryUnlessCancelled<IOperatorsSchema>({
|
|
1745
|
-
url: operatorsUrl.replace(/^\//, ''),
|
|
1746
|
-
});
|
|
1747
|
-
|
|
1748
|
-
if (!data) {
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
if (!data.ok) {
|
|
1753
|
-
setLoading(false);
|
|
1754
|
-
setOperators({});
|
|
1755
|
-
return;
|
|
1756
|
-
}
|
|
1757
|
-
setOperators(data.data);
|
|
1758
|
-
setLoading(false);
|
|
1759
|
-
})();
|
|
1760
|
-
}
|
|
1761
|
-
});
|
|
1762
|
-
|
|
1763
|
-
// Changing the source clears the current value and re-seeds the form from
|
|
1764
|
-
// the freshly fetched schema (IDE semantics).
|
|
1765
|
-
useUpdateEffect(() => {
|
|
1766
|
-
if (url || customUrl) {
|
|
1767
|
-
(async () => {
|
|
1768
|
-
setOptions(undefined);
|
|
1769
|
-
setLoading(true);
|
|
1770
|
-
const data = await queryUnlessCancelled<IQorusFormSchema>({ url: getFetchUrl() });
|
|
1771
|
-
|
|
1772
|
-
// abandoned: nobody is waiting for this schema any more
|
|
1773
|
-
if (!data) {
|
|
1774
|
-
return;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
if (!data.ok) {
|
|
1778
|
-
setLoading(false);
|
|
1779
|
-
setOptions({});
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
if (!operatorsUrl) {
|
|
1783
|
-
setLoading(false);
|
|
1784
|
-
}
|
|
1785
|
-
setOptions(data.data);
|
|
1786
|
-
onOptionsLoaded?.(data.data);
|
|
1787
|
-
setLocalValue({
|
|
1788
|
-
fields: fixOptions({}, data.data, undefined, isRendererOnly),
|
|
1789
|
-
meta: undefined,
|
|
1790
|
-
});
|
|
1791
|
-
})();
|
|
1792
|
-
}
|
|
1793
|
-
}, [url, customUrl]);
|
|
1794
|
-
|
|
1795
|
-
useUpdateEffect(() => {
|
|
1796
|
-
if (operatorsUrl) {
|
|
1797
|
-
(async () => {
|
|
1798
|
-
setOperators(undefined);
|
|
1799
|
-
setLoading(true);
|
|
1800
|
-
const data = await queryUnlessCancelled<IOperatorsSchema>({
|
|
1801
|
-
url: operatorsUrl.replace(/^\//, ''),
|
|
1802
|
-
});
|
|
1803
|
-
|
|
1804
|
-
if (!data) {
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
if (!data.ok) {
|
|
1809
|
-
setLoading(false);
|
|
1810
|
-
setOperators({});
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
setLoading(false);
|
|
1814
|
-
setOperators(data.data);
|
|
1815
|
-
})();
|
|
1816
|
-
}
|
|
1817
|
-
}, [operatorsUrl]);
|
|
1818
|
-
|
|
1819
|
-
useUpdateEffect(() => {
|
|
1820
|
-
const fixedValue = fixOptions(value, options || {}, undefined, isRendererOnly);
|
|
1821
|
-
|
|
1822
|
-
// When the value we're receiving is the one we just emitted AND fixOptions has nothing to
|
|
1823
|
-
// meaningfully add or change, skip the update. This breaks the controlled-component loop for
|
|
1824
|
-
// arg_schema fields while still allowing required/preselected options to be restored (in that
|
|
1825
|
-
// case fixedValue will differ from value, so we don't skip).
|
|
1826
|
-
// The compare is empty-normalized: fixOptions can round-trip a required-empty field between
|
|
1827
|
-
// `{ value: '' }` and no `value` key — without normalizing, that cosmetic difference reads as a
|
|
1828
|
-
// change and re-fixes forever (e.g. byte-size / expression / ruled string fields).
|
|
1829
|
-
// Note: compare fixedValue against value, not localValue.fields — localValue may have been
|
|
1830
|
-
// updated by nested FormEngine emissions, so comparing against it would never skip.
|
|
1831
|
-
const normalizedValue = normalizeEmptyFieldValues(value);
|
|
1832
|
-
/* What this rebuild lets us agree with the parent on — `undefined` when it
|
|
1833
|
-
contributed something the parent has yet to learn. */
|
|
1834
|
-
const agreed = agreementFromRebuild(fixedValue, value);
|
|
1835
|
-
// the echo of an emit still in flight - the last or an older one - is not the parent's change
|
|
1836
|
-
const echo = inFlight.isEcho(value);
|
|
1837
|
-
if (
|
|
1838
|
-
(echo || isEqual(normalizedValue, normalizeEmptyFieldValues(lastEmittedValue.current))) &&
|
|
1839
|
-
agreed !== undefined
|
|
1840
|
-
) {
|
|
1841
|
-
return;
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
if (!originalValue.current && size(fixedValue)) {
|
|
1845
|
-
originalValue.current = fixedValue;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
/* Remember a mirror so the emit effect can tell one from an answer, and
|
|
1849
|
-
record the agreement it represents. A rebuild that DID add something
|
|
1850
|
-
leaves both alone — the parent has to learn about it. */
|
|
1851
|
-
mirroredValue.current = agreed;
|
|
1852
|
-
if (agreed !== undefined) {
|
|
1853
|
-
lastEmittedValue.current = agreed;
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
setLocalValue?.({ fields: fixedValue, meta: undefined });
|
|
1857
|
-
}, [JSON.stringify(options), JSON.stringify(value), isRendererOnly]);
|
|
1858
|
-
|
|
1859
|
-
const handleValueChange = useCallback(
|
|
1860
|
-
(optionName: string, val?: any, _type?: string, isFunction?: boolean) => {
|
|
1861
|
-
setLocalValue(({ fields = {} }) => {
|
|
1862
|
-
const schemaType = getOptionSchemaStorageType(options?.[optionName], isRendererOnly);
|
|
1863
|
-
const isAnyLike = isUntypedOptionType(schemaType);
|
|
1864
|
-
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
1865
|
-
const resolvedSchemaType =
|
|
1866
|
-
isAnyLike && (fields[optionName] as IQorusFormField)?.type ?
|
|
1867
|
-
((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
1868
|
-
: (
|
|
1869
|
-
(fields[optionName] as IQorusFormField)?.type === options?.[optionName]?.ui_type &&
|
|
1870
|
-
isRendererOnly((fields[optionName] as IQorusFormField)?.type)
|
|
1871
|
-
) ?
|
|
1872
|
-
schemaType
|
|
1873
|
-
: schemaType || ((fields[optionName] as IQorusFormField)?.type as TQorusType);
|
|
1874
|
-
// A field reports back the type it was drawn as, which for a renderer-only editor (markdown, cron,
|
|
1875
|
-
// dpql, ...) is the editor's name rather than the type the value is stored as. Storing that name
|
|
1876
|
-
// sent a description as `{type: "markdown"}`, which the server decodes by type and so parsed as
|
|
1877
|
-
// YAML. A type picked for an untyped field is never renderer-only, so it still wins.
|
|
1878
|
-
const emittedType = _type && !isRendererOnly(_type as TQorusType) ? _type : undefined;
|
|
1879
|
-
const type =
|
|
1880
|
-
emittedType ||
|
|
1881
|
-
getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
|
|
1882
|
-
|
|
1883
|
-
if (!(fields as TQorusForm)[optionName]) {
|
|
1884
|
-
const defaultOperators: TOperatorValue = reduce(
|
|
1885
|
-
operators || {},
|
|
1886
|
-
(filteredOperators: TOperatorValue, operator, operatorKey) => {
|
|
1887
|
-
if (operator.selected) {
|
|
1888
|
-
return [...(filteredOperators as string[]), operatorKey];
|
|
1889
|
-
}
|
|
1890
|
-
return filteredOperators;
|
|
1891
|
-
},
|
|
1892
|
-
[]
|
|
1893
|
-
);
|
|
1894
|
-
if ((defaultOperators as string[])?.length) {
|
|
1895
|
-
return {
|
|
1896
|
-
fields: {
|
|
1897
|
-
...fields,
|
|
1898
|
-
[optionName]: {
|
|
1899
|
-
type,
|
|
1900
|
-
value: val,
|
|
1901
|
-
op: defaultOperators,
|
|
1902
|
-
},
|
|
1903
|
-
},
|
|
1904
|
-
};
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
// IDE Options model: the 4th `onChange` argument (`isFunction`, sent
|
|
1909
|
-
// by TemplateField/auto) drives the option's `is_expression` flag;
|
|
1910
|
-
// the value itself is the raw AST `{ exp, args }`.
|
|
1911
|
-
const updatedValue: TQorusForm = {
|
|
1912
|
-
...(fields as TQorusForm),
|
|
1913
|
-
[optionName]: {
|
|
1914
|
-
...(fields as TQorusForm)[optionName],
|
|
1915
|
-
type: type as TQorusType,
|
|
1916
|
-
value: val,
|
|
1917
|
-
},
|
|
1918
|
-
};
|
|
1919
|
-
|
|
1920
|
-
/* The flag belongs to the VALUE, so a change that said nothing about it
|
|
1921
|
-
does not throw it away — `undefined` is no opinion, not a denial.
|
|
1922
|
-
Deleting it unconditionally left the AST on the row with nothing
|
|
1923
|
-
marking it as one: an assertion's Expected Value came back from a
|
|
1924
|
-
saved draft drawn as a two-field hash of `exp` and `args`, because
|
|
1925
|
-
merely opening the row and reverting had stripped the flag. */
|
|
1926
|
-
if (
|
|
1927
|
-
shouldMarkAsExpression(
|
|
1928
|
-
isFunction,
|
|
1929
|
-
val,
|
|
1930
|
-
((fields as TQorusForm)[optionName] as { is_expression?: boolean })?.is_expression
|
|
1931
|
-
)
|
|
1932
|
-
) {
|
|
1933
|
-
(updatedValue[optionName] as { is_expression?: boolean }).is_expression = true;
|
|
1934
|
-
} else {
|
|
1935
|
-
delete updatedValue[optionName].is_expression;
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
const meta: IOptionsOnChangeMeta = {};
|
|
1939
|
-
|
|
1940
|
-
if (
|
|
1941
|
-
options?.[optionName]?.has_dependents &&
|
|
1942
|
-
val !== undefined &&
|
|
1943
|
-
val !== (fields as TQorusForm)[optionName]?.value
|
|
1944
|
-
) {
|
|
1945
|
-
forEach(options, (option, depName) => {
|
|
1946
|
-
/* A dependency is `name`, `name=value` or `name!=value`, so the
|
|
1947
|
-
entry has to be PARSED to find which option it names. Compared as
|
|
1948
|
-
a whole string, only the bare spelling ever matched — so a field
|
|
1949
|
-
declared `depends_on: ['subject_iface_kind=workflow']` was never
|
|
1950
|
-
cleared when that option changed.
|
|
1951
|
-
|
|
1952
|
-
What that cost: a test whose subject was switched from a workflow
|
|
1953
|
-
to a service kept the workflow's `Subject Interface Version`.
|
|
1954
|
-
Services and jobs are versioned but only the latest is testable,
|
|
1955
|
-
so the field no longer applied, yet it stayed on the form as a
|
|
1956
|
-
locked control in front of a value the author could not clear —
|
|
1957
|
-
the value being there is exactly what stops the field being
|
|
1958
|
-
withheld outright. */
|
|
1959
|
-
if (
|
|
1960
|
-
option.depends_on &&
|
|
1961
|
-
flatten(option.depends_on).some(
|
|
1962
|
-
(dependency) => parseDependency(dependency as string).name === optionName
|
|
1963
|
-
) &&
|
|
1964
|
-
updatedValue[depName]
|
|
1965
|
-
) {
|
|
1966
|
-
updatedValue[depName].value = undefined;
|
|
1967
|
-
}
|
|
1968
|
-
});
|
|
1969
|
-
|
|
1970
|
-
onDependableOptionChange?.(optionName, val, updatedValue, options);
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
if (size(options?.[optionName]?.on_change)) {
|
|
1974
|
-
meta.events = options?.[optionName]?.on_change;
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
onSingleOptionsChange?.(optionName, updatedValue[optionName]);
|
|
1978
|
-
|
|
1979
|
-
if (
|
|
1980
|
-
compact &&
|
|
1981
|
-
!readOnly &&
|
|
1982
|
-
shouldAutoCollapseCompactOption(
|
|
1983
|
-
options?.[optionName],
|
|
1984
|
-
val,
|
|
1985
|
-
!!(updatedValue[optionName] as { is_expression?: boolean } | undefined)?.is_expression
|
|
1986
|
-
)
|
|
1987
|
-
) {
|
|
1988
|
-
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
return {
|
|
1992
|
-
fields: updatedValue,
|
|
1993
|
-
meta,
|
|
1994
|
-
};
|
|
1995
|
-
});
|
|
1996
|
-
},
|
|
1997
|
-
[
|
|
1998
|
-
onSingleOptionsChange,
|
|
1999
|
-
onDependableOptionChange,
|
|
2000
|
-
isRendererOnly,
|
|
2001
|
-
compact,
|
|
2002
|
-
readOnly,
|
|
2003
|
-
JSON.stringify(options),
|
|
2004
|
-
JSON.stringify(operators),
|
|
2005
|
-
]
|
|
2006
|
-
);
|
|
2007
|
-
|
|
2008
|
-
const handleOperatorChange = useCallback(
|
|
2009
|
-
(optionName: string, currentValue: TQorusForm, operator: string, index: number) => {
|
|
2010
|
-
setLocalValue?.({
|
|
2011
|
-
fields: {
|
|
2012
|
-
...currentValue,
|
|
2013
|
-
[optionName]: {
|
|
2014
|
-
...currentValue[optionName],
|
|
2015
|
-
op: fixOperatorValue(currentValue[optionName].op).map((op, idx) => {
|
|
2016
|
-
if (idx === index) {
|
|
2017
|
-
return operator;
|
|
2018
|
-
}
|
|
2019
|
-
return op as string;
|
|
2020
|
-
}),
|
|
2021
|
-
},
|
|
2022
|
-
},
|
|
2023
|
-
meta: undefined,
|
|
2024
|
-
});
|
|
2025
|
-
},
|
|
2026
|
-
[]
|
|
2027
|
-
);
|
|
2028
|
-
|
|
2029
|
-
const handleAddOperator = useCallback(
|
|
2030
|
-
(optionName: string, currentValue: TQorusForm, index: number) => {
|
|
2031
|
-
setLocalValue?.({
|
|
2032
|
-
fields: {
|
|
2033
|
-
...currentValue,
|
|
2034
|
-
[optionName]: {
|
|
2035
|
-
...currentValue[optionName],
|
|
2036
|
-
op: insertAtIndex(fixOperatorValue(currentValue[optionName].op), index, null),
|
|
2037
|
-
},
|
|
2038
|
-
},
|
|
2039
|
-
meta: undefined,
|
|
2040
|
-
});
|
|
2041
|
-
},
|
|
2042
|
-
[]
|
|
2043
|
-
);
|
|
2044
|
-
|
|
2045
|
-
const handleRemoveOperator = useCallback(
|
|
2046
|
-
(optionName: string, currentValue: TQorusForm, index: number) => {
|
|
2047
|
-
setLocalValue?.({
|
|
2048
|
-
fields: {
|
|
2049
|
-
...currentValue,
|
|
2050
|
-
[optionName]: {
|
|
2051
|
-
...currentValue[optionName],
|
|
2052
|
-
op: fixOperatorValue(currentValue[optionName].op).filter((_op, idx) => idx !== index),
|
|
2053
|
-
},
|
|
2054
|
-
},
|
|
2055
|
-
meta: undefined,
|
|
2056
|
-
});
|
|
2057
|
-
},
|
|
2058
|
-
[]
|
|
2059
|
-
);
|
|
2060
|
-
|
|
2061
|
-
const buildBadges = useCallback(
|
|
2062
|
-
(option: TQorusFormFieldSchema, optionName: string): IReqorePanelProps['badge'] => {
|
|
2063
|
-
const badges: IReqorePanelProps['badge'] = [];
|
|
2064
|
-
|
|
2065
|
-
if (option.required || option.required_groups) {
|
|
2066
|
-
badges.push({
|
|
2067
|
-
icon: 'Asterisk',
|
|
2068
|
-
size: 'tiny',
|
|
2069
|
-
leftIconProps: {
|
|
2070
|
-
size: 'tiny',
|
|
2071
|
-
},
|
|
2072
|
-
color: 'transparent',
|
|
2073
|
-
minimal: true,
|
|
2074
|
-
iconColor: option.required_groups ? 'warning:lighten:7' : 'danger:lighten:7',
|
|
2075
|
-
tooltip: {
|
|
2076
|
-
delay: 300,
|
|
2077
|
-
content: getRequiredOptionMessage(options || {}, option.required_groups, optionName),
|
|
2078
|
-
},
|
|
2079
|
-
});
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
if (option.has_dependents) {
|
|
2083
|
-
badges.push({
|
|
2084
|
-
icon: 'LinkUnlink',
|
|
2085
|
-
intent: 'info',
|
|
2086
|
-
size: 'tiny',
|
|
2087
|
-
tooltip: {
|
|
2088
|
-
content:
|
|
2089
|
-
'Other options depend on this option, changing it may result in configuration changes.',
|
|
2090
|
-
delay: 300,
|
|
2091
|
-
},
|
|
2092
|
-
});
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
return badges;
|
|
2096
|
-
},
|
|
2097
|
-
[options]
|
|
2098
|
-
);
|
|
2099
|
-
|
|
2100
|
-
const fixedValue: TQorusForm = (localValue.fields || {}) as TQorusForm;
|
|
2101
|
-
|
|
2102
|
-
const removeSelectedOption = useCallback((optionName: string) => {
|
|
2103
|
-
setLocalValue?.(({ fields }) => {
|
|
2104
|
-
const newFields = cloneDeep(fields as TQorusForm);
|
|
2105
|
-
delete newFields[optionName];
|
|
2106
|
-
return {
|
|
2107
|
-
fields: newFields,
|
|
2108
|
-
meta: undefined,
|
|
2109
|
-
};
|
|
2110
|
-
});
|
|
2111
|
-
// Collapse it too: a removed field drops back to the (collapsed) Optional box
|
|
2112
|
-
// as a quiet addable row — if it was being edited, that editor must close
|
|
2113
|
-
// rather than linger as an open editor for a field that's no longer added.
|
|
2114
|
-
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
2115
|
-
}, []);
|
|
2116
|
-
|
|
2117
|
-
const handleAddOptionalFieldChange = useCallback(
|
|
2118
|
-
(_name: string, optionName: unknown) => {
|
|
2119
|
-
handleValueChange(
|
|
2120
|
-
optionName as string,
|
|
2121
|
-
getDefaultValue(options?.[optionName as string]),
|
|
2122
|
-
getTypeAndCanBeNull(
|
|
2123
|
-
getOptionSchemaStorageType(options?.[optionName as string], isRendererOnly),
|
|
2124
|
-
options?.[optionName as string]?.allowed_values
|
|
2125
|
-
).type
|
|
2126
|
-
);
|
|
2127
|
-
},
|
|
2128
|
-
[JSON.stringify(options), handleValueChange]
|
|
2129
|
-
);
|
|
2130
|
-
|
|
2131
|
-
const availableOptions: TQorusForm = useMemo(() => {
|
|
2132
|
-
if (!options) {
|
|
2133
|
-
return {};
|
|
2134
|
-
}
|
|
2135
|
-
unavailableOptionsCount.current = 0;
|
|
2136
|
-
|
|
2137
|
-
return Object.keys(fixedValue)
|
|
2138
|
-
.sort((a, b) => {
|
|
2139
|
-
const aSort = (options[a] as any)?.sort || 0;
|
|
2140
|
-
const bSort = (options[b] as any)?.sort || 0;
|
|
2141
|
-
return aSort - bSort;
|
|
2142
|
-
})
|
|
2143
|
-
.reduce((newValue: TQorusForm, optionName) => {
|
|
2144
|
-
const option = fixedValue[optionName];
|
|
2145
|
-
if (!options?.[optionName]) {
|
|
2146
|
-
unavailableOptionsCount.current += 1;
|
|
2147
|
-
removeSelectedOption(optionName);
|
|
2148
|
-
return newValue;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
const rendererType = getType(
|
|
2153
|
-
(options[optionName].ui_type || options[optionName].type) as TQorusType,
|
|
2154
|
-
operators,
|
|
2155
|
-
(option as IQorusFormField)?.op
|
|
2156
|
-
);
|
|
2157
|
-
const storageType = getOptionFieldStorageType(
|
|
2158
|
-
optionName,
|
|
2159
|
-
(option as IQorusFormField)?.type,
|
|
2160
|
-
options,
|
|
2161
|
-
operators,
|
|
2162
|
-
(option as IQorusFormField)?.op,
|
|
2163
|
-
isRendererOnly,
|
|
2164
|
-
optionHoldsExpression(option)
|
|
2165
|
-
);
|
|
2166
|
-
|
|
2167
|
-
if (!isPlainObject(option)) {
|
|
2168
|
-
return {
|
|
2169
|
-
...newValue,
|
|
2170
|
-
[optionName]: {
|
|
2171
|
-
type: storageType || rendererType,
|
|
2172
|
-
value: option,
|
|
2173
|
-
},
|
|
2174
|
-
};
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
return {
|
|
2178
|
-
...newValue,
|
|
2179
|
-
[optionName]: { ...(option as IQorusFormField), type: storageType || rendererType },
|
|
2180
|
-
};
|
|
2181
|
-
}, {});
|
|
2182
|
-
}, [
|
|
2183
|
-
isRendererOnly,
|
|
2184
|
-
JSON.stringify(fixedValue),
|
|
2185
|
-
JSON.stringify(options),
|
|
2186
|
-
unavailableOptionsCount.current,
|
|
2187
|
-
JSON.stringify(operators),
|
|
2188
|
-
showInvalidOptionsOnly,
|
|
2189
|
-
]);
|
|
2190
|
-
|
|
2191
|
-
// Per required-group: its member options, and which member (if any) already
|
|
2192
|
-
// satisfies it — drives the "One of" chips and the "covered by" notes.
|
|
2193
|
-
const requiredGroupsInfo = useMemo(() => {
|
|
2194
|
-
const members: Record<string, string[]> = {};
|
|
2195
|
-
forEach(options || {}, (optionSchema, name) => {
|
|
2196
|
-
(optionSchema?.required_groups || []).forEach((groupName: string) => {
|
|
2197
|
-
(members[groupName] = members[groupName] || []).push(name);
|
|
2198
|
-
});
|
|
2199
|
-
});
|
|
2200
|
-
const satisfiedBy: Record<string, string | undefined> = {};
|
|
2201
|
-
Object.keys(members).forEach((groupName) => {
|
|
2202
|
-
satisfiedBy[groupName] = members[groupName].find(
|
|
2203
|
-
(name) => !isOptionValueEmpty((availableOptions as TQorusForm)?.[name]?.value)
|
|
2204
|
-
);
|
|
2205
|
-
});
|
|
2206
|
-
return { members, satisfiedBy };
|
|
2207
|
-
}, [JSON.stringify(options), JSON.stringify(availableOptions)]);
|
|
2208
|
-
|
|
2209
|
-
/* One bag per field, rebuilt only when something it is made of changes — the
|
|
2210
|
-
schema that declares the `inherit_props`, the sibling values they resolve
|
|
2211
|
-
against, or the scope handed in from an outer form. Held per field rather
|
|
2212
|
-
than per form because each field's bag is different: `inherit_props` is
|
|
2213
|
-
declared on the field. See `inheritedScopeFor` for what a churning bag
|
|
2214
|
-
costs the field that receives it. */
|
|
2215
|
-
const inheritedScopes = useMemo(() => {
|
|
2216
|
-
const scopes = new Map<string, Record<string, unknown>>();
|
|
2217
|
-
for (const optionName of Object.keys(options ?? {})) {
|
|
2218
|
-
scopes.set(
|
|
2219
|
-
optionName,
|
|
2220
|
-
inheritedScopeFor(
|
|
2221
|
-
options?.[optionName]?.inherit_props,
|
|
2222
|
-
availableOptions,
|
|
2223
|
-
inheritedFromParent
|
|
2224
|
-
)
|
|
2225
|
-
);
|
|
2226
|
-
}
|
|
2227
|
-
return scopes;
|
|
2228
|
-
}, [options, availableOptions, inheritedFromParent]);
|
|
2229
|
-
|
|
2230
|
-
// Dependency linkage (3a): when filling a dependency unlocks rows, flash
|
|
2231
|
-
// them once — the form visibly "opens up" instead of silently changing.
|
|
2232
|
-
const dependencyLockedNames = useMemo(() => {
|
|
2233
|
-
const names: string[] = [];
|
|
2234
|
-
forEach(options || {}, (optionSchema, name) => {
|
|
2235
|
-
if (
|
|
2236
|
-
optionSchema?.depends_on &&
|
|
2237
|
-
!hasAllDependenciesFullfilled(optionSchema.depends_on, availableOptions, options || {})
|
|
2238
|
-
) {
|
|
2239
|
-
names.push(name);
|
|
2240
|
-
}
|
|
2241
|
-
});
|
|
2242
|
-
return names;
|
|
2243
|
-
}, [JSON.stringify(options), JSON.stringify(availableOptions)]);
|
|
2244
|
-
const previousDependencyLocked = useRef<string[] | null>(null);
|
|
2245
|
-
useEffect(() => {
|
|
2246
|
-
const previous = previousDependencyLocked.current;
|
|
2247
|
-
previousDependencyLocked.current = dependencyLockedNames;
|
|
2248
|
-
if (!previous) {
|
|
2249
|
-
return;
|
|
2250
|
-
}
|
|
2251
|
-
const unlocked = previous.filter(
|
|
2252
|
-
(name) => !dependencyLockedNames.includes(name) && !options?.[name]?.disabled
|
|
2253
|
-
);
|
|
2254
|
-
if (unlocked.length) {
|
|
2255
|
-
flashOptions(unlocked);
|
|
2256
|
-
}
|
|
2257
|
-
}, [dependencyLockedNames.join('|')]);
|
|
2258
|
-
|
|
2259
|
-
/* The same thing one level down: a CHOICE that becomes available is as
|
|
2260
|
-
invisible as a field that becomes editable, and it is worse, because it
|
|
2261
|
-
opens up inside a control the reader may not have open. So the field that
|
|
2262
|
-
offers it flashes, which is the affordance that says "look here" without
|
|
2263
|
-
claiming the choice was made.
|
|
2264
|
-
|
|
2265
|
-
Keyed by field and by the value's identity, so a value that merely moves in
|
|
2266
|
-
the list is not mistaken for one that opened. Fields whose values gate on
|
|
2267
|
-
nothing never enter the set, which is nearly all of them. */
|
|
2268
|
-
const lockedAllowedValues = useMemo(() => {
|
|
2269
|
-
const locked: string[] = [];
|
|
2270
|
-
forEach(options || {}, (optionSchema, name) => {
|
|
2271
|
-
forEach(optionSchema?.allowed_values || [], (allowedValue) => {
|
|
2272
|
-
if (!getAllowedValueAvailability(allowedValue, availableOptions, options || {}).available) {
|
|
2273
|
-
locked.push(`${name}\u0000${JSON.stringify(allowedValue?.value?.value)}`);
|
|
2274
|
-
}
|
|
2275
|
-
});
|
|
2276
|
-
});
|
|
2277
|
-
return locked;
|
|
2278
|
-
}, [JSON.stringify(options), JSON.stringify(availableOptions)]);
|
|
2279
|
-
const previousLockedAllowedValues = useRef<string[] | null>(null);
|
|
2280
|
-
useEffect(() => {
|
|
2281
|
-
const previous = previousLockedAllowedValues.current;
|
|
2282
|
-
previousLockedAllowedValues.current = lockedAllowedValues;
|
|
2283
|
-
if (!previous) {
|
|
2284
|
-
return;
|
|
2285
|
-
}
|
|
2286
|
-
const opened = previous
|
|
2287
|
-
.filter((key) => !lockedAllowedValues.includes(key))
|
|
2288
|
-
.map((key) => key.split('\u0000')[0]);
|
|
2289
|
-
const fields = uniq(opened).filter(
|
|
2290
|
-
(name) =>
|
|
2291
|
-
!!options?.[name] &&
|
|
2292
|
-
!options?.[name]?.disabled &&
|
|
2293
|
-
// A field that is itself still locked flashes for its OWN unlocking, in
|
|
2294
|
-
// the effect above; two flashes for one change is a flicker, not a cue.
|
|
2295
|
-
!dependencyLockedNames.includes(name)
|
|
2296
|
-
);
|
|
2297
|
-
if (fields.length) {
|
|
2298
|
-
flashOptions(fields);
|
|
2299
|
-
}
|
|
2300
|
-
}, [lockedAllowedValues.join('|')]);
|
|
2301
|
-
|
|
2302
|
-
// The not-yet-added optional fields: everything in the schema the form is not
|
|
2303
|
-
// already showing a row for. This ONE list feeds all three ways a field gets
|
|
2304
|
-
// added — the inline addable rows, the Fields menu, and its "Select all" — so
|
|
2305
|
-
// whatever it leaves out is out of all three at once, and they cannot disagree.
|
|
2306
|
-
//
|
|
2307
|
-
// A field whose `depends_on` is not fulfilled is left out. It is not an option
|
|
2308
|
-
// yet: `hasAllDependenciesFullfilled` is the same predicate that then refuses
|
|
2309
|
-
// to let it be edited, so offering it can only end at a row that opens and says
|
|
2310
|
-
// it is disabled — the dead end an auth profile's cookie-only fields showed
|
|
2311
|
-
// while the scheme was Permissive. Setting the dependency brings the row back
|
|
2312
|
-
// (and flashes it — see `dependencyLockedNames`), which is the discoverable
|
|
2313
|
-
// path: pick the scheme, and the fields that scheme has appear.
|
|
2314
|
-
//
|
|
2315
|
-
// Only the not-yet-added ones. A field that already HAS a value stays listed
|
|
2316
|
-
// even when its dependency later stops holding — it renders disabled with the
|
|
2317
|
-
// reason, so the value is visible and removable rather than silently orphaned
|
|
2318
|
-
// in a form that no longer mentions it.
|
|
2319
|
-
const filteredOptions: IQorusFormSchema = useMemo(
|
|
2320
|
-
() =>
|
|
2321
|
-
reduce(
|
|
2322
|
-
options,
|
|
2323
|
-
(newOptions, option, optName) => {
|
|
2324
|
-
if (optName in fixedValue) {
|
|
2325
|
-
return newOptions;
|
|
2326
|
-
}
|
|
2327
|
-
if (
|
|
2328
|
-
option?.depends_on &&
|
|
2329
|
-
!hasAllDependenciesFullfilled(option.depends_on, availableOptions, options || {})
|
|
2330
|
-
) {
|
|
2331
|
-
return newOptions;
|
|
2332
|
-
}
|
|
2333
|
-
return { ...newOptions, [optName]: option };
|
|
2334
|
-
},
|
|
2335
|
-
{} as IQorusFormSchema
|
|
2336
|
-
),
|
|
2337
|
-
[JSON.stringify(options), JSON.stringify(fixedValue), JSON.stringify(availableOptions)]
|
|
2338
|
-
);
|
|
2339
|
-
|
|
2340
|
-
const isOptionValid = useCallback(
|
|
2341
|
-
(optionName: string, type: TQorusType, optionValue: any) => {
|
|
2342
|
-
const isEmpty = optionValue === undefined || optionValue === '';
|
|
2343
|
-
// An empty field locked by an unmet `depends_on` does not apply, so it is
|
|
2344
|
-
// not missing — the same rule the read-first status follows.
|
|
2345
|
-
if (
|
|
2346
|
-
isEmpty &&
|
|
2347
|
-
((!options?.[optionName]?.required && !options?.[optionName]?.required_groups) ||
|
|
2348
|
-
dependencyLockedNames.includes(optionName))
|
|
2349
|
-
) {
|
|
2350
|
-
return true;
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
return validateField(getType(type), optionValue, {
|
|
2354
|
-
has_to_have_value: true,
|
|
2355
|
-
optionSchema: options,
|
|
2356
|
-
options: availableOptions,
|
|
2357
|
-
...options?.[optionName],
|
|
2358
|
-
// The expression flag lives on the field value, not the schema — so an
|
|
2359
|
-
// expression value is validated as an expression, not the base type.
|
|
2360
|
-
isFunction: (availableOptions?.[optionName] as { is_expression?: boolean })?.is_expression,
|
|
2361
|
-
// A field drawn by a bespoke editor (the consumer's own included) is
|
|
2362
|
-
// not judged against `allowed_values` as fixed choices.
|
|
2363
|
-
hasOwnEditor: isRendererOnly(options?.[optionName]?.ui_type as TQorusType),
|
|
2364
|
-
} as any);
|
|
2365
|
-
},
|
|
2366
|
-
[
|
|
2367
|
-
JSON.stringify(options),
|
|
2368
|
-
JSON.stringify(availableOptions),
|
|
2369
|
-
JSON.stringify(localValue.fields),
|
|
2370
|
-
dependencyLockedNames,
|
|
2371
|
-
isRendererOnly,
|
|
2372
|
-
]
|
|
2373
|
-
);
|
|
2374
|
-
|
|
2375
|
-
const getValidityData = useCallback((): IFormValidityData => {
|
|
2376
|
-
const fields: IFormFieldValidityData[] = reduce(
|
|
2377
|
-
availableOptions,
|
|
2378
|
-
(result, option, optionName) => {
|
|
2379
|
-
const type = getOptionFieldStorageType(
|
|
2380
|
-
optionName,
|
|
2381
|
-
(option as IQorusFormField).type,
|
|
2382
|
-
options,
|
|
2383
|
-
operators,
|
|
2384
|
-
(option as IQorusFormField).op,
|
|
2385
|
-
isRendererOnly,
|
|
2386
|
-
optionHoldsExpression(option)
|
|
2387
|
-
);
|
|
2388
|
-
const optionValue = (option as IQorusFormField).value;
|
|
2389
|
-
|
|
2390
|
-
const isRequired = options?.[optionName]?.required;
|
|
2391
|
-
const hasRequiredGroups = !!options?.[optionName]?.required_groups;
|
|
2392
|
-
const isEmpty = optionValue === undefined || optionValue === '';
|
|
2393
|
-
|
|
2394
|
-
let validation: IValidationResult;
|
|
2395
|
-
|
|
2396
|
-
/* A required field locked by an unmet `depends_on` does not apply
|
|
2397
|
-
while it is locked: it is not in "Needs attention", and an empty one
|
|
2398
|
-
does not make the form incomplete either. Otherwise a check whose
|
|
2399
|
-
kind is Equals could never be saved, because Minimum — required for
|
|
2400
|
-
Between alone — is empty; and a form whose only gaps are fields
|
|
2401
|
-
nobody can fill in would say Incomplete with nothing to act on. A
|
|
2402
|
-
locked field that still HOLDS a value is validated as before. */
|
|
2403
|
-
if (
|
|
2404
|
-
isEmpty &&
|
|
2405
|
-
((!isRequired && !hasRequiredGroups) || dependencyLockedNames.includes(optionName))
|
|
2406
|
-
) {
|
|
2407
|
-
validation = { isValid: true, reasons: [] };
|
|
2408
|
-
} else {
|
|
2409
|
-
validation = validateFieldWithResult(getType(type), optionValue, {
|
|
2410
|
-
has_to_have_value: true,
|
|
2411
|
-
optionSchema: options,
|
|
2412
|
-
options: availableOptions,
|
|
2413
|
-
...options?.[optionName],
|
|
2414
|
-
// The expression flag lives on the field value, not the schema.
|
|
2415
|
-
isFunction: (option as { is_expression?: boolean }).is_expression,
|
|
2416
|
-
hasOwnEditor: isRendererOnly(options?.[optionName]?.ui_type as TQorusType),
|
|
2417
|
-
} as any);
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
result.push({
|
|
2421
|
-
fieldName: optionName,
|
|
2422
|
-
type: getType(type),
|
|
2423
|
-
value: optionValue,
|
|
2424
|
-
validation,
|
|
2425
|
-
});
|
|
2426
|
-
|
|
2427
|
-
return result;
|
|
2428
|
-
},
|
|
2429
|
-
[] as IFormFieldValidityData[]
|
|
2430
|
-
);
|
|
2431
|
-
|
|
2432
|
-
const invalidFields = fields.filter((f) => !f.validation.isValid);
|
|
2433
|
-
|
|
2434
|
-
return {
|
|
2435
|
-
isValid: invalidFields.length === 0,
|
|
2436
|
-
fields,
|
|
2437
|
-
invalidFields,
|
|
2438
|
-
};
|
|
2439
|
-
}, [
|
|
2440
|
-
isRendererOnly,
|
|
2441
|
-
JSON.stringify(availableOptions),
|
|
2442
|
-
JSON.stringify(options),
|
|
2443
|
-
JSON.stringify(operators),
|
|
2444
|
-
JSON.stringify(localValue.fields),
|
|
2445
|
-
dependencyLockedNames,
|
|
2446
|
-
]);
|
|
2447
|
-
|
|
2448
|
-
const validityData = useMemo(() => getValidityData(), [getValidityData]);
|
|
2449
|
-
const optionsAreValid = validityData.isValid;
|
|
2450
|
-
|
|
2451
|
-
useEffect(() => {
|
|
2452
|
-
onValidityChange?.(optionsAreValid, validityData);
|
|
2453
|
-
}, [JSON.stringify(validityData)]);
|
|
2454
|
-
|
|
2455
|
-
const shownOptions = useMemo((): TQorusForm => {
|
|
2456
|
-
return reduce(
|
|
2457
|
-
availableOptions,
|
|
2458
|
-
(newValue, option, optionName) => {
|
|
2459
|
-
if (
|
|
2460
|
-
showInvalidOptionsOnly &&
|
|
2461
|
-
isOptionValid(
|
|
2462
|
-
optionName,
|
|
2463
|
-
getOptionFieldStorageType(
|
|
2464
|
-
optionName,
|
|
2465
|
-
(option as IQorusFormField).type,
|
|
2466
|
-
options,
|
|
2467
|
-
operators,
|
|
2468
|
-
(option as IQorusFormField).op,
|
|
2469
|
-
isRendererOnly,
|
|
2470
|
-
optionHoldsExpression(option)
|
|
2471
|
-
),
|
|
2472
|
-
(option as IQorusFormField).value
|
|
2473
|
-
)
|
|
2474
|
-
) {
|
|
2475
|
-
return newValue;
|
|
2476
|
-
}
|
|
2477
|
-
return { ...newValue, [optionName]: option };
|
|
2478
|
-
},
|
|
2479
|
-
{}
|
|
2480
|
-
);
|
|
2481
|
-
}, [
|
|
2482
|
-
showInvalidOptionsOnly,
|
|
2483
|
-
isRendererOnly,
|
|
2484
|
-
JSON.stringify(availableOptions),
|
|
2485
|
-
JSON.stringify(options),
|
|
2486
|
-
JSON.stringify(operators),
|
|
2487
|
-
]);
|
|
2488
|
-
|
|
2489
|
-
// Read-first STATUS / BOX for one option — lifted to component scope so the
|
|
2490
|
-
// status boxes (renderCompact) and the header's "needs attention" count share
|
|
2491
|
-
// exactly one definition. One-of group members travel together (bucket by the
|
|
2492
|
-
// group's satisfaction); everything else by its own status.
|
|
2493
|
-
const schemaMsgIntent = useCallback(
|
|
2494
|
-
(name: string): 'danger' | 'warning' | undefined => {
|
|
2495
|
-
// Filtered, not raw: a message hidden by its own `when`/`unless` must not
|
|
2496
|
-
// colour the row or push the field into "Needs attention" — the whole point
|
|
2497
|
-
// of a conditional warning is that it is absent while it does not apply.
|
|
2498
|
-
const msgs = getShownSchemaMessages(
|
|
2499
|
-
(options?.[name] as { messages?: Array<{ intent?: string }> } | undefined)?.messages,
|
|
2500
|
-
availableOptions,
|
|
2501
|
-
options
|
|
2502
|
-
);
|
|
2503
|
-
if (msgs.some((m) => m.intent === 'danger')) return 'danger';
|
|
2504
|
-
if (msgs.some((m) => m.intent === 'warning')) return 'warning';
|
|
2505
|
-
return undefined;
|
|
2506
|
-
},
|
|
2507
|
-
[JSON.stringify(options), JSON.stringify(availableOptions)]
|
|
2508
|
-
);
|
|
2509
|
-
const getOptionStatus = useCallback(
|
|
2510
|
-
(name: string, hidden = false): TReadFirstStatus => {
|
|
2511
|
-
if (hidden) return 'optional';
|
|
2512
|
-
const schema = options?.[name];
|
|
2513
|
-
const type = getOptionFieldStorageType(
|
|
2514
|
-
name,
|
|
2515
|
-
(availableOptions as TQorusForm)?.[name]?.type,
|
|
2516
|
-
options,
|
|
2517
|
-
operators,
|
|
2518
|
-
(availableOptions as TQorusForm)?.[name]?.op,
|
|
2519
|
-
isRendererOnly,
|
|
2520
|
-
optionHoldsExpression((availableOptions as TQorusForm)?.[name])
|
|
2521
|
-
);
|
|
2522
|
-
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
2523
|
-
// A schema-declared hash whose every entry is a materialised-but-unset
|
|
2524
|
-
// field holds nothing, so it is not "set" — without this the row showed a
|
|
2525
|
-
// green set-dot and counted toward the completion meter while the
|
|
2526
|
-
// sub-form it summarises said 0/3 set.
|
|
2527
|
-
const empty = isOptionValueEmpty(value) || isUnsetSchemaHash(value, schema);
|
|
2528
|
-
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
2529
|
-
// Required only while it applies. A field locked by an unmet `depends_on`
|
|
2530
|
-
// renders disabled with what unlocks it; nothing the author can do to it
|
|
2531
|
-
// is a next step — the next step is the field it depends on, which carries
|
|
2532
|
-
// its own status. Counting it here put a guided check's locked Path /
|
|
2533
|
-
// Minimum / Maximum under "Needs attention" before its kind was even
|
|
2534
|
-
// picked. The moment the dependency holds, it is required again.
|
|
2535
|
-
const required =
|
|
2536
|
-
!dependencyLockedNames.includes(name) && !!(schema?.required || reqGroups.length);
|
|
2537
|
-
const covered =
|
|
2538
|
-
empty &&
|
|
2539
|
-
reqGroups.some((g) => {
|
|
2540
|
-
const by = requiredGroupsInfo.satisfiedBy[g];
|
|
2541
|
-
return !!by && by !== name;
|
|
2542
|
-
});
|
|
2543
|
-
const msgIntent = schemaMsgIntent(name);
|
|
2544
|
-
const invalid = (!empty && !isOptionValid(name, type, value)) || msgIntent === 'danger';
|
|
2545
|
-
return getReadFirstStatus({
|
|
2546
|
-
empty,
|
|
2547
|
-
required,
|
|
2548
|
-
covered,
|
|
2549
|
-
invalid,
|
|
2550
|
-
warned: msgIntent === 'warning',
|
|
2551
|
-
});
|
|
2552
|
-
},
|
|
2553
|
-
[
|
|
2554
|
-
isRendererOnly,
|
|
2555
|
-
JSON.stringify(options),
|
|
2556
|
-
JSON.stringify(availableOptions),
|
|
2557
|
-
JSON.stringify(operators),
|
|
2558
|
-
isOptionValid,
|
|
2559
|
-
requiredGroupsInfo,
|
|
2560
|
-
schemaMsgIntent,
|
|
2561
|
-
dependencyLockedNames,
|
|
2562
|
-
]
|
|
2563
|
-
);
|
|
2564
|
-
const getOptionBucket = useCallback(
|
|
2565
|
-
(name: string, hidden = false): 'attention' | 'set' | 'optional' => {
|
|
2566
|
-
// A locked one-of member cannot be the one that satisfies its group, so it
|
|
2567
|
-
// does not travel with the group into "Needs attention"; its own status
|
|
2568
|
-
// (not required while locked) decides its box.
|
|
2569
|
-
if (!hidden && !dependencyLockedNames.includes(name)) {
|
|
2570
|
-
const reqGroups = (options?.[name]?.required_groups as string[] | undefined) || [];
|
|
2571
|
-
if (reqGroups.length) {
|
|
2572
|
-
return reqGroups.some((g) => !requiredGroupsInfo.satisfiedBy[g]) ? 'attention' : 'set';
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
return getReadFirstBucket(getOptionStatus(name, hidden));
|
|
2576
|
-
},
|
|
2577
|
-
[JSON.stringify(options), requiredGroupsInfo, getOptionStatus, dependencyLockedNames]
|
|
2578
|
-
);
|
|
2579
|
-
// How many fields are in the "Needs attention" box — drives the header link.
|
|
2580
|
-
const readFirstAttentionCount = useMemo(
|
|
2581
|
-
() =>
|
|
2582
|
-
Object.keys(availableOptions || {}).filter((name) => getOptionBucket(name) === 'attention')
|
|
2583
|
-
.length,
|
|
2584
|
-
[JSON.stringify(availableOptions), getOptionBucket]
|
|
2585
|
-
);
|
|
2586
|
-
|
|
2587
|
-
const getIntent = useCallback(
|
|
2588
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2589
|
-
(optName: string, type: TQorusType, optValue: any, _op: any): TReqoreIntent => {
|
|
2590
|
-
const intent =
|
|
2591
|
-
isOptionValid(optName, type, optValue) ? undefined
|
|
2592
|
-
: recordRequiresSearchOptions ? 'info'
|
|
2593
|
-
: 'danger';
|
|
2594
|
-
|
|
2595
|
-
return intent || (options?.[optName] as any)?.intent;
|
|
2596
|
-
},
|
|
2597
|
-
[isOptionValid, recordRequiresSearchOptions, JSON.stringify(options)]
|
|
2598
|
-
);
|
|
2599
|
-
|
|
2600
|
-
const handleShowFieldTypesClick = useCallback(() => setShowFieldTypes((prev) => !prev), []);
|
|
2601
|
-
const handleToggleAllDescriptions = useCallback(() => {
|
|
2602
|
-
setInfoPanelOverrides({});
|
|
2603
|
-
setShowAllDescriptions((prev) => prev !== true);
|
|
2604
|
-
}, []);
|
|
2605
|
-
const handleToggleInvalidOnly = useCallback(() => setShowInvalidOptionsOnly((prev) => !prev), []);
|
|
2606
|
-
const handleRevertChangesClick = useCallback(() => {
|
|
2607
|
-
setLocalValue({
|
|
2608
|
-
fields: originalValue.current,
|
|
2609
|
-
meta: undefined,
|
|
2610
|
-
});
|
|
2611
|
-
}, [JSON.stringify(options), JSON.stringify(originalValue.current)]);
|
|
2612
|
-
|
|
2613
|
-
// `commitMode='batched'`: which options differ from the committed baseline —
|
|
2614
|
-
// drives the per-row Draft chips and the Save/Discard bar.
|
|
2615
|
-
const dirtyOptionNames = useMemo(() => {
|
|
2616
|
-
if (commitMode !== 'batched') {
|
|
2617
|
-
return [];
|
|
2618
|
-
}
|
|
2619
|
-
const keys = new Set([
|
|
2620
|
-
...Object.keys((localValue.fields as TQorusForm) || {}),
|
|
2621
|
-
...Object.keys((originalValue.current as TQorusForm) || {}),
|
|
2622
|
-
]);
|
|
2623
|
-
return Array.from(keys).filter(
|
|
2624
|
-
(key) =>
|
|
2625
|
-
!isEqual(
|
|
2626
|
-
(localValue.fields as TQorusForm)?.[key]?.value,
|
|
2627
|
-
originalValue.current?.[key]?.value
|
|
2628
|
-
)
|
|
2629
|
-
);
|
|
2630
|
-
}, [commitMode, JSON.stringify(localValue.fields), JSON.stringify(originalValue.current)]);
|
|
2631
|
-
|
|
2632
|
-
// Save: emit the staged form via onCommit and make it the new baseline, so
|
|
2633
|
-
// the Draft chips and the bar clear. Validation gates the button itself.
|
|
2634
|
-
const handleCommitClick = useCallback(() => {
|
|
2635
|
-
const toEmit = size(localValue.fields) ? (localValue.fields as TQorusForm) : undefined;
|
|
2636
|
-
onCommit?.(name, toEmit, size(localValue.meta) ? localValue.meta : undefined);
|
|
2637
|
-
originalValue.current = localValue.fields;
|
|
2638
|
-
setLocalValue((current) => ({ ...current }));
|
|
2639
|
-
}, [JSON.stringify(localValue), name, onCommit]);
|
|
2640
|
-
|
|
2641
|
-
const hasOptionChanged = useCallback(
|
|
2642
|
-
(optionValue: unknown, optionName: string): boolean => {
|
|
2643
|
-
return !isEqual(optionValue, originalValue.current?.[optionName]?.value);
|
|
2644
|
-
},
|
|
2645
|
-
[JSON.stringify(originalValue.current)]
|
|
2646
|
-
);
|
|
2647
|
-
|
|
2648
|
-
const handleOptionLabelClick = useCallback((optionName: string) => {
|
|
2649
|
-
setShowHelpForOption(optionName);
|
|
2650
|
-
}, []);
|
|
2651
|
-
|
|
2652
|
-
const toggleExpandedOption = useCallback(
|
|
2653
|
-
(optionName: string) => {
|
|
2654
|
-
setExpandedOptions((prev) =>
|
|
2655
|
-
prev.includes(optionName) ? prev.filter((name) => name !== optionName)
|
|
2656
|
-
// single (default): opening a row collapses any other open one.
|
|
2657
|
-
: expandMode === 'single' ? [optionName]
|
|
2658
|
-
: [...prev, optionName]
|
|
2659
|
-
);
|
|
2660
|
-
},
|
|
2661
|
-
[expandMode]
|
|
2662
|
-
);
|
|
2663
|
-
|
|
2664
|
-
// --- Caller-named starting rows (opt-in) ----------------------------------
|
|
2665
|
-
// `initialExpandedOptions` lets a consumer name the rows that must already
|
|
2666
|
-
// be open when the form first paints — the case where the expanded row is
|
|
2667
|
-
// part of an address rather than a click (see the prop's docs).
|
|
2668
|
-
//
|
|
2669
|
-
// One-shot for the same reason autofocus is: it fires on the first render
|
|
2670
|
-
// that actually has rows (so an async-loaded schema is covered) and then
|
|
2671
|
-
// never again, so a row the user collapses afterwards stays collapsed and a
|
|
2672
|
-
// later schema reload does not reopen it.
|
|
2673
|
-
const hasAppliedInitialExpansionRef = useRef(false);
|
|
2674
|
-
useEffect(() => {
|
|
2675
|
-
if (!initialExpandedOptions?.length || !compact || hasAppliedInitialExpansionRef.current) {
|
|
2676
|
-
return;
|
|
2677
|
-
}
|
|
2678
|
-
const names = Object.keys(availableOptions);
|
|
2679
|
-
if (!names.length) {
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
const toExpand = initialExpandedOptions.filter((name) => names.includes(name));
|
|
2683
|
-
if (!toExpand.length) {
|
|
2684
|
-
// Not "there is no such row" — "not YET". A server-driven schema arrives
|
|
2685
|
-
// in pieces, and the row the caller named can be in a later one (a test's
|
|
2686
|
-
// `cases` field materialises once its step-kind catalogue lands). Latching
|
|
2687
|
-
// here would spend the one shot on a form that did not yet contain the
|
|
2688
|
-
// field, and the address would never be applied at all.
|
|
2689
|
-
return;
|
|
2690
|
-
}
|
|
2691
|
-
hasAppliedInitialExpansionRef.current = true;
|
|
2692
|
-
setExpandedOptions((prev) => [...prev, ...toExpand.filter((name) => !prev.includes(name))]);
|
|
2693
|
-
}, [initialExpandedOptions, compact, availableOptions]);
|
|
2694
|
-
|
|
2695
|
-
// --- First-attention-field autofocus (opt-in) -----------------------------
|
|
2696
|
-
// With `autoFocusFirstRequired`, drop the user straight into the first field
|
|
2697
|
-
// they must fix. We reuse the engine's own ordering (`availableOptions`), the
|
|
2698
|
-
// very same `getOptionBucket` the status boxes use to decide "needs attention"
|
|
2699
|
-
// (so we cover empty-required, unsatisfied one-of, AND filled-but-invalid rows
|
|
2700
|
-
// without ever drifting from what the user sees), and dependency gating
|
|
2701
|
-
// (`dependencyLockedNames`), then expand the target row through
|
|
2702
|
-
// `expandedOptions` — the row's editor-focus effect does the rest. No DOM
|
|
2703
|
-
// scraping, no synthetic clicks, no polling.
|
|
2704
|
-
//
|
|
2705
|
-
// Strictly one-shot: it fires the first time the form has focusable content
|
|
2706
|
-
// (so an async-loaded schema is still covered — the empty first pass doesn't
|
|
2707
|
-
// count), then never again for the life of this instance. A server-driven
|
|
2708
|
-
// field update or an in-place schema reload will NOT re-grab focus; only a
|
|
2709
|
-
// genuine remount (a fresh instance) auto-focuses again, which is the intended
|
|
2710
|
-
// on-mount behaviour.
|
|
2711
|
-
const hasAutoFocusedRef = useRef(false);
|
|
2712
|
-
useEffect(() => {
|
|
2713
|
-
// Two decisions, one scan. Both open the row that needs attention;
|
|
2714
|
-
// `autoFocusFirstRequired` additionally promises the caret lands in it, and
|
|
2715
|
-
// so refuses to run at all while the reader holds the caret somewhere else
|
|
2716
|
-
// (the guard below). An opened row takes the caret either way — that is
|
|
2717
|
-
// CompactRow's tap-to-edit rule, and it declines just as firmly to take one
|
|
2718
|
-
// that is already somewhere outside the row.
|
|
2719
|
-
const wantsFocus = !!autoFocusFirstRequired;
|
|
2720
|
-
if ((!wantsFocus && !expandFirstRequired) || !compact || !options) {
|
|
2721
|
-
return;
|
|
2722
|
-
}
|
|
2723
|
-
if (hasAutoFocusedRef.current) {
|
|
2724
|
-
return;
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
const orderedNames = Object.keys(availableOptions);
|
|
2728
|
-
if (!orderedNames.length) {
|
|
2729
|
-
return;
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
// Never grab focus from a control the user is already in — including a field
|
|
2733
|
-
// *inside* this form. If a slow/async schema lets the user start typing
|
|
2734
|
-
// before this first-required scan runs, we must not steal their caret. On a
|
|
2735
|
-
// clean mount focus rests on the body, so the intended "drop into the first
|
|
2736
|
-
// field" still fires. Bailing here leaves `hasAutoFocusedRef` false, so it
|
|
2737
|
-
// retries once focus is free.
|
|
2738
|
-
//
|
|
2739
|
-
// Only the FOCUSING variant waits: opening a row moves no caret, so there is
|
|
2740
|
-
// nothing to steal and nothing to wait for. Making the expand-only variant
|
|
2741
|
-
// wait here would make it a no-op in its main use — a sub-form mounted by a
|
|
2742
|
-
// click, where the button that mounted it still holds focus and never gives
|
|
2743
|
-
// it up.
|
|
2744
|
-
const active = document.activeElement as HTMLElement | null;
|
|
2745
|
-
if (wantsFocus && active && active !== document.body) {
|
|
2746
|
-
return;
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
const target = getFirstAttentionOptionName(orderedNames, (fieldName) => {
|
|
2750
|
-
const schema = options[fieldName];
|
|
2751
|
-
if (!schema) {
|
|
2752
|
-
return undefined;
|
|
2753
|
-
}
|
|
2754
|
-
return {
|
|
2755
|
-
focusable:
|
|
2756
|
-
!readOnly &&
|
|
2757
|
-
!schema.disabled &&
|
|
2758
|
-
!(schema as { readonly?: boolean }).readonly &&
|
|
2759
|
-
!dependencyLockedNames.includes(fieldName),
|
|
2760
|
-
// Single source of truth: the same bucket the read-first status boxes
|
|
2761
|
-
// show, so a filled-but-invalid required row is a target too.
|
|
2762
|
-
needsAttention: getOptionBucket(fieldName) === 'attention',
|
|
2763
|
-
};
|
|
2764
|
-
});
|
|
2765
|
-
|
|
2766
|
-
// Mark done even when nothing needs attention, so later value edits or an
|
|
2767
|
-
// in-place schema reload never re-scan or re-focus. Only a remount arms it
|
|
2768
|
-
// again.
|
|
2769
|
-
hasAutoFocusedRef.current = true;
|
|
2770
|
-
|
|
2771
|
-
if (target) {
|
|
2772
|
-
setExpandedOptions((prev) =>
|
|
2773
|
-
prev.includes(target) ? prev
|
|
2774
|
-
: expandMode === 'multi' ? [...prev, target]
|
|
2775
|
-
// `expandMode: 'single'` is a rule about what a CLICK does: opening a
|
|
2776
|
-
// row closes the one the reader last opened. It is not a licence to
|
|
2777
|
-
// close a row the HOST named through `initialExpandedOptions`. That
|
|
2778
|
-
// is an address — a statement about what the page is FOR, made before
|
|
2779
|
-
// the reader did anything — and this scan is a convenience layered on
|
|
2780
|
-
// top of it. Keep the named rows; replace only what a click could.
|
|
2781
|
-
: [...prev.filter((name) => initialExpandedOptions?.includes(name)), target]
|
|
2782
|
-
);
|
|
2783
|
-
}
|
|
2784
|
-
}, [
|
|
2785
|
-
autoFocusFirstRequired,
|
|
2786
|
-
expandFirstRequired,
|
|
2787
|
-
compact,
|
|
2788
|
-
options,
|
|
2789
|
-
availableOptions,
|
|
2790
|
-
readOnly,
|
|
2791
|
-
dependencyLockedNames,
|
|
2792
|
-
getOptionBucket,
|
|
2793
|
-
expandMode,
|
|
2794
|
-
initialExpandedOptions,
|
|
2795
|
-
]);
|
|
2796
|
-
|
|
2797
|
-
// Read-first completion summary (how many shown options have a value set),
|
|
2798
|
-
// surfaced as a progress meter at the top of the compact form.
|
|
2799
|
-
const readFirstCompletion = useMemo(
|
|
2800
|
-
() =>
|
|
2801
|
-
getReadFirstCompletion(
|
|
2802
|
-
availableOptions as Record<string, IQorusFormField | undefined>,
|
|
2803
|
-
(name) => getOptionBucket(name) === 'attention'
|
|
2804
|
-
),
|
|
2805
|
-
[JSON.stringify(availableOptions), getOptionBucket]
|
|
2806
|
-
);
|
|
2807
|
-
|
|
2808
|
-
const optionalFields = useMemo(
|
|
2809
|
-
() =>
|
|
2810
|
-
Object.keys(filteredOptions).map((option) => ({
|
|
2811
|
-
value: option,
|
|
2812
|
-
desc: options?.[option]?.desc,
|
|
2813
|
-
short_desc: options?.[option]?.short_desc,
|
|
2814
|
-
disabled: options?.[option]?.disabled,
|
|
2815
|
-
display_name: options?.[option]?.display_name,
|
|
2816
|
-
intent: (options?.[option] as any)?.intent,
|
|
2817
|
-
messages: (options?.[option] as any)?.messages,
|
|
2818
|
-
})),
|
|
2819
|
-
[JSON.stringify(filteredOptions), JSON.stringify(options)]
|
|
2820
|
-
);
|
|
2821
|
-
|
|
2822
|
-
// Compact "Fields" menu — Select all: add every not-yet-selected, enabled
|
|
2823
|
-
// optional field (mirrors the IDE's handleAddAll).
|
|
2824
|
-
const handleAddAllOptional = useCallback(() => {
|
|
2825
|
-
forEach(filteredOptions, (schema, optionName) => {
|
|
2826
|
-
if (!schema?.disabled) {
|
|
2827
|
-
handleAddOptionalFieldChange('options', optionName);
|
|
2828
|
-
}
|
|
2829
|
-
});
|
|
2830
|
-
}, [JSON.stringify(filteredOptions), handleAddOptionalFieldChange]);
|
|
2831
|
-
|
|
2832
|
-
// Default fields: drop user-added optionals, keep required/preselected/valued,
|
|
2833
|
-
// clear the required-only filter (mirrors the IDE's handleResetToDefault).
|
|
2834
|
-
const handleResetToDefaultFields = useCallback(() => {
|
|
2835
|
-
setLocalValue(({ fields }) => {
|
|
2836
|
-
const current = (fields || {}) as TQorusForm;
|
|
2837
|
-
const next: TQorusForm = {};
|
|
2838
|
-
forEach(current, (value, optionName) => {
|
|
2839
|
-
const schema = options?.[optionName];
|
|
2840
|
-
const isDefault = !!(
|
|
2841
|
-
schema?.required ||
|
|
2842
|
-
schema?.required_groups ||
|
|
2843
|
-
schema?.preselected ||
|
|
2844
|
-
originalValue.current?.[optionName]
|
|
2845
|
-
);
|
|
2846
|
-
if (isDefault) {
|
|
2847
|
-
next[optionName] = value as IQorusFormField;
|
|
2848
|
-
}
|
|
2849
|
-
});
|
|
2850
|
-
return {
|
|
2851
|
-
fields: fixOptions(next, options || {}, operators, isRendererOnly),
|
|
2852
|
-
meta: undefined,
|
|
2853
|
-
};
|
|
2854
|
-
});
|
|
2855
|
-
setRequiredOnly(false);
|
|
2856
|
-
}, [JSON.stringify(options), JSON.stringify(operators), isRendererOnly]);
|
|
2857
|
-
|
|
2858
|
-
const getCustomMenuTemplateItems = useCallback<(optionName: string) => TCustomTemplateItems>(
|
|
2859
|
-
(optionName) => {
|
|
2860
|
-
return compactValue?.map((type) => ({
|
|
2861
|
-
label: type.display_name,
|
|
2862
|
-
description: type.short_desc,
|
|
2863
|
-
onClick: (_e: any, removeTemplate: any) => {
|
|
2864
|
-
removeTemplate?.();
|
|
2865
|
-
handleValueChange(optionName, undefined, type.name);
|
|
2866
|
-
},
|
|
2867
|
-
}));
|
|
2868
|
-
},
|
|
2869
|
-
[compactValue, handleValueChange]
|
|
2870
|
-
);
|
|
2871
|
-
|
|
2872
|
-
const getTypeForOption = useCallback((type: string) => type, []);
|
|
2873
|
-
|
|
2874
|
-
const renderOption = useCallback(
|
|
2875
|
-
(
|
|
2876
|
-
optionName: string,
|
|
2877
|
-
{ type, ...other }: IQorusFormField,
|
|
2878
|
-
// Inline (in-row) editing renders the editor a size down so it fits the
|
|
2879
|
-
// read row's height without shifting the rows around it.
|
|
2880
|
-
editorSize?: 'small',
|
|
2881
|
-
// The info panel below the row keeps showing schema messages while editing —
|
|
2882
|
-
// rendering them in the editor too would balloon a one-line edit.
|
|
2883
|
-
suppressSchemaMessages?: boolean
|
|
2884
|
-
) => {
|
|
2885
|
-
const operatorParts = fixOperatorValue(other.op);
|
|
2886
|
-
// The RENDERER type for this row — which editor mounts. Distinct from the
|
|
2887
|
-
// storage type on the value envelope: a `richtext` field stores a string
|
|
2888
|
-
// but must render the richtext editor, so `ui_type` wins here.
|
|
2889
|
-
//
|
|
2890
|
-
// The exception is an any-like `ui_type`. Those schemas say
|
|
2891
|
-
// `ui_type: 'any'` precisely so the user can pick a concrete type, and
|
|
2892
|
-
// that pick lives on the field's stored `type` — letting 'any' win would
|
|
2893
|
-
// pin the row to the untyped editor forever.
|
|
2894
|
-
//
|
|
2895
|
-
// The trailing fallbacks keep the row rendering when a server-driven
|
|
2896
|
-
// schema has not arrived yet (`type` undefined) instead of crashing.
|
|
2897
|
-
const optionSchema = options?.[optionName];
|
|
2898
|
-
const schemaUiType = optionSchema?.ui_type as TQorusType;
|
|
2899
|
-
const uiTypeIsAnyLike = isUntypedOptionType(schemaUiType);
|
|
2900
|
-
/* An EXPRESSION's stored type is skipped in this chain.
|
|
2901
|
-
|
|
2902
|
-
The value is `{ is_expression: true, value: {...} }`, so the type saved
|
|
2903
|
-
beside it describes that envelope — `hash` — not what the field holds.
|
|
2904
|
-
With no `ui_type` to win first, that `hash` beat the schema's `auto`
|
|
2905
|
-
and was handed to the expression editor as the return type to check
|
|
2906
|
-
against: `1 + 2` on a field declared `auto` came back "This does not
|
|
2907
|
-
fit. The expression returns int, and this field holds hash". A literal
|
|
2908
|
-
`1` was fine, because a literal is stored as a plain value whose type
|
|
2909
|
-
matches.
|
|
2910
|
-
|
|
2911
|
-
Dropping the stored type here lets the schema decide, which is the
|
|
2912
|
-
only party that knows what the field accepts. This is the row
|
|
2913
|
-
renderer's OWN resolution and does not go through
|
|
2914
|
-
`getOptionFieldStorageType`, so guarding that one alone left this
|
|
2915
|
-
untouched. */
|
|
2916
|
-
const storedRowType = optionHoldsExpression(other) ? undefined : type;
|
|
2917
|
-
|
|
2918
|
-
/* A RELOADED expression is handed to the editor in the shape the editor
|
|
2919
|
-
was written against.
|
|
2920
|
-
|
|
2921
|
-
An expression is written as `{ type, is_expression: true, value: ast }`
|
|
2922
|
-
and saved as `{ type, value: { is_expression: true, value: ast } }` —
|
|
2923
|
-
the flag moves ONTO the value. The editor was only ever given
|
|
2924
|
-
`other.is_expression`, which is absent in the saved shape, so a
|
|
2925
|
-
reloaded expression was not recognised as one: an `auto` field then
|
|
2926
|
-
resolved its type from the value it could see, decided the envelope was
|
|
2927
|
-
a `hash`, and rendered `1 + 2` as a raw tree of `is_expression` /
|
|
2928
|
-
`exp` / `args` for the author to edit by hand.
|
|
2929
|
-
|
|
2930
|
-
Both halves have to move together. Flipping the flag alone would put
|
|
2931
|
-
the editor into expression mode and then hand it the ENVELOPE where it
|
|
2932
|
-
expects the expression, so the value is unwrapped here too — after
|
|
2933
|
-
which a saved expression is indistinguishable from one just typed,
|
|
2934
|
-
which is what every component downstream already assumes.
|
|
2935
|
-
|
|
2936
|
-
Reported as "after Run assertion the value shows as raw data", but the
|
|
2937
|
-
run is incidental: the field beside it, which no run touched, was
|
|
2938
|
-
equally raw. The trigger is a RELOAD, which is the only thing that puts
|
|
2939
|
-
an expression into its saved shape. */
|
|
2940
|
-
const nestedExpression = !!(other as { value?: { is_expression?: unknown } })?.value
|
|
2941
|
-
?.is_expression;
|
|
2942
|
-
const expressionAwareValue =
|
|
2943
|
-
nestedExpression ?
|
|
2944
|
-
(other as unknown as { value: { value: unknown } }).value.value
|
|
2945
|
-
: other.value;
|
|
2946
|
-
const resolvedType =
|
|
2947
|
-
(isFixedCompactAllowedValueOption(optionSchema) ?
|
|
2948
|
-
getOptionSchemaStorageType(optionSchema, isRendererOnly)
|
|
2949
|
-
: schemaUiType && !uiTypeIsAnyLike ? schemaUiType
|
|
2950
|
-
: undefined) ||
|
|
2951
|
-
storedRowType ||
|
|
2952
|
-
schemaUiType ||
|
|
2953
|
-
(optionSchema?.type as TQorusType) ||
|
|
2954
|
-
'any';
|
|
2955
|
-
const effectiveForceDropdown =
|
|
2956
|
-
forceDropdown ??
|
|
2957
|
-
(templateFieldProps as { forceDropdown?: boolean } | undefined)?.forceDropdown;
|
|
2958
|
-
|
|
2959
|
-
return (
|
|
2960
|
-
<>
|
|
2961
|
-
{(() => {
|
|
2962
|
-
const schemaMsgs = (
|
|
2963
|
-
suppressSchemaMessages ?
|
|
2964
|
-
[]
|
|
2965
|
-
: getShownSchemaMessages(
|
|
2966
|
-
(options?.[optionName] as any)?.messages,
|
|
2967
|
-
availableOptions,
|
|
2968
|
-
options
|
|
2969
|
-
)) as {
|
|
2970
|
-
intent?: string;
|
|
2971
|
-
title?: string;
|
|
2972
|
-
content?: string;
|
|
2973
|
-
}[];
|
|
2974
|
-
if (!schemaMsgs.length) return null;
|
|
2975
|
-
const items = schemaMsgs.map(({ intent, title, content }, index) => (
|
|
2976
|
-
<ReqoreMessage
|
|
2977
|
-
intent={intent as never}
|
|
2978
|
-
title={title}
|
|
2979
|
-
key={title || index}
|
|
2980
|
-
opaque={false}
|
|
2981
|
-
size='small'
|
|
2982
|
-
// Compact: flat (no border) to match the read-row info panels;
|
|
2983
|
-
// classic forms keep the bordered, bottom-margined message.
|
|
2984
|
-
flat={compact || undefined}
|
|
2985
|
-
margin={compact ? undefined : 'bottom'}
|
|
2986
|
-
>
|
|
2987
|
-
{content}
|
|
2988
|
-
</ReqoreMessage>
|
|
2989
|
-
));
|
|
2990
|
-
// Compact: stack them in a 4px-gap panel so a field's messages look
|
|
2991
|
-
// identical whether the row is collapsed (read panel) or expanded.
|
|
2992
|
-
return compact ?
|
|
2993
|
-
<div
|
|
2994
|
-
className='options-readfirst-info-panel'
|
|
2995
|
-
style={{ display: 'flex', flexFlow: 'column', gap: 4, marginBottom: 8 }}
|
|
2996
|
-
>
|
|
2997
|
-
{items}
|
|
2998
|
-
</div>
|
|
2999
|
-
: <>{items}</>;
|
|
3000
|
-
})()}
|
|
3001
|
-
{operators && size(operators) ?
|
|
3002
|
-
<>
|
|
3003
|
-
<ReqoreControlGroup fill wrap className='operators'>
|
|
3004
|
-
{operatorParts.map((operator, index) => (
|
|
3005
|
-
<React.Fragment key={index}>
|
|
3006
|
-
<SelectFormField
|
|
3007
|
-
items={map(operators, (op) => ({
|
|
3008
|
-
...op,
|
|
3009
|
-
value: op.name,
|
|
3010
|
-
}))}
|
|
3011
|
-
disabled={readOnly}
|
|
3012
|
-
value={operator && `${(operators as any)?.[operator as string]?.name}`}
|
|
3013
|
-
onChange={(val) => {
|
|
3014
|
-
if (val !== undefined) {
|
|
3015
|
-
handleOperatorChange(
|
|
3016
|
-
optionName,
|
|
3017
|
-
fixedValue,
|
|
3018
|
-
findKey(operators, (op) => op.name === val) as string,
|
|
3019
|
-
index
|
|
3020
|
-
);
|
|
3021
|
-
}
|
|
3022
|
-
}}
|
|
3023
|
-
/>
|
|
3024
|
-
{(
|
|
3025
|
-
index === operatorParts.length - 1 &&
|
|
3026
|
-
operator &&
|
|
3027
|
-
(operators as any)[operator as string]?.supports_nesting
|
|
3028
|
-
) ?
|
|
3029
|
-
<ReqoreButton
|
|
3030
|
-
icon='AddLine'
|
|
3031
|
-
disabled={readOnly}
|
|
3032
|
-
fixed
|
|
3033
|
-
effect={PositiveColorEffect}
|
|
3034
|
-
onClick={() => handleAddOperator(optionName, fixedValue, index + 1)}
|
|
3035
|
-
/>
|
|
3036
|
-
: null}
|
|
3037
|
-
{size(operatorParts) > 1 ?
|
|
3038
|
-
<ReqoreButton
|
|
3039
|
-
disabled={readOnly}
|
|
3040
|
-
icon='DeleteBinLine'
|
|
3041
|
-
effect={NegativeColorEffect}
|
|
3042
|
-
fixed
|
|
3043
|
-
onClick={() => handleRemoveOperator(optionName, fixedValue, index)}
|
|
3044
|
-
/>
|
|
3045
|
-
: null}
|
|
3046
|
-
</React.Fragment>
|
|
3047
|
-
))}
|
|
3048
|
-
</ReqoreControlGroup>
|
|
3049
|
-
<ReqoreVerticalSpacer height={5} />
|
|
3050
|
-
</>
|
|
3051
|
-
: null}
|
|
3052
|
-
<TemplateField
|
|
3053
|
-
fluid
|
|
3054
|
-
{...(options?.[optionName] as any)}
|
|
3055
|
-
// qorus#347-followup: resolve the field's `inherit_props` against
|
|
3056
|
-
// the CURRENT sibling values (with a fallback to
|
|
3057
|
-
// `inheritedFromParent` — the bag threaded in from an outer
|
|
3058
|
-
// FormEngine when this scope is an `arg_schema` sub-form),
|
|
3059
|
-
// threading each entry as a top-level prop. Each
|
|
3060
|
-
// `<prop-name>: <sibling-field-name>` mapping copies the
|
|
3061
|
-
// sibling's value onto the rendered field's renderer — e.g. a
|
|
3062
|
-
// `code-editor` with `inherit_props: { language: 'language' }`
|
|
3063
|
-
// picks up the live `language` value as a `language` prop without
|
|
3064
|
-
// a schema refetch. Spread AFTER `{...options?.[optionName]}` so
|
|
3065
|
-
// the runtime value wins over any schema-defined default of the
|
|
3066
|
-
// same key. Mirrored in qorus-ide's `systemOptions.tsx`; see the
|
|
3067
|
-
// CLAUDE.md rule there.
|
|
3068
|
-
{...resolveInheritProps(
|
|
3069
|
-
options?.[optionName]?.inherit_props,
|
|
3070
|
-
availableOptions,
|
|
3071
|
-
inheritedFromParent
|
|
3072
|
-
)}
|
|
3073
|
-
// qorus#347-followup (scope forwarding): merge accumulated
|
|
3074
|
-
// inheritance (`inheritedFromParent`) with THIS field's freshly
|
|
3075
|
-
// resolved inherit_props, and pass down as a single bag so any
|
|
3076
|
-
// nested `arg_schema` sub-form (mounted by AutoFormField for
|
|
3077
|
-
// `hash` / `free-hash` / list-of-hash fields) sees every value
|
|
3078
|
-
// the ancestor chain forwarded. This is the plumbing that lets a
|
|
3079
|
-
// service-method row's `body` sub-field pick up the parent
|
|
3080
|
-
// service form's `language` — the parent field declares
|
|
3081
|
-
// `inherit_props: { language: 'language' }`, the list renderer
|
|
3082
|
-
// forwards it into each row, and the row's body resolves against
|
|
3083
|
-
// the accumulated bag.
|
|
3084
|
-
//
|
|
3085
|
-
// Read from the memoised map rather than built here: this is a
|
|
3086
|
-
// PROP, and one built inline is a new object on every render of
|
|
3087
|
-
// the form, which re-renders every field that receives it. The
|
|
3088
|
-
// `??` chain answers for a field the schema does not describe —
|
|
3089
|
-
// `inheritedScopeFor` returns the SAME references, so an
|
|
3090
|
-
// undescribed field is as stable as a described one.
|
|
3091
|
-
inheritedFromParent={
|
|
3092
|
-
inheritedScopes.get(optionName) ?? inheritedFromParent ?? NO_INHERITED_SCOPE
|
|
3093
|
-
}
|
|
3094
|
-
// Propagate compact so an arg_schema field renders a COMPACT sub-form
|
|
3095
|
-
// (consistent with the parent) rather than the classic FormEngine.
|
|
3096
|
-
compact={compact}
|
|
3097
|
-
// SEAM: forwarded through TemplateField's rest-spread to AutoFormField,
|
|
3098
|
-
// which renders consumer-injected editors by field type/ui_type.
|
|
3099
|
-
componentOverrides={componentOverrides}
|
|
3100
|
-
templateAwareUiTypes={templateAwareUiTypes}
|
|
3101
|
-
allowTemplates={!!(allowTemplates && options?.[optionName]?.supports_templates)}
|
|
3102
|
-
allowFunctions={!!options?.[optionName]?.supports_expressions}
|
|
3103
|
-
// reqraft: form-level expression fields get the Visual/Text shell
|
|
3104
|
-
// (DPQL text mode); opt out per-form via `templateFieldProps`.
|
|
3105
|
-
allowTextExpressions
|
|
3106
|
-
// A fixed allowed-value field still needs its selector even when
|
|
3107
|
-
// arbitrary custom values are forbidden. TemplateField uses this
|
|
3108
|
-
// flag to decide whether to mount AutoFormField at all; treating
|
|
3109
|
-
// `supports_custom_values: false` as "no input" left dependent
|
|
3110
|
-
// delivery fields (Discord server/channel, for example) as empty
|
|
3111
|
-
// control groups. The allowed-values renderer itself enforces the
|
|
3112
|
-
// fixed catalogue and never exposes free-form entry.
|
|
3113
|
-
allowCustomValues={
|
|
3114
|
-
isFixedCompactAllowedValueOption(optionSchema) ||
|
|
3115
|
-
(options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any')
|
|
3116
|
-
}
|
|
3117
|
-
// A field may declare a template list OF ITS OWN, and when it does
|
|
3118
|
-
// that list is the one to offer. The engine-wide `stringTemplates`
|
|
3119
|
-
// are the form's shared vocabulary - config items, system properties
|
|
3120
|
-
// - and every field draws on the same set. A per-field list answers a
|
|
3121
|
-
// question only that field asks: a test assertion's `$.` references
|
|
3122
|
-
// are the values ITS case captured, and no other field in the form
|
|
3123
|
-
// should be offering them.
|
|
3124
|
-
//
|
|
3125
|
-
// Passing the engine's list unconditionally overwrote the schema's,
|
|
3126
|
-
// so a field that declared templates had none by the time it
|
|
3127
|
-
// rendered. On a typed field that only cost the picker; on an
|
|
3128
|
-
// ANY-LIKE one it changed the question, because `TemplateField`
|
|
3129
|
-
// opens on the template selector only when there is something to
|
|
3130
|
-
// pick and otherwise falls back to the type picker. An untyped field
|
|
3131
|
-
// therefore asked the author to choose `string`/`int`/`hash` before
|
|
3132
|
-
// it would let them name a value they had already captured.
|
|
3133
|
-
templates={
|
|
3134
|
-
(optionSchema as TFieldWithOwnTemplates | undefined)?.templates ?? templates.value
|
|
3135
|
-
}
|
|
3136
|
-
{...getTypeAndCanBeNull(
|
|
3137
|
-
// The RENDERER type picks the editor — the storage type lives on
|
|
3138
|
-
// the value envelope. Passing storage here rendered a `richtext`
|
|
3139
|
-
// field as a plain string input.
|
|
3140
|
-
resolvedType,
|
|
3141
|
-
options?.[optionName]?.allowed_values,
|
|
3142
|
-
other.op
|
|
3143
|
-
)}
|
|
3144
|
-
ui_type={resolvedType}
|
|
3145
|
-
name={optionName}
|
|
3146
|
-
uniqueName={`${uniqueName ? `${uniqueName}.` : `${name ? `${name}.` : ''}`}${optionName}`}
|
|
3147
|
-
onChange={
|
|
3148
|
-
// Identity-stable on purpose: the typed fields debounce on
|
|
3149
|
-
// `[localValue, onChange]` — an inline lambda resets the pending emit
|
|
3150
|
-
// every render and the typed value can starve.
|
|
3151
|
-
handleValueChange
|
|
3152
|
-
}
|
|
3153
|
-
key={optionName}
|
|
3154
|
-
arg_schema={options?.[optionName]?.arg_schema}
|
|
3155
|
-
value={expressionAwareValue}
|
|
3156
|
-
isFunction={optionHoldsExpression(other)}
|
|
3157
|
-
isDefaultFunction={options?.[optionName]?.default_view === 'expression'}
|
|
3158
|
-
sensitive={options?.[optionName]?.sensitive}
|
|
3159
|
-
default_value={getDefaultValue(options?.[optionName])}
|
|
3160
|
-
isDefaultTemplate={options?.[optionName]?.default_view === 'template'}
|
|
3161
|
-
allowed_values={options?.[optionName]?.allowed_values}
|
|
3162
|
-
allowed_values_creatable={options?.[optionName]?.allowed_values_creatable}
|
|
3163
|
-
disabled={
|
|
3164
|
-
options?.[optionName]?.disabled ||
|
|
3165
|
-
// a system-owned field states that as `readonly`; only the older
|
|
3166
|
-
// compat projection also sent `disabled`, so honouring only
|
|
3167
|
-
// `disabled` rendered an editable control for a value the user
|
|
3168
|
-
// cannot change (see CompactRow's `fieldReadonly`)
|
|
3169
|
-
(options?.[optionName] as { readonly?: boolean } | undefined)?.readonly ||
|
|
3170
|
-
readOnly ||
|
|
3171
|
-
!hasAllDependenciesFullfilled(
|
|
3172
|
-
options?.[optionName]?.depends_on,
|
|
3173
|
-
availableOptions,
|
|
3174
|
-
options || {}
|
|
3175
|
-
)
|
|
3176
|
-
}
|
|
3177
|
-
readOnly={readOnly}
|
|
3178
|
-
size={editorSize || rest.size}
|
|
3179
|
-
menuItems={
|
|
3180
|
-
offersTypeChoices(options?.[optionName], anyLikeUiTypes) ?
|
|
3181
|
-
getCustomMenuTemplateItems(optionName)
|
|
3182
|
-
: undefined
|
|
3183
|
-
}
|
|
3184
|
-
{...templateFieldProps}
|
|
3185
|
-
forceDropdown={effectiveForceDropdown}
|
|
3186
|
-
/>
|
|
3187
|
-
<OptionFieldMessages
|
|
3188
|
-
schema={options || {}}
|
|
3189
|
-
allOptions={availableOptions}
|
|
3190
|
-
name={optionName}
|
|
3191
|
-
option={{ type: resolvedType, ...other }}
|
|
3192
|
-
getType={getTypeForOption}
|
|
3193
|
-
isRendererOnly={isRendererOnly}
|
|
3194
|
-
/>
|
|
3195
|
-
{operators && size(operators) && size(other.op) ?
|
|
3196
|
-
<>
|
|
3197
|
-
<ReqoreVerticalSpacer height={5} />
|
|
3198
|
-
<ReqoreMessage size='small'>
|
|
3199
|
-
<ReqoreTagGroup>
|
|
3200
|
-
<ReqoreTag size='small' labelKey='WHERE' label={optionName} />
|
|
3201
|
-
<ReqoreTag size='small' labelKey='IS' label={operatorParts.join(' ')} />
|
|
3202
|
-
<ReqoreTag
|
|
3203
|
-
size='small'
|
|
3204
|
-
intent='info'
|
|
3205
|
-
label={
|
|
3206
|
-
other.value ?
|
|
3207
|
-
resolvedType === 'richtext' ?
|
|
3208
|
-
richtextToString(other.value)
|
|
3209
|
-
: JSON.stringify(other.value)
|
|
3210
|
-
: ''
|
|
3211
|
-
}
|
|
3212
|
-
/>
|
|
3213
|
-
</ReqoreTagGroup>
|
|
3214
|
-
</ReqoreMessage>
|
|
3215
|
-
</>
|
|
3216
|
-
: null}
|
|
3217
|
-
</>
|
|
3218
|
-
);
|
|
3219
|
-
},
|
|
3220
|
-
[
|
|
3221
|
-
options,
|
|
3222
|
-
operators,
|
|
3223
|
-
readOnly,
|
|
3224
|
-
allowTemplates,
|
|
3225
|
-
templates,
|
|
3226
|
-
name,
|
|
3227
|
-
uniqueName,
|
|
3228
|
-
componentOverrides,
|
|
3229
|
-
templateFieldProps,
|
|
3230
|
-
forceDropdown,
|
|
3231
|
-
availableOptions,
|
|
3232
|
-
fixedValue,
|
|
3233
|
-
// Depend on the specific `rest` values used, not the whole `rest` object
|
|
3234
|
-
// (which is a fresh `{...rest}` every render and would defeat the memo).
|
|
3235
|
-
rest?.options,
|
|
3236
|
-
rest?.size,
|
|
3237
|
-
handleValueChange,
|
|
3238
|
-
handleOperatorChange,
|
|
3239
|
-
handleAddOperator,
|
|
3240
|
-
handleRemoveOperator,
|
|
3241
|
-
getCustomMenuTemplateItems,
|
|
3242
|
-
getTypeForOption,
|
|
3243
|
-
inheritedScopes,
|
|
3244
|
-
inheritedFromParent,
|
|
3245
|
-
]
|
|
3246
|
-
);
|
|
3247
|
-
|
|
3248
|
-
// Compact (read-first) rendering.
|
|
3249
|
-
// Theme-derived colours so the flat-row layout adapts to light/dark/custom themes.
|
|
3250
|
-
const cText = theme?.text?.color || '#f1f0ee';
|
|
3251
|
-
// Text emphasis tiers via reqore's readable-colour helper (key = full readable
|
|
3252
|
-
// text, muted = its dimmed variant, faint = the dimmed variant softened
|
|
3253
|
-
// further) instead of hand-rolled hex-alpha suffixes on the raw text colour.
|
|
3254
|
-
const cKey = getReadableColor(theme);
|
|
3255
|
-
const cMuted = getReadableColor(theme, undefined, undefined, true);
|
|
3256
|
-
const cFaint = `${cMuted}99`;
|
|
3257
|
-
const cDivider = `${cText}14`;
|
|
3258
|
-
const cHover = `${cText}0d`;
|
|
3259
|
-
const cDanger = theme?.intents?.danger || '#e35a5a';
|
|
3260
|
-
const cWarning = theme?.intents?.warning || '#ffdf34';
|
|
3261
|
-
const cInfo = theme?.intents?.info || '#3b8eea';
|
|
3262
|
-
const cSuccess = theme?.intents?.success || '#36b37e';
|
|
3263
|
-
const cBg = (theme as { main?: string } | undefined)?.main || '#181818';
|
|
3264
|
-
const cRowBg = changeDarkness(getMainBackgroundColor(theme), 0.01);
|
|
3265
|
-
const cGroupLine = `${cText}1f`;
|
|
3266
|
-
// The "N more fields" row's dashed edge. Well above the divider tint: this
|
|
3267
|
-
// one is an affordance the reader has to notice, not a hairline between rows.
|
|
3268
|
-
const cMoreBorder = `${cText}4d`;
|
|
3269
|
-
|
|
3270
|
-
// The closure surface the extracted CompactRow reads through context. Refs and
|
|
3271
|
-
// setters are stable; the state/memo/handler fields change identity as they do
|
|
3272
|
-
// today, so a row re-renders exactly when its inputs do.
|
|
3273
|
-
const compactRowContextValue = useMemo<ICompactRowContext>(
|
|
3274
|
-
() => ({
|
|
3275
|
-
templates: templates.value,
|
|
3276
|
-
readOnly,
|
|
3277
|
-
onReadOnlyActivate,
|
|
3278
|
-
commitMode,
|
|
3279
|
-
expandMode,
|
|
3280
|
-
options,
|
|
3281
|
-
operators,
|
|
3282
|
-
focusedEditing,
|
|
3283
|
-
showFieldTypes,
|
|
3284
|
-
codePreviewRenderer,
|
|
3285
|
-
showAllDescriptions,
|
|
3286
|
-
expandedOptions,
|
|
3287
|
-
highlightedOptions,
|
|
3288
|
-
flashedOptions,
|
|
3289
|
-
infoPanelOverrides,
|
|
3290
|
-
setHighlightedOptions,
|
|
3291
|
-
setInfoPanelOverrides,
|
|
3292
|
-
setFocusedEditing,
|
|
3293
|
-
readRowHeights,
|
|
3294
|
-
originalValue,
|
|
3295
|
-
availableOptions,
|
|
3296
|
-
requiredGroupsInfo,
|
|
3297
|
-
handleValueChange,
|
|
3298
|
-
handleAddOptionalFieldChange,
|
|
3299
|
-
toggleExpandedOption,
|
|
3300
|
-
flashOption,
|
|
3301
|
-
hasOptionChanged,
|
|
3302
|
-
handleOptionLabelClick,
|
|
3303
|
-
removeSelectedOption,
|
|
3304
|
-
getTypeForOption,
|
|
3305
|
-
isRendererOnly,
|
|
3306
|
-
isOptionValid,
|
|
3307
|
-
confirmAction,
|
|
3308
|
-
optionActions,
|
|
3309
|
-
collapseOptionActions,
|
|
3310
|
-
renderOption,
|
|
3311
|
-
theme,
|
|
3312
|
-
cText,
|
|
3313
|
-
cMuted,
|
|
3314
|
-
cFaint,
|
|
3315
|
-
cKey,
|
|
3316
|
-
cDivider,
|
|
3317
|
-
cHover,
|
|
3318
|
-
cDanger,
|
|
3319
|
-
cWarning,
|
|
3320
|
-
cInfo,
|
|
3321
|
-
cBg,
|
|
3322
|
-
}),
|
|
3323
|
-
[
|
|
3324
|
-
templates.value,
|
|
3325
|
-
readOnly,
|
|
3326
|
-
commitMode,
|
|
3327
|
-
expandMode,
|
|
3328
|
-
options,
|
|
3329
|
-
operators,
|
|
3330
|
-
focusedEditing,
|
|
3331
|
-
showFieldTypes,
|
|
3332
|
-
codePreviewRenderer,
|
|
3333
|
-
showAllDescriptions,
|
|
3334
|
-
expandedOptions,
|
|
3335
|
-
highlightedOptions,
|
|
3336
|
-
flashedOptions,
|
|
3337
|
-
infoPanelOverrides,
|
|
3338
|
-
setHighlightedOptions,
|
|
3339
|
-
setInfoPanelOverrides,
|
|
3340
|
-
setFocusedEditing,
|
|
3341
|
-
readRowHeights,
|
|
3342
|
-
originalValue,
|
|
3343
|
-
availableOptions,
|
|
3344
|
-
requiredGroupsInfo,
|
|
3345
|
-
handleValueChange,
|
|
3346
|
-
handleAddOptionalFieldChange,
|
|
3347
|
-
toggleExpandedOption,
|
|
3348
|
-
flashOption,
|
|
3349
|
-
hasOptionChanged,
|
|
3350
|
-
handleOptionLabelClick,
|
|
3351
|
-
removeSelectedOption,
|
|
3352
|
-
getTypeForOption,
|
|
3353
|
-
isRendererOnly,
|
|
3354
|
-
isOptionValid,
|
|
3355
|
-
confirmAction,
|
|
3356
|
-
optionActions,
|
|
3357
|
-
collapseOptionActions,
|
|
3358
|
-
renderOption,
|
|
3359
|
-
theme,
|
|
3360
|
-
cText,
|
|
3361
|
-
cMuted,
|
|
3362
|
-
cFaint,
|
|
3363
|
-
cKey,
|
|
3364
|
-
cDivider,
|
|
3365
|
-
cHover,
|
|
3366
|
-
cDanger,
|
|
3367
|
-
cWarning,
|
|
3368
|
-
cInfo,
|
|
3369
|
-
cBg,
|
|
3370
|
-
]
|
|
3371
|
-
);
|
|
3372
|
-
|
|
3373
|
-
// `compactToolbar` resolved to a full object, so neither the context type nor
|
|
3374
|
-
// the toolbar itself has to re-default anything. `null` means no toolbar.
|
|
3375
|
-
const compactToolbarParts = useMemo(() => {
|
|
3376
|
-
if (compactToolbar === false) return null;
|
|
3377
|
-
const all = { completion: true, search: true, fields: true, help: true };
|
|
3378
|
-
return compactToolbar === true ? all : { ...all, ...compactToolbar };
|
|
3379
|
-
}, [JSON.stringify(compactToolbar)]);
|
|
3380
|
-
|
|
3381
|
-
const compactToolbarContextValue = useMemo<ICompactToolbarContext>(
|
|
3382
|
-
() => ({
|
|
3383
|
-
readOnly,
|
|
3384
|
-
invalidCount: size(validityData.invalidFields),
|
|
3385
|
-
attentionCount: readFirstAttentionCount,
|
|
3386
|
-
completion: readFirstCompletion,
|
|
3387
|
-
showInvalidOnly: showInvalidOptionsOnly,
|
|
3388
|
-
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
3389
|
-
parts: compactToolbarParts ?? {
|
|
3390
|
-
completion: false,
|
|
3391
|
-
search: false,
|
|
3392
|
-
fields: false,
|
|
3393
|
-
help: false,
|
|
3394
|
-
},
|
|
3395
|
-
/* From the SCHEMA, not from the value.
|
|
3396
|
-
*
|
|
3397
|
-
* `availableOptions` is keyed off the form's VALUE, so it grows as data
|
|
3398
|
-
* arrives — and the search row is rendered only when this is true
|
|
3399
|
-
* (`CompactToolbar`). A form whose value lands after its first render
|
|
3400
|
-
* therefore ADDED a 38px row to its header a beat later, and everything
|
|
3401
|
-
* below it moved. Measured on the Qorus IDE's test editor, on the nested
|
|
3402
|
-
* sub-form of a case: 33 elements jumped down 51px in one frame, on every
|
|
3403
|
-
* cold load.
|
|
3404
|
-
*
|
|
3405
|
-
* How many fields a form has is a property of its schema. The two agree
|
|
3406
|
-
* once the value has arrived — `fixOptions` materialises an entry per
|
|
3407
|
-
* declared field — so this only changes the answer during the load, which
|
|
3408
|
-
* is the whole point. */
|
|
3409
|
-
hasMultipleOptions: size(options) > 1,
|
|
3410
|
-
compactQuery,
|
|
3411
|
-
setCompactQuery,
|
|
3412
|
-
requiredOnly,
|
|
3413
|
-
setRequiredOnly,
|
|
3414
|
-
compactSort,
|
|
3415
|
-
setCompactSort,
|
|
3416
|
-
showFieldTypes,
|
|
3417
|
-
showAllDescriptions,
|
|
3418
|
-
onToggleFieldTypes: handleShowFieldTypesClick,
|
|
3419
|
-
onToggleAllDescriptions: handleToggleAllDescriptions,
|
|
3420
|
-
filteredCount: size(filteredOptions),
|
|
3421
|
-
optionalFields,
|
|
3422
|
-
canRevert: !!(originalValue.current && !isEqual(localValue.fields, originalValue.current)),
|
|
3423
|
-
onAddOptionalField: (value) => handleAddOptionalFieldChange('options', value),
|
|
3424
|
-
onAddAll: handleAddAllOptional,
|
|
3425
|
-
onResetDefaults: handleResetToDefaultFields,
|
|
3426
|
-
onRevertAll: handleRevertChangesClick,
|
|
3427
|
-
}),
|
|
3428
|
-
[
|
|
3429
|
-
readOnly,
|
|
3430
|
-
onReadOnlyActivate,
|
|
3431
|
-
validityData,
|
|
3432
|
-
readFirstAttentionCount,
|
|
3433
|
-
readFirstCompletion,
|
|
3434
|
-
showInvalidOptionsOnly,
|
|
3435
|
-
handleToggleInvalidOnly,
|
|
3436
|
-
availableOptions,
|
|
3437
|
-
compactQuery,
|
|
3438
|
-
setCompactQuery,
|
|
3439
|
-
requiredOnly,
|
|
3440
|
-
setRequiredOnly,
|
|
3441
|
-
compactSort,
|
|
3442
|
-
setCompactSort,
|
|
3443
|
-
showFieldTypes,
|
|
3444
|
-
showAllDescriptions,
|
|
3445
|
-
handleShowFieldTypesClick,
|
|
3446
|
-
handleToggleAllDescriptions,
|
|
3447
|
-
filteredOptions,
|
|
3448
|
-
optionalFields,
|
|
3449
|
-
originalValue,
|
|
3450
|
-
localValue,
|
|
3451
|
-
handleAddOptionalFieldChange,
|
|
3452
|
-
handleAddAllOptional,
|
|
3453
|
-
handleResetToDefaultFields,
|
|
3454
|
-
handleRevertChangesClick,
|
|
3455
|
-
]
|
|
3456
|
-
);
|
|
3457
|
-
|
|
3458
|
-
// The engine's own toolbar action, plus whatever the consumer added through
|
|
3459
|
-
// `compactPanelProps.actions` — appended, so an outside action can never
|
|
3460
|
-
// knock the form's toolbar out of the header. Only `compactToolbar={false}`
|
|
3461
|
-
// removes it, and then the header carries just the consumer's own actions.
|
|
3462
|
-
const compactHeaderActions = useMemo(
|
|
3463
|
-
() => [
|
|
3464
|
-
...(compactToolbarParts ? [{ as: CompactToolbar, responsive: false }] : []),
|
|
3465
|
-
...(compactPanelProps?.actions || []),
|
|
3466
|
-
],
|
|
3467
|
-
[compactPanelProps?.actions, compactToolbarParts]
|
|
3468
|
-
);
|
|
3469
|
-
const renderCompact = () => {
|
|
3470
|
-
const headerBg = `${changeDarkness(getMainBackgroundColor(theme), 0.02)}${percentToHexAlpha(88)}`;
|
|
3471
|
-
// Toolbar filters narrow the listed rows; the meter reflects the full set.
|
|
3472
|
-
const query = compactQuery.trim().toLowerCase();
|
|
3473
|
-
const matchesQuery = (optionName: string): boolean =>
|
|
3474
|
-
!query || (options?.[optionName]?.display_name || optionName).toLowerCase().includes(query);
|
|
3475
|
-
const matchesFilters = (optionName: string): boolean => {
|
|
3476
|
-
const schema = options?.[optionName];
|
|
3477
|
-
if (requiredOnly && !(schema?.required || schema?.required_groups)) {
|
|
3478
|
-
return false;
|
|
3479
|
-
}
|
|
3480
|
-
// A field that absorbs a sibling also answers for it: searching
|
|
3481
|
-
// "language" has to surface the row that now holds the language control,
|
|
3482
|
-
// or the field becomes unreachable through the filter.
|
|
3483
|
-
if (matchesQuery(optionName)) return true;
|
|
3484
|
-
const absorb = (schema as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
3485
|
-
return !!absorb?.some((absorbedName) => matchesQuery(absorbedName));
|
|
3486
|
-
};
|
|
3487
|
-
|
|
3488
|
-
// Fields a sibling absorbs into its own container.
|
|
3489
|
-
//
|
|
3490
|
-
// A field declaring `absorb_fields: ['language']` renders that sibling's
|
|
3491
|
-
// control inside its own row instead of letting it have a row of its own.
|
|
3492
|
-
// The pair this exists for is a code editor and its language: they are one
|
|
3493
|
-
// decision — "what is this code, and in what language" — and reading them
|
|
3494
|
-
// as two unrelated rows one above the other is what the layout used to say.
|
|
3495
|
-
//
|
|
3496
|
-
// Absorbed fields stay in `grouped`, and therefore in the Set / Optional /
|
|
3497
|
-
// Needs-attention counts and in the completion meter. Only the ROW is
|
|
3498
|
-
// skipped. Dropping them from the buckets instead would leave the meter
|
|
3499
|
-
// ("13/18 set", computed from `availableOptions`) disagreeing with the box
|
|
3500
|
-
// badge beside it, which counts row entries.
|
|
3501
|
-
const absorbedBy: Record<string, string> = {};
|
|
3502
|
-
forEach(options, (option, optionName) => {
|
|
3503
|
-
const absorb = (option as { absorb_fields?: string[] } | undefined)?.absorb_fields;
|
|
3504
|
-
// A read-only form absorbs nothing. A host row shows an absorbed
|
|
3505
|
-
// sibling only inside its EDITOR, and a read view never opens one — so
|
|
3506
|
-
// the language a code field had absorbed would be nowhere on the page.
|
|
3507
|
-
// Every set field gets a row of its own instead.
|
|
3508
|
-
if (!absorb?.length || readOnly) return;
|
|
3509
|
-
absorb.forEach((absorbedName) => {
|
|
3510
|
-
// Only absorb a sibling that exists and is not already spoken for; a
|
|
3511
|
-
// schema naming a missing field must not make the field disappear.
|
|
3512
|
-
if (options?.[absorbedName] && !absorbedBy[absorbedName]) {
|
|
3513
|
-
absorbedBy[absorbedName] = optionName;
|
|
3514
|
-
}
|
|
3515
|
-
});
|
|
3516
|
-
});
|
|
3517
|
-
|
|
3518
|
-
// Group the options by their raw `group` key, remembering the order groups
|
|
3519
|
-
// first appear in the schema. Each row carries a `hidden` flag.
|
|
3520
|
-
const groupOrder: string[] = [];
|
|
3521
|
-
const grouped: Record<string, Array<{ name: string; hidden: boolean }>> = {};
|
|
3522
|
-
const pushRow = (optionName: string, hidden: boolean) => {
|
|
3523
|
-
const group = getOptionGroup(options?.[optionName]);
|
|
3524
|
-
if (!grouped[group]) {
|
|
3525
|
-
grouped[group] = [];
|
|
3526
|
-
groupOrder.push(group);
|
|
3527
|
-
}
|
|
3528
|
-
grouped[group].push({ name: optionName, hidden });
|
|
3529
|
-
};
|
|
3530
|
-
// Listed (added / preselected / required) options that pass the filters.
|
|
3531
|
-
forEach(shownOptions, (_option, optionName) => {
|
|
3532
|
-
if (matchesFilters(optionName)) {
|
|
3533
|
-
pushRow(optionName, false);
|
|
3534
|
-
}
|
|
3535
|
-
});
|
|
3536
|
-
// Surface EVERY not-yet-added optional field as an addable (hidden) row, so
|
|
3537
|
-
// the whole schema is browsable inline — they all land in the Optional box
|
|
3538
|
-
// (hidden ⇒ 'optional' bucket) instead of being buried in the Fields menu.
|
|
3539
|
-
// Narrowed by the same filters as the listed rows (search query + required-
|
|
3540
|
-
// only). availableOptions (listed) and filteredOptions (these) are disjoint —
|
|
3541
|
-
// the former is built from fixedValue keys, the latter excludes them — so a
|
|
3542
|
-
// field is never both a listed and a hidden row.
|
|
3543
|
-
// Not in a read-only form: a not-yet-added field is an offer to add it,
|
|
3544
|
-
// and a reader cannot take it up.
|
|
3545
|
-
if (!readOnly) {
|
|
3546
|
-
forEach(filteredOptions, (_schema, optionName) => {
|
|
3547
|
-
if (matchesFilters(optionName)) {
|
|
3548
|
-
pushRow(optionName, true);
|
|
3549
|
-
}
|
|
3550
|
-
});
|
|
3551
|
-
}
|
|
3552
|
-
|
|
3553
|
-
// User sort (Fields menu → "Sort by"), applied WITHIN each group so the
|
|
3554
|
-
// group sections and the required-group rails are preserved. Schema order is
|
|
3555
|
-
// the default and the stable tiebreaker (Array.sort is stable, and each
|
|
3556
|
-
// group's array is already in schema order). Clustering (renderGroupRows)
|
|
3557
|
-
// still pulls required-group members together at the first member's — now
|
|
3558
|
-
// sorted — slot.
|
|
3559
|
-
if (compactSort !== 'schema') {
|
|
3560
|
-
const labelOf = (name: string) => (options?.[name]?.display_name || name).toLowerCase();
|
|
3561
|
-
const isUnset = (name: string) =>
|
|
3562
|
-
isOptionValueEmpty((shownOptions as TQorusForm)[name]?.value);
|
|
3563
|
-
const isFieldInvalid = (name: string) =>
|
|
3564
|
-
!isOptionValid(
|
|
3565
|
-
name,
|
|
3566
|
-
getOptionFieldStorageType(
|
|
3567
|
-
name,
|
|
3568
|
-
(shownOptions as TQorusForm)[name]?.type,
|
|
3569
|
-
options,
|
|
3570
|
-
operators,
|
|
3571
|
-
(shownOptions as TQorusForm)[name]?.op,
|
|
3572
|
-
isRendererOnly
|
|
3573
|
-
),
|
|
3574
|
-
(shownOptions as TQorusForm)[name]?.value
|
|
3575
|
-
);
|
|
3576
|
-
const comparator = (a: { name: string }, b: { name: string }): number => {
|
|
3577
|
-
switch (compactSort) {
|
|
3578
|
-
case 'alpha':
|
|
3579
|
-
return labelOf(a.name).localeCompare(labelOf(b.name));
|
|
3580
|
-
case 'alpha-desc':
|
|
3581
|
-
return labelOf(b.name).localeCompare(labelOf(a.name));
|
|
3582
|
-
// unset/invalid first — falsy(0) sorts after truthy(1), so b - a.
|
|
3583
|
-
case 'unset':
|
|
3584
|
-
return Number(isUnset(b.name)) - Number(isUnset(a.name));
|
|
3585
|
-
case 'invalid':
|
|
3586
|
-
return Number(isFieldInvalid(b.name)) - Number(isFieldInvalid(a.name));
|
|
3587
|
-
default:
|
|
3588
|
-
return 0;
|
|
3589
|
-
}
|
|
3590
|
-
};
|
|
3591
|
-
forEach(grouped, (entries) => entries.sort(comparator));
|
|
3592
|
-
}
|
|
3593
|
-
|
|
3594
|
-
// Order groups by the consumer-supplied `sort` (when given), else first-seen.
|
|
3595
|
-
const groupKeys = groupOrder.slice().sort((a, b) => {
|
|
3596
|
-
const sa = groups?.[a]?.sort;
|
|
3597
|
-
const sb = groups?.[b]?.sort;
|
|
3598
|
-
if (sa != null && sb != null && sa !== sb) return sa - sb;
|
|
3599
|
-
if (sa != null && sb == null) return -1;
|
|
3600
|
-
if (sb != null && sa == null) return 1;
|
|
3601
|
-
return groupOrder.indexOf(a) - groupOrder.indexOf(b);
|
|
3602
|
-
});
|
|
3603
|
-
|
|
3604
|
-
// Read-first STATUS per row → one of three boxes (Needs attention / Set /
|
|
3605
|
-
// Optional), via the component-scope getOptionBucket (shared with the header
|
|
3606
|
-
// count and the row dot, so box ↔ dot ↔ header always agree). Buckets keep
|
|
3607
|
-
// schema-group order (thin sub-labels) and the required-group clustering: an
|
|
3608
|
-
// unmet one-of group's members all land in attention and still rail up.
|
|
3609
|
-
type TRowEntry = { name: string; hidden: boolean };
|
|
3610
|
-
const buckets: Record<TFormEngineBoxKey, Record<string, TRowEntry[]>> = {
|
|
3611
|
-
attention: {},
|
|
3612
|
-
set: {},
|
|
3613
|
-
optional: {},
|
|
3614
|
-
};
|
|
3615
|
-
const bucketGroups: Record<TFormEngineBoxKey, string[]> = {
|
|
3616
|
-
attention: [],
|
|
3617
|
-
set: [],
|
|
3618
|
-
optional: [],
|
|
3619
|
-
};
|
|
3620
|
-
// Freeze the box of any field currently being edited (or whose one-of group
|
|
3621
|
-
// has an edited member) to its last settled box — so finishing an edit that
|
|
3622
|
-
// flips its status doesn't remount it in another box and steal focus.
|
|
3623
|
-
const stableBucketOf = (entry: TRowEntry): TFormEngineBoxKey => {
|
|
3624
|
-
const fresh = getOptionBucket(entry.name, entry.hidden);
|
|
3625
|
-
const groupBeingEdited =
|
|
3626
|
-
!entry.hidden &&
|
|
3627
|
-
((options?.[entry.name]?.required_groups as string[] | undefined) || []).some((g) =>
|
|
3628
|
-
(requiredGroupsInfo.members[g] || []).some((m) => expandedOptions.includes(m))
|
|
3629
|
-
);
|
|
3630
|
-
if (!entry.hidden && (expandedOptions.includes(entry.name) || groupBeingEdited)) {
|
|
3631
|
-
const memo = settledBucket.current[entry.name];
|
|
3632
|
-
if (memo) return memo;
|
|
3633
|
-
}
|
|
3634
|
-
settledBucket.current[entry.name] = fresh;
|
|
3635
|
-
return fresh;
|
|
3636
|
-
};
|
|
3637
|
-
// Read-only: one list, not three boxes. "Needs attention" is an
|
|
3638
|
-
// instruction, and a reader has nothing to do with it — a required field
|
|
3639
|
-
// nobody filled in is shown unset, without the amber. "Optional" holds
|
|
3640
|
-
// the fields nobody set, which in a read view is a list of things that
|
|
3641
|
-
// are not there, so it is left out altogether.
|
|
3642
|
-
const readBucketOf = (entry: TRowEntry): TFormEngineBoxKey | undefined => {
|
|
3643
|
-
const fresh = stableBucketOf(entry);
|
|
3644
|
-
if (!readOnly) return fresh;
|
|
3645
|
-
return fresh === 'optional' ? undefined : 'set';
|
|
3646
|
-
};
|
|
3647
|
-
groupKeys.forEach((groupName) => {
|
|
3648
|
-
grouped[groupName].forEach((entry) => {
|
|
3649
|
-
const b = readBucketOf(entry);
|
|
3650
|
-
if (!b) return;
|
|
3651
|
-
if (!buckets[b][groupName]) {
|
|
3652
|
-
buckets[b][groupName] = [];
|
|
3653
|
-
bucketGroups[b].push(groupName);
|
|
3654
|
-
}
|
|
3655
|
-
buckets[b][groupName].push(entry);
|
|
3656
|
-
});
|
|
3657
|
-
});
|
|
3658
|
-
const bucketCount = (b: TFormEngineBoxKey) =>
|
|
3659
|
-
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
3660
|
-
|
|
3661
|
-
/* The default is to collapse the Optional box, and an explicit pass is
|
|
3662
|
-
what waives the "never collapse the sole box" escape below. Resolving it
|
|
3663
|
-
here rather than in the parameter list is what makes the two
|
|
3664
|
-
distinguishable — `['optional']` as a default and `['optional']` from a
|
|
3665
|
-
caller mean different things. */
|
|
3666
|
-
const compactCollapsedGroups = compactCollapsedGroupsProp ?? ['optional'];
|
|
3667
|
-
const collapsedGroupsRequested = compactCollapsedGroupsProp !== undefined;
|
|
3668
|
-
// "Is the Optional box the whole form?" — when nothing needs attention and
|
|
3669
|
-
// nothing is set, collapsing it leaves a card with no visible content at all.
|
|
3670
|
-
const onlyOptionalRows =
|
|
3671
|
-
bucketCount('optional') > 0 && bucketCount('attention') === 0 && bucketCount('set') === 0;
|
|
3672
|
-
// 'general' / 'optional' are the SYNTHETIC fallback group keys getOptionGroup
|
|
3673
|
-
// assigns to fields with no explicit `group` — printing a "General"/"Optional"
|
|
3674
|
-
// sub-label for those is just noise, so suppress it. BUT a consumer may also
|
|
3675
|
-
// use 'general' as a REAL group (defining it in the `groups` prop and tagging
|
|
3676
|
-
// fields with `group: 'general'`); in that case it's a named group like any
|
|
3677
|
-
// other and DOES get its sub-label.
|
|
3678
|
-
const showGroupSubLabel = (groupName: string) =>
|
|
3679
|
-
(groupName !== 'general' && groupName !== 'optional') || !!groups?.[groupName];
|
|
3680
|
-
|
|
3681
|
-
// `preselected` means "show this option by default when a new form is
|
|
3682
|
-
// loaded, so it's visible to the user". An empty non-required preselected
|
|
3683
|
-
// field buckets to 'optional' (nothing is wrong with it, and it has no
|
|
3684
|
-
// value), and the Optional box is collapsed by default — so the very
|
|
3685
|
-
// fields the schema asked to put in front of the author were the ones
|
|
3686
|
-
// nobody could see. A preselected row therefore forces its box open.
|
|
3687
|
-
//
|
|
3688
|
-
// A hidden row is an addable the author has not put in the form yet, so it
|
|
3689
|
-
// is never what `preselected` is talking about, however its schema is
|
|
3690
|
-
// flagged.
|
|
3691
|
-
const isPreselectedRow = (entry: TRowEntry) =>
|
|
3692
|
-
!entry.hidden &&
|
|
3693
|
-
!!(options?.[entry.name] as { preselected?: boolean } | undefined)?.preselected;
|
|
3694
|
-
// ONLY the Optional box. A preselected field that HAS a value buckets to
|
|
3695
|
-
// 'set' and one that fails validation to 'attention'; in both boxes the
|
|
3696
|
-
// field is already accounted for, and whether those boxes start collapsed
|
|
3697
|
-
// is the consumer's deliberate `compactCollapsedGroups` choice. Overriding
|
|
3698
|
-
// them here forced the Set box open against an explicit request.
|
|
3699
|
-
const groupHasPreselectedRow = (boxKey: TFormEngineBoxKey, groupName: string) =>
|
|
3700
|
-
boxKey === 'optional' && (buckets[boxKey][groupName] || []).some(isPreselectedRow);
|
|
3701
|
-
const boxHasPreselectedRows = (boxKey: TFormEngineBoxKey) =>
|
|
3702
|
-
boxKey === 'optional' &&
|
|
3703
|
-
bucketGroups[boxKey].some((groupName) => groupHasPreselectedRow(boxKey, groupName));
|
|
3704
|
-
/* The pin, and where it lands. A nested sub-form sits inside the parent's
|
|
3705
|
-
card and owns no scroll context, so it never pins. */
|
|
3706
|
-
const stickyBoxHeaders = compactStickyBoxHeaders && !compactNested;
|
|
3707
|
-
const boxStickyOffset = compactStickyOffset + compactToolbarHeight;
|
|
3708
|
-
const STATUS_BOXES: Array<{
|
|
3709
|
-
key: TFormEngineBoxKey;
|
|
3710
|
-
label: string;
|
|
3711
|
-
intent?: 'warning' | 'success';
|
|
3712
|
-
icon: IReqoreIconName;
|
|
3713
|
-
}> = [
|
|
3714
|
-
{
|
|
3715
|
-
key: 'attention',
|
|
3716
|
-
label: compactBoxLabels?.attention ?? 'Needs attention',
|
|
3717
|
-
intent: 'warning',
|
|
3718
|
-
icon: 'ErrorWarningLine',
|
|
3719
|
-
},
|
|
3720
|
-
{ key: 'set', label: compactBoxLabels?.set ?? 'Set', intent: 'success', icon: 'CheckLine' },
|
|
3721
|
-
{
|
|
3722
|
-
key: 'optional',
|
|
3723
|
-
label: compactBoxLabels?.optional ?? 'Optional',
|
|
3724
|
-
icon: 'CheckboxBlankCircleLine',
|
|
3725
|
-
},
|
|
3726
|
-
];
|
|
3727
|
-
|
|
3728
|
-
// Build the rows for one group: contiguous required-group members are pulled
|
|
3729
|
-
// together at the first member's slot and rendered as a connected rail (flat
|
|
3730
|
-
// rows — no wrapper — so the value surface applies normally; the rail + nodes
|
|
3731
|
-
// are drawn per member). Narrow stacks fall back to flat rows.
|
|
3732
|
-
// The absorbed siblings a host row must render, in schema order. Empty for
|
|
3733
|
-
// every row that absorbs nothing, which is all of them by default.
|
|
3734
|
-
const absorbedNamesFor = (hostName: string): string[] =>
|
|
3735
|
-
Object.keys(absorbedBy).filter((absorbedName) => absorbedBy[absorbedName] === hostName);
|
|
3736
|
-
|
|
3737
|
-
const renderGroupRows = (names: Array<{ name: string; hidden: boolean }>) => {
|
|
3738
|
-
const renderRow = (
|
|
3739
|
-
entry: { name: string; hidden?: boolean },
|
|
3740
|
-
clustered: boolean,
|
|
3741
|
-
clusterFirst?: boolean,
|
|
3742
|
-
clusterLast?: boolean
|
|
3743
|
-
) => (
|
|
3744
|
-
<CompactRow
|
|
3745
|
-
key={entry.name}
|
|
3746
|
-
optionName={entry.name}
|
|
3747
|
-
optionField={
|
|
3748
|
-
entry.hidden ?
|
|
3749
|
-
({
|
|
3750
|
-
type: getOptionSchemaStorageType(options?.[entry.name], isRendererOnly),
|
|
3751
|
-
value: undefined,
|
|
3752
|
-
} as IQorusFormField)
|
|
3753
|
-
: ((shownOptions as TQorusForm)[entry.name] as IQorusFormField)
|
|
3754
|
-
}
|
|
3755
|
-
hidden={entry.hidden}
|
|
3756
|
-
clustered={clustered}
|
|
3757
|
-
clusterFirst={clusterFirst}
|
|
3758
|
-
clusterLast={clusterLast}
|
|
3759
|
-
// The siblings this row renders inside itself instead of leaving
|
|
3760
|
-
// them a row of their own — see `absorbedBy`.
|
|
3761
|
-
absorbedFields={absorbedNamesFor(entry.name)}
|
|
3762
|
-
/>
|
|
3763
|
-
);
|
|
3764
|
-
// (Clustering runs in narrow mode too now — the "One of the below is
|
|
3765
|
-
// required" box wraps the members regardless of width; it no longer relies
|
|
3766
|
-
// on a contiguous rail.)
|
|
3767
|
-
const emitted = new Set<string>();
|
|
3768
|
-
const groupOf = (name: string) =>
|
|
3769
|
-
(options?.[name]?.required_groups as string[] | undefined)?.[0];
|
|
3770
|
-
// An absorbed field keeps its place in the buckets — so the Set /
|
|
3771
|
-
// Optional counts and the completion meter still see it — but its host
|
|
3772
|
-
// renders its control, so it gets no row of its own here.
|
|
3773
|
-
names = names.filter((entry) => !absorbedBy[entry.name]);
|
|
3774
|
-
return names.map((entry) => {
|
|
3775
|
-
const grp = groupOf(entry.name);
|
|
3776
|
-
if (!grp) return renderRow(entry, false);
|
|
3777
|
-
if (emitted.has(grp)) return null;
|
|
3778
|
-
const memberEntries = names.filter((e) => !e.hidden && groupOf(e.name) === grp);
|
|
3779
|
-
if (memberEntries.length < 2) return renderRow(entry, false);
|
|
3780
|
-
emitted.add(grp);
|
|
3781
|
-
const railed = memberEntries.map((e, idx) =>
|
|
3782
|
-
renderRow(e, true, idx === 0, idx === memberEntries.length - 1)
|
|
3783
|
-
);
|
|
3784
|
-
// An UNMET one-of group gets the explicit "One of the below is required"
|
|
3785
|
-
// box (the Focus cluster). A met group needs no banner — the rail + the
|
|
3786
|
-
// "Covers"/"Covered by" chips already say which member satisfies it.
|
|
3787
|
-
if (requiredGroupsInfo.satisfiedBy[grp]) return railed;
|
|
3788
|
-
return (
|
|
3789
|
-
<StyledRequiredClusterBox
|
|
3790
|
-
key={grp}
|
|
3791
|
-
className='options-readfirst-required-cluster'
|
|
3792
|
-
$border={`${cWarning}33`}
|
|
3793
|
-
$tint={`${cWarning}0d`}
|
|
3794
|
-
>
|
|
3795
|
-
<StyledRequiredClusterHeader $color={cWarning}>
|
|
3796
|
-
<ReqoreIcon icon='LinkM' size='11px' style={{ color: cWarning }} />
|
|
3797
|
-
One of the below is required
|
|
3798
|
-
</StyledRequiredClusterHeader>
|
|
3799
|
-
{railed}
|
|
3800
|
-
</StyledRequiredClusterBox>
|
|
3801
|
-
);
|
|
3802
|
-
});
|
|
3803
|
-
};
|
|
3804
|
-
|
|
3805
|
-
// One schema group inside a status box: its thin sub-label, the group's
|
|
3806
|
-
// subtitle, then its rows. Factored out of the box body so the SAME markup
|
|
3807
|
-
// renders both the groups a box shows directly and the ones it defers
|
|
3808
|
-
// behind the inner collapse.
|
|
3809
|
-
/** @param only when given, render just these rows of the group — the group
|
|
3810
|
-
can be split across the shown and folded halves of a capped box, and its
|
|
3811
|
-
label must appear above whichever half holds its first row. */
|
|
3812
|
-
const renderBoxGroup = (boxKey: TFormEngineBoxKey, groupName: string, only?: Set<string>) => {
|
|
3813
|
-
const groupConfig = groups?.[groupName];
|
|
3814
|
-
const rows =
|
|
3815
|
-
only ?
|
|
3816
|
-
(buckets[boxKey][groupName] || []).filter((entry) => only.has(entry.name))
|
|
3817
|
-
: buckets[boxKey][groupName];
|
|
3818
|
-
if (only && !rows.length) {
|
|
3819
|
-
return null;
|
|
3820
|
-
}
|
|
3821
|
-
return (
|
|
3822
|
-
<React.Fragment key={groupName}>
|
|
3823
|
-
{showGroupSubLabel(groupName) ?
|
|
3824
|
-
<StyledStatusBoxGroupLabel>
|
|
3825
|
-
{getOptionGroupLabel(groupName, groups)}
|
|
3826
|
-
</StyledStatusBoxGroupLabel>
|
|
3827
|
-
: null}
|
|
3828
|
-
{showGroupSubLabel(groupName) && groupConfig?.subtitle ?
|
|
3829
|
-
<ReqoreP
|
|
3830
|
-
size='small'
|
|
3831
|
-
effect={{ opacity: 0.6 }}
|
|
3832
|
-
style={{
|
|
3833
|
-
marginTop: 2,
|
|
3834
|
-
marginBottom: 8,
|
|
3835
|
-
marginLeft: GROUP_INDENT,
|
|
3836
|
-
paddingRight: 10,
|
|
3837
|
-
}}
|
|
3838
|
-
>
|
|
3839
|
-
{groupConfig.subtitle}
|
|
3840
|
-
</ReqoreP>
|
|
3841
|
-
: null}
|
|
3842
|
-
{renderGroupRows(rows)}
|
|
3843
|
-
</React.Fragment>
|
|
3844
|
-
);
|
|
3845
|
-
};
|
|
3846
|
-
|
|
3847
|
-
return (
|
|
3848
|
-
<OptionsContext.Provider value={{ schema: options, value: availableOptions }}>
|
|
3849
|
-
<CompactRowContext.Provider value={compactRowContextValue}>
|
|
3850
|
-
<ReqoreErrorBoundary>
|
|
3851
|
-
{showHelpForOption && (
|
|
3852
|
-
<OptionsHelpDialog
|
|
3853
|
-
onClose={() => setShowHelpForOption(undefined)}
|
|
3854
|
-
option={options[showHelpForOption]}
|
|
3855
|
-
/>
|
|
3856
|
-
)}
|
|
3857
|
-
<CompactToolbarContext.Provider value={compactToolbarContextValue}>
|
|
3858
|
-
<StyledCompactWrap
|
|
3859
|
-
ref={setCompactWrap}
|
|
3860
|
-
className='options-readfirst-scroll'
|
|
3861
|
-
// A nested sub-form is never the scroller — it sits inside the
|
|
3862
|
-
// parent form's card, which is inside whatever actually scrolls.
|
|
3863
|
-
$ownScroll={compactScroll === 'own' && !compactNested}
|
|
3864
|
-
>
|
|
3865
|
-
<StyledCompactPanel
|
|
3866
|
-
// Mirrors the toolbar's own search-row gate: when no search
|
|
3867
|
-
// row renders, the header is a thin strip and sits tight to
|
|
3868
|
-
// the first status box (see StyledCompactPanel).
|
|
3869
|
-
$tightHeader={!(compactToolbarParts?.search && size(options) > 1)}
|
|
3870
|
-
// The top-level form scrolls, so its toolbar STICKS and carries a
|
|
3871
|
-
// dark blurred backdrop so content ghosts cleanly beneath it. A
|
|
3872
|
-
// nested (arg_schema) sub-form owns no scroll context — drop the
|
|
3873
|
-
// sticky, the backdrop, and the stacking context so its header is
|
|
3874
|
-
// transparent inside the parent's card.
|
|
3875
|
-
$headerBg={compactNested ? 'transparent' : headerBg}
|
|
3876
|
-
$nested={compactNested}
|
|
3877
|
-
// Keeps the wrapper from clipping, which is what a pinned box
|
|
3878
|
-
// header inside it needs — see StyledCompactPanel.
|
|
3879
|
-
$stickyBoxes={compactStickyBoxHeaders && !compactNested}
|
|
3880
|
-
// A narrow header the consumer dressed stacks its toolbar
|
|
3881
|
-
// under the label and actions (see StyledCompactPanel).
|
|
3882
|
-
$stackHeader={
|
|
3883
|
-
compactNarrow &&
|
|
3884
|
-
!!(
|
|
3885
|
-
compactPanelProps?.label ||
|
|
3886
|
-
compactPanelProps?.icon ||
|
|
3887
|
-
compactPanelProps?.actions?.length
|
|
3888
|
-
)
|
|
3889
|
-
}
|
|
3890
|
-
flat
|
|
3891
|
-
raised
|
|
3892
|
-
minimal
|
|
3893
|
-
// No panel background: the form sits transparently on whatever
|
|
3894
|
-
// hosts it (page, drawer, or — for an arg_schema field — the
|
|
3895
|
-
// parent's edit card) instead of stacking its own dark surface.
|
|
3896
|
-
// The status boxes keep their own tints; the sticky toolbar keeps
|
|
3897
|
-
// its blurred header via the $headerBg override.
|
|
3898
|
-
// NARROW: the toolbar gives up its pin so the box headers
|
|
3899
|
-
// can keep theirs.
|
|
3900
|
-
//
|
|
3901
|
-
// Measured at 380px, a pinned toolbar is 124px and a pinned
|
|
3902
|
-
// box header is 48 — together a third of a phone's readable
|
|
3903
|
-
// body, before the form has drawn a line. Only one of them
|
|
3904
|
-
// can be afforded there, and the box header is the better
|
|
3905
|
-
// buy: the toolbar is chrome you GO to (a search, a field
|
|
3906
|
-
// picker, a meter — all of it still one scroll away at the
|
|
3907
|
-
// top of the form), while the box header is chrome you READ,
|
|
3908
|
-
// and it is the only thing that says which bucket the rows
|
|
3909
|
-
// under the pointer are in.
|
|
3910
|
-
//
|
|
3911
|
-
// Same shape of decision the engine already makes one level
|
|
3912
|
-
// down, where `optionActionsCollapse: 'auto'` folds a row's
|
|
3913
|
-
// actions into a menu because a narrow row has no width for
|
|
3914
|
-
// a button strip.
|
|
3915
|
-
//
|
|
3916
|
-
// A consumer can still force either way: `compactPanelProps`
|
|
3917
|
-
// spreads after this.
|
|
3918
|
-
stickyHeader={!compactNested && !(compactNarrow && compactStickyBoxHeaders)}
|
|
3919
|
-
// Below the host's own pinned chrome, if it has any — see
|
|
3920
|
-
// `compactStickyOffset`.
|
|
3921
|
-
stickyHeaderOffset={compactStickyOffset || undefined}
|
|
3922
|
-
// Consumer overrides land last so they win over every default
|
|
3923
|
-
// above; `actions` and `contentStyle` below merge rather than
|
|
3924
|
-
// replace (see `compactPanelProps`).
|
|
3925
|
-
{...compactPanelProps}
|
|
3926
|
-
actions={compactHeaderActions}
|
|
3927
|
-
contentStyle={{
|
|
3928
|
-
display: 'flex',
|
|
3929
|
-
flexFlow: 'column',
|
|
3930
|
-
gap: '10px',
|
|
3931
|
-
/* The panel body must not be a SCROLL CONTAINER.
|
|
3932
|
-
|
|
3933
|
-
`StyledPanelContent` ships `overflow: auto` so a
|
|
3934
|
-
height-capped panel can scroll its body. This one is never
|
|
3935
|
-
capped — in `compactScroll: 'host'` the page scrolls and in
|
|
3936
|
-
'own' the wrap above does — so the `auto` never engages and
|
|
3937
|
-
reads as inert. It is not: an `overflow: auto` box is a
|
|
3938
|
-
scrollport whether or not it scrolls, and every
|
|
3939
|
-
`position: sticky` inside resolves against IT. The status
|
|
3940
|
-
box headers (and the toolbar, in a nested host) would pin
|
|
3941
|
-
to a box that never moves, which looks exactly like sticky
|
|
3942
|
-
being ignored.
|
|
3943
|
-
|
|
3944
|
-
`overflow-x: clip`, NOT `hidden`, for the reason spelled
|
|
3945
|
-
out on `StyledCompactWrap`: CSS coerces an `overflow-y:
|
|
3946
|
-
visible` to `auto` unless the other axis is visible or
|
|
3947
|
-
clip, which would put the scrollport straight back. `clip`
|
|
3948
|
-
keeps the horizontal guard the row ellipsis relies on. */
|
|
3949
|
-
overflowY: 'visible',
|
|
3950
|
-
overflowX: 'clip',
|
|
3951
|
-
...compactPanelProps?.contentStyle,
|
|
3952
|
-
}}
|
|
3953
|
-
>
|
|
3954
|
-
{size(groupKeys) === 0 || (readOnly && !bucketCount('set')) ?
|
|
3955
|
-
<ReqoreMessage flat opaque={false} size='small'>
|
|
3956
|
-
{readOnly && !query ?
|
|
3957
|
-
'Nothing is set.'
|
|
3958
|
-
: 'No fields match the current filters.'}
|
|
3959
|
-
</ReqoreMessage>
|
|
3960
|
-
: null}
|
|
3961
|
-
|
|
3962
|
-
{/* A read-only form is one flat list of what is set: no
|
|
3963
|
-
"Set" heading with a count over it (there is nothing else
|
|
3964
|
-
it could be), no green tint (a colour that means "done"
|
|
3965
|
-
on a form nobody is doing), nothing to collapse — and no
|
|
3966
|
-
`maxFieldsShown` fold: "12 more fields" behind a click is
|
|
3967
|
-
the reader's record withheld, and a page scrolls. */}
|
|
3968
|
-
{readOnly ?
|
|
3969
|
-
bucketCount('set') ?
|
|
3970
|
-
<div className='options-readfirst-read' style={{ padding: '4px 4px 6px' }}>
|
|
3971
|
-
<StyledGroupBody
|
|
3972
|
-
$divider={cDivider}
|
|
3973
|
-
$hover={cHover}
|
|
3974
|
-
$focus={cWarning}
|
|
3975
|
-
$success={cSuccess}
|
|
3976
|
-
$rowBg={cRowBg}
|
|
3977
|
-
$lineColor={cGroupLine}
|
|
3978
|
-
$moreBorder={cMoreBorder}
|
|
3979
|
-
className={compactNarrow ? 'readfirst-narrow' : undefined}
|
|
3980
|
-
>
|
|
3981
|
-
{bucketGroups.set.map((groupName) => renderBoxGroup('set', groupName))}
|
|
3982
|
-
</StyledGroupBody>
|
|
3983
|
-
</div>
|
|
3984
|
-
: null
|
|
3985
|
-
: STATUS_BOXES.map((box) => {
|
|
3986
|
-
const groupsInBox = bucketGroups[box.key];
|
|
3987
|
-
const count = bucketCount(box.key);
|
|
3988
|
-
if (!count) return null;
|
|
3989
|
-
const hasPreselectedRows = boxHasPreselectedRows(box.key);
|
|
3990
|
-
const wouldCollapse =
|
|
3991
|
-
compactCollapsedGroups.includes(box.key) &&
|
|
3992
|
-
!query &&
|
|
3993
|
-
!(box.key === 'optional' && onlyOptionalRows && !collapsedGroupsRequested);
|
|
3994
|
-
// Only a box that was opened SOLELY to reveal a preselected
|
|
3995
|
-
// row splits its body; a box that renders open anyway shows
|
|
3996
|
-
// all of its groups as before.
|
|
3997
|
-
const splitForPreselected = wouldCollapse && hasPreselectedRows;
|
|
3998
|
-
|
|
3999
|
-
// Which rows this box shows, and which fold away, decided at
|
|
4000
|
-
// ROW level because `maxFieldsShown` counts fields — a
|
|
4001
|
-
// group-level split can only ever cut at a group boundary.
|
|
4002
|
-
// Group order is preserved so the visible rows read in the
|
|
4003
|
-
// same order they would if nothing were folded.
|
|
4004
|
-
const orderedRows: TRowEntry[] = groupsInBox.flatMap(
|
|
4005
|
-
(groupName) => buckets[box.key][groupName] || []
|
|
4006
|
-
);
|
|
4007
|
-
// Optional's own rule first: a box opened solely to reveal
|
|
4008
|
-
// preselected rows offers those, not an arbitrary first-N.
|
|
4009
|
-
const candidateRows =
|
|
4010
|
-
splitForPreselected ? orderedRows.filter(isPreselectedRow) : orderedRows;
|
|
4011
|
-
// …then the cap applies to whatever that left.
|
|
4012
|
-
const capped =
|
|
4013
|
-
maxFieldsShown !== undefined && candidateRows.length > maxFieldsShown;
|
|
4014
|
-
const shownRows =
|
|
4015
|
-
capped ? candidateRows.slice(0, maxFieldsShown) : candidateRows;
|
|
4016
|
-
const shownRowSet = new Set(shownRows.map((entry) => entry.name));
|
|
4017
|
-
// Everything the box holds that is not on show. Once the
|
|
4018
|
-
// reader asks for more they get the whole box — withholding
|
|
4019
|
-
// the non-preselected remainder behind a second control is
|
|
4020
|
-
// the same defect one level down.
|
|
4021
|
-
const deferredRowSet = new Set(
|
|
4022
|
-
orderedRows
|
|
4023
|
-
.filter((entry) => !shownRowSet.has(entry.name))
|
|
4024
|
-
.map((entry) => entry.name)
|
|
4025
|
-
);
|
|
4026
|
-
const deferredCount = deferredRowSet.size;
|
|
4027
|
-
const shownGroups = groupsInBox.filter((groupName) =>
|
|
4028
|
-
(buckets[box.key][groupName] || []).some((entry) =>
|
|
4029
|
-
shownRowSet.has(entry.name)
|
|
4030
|
-
)
|
|
4031
|
-
);
|
|
4032
|
-
const deferredGroups = groupsInBox.filter((groupName) =>
|
|
4033
|
-
(buckets[box.key][groupName] || []).some(
|
|
4034
|
-
(entry) => !shownRowSet.has(entry.name)
|
|
4035
|
-
)
|
|
4036
|
-
);
|
|
4037
|
-
const moreOpen = Boolean(query) || expandedMoreBoxes.includes(box.key);
|
|
4038
|
-
const accent =
|
|
4039
|
-
box.key === 'attention' ? cWarning
|
|
4040
|
-
: box.key === 'set' ? cSuccess
|
|
4041
|
-
: cMuted;
|
|
4042
|
-
// The muted "Optional" box reads as a quieter, recessed
|
|
4043
|
-
// surface — a touch darker than the page rather than the
|
|
4044
|
-
// faint grey tint the accent would give.
|
|
4045
|
-
const boxBg =
|
|
4046
|
-
box.key === 'optional' ?
|
|
4047
|
-
changeDarkness(getMainBackgroundColor(theme), 0.06)
|
|
4048
|
-
: undefined;
|
|
4049
|
-
return (
|
|
4050
|
-
<StyledStatusBox
|
|
4051
|
-
$accent={accent}
|
|
4052
|
-
$bg={boxBg}
|
|
4053
|
-
// Only a box that PINS needs an opaque header (see
|
|
4054
|
-
// StyledStatusBox); passing it unconditionally would
|
|
4055
|
-
// repaint every form's boxes for nothing.
|
|
4056
|
-
$surface={stickyBoxHeaders ? getMainBackgroundColor(theme) : undefined}
|
|
4057
|
-
key={box.key}
|
|
4058
|
-
ref={(node: HTMLDivElement | null) => {
|
|
4059
|
-
statusBoxRefs.current[box.key] = node;
|
|
4060
|
-
}}
|
|
4061
|
-
// Opening arms the reveal; collapsing disarms it, so a
|
|
4062
|
-
// close never scrolls.
|
|
4063
|
-
onCollapseChange={(collapsed) =>
|
|
4064
|
-
setOpenedStatusBox(collapsed ? undefined : box.key)
|
|
4065
|
-
}
|
|
4066
|
-
flat
|
|
4067
|
-
minimal
|
|
4068
|
-
/* The header stays on screen while its own box is
|
|
4069
|
-
being read — see `compactStickyBoxHeaders`. Sticky
|
|
4070
|
-
is bounded by its containing block, so the header
|
|
4071
|
-
leaves with its box and only one is ever pinned. */
|
|
4072
|
-
stickyHeader={stickyBoxHeaders}
|
|
4073
|
-
stickyHeaderOffset={stickyBoxHeaders ? boxStickyOffset : undefined}
|
|
4074
|
-
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
4075
|
-
collapsible
|
|
4076
|
-
// Which boxes start closed is the consumer's call
|
|
4077
|
-
// (`compactCollapsedGroups`, default `['optional']`: the
|
|
4078
|
-
// Optional box holds every not-yet-added field, so a form
|
|
4079
|
-
// opens on what is in use). Two rules override it:
|
|
4080
|
-
//
|
|
4081
|
-
// A SEARCH forces every box open — ReqorePanel unmounts
|
|
4082
|
-
// collapsed content, so a match inside a closed box would
|
|
4083
|
-
// be unreachable. (isCollapsed is the panel's controllable
|
|
4084
|
-
// state; manual toggling still works with no query set.)
|
|
4085
|
-
//
|
|
4086
|
-
// The Optional box stays open when it is the ONLY box,
|
|
4087
|
-
// because collapsing it renders a card that looks empty and
|
|
4088
|
-
// broken: an auth profile's Authorization block, whose nine
|
|
4089
|
-
// requirement fields are all optional, showed as a titled
|
|
4090
|
-
// card containing one collapsed "Optional 9" strip and
|
|
4091
|
-
// nothing else. This does not generalise to the other two —
|
|
4092
|
-
// a lone Set box collapsed is a deliberate ask, and its
|
|
4093
|
-
// header still reports what is in there.
|
|
4094
|
-
//
|
|
4095
|
-
// A PRESELECTED row overrides it too: the schema asked
|
|
4096
|
-
// for that field to be visible on a new form, so the
|
|
4097
|
-
// box it lands in opens. The groups it does NOT appear
|
|
4098
|
-
// in stay behind an inner collapse below, so opening
|
|
4099
|
-
// the box reveals the preselected fields without also
|
|
4100
|
-
// dumping every not-yet-added optional field on the
|
|
4101
|
-
// author.
|
|
4102
|
-
isCollapsed={wouldCollapse && !hasPreselectedRows}
|
|
4103
|
-
label={
|
|
4104
|
-
<StyledGroupHeader>
|
|
4105
|
-
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
4106
|
-
{box.label}
|
|
4107
|
-
</ReqoreP>
|
|
4108
|
-
<ReqoreTag
|
|
4109
|
-
size='small'
|
|
4110
|
-
minimal
|
|
4111
|
-
compact
|
|
4112
|
-
intent={box.intent}
|
|
4113
|
-
label={String(count)}
|
|
4114
|
-
/>
|
|
4115
|
-
</StyledGroupHeader>
|
|
4116
|
-
}
|
|
4117
|
-
icon={box.icon}
|
|
4118
|
-
className='options-readfirst-group'
|
|
4119
|
-
padded={false}
|
|
4120
|
-
contentStyle={{ padding: '4px 4px 6px' }}
|
|
4121
|
-
>
|
|
4122
|
-
{/* ONE group body per box: every field block (and the
|
|
4123
|
-
thin schema sub-labels) is a direct flex child, so the
|
|
4124
|
-
inter-field gap is identical everywhere — including
|
|
4125
|
-
across schema-group boundaries. */}
|
|
4126
|
-
<StyledGroupBody
|
|
4127
|
-
$divider={cDivider}
|
|
4128
|
-
$hover={cHover}
|
|
4129
|
-
$focus={cWarning}
|
|
4130
|
-
$success={cSuccess}
|
|
4131
|
-
$rowBg={cRowBg}
|
|
4132
|
-
$lineColor={cGroupLine}
|
|
4133
|
-
$moreBorder={cMoreBorder}
|
|
4134
|
-
className={compactNarrow ? 'readfirst-narrow' : undefined}
|
|
4135
|
-
>
|
|
4136
|
-
{shownGroups.map((groupName) =>
|
|
4137
|
-
renderBoxGroup(box.key, groupName, shownRowSet)
|
|
4138
|
-
)}
|
|
4139
|
-
{/* The control lives INSIDE the group body, as the last
|
|
4140
|
-
row, so it inherits the rows' grid, padding and
|
|
4141
|
-
hover and lines up with them. Outside it was a bare
|
|
4142
|
-
line of text in the box's corner, which is what the
|
|
4143
|
-
build #154 review rejected. */}
|
|
4144
|
-
{deferredCount ?
|
|
4145
|
-
<button
|
|
4146
|
-
type='button'
|
|
4147
|
-
className='readfirst-row readfirst-more-row options-readfirst-more'
|
|
4148
|
-
aria-expanded={moreOpen}
|
|
4149
|
-
onClick={() =>
|
|
4150
|
-
setExpandedMoreBoxes((prev) =>
|
|
4151
|
-
prev.includes(box.key) ?
|
|
4152
|
-
prev.filter((k) => k !== box.key)
|
|
4153
|
-
: [...prev, box.key]
|
|
4154
|
-
)
|
|
4155
|
-
}
|
|
4156
|
-
>
|
|
4157
|
-
<ReqoreP size='small' effect={{ opacity: 0.8 }}>
|
|
4158
|
-
{moreOpen ?
|
|
4159
|
-
'Show fewer'
|
|
4160
|
-
: `${deferredCount} more ${box.key === 'optional' ? 'optional ' : ''}field${deferredCount === 1 ? '' : 's'}`
|
|
4161
|
-
}
|
|
4162
|
-
</ReqoreP>
|
|
4163
|
-
<ReqoreIcon
|
|
4164
|
-
icon='ArrowDownSLine'
|
|
4165
|
-
size='small'
|
|
4166
|
-
className='readfirst-more-chevron'
|
|
4167
|
-
effect={{ opacity: 0.8 }}
|
|
4168
|
-
/>
|
|
4169
|
-
</button>
|
|
4170
|
-
: null}
|
|
4171
|
-
{/* A search forces the fold open for the same reason
|
|
4172
|
-
the boxes open: an unmounted row cannot be found. */}
|
|
4173
|
-
{deferredCount && moreOpen ?
|
|
4174
|
-
deferredGroups.map((groupName) =>
|
|
4175
|
-
renderBoxGroup(box.key, groupName, deferredRowSet)
|
|
4176
|
-
)
|
|
4177
|
-
: null}
|
|
4178
|
-
</StyledGroupBody>
|
|
4179
|
-
</StyledStatusBox>
|
|
4180
|
-
);
|
|
4181
|
-
})
|
|
4182
|
-
}
|
|
4183
|
-
</StyledCompactPanel>
|
|
4184
|
-
|
|
4185
|
-
{/* Batched commit: the Save/Discard bar docks bottom-right, floating
|
|
4186
|
-
over the rows while anything is staged — the product's draft
|
|
4187
|
-
convention (edits are a draft until explicitly applied). Save is
|
|
4188
|
-
gated on overall validity; Discard is the existing revert-all. */}
|
|
4189
|
-
{commitMode === 'batched' && !readOnly && dirtyOptionNames.length ?
|
|
4190
|
-
<StyledCommitDock $bg={cBg} $border={cDivider}>
|
|
4191
|
-
<ReqoreControlGroup
|
|
4192
|
-
className='options-readfirst-commitbar'
|
|
4193
|
-
verticalAlign='center'
|
|
4194
|
-
wrap
|
|
4195
|
-
>
|
|
4196
|
-
<ReqoreTag
|
|
4197
|
-
size='tiny'
|
|
4198
|
-
minimal
|
|
4199
|
-
flat
|
|
4200
|
-
compact
|
|
4201
|
-
effect={{ uppercase: true, spaced: 1 }}
|
|
4202
|
-
intent='warning'
|
|
4203
|
-
icon='EditLine'
|
|
4204
|
-
label={`${dirtyOptionNames.length} unsaved change${dirtyOptionNames.length === 1 ? '' : 's'}`}
|
|
4205
|
-
/>
|
|
4206
|
-
<ReqoreButton
|
|
4207
|
-
size='small'
|
|
4208
|
-
intent='success'
|
|
4209
|
-
icon='CheckLine'
|
|
4210
|
-
fixed
|
|
4211
|
-
className='options-readfirst-save'
|
|
4212
|
-
disabled={!validityData.isValid}
|
|
4213
|
-
tooltip={
|
|
4214
|
-
validityData.isValid ?
|
|
4215
|
-
'Apply the staged changes'
|
|
4216
|
-
: 'Resolve the invalid fields before saving'
|
|
4217
|
-
}
|
|
4218
|
-
onClick={handleCommitClick}
|
|
4219
|
-
>
|
|
4220
|
-
Save
|
|
4221
|
-
</ReqoreButton>
|
|
4222
|
-
<ReqoreButton
|
|
4223
|
-
size='small'
|
|
4224
|
-
minimal
|
|
4225
|
-
flat
|
|
4226
|
-
fixed
|
|
4227
|
-
icon='HistoryLine'
|
|
4228
|
-
className='options-readfirst-discard'
|
|
4229
|
-
onClick={handleRevertChangesClick}
|
|
4230
|
-
>
|
|
4231
|
-
Discard
|
|
4232
|
-
</ReqoreButton>
|
|
4233
|
-
</ReqoreControlGroup>
|
|
4234
|
-
</StyledCommitDock>
|
|
4235
|
-
: null}
|
|
4236
|
-
</StyledCompactWrap>
|
|
4237
|
-
</CompactToolbarContext.Provider>
|
|
4238
|
-
</ReqoreErrorBoundary>
|
|
4239
|
-
</CompactRowContext.Provider>
|
|
4240
|
-
</OptionsContext.Provider>
|
|
4241
|
-
);
|
|
4242
|
-
};
|
|
4243
|
-
|
|
4244
|
-
/* Which of the seven waits is holding the form, in the order they are
|
|
4245
|
-
tested. Rendered as `data-wait` on the placeholder so a page can be asked
|
|
4246
|
-
what it is waiting FOR instead of only that it is waiting. */
|
|
4247
|
-
const waitReason =
|
|
4248
|
-
rest.skeleton ? (rest.skeletonReason ? `host:${rest.skeletonReason}` : 'host')
|
|
4249
|
-
: templates.loading ? 'templates'
|
|
4250
|
-
: typesLoading ? 'types'
|
|
4251
|
-
: optionsLoading ? 'options'
|
|
4252
|
-
: loading ? 'fetch'
|
|
4253
|
-
: operatorsUrl && !operators ? 'operators'
|
|
4254
|
-
: (url || customUrl) && !options ? 'schema'
|
|
4255
|
-
: undefined;
|
|
4256
|
-
|
|
4257
|
-
if (waitReason) {
|
|
4258
|
-
/* The shape of the FORM, not a generic block arrangement. This used to be
|
|
4259
|
-
three bars over three big panels, which resembles nothing this gate is
|
|
4260
|
-
waiting for and shared no vocabulary with the other waits on the same
|
|
4261
|
-
page — see `FormFieldsSkeleton`. */
|
|
4262
|
-
return <FormFieldsSkeleton fill className='options-loading-skeleton' reason={waitReason} />;
|
|
4263
|
-
}
|
|
4264
|
-
|
|
4265
|
-
// A loader that rejected (and produced no usable schema) surfaces its error
|
|
4266
|
-
// instead of the generic "No options available" empty state.
|
|
4267
|
-
if (optionsError && (!options || !size(options))) {
|
|
4268
|
-
return (
|
|
4269
|
-
<ReqoreMessage intent='danger' opaque={false}>
|
|
4270
|
-
{optionsError}
|
|
4271
|
-
</ReqoreMessage>
|
|
4272
|
-
);
|
|
4273
|
-
}
|
|
4274
|
-
|
|
4275
|
-
if (!options || !size(options)) {
|
|
4276
|
-
return (
|
|
4277
|
-
<ReqoreMessage intent='warning' opaque={false}>
|
|
4278
|
-
No options available
|
|
4279
|
-
</ReqoreMessage>
|
|
4280
|
-
);
|
|
4281
|
-
}
|
|
4282
|
-
|
|
4283
|
-
if (compact) {
|
|
4284
|
-
return renderCompact();
|
|
4285
|
-
}
|
|
4286
|
-
|
|
4287
|
-
return (
|
|
4288
|
-
<OptionsContext.Provider value={{ schema: options, value: availableOptions }}>
|
|
4289
|
-
<ReqoreErrorBoundary>
|
|
4290
|
-
{showHelpForOption && (
|
|
4291
|
-
<OptionsHelpDialog
|
|
4292
|
-
onClose={() => setShowHelpForOption(undefined)}
|
|
4293
|
-
option={options[showHelpForOption]}
|
|
4294
|
-
/>
|
|
4295
|
-
)}
|
|
4296
|
-
{recordRequiresSearchOptions && !readOnly ?
|
|
4297
|
-
<>
|
|
4298
|
-
<ReqoreMessage intent='info'>
|
|
4299
|
-
This provider record requires some search options to be set. You can set them below.
|
|
4300
|
-
</ReqoreMessage>
|
|
4301
|
-
<ReqoreVerticalSpacer height={10} />
|
|
4302
|
-
</>
|
|
4303
|
-
: null}
|
|
4304
|
-
<ReqoreCollection
|
|
4305
|
-
minColumnWidth='350px'
|
|
4306
|
-
flat
|
|
4307
|
-
padded={false}
|
|
4308
|
-
minimal
|
|
4309
|
-
{...rest}
|
|
4310
|
-
label={compact ? undefined : rest.label}
|
|
4311
|
-
responsiveTitle={false}
|
|
4312
|
-
inputProps={{
|
|
4313
|
-
disabled: size(availableOptions) < 2,
|
|
4314
|
-
}}
|
|
4315
|
-
labelSize={4}
|
|
4316
|
-
filterable={'filterable' in rest ? rest.filterable : !compact}
|
|
4317
|
-
sortable={'sortable' in rest ? rest.sortable : !compact && size(availableOptions) > 1}
|
|
4318
|
-
defaultSortBy={null}
|
|
4319
|
-
contentRenderer={(children) => (
|
|
4320
|
-
<>
|
|
4321
|
-
{size(validityData.invalidFields) && !readOnly ?
|
|
4322
|
-
<ReqoreMessage
|
|
4323
|
-
intent={showInvalidOptionsOnly ? 'info' : 'danger'}
|
|
4324
|
-
opaque={false}
|
|
4325
|
-
size='small'
|
|
4326
|
-
margin='bottom'
|
|
4327
|
-
onClick={() => setShowInvalidOptionsOnly(!showInvalidOptionsOnly)}
|
|
4328
|
-
>
|
|
4329
|
-
{showInvalidOptionsOnly ?
|
|
4330
|
-
'Showing invalid fields only. Click here again to show all fields.'
|
|
4331
|
-
: `${size(validityData.invalidFields) < 2 ? 'A field is not valid and requires' : `${size(validityData.invalidFields)} fields are not valid and require`} attention. Click here to only show invalid fields.`
|
|
4332
|
-
}
|
|
4333
|
-
</ReqoreMessage>
|
|
4334
|
-
: null}
|
|
4335
|
-
{unavailableOptionsCount.current ?
|
|
4336
|
-
<>
|
|
4337
|
-
<ReqoreMessage intent='warning' opaque={false} size='small' margin='bottom'>
|
|
4338
|
-
{`${unavailableOptionsCount.current} fields(s) hidden because they are not supported on the current instance`}
|
|
4339
|
-
</ReqoreMessage>
|
|
4340
|
-
<ReqoreVerticalSpacer height={10} />
|
|
4341
|
-
</>
|
|
4342
|
-
: null}
|
|
4343
|
-
{children}
|
|
4344
|
-
{size(filteredOptions) >= 1 && !readOnly ?
|
|
4345
|
-
<>
|
|
4346
|
-
<ReqoreVerticalSpacer height={10} />
|
|
4347
|
-
<SelectFormField
|
|
4348
|
-
items={optionalFields}
|
|
4349
|
-
onChange={(val) => handleAddOptionalFieldChange('options', val)}
|
|
4350
|
-
placeholder={placeholder || 'More Options Available'}
|
|
4351
|
-
minimal
|
|
4352
|
-
icon='ListView'
|
|
4353
|
-
showDescription={false}
|
|
4354
|
-
fluid
|
|
4355
|
-
size={rest.size}
|
|
4356
|
-
/>
|
|
4357
|
-
</>
|
|
4358
|
-
: null}
|
|
4359
|
-
</>
|
|
4360
|
-
)}
|
|
4361
|
-
sortButtonProps={{ minimal: true }}
|
|
4362
|
-
showLayoutSwitch={false}
|
|
4363
|
-
style={{ width: '100%' }}
|
|
4364
|
-
items={map(
|
|
4365
|
-
shownOptions,
|
|
4366
|
-
({ type, ...other }, optionName): IReqoreCollectionItemProps => ({
|
|
4367
|
-
label: options[optionName]?.display_name || optionName,
|
|
4368
|
-
tags: (options[optionName] as any)?.tags,
|
|
4369
|
-
labelEffect: {
|
|
4370
|
-
textSize: '11px',
|
|
4371
|
-
uppercase: true,
|
|
4372
|
-
opacity: 0.9,
|
|
4373
|
-
},
|
|
4374
|
-
labelProps: {
|
|
4375
|
-
style:
|
|
4376
|
-
options[optionName]?.short_desc ?
|
|
4377
|
-
{
|
|
4378
|
-
cursor: 'help',
|
|
4379
|
-
}
|
|
4380
|
-
: undefined,
|
|
4381
|
-
onClick:
|
|
4382
|
-
options[optionName]?.desc ? () => handleOptionLabelClick(optionName) : undefined,
|
|
4383
|
-
},
|
|
4384
|
-
showLabelTooltip: !!options[optionName]?.short_desc,
|
|
4385
|
-
customLabelTooltip: {
|
|
4386
|
-
delay: 300,
|
|
4387
|
-
content: (
|
|
4388
|
-
<Description
|
|
4389
|
-
longDescription={`${options[optionName]?.short_desc}${options[optionName]?.desc ? '. Click to learn more.' : ''}`}
|
|
4390
|
-
margin='none'
|
|
4391
|
-
/>
|
|
4392
|
-
),
|
|
4393
|
-
},
|
|
4394
|
-
description:
|
|
4395
|
-
showFieldTypes ?
|
|
4396
|
-
`<${options[optionName].ui_type || options[optionName].type}${options[optionName]?.ui_element_type ? `[${options[optionName].ui_element_type}]` : ''}> ${options[optionName].short_desc || ''}`
|
|
4397
|
-
: options[optionName].short_desc || '',
|
|
4398
|
-
descriptionEffect: {
|
|
4399
|
-
textSize: '11px',
|
|
4400
|
-
opacity: 0.3,
|
|
4401
|
-
underline: options[optionName]?.desc ? 'underline dotted #878787' : undefined,
|
|
4402
|
-
},
|
|
4403
|
-
customTheme: {
|
|
4404
|
-
main: 'main:darken:1',
|
|
4405
|
-
},
|
|
4406
|
-
flat: false,
|
|
4407
|
-
icon:
|
|
4408
|
-
!isOptionValid(optionName, type as TQorusType, (other as any).value) ? 'SpamFill'
|
|
4409
|
-
: undefined,
|
|
4410
|
-
iconColor:
|
|
4411
|
-
!isOptionValid(optionName, type as TQorusType, (other as any).value) ?
|
|
4412
|
-
'danger:lighten:5'
|
|
4413
|
-
: undefined,
|
|
4414
|
-
transparent: false,
|
|
4415
|
-
intent: getIntent(
|
|
4416
|
-
optionName,
|
|
4417
|
-
type as TQorusType,
|
|
4418
|
-
(other as any).value,
|
|
4419
|
-
(other as any).op
|
|
4420
|
-
),
|
|
4421
|
-
badge: buildBadges(options[optionName], optionName),
|
|
4422
|
-
className: 'system-option',
|
|
4423
|
-
// Ported from IDE Options (commit 8e6b7781): full-width layout
|
|
4424
|
-
// for stretchable option types.
|
|
4425
|
-
stretch:
|
|
4426
|
-
STRECHABLE_TYPES.has(options[optionName].type as TQorusType) ||
|
|
4427
|
-
(options[optionName] as { stretch?: boolean }).stretch,
|
|
4428
|
-
size: 'small',
|
|
4429
|
-
/* No floating bar. It is portalled over whatever sits above the
|
|
4430
|
-
row — on the first row, the form's own header buttons, which it
|
|
4431
|
-
made unclickable — and a bar that only appears on a hover is out
|
|
4432
|
-
of reach on a phone. The row's actions render in its own
|
|
4433
|
-
header: injected ones as buttons (all of them in the menu on
|
|
4434
|
-
touch or a narrow viewport), the row's own secondary ones in
|
|
4435
|
-
its ⋯ menu. */
|
|
4436
|
-
actions: optionRowActions({
|
|
4437
|
-
injected: resolveOptionActions(optionActions, {
|
|
4438
|
-
name: optionName,
|
|
4439
|
-
schema: options[optionName],
|
|
4440
|
-
value: availableOptions?.[optionName] as TOption,
|
|
4441
|
-
}).filter((action) => action.show !== false),
|
|
4442
|
-
collapse: collapseOptionActions,
|
|
4443
|
-
menu: [
|
|
4444
|
-
!readOnly &&
|
|
4445
|
-
type !== 'code-editor' &&
|
|
4446
|
-
((options[optionName] as any)?.ui_type || options[optionName]?.type) !==
|
|
4447
|
-
'code-editor' && {
|
|
4448
|
-
label: 'Focused editing',
|
|
4449
|
-
icon: 'FullscreenLine',
|
|
4450
|
-
className: 'options-item-fullscreen',
|
|
4451
|
-
onClick: () => setFocusedEditing(optionName),
|
|
4452
|
-
},
|
|
4453
|
-
!readOnly &&
|
|
4454
|
-
hasOptionChanged((other as any).value, optionName) && {
|
|
4455
|
-
label: 'Revert changes',
|
|
4456
|
-
icon: 'HistoryLine',
|
|
4457
|
-
className: 'options-item-revert',
|
|
4458
|
-
onClick: () => {
|
|
4459
|
-
handleValueChange(
|
|
4460
|
-
optionName,
|
|
4461
|
-
originalValue.current?.[optionName]?.value,
|
|
4462
|
-
originalValue.current?.[optionName]?.type
|
|
4463
|
-
);
|
|
4464
|
-
},
|
|
4465
|
-
},
|
|
4466
|
-
!readOnly &&
|
|
4467
|
-
(other as any).value &&
|
|
4468
|
-
!isEqual((other as any).value, getDefaultValue(options[optionName])) &&
|
|
4469
|
-
!(options[optionName]?.disabled || (options[optionName] as any)?.readonly) && {
|
|
4470
|
-
label: 'Remove value',
|
|
4471
|
-
icon: 'CloseLine',
|
|
4472
|
-
className: 'options-item-remove',
|
|
4473
|
-
onClick: () => handleValueChange(optionName, undefined),
|
|
4474
|
-
},
|
|
4475
|
-
!readOnly &&
|
|
4476
|
-
!options[optionName]?.preselected &&
|
|
4477
|
-
!options[optionName]?.required && {
|
|
4478
|
-
label: 'Remove option',
|
|
4479
|
-
icon: 'DeleteBinLine',
|
|
4480
|
-
intent: 'danger',
|
|
4481
|
-
className: 'options-optional-remove',
|
|
4482
|
-
onClick: () => {
|
|
4483
|
-
confirmAction({
|
|
4484
|
-
title: 'Remove Selected Option',
|
|
4485
|
-
onConfirm: () => {
|
|
4486
|
-
removeSelectedOption(optionName);
|
|
4487
|
-
},
|
|
4488
|
-
});
|
|
4489
|
-
},
|
|
4490
|
-
},
|
|
4491
|
-
],
|
|
4492
|
-
}),
|
|
4493
|
-
content: (
|
|
4494
|
-
<FocusedEditing
|
|
4495
|
-
isFullscreen={focusedEditing === optionName}
|
|
4496
|
-
onClose={() => setFocusedEditing(undefined)}
|
|
4497
|
-
description={options[optionName]?.display_name}
|
|
4498
|
-
intent={getIntent(
|
|
4499
|
-
optionName,
|
|
4500
|
-
type as TQorusType,
|
|
4501
|
-
(other as any).value,
|
|
4502
|
-
(other as any).op
|
|
4503
|
-
)}
|
|
4504
|
-
>
|
|
4505
|
-
{focusedEditing === optionName ?
|
|
4506
|
-
<Description
|
|
4507
|
-
longDescription={options[optionName]?.desc}
|
|
4508
|
-
shortDescription={options[optionName]?.short_desc}
|
|
4509
|
-
longDescriptionShownByDefault
|
|
4510
|
-
/>
|
|
4511
|
-
: null}
|
|
4512
|
-
{renderOption(optionName, { type, ...other } as IQorusFormField)}
|
|
4513
|
-
</FocusedEditing>
|
|
4514
|
-
),
|
|
4515
|
-
})
|
|
4516
|
-
)}
|
|
4517
|
-
defaultZoom={1}
|
|
4518
|
-
zoomable={false}
|
|
4519
|
-
actions={[
|
|
4520
|
-
...(rest.actions || []).map((action) => ({
|
|
4521
|
-
...action,
|
|
4522
|
-
minimal: true,
|
|
4523
|
-
})),
|
|
4524
|
-
{
|
|
4525
|
-
icon: 'HistoryLine',
|
|
4526
|
-
className: 'fields-revert',
|
|
4527
|
-
tooltip: 'Revert Changes',
|
|
4528
|
-
minimal: true,
|
|
4529
|
-
position: 'right',
|
|
4530
|
-
show:
|
|
4531
|
-
showTypeToggle &&
|
|
4532
|
-
!readOnly &&
|
|
4533
|
-
!!originalValue.current &&
|
|
4534
|
-
!isEqual(localValue.fields, originalValue.current),
|
|
4535
|
-
onClick: handleRevertChangesClick,
|
|
4536
|
-
},
|
|
4537
|
-
{
|
|
4538
|
-
icon: 'CodeLine',
|
|
4539
|
-
minimal: true,
|
|
4540
|
-
className: 'fields-show-types',
|
|
4541
|
-
tooltip: showFieldTypes ? 'Hide field types' : 'Show field types',
|
|
4542
|
-
intent: showFieldTypes ? 'info' : undefined,
|
|
4543
|
-
show: showTypeToggle,
|
|
4544
|
-
position: 'right',
|
|
4545
|
-
onClick: handleShowFieldTypesClick,
|
|
4546
|
-
},
|
|
4547
|
-
]}
|
|
4548
|
-
/>
|
|
4549
|
-
</ReqoreErrorBoundary>
|
|
4550
|
-
</OptionsContext.Provider>
|
|
4551
|
-
);
|
|
4552
|
-
};
|
|
4553
|
-
|
|
4554
|
-
/**
|
|
4555
|
-
* Publishes the host's markdown renderer to every description this form draws.
|
|
4556
|
-
*
|
|
4557
|
-
* A provider rather than prop-drilling because the descriptions are not in one
|
|
4558
|
-
* place: the inline row description, the focused-editing header and the field
|
|
4559
|
-
* help dialog are three components at three depths, and the dialog is not even
|
|
4560
|
-
* a child of the row it belongs to. Threading a renderer to each of them by
|
|
4561
|
-
* hand is how one of them gets missed.
|
|
4562
|
-
*/
|
|
4563
|
-
export const FormEngine = (props: IFormEngineProps) => (
|
|
4564
|
-
<MarkdownRendererContext.Provider value={props.markdownRenderer}>
|
|
4565
|
-
<FormEngineImpl {...props} />
|
|
4566
|
-
</MarkdownRendererContext.Provider>
|
|
4567
|
-
);
|
|
4568
|
-
|
|
4569
|
-
export default FormEngine;
|