@qoretechnologies/reqraft 0.10.54 → 0.10.55-pr.115.g37abdfc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +25 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
- package/README.md +1 -1
- package/design/FORM_ENGINE_COMPACT.md +34 -0
- package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
- package/design/FORM_ENGINE_FIELD_MIGRATION.md +155 -1
- package/design/IDE_INTEGRATION.md +13 -0
- package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
- package/design/SELECT_DIALOG_FLOOR.md +86 -0
- package/design/SMART_EDITOR_UX.md +21 -0
- package/design/UNAVAILABLE_ALLOWED_VALUES.md +226 -0
- package/dist/components/composer/Composer.d.ts +8 -3
- package/dist/components/composer/Composer.d.ts.map +1 -1
- package/dist/components/composer/Composer.js.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
- package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
- package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
- package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlTags.js +5 -1
- package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
- package/dist/components/dpqlEditor/index.d.ts +4 -0
- package/dist/components/dpqlEditor/index.d.ts.map +1 -1
- package/dist/components/dpqlEditor/index.js +5 -1
- package/dist/components/dpqlEditor/index.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +42 -2
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/dpqlEditor/types.js +0 -4
- package/dist/components/dpqlEditor/types.js.map +1 -1
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
- package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
- package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
- package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +414 -128
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +5 -2
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +272 -19
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +666 -161
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js +41 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
- package/dist/components/form/engine/OptionFieldMessages.d.ts +108 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +203 -13
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +68 -5
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +15 -1
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +55 -29
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/optionActions.d.ts +37 -1
- package/dist/components/form/engine/optionActions.d.ts.map +1 -1
- package/dist/components/form/engine/optionActions.js +71 -1
- package/dist/components/form/engine/optionActions.js.map +1 -1
- package/dist/components/form/engine/optionsContext.d.ts +22 -0
- package/dist/components/form/engine/optionsContext.d.ts.map +1 -0
- package/dist/components/form/engine/optionsContext.js +20 -0
- package/dist/components/form/engine/optionsContext.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +42 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +76 -8
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rendererTypes.d.ts +42 -3
- package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
- package/dist/components/form/engine/rendererTypes.js +3 -1
- package/dist/components/form/engine/rendererTypes.js.map +1 -1
- package/dist/components/form/engine/rowFocus.d.ts +30 -0
- package/dist/components/form/engine/rowFocus.d.ts.map +1 -0
- package/dist/components/form/engine/rowFocus.js +93 -0
- package/dist/components/form/engine/rowFocus.js.map +1 -0
- package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
- package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
- package/dist/components/form/engine/rowMenuContext.js +103 -0
- package/dist/components/form/engine/rowMenuContext.js.map +1 -0
- package/dist/components/form/engine/rowOpenPicker.d.ts +27 -0
- package/dist/components/form/engine/rowOpenPicker.d.ts.map +1 -0
- package/dist/components/form/engine/rowOpenPicker.js +30 -0
- package/dist/components/form/engine/rowOpenPicker.js.map +1 -0
- package/dist/components/form/engine/typeChoices.d.ts +42 -0
- package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
- package/dist/components/form/engine/typeChoices.js +33 -0
- package/dist/components/form/engine/typeChoices.js.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.js +53 -0
- package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
- package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +203 -45
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/argumentPresence.d.ts +84 -0
- package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
- package/dist/components/form/expressions/argumentPresence.js +217 -0
- package/dist/components/form/expressions/argumentPresence.js.map +1 -0
- package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
- package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts +15 -2
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +21 -65
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
- package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +8 -4
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +95 -19
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/types.d.ts +7 -0
- package/dist/components/form/expressions/types.d.ts.map +1 -1
- package/dist/components/form/expressions/useExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/useExpressions.js +5 -2
- package/dist/components/form/expressions/useExpressions.js.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +50 -126
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts +18 -0
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +54 -8
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +52 -8
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts +7 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +91 -28
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/date/Date.d.ts +1 -1
- package/dist/components/form/fields/date/Date.d.ts.map +1 -1
- package/dist/components/form/fields/date/Date.js +16 -2
- package/dist/components/form/fields/date/Date.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +14 -2
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js +50 -4
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +47 -2
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +153 -12
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/select/Select.d.ts.map +1 -1
- package/dist/components/form/fields/select/Select.js +120 -20
- package/dist/components/form/fields/select/Select.js.map +1 -1
- package/dist/components/form/fields/select/SelectCollection.d.ts +166 -9
- package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
- package/dist/components/form/fields/select/SelectCollection.js +200 -7
- package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts +34 -2
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +606 -58
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/TemplateText.d.ts +36 -0
- package/dist/components/form/fields/template/TemplateText.d.ts.map +1 -0
- package/dist/components/form/fields/template/TemplateText.js +80 -0
- package/dist/components/form/fields/template/TemplateText.js.map +1 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +4 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
- package/dist/components/smartEditor/SmartEditor.js +29 -8
- package/dist/components/smartEditor/SmartEditor.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +5 -1
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +16 -2
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.d.ts +8 -4
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +7 -3
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/dpqlDetection.d.ts +58 -0
- package/dist/helpers/dpqlDetection.d.ts.map +1 -0
- package/dist/helpers/dpqlDetection.js +164 -0
- package/dist/helpers/dpqlDetection.js.map +1 -0
- package/dist/helpers/optionUiTypes.d.ts +31 -3
- package/dist/helpers/optionUiTypes.d.ts.map +1 -1
- package/dist/helpers/optionUiTypes.js +36 -1
- package/dist/helpers/optionUiTypes.js.map +1 -1
- package/dist/helpers/options.d.ts +57 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +97 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/templateItems.d.ts +47 -0
- package/dist/helpers/templateItems.d.ts.map +1 -0
- package/dist/helpers/templateItems.js +88 -0
- package/dist/helpers/templateItems.js.map +1 -0
- package/dist/helpers/templateText.d.ts +80 -0
- package/dist/helpers/templateText.d.ts.map +1 -0
- package/dist/helpers/templateText.js +291 -0
- package/dist/helpers/templateText.js.map +1 -0
- package/dist/helpers/templates.d.ts +30 -12
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +91 -33
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +17 -8
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +117 -22
- package/dist/helpers/validations.js.map +1 -1
- package/dist/hooks/useArgSchema.d.ts +2 -1
- package/dist/hooks/useArgSchema.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.d.ts +3 -1
- package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.js +47 -9
- package/dist/hooks/useFetch/useFetch.js.map +1 -1
- package/dist/hooks/useTemplates.d.ts +20 -0
- package/dist/hooks/useTemplates.d.ts.map +1 -1
- package/dist/hooks/useTemplates.js +34 -2
- package/dist/hooks/useTemplates.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/fetch.d.ts +151 -1
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +256 -44
- package/dist/utils/fetch.js.map +1 -1
- package/dist/utils/lspClient.d.ts +6 -0
- package/dist/utils/lspClient.d.ts.map +1 -1
- package/dist/utils/lspClient.js +62 -11
- package/dist/utils/lspClient.js.map +1 -1
- package/dist/utils/websocket.d.ts +6 -0
- package/dist/utils/websocket.d.ts.map +1 -1
- package/dist/utils/websocket.js +15 -7
- package/dist/utils/websocket.js.map +1 -1
- package/package.json +8 -4
- package/src/components/composer/Composer.tsx +7 -2
- package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
- package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
- package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
- package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
- package/src/components/dpqlEditor/dpqlTags.ts +13 -0
- package/src/components/dpqlEditor/index.ts +4 -0
- package/src/components/dpqlEditor/types.ts +38 -2
- package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
- package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
- package/src/components/form/engine/CompactRow.tsx +487 -160
- package/src/components/form/engine/CompactToolbar.tsx +10 -3
- package/src/components/form/engine/FormEngine.stories.tsx +1294 -161
- package/src/components/form/engine/FormEngine.tsx +916 -213
- package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +331 -0
- package/src/components/form/engine/FormFieldsSkeleton.tsx +77 -0
- package/src/components/form/engine/OptionFieldMessages.tsx +300 -14
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +94 -3
- package/src/components/form/engine/compactRowContext.ts +15 -6
- package/src/components/form/engine/compactRowStyles.ts +56 -2
- package/src/components/form/engine/compactToolbarContext.ts +1 -1
- package/src/components/form/engine/optionActions.ts +89 -1
- package/src/components/form/engine/optionsContext.ts +24 -0
- package/src/components/form/engine/readFirst.ts +88 -8
- package/src/components/form/engine/rendererTypes.ts +48 -4
- package/src/components/form/engine/rowFocus.ts +101 -0
- package/src/components/form/engine/rowMenuContext.ts +159 -0
- package/src/components/form/engine/rowOpenPicker.ts +27 -0
- package/src/components/form/engine/typeChoices.ts +63 -0
- package/src/components/form/expressions/DpqlRendering.tsx +85 -0
- package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
- package/src/components/form/expressions/ExpressionField.tsx +269 -55
- package/src/components/form/expressions/argumentPresence.ts +213 -0
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +38 -21
- package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
- package/src/components/form/expressions/builder/index.tsx +39 -81
- package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
- package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
- package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
- package/src/components/form/expressions/mockExpressions.ts +8 -4
- package/src/components/form/expressions/renderExpressionToText.ts +112 -22
- package/src/components/form/expressions/types.ts +7 -0
- package/src/components/form/expressions/useExpressions.ts +5 -1
- package/src/components/form/expressions/useRenderExpression.ts +43 -127
- package/src/components/form/fields/Field.tsx +99 -20
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +106 -44
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +85 -18
- package/src/components/form/fields/auto/AutoFormField.tsx +97 -21
- package/src/components/form/fields/date/Date.tsx +17 -1
- package/src/components/form/fields/long-string/LongString.tsx +15 -3
- package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +65 -6
- package/src/components/form/fields/rich-text/RichText.tsx +248 -30
- package/src/components/form/fields/select/Select.stories.tsx +392 -0
- package/src/components/form/fields/select/Select.tsx +164 -27
- package/src/components/form/fields/select/SelectCollection.tsx +350 -6
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
- package/src/components/form/fields/template/TemplateField.stories.tsx +93 -14
- package/src/components/form/fields/template/TemplateField.tsx +825 -131
- package/src/components/form/fields/template/TemplateText.tsx +115 -0
- package/src/components/form/index.tsx +4 -0
- package/src/components/log/Log.stories.tsx +2 -1
- package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +3 -3
- package/src/components/smartEditor/SmartEditor.tsx +41 -9
- package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
- package/src/components/smartEditor/useLspSession.ts +21 -3
- package/src/components/supportTicket/TicketReplyBox.tsx +8 -4
- package/src/components/ticketThread/TicketThread.stories.tsx +1 -0
- package/src/helpers/common.ts +7 -3
- package/src/helpers/dpqlDetection.ts +209 -0
- package/src/helpers/optionUiTypes.ts +38 -0
- package/src/helpers/options.ts +96 -0
- package/src/helpers/templateItems.ts +81 -0
- package/src/helpers/templateText.ts +350 -0
- package/src/helpers/templates.ts +94 -41
- package/src/helpers/validations.ts +138 -39
- package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
- package/src/hooks/useFetch/useFetch.tsx +53 -7
- package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
- package/src/hooks/useTemplates.ts +41 -0
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
- package/src/index.tsx +14 -1
- package/src/stores/currentUser/currentUser.stories.tsx +2 -1
- package/src/stories/storyNetwork.ts +92 -0
- package/src/utils/fetch.ts +311 -21
- package/src/utils/lspClient.ts +68 -11
- package/src/utils/websocket.ts +20 -9
- package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts +0 -73
- package/dist/components/form/engine/__fixtures__/basicFormOptions.d.ts.map +0 -1
- package/dist/components/form/engine/__fixtures__/basicFormOptions.js +0 -317
- package/dist/components/form/engine/__fixtures__/basicFormOptions.js.map +0 -1
- package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts +0 -41
- package/dist/components/form/engine/__fixtures__/fieldChromeOptions.d.ts.map +0 -1
- package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js +0 -45
- package/dist/components/form/engine/__fixtures__/fieldChromeOptions.js.map +0 -1
- package/dist/components/form/expressions/dpqlMockLsp.d.ts +0 -3
- package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +0 -1
- package/dist/components/form/expressions/dpqlMockLsp.js +0 -91
- package/dist/components/form/expressions/dpqlMockLsp.js.map +0 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +0 -2
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +0 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +0 -9
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +0 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +0 -55
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +0 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js +0 -126
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +0 -1
- package/dist/stories/Data/fileFieldArgSchema.json +0 -86
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ReqoreButton,
|
|
3
|
+
ReqoreInput,
|
|
4
|
+
ReqoreP,
|
|
5
|
+
ReqoreTag,
|
|
6
|
+
ReqoreTagGroup,
|
|
7
|
+
ReqoreVerticalSpacer,
|
|
8
|
+
} from '@qoretechnologies/reqore';
|
|
2
9
|
import { TSizes } from '@qoretechnologies/reqore/dist/constants/sizes';
|
|
3
10
|
import { IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
|
|
4
11
|
import { Meta, StoryObj } from '@storybook/react-vite';
|
|
5
|
-
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
|
|
12
|
+
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
13
|
import { expect, fireEvent, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
7
14
|
import { validateField } from '../../../helpers/validations';
|
|
8
|
-
import { defaultQorusTypes } from '../../../hooks/useQorusTypes';
|
|
9
15
|
import {
|
|
10
16
|
_testsChangeRichText,
|
|
11
17
|
_testsChangeStringField,
|
|
@@ -19,7 +25,6 @@ import {
|
|
|
19
25
|
_testsWaitForTextToNotExist,
|
|
20
26
|
sleep,
|
|
21
27
|
} from '../../../stories/Tests/utils';
|
|
22
|
-
import { startDpqlMockLsp } from '../expressions/dpqlMockLsp';
|
|
23
28
|
import { mockExpressions } from '../expressions/mockExpressions';
|
|
24
29
|
import { mockPopulatedDefinition } from '../fields/schema-definition/mockDefinition';
|
|
25
30
|
import { defaultMarkdownRenderer } from '../fields/markdown/MarkdownView';
|
|
@@ -92,29 +97,9 @@ const meta: Meta<typeof FormEngine> = {
|
|
|
92
97
|
chromatic: {
|
|
93
98
|
viewports: [2560],
|
|
94
99
|
},
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// request fires anyway — which made `Option With Any Type` pass locally (401 → built-in
|
|
99
|
-
// list, so "Boolean" exists) and fail in CI, where the token is valid and the server's
|
|
100
|
-
// list decides the labels. Pin the catalogue so the type names the plays click are ours.
|
|
101
|
-
mockData: [
|
|
102
|
-
{
|
|
103
|
-
// `query()` builds `${instance}api/latest/${url}`, and the hook's url is
|
|
104
|
-
// `/system/qorus-type-info` — hence the doubled slash. Both spellings are listed
|
|
105
|
-
// so the mock keeps matching if that leading slash is ever dropped.
|
|
106
|
-
url: 'https://hq.qoretechnologies.com:8092/api/latest//system/qorus-type-info',
|
|
107
|
-
method: 'GET',
|
|
108
|
-
status: 200,
|
|
109
|
-
response: defaultQorusTypes,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
url: 'https://hq.qoretechnologies.com:8092/api/latest/system/qorus-type-info',
|
|
113
|
-
method: 'GET',
|
|
114
|
-
status: 200,
|
|
115
|
-
response: defaultQorusTypes,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
100
|
+
// The type catalogue the plays click through (`Option With Any Type` picks
|
|
101
|
+
// "Boolean") is pinned for every story by the global mocks in
|
|
102
|
+
// `src/stories/storyNetwork.ts`.
|
|
118
103
|
},
|
|
119
104
|
render: ({ value, onChange, ...rest }: IFormEngineProps) => {
|
|
120
105
|
const [val, setValue] = useState(value);
|
|
@@ -191,11 +176,17 @@ export const Basic: Story = {
|
|
|
191
176
|
value: basicFormValue,
|
|
192
177
|
},
|
|
193
178
|
play: async ({ canvasElement, args }) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await waitFor(
|
|
197
|
-
|
|
198
|
-
|
|
179
|
+
// The template option's `$local:test` reads as the name the form's
|
|
180
|
+
// templates give it, as a chip in its editor.
|
|
181
|
+
await waitFor(
|
|
182
|
+
() =>
|
|
183
|
+
expect(
|
|
184
|
+
Array.from(canvasElement.querySelectorAll('[contenteditable="true"] .reqore-tag')).some((chip) =>
|
|
185
|
+
chip.textContent?.includes('Test (local)')
|
|
186
|
+
)
|
|
187
|
+
).toBe(true),
|
|
188
|
+
{ timeout: 10000 }
|
|
189
|
+
);
|
|
199
190
|
await waitFor(
|
|
200
191
|
() =>
|
|
201
192
|
expect(
|
|
@@ -299,19 +290,62 @@ export const OptionalOpened: Story = {
|
|
|
299
290
|
},
|
|
300
291
|
};
|
|
301
292
|
|
|
293
|
+
/** Opens one option row's ⋯ menu. */
|
|
294
|
+
const openOptionRowMenu = async (nth = 0) => {
|
|
295
|
+
await _testsClickButton({ selector: '.options-item-more', nth });
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* A row's own actions stay inside the row.
|
|
300
|
+
*
|
|
301
|
+
* They used to float: a bar portalled above the hovered row, which on the
|
|
302
|
+
* first row landed on the form's own header buttons and made them
|
|
303
|
+
* unclickable — and a bar that only appears on hover is out of reach on a
|
|
304
|
+
* phone. The row's secondary actions now live in its ⋯ menu.
|
|
305
|
+
*/
|
|
306
|
+
export const RowActionsStayInTheRow: Story = {
|
|
307
|
+
...Basic,
|
|
308
|
+
parameters: {
|
|
309
|
+
docs: {
|
|
310
|
+
description: {
|
|
311
|
+
story:
|
|
312
|
+
"Renders the Basic FormEngine, hovers its first option and opens that option's ⋯ menu. No action bar floats over the form's header when the row is hovered; the row's secondary actions — Focused editing among them — are listed in its menu, where they can be reached without a hover.",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
play: async (args) => {
|
|
317
|
+
await Basic.play!(args);
|
|
318
|
+
const firstRow = document.querySelectorAll<HTMLElement>('.system-option')[0];
|
|
319
|
+
// The row draws its own actions, in its own header, before anyone hovers
|
|
320
|
+
// it. A floating bar is only ever drawn elsewhere, and only on a hover.
|
|
321
|
+
const more = await waitFor(
|
|
322
|
+
() => {
|
|
323
|
+
const button = firstRow.querySelector<HTMLElement>('.options-item-more');
|
|
324
|
+
expect(button).toBeTruthy();
|
|
325
|
+
return button!;
|
|
326
|
+
},
|
|
327
|
+
{ timeout: 10000 }
|
|
328
|
+
);
|
|
329
|
+
await userEvent.hover(firstRow);
|
|
330
|
+
await expect(document.querySelector('.reqore-panel-floating-actions')).toBeNull();
|
|
331
|
+
await userEvent.click(more);
|
|
332
|
+
await _testsWaitForText('Focused editing');
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
302
336
|
export const FocusedEditing: Story = {
|
|
303
337
|
...Basic,
|
|
304
338
|
parameters: {
|
|
305
339
|
docs: {
|
|
306
340
|
description: {
|
|
307
341
|
story:
|
|
308
|
-
|
|
342
|
+
"Renders the Basic FormEngine, opens an option's ⋯ menu and picks Focused editing — the Focused Editing modal opens over that single field.",
|
|
309
343
|
},
|
|
310
344
|
},
|
|
311
345
|
},
|
|
312
346
|
play: async (args) => {
|
|
313
347
|
await Basic.play!(args);
|
|
314
|
-
await
|
|
348
|
+
await openOptionRowMenu(0);
|
|
315
349
|
await _testsClickButton({ selector: '.options-item-fullscreen', nth: 0 });
|
|
316
350
|
await _testsWaitForText('Focused Editing');
|
|
317
351
|
},
|
|
@@ -341,7 +375,7 @@ export const ValueCanBeRemoved: Story = {
|
|
|
341
375
|
docs: {
|
|
342
376
|
description: {
|
|
343
377
|
story:
|
|
344
|
-
|
|
378
|
+
"Renders FormEngine holding a text option and a file option, both with values. Picking Remove value from each row's ⋯ menu clears the value, and the row's menu then offers Revert changes.",
|
|
345
379
|
},
|
|
346
380
|
},
|
|
347
381
|
},
|
|
@@ -370,36 +404,61 @@ export const ValueCanBeRemoved: Story = {
|
|
|
370
404
|
},
|
|
371
405
|
play: async () => {
|
|
372
406
|
await _testsWaitForText('Click here to upload a different file');
|
|
373
|
-
|
|
374
|
-
await
|
|
375
|
-
timeout: 10000,
|
|
376
|
-
});
|
|
407
|
+
// The first row's value goes, and its menu now offers the way back.
|
|
408
|
+
await openOptionRowMenu(0);
|
|
377
409
|
await _testsClickButton({ selector: '.options-item-remove', nth: 0 });
|
|
410
|
+
await openOptionRowMenu(0);
|
|
378
411
|
await waitFor(() => expect(document.querySelectorAll('.options-item-revert').length).toBe(1), {
|
|
379
412
|
timeout: 10000,
|
|
380
413
|
});
|
|
381
|
-
await userEvent.
|
|
382
|
-
await waitFor(() => expect(document.
|
|
414
|
+
await userEvent.keyboard('{Escape}');
|
|
415
|
+
await waitFor(() => expect(document.querySelectorAll('.options-item-revert').length).toBe(0), {
|
|
383
416
|
timeout: 10000,
|
|
384
417
|
});
|
|
418
|
+
// Then the second row's.
|
|
419
|
+
await openOptionRowMenu(1);
|
|
385
420
|
await _testsClickButton({ selector: '.options-item-remove', nth: 0 });
|
|
386
421
|
await _testsWaitForTextToNotExist('Click here to upload a different file');
|
|
387
422
|
},
|
|
388
423
|
};
|
|
389
424
|
|
|
425
|
+
/**
|
|
426
|
+
* A phone reaches a row's actions through its ⋯ menu: there is no hover to
|
|
427
|
+
* reveal anything, and no bar floating over the row above.
|
|
428
|
+
*/
|
|
429
|
+
export const RowActionsOnAPhone: Story = {
|
|
430
|
+
...ValueCanBeRemoved,
|
|
431
|
+
parameters: {
|
|
432
|
+
qlip: { viewport: { width: 390, height: 844 } },
|
|
433
|
+
docs: {
|
|
434
|
+
description: {
|
|
435
|
+
story:
|
|
436
|
+
"Renders a text option and a file option, both with values, at phone width with the first option's ⋯ menu open — Focused editing and Remove value are listed there, reachable without a hover.",
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
play: async () => {
|
|
441
|
+
await _testsWaitForText('Click here to upload a different file');
|
|
442
|
+
await openOptionRowMenu(0);
|
|
443
|
+
await _testsWaitForText('Focused editing');
|
|
444
|
+
await _testsWaitForText('Remove value');
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
|
|
390
448
|
export const ChangeCanBeReverted: Story = {
|
|
391
449
|
...ValueCanBeRemoved,
|
|
392
450
|
parameters: {
|
|
393
451
|
docs: {
|
|
394
452
|
description: {
|
|
395
453
|
story:
|
|
396
|
-
|
|
454
|
+
"Renders the ValueCanBeRemoved fixture after both values are removed, then picks Revert changes from the file row's ⋯ menu — the file value comes back.",
|
|
397
455
|
},
|
|
398
456
|
},
|
|
399
457
|
},
|
|
400
458
|
play: async (args) => {
|
|
401
459
|
await ValueCanBeRemoved.play!(args);
|
|
402
|
-
await
|
|
460
|
+
await openOptionRowMenu(1);
|
|
461
|
+
await _testsClickButton({ selector: '.options-item-revert', nth: 0 });
|
|
403
462
|
await _testsWaitForText('Click here to upload a different file');
|
|
404
463
|
},
|
|
405
464
|
};
|
|
@@ -510,11 +569,11 @@ export const OptionDependsOnOptionOrAnotherOption: Story = {
|
|
|
510
569
|
});
|
|
511
570
|
|
|
512
571
|
await _testsWaitForText(
|
|
513
|
-
'This field is disabled because some dependencies are not fulfilled: "Required Option 2"
|
|
572
|
+
'This field is disabled because some dependencies are not fulfilled: ("Required Option 2" or "Required Option 5")'
|
|
514
573
|
);
|
|
515
574
|
await _testsChangeRichText('I have value', 5);
|
|
516
575
|
await _testsWaitForTextToNotExist(
|
|
517
|
-
'This field is disabled because some dependencies are not fulfilled: "Required Option 2"
|
|
576
|
+
'This field is disabled because some dependencies are not fulfilled: ("Required Option 2" or "Required Option 5")'
|
|
518
577
|
);
|
|
519
578
|
},
|
|
520
579
|
};
|
|
@@ -582,7 +641,7 @@ export const OptionWithAnyType: Story = {
|
|
|
582
641
|
docs: {
|
|
583
642
|
description: {
|
|
584
643
|
story:
|
|
585
|
-
'Renders four options typed as any with templates enabled — empty ones show a Select Template dropdown, the pre-typed number field renders as a Number input and the operator can switch types via the More menu.',
|
|
644
|
+
'Renders four options typed as any with templates enabled — empty ones show a Select Template dropdown, the pre-typed number field renders as a Number input and the operator can switch types via the More menu. That menu holds one group, "Set Custom Value", so it opens already expanded and the data types are one click away rather than two.',
|
|
586
645
|
},
|
|
587
646
|
},
|
|
588
647
|
},
|
|
@@ -644,9 +703,10 @@ export const OptionWithAnyType: Story = {
|
|
|
644
703
|
await _testsClickButton({ label: 'Richtext Template' });
|
|
645
704
|
|
|
646
705
|
// Open the ... menu on the 4th field (optionWithAnyTypeToChangeToTemplateToCustomData)
|
|
647
|
-
// and switch it to a specific custom type (Boolean).
|
|
706
|
+
// and switch it to a specific custom type (Boolean). The type rows are
|
|
707
|
+
// reachable straight from the menu: "Set Custom Value" is its only group,
|
|
708
|
+
// so that section opens itself rather than asking for a click first.
|
|
648
709
|
await _testsOpenTemplateMenu(4);
|
|
649
|
-
await _testsClickButton({ label: 'Set Custom Value' });
|
|
650
710
|
await _testsClickButton({ label: 'Boolean' });
|
|
651
711
|
},
|
|
652
712
|
};
|
|
@@ -997,6 +1057,154 @@ export const CompactRowCancelEdit: Story = {
|
|
|
997
1057
|
},
|
|
998
1058
|
};
|
|
999
1059
|
|
|
1060
|
+
export const CompactRowReadOnlyIsAWayIn: Story = {
|
|
1061
|
+
parameters: {
|
|
1062
|
+
/* Nothing to SEE here: what this story asserts is a role, a focus stop, a
|
|
1063
|
+
cursor and an accessible name — none of which a pixel capture records,
|
|
1064
|
+
so the frame is an ordinary read-only row either way. Rejected on qlip
|
|
1065
|
+
build #212 as "visually empty / not worth snapshotting"; the story stays
|
|
1066
|
+
because the behaviour it pins is the point. */
|
|
1067
|
+
qlip: { skip: true },
|
|
1068
|
+
docs: {
|
|
1069
|
+
description: {
|
|
1070
|
+
story:
|
|
1071
|
+
'A read-only row opens nothing by itself — it shows everything it has. Where the consumer says the field is editable somewhere else (`onReadOnlyActivate`), the row becomes the way in to that place and announces it: a button role, keyboard handling, a pointer cursor, and an accessible name that says what the click DOES ("Edit Cookie Name") rather than just naming the field. A reader clicking a value they want to change has said exactly what they want; answering with silence teaches them the surface is broken.',
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
args: {
|
|
1076
|
+
compact: true,
|
|
1077
|
+
readOnly: true,
|
|
1078
|
+
minColumnWidth: '360px',
|
|
1079
|
+
value: { cookie_name: { type: 'string', value: 'my-cookie' } },
|
|
1080
|
+
options: {
|
|
1081
|
+
cookie_name: {
|
|
1082
|
+
type: 'string',
|
|
1083
|
+
ui_type: 'string',
|
|
1084
|
+
display_name: 'Cookie Name',
|
|
1085
|
+
short_desc: 'Cookie name for cookie authentication',
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
onReadOnlyActivate: fn(),
|
|
1089
|
+
},
|
|
1090
|
+
play: async ({ canvasElement, args }) => {
|
|
1091
|
+
await waitFor(() =>
|
|
1092
|
+
expect(canvasElement.querySelector('[data-field="cookie_name"]')).toBeTruthy()
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
const row = canvasElement.querySelector<HTMLElement>('[data-field="cookie_name"]')!;
|
|
1096
|
+
// It reads as a control, not as text that happens to respond.
|
|
1097
|
+
expect(row.getAttribute('role')).toBe('button');
|
|
1098
|
+
expect(row.getAttribute('aria-label')).toBe('Edit Cookie Name');
|
|
1099
|
+
expect(row.getAttribute('tabindex')).toBe('0');
|
|
1100
|
+
|
|
1101
|
+
fireEvent.click(row);
|
|
1102
|
+
await waitFor(() => expect((args as any).onReadOnlyActivate).toHaveBeenCalledWith('cookie_name'));
|
|
1103
|
+
|
|
1104
|
+
// And it still does not open an editor in place — the point is that the
|
|
1105
|
+
// editing happens where the consumer sent it.
|
|
1106
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeNull();
|
|
1107
|
+
},
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
export const CompactRowReadOnlyStaysInertWithoutAWayIn: Story = {
|
|
1111
|
+
parameters: {
|
|
1112
|
+
/* Nothing to SEE here: what this story asserts is a role, a focus stop, a
|
|
1113
|
+
cursor and an accessible name — none of which a pixel capture records,
|
|
1114
|
+
so the frame is an ordinary read-only row either way. Rejected on qlip
|
|
1115
|
+
build #212 as "visually empty / not worth snapshotting"; the story stays
|
|
1116
|
+
because the behaviour it pins is the point. */
|
|
1117
|
+
qlip: { skip: true },
|
|
1118
|
+
docs: {
|
|
1119
|
+
description: {
|
|
1120
|
+
story:
|
|
1121
|
+
'The same read-only row with nowhere to send the reader. It stays inert: no button role, no focus stop, no cursor — because a control that does nothing is worse than plain text, and a read-only form with no editor behind it is a perfectly ordinary thing.',
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
args: {
|
|
1126
|
+
...(CompactRowReadOnlyIsAWayIn.args as any),
|
|
1127
|
+
onReadOnlyActivate: undefined,
|
|
1128
|
+
},
|
|
1129
|
+
play: async ({ canvasElement }) => {
|
|
1130
|
+
await waitFor(() =>
|
|
1131
|
+
expect(canvasElement.querySelector('[data-field="cookie_name"]')).toBeTruthy()
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
const row = canvasElement.querySelector<HTMLElement>('[data-field="cookie_name"]')!;
|
|
1135
|
+
expect(row.getAttribute('role')).toBeNull();
|
|
1136
|
+
expect(row.getAttribute('tabindex')).toBeNull();
|
|
1137
|
+
|
|
1138
|
+
fireEvent.click(row);
|
|
1139
|
+
// Nothing opens, and nothing is claimed to have happened.
|
|
1140
|
+
expect(canvasElement.querySelector('.readfirst-row-editing')).toBeNull();
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
export const CompactRowReleasesItsOpeningFloor: Story = {
|
|
1145
|
+
parameters: {
|
|
1146
|
+
docs: {
|
|
1147
|
+
description: {
|
|
1148
|
+
story:
|
|
1149
|
+
'Opening a row pins the height it had while being READ, so the click cannot yank the content below it up under the pointer. That floor is for the transition only: it is released the moment the author changes the value. Without the release, a row that was tall because it carried a warning kept that height after the author fixed the very thing the warning was about — reported as "the chip is removed, but the vertical size of the option remains the same", leaving a large empty box where the explanation had been.',
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
},
|
|
1153
|
+
args: {
|
|
1154
|
+
compact: true,
|
|
1155
|
+
minColumnWidth: '360px',
|
|
1156
|
+
value: { cookie_name: { type: 'string', value: 'my-cookie' } },
|
|
1157
|
+
options: {
|
|
1158
|
+
cookie_name: {
|
|
1159
|
+
type: 'string',
|
|
1160
|
+
ui_type: 'string',
|
|
1161
|
+
display_name: 'Cookie Name',
|
|
1162
|
+
short_desc: 'Cookie name for cookie authentication',
|
|
1163
|
+
// A message is what makes a read row tall enough for the stuck floor
|
|
1164
|
+
// to be visible rather than merely present.
|
|
1165
|
+
messages: [
|
|
1166
|
+
{
|
|
1167
|
+
intent: 'warning',
|
|
1168
|
+
title: 'This name is not one the browser will keep',
|
|
1169
|
+
content:
|
|
1170
|
+
'A cookie name may not contain a space, a comma or a semicolon. Rename it, and this notice goes away — and so should the space it took up.',
|
|
1171
|
+
},
|
|
1172
|
+
],
|
|
1173
|
+
},
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
play: async ({ canvasElement }) => {
|
|
1177
|
+
await waitFor(() =>
|
|
1178
|
+
expect(canvasElement.querySelector('[data-field="cookie_name"]')).toBeTruthy()
|
|
1179
|
+
);
|
|
1180
|
+
fireEvent.click(canvasElement.querySelector<HTMLElement>('[data-field="cookie_name"]')!);
|
|
1181
|
+
|
|
1182
|
+
await waitFor(() => expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy());
|
|
1183
|
+
|
|
1184
|
+
/* Opening pins the read height. Asserted on the inline style rather than
|
|
1185
|
+
on the measured height: jsdom lays nothing out, so a height comparison
|
|
1186
|
+
here would pass on any code at all. */
|
|
1187
|
+
const editing = canvasElement.querySelector<HTMLElement>('.readfirst-row-editing');
|
|
1188
|
+
expect(editing!.style.minHeight).not.toBe('');
|
|
1189
|
+
|
|
1190
|
+
const input = canvasElement.querySelector<HTMLInputElement>(
|
|
1191
|
+
'[data-field="cookie_name"] input, [data-field="cookie_name"] textarea'
|
|
1192
|
+
);
|
|
1193
|
+
expect(input).toBeTruthy();
|
|
1194
|
+
fireEvent.change(input!, { target: { value: 'changed-cookie' } });
|
|
1195
|
+
|
|
1196
|
+
// ...and the author changing the value releases it, so the row can settle
|
|
1197
|
+
// to whatever it now holds.
|
|
1198
|
+
await waitFor(
|
|
1199
|
+
() => {
|
|
1200
|
+
const still = canvasElement.querySelector<HTMLElement>('.readfirst-row-editing');
|
|
1201
|
+
expect(still?.style.minHeight ?? '').toBe('');
|
|
1202
|
+
},
|
|
1203
|
+
{ timeout: 5000 }
|
|
1204
|
+
);
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1000
1208
|
// Language and Source code are one decision — "what is this code, and in what
|
|
1001
1209
|
// language" — and the form used to ask it as two unrelated rows one above the
|
|
1002
1210
|
// other. `absorb_fields` lets the editor take the language into its own
|
|
@@ -1544,6 +1752,119 @@ export const NestedOptionInheritsRenderPropFromAncestorCompact: Story = {
|
|
|
1544
1752
|
},
|
|
1545
1753
|
};
|
|
1546
1754
|
|
|
1755
|
+
/**
|
|
1756
|
+
* Counts how often the inheritance bag it is handed becomes a DIFFERENT object
|
|
1757
|
+
* while the value in it never changes — the cost of a prop built inline.
|
|
1758
|
+
*/
|
|
1759
|
+
const InheritedScopeProbe = ({ inheritedFromParent }: any) => {
|
|
1760
|
+
const renders = useRef(0);
|
|
1761
|
+
const identities = useRef(0);
|
|
1762
|
+
const lastBag = useRef<unknown>(undefined);
|
|
1763
|
+
renders.current += 1;
|
|
1764
|
+
if (lastBag.current !== inheritedFromParent) {
|
|
1765
|
+
identities.current += 1;
|
|
1766
|
+
lastBag.current = inheritedFromParent;
|
|
1767
|
+
}
|
|
1768
|
+
return (
|
|
1769
|
+
<ReqoreTagGroup>
|
|
1770
|
+
<ReqoreTag labelKey='renders' label={`${renders.current}`} />
|
|
1771
|
+
<ReqoreTag
|
|
1772
|
+
labelKey='bag identities'
|
|
1773
|
+
label={`${identities.current}`}
|
|
1774
|
+
intent={identities.current > 1 ? 'danger' : 'success'}
|
|
1775
|
+
/>
|
|
1776
|
+
<ReqoreTag labelKey='language' label={`${inheritedFromParent?.language ?? 'none'}`} />
|
|
1777
|
+
</ReqoreTagGroup>
|
|
1778
|
+
);
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* Re-renders the form on demand, the way a host does on every keystroke
|
|
1783
|
+
* somewhere else on the page.
|
|
1784
|
+
*/
|
|
1785
|
+
const InheritedScopeHarness = (args: any) => {
|
|
1786
|
+
const [tick, setTick] = useState(0);
|
|
1787
|
+
return (
|
|
1788
|
+
<>
|
|
1789
|
+
<ReqoreButton id='rerender-the-form' onClick={() => setTick((current) => current + 1)}>
|
|
1790
|
+
Re-render the form ({tick})
|
|
1791
|
+
</ReqoreButton>
|
|
1792
|
+
<ReqoreVerticalSpacer height={10} />
|
|
1793
|
+
<FormEngine {...args} />
|
|
1794
|
+
</>
|
|
1795
|
+
);
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
// The bag a field forwards to any sub-form it hosts is a PROP, and it used to be
|
|
1799
|
+
// built inline — `{ ...inheritedFromParent, ...resolveInheritProps(...) }` — so
|
|
1800
|
+
// every field got a NEW object on every render of the form, including the
|
|
1801
|
+
// majority that declare no `inherit_props` at all and were handed
|
|
1802
|
+
// `{ ...(undefined ?? {}) }`. A memoised field re-renders on an unequal prop, so
|
|
1803
|
+
// this churn re-rendered every field of every form the engine draws, forever,
|
|
1804
|
+
// over a value that never changed (it surfaced as `Template field kind Updated
|
|
1805
|
+
// because: {inheritedFromParent}` repeating in the console). The probe below
|
|
1806
|
+
// counts the bag's identities: it must stay at 1 however often the form renders.
|
|
1807
|
+
export const CompactInheritedScopeKeepsItsIdentity: Story = {
|
|
1808
|
+
parameters: {
|
|
1809
|
+
docs: {
|
|
1810
|
+
description: {
|
|
1811
|
+
story:
|
|
1812
|
+
'Re-renders a form whose fields inherit a scope from their parent — the inheritance bag each field is handed stays the same object, so a field that receives it is not re-rendered over a value that never changed.',
|
|
1813
|
+
},
|
|
1814
|
+
},
|
|
1815
|
+
},
|
|
1816
|
+
args: {
|
|
1817
|
+
compact: true,
|
|
1818
|
+
minColumnWidth: '300px',
|
|
1819
|
+
name: 'inherited-scope',
|
|
1820
|
+
inheritedFromParent: { language: 'qore' },
|
|
1821
|
+
value: {
|
|
1822
|
+
kind: { type: 'string', value: 'service' },
|
|
1823
|
+
body: { type: 'string', value: 'sub my_method() {}' },
|
|
1824
|
+
},
|
|
1825
|
+
options: {
|
|
1826
|
+
kind: {
|
|
1827
|
+
type: 'string',
|
|
1828
|
+
ui_type: 'scope-probe',
|
|
1829
|
+
display_name: 'Kind',
|
|
1830
|
+
short_desc: 'Declares no inherit_props — it forwards the bag it was handed',
|
|
1831
|
+
},
|
|
1832
|
+
body: {
|
|
1833
|
+
type: 'string',
|
|
1834
|
+
ui_type: 'scope-probe',
|
|
1835
|
+
display_name: 'Body',
|
|
1836
|
+
short_desc: 'Declares inherit_props — its bag is a merge, made once',
|
|
1837
|
+
inherit_props: { subjectKind: 'kind' },
|
|
1838
|
+
},
|
|
1839
|
+
} as unknown as IOptionsSchema,
|
|
1840
|
+
componentOverrides: { 'scope-probe': InheritedScopeProbe },
|
|
1841
|
+
initialExpandedOptions: ['kind', 'body'],
|
|
1842
|
+
},
|
|
1843
|
+
render: (args) => <InheritedScopeHarness {...args} />,
|
|
1844
|
+
play: async ({ canvasElement }) => {
|
|
1845
|
+
const canvas = within(canvasElement);
|
|
1846
|
+
await waitFor(() => expect(canvas.getAllByText('renders').length).toBe(2), { timeout: 10000 });
|
|
1847
|
+
|
|
1848
|
+
const identityCounts = () =>
|
|
1849
|
+
Array.from(canvasElement.querySelectorAll('.reqore-tag'))
|
|
1850
|
+
.filter((tag) => (tag.textContent ?? '').startsWith('bag identities'))
|
|
1851
|
+
.map((tag) => (tag.textContent ?? '').replace('bag identities', ''));
|
|
1852
|
+
|
|
1853
|
+
const before = identityCounts();
|
|
1854
|
+
expect(before).toEqual(['1', '1']);
|
|
1855
|
+
|
|
1856
|
+
// Render the form again, three times over, with nothing about the fields
|
|
1857
|
+
// changed.
|
|
1858
|
+
await _testsClickButton({ selector: '#rerender-the-form' });
|
|
1859
|
+
await _testsClickButton({ selector: '#rerender-the-form' });
|
|
1860
|
+
await _testsClickButton({ selector: '#rerender-the-form' });
|
|
1861
|
+
await _testsWaitForText('Re-render the form (3)');
|
|
1862
|
+
|
|
1863
|
+
// Every field was handed the SAME bag each time.
|
|
1864
|
+
expect(identityCounts()).toEqual(['1', '1']);
|
|
1865
|
+
},
|
|
1866
|
+
};
|
|
1867
|
+
|
|
1547
1868
|
// The companion above deliberately stops at the collapsed summary, which is
|
|
1548
1869
|
// exactly where this bug hid: nothing expanded a list-of-hash row inside a
|
|
1549
1870
|
// compact form. `compact` is destructured out of AutoFormField's `...rest`, so
|
|
@@ -1750,12 +2071,15 @@ export const DependantsResetWhenParentChanges: Story = {
|
|
|
1750
2071
|
|
|
1751
2072
|
await sleep(500);
|
|
1752
2073
|
|
|
2074
|
+
// The engine clears a dependant to `undefined`. This read `''` while an
|
|
2075
|
+
// emptied text field reported '' on its own after the reset, which is not
|
|
2076
|
+
// the engine's value and marked untouched forms as changed.
|
|
1753
2077
|
await expect(args.onChange).toHaveBeenLastCalledWith(
|
|
1754
2078
|
'test',
|
|
1755
2079
|
expect.objectContaining({
|
|
1756
2080
|
optionWithDependents: { type: 'long-string', value: 'My value changed' },
|
|
1757
2081
|
anotherDependent: { type: 'long-string', value: 'My value will not change' },
|
|
1758
|
-
dependent1: { type: 'long-string', value:
|
|
2082
|
+
dependent1: { type: 'long-string', value: undefined },
|
|
1759
2083
|
dependent2: { type: 'hash', value: undefined },
|
|
1760
2084
|
}),
|
|
1761
2085
|
undefined
|
|
@@ -4364,7 +4688,7 @@ const FieldTypeCatalogGroups: Record<string, IFormEngineGroup> = {
|
|
|
4364
4688
|
* summary of the `{exp,args}` AST (`renderExpressionToText`); drilling in opens
|
|
4365
4689
|
* the full `ExpressionField` in the card — the Visual builder (catalogue offline
|
|
4366
4690
|
* via `mockExpressions`) plus the Text/DPQL editor, whose seed + Explain are
|
|
4367
|
-
* served by the
|
|
4691
|
+
* served by the story language server every story gets. No instance required.
|
|
4368
4692
|
*/
|
|
4369
4693
|
export const CompactExpressions: Story = {
|
|
4370
4694
|
// chromatic off: ends with the live ExpressionField editor (Text mode) open.
|
|
@@ -4404,65 +4728,61 @@ export const CompactExpressions: Story = {
|
|
|
4404
4728
|
} as unknown as IOptions,
|
|
4405
4729
|
},
|
|
4406
4730
|
play: async () => {
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
const chip = Array.from(document.querySelectorAll('.reqore-tag')).find((tag) =>
|
|
4414
|
-
tag.textContent?.includes('$local:name')
|
|
4415
|
-
);
|
|
4416
|
-
expect(chip, 'the reference in the summary renders as a chip').toBeTruthy();
|
|
4417
|
-
});
|
|
4418
|
-
await _testsWaitForText('" == "John"');
|
|
4419
|
-
|
|
4420
|
-
// Drill in → the card hosts the ExpressionField (Visual builder).
|
|
4421
|
-
await fireEvent.click(
|
|
4422
|
-
document.querySelector('.readfirst-row[data-field="condition"]') as HTMLElement
|
|
4423
|
-
);
|
|
4424
|
-
await waitFor(
|
|
4425
|
-
() =>
|
|
4426
|
-
expect(
|
|
4427
|
-
document.querySelector('.options-readfirst-card[data-field="condition"] .expression')
|
|
4428
|
-
).toBeInTheDocument(),
|
|
4429
|
-
{ timeout: 10000 }
|
|
4731
|
+
// Read-first: the summary of the AST reads as DPQL, through the same
|
|
4732
|
+
// rendering the Explain panel and Preview use — the template reference a
|
|
4733
|
+
// chip rather than its raw `$local:name` token, the literal as the text it is.
|
|
4734
|
+
await waitFor(() => {
|
|
4735
|
+
const summary = document.querySelector(
|
|
4736
|
+
'.readfirst-row[data-field="condition"] .dpql-rendering'
|
|
4430
4737
|
);
|
|
4431
|
-
|
|
4432
|
-
|
|
4738
|
+
expect(summary, 'the collapsed row renders its summary as DPQL').toBeTruthy();
|
|
4739
|
+
expect(summary?.textContent).toContain('local: name');
|
|
4740
|
+
expect(summary?.textContent).toContain('== "John"');
|
|
4741
|
+
});
|
|
4433
4742
|
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
()
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
).toBeInTheDocument(),
|
|
4448
|
-
{ timeout: 10000 }
|
|
4449
|
-
);
|
|
4743
|
+
// Drill in → the card hosts the ExpressionField (Visual builder).
|
|
4744
|
+
await fireEvent.click(
|
|
4745
|
+
document.querySelector('.readfirst-row[data-field="condition"]') as HTMLElement
|
|
4746
|
+
);
|
|
4747
|
+
await waitFor(
|
|
4748
|
+
() =>
|
|
4749
|
+
expect(
|
|
4750
|
+
document.querySelector('.options-readfirst-card[data-field="condition"] .expression')
|
|
4751
|
+
).toBeInTheDocument(),
|
|
4752
|
+
{ timeout: 10000 }
|
|
4753
|
+
);
|
|
4754
|
+
// The builder resolves the operator from the offline catalogue.
|
|
4755
|
+
await _testsWaitForText('Logical Equals');
|
|
4450
4756
|
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4757
|
+
// Text/DPQL tab — seeded from the AST via the mock LSP (dpql/serialize).
|
|
4758
|
+
const textBtn = Array.from(
|
|
4759
|
+
document.querySelectorAll(
|
|
4760
|
+
'.options-readfirst-card[data-field="condition"] .expression-field button'
|
|
4761
|
+
)
|
|
4762
|
+
).find((b) => b.textContent?.trim() === 'Text') as HTMLElement;
|
|
4763
|
+
await fireEvent.click(textBtn);
|
|
4764
|
+
await waitFor(
|
|
4765
|
+
() =>
|
|
4766
|
+
expect(
|
|
4767
|
+
document.querySelector(
|
|
4768
|
+
'.options-readfirst-card[data-field="condition"] [data-testid="expression-preview"]'
|
|
4769
|
+
)
|
|
4770
|
+
).toBeInTheDocument(),
|
|
4771
|
+
{ timeout: 10000 }
|
|
4772
|
+
);
|
|
4773
|
+
|
|
4774
|
+
// The "Parsed" line is the single live rendering of the AST (over the
|
|
4775
|
+
// mock dpql/renderExpression) — it reflects the seeded expression. The
|
|
4776
|
+
// separate Text-mode "Explain" button was dropped; Parsed is canonical.
|
|
4777
|
+
await waitFor(
|
|
4778
|
+
() =>
|
|
4779
|
+
expect(
|
|
4780
|
+
document.querySelector(
|
|
4781
|
+
'.options-readfirst-card[data-field="condition"] [data-testid="expression-preview"]'
|
|
4782
|
+
)?.textContent
|
|
4783
|
+
).toContain('John'),
|
|
4784
|
+
{ timeout: 10000 }
|
|
4785
|
+
);
|
|
4466
4786
|
},
|
|
4467
4787
|
};
|
|
4468
4788
|
|
|
@@ -5242,15 +5562,18 @@ export const CompactFieldTypes: Story = {
|
|
|
5242
5562
|
// than a raw hash key-count.
|
|
5243
5563
|
await _testsWaitForText('512MiB');
|
|
5244
5564
|
await _testsWaitForText(/example_customer_addresses/);
|
|
5245
|
-
// Expression field → read-first shows the offline DPQL summary of the AST
|
|
5246
|
-
// with the template reference inside it chipped rather than
|
|
5565
|
+
// Expression field → read-first shows the offline DPQL summary of the AST
|
|
5566
|
+
// as DPQL, with the template reference inside it chipped rather than
|
|
5567
|
+
// printed raw.
|
|
5247
5568
|
await waitFor(() => {
|
|
5248
|
-
const
|
|
5249
|
-
|
|
5569
|
+
const summary = document.querySelector('.readfirst-row[data-field="expr"] .dpql-rendering');
|
|
5570
|
+
const chip = Array.from(summary?.querySelectorAll('.reqore-tag') ?? []).find((tag) =>
|
|
5571
|
+
tag.textContent?.includes('name')
|
|
5250
5572
|
);
|
|
5251
5573
|
expect(chip, 'the reference in the summary renders as a chip').toBeTruthy();
|
|
5574
|
+
expect(summary?.textContent).toContain('== "John"');
|
|
5575
|
+
expect(summary?.textContent).not.toContain('$local:name');
|
|
5252
5576
|
});
|
|
5253
|
-
await _testsWaitForText('" == "John"');
|
|
5254
5577
|
await expect(document.querySelectorAll('.options-readfirst-card')).toHaveLength(0);
|
|
5255
5578
|
|
|
5256
5579
|
// A hash renders its read-first preview (the structured tree by default)
|
|
@@ -6572,15 +6895,13 @@ export const CompactAutoFocusTargetsInvalidFilledField: Story = {
|
|
|
6572
6895
|
},
|
|
6573
6896
|
};
|
|
6574
6897
|
|
|
6575
|
-
//
|
|
6576
|
-
//
|
|
6577
|
-
//
|
|
6578
|
-
// `enabled`
|
|
6579
|
-
//
|
|
6580
|
-
//
|
|
6581
|
-
//
|
|
6582
|
-
// nowhere. (This is the non-text-editor focus gap; kept as a playground rather
|
|
6583
|
-
// than a hard assertion until we decide how CompactRow should focus such rows.)
|
|
6898
|
+
// The first "needs attention" field is a BOOLEAN, whose compact editor is a
|
|
6899
|
+
// focusable icon (ReqoreCheckbox) — not an input/textarea/contenteditable. Both
|
|
6900
|
+
// `enabled` and `name` are required and unset, so both need attention;
|
|
6901
|
+
// `enabled` is first. The row's scan used to look for text controls alone, so
|
|
6902
|
+
// autofocus targeted the boolean, opened its row, and left the caret nowhere —
|
|
6903
|
+
// this story was the playground that documented the gap. The row now focuses
|
|
6904
|
+
// whatever control it offers, so the caret is ON the boolean.
|
|
6584
6905
|
const CompactNonFocusableFirstSchema: Record<string, TCompactField> = {
|
|
6585
6906
|
enabled: {
|
|
6586
6907
|
type: 'bool',
|
|
@@ -6609,7 +6930,7 @@ export const CompactAutoFocusNonFocusableFirstField: Story = {
|
|
|
6609
6930
|
docs: {
|
|
6610
6931
|
description: {
|
|
6611
6932
|
story:
|
|
6612
|
-
'Renders a compact form with autoFocus enabled where the first needs-attention field is a boolean
|
|
6933
|
+
'Renders a compact form with autoFocus enabled where the first needs-attention field is a boolean, whose editor is not a text box — the caret lands on the boolean itself rather than being left nowhere.',
|
|
6613
6934
|
},
|
|
6614
6935
|
},
|
|
6615
6936
|
},
|
|
@@ -6622,29 +6943,167 @@ export const CompactAutoFocusNonFocusableFirstField: Story = {
|
|
|
6622
6943
|
autoFocusFirstRequired: true,
|
|
6623
6944
|
},
|
|
6624
6945
|
play: async () => {
|
|
6625
|
-
// Smoke: both required-but-unset fields render (both land in "needs
|
|
6626
|
-
// attention"), with the boolean first. Focus behaviour is intentionally left
|
|
6627
|
-
// for manual observation — see the note above.
|
|
6628
6946
|
await _testsWaitForText('Enabled');
|
|
6629
6947
|
await _testsWaitForText('Name');
|
|
6948
|
+
|
|
6949
|
+
// The caret is inside the boolean's row — the field the engine targeted.
|
|
6950
|
+
await waitFor(
|
|
6951
|
+
() => {
|
|
6952
|
+
const active = document.activeElement as HTMLElement | null;
|
|
6953
|
+
expect(document.querySelector('[data-field="enabled"]')?.contains(active!)).toBe(true);
|
|
6954
|
+
},
|
|
6955
|
+
{ timeout: 10000 }
|
|
6956
|
+
);
|
|
6957
|
+
// On the control itself, which is not a text box: that is the whole case.
|
|
6958
|
+
const active = document.activeElement as HTMLElement;
|
|
6959
|
+
expect(['INPUT', 'TEXTAREA']).not.toContain(active.tagName);
|
|
6960
|
+
// It did not settle for the focusable text field further down instead.
|
|
6961
|
+
expect(document.querySelector('[data-field="name"]')?.contains(active)).toBe(false);
|
|
6630
6962
|
},
|
|
6631
6963
|
};
|
|
6632
6964
|
|
|
6633
|
-
//
|
|
6634
|
-
//
|
|
6635
|
-
//
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6965
|
+
// The first question most forms ask is "which kind?", and a pick-one is a set
|
|
6966
|
+
// of buttons: with ≤ 3 candidates the engine renders reqore checkboxes, which
|
|
6967
|
+
// are focusable icons rather than inputs. The engine opened this row and the
|
|
6968
|
+
// caret stayed on whatever opened the form.
|
|
6969
|
+
const CompactPickOneFirstSchema: Record<string, TCompactField> = {
|
|
6970
|
+
kind: {
|
|
6971
|
+
type: 'string',
|
|
6972
|
+
ui_type: 'string',
|
|
6973
|
+
display_name: 'Kind',
|
|
6974
|
+
short_desc: 'Which kind of interface is this? — the first required question',
|
|
6975
|
+
required: true,
|
|
6976
|
+
preselected: true,
|
|
6977
|
+
group: 'general',
|
|
6978
|
+
allowed_values: [
|
|
6979
|
+
{ display_name: 'Service', value: { type: 'string', value: 'service' } },
|
|
6980
|
+
{ display_name: 'Job', value: { type: 'string', value: 'job' } },
|
|
6981
|
+
{ display_name: 'Workflow', value: { type: 'string', value: 'workflow' } },
|
|
6982
|
+
],
|
|
6983
|
+
} as TCompactField,
|
|
6984
|
+
name: {
|
|
6985
|
+
type: 'string',
|
|
6986
|
+
ui_type: 'string',
|
|
6987
|
+
display_name: 'Name',
|
|
6988
|
+
short_desc: 'A text field — comes second',
|
|
6641
6989
|
required: true,
|
|
6990
|
+
preselected: true,
|
|
6991
|
+
group: 'general',
|
|
6642
6992
|
},
|
|
6643
6993
|
};
|
|
6644
6994
|
|
|
6645
|
-
const
|
|
6646
|
-
|
|
6647
|
-
|
|
6995
|
+
export const CompactAutoFocusPickOneFirstQuestion: Story = {
|
|
6996
|
+
parameters: {
|
|
6997
|
+
docs: {
|
|
6998
|
+
description: {
|
|
6999
|
+
story:
|
|
7000
|
+
'Renders a compact form with autoFocus enabled whose first required question is a pick-one — the caret lands on the first choice, so the answer can be given from the keyboard.',
|
|
7001
|
+
},
|
|
7002
|
+
},
|
|
7003
|
+
},
|
|
7004
|
+
args: {
|
|
7005
|
+
compact: true,
|
|
7006
|
+
minColumnWidth: '300px',
|
|
7007
|
+
options: CompactPickOneFirstSchema as IOptionsSchema,
|
|
7008
|
+
value: {},
|
|
7009
|
+
groups: CompactGroups,
|
|
7010
|
+
autoFocusFirstRequired: true,
|
|
7011
|
+
},
|
|
7012
|
+
play: async () => {
|
|
7013
|
+
await _testsWaitForText('Service');
|
|
7014
|
+
|
|
7015
|
+
await waitFor(
|
|
7016
|
+
() => {
|
|
7017
|
+
const active = document.activeElement as HTMLElement | null;
|
|
7018
|
+
expect(document.querySelector('[data-field="kind"]')?.contains(active!)).toBe(true);
|
|
7019
|
+
},
|
|
7020
|
+
{ timeout: 10000 }
|
|
7021
|
+
);
|
|
7022
|
+
const active = document.activeElement as HTMLElement;
|
|
7023
|
+
// The control the row actually offers: a choice, not a text box.
|
|
7024
|
+
expect(['INPUT', 'TEXTAREA']).not.toContain(active.tagName);
|
|
7025
|
+
expect(active.getAttribute('tabindex')).toBe('0');
|
|
7026
|
+
},
|
|
7027
|
+
};
|
|
7028
|
+
|
|
7029
|
+
/**
|
|
7030
|
+
* A host editor that cannot draw its control on the first frame — it resolves
|
|
7031
|
+
* its catalogue first, the way the IDE's connection and interface selectors do.
|
|
7032
|
+
* The row the engine opens is empty for the first commits, and the control is
|
|
7033
|
+
* the LAST thing to arrive.
|
|
7034
|
+
*/
|
|
7035
|
+
const LateMountingEditor = () => {
|
|
7036
|
+
const [ready, setReady] = useState(false);
|
|
7037
|
+
useEffect(() => {
|
|
7038
|
+
const timer = setTimeout(() => setReady(true), 400);
|
|
7039
|
+
return () => clearTimeout(timer);
|
|
7040
|
+
}, []);
|
|
7041
|
+
return ready ?
|
|
7042
|
+
<ReqoreButton className='late-editor-control'>Choose a connection…</ReqoreButton>
|
|
7043
|
+
: <ReqoreP effect={{ opacity: 0.5 }}>Loading connections…</ReqoreP>;
|
|
7044
|
+
};
|
|
7045
|
+
|
|
7046
|
+
const CompactLateEditorSchema = {
|
|
7047
|
+
connection: {
|
|
7048
|
+
type: 'string',
|
|
7049
|
+
// A host `ui_type`: the editor is injected, exactly as the IDE injects its
|
|
7050
|
+
// connection and interface selectors.
|
|
7051
|
+
ui_type: 'late-editor',
|
|
7052
|
+
display_name: 'Connection',
|
|
7053
|
+
short_desc: 'A host editor that resolves its catalogue before it can draw a control',
|
|
7054
|
+
required: true,
|
|
7055
|
+
},
|
|
7056
|
+
} as unknown as IOptionsSchema;
|
|
7057
|
+
|
|
7058
|
+
export const CompactAutoFocusLateMountingEditor: Story = {
|
|
7059
|
+
parameters: {
|
|
7060
|
+
docs: {
|
|
7061
|
+
description: {
|
|
7062
|
+
story:
|
|
7063
|
+
"Renders a compact form whose first required question is drawn by a host editor that mounts its control after the form has rendered — the caret follows the control when it arrives, instead of being spent on a row that was still empty.",
|
|
7064
|
+
},
|
|
7065
|
+
},
|
|
7066
|
+
},
|
|
7067
|
+
args: {
|
|
7068
|
+
compact: true,
|
|
7069
|
+
minColumnWidth: '300px',
|
|
7070
|
+
options: CompactLateEditorSchema,
|
|
7071
|
+
value: {},
|
|
7072
|
+
autoFocusFirstRequired: true,
|
|
7073
|
+
componentOverrides: { 'late-editor': LateMountingEditor },
|
|
7074
|
+
},
|
|
7075
|
+
render: (args) => <FormEngine {...(args as IFormEngineProps)} />,
|
|
7076
|
+
play: async () => {
|
|
7077
|
+
// Nothing to focus yet: the row is open and its editor is still loading.
|
|
7078
|
+
await _testsWaitForText('Loading connections…');
|
|
7079
|
+
expect(document.activeElement).toBe(document.body);
|
|
7080
|
+
|
|
7081
|
+
await _testsWaitForText('Choose a connection…');
|
|
7082
|
+
await waitFor(
|
|
7083
|
+
() => {
|
|
7084
|
+
const active = document.activeElement as HTMLElement | null;
|
|
7085
|
+
expect(active?.closest('.late-editor-control')).toBeTruthy();
|
|
7086
|
+
},
|
|
7087
|
+
{ timeout: 10000 }
|
|
7088
|
+
);
|
|
7089
|
+
},
|
|
7090
|
+
};
|
|
7091
|
+
|
|
7092
|
+
// A richtext value whose prose is interleaved with template chips — the shape an
|
|
7093
|
+
// alert rule's message body has. The read-first summary renders the chips inline
|
|
7094
|
+
// with the surrounding words, so the two have to sit on the same baseline.
|
|
7095
|
+
const RichtextTemplateSchema: IOptionsSchema = {
|
|
7096
|
+
body: {
|
|
7097
|
+
type: 'richtext',
|
|
7098
|
+
ui_type: 'richtext',
|
|
7099
|
+
display_name: 'Message Body',
|
|
7100
|
+
required: true,
|
|
7101
|
+
},
|
|
7102
|
+
};
|
|
7103
|
+
|
|
7104
|
+
const RichtextTemplateValue: IOptions = {
|
|
7105
|
+
body: {
|
|
7106
|
+
type: 'richtext',
|
|
6648
7107
|
value: [
|
|
6649
7108
|
{
|
|
6650
7109
|
type: 'paragraph',
|
|
@@ -7179,10 +7638,10 @@ export const CompactListOfHashReadsInSchemaWordsMobile: Story = {
|
|
|
7179
7638
|
};
|
|
7180
7639
|
|
|
7181
7640
|
/**
|
|
7182
|
-
* The read-first summary of an EXPRESSION: it
|
|
7183
|
-
*
|
|
7184
|
-
*
|
|
7185
|
-
*
|
|
7641
|
+
* The read-first summary of an EXPRESSION: it reads as DPQL, and a template
|
|
7642
|
+
* reference inside it used to print as its own raw token — the same reference
|
|
7643
|
+
* the editor shows as a named chip. This is the Save Reply row of the Discord
|
|
7644
|
+
* assistant template, whose value is
|
|
7186
7645
|
* `trim($data:{dc_ai_reply.choices[0].message.content})`.
|
|
7187
7646
|
*/
|
|
7188
7647
|
export const CompactExpressionTemplateChips: Story = {
|
|
@@ -7190,7 +7649,7 @@ export const CompactExpressionTemplateChips: Story = {
|
|
|
7190
7649
|
docs: {
|
|
7191
7650
|
description: {
|
|
7192
7651
|
story:
|
|
7193
|
-
'Renders a compact read-first row whose value is an expression wrapping a template reference — the
|
|
7652
|
+
'Renders a compact read-first row whose value is an expression wrapping a template reference — the summary reads as DPQL and the reference inside it is a chip named from the form\'s template catalogue, trim(Choices[0].message.content), rather than its raw $data token or its path.',
|
|
7194
7653
|
},
|
|
7195
7654
|
},
|
|
7196
7655
|
chromatic: { disable: true },
|
|
@@ -7241,20 +7700,20 @@ export const CompactExpressionTemplateChips: Story = {
|
|
|
7241
7700
|
onChange={fn()}
|
|
7242
7701
|
/>
|
|
7243
7702
|
),
|
|
7244
|
-
play: async () => {
|
|
7245
|
-
// The summary keeps its literal text…
|
|
7246
|
-
await _testsWaitForText('trim("', undefined, 1);
|
|
7247
|
-
// …and the reference inside it is a named chip, not the raw token.
|
|
7703
|
+
play: async ({ canvasElement }) => {
|
|
7248
7704
|
await waitFor(() => {
|
|
7249
|
-
const
|
|
7705
|
+
const summary = canvasElement.querySelector('.readfirst-row .dpql-rendering');
|
|
7706
|
+
// The summary keeps its literal text…
|
|
7707
|
+
expect(summary?.textContent, 'the row summarises the expression as DPQL').toContain('trim(');
|
|
7708
|
+
// …and the reference inside it is a chip named from the catalogue.
|
|
7709
|
+
const chip = Array.from(summary?.querySelectorAll('.reqore-tag') ?? []).find((tag) =>
|
|
7250
7710
|
tag.textContent?.includes('Choices[0].message.content')
|
|
7251
7711
|
);
|
|
7252
7712
|
expect(chip, 'the reference renders as a named chip in the summary').toBeTruthy();
|
|
7713
|
+
// Named, so neither the raw token nor the path it resolves.
|
|
7714
|
+
expect(summary?.textContent).not.toContain('$data:{');
|
|
7715
|
+
expect(summary?.textContent).not.toContain('dc_ai_reply');
|
|
7253
7716
|
});
|
|
7254
|
-
const raw = Array.from(document.querySelectorAll('.readfirst-row')).filter((row) =>
|
|
7255
|
-
row.textContent?.includes('$data:{')
|
|
7256
|
-
);
|
|
7257
|
-
expect(raw, 'the row prints no raw token').toHaveLength(0);
|
|
7258
7717
|
},
|
|
7259
7718
|
};
|
|
7260
7719
|
|
|
@@ -7269,7 +7728,7 @@ export const CompactExpressionWithoutCatalogue: Story = {
|
|
|
7269
7728
|
docs: {
|
|
7270
7729
|
description: {
|
|
7271
7730
|
story:
|
|
7272
|
-
'Renders a compact read-first row whose value is an expression wrapping a template reference, with no templates supplied — the reference still renders as a chip carrying its own path (dc_ai_reply.choices[0].message.content) rather than the raw $data token.',
|
|
7731
|
+
'Renders a compact read-first row whose value is an expression wrapping a template reference, with no templates supplied — the summary reads as DPQL and the reference still renders as a chip carrying its own path (data: dc_ai_reply.choices[0].message.content) rather than the raw $data token.',
|
|
7273
7732
|
},
|
|
7274
7733
|
},
|
|
7275
7734
|
chromatic: { disable: true },
|
|
@@ -7304,18 +7763,16 @@ export const CompactExpressionWithoutCatalogue: Story = {
|
|
|
7304
7763
|
onChange={fn()}
|
|
7305
7764
|
/>
|
|
7306
7765
|
),
|
|
7307
|
-
play: async () => {
|
|
7308
|
-
await _testsWaitForText('trim("', undefined, 1);
|
|
7766
|
+
play: async ({ canvasElement }) => {
|
|
7309
7767
|
await waitFor(() => {
|
|
7310
|
-
const
|
|
7768
|
+
const summary = canvasElement.querySelector('.readfirst-row .dpql-rendering');
|
|
7769
|
+
expect(summary?.textContent, 'the row summarises the expression as DPQL').toContain('trim(');
|
|
7770
|
+
const chip = Array.from(summary?.querySelectorAll('.reqore-tag') ?? []).find((tag) =>
|
|
7311
7771
|
tag.textContent?.includes('dc_ai_reply.choices[0].message.content')
|
|
7312
7772
|
);
|
|
7313
7773
|
expect(chip, 'the reference chips even with no catalogue').toBeTruthy();
|
|
7774
|
+
expect(summary?.textContent, 'the row prints no raw token').not.toContain('$data:{');
|
|
7314
7775
|
});
|
|
7315
|
-
const raw = Array.from(document.querySelectorAll('.readfirst-row')).filter((row) =>
|
|
7316
|
-
row.textContent?.includes('$data:{')
|
|
7317
|
-
);
|
|
7318
|
-
expect(raw, 'the row prints no raw token').toHaveLength(0);
|
|
7319
7776
|
},
|
|
7320
7777
|
};
|
|
7321
7778
|
|
|
@@ -7469,12 +7926,12 @@ export const CompactToolbarTrimmed: Story = {
|
|
|
7469
7926
|
},
|
|
7470
7927
|
};
|
|
7471
7928
|
|
|
7472
|
-
export const
|
|
7929
|
+
export const CompactLoneOptionalStaysOpenByDefault: Story = {
|
|
7473
7930
|
parameters: {
|
|
7474
7931
|
docs: {
|
|
7475
7932
|
description: {
|
|
7476
7933
|
story:
|
|
7477
|
-
|
|
7934
|
+
'Renders an all-optional compact form with NO `compactCollapsedGroups` — the Optional box is the whole form, so it stays open. Collapsing the only box would render a titled card containing one collapsed strip and nothing else, which reads as broken rather than as tidy. This is the default half of the contract; its pair below is what an explicit collapse list changes.',
|
|
7478
7935
|
},
|
|
7479
7936
|
},
|
|
7480
7937
|
},
|
|
@@ -7486,16 +7943,42 @@ export const CompactCollapseListLeavesLoneOptionalOpen: Story = {
|
|
|
7486
7943
|
beta: { type: 'string', ui_type: 'string', display_name: 'Beta' },
|
|
7487
7944
|
} as unknown as IOptionsSchema,
|
|
7488
7945
|
value: {} as IOptions,
|
|
7489
|
-
compactCollapsedGroups: ['optional'],
|
|
7490
7946
|
},
|
|
7491
7947
|
play: async () => {
|
|
7492
|
-
//
|
|
7948
|
+
// No collapse list was passed, so the invariant holds and the rows are on
|
|
7949
|
+
// screen with no click.
|
|
7493
7950
|
await expectBoxOpen('Optional');
|
|
7494
7951
|
await _testsWaitForText('Alpha');
|
|
7495
7952
|
await _testsWaitForText('Beta');
|
|
7496
7953
|
},
|
|
7497
7954
|
};
|
|
7498
7955
|
|
|
7956
|
+
export const CompactCollapseListCollapsesLoneOptional: Story = {
|
|
7957
|
+
parameters: {
|
|
7958
|
+
docs: {
|
|
7959
|
+
description: {
|
|
7960
|
+
story:
|
|
7961
|
+
"Renders the same all-optional form with an explicit `compactCollapsedGroups={['optional']}` — and this time the Optional box IS collapsed. Passing the prop waives the \"never collapse the sole box\" invariant: it is a default, not a law. A host that supplies its own heading and explanation around the form — a run-options panel headed \"Change a setting for this run\", say — has already told the reader what the card is, and has asked for the settings to start folded away. The default (the story above) is unchanged for every caller that says nothing.",
|
|
7962
|
+
},
|
|
7963
|
+
},
|
|
7964
|
+
},
|
|
7965
|
+
args: {
|
|
7966
|
+
compact: true,
|
|
7967
|
+
minColumnWidth: '300px',
|
|
7968
|
+
options: {
|
|
7969
|
+
alpha: { type: 'string', ui_type: 'string', display_name: 'Alpha' },
|
|
7970
|
+
beta: { type: 'string', ui_type: 'string', display_name: 'Beta' },
|
|
7971
|
+
} as unknown as IOptionsSchema,
|
|
7972
|
+
value: {} as IOptions,
|
|
7973
|
+
compactCollapsedGroups: ['optional'],
|
|
7974
|
+
},
|
|
7975
|
+
play: async () => {
|
|
7976
|
+
// The caller asked for it folded, so it is folded even though it is the
|
|
7977
|
+
// only box: `ReqorePanel` unmounts collapsed content, so the rows are gone.
|
|
7978
|
+
await expectBoxCollapsed('Optional');
|
|
7979
|
+
},
|
|
7980
|
+
};
|
|
7981
|
+
|
|
7499
7982
|
export const CompactNoSearchCollapseIsFinal: Story = {
|
|
7500
7983
|
parameters: {
|
|
7501
7984
|
docs: {
|
|
@@ -7831,3 +8314,653 @@ export const CompactNoMaxFieldsShown: Story = {
|
|
|
7831
8314
|
expect(canvasElement.querySelector('.readfirst-more-row')).toBeNull();
|
|
7832
8315
|
},
|
|
7833
8316
|
};
|
|
8317
|
+
|
|
8318
|
+
/**
|
|
8319
|
+
* A test's cases read as chips on the collapsed row.
|
|
8320
|
+
*
|
|
8321
|
+
* `test-cases` is a renderer-only `ui_type`, so it reached none of the typed
|
|
8322
|
+
* branches in `renderReadFirstValue` and fell through to the generic summary,
|
|
8323
|
+
* which flattens the array of case hashes to `"case_1, resolved_expected_values"`.
|
|
8324
|
+
* The table the row opens already draws each name as a chip, so the collapsed
|
|
8325
|
+
* row was the only surface still printing an identifier as prose.
|
|
8326
|
+
*
|
|
8327
|
+
* The fall-throughs are the point of the other two fields: chips are all-or-
|
|
8328
|
+
* nothing, because a partly-chipped row reads as though the unnamed cases were
|
|
8329
|
+
* missing entirely.
|
|
8330
|
+
*/
|
|
8331
|
+
export const CompactRowTestCaseChips: Story = {
|
|
8332
|
+
parameters: {
|
|
8333
|
+
docs: {
|
|
8334
|
+
description: {
|
|
8335
|
+
story:
|
|
8336
|
+
'Renders a collapsed `test-cases` row: every case name is a chip, matching the cases table the row opens. A value whose entries are not all named, and an empty value, keep the plain summary instead — chips are all-or-nothing.',
|
|
8337
|
+
},
|
|
8338
|
+
},
|
|
8339
|
+
},
|
|
8340
|
+
args: {
|
|
8341
|
+
compact: true,
|
|
8342
|
+
minColumnWidth: '360px',
|
|
8343
|
+
value: {
|
|
8344
|
+
cases: {
|
|
8345
|
+
type: 'list',
|
|
8346
|
+
value: [
|
|
8347
|
+
{ name: 'case_1', desc: 'the happy path' },
|
|
8348
|
+
{ name: 'resolved_expected_values', desc: 'reference resolution' },
|
|
8349
|
+
],
|
|
8350
|
+
},
|
|
8351
|
+
mixed: {
|
|
8352
|
+
type: 'list',
|
|
8353
|
+
value: [{ name: 'case_1' }, { desc: 'a case that has no name yet' }],
|
|
8354
|
+
},
|
|
8355
|
+
none: { type: 'list', value: [] },
|
|
8356
|
+
},
|
|
8357
|
+
// Cast for the same reason the markdown/cron/dpql stories need one: a
|
|
8358
|
+
// renderer-only `ui_type` names an EDITOR, and ts-toolkit's `TQorusType`
|
|
8359
|
+
// enumerates STORAGE types, so it has no member for one.
|
|
8360
|
+
options: {
|
|
8361
|
+
cases: { type: 'list', ui_type: 'test-cases', display_name: 'Cases' },
|
|
8362
|
+
mixed: { type: 'list', ui_type: 'test-cases', display_name: 'Mixed' },
|
|
8363
|
+
none: { type: 'list', ui_type: 'test-cases', display_name: 'None' },
|
|
8364
|
+
} as never,
|
|
8365
|
+
},
|
|
8366
|
+
play: async ({ canvasElement }) => {
|
|
8367
|
+
// (a) Every case name is a chip, in order.
|
|
8368
|
+
await waitFor(
|
|
8369
|
+
() => {
|
|
8370
|
+
const chips = canvasElement.querySelectorAll(
|
|
8371
|
+
'[data-field="cases"] .options-readfirst-case-tag'
|
|
8372
|
+
);
|
|
8373
|
+
expect(chips.length).toBe(2);
|
|
8374
|
+
expect(Array.from(chips).map((chip) => chip.textContent?.trim())).toEqual([
|
|
8375
|
+
'case_1',
|
|
8376
|
+
'resolved_expected_values',
|
|
8377
|
+
]);
|
|
8378
|
+
},
|
|
8379
|
+
{ timeout: 5000 }
|
|
8380
|
+
);
|
|
8381
|
+
|
|
8382
|
+
// (b) …and the comma-joined prose they replace is gone. Asserting the text
|
|
8383
|
+
// rather than only the chip count: the first cut rendered the chips
|
|
8384
|
+
// BESIDE the summary, which is the same identifiers twice.
|
|
8385
|
+
const casesText =
|
|
8386
|
+
canvasElement
|
|
8387
|
+
.querySelector('[data-field="cases"] .options-readfirst-valuetext')
|
|
8388
|
+
?.textContent?.trim() ?? '';
|
|
8389
|
+
expect(casesText).not.toContain('case_1,');
|
|
8390
|
+
|
|
8391
|
+
// (c) NEGATIVE: one entry has no name, so no entry is chipped — a
|
|
8392
|
+
// half-chipped row would read as though that case were missing.
|
|
8393
|
+
const mixedRow = canvasElement.querySelector('[data-field="mixed"]');
|
|
8394
|
+
expect(mixedRow?.querySelectorAll('.options-readfirst-case-tag').length).toBe(0);
|
|
8395
|
+
|
|
8396
|
+
// (d) NEGATIVE: an empty value is not a SET value, so it never reaches
|
|
8397
|
+
// this branch at all — its row sits in the collapsed optional group and
|
|
8398
|
+
// is not in the DOM. Counting canvas-wide says the same thing more
|
|
8399
|
+
// strongly than probing for a row that does not exist: the only chips
|
|
8400
|
+
// on the whole form are the two named cases, so neither the unnamed nor
|
|
8401
|
+
// the empty field contributed one.
|
|
8402
|
+
expect(canvasElement.querySelectorAll('.options-readfirst-case-tag').length).toBe(2);
|
|
8403
|
+
},
|
|
8404
|
+
};
|
|
8405
|
+
|
|
8406
|
+
/**
|
|
8407
|
+
* Clear-value is offered only when there is something to clear.
|
|
8408
|
+
*
|
|
8409
|
+
* `[]` is neither `undefined`, `null` nor `''`, so a list field whose last item
|
|
8410
|
+
* had just been removed went on showing the red clear button, and confirming it
|
|
8411
|
+
* cleared nothing. It is visible wherever a list field stays open — a test's
|
|
8412
|
+
* cases table sits above exactly such a button once its last case is deleted.
|
|
8413
|
+
*
|
|
8414
|
+
* Both fields here are lists with the same schema; only the value differs, so
|
|
8415
|
+
* the button's presence is decided by the value alone.
|
|
8416
|
+
*/
|
|
8417
|
+
export const ClearValueOnlyWhenThereIsAValue: Story = {
|
|
8418
|
+
parameters: {
|
|
8419
|
+
docs: {
|
|
8420
|
+
description: {
|
|
8421
|
+
story:
|
|
8422
|
+
'A list field with items offers Clear value; the same field with an empty list does not, because there is nothing to clear.',
|
|
8423
|
+
},
|
|
8424
|
+
},
|
|
8425
|
+
},
|
|
8426
|
+
args: {
|
|
8427
|
+
compact: true,
|
|
8428
|
+
minColumnWidth: '360px',
|
|
8429
|
+
/* Both rows OPEN, because that is where this button lives. A collapsed list
|
|
8430
|
+
row shows no clear at all — the inline one is only for editors with no
|
|
8431
|
+
clear of their own (bools, fixed-choice pickers) — so the button under
|
|
8432
|
+
test is the expanded card's, which is what a consumer that opens a field
|
|
8433
|
+
by default puts permanently on screen. */
|
|
8434
|
+
initialExpandedOptions: ['filled', 'emptied'],
|
|
8435
|
+
value: {
|
|
8436
|
+
filled: { type: 'list', value: ['alpha', 'beta'] },
|
|
8437
|
+
emptied: { type: 'list', value: [] },
|
|
8438
|
+
},
|
|
8439
|
+
options: {
|
|
8440
|
+
/* Both REQUIRED, like a test's `cases`. An unset optional field folds
|
|
8441
|
+
into the collapsed Optional box and is not in the DOM at all, so an
|
|
8442
|
+
empty list only stays on screen — where this button can be misclicked —
|
|
8443
|
+
when the field is one the form insists on. */
|
|
8444
|
+
filled: {
|
|
8445
|
+
type: 'list',
|
|
8446
|
+
ui_type: 'list',
|
|
8447
|
+
element_type: 'string',
|
|
8448
|
+
display_name: 'Filled',
|
|
8449
|
+
required: true,
|
|
8450
|
+
},
|
|
8451
|
+
emptied: {
|
|
8452
|
+
type: 'list',
|
|
8453
|
+
ui_type: 'list',
|
|
8454
|
+
element_type: 'string',
|
|
8455
|
+
display_name: 'Emptied',
|
|
8456
|
+
required: true,
|
|
8457
|
+
},
|
|
8458
|
+
} as never,
|
|
8459
|
+
},
|
|
8460
|
+
play: async ({ canvasElement }) => {
|
|
8461
|
+
await waitFor(
|
|
8462
|
+
() => {
|
|
8463
|
+
expect(canvasElement.querySelector('[data-field="filled"]')).toBeTruthy();
|
|
8464
|
+
},
|
|
8465
|
+
{ timeout: 5000 }
|
|
8466
|
+
);
|
|
8467
|
+
|
|
8468
|
+
// (a) items present -> the clear button is offered
|
|
8469
|
+
const filled = canvasElement.querySelector('[data-field="filled"]');
|
|
8470
|
+
expect(filled?.querySelectorAll('.options-readfirst-clear').length).toBe(1);
|
|
8471
|
+
|
|
8472
|
+
// (b) empty list -> nothing to clear, so no button and no dialog to reach
|
|
8473
|
+
const emptied = canvasElement.querySelector('[data-field="emptied"]');
|
|
8474
|
+
expect(emptied).toBeTruthy();
|
|
8475
|
+
expect(emptied?.querySelectorAll('.options-readfirst-clear').length).toBe(0);
|
|
8476
|
+
},
|
|
8477
|
+
};
|
|
8478
|
+
|
|
8479
|
+
/**
|
|
8480
|
+
* A creatable picker is ONE control, not two.
|
|
8481
|
+
*
|
|
8482
|
+
* `rendersCreatableValueSelect` exists so that exactly one of the two renderers
|
|
8483
|
+
* draws the value: the chip picker holds it and offers the candidates, and the
|
|
8484
|
+
* raw editor stands down. It asked for `type === 'string'` — but a picker field
|
|
8485
|
+
* declares a storage type (`*string`) AND a `ui_type` (`select-string`), and by
|
|
8486
|
+
* the time the schema arrives the two are flattened to `type: 'select-string'`.
|
|
8487
|
+
* So neither renderer stood down: the picker drew the value, and the
|
|
8488
|
+
* saved-and-suggested list drew the same candidates again beneath it.
|
|
8489
|
+
*
|
|
8490
|
+
* It showed on a test's service-method step, where Service and Method offered
|
|
8491
|
+
* 183 and 7 values twice over in two controls that set one value each.
|
|
8492
|
+
*/
|
|
8493
|
+
export const CreatablePickerIsOneControl: Story = {
|
|
8494
|
+
parameters: {
|
|
8495
|
+
docs: {
|
|
8496
|
+
description: {
|
|
8497
|
+
story:
|
|
8498
|
+
'A creatable select-string with candidates renders a single picker that both holds the value and offers the list — not a picker plus a second "Saved & Suggested Values" control listing the same items.',
|
|
8499
|
+
},
|
|
8500
|
+
},
|
|
8501
|
+
},
|
|
8502
|
+
args: {
|
|
8503
|
+
compact: true,
|
|
8504
|
+
minColumnWidth: '360px',
|
|
8505
|
+
initialExpandedOptions: ['method'],
|
|
8506
|
+
value: { method: { type: 'select-string', value: 'init' } },
|
|
8507
|
+
options: {
|
|
8508
|
+
method: {
|
|
8509
|
+
type: 'select-string',
|
|
8510
|
+
ui_type: 'select-string',
|
|
8511
|
+
display_name: 'Method',
|
|
8512
|
+
required: true,
|
|
8513
|
+
allowed_values_creatable: true,
|
|
8514
|
+
allowed_values: [
|
|
8515
|
+
{ display_name: 'init', value: { type: 'string', value: 'init' } },
|
|
8516
|
+
{ display_name: 'start', value: { type: 'string', value: 'start' } },
|
|
8517
|
+
{ display_name: 'stop', value: { type: 'string', value: 'stop' } },
|
|
8518
|
+
],
|
|
8519
|
+
},
|
|
8520
|
+
} as never,
|
|
8521
|
+
},
|
|
8522
|
+
play: async ({ canvasElement }) => {
|
|
8523
|
+
const row = await waitFor(
|
|
8524
|
+
() => {
|
|
8525
|
+
const el = canvasElement.querySelector('[data-field="method"]');
|
|
8526
|
+
expect(el).toBeTruthy();
|
|
8527
|
+
return el as HTMLElement;
|
|
8528
|
+
},
|
|
8529
|
+
{ timeout: 5000 }
|
|
8530
|
+
);
|
|
8531
|
+
|
|
8532
|
+
// The value is still shown and still editable — one control, not none.
|
|
8533
|
+
expect(row.textContent).toContain('init');
|
|
8534
|
+
|
|
8535
|
+
// …and the second way to set the same field is gone. Asserting the label
|
|
8536
|
+
// rather than a count: it is what the duplicate control announces itself as,
|
|
8537
|
+
// and what a reader saw twice.
|
|
8538
|
+
expect(row.textContent).not.toContain('Saved & Suggested Values');
|
|
8539
|
+
},
|
|
8540
|
+
};
|
|
8541
|
+
|
|
8542
|
+
/**
|
|
8543
|
+
* A chosen template hovers ONCE, and says what the value is.
|
|
8544
|
+
*
|
|
8545
|
+
* Two defects met on the same row. The chip carries its own popover and the row
|
|
8546
|
+
* carried a native `title` as well, so hovering fired both — in two places, and
|
|
8547
|
+
* only the popover appears in a screenshot, which is what makes the pair hard to
|
|
8548
|
+
* report. `showsTemplateChips` had already closed this for prose with templates
|
|
8549
|
+
* embedded in it; a value that IS one template was left open.
|
|
8550
|
+
*
|
|
8551
|
+
* And both of them showed the reference — `$._case.title` — which is the
|
|
8552
|
+
* grammar's business, not the author's. The catalogue already carries what they
|
|
8553
|
+
* want: the entry's name and the prose describing it. The reference stays in the
|
|
8554
|
+
* field's own help and in the picker, where it is genuinely needed.
|
|
8555
|
+
*/
|
|
8556
|
+
export const TemplateValueHoversOnceAndDescribesItself: Story = {
|
|
8557
|
+
parameters: {
|
|
8558
|
+
docs: {
|
|
8559
|
+
description: {
|
|
8560
|
+
story:
|
|
8561
|
+
'A row whose value is a chosen template shows one hover — the chip’s own — carrying the value’s name and description rather than the raw reference, and the row adds no second browser tooltip.',
|
|
8562
|
+
},
|
|
8563
|
+
},
|
|
8564
|
+
},
|
|
8565
|
+
args: {
|
|
8566
|
+
compact: true,
|
|
8567
|
+
minColumnWidth: '360px',
|
|
8568
|
+
value: { expected: { type: 'string', value: '$._case.title' } },
|
|
8569
|
+
options: {
|
|
8570
|
+
expected: {
|
|
8571
|
+
type: 'string',
|
|
8572
|
+
display_name: 'Expected Value',
|
|
8573
|
+
supports_templates: true,
|
|
8574
|
+
templates: {
|
|
8575
|
+
items: [
|
|
8576
|
+
{
|
|
8577
|
+
label: 'Values this case captures',
|
|
8578
|
+
items: [
|
|
8579
|
+
{
|
|
8580
|
+
value: '$._case.title',
|
|
8581
|
+
label: 'title (this case)',
|
|
8582
|
+
description: 'A short line naming what this case verifies. (string)',
|
|
8583
|
+
},
|
|
8584
|
+
],
|
|
8585
|
+
},
|
|
8586
|
+
],
|
|
8587
|
+
},
|
|
8588
|
+
},
|
|
8589
|
+
} as never,
|
|
8590
|
+
},
|
|
8591
|
+
play: async ({ canvasElement }) => {
|
|
8592
|
+
const row = await waitFor(
|
|
8593
|
+
() => {
|
|
8594
|
+
const el = canvasElement.querySelector('[data-field="expected"]');
|
|
8595
|
+
expect(el).toBeTruthy();
|
|
8596
|
+
return el as HTMLElement;
|
|
8597
|
+
},
|
|
8598
|
+
{ timeout: 5000 }
|
|
8599
|
+
);
|
|
8600
|
+
|
|
8601
|
+
// (a) The row draws the friendly name, not the reference.
|
|
8602
|
+
expect(row.textContent).toContain('title (this case)');
|
|
8603
|
+
|
|
8604
|
+
// (b) ONE hover on the value. The row must not add a native `title`
|
|
8605
|
+
// alongside the chip's own popover — the label keeps its own, which is
|
|
8606
|
+
// a different element and a different thing to say.
|
|
8607
|
+
const nativeTitles = Array.from(row.querySelectorAll('[title]')).map((el) =>
|
|
8608
|
+
el.getAttribute('title')
|
|
8609
|
+
);
|
|
8610
|
+
expect(nativeTitles).not.toContain('$._case.title');
|
|
8611
|
+
},
|
|
8612
|
+
};
|
|
8613
|
+
|
|
8614
|
+
/** The grammar a Qorus test writes its references in — `$.step.field`, which
|
|
8615
|
+
* carries no `$key:` and so matches none of reqraft's own token grammar. */
|
|
8616
|
+
const SKIP_WHEN_GRAMMAR = /(\$\.[A-Za-z_][\w]*(?:(?:\.[A-Za-z_][\w]*)|(?:\[\d+\]))*)/g;
|
|
8617
|
+
|
|
8618
|
+
/** What that case's own picker offers, and the names it offers them under. */
|
|
8619
|
+
const SKIP_WHEN_TEMPLATES = {
|
|
8620
|
+
items: [
|
|
8621
|
+
{
|
|
8622
|
+
label: 'Values this case captures',
|
|
8623
|
+
items: [
|
|
8624
|
+
{
|
|
8625
|
+
value: '$._case.mode',
|
|
8626
|
+
label: 'mode (this case)',
|
|
8627
|
+
description: 'How this case runs — live, or simulated. (string)',
|
|
8628
|
+
},
|
|
8629
|
+
],
|
|
8630
|
+
},
|
|
8631
|
+
],
|
|
8632
|
+
};
|
|
8633
|
+
|
|
8634
|
+
/**
|
|
8635
|
+
* A value that MENTIONS a reference reads as a sentence, not as a token.
|
|
8636
|
+
*
|
|
8637
|
+
* The story above closed the case where the value IS one template. This is the
|
|
8638
|
+
* one next to it, and it is the shape a condition actually arrives in: a
|
|
8639
|
+
* comparison the author wrote with a reference inside it. It matched neither the
|
|
8640
|
+
* whole-value template branch nor the rich-text one, so it fell all the way
|
|
8641
|
+
* through to the plain string and the row printed `$._case.mode != 'simulate'` —
|
|
8642
|
+
* the engine's spelling of a value the author had picked by name, on a surface
|
|
8643
|
+
* whose only job is to show them what they wrote.
|
|
8644
|
+
*
|
|
8645
|
+
* Reported against a test case's **Skip when** field, where the case list above
|
|
8646
|
+
* the drawer already named the reference and the drawer beneath it did not.
|
|
8647
|
+
*
|
|
8648
|
+
* Both halves matter. The reference becomes the chip the picker offered; the
|
|
8649
|
+
* comparison is the author's own text and survives exactly as typed.
|
|
8650
|
+
*/
|
|
8651
|
+
export const AValueThatMentionsATemplateReadsAsOne: Story = {
|
|
8652
|
+
parameters: {
|
|
8653
|
+
docs: {
|
|
8654
|
+
description: {
|
|
8655
|
+
story:
|
|
8656
|
+
'A row whose value holds a reference inside the author’s own text — a skip-when predicate — draws the reference as the chip the picker names it by and leaves the comparison beside it exactly as written, instead of printing the raw token.',
|
|
8657
|
+
},
|
|
8658
|
+
},
|
|
8659
|
+
},
|
|
8660
|
+
args: {
|
|
8661
|
+
compact: true,
|
|
8662
|
+
minColumnWidth: '360px',
|
|
8663
|
+
value: {
|
|
8664
|
+
skip_when: { type: 'string', value: "$._case.mode != 'simulate'" },
|
|
8665
|
+
// The same value one level down, so the row's own line and the preview it
|
|
8666
|
+
// opens are compared against each other in one story.
|
|
8667
|
+
guard: {
|
|
8668
|
+
type: 'hash',
|
|
8669
|
+
value: { when: { type: 'string', value: "$._case.mode != 'simulate'" } },
|
|
8670
|
+
},
|
|
8671
|
+
},
|
|
8672
|
+
options: {
|
|
8673
|
+
skip_when: {
|
|
8674
|
+
type: 'string',
|
|
8675
|
+
display_name: 'Skip when',
|
|
8676
|
+
supports_templates: true,
|
|
8677
|
+
// The grammar this host writes its references in — `$.step.field`, which
|
|
8678
|
+
// carries no `$key:` and so matches none of reqraft's own token grammar.
|
|
8679
|
+
// A field that speaks one says so, exactly as it hands down its list.
|
|
8680
|
+
templateToken: SKIP_WHEN_GRAMMAR,
|
|
8681
|
+
templates: SKIP_WHEN_TEMPLATES,
|
|
8682
|
+
},
|
|
8683
|
+
guard: {
|
|
8684
|
+
type: 'hash',
|
|
8685
|
+
display_name: 'Guard',
|
|
8686
|
+
arg_schema: {
|
|
8687
|
+
when: {
|
|
8688
|
+
type: 'string',
|
|
8689
|
+
display_name: 'When',
|
|
8690
|
+
supports_templates: true,
|
|
8691
|
+
templateToken: SKIP_WHEN_GRAMMAR,
|
|
8692
|
+
templates: SKIP_WHEN_TEMPLATES,
|
|
8693
|
+
},
|
|
8694
|
+
},
|
|
8695
|
+
},
|
|
8696
|
+
} as never,
|
|
8697
|
+
},
|
|
8698
|
+
play: async ({ canvasElement }) => {
|
|
8699
|
+
const row = await waitFor(
|
|
8700
|
+
() => {
|
|
8701
|
+
const el = canvasElement.querySelector('[data-field="skip_when"]');
|
|
8702
|
+
expect(el).toBeTruthy();
|
|
8703
|
+
return el as HTMLElement;
|
|
8704
|
+
},
|
|
8705
|
+
{ timeout: 5000 }
|
|
8706
|
+
);
|
|
8707
|
+
|
|
8708
|
+
// (a) The reference wears the name it was chosen by, on a chip.
|
|
8709
|
+
const chip = await waitFor(() => {
|
|
8710
|
+
const el = row.querySelector('.reqraft-template-chip');
|
|
8711
|
+
expect(el).toBeTruthy();
|
|
8712
|
+
return el as HTMLElement;
|
|
8713
|
+
});
|
|
8714
|
+
expect(chip.textContent).toContain('mode (this case)');
|
|
8715
|
+
|
|
8716
|
+
// (b) The author's comparison is still there, word for word.
|
|
8717
|
+
expect(row.textContent).toContain("!= 'simulate'");
|
|
8718
|
+
|
|
8719
|
+
// (c) And the engine's own spelling is nowhere on the row — not in its text,
|
|
8720
|
+
// and not in a native tooltip hovering the very thing the chip replaced.
|
|
8721
|
+
expect(row.textContent).not.toContain('$._case.mode');
|
|
8722
|
+
const nativeTitles = Array.from(row.querySelectorAll('[title]')).map((el) =>
|
|
8723
|
+
el.getAttribute('title')
|
|
8724
|
+
);
|
|
8725
|
+
expect(nativeTitles.some((title) => title?.includes('$._case.mode'))).toBe(false);
|
|
8726
|
+
|
|
8727
|
+
// (d) The schema preview under the hash row is the same value one level
|
|
8728
|
+
// down, and it used to reach the mono cell as a raw string — a row
|
|
8729
|
+
// disagreeing with its own inset about what its value is.
|
|
8730
|
+
const preview = await waitFor(() => {
|
|
8731
|
+
const el = canvasElement.querySelector('[data-field="guard"] .schema-data-view');
|
|
8732
|
+
expect(el).toBeTruthy();
|
|
8733
|
+
return el as HTMLElement;
|
|
8734
|
+
});
|
|
8735
|
+
expect(preview.querySelector('.reqraft-template-chip')?.textContent).toContain(
|
|
8736
|
+
'mode (this case)'
|
|
8737
|
+
);
|
|
8738
|
+
expect(preview.textContent).toContain("!= 'simulate'");
|
|
8739
|
+
expect(preview.textContent).not.toContain('$._case.mode');
|
|
8740
|
+
},
|
|
8741
|
+
};
|
|
8742
|
+
|
|
8743
|
+
/**
|
|
8744
|
+
* The editor a row opens recognises the SAME references the row does.
|
|
8745
|
+
*
|
|
8746
|
+
* The story above asserts the two READ surfaces — the collapsed row and the
|
|
8747
|
+
* schema preview under a hash row. Neither of them opens anything, and the
|
|
8748
|
+
* editor was the surface that disagreed: `templateTextSegments` took the
|
|
8749
|
+
* field's declared grammar, while `templateTextToNodes` had no grammar
|
|
8750
|
+
* parameter at all and knew only reqraft's built-in `$key:{path}`. So a host's
|
|
8751
|
+
* own spelling was a named chip on the row and raw text the moment the author
|
|
8752
|
+
* clicked it — the same defect as before, pointing the other way.
|
|
8753
|
+
*
|
|
8754
|
+
* Both now resolve references through one `lineReferenceSpans`, and the field's
|
|
8755
|
+
* `templateToken` reaches the editor as a prop. This opens the row and asserts
|
|
8756
|
+
* the chip is still a chip, with the author's comparison beside it.
|
|
8757
|
+
*/
|
|
8758
|
+
export const AHostGrammarSurvivesTheEditorTheRowOpens: Story = {
|
|
8759
|
+
parameters: {
|
|
8760
|
+
docs: {
|
|
8761
|
+
description: {
|
|
8762
|
+
story:
|
|
8763
|
+
'Opens the Skip when row whose field declares its own token grammar — the reference stays the chip the picker names it by inside the editor, with the author’s comparison beside it, instead of reverting to its raw spelling.',
|
|
8764
|
+
},
|
|
8765
|
+
},
|
|
8766
|
+
},
|
|
8767
|
+
args: {
|
|
8768
|
+
...AValueThatMentionsATemplateReadsAsOne.args,
|
|
8769
|
+
/* A SECOND reference the catalogue does not list. It is the one that
|
|
8770
|
+
separates the two mechanisms: `$._case.mode` is an offered value and
|
|
8771
|
+
would be found by matching the catalogue's literals alone, while
|
|
8772
|
+
`$._case.attempt` exists only as far as the declared grammar says it
|
|
8773
|
+
does. A surface that has not been given the grammar draws one chip
|
|
8774
|
+
here and the raw spelling of the other. */
|
|
8775
|
+
value: {
|
|
8776
|
+
skip_when: {
|
|
8777
|
+
type: 'string',
|
|
8778
|
+
value: "$._case.mode != 'simulate' && $._case.attempt == 1",
|
|
8779
|
+
},
|
|
8780
|
+
},
|
|
8781
|
+
},
|
|
8782
|
+
play: async ({ canvasElement }) => {
|
|
8783
|
+
const row = await waitFor(
|
|
8784
|
+
() => {
|
|
8785
|
+
const el = canvasElement.querySelector('[data-field="skip_when"]');
|
|
8786
|
+
expect(el).toBeTruthy();
|
|
8787
|
+
return el as HTMLElement;
|
|
8788
|
+
},
|
|
8789
|
+
{ timeout: 5000 }
|
|
8790
|
+
);
|
|
8791
|
+
|
|
8792
|
+
// The collapsed row draws BOTH — the named one and the one only the
|
|
8793
|
+
// grammar knows the extent of.
|
|
8794
|
+
await waitFor(() =>
|
|
8795
|
+
expect(
|
|
8796
|
+
Array.from(row.querySelectorAll('.reqraft-template-chip')).map((chip) =>
|
|
8797
|
+
chip.textContent?.trim()
|
|
8798
|
+
)
|
|
8799
|
+
).toEqual(['mode (this case)', '$._case.attempt'])
|
|
8800
|
+
);
|
|
8801
|
+
|
|
8802
|
+
// Open the row.
|
|
8803
|
+
fireEvent.click(row);
|
|
8804
|
+
await waitFor(() => expect(canvasElement.querySelector('.readfirst-row-editing')).toBeTruthy(), {
|
|
8805
|
+
timeout: 5000,
|
|
8806
|
+
});
|
|
8807
|
+
|
|
8808
|
+
// The editor draws the same reference as the same name.
|
|
8809
|
+
const editor = await waitFor(
|
|
8810
|
+
() => {
|
|
8811
|
+
const el = canvasElement.querySelector<HTMLElement>(
|
|
8812
|
+
'[data-field="skip_when"] [contenteditable="true"]'
|
|
8813
|
+
);
|
|
8814
|
+
expect(el).toBeTruthy();
|
|
8815
|
+
return el!;
|
|
8816
|
+
},
|
|
8817
|
+
{ timeout: 10000 }
|
|
8818
|
+
);
|
|
8819
|
+
await waitFor(
|
|
8820
|
+
() =>
|
|
8821
|
+
expect(
|
|
8822
|
+
Array.from(editor.querySelectorAll('.reqore-tag')).map((chip) => chip.textContent?.trim())
|
|
8823
|
+
).toEqual(['mode (this case)', '$._case.attempt']),
|
|
8824
|
+
{ timeout: 10000 }
|
|
8825
|
+
);
|
|
8826
|
+
|
|
8827
|
+
// And the comparison the author wrote is beside them, unchanged — while
|
|
8828
|
+
// the engine's own spelling of the NAMED reference is nowhere in the
|
|
8829
|
+
// editor. (`$._case.attempt` has no name to wear: an unnamed reference is
|
|
8830
|
+
// its own honest label, and it is a chip either way.)
|
|
8831
|
+
expect(editor.textContent).toContain("!= 'simulate'");
|
|
8832
|
+
expect(editor.textContent).toContain('== 1');
|
|
8833
|
+
expect(editor.textContent).not.toContain('$._case.mode');
|
|
8834
|
+
},
|
|
8835
|
+
};
|
|
8836
|
+
|
|
8837
|
+
/**
|
|
8838
|
+
* A choice whose author has to click twice to change it.
|
|
8839
|
+
*
|
|
8840
|
+
* Read-first is the collapsed row's job: the row prints the value, and clicking
|
|
8841
|
+
* it mounts the editor. A PICKER's editor is a closed trigger printing that
|
|
8842
|
+
* same value — so the row opened, nothing on screen changed, and the author had
|
|
8843
|
+
* to click again to see the list. Reported on a test case's Kind row:
|
|
8844
|
+
* "clicking on a closed option like 'Kind' opens the option, but the same
|
|
8845
|
+
* chosen value is displayed … the user has to click twice to edit the option".
|
|
8846
|
+
*
|
|
8847
|
+
* A row the AUTHOR opens now opens its picker with it. A row the FORM opens
|
|
8848
|
+
* still does not — see `CompactInitialExpandedOptions` and
|
|
8849
|
+
* `CompactAutoFocusFirstRequired`, where the host is showing a row rather than
|
|
8850
|
+
* asking the question, and a list thrown over it would cover what it came to
|
|
8851
|
+
* show.
|
|
8852
|
+
*/
|
|
8853
|
+
const CompactPickerRowSchema: Record<string, TCompactField> = {
|
|
8854
|
+
kind: {
|
|
8855
|
+
type: 'string',
|
|
8856
|
+
ui_type: 'string',
|
|
8857
|
+
display_name: 'Kind',
|
|
8858
|
+
short_desc: 'Which kind of assertion is this?',
|
|
8859
|
+
required: true,
|
|
8860
|
+
preselected: true,
|
|
8861
|
+
group: 'general',
|
|
8862
|
+
allowed_values: [
|
|
8863
|
+
{
|
|
8864
|
+
display_name: 'Process coverage',
|
|
8865
|
+
short_desc: 'Which Qorus process ran the code',
|
|
8866
|
+
value: { type: 'string', value: 'process_coverage' },
|
|
8867
|
+
},
|
|
8868
|
+
{
|
|
8869
|
+
display_name: 'Log match',
|
|
8870
|
+
short_desc: 'A line the run had to log',
|
|
8871
|
+
value: { type: 'string', value: 'log_match' },
|
|
8872
|
+
},
|
|
8873
|
+
{
|
|
8874
|
+
display_name: 'Value equals',
|
|
8875
|
+
short_desc: 'A captured value, compared',
|
|
8876
|
+
value: { type: 'string', value: 'value_equals' },
|
|
8877
|
+
},
|
|
8878
|
+
{
|
|
8879
|
+
display_name: 'Row count',
|
|
8880
|
+
short_desc: 'How many rows a query returned',
|
|
8881
|
+
value: { type: 'string', value: 'row_count' },
|
|
8882
|
+
},
|
|
8883
|
+
{
|
|
8884
|
+
display_name: 'Raises error',
|
|
8885
|
+
short_desc: 'The error the step had to raise',
|
|
8886
|
+
value: { type: 'string', value: 'raises_error' },
|
|
8887
|
+
},
|
|
8888
|
+
],
|
|
8889
|
+
} as TCompactField,
|
|
8890
|
+
name: {
|
|
8891
|
+
type: 'string',
|
|
8892
|
+
ui_type: 'string',
|
|
8893
|
+
display_name: 'Name',
|
|
8894
|
+
short_desc: 'A text field — its editor is the text box itself',
|
|
8895
|
+
required: true,
|
|
8896
|
+
preselected: true,
|
|
8897
|
+
group: 'general',
|
|
8898
|
+
},
|
|
8899
|
+
};
|
|
8900
|
+
|
|
8901
|
+
export const CompactRowOpensItsPicker: Story = {
|
|
8902
|
+
parameters: {
|
|
8903
|
+
docs: {
|
|
8904
|
+
description: {
|
|
8905
|
+
story:
|
|
8906
|
+
'Clicking a closed row whose value is a choice lands the author in the list of choices, in one click, instead of opening the row onto a closed picker showing the value it already had.',
|
|
8907
|
+
},
|
|
8908
|
+
},
|
|
8909
|
+
},
|
|
8910
|
+
args: {
|
|
8911
|
+
compact: true,
|
|
8912
|
+
minColumnWidth: '360px',
|
|
8913
|
+
options: CompactPickerRowSchema as IOptionsSchema,
|
|
8914
|
+
value: { kind: { type: 'string', value: 'log_match' } },
|
|
8915
|
+
groups: CompactGroups,
|
|
8916
|
+
},
|
|
8917
|
+
play: async () => {
|
|
8918
|
+
await _testsWaitForText('Kind');
|
|
8919
|
+
// Read-first: the row prints the chosen kind, and no other kind is on screen.
|
|
8920
|
+
await _testsWaitForTextToNotExist('Row count');
|
|
8921
|
+
|
|
8922
|
+
await _testsClickButton({ selector: '[data-field="kind"].readfirst-row' });
|
|
8923
|
+
|
|
8924
|
+
// ONE click, and the choices the author came for are in front of them.
|
|
8925
|
+
await _testsWaitForText('Row count');
|
|
8926
|
+
await _testsWaitForText('Raises error');
|
|
8927
|
+
|
|
8928
|
+
/* ...in ONE picker. The collection modal is rendered beside the trigger
|
|
8929
|
+
rather than as one of its branches, so it is not mutually exclusive with
|
|
8930
|
+
the anchored list, and `forceDropdown` defaults to true — so asking for
|
|
8931
|
+
the choices used to open a dialog ON TOP of the list, both offering the
|
|
8932
|
+
same five items. Asserting the items alone did not catch it: they were
|
|
8933
|
+
present twice. Reported on qlip build #212. */
|
|
8934
|
+
await _testsWaitForTextToNotExist('Select from items');
|
|
8935
|
+
},
|
|
8936
|
+
};
|
|
8937
|
+
|
|
8938
|
+
export const CompactTextRowOpensItsTextBox: Story = {
|
|
8939
|
+
parameters: {
|
|
8940
|
+
docs: {
|
|
8941
|
+
description: {
|
|
8942
|
+
story:
|
|
8943
|
+
'The other half of the same rule: a row whose editor is a text box opens straight into it with the caret, and opens nothing over the form.',
|
|
8944
|
+
},
|
|
8945
|
+
},
|
|
8946
|
+
},
|
|
8947
|
+
args: {
|
|
8948
|
+
compact: true,
|
|
8949
|
+
minColumnWidth: '360px',
|
|
8950
|
+
options: CompactPickerRowSchema as IOptionsSchema,
|
|
8951
|
+
value: { name: { type: 'string', value: 'the assertion' } },
|
|
8952
|
+
groups: CompactGroups,
|
|
8953
|
+
},
|
|
8954
|
+
play: async () => {
|
|
8955
|
+
await _testsWaitForText('Name');
|
|
8956
|
+
await _testsClickButton({ selector: '[data-field="name"].readfirst-row' });
|
|
8957
|
+
|
|
8958
|
+
await waitFor(() => {
|
|
8959
|
+
const active = document.activeElement as HTMLElement;
|
|
8960
|
+
expect(['INPUT', 'TEXTAREA']).toContain(active.tagName);
|
|
8961
|
+
expect(document.querySelector('[data-field="name"]')?.contains(active)).toBe(true);
|
|
8962
|
+
});
|
|
8963
|
+
// Nothing was opened over the form — the text box IS the editor.
|
|
8964
|
+
expect(document.querySelector('.reqraft-select-dialog')).toBeNull();
|
|
8965
|
+
},
|
|
8966
|
+
};
|