@qoretechnologies/reqraft 0.10.25 → 0.10.26
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 +52 -23
- package/design/COMPACT_ENGINE_REDESIGN.md +156 -0
- package/design/FORM_ENGINE_COMPACT_UX_PLAN.md +353 -0
- package/dist/components/FocusedEditing/index.d.ts +9 -1
- package/dist/components/FocusedEditing/index.d.ts.map +1 -1
- package/dist/components/FocusedEditing/index.js +2 -2
- package/dist/components/FocusedEditing/index.js.map +1 -1
- package/dist/components/composer/Composer.d.ts +108 -0
- package/dist/components/composer/Composer.d.ts.map +1 -0
- package/dist/components/composer/Composer.js +238 -0
- package/dist/components/composer/Composer.js.map +1 -0
- package/dist/components/composer/helpers.d.ts +21 -0
- package/dist/components/composer/helpers.d.ts.map +1 -0
- package/dist/components/composer/helpers.js +75 -0
- package/dist/components/composer/helpers.js.map +1 -0
- package/dist/components/composer/index.d.ts +3 -0
- package/dist/components/composer/index.d.ts.map +1 -0
- package/dist/components/composer/index.js +19 -0
- package/dist/components/composer/index.js.map +1 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +307 -110
- 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 +123 -106
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +107 -11
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +618 -179
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +9 -0
- 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 +7 -3
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +88 -51
- 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/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/optionActions.d.ts +28 -0
- package/dist/components/form/engine/optionActions.d.ts.map +1 -0
- package/dist/components/form/engine/optionActions.js +18 -0
- package/dist/components/form/engine/optionActions.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +63 -2
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +121 -6
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rendererTypes.d.ts +35 -0
- package/dist/components/form/engine/rendererTypes.d.ts.map +1 -0
- package/dist/components/form/engine/rendererTypes.js +63 -0
- package/dist/components/form/engine/rendererTypes.js.map +1 -0
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js +94 -0
- package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantCards.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantCards.js +80 -0
- package/dist/components/form/engine/variants/VariantCards.js.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts +7 -0
- package/dist/components/form/engine/variants/VariantFocus.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantFocus.js +138 -0
- package/dist/components/form/engine/variants/VariantFocus.js.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts +6 -0
- package/dist/components/form/engine/variants/VariantMinimal.d.ts.map +1 -0
- package/dist/components/form/engine/variants/VariantMinimal.js +73 -0
- package/dist/components/form/engine/variants/VariantMinimal.js.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts +13 -0
- package/dist/components/form/engine/variants/focusDemo.d.ts.map +1 -0
- package/dist/components/form/engine/variants/focusDemo.js +139 -0
- package/dist/components/form/engine/variants/focusDemo.js.map +1 -0
- package/dist/components/form/engine/variants/variantModel.d.ts +70 -0
- package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantModel.js +133 -0
- package/dist/components/form/engine/variants/variantModel.js.map +1 -0
- package/dist/components/form/engine/variants/variantParts.d.ts +79 -0
- package/dist/components/form/engine/variants/variantParts.d.ts.map +1 -0
- package/dist/components/form/engine/variants/variantParts.js +191 -0
- package/dist/components/form/engine/variants/variantParts.js.map +1 -0
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +13 -2
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/useExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/useExpressions.js +4 -1
- package/dist/components/form/expressions/useExpressions.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +5 -3
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +2 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +8 -26
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts +3 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +42 -8
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/binary/Binary.d.ts +15 -0
- package/dist/components/form/fields/binary/Binary.d.ts.map +1 -0
- package/dist/components/form/fields/binary/Binary.js +74 -0
- package/dist/components/form/fields/binary/Binary.js.map +1 -0
- package/dist/components/form/fields/boolean/Boolean.js +1 -1
- package/dist/components/form/fields/boolean/Boolean.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 +5 -4
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
- package/dist/components/form/fields/select/Select.d.ts.map +1 -1
- package/dist/components/form/fields/select/Select.js +29 -12
- package/dist/components/form/fields/select/Select.js.map +1 -1
- package/dist/components/form/fields/select/SelectCollection.d.ts +8 -2
- package/dist/components/form/fields/select/SelectCollection.d.ts.map +1 -1
- package/dist/components/form/fields/select/SelectCollection.js +52 -16
- package/dist/components/form/fields/select/SelectCollection.js.map +1 -1
- 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 +2 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/imageLightbox/ImageLightbox.d.ts +21 -0
- package/dist/components/imageLightbox/ImageLightbox.d.ts.map +1 -0
- package/dist/components/imageLightbox/ImageLightbox.js +169 -0
- package/dist/components/imageLightbox/ImageLightbox.js.map +1 -0
- package/dist/components/imageLightbox/index.d.ts +2 -0
- package/dist/components/imageLightbox/index.d.ts.map +1 -0
- package/dist/components/imageLightbox/index.js +18 -0
- package/dist/components/imageLightbox/index.js.map +1 -0
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts +33 -0
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -0
- package/dist/components/searchFilterBar/SearchFilterBar.js +76 -0
- package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -0
- package/dist/components/searchFilterBar/index.d.ts +2 -0
- package/dist/components/searchFilterBar/index.d.ts.map +1 -0
- package/dist/components/searchFilterBar/index.js +18 -0
- package/dist/components/searchFilterBar/index.js.map +1 -0
- package/dist/components/supportTicket/AttachmentChips.d.ts +10 -0
- package/dist/components/supportTicket/AttachmentChips.d.ts.map +1 -0
- package/dist/components/supportTicket/AttachmentChips.js +122 -0
- package/dist/components/supportTicket/AttachmentChips.js.map +1 -0
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +43 -0
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -0
- package/dist/components/supportTicket/InterfaceReferenceTags.js +31 -0
- package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -0
- package/dist/components/supportTicket/ReferencePicker.d.ts +87 -0
- package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -0
- package/dist/components/supportTicket/ReferencePicker.js +295 -0
- package/dist/components/supportTicket/ReferencePicker.js.map +1 -0
- package/dist/components/supportTicket/TicketHeader.d.ts +86 -0
- package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -0
- package/dist/components/supportTicket/TicketHeader.js +149 -0
- package/dist/components/supportTicket/TicketHeader.js.map +1 -0
- package/dist/components/supportTicket/TicketMetaTags.d.ts +32 -0
- package/dist/components/supportTicket/TicketMetaTags.d.ts.map +1 -0
- package/dist/components/supportTicket/TicketMetaTags.js +21 -0
- package/dist/components/supportTicket/TicketMetaTags.js.map +1 -0
- package/dist/components/supportTicket/TicketReplyBox.d.ts +63 -0
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -0
- package/dist/components/supportTicket/TicketReplyBox.js +115 -0
- package/dist/components/supportTicket/TicketReplyBox.js.map +1 -0
- package/dist/components/supportTicket/attachmentAccess.d.ts +37 -0
- package/dist/components/supportTicket/attachmentAccess.d.ts.map +1 -0
- package/dist/components/supportTicket/attachmentAccess.js +132 -0
- package/dist/components/supportTicket/attachmentAccess.js.map +1 -0
- package/dist/components/supportTicket/fileToAttachment.d.ts +9 -0
- package/dist/components/supportTicket/fileToAttachment.d.ts.map +1 -0
- package/dist/components/supportTicket/fileToAttachment.js +28 -0
- package/dist/components/supportTicket/fileToAttachment.js.map +1 -0
- package/dist/components/supportTicket/index.d.ts +11 -0
- package/dist/components/supportTicket/index.d.ts.map +1 -0
- package/dist/components/supportTicket/index.js +27 -0
- package/dist/components/supportTicket/index.js.map +1 -0
- package/dist/components/supportTicket/meta.d.ts +71 -0
- package/dist/components/supportTicket/meta.d.ts.map +1 -0
- package/dist/components/supportTicket/meta.js +115 -0
- package/dist/components/supportTicket/meta.js.map +1 -0
- package/dist/components/supportTicket/ticketModel.d.ts +102 -0
- package/dist/components/supportTicket/ticketModel.d.ts.map +1 -0
- package/dist/components/supportTicket/ticketModel.js +3 -0
- package/dist/components/supportTicket/ticketModel.js.map +1 -0
- package/dist/components/ticketReferences/TicketReferences.d.ts +52 -0
- package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -0
- package/dist/components/ticketReferences/TicketReferences.js +492 -0
- package/dist/components/ticketReferences/TicketReferences.js.map +1 -0
- package/dist/components/ticketReferences/index.d.ts +3 -0
- package/dist/components/ticketReferences/index.d.ts.map +1 -0
- package/dist/components/ticketReferences/index.js +6 -0
- package/dist/components/ticketReferences/index.js.map +1 -0
- package/dist/components/ticketThread/TicketThread.d.ts +57 -0
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -0
- package/dist/components/ticketThread/TicketThread.js +299 -0
- package/dist/components/ticketThread/TicketThread.js.map +1 -0
- package/dist/components/ticketThread/index.d.ts +3 -0
- package/dist/components/ticketThread/index.d.ts.map +1 -0
- package/dist/components/ticketThread/index.js +7 -0
- package/dist/components/ticketThread/index.js.map +1 -0
- package/dist/helpers/common.d.ts +5 -0
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +20 -1
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/scrollbar.d.ts +2 -0
- package/dist/helpers/scrollbar.d.ts.map +1 -0
- package/dist/helpers/scrollbar.js +25 -0
- package/dist/helpers/scrollbar.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/StorageProvider.d.ts +13 -0
- package/dist/providers/StorageProvider.d.ts.map +1 -1
- package/dist/providers/StorageProvider.js +29 -5
- package/dist/providers/StorageProvider.js.map +1 -1
- package/package.json +6 -5
- package/src/components/FocusedEditing/index.tsx +17 -2
- package/src/components/composer/Composer.stories.tsx +238 -0
- package/src/components/composer/Composer.tsx +545 -0
- package/src/components/composer/helpers.ts +74 -0
- package/src/components/composer/index.ts +2 -0
- package/src/components/dpqlEditor/DpqlEditor.stories.tsx +74 -2
- package/src/components/form/engine/CompactRow.tsx +561 -283
- package/src/components/form/engine/CompactToolbar.tsx +216 -182
- package/src/components/form/engine/FormEngine.stories.tsx +1736 -162
- package/src/components/form/engine/FormEngine.tsx +886 -170
- package/src/components/form/engine/FormEngineRemote.stories.tsx +377 -0
- package/src/components/form/engine/_structuredData/StructuredDataView.stories.tsx +8 -0
- package/src/components/form/engine/compactRowContext.ts +14 -0
- package/src/components/form/engine/compactRowStyles.ts +273 -146
- package/src/components/form/engine/compactToolbarContext.ts +1 -0
- package/src/components/form/engine/optionActions.ts +40 -0
- package/src/components/form/engine/readFirst.ts +169 -10
- package/src/components/form/engine/rendererTypes.ts +55 -0
- package/src/components/form/engine/variants/FormEngineVariants.stories.tsx +159 -0
- package/src/components/form/engine/variants/VariantCalmTable.tsx +242 -0
- package/src/components/form/engine/variants/VariantCards.tsx +212 -0
- package/src/components/form/engine/variants/VariantFocus.tsx +382 -0
- package/src/components/form/engine/variants/VariantMinimal.tsx +170 -0
- package/src/components/form/engine/variants/focusDemo.ts +145 -0
- package/src/components/form/engine/variants/variantModel.ts +216 -0
- package/src/components/form/engine/variants/variantParts.tsx +313 -0
- package/src/components/form/expressions/ExpressionField.stories.tsx +84 -2
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +213 -19
- package/src/components/form/expressions/builder/index.tsx +12 -1
- package/src/components/form/expressions/useExpressions.ts +4 -1
- package/src/components/form/fields/Field.stories.tsx +40 -0
- package/src/components/form/fields/Field.tsx +10 -1
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -36
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +64 -0
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +146 -2
- package/src/components/form/fields/auto/AutoFormField.tsx +42 -1
- package/src/components/form/fields/binary/Binary.stories.tsx +122 -0
- package/src/components/form/fields/binary/Binary.tsx +81 -0
- package/src/components/form/fields/boolean/Boolean.stories.tsx +58 -0
- package/src/components/form/fields/boolean/Boolean.tsx +1 -1
- package/src/components/form/fields/byte-size/ByteSize.stories.tsx +24 -0
- package/src/components/form/fields/color/Color.stories.tsx +30 -0
- package/src/components/form/fields/cron/Cron.stories.tsx +8 -0
- package/src/components/form/fields/date/Date.stories.tsx +22 -0
- package/src/components/form/fields/file/File.stories.tsx +48 -0
- package/src/components/form/fields/long-string/LongString.stories.tsx +24 -0
- package/src/components/form/fields/markdown/Markdown.stories.tsx +8 -0
- package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +14 -10
- package/src/components/form/fields/number/Number.stories.tsx +31 -0
- package/src/components/form/fields/object/Object.stories.tsx +88 -0
- package/src/components/form/fields/radio-group/RadioGroup.stories.tsx +24 -0
- package/src/components/form/fields/rich-text/RichText.stories.tsx +23 -0
- package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +72 -0
- package/src/components/form/fields/select/Select.stories.tsx +108 -0
- package/src/components/form/fields/select/Select.tsx +38 -12
- package/src/components/form/fields/select/SelectCollection.tsx +78 -17
- package/src/components/form/fields/string/String.stories.tsx +16 -0
- package/src/components/form/fields/template/TemplateField.stories.tsx +160 -0
- package/src/components/form/fields/url/Url.stories.tsx +24 -0
- package/src/components/form/index.tsx +2 -0
- package/src/components/imageLightbox/ImageLightbox.stories.tsx +238 -0
- package/src/components/imageLightbox/ImageLightbox.tsx +236 -0
- package/src/components/imageLightbox/index.ts +1 -0
- package/src/components/log/Log.stories.tsx +96 -0
- package/src/components/menu/Menu.stories.tsx +54 -0
- package/src/components/qonsoleSmartInput/QonsoleSmartInput.stories.tsx +83 -37
- package/src/components/searchFilterBar/SearchFilterBar.tsx +131 -0
- package/src/components/searchFilterBar/index.ts +1 -0
- package/src/components/smartEditor/SmartEditor.stories.tsx +32 -0
- package/src/components/supportTicket/AttachmentChips.stories.tsx +171 -0
- package/src/components/supportTicket/AttachmentChips.tsx +147 -0
- package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +171 -0
- package/src/components/supportTicket/InterfaceReferenceTags.tsx +96 -0
- package/src/components/supportTicket/ReferencePicker.stories.tsx +519 -0
- package/src/components/supportTicket/ReferencePicker.tsx +563 -0
- package/src/components/supportTicket/TicketConversation.stories.tsx +121 -0
- package/src/components/supportTicket/TicketHeader.stories.tsx +183 -0
- package/src/components/supportTicket/TicketHeader.tsx +247 -0
- package/src/components/supportTicket/TicketMetaTags.stories.tsx +103 -0
- package/src/components/supportTicket/TicketMetaTags.tsx +85 -0
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +351 -0
- package/src/components/supportTicket/TicketReplyBox.tsx +146 -0
- package/src/components/supportTicket/attachmentAccess.ts +97 -0
- package/src/components/supportTicket/fileToAttachment.ts +24 -0
- package/src/components/supportTicket/index.ts +10 -0
- package/src/components/supportTicket/meta.ts +185 -0
- package/src/components/supportTicket/ticketModel.ts +131 -0
- package/src/components/ticketReferences/TicketReferences.stories.tsx +315 -0
- package/src/components/ticketReferences/TicketReferences.tsx +886 -0
- package/src/components/ticketReferences/index.ts +2 -0
- package/src/components/ticketThread/TicketThread.stories.tsx +828 -0
- package/src/components/ticketThread/TicketThread.tsx +704 -0
- package/src/components/ticketThread/index.ts +6 -0
- package/src/helpers/common.ts +19 -0
- package/src/helpers/scrollbar.ts +32 -0
- package/src/hooks/useFetch/useFetch.stories.tsx +30 -0
- package/src/hooks/useStorage/useStorage.stories.tsx +82 -0
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +127 -1
- package/src/index.tsx +8 -1
- package/src/providers/StorageProvider.tsx +30 -4
- package/src/stores/currentUser/currentUser.stories.tsx +49 -0
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
ReqoreCollection,
|
|
4
4
|
ReqoreControlGroup,
|
|
5
5
|
ReqoreErrorBoundary,
|
|
6
|
+
ReqoreIcon,
|
|
6
7
|
ReqoreMessage,
|
|
7
8
|
ReqoreP,
|
|
8
|
-
ReqorePanel,
|
|
9
9
|
ReqoreSkeleton,
|
|
10
10
|
ReqoreTag,
|
|
11
11
|
ReqoreTagGroup,
|
|
@@ -15,19 +15,16 @@ import {
|
|
|
15
15
|
} from '@qoretechnologies/reqore';
|
|
16
16
|
import { IReqoreCollectionProps } from '@qoretechnologies/reqore/dist/components/Collection';
|
|
17
17
|
import { IReqoreCollectionItemProps } from '@qoretechnologies/reqore/dist/components/Collection/item';
|
|
18
|
-
import {
|
|
19
|
-
IReqorePanelAction,
|
|
20
|
-
IReqorePanelProps,
|
|
21
|
-
} from '@qoretechnologies/reqore/dist/components/Panel';
|
|
18
|
+
import { IReqorePanelProps } from '@qoretechnologies/reqore/dist/components/Panel';
|
|
22
19
|
import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
23
20
|
import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
|
|
24
|
-
import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
|
|
25
21
|
import {
|
|
26
22
|
changeDarkness,
|
|
27
23
|
getMainBackgroundColor,
|
|
28
24
|
getReadableColor,
|
|
29
25
|
percentToHexAlpha,
|
|
30
26
|
} from '@qoretechnologies/reqore/dist/helpers/colors';
|
|
27
|
+
import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
|
|
31
28
|
import {
|
|
32
29
|
IQorusFormField,
|
|
33
30
|
IQorusFormFieldMessage,
|
|
@@ -41,6 +38,8 @@ import {
|
|
|
41
38
|
TQorusFormOperatorValue,
|
|
42
39
|
TQorusType,
|
|
43
40
|
} from '@qoretechnologies/ts-toolkit';
|
|
41
|
+
import { resolveOptionActions, TOptionActions } from './optionActions';
|
|
42
|
+
import { createRendererOnlyUiTypeCheck, isRendererOnlyUiType } from './rendererTypes';
|
|
44
43
|
import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last } from 'lodash';
|
|
45
44
|
import isArray from 'lodash/isArray';
|
|
46
45
|
import map from 'lodash/map';
|
|
@@ -48,11 +47,10 @@ import reduce from 'lodash/reduce';
|
|
|
48
47
|
import size from 'lodash/size';
|
|
49
48
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
50
49
|
import { useMeasure, useMount, useUpdateEffect } from 'react-use';
|
|
51
|
-
import { createContext } from 'use-context-selector';
|
|
52
50
|
import styled from 'styled-components';
|
|
51
|
+
import { createContext } from 'use-context-selector';
|
|
53
52
|
import { getDefaultValue, insertAtIndex, richtextToString } from '../../../helpers/common';
|
|
54
53
|
import { getRequiredOptionMessage } from '../../../helpers/options';
|
|
55
|
-
import { query } from '../../../utils/fetch';
|
|
56
54
|
import {
|
|
57
55
|
IValidationResult,
|
|
58
56
|
hasAllDependenciesFullfilled,
|
|
@@ -61,6 +59,7 @@ import {
|
|
|
61
59
|
} from '../../../helpers/validations';
|
|
62
60
|
import { useQorusTypes } from '../../../hooks/useQorusTypes';
|
|
63
61
|
import { useTemplates } from '../../../hooks/useTemplates';
|
|
62
|
+
import { query } from '../../../utils/fetch';
|
|
64
63
|
import { Description } from '../../Description';
|
|
65
64
|
import { FocusedEditing } from '../../FocusedEditing';
|
|
66
65
|
import { SelectFormField } from '../fields/select/Select';
|
|
@@ -72,12 +71,6 @@ import {
|
|
|
72
71
|
} from '../fields/template/TemplateField';
|
|
73
72
|
import { CompactRow } from './CompactRow';
|
|
74
73
|
import { CompactRowContext, ICompactRowContext } from './compactRowContext';
|
|
75
|
-
import { CompactToolbar } from './CompactToolbar';
|
|
76
|
-
import {
|
|
77
|
-
CompactToolbarContext,
|
|
78
|
-
ICompactToolbarContext,
|
|
79
|
-
TCompactSort,
|
|
80
|
-
} from './compactToolbarContext';
|
|
81
74
|
import {
|
|
82
75
|
GROUP_INDENT,
|
|
83
76
|
LABEL_COL_MAX,
|
|
@@ -86,15 +79,30 @@ import {
|
|
|
86
79
|
StyledCompactPanel,
|
|
87
80
|
StyledGroupBody,
|
|
88
81
|
StyledGroupHeader,
|
|
89
|
-
|
|
82
|
+
StyledRequiredClusterBox,
|
|
83
|
+
StyledRequiredClusterHeader,
|
|
84
|
+
StyledStatusBox,
|
|
85
|
+
StyledStatusBoxGroupLabel,
|
|
90
86
|
} from './compactRowStyles';
|
|
87
|
+
import { CompactToolbar } from './CompactToolbar';
|
|
88
|
+
import {
|
|
89
|
+
CompactToolbarContext,
|
|
90
|
+
ICompactToolbarContext,
|
|
91
|
+
TCompactSort,
|
|
92
|
+
} from './compactToolbarContext';
|
|
91
93
|
import { OptionFieldMessages } from './OptionFieldMessages';
|
|
92
94
|
import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
93
95
|
import {
|
|
96
|
+
TReadFirstStatus,
|
|
97
|
+
getFirstAttentionOptionName,
|
|
94
98
|
getOptionGroup,
|
|
95
99
|
getOptionGroupLabel,
|
|
100
|
+
getReadFirstBucket,
|
|
96
101
|
getReadFirstCompletion,
|
|
102
|
+
getReadFirstStatus,
|
|
97
103
|
isOptionValueEmpty,
|
|
104
|
+
isFixedCompactAllowedValueOption,
|
|
105
|
+
shouldAutoCollapseCompactOption,
|
|
98
106
|
} from './readFirst';
|
|
99
107
|
|
|
100
108
|
// Re-export types for consumers
|
|
@@ -125,6 +133,47 @@ export interface IFormValidityData {
|
|
|
125
133
|
invalidFields: IFormFieldValidityData[];
|
|
126
134
|
}
|
|
127
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Resolve a field's `inherit_props` map against the current available
|
|
138
|
+
* options (plus any values inherited from an outer FormEngine scope),
|
|
139
|
+
* producing a `{ propName: siblingValue }` hash suitable for spreading
|
|
140
|
+
* onto the field's renderer.
|
|
141
|
+
*
|
|
142
|
+
* For each entry `<prop-name-on-renderer> -> <sibling-field-name>`, the
|
|
143
|
+
* sibling's current value is forwarded under the receiving prop name.
|
|
144
|
+
* Lookup order:
|
|
145
|
+
* 1. `availableOptions[siblingName]?.value` — the field's local scope.
|
|
146
|
+
* 2. `inheritedFromParent[siblingName]` — the bag threaded in by an
|
|
147
|
+
* outer FormEngine (see the `inheritedFromParent` prop). Used when
|
|
148
|
+
* a composite field's `arg_schema` sub-form needs to reach a value
|
|
149
|
+
* on an ancestor scope — e.g. a service-method row's `body` picking
|
|
150
|
+
* up `language` from the parent service form.
|
|
151
|
+
* Missing siblings emit `undefined`, which the renderer's prop type can
|
|
152
|
+
* treat as "no hint".
|
|
153
|
+
*
|
|
154
|
+
* Designed to be JSON-pure: no closures, no transformations. Renderers
|
|
155
|
+
* decide how to use the forwarded value (e.g. the consumer-injected
|
|
156
|
+
* `code-editor` renderer maps a `language: "qore"` prop to its
|
|
157
|
+
* highlighter mode).
|
|
158
|
+
*
|
|
159
|
+
* Mirrored in qorus-ide `src/components/Field/systemOptions.tsx`; keep
|
|
160
|
+
* the two in sync (see qorus-ide's `.claude/CLAUDE.md`).
|
|
161
|
+
*/
|
|
162
|
+
const resolveInheritProps = (
|
|
163
|
+
inheritProps: Record<string, string> | undefined,
|
|
164
|
+
availableOptions: TQorusForm | undefined,
|
|
165
|
+
inheritedFromParent?: Record<string, unknown>
|
|
166
|
+
): Record<string, unknown> => {
|
|
167
|
+
if (!inheritProps) return {};
|
|
168
|
+
const out: Record<string, unknown> = {};
|
|
169
|
+
for (const propName in inheritProps) {
|
|
170
|
+
const siblingName = inheritProps[propName];
|
|
171
|
+
const localValue = (availableOptions?.[siblingName] as IQorusFormField | undefined)?.value;
|
|
172
|
+
out[propName] = localValue !== undefined ? localValue : inheritedFromParent?.[siblingName];
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
};
|
|
176
|
+
|
|
128
177
|
const NegativeColorEffect: any = {
|
|
129
178
|
gradient: {
|
|
130
179
|
colors: { 0: 'danger', 100: 'danger:darken:2' },
|
|
@@ -153,13 +202,6 @@ const StyledCompactWrap = styled.div<{ $flush?: boolean }>`
|
|
|
153
202
|
engage instead of overflowing the container horizontally. */
|
|
154
203
|
min-width: 0;
|
|
155
204
|
max-width: 100%;
|
|
156
|
-
/* A bit of horizontal breathing room for the whole form (header + content
|
|
157
|
-
alike). Horizontal only — top padding would break the sticky toolbar's
|
|
158
|
-
flush pin (see the scroll-context note below). Dropped to flush via the
|
|
159
|
-
compactFlush prop, for embeds that own their own gutters (e.g. the
|
|
160
|
-
SchemaDefinition tab body). */
|
|
161
|
-
padding: ${({ $flush }) => ($flush ? '0' : '0 12px')};
|
|
162
|
-
|
|
163
205
|
/* Own our scroll context instead of borrowing the host's. The sticky toolbar
|
|
164
206
|
pins to whatever scrolls; if that scroller carries top padding (e.g. a
|
|
165
207
|
ReqorePanel/ReqoreContent body), sticky \`top: 0\` resolves against its
|
|
@@ -204,12 +246,16 @@ const StyledCommitDock = styled.div<{ $bg: string; $border: string }>`
|
|
|
204
246
|
`;
|
|
205
247
|
|
|
206
248
|
export const getType = (
|
|
207
|
-
type
|
|
249
|
+
type?: TQorusType | TQorusType[],
|
|
208
250
|
operators?: IOperatorsSchema,
|
|
209
251
|
operator?: TOperatorValue
|
|
210
252
|
): TQorusType => {
|
|
211
253
|
const finalType = getTypeFromOperator(operators, fixOperatorValue(operator)) || type;
|
|
212
|
-
|
|
254
|
+
const resolvedType = isArray(finalType) ? finalType[0] : finalType;
|
|
255
|
+
|
|
256
|
+
// A value can be received before its server-driven option schema. Keep the
|
|
257
|
+
// renderer type-safe during that transition without guessing a concrete type.
|
|
258
|
+
return (resolvedType || 'any') as TQorusType;
|
|
213
259
|
};
|
|
214
260
|
|
|
215
261
|
const getTypeFromOperator = (
|
|
@@ -226,6 +272,40 @@ export const fixOperatorValue = (operator: TOperatorValue): (string | null | und
|
|
|
226
272
|
return isArray(operator) ? operator : [operator];
|
|
227
273
|
};
|
|
228
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The renderer-only predicate. Defaults to reqraft's built-ins; a FormEngine
|
|
277
|
+
* instance passes its own (built-ins + the `rendererOnlyUiTypes` prop) so a
|
|
278
|
+
* consumer's injected editors are recognised too. See `./rendererTypes`.
|
|
279
|
+
*/
|
|
280
|
+
type TRendererOnlyCheck = (type?: TQorusType | TQorusType[]) => boolean;
|
|
281
|
+
|
|
282
|
+
const getOptionSchemaStorageType = (
|
|
283
|
+
option?: TQorusFormFieldSchema,
|
|
284
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
285
|
+
): TQorusType =>
|
|
286
|
+
((isFixedCompactAllowedValueOption(option)
|
|
287
|
+
? option?.type || option?.ui_type
|
|
288
|
+
: isRendererOnly(option?.ui_type)
|
|
289
|
+
? option?.type || option?.ui_type
|
|
290
|
+
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
291
|
+
|
|
292
|
+
const getOptionFieldStorageType = (
|
|
293
|
+
optionName: string,
|
|
294
|
+
fieldType: TQorusType | TQorusType[] | undefined,
|
|
295
|
+
schema?: IQorusFormSchema,
|
|
296
|
+
operators?: IOperatorsSchema,
|
|
297
|
+
operatorData?: TOperatorValue,
|
|
298
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
299
|
+
): TQorusType => {
|
|
300
|
+
const schemaOption = schema?.[optionName];
|
|
301
|
+
const storedType =
|
|
302
|
+
fieldType && !(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType))
|
|
303
|
+
? fieldType
|
|
304
|
+
: getOptionSchemaStorageType(schemaOption, isRendererOnly);
|
|
305
|
+
|
|
306
|
+
return getType(storedType as TQorusType, operators, operatorData);
|
|
307
|
+
};
|
|
308
|
+
|
|
229
309
|
export const hasRequiredOptions = (options: IQorusFormSchema = {}) => {
|
|
230
310
|
return !!findKey(options, (option) => option.required);
|
|
231
311
|
};
|
|
@@ -237,11 +317,19 @@ export const OptionsContext = createContext<{
|
|
|
237
317
|
|
|
238
318
|
export const fixOptions = (
|
|
239
319
|
value: TQorusForm | TQorusFlatForm = {},
|
|
240
|
-
options
|
|
241
|
-
operators?: IOperatorsSchema
|
|
320
|
+
options?: IQorusFormSchema,
|
|
321
|
+
operators?: IOperatorsSchema,
|
|
322
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
242
323
|
): TQorusForm => {
|
|
243
324
|
const fixedValue = cloneDeep(value);
|
|
244
325
|
|
|
326
|
+
// Server-driven schemas can arrive after the persisted value. Preserve that
|
|
327
|
+
// value verbatim until its schema is available instead of manufacturing
|
|
328
|
+
// partially typed form fields that can crash the following render.
|
|
329
|
+
if (!size(options)) {
|
|
330
|
+
return fixedValue as TQorusForm;
|
|
331
|
+
}
|
|
332
|
+
|
|
245
333
|
forEach(options, (option, name) => {
|
|
246
334
|
if (
|
|
247
335
|
option.value ||
|
|
@@ -251,7 +339,7 @@ export const fixOptions = (
|
|
|
251
339
|
) {
|
|
252
340
|
let obj: IQorusFormField;
|
|
253
341
|
const type = getType(
|
|
254
|
-
(option
|
|
342
|
+
getOptionSchemaStorageType(option, isRendererOnly),
|
|
255
343
|
operators,
|
|
256
344
|
(fixedValue[name] as IQorusFormField)?.op
|
|
257
345
|
);
|
|
@@ -287,20 +375,35 @@ export const fixOptions = (
|
|
|
287
375
|
let newOption = option as IQorusFormField;
|
|
288
376
|
|
|
289
377
|
if (!isPlainObject(newOption) || !(newOption as IQorusFormField)?.type) {
|
|
378
|
+
const isUntypedEnvelope = isPlainObject(newOption) && 'value' in newOption;
|
|
379
|
+
const untypedFieldValue =
|
|
380
|
+
isUntypedEnvelope ? (newOption as IQorusFormField).value : newOption;
|
|
381
|
+
// Spread the original envelope first so every key it carried survives —
|
|
382
|
+
// rebuilding from just {type, value} silently dropped `op`, `is_expression`
|
|
383
|
+
// and anything else a field needs to render, which emptied the card.
|
|
290
384
|
const fixedOption: IQorusFormField = {
|
|
385
|
+
...(isPlainObject(newOption) ? (newOption as IQorusFormField) : {}),
|
|
291
386
|
type: getType(
|
|
292
|
-
(options?.[optionName]
|
|
387
|
+
getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
|
|
293
388
|
operators,
|
|
294
389
|
(newOption as IQorusFormField)?.op
|
|
295
390
|
),
|
|
296
|
-
value:
|
|
391
|
+
value: untypedFieldValue,
|
|
297
392
|
};
|
|
298
393
|
|
|
299
|
-
if ((newOption as IQorusFormField)?.is_expression) {
|
|
300
|
-
fixedOption.is_expression = true;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
394
|
newOption = fixedOption;
|
|
395
|
+
} else if (
|
|
396
|
+
newOption.type === options?.[optionName]?.ui_type &&
|
|
397
|
+
isRendererOnly(newOption.type)
|
|
398
|
+
) {
|
|
399
|
+
newOption = {
|
|
400
|
+
...newOption,
|
|
401
|
+
type: getType(
|
|
402
|
+
getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
|
|
403
|
+
operators,
|
|
404
|
+
newOption.op
|
|
405
|
+
),
|
|
406
|
+
};
|
|
304
407
|
}
|
|
305
408
|
|
|
306
409
|
if (
|
|
@@ -370,7 +473,7 @@ export const flattenOptions = (options: TQorusForm): TQorusFlatForm => {
|
|
|
370
473
|
};
|
|
371
474
|
|
|
372
475
|
export const getTypeAndCanBeNull = (
|
|
373
|
-
type
|
|
476
|
+
type?: TQorusType | TQorusType[],
|
|
374
477
|
allowed_values?: any[],
|
|
375
478
|
operatorData?: TOperatorValue,
|
|
376
479
|
operators?: IOperatorsSchema
|
|
@@ -456,6 +559,11 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
456
559
|
/** Opt-in: fetch global templates from `system/getContextData` for this context. */
|
|
457
560
|
interfaceContext?: string;
|
|
458
561
|
templateFieldProps?: Partial<ITemplateFieldProps>;
|
|
562
|
+
/**
|
|
563
|
+
* Force single-value selectors to use inline dropdowns even when their
|
|
564
|
+
* allowed values carry descriptions.
|
|
565
|
+
*/
|
|
566
|
+
forceDropdown?: boolean;
|
|
459
567
|
showTypeToggle?: boolean;
|
|
460
568
|
/**
|
|
461
569
|
* Compact (read-first) mode: each option renders as a row with its formatted
|
|
@@ -469,6 +577,26 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
469
577
|
* form should line up with the section description above it. Default `false`.
|
|
470
578
|
*/
|
|
471
579
|
compactFlush?: boolean;
|
|
580
|
+
/**
|
|
581
|
+
* Compact mode only: this form is an EMBEDDED sub-form (e.g. an arg_schema
|
|
582
|
+
* field's nested form) rather than the top-level scroller. It doesn't own a
|
|
583
|
+
* scroll context, so the toolbar isn't sticky and its header drops the dark
|
|
584
|
+
* blurred backdrop (and the stacking context that goes with it) — it sits
|
|
585
|
+
* transparently inside the parent's edit card. Default `false`.
|
|
586
|
+
*/
|
|
587
|
+
compactNested?: boolean;
|
|
588
|
+
/**
|
|
589
|
+
* Compact mode only: props forwarded to the read-first form's outer
|
|
590
|
+
* `ReqorePanel` — the panel that carries the sticky toolbar header and holds
|
|
591
|
+
* the status boxes. Spread AFTER the engine's own defaults, so anything set
|
|
592
|
+
* here wins (`flat`, `raised`, `minimal`, `stickyHeader`, `label`, `icon`,
|
|
593
|
+
* `intent`, `padded`, `size`, …). Two props merge instead of replacing, so a
|
|
594
|
+
* consumer can add to the panel without dismantling the form:
|
|
595
|
+
* - `actions` — appended after the engine's toolbar action.
|
|
596
|
+
* - `contentStyle` — merged over the engine's flex-column layout.
|
|
597
|
+
* No-op in classic (non-compact) mode.
|
|
598
|
+
*/
|
|
599
|
+
compactPanelProps?: IReqorePanelProps;
|
|
472
600
|
/** Compact mode only: per-group display metadata (label / icon / subtitle /
|
|
473
601
|
* order) — the server only sends the bare group key. */
|
|
474
602
|
groups?: Record<string, IFormEngineGroup>;
|
|
@@ -485,19 +613,84 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
485
613
|
* option's name/schema/value (the context the IDE's `AiAssistanceAction`
|
|
486
614
|
* captures); the consumer injects the button, reqraft stays AI-free.
|
|
487
615
|
*/
|
|
488
|
-
optionActions?:
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
616
|
+
optionActions?: TOptionActions;
|
|
617
|
+
/**
|
|
618
|
+
* Whether per-option injected actions collapse into the row's overflow menu
|
|
619
|
+
* instead of rendering as inline buttons.
|
|
620
|
+
*
|
|
621
|
+
* `'auto'` (default) collapses when the device cannot hover or the viewport is
|
|
622
|
+
* narrow — on a phone a hover-gated action would otherwise be unreachable, and
|
|
623
|
+
* a row has no width for a button strip. `'always'` / `'never'` force it, which
|
|
624
|
+
* consumers can use for a known-mobile surface and stories use to capture the
|
|
625
|
+
* collapsed state deterministically.
|
|
626
|
+
*
|
|
627
|
+
* Actions beyond `MAX_INLINE_OPTION_ACTIONS` always overflow into the menu, so
|
|
628
|
+
* a consumer injecting many actions can never blow out the row.
|
|
629
|
+
*/
|
|
630
|
+
optionActionsCollapse?: 'auto' | 'always' | 'never';
|
|
495
631
|
/**
|
|
496
632
|
* SEAM (reqraft): consumer-injected field editors for types reqraft doesn't
|
|
497
633
|
* ship (IDE domain fields). Keyed by field `type`/`ui_type`; forwarded through
|
|
498
634
|
* `TemplateField` to the `AutoFormField` override seam.
|
|
499
635
|
*/
|
|
500
636
|
componentOverrides?: Record<string, React.FC<any>>;
|
|
637
|
+
/**
|
|
638
|
+
* The `ui_type` names among `componentOverrides` that select a bespoke editor
|
|
639
|
+
* but store their value as the schema's plainer `type` (a `cron` editor stores
|
|
640
|
+
* a string). Declaring them here keeps the field's stored `type` correct —
|
|
641
|
+
* without it the renderer name is written into the value and validation and
|
|
642
|
+
* round-tripping both break. Merged with reqraft's own built-in list, so a
|
|
643
|
+
* consumer's new editor no longer needs a reqraft release to be handled.
|
|
644
|
+
*/
|
|
645
|
+
rendererOnlyUiTypes?: string[];
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Bag of values forwarded from an outer FormEngine scope, used as a
|
|
649
|
+
* fallback when a field's `inherit_props` names a sibling that isn't in
|
|
650
|
+
* this form's own `availableOptions`. Populated automatically when
|
|
651
|
+
* FormEngine renders a nested `arg_schema` sub-form (through
|
|
652
|
+
* AutoFormField's hash / list mount sites) so that each level accumulates
|
|
653
|
+
* its ancestors' inherited props. Consumers rarely set this by hand — it's
|
|
654
|
+
* plumbing for the inherit_props scope-forwarding contract.
|
|
655
|
+
*/
|
|
656
|
+
inheritedFromParent?: Record<string, unknown>;
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Opt-in: on mount, drop the user straight into the first field they must
|
|
660
|
+
* fill — the first empty, focusable field (in schema/sort order) that is
|
|
661
|
+
* `required` or a member of a still-unsatisfied one-of `required_groups`
|
|
662
|
+
* group. Disabled, readonly, read-only-form, and dependency-locked fields are
|
|
663
|
+
* skipped. In compact (read-first) mode the target row is expanded through
|
|
664
|
+
* the engine's own `expandedOptions` state, so its editor gains focus with no
|
|
665
|
+
* DOM scraping or synthetic clicks. Strictly one-shot: it fires the first
|
|
666
|
+
* time the form has focusable content (so an async-loaded schema is covered)
|
|
667
|
+
* and then never again — a later value edit, server-driven field update, or
|
|
668
|
+
* in-place schema reload will not re-focus; only a remount re-arms it. It also
|
|
669
|
+
* never steals focus from a control the user has already moved into. No-op in
|
|
670
|
+
* classic (non-compact) mode. Default: off.
|
|
671
|
+
*/
|
|
672
|
+
autoFocusFirstRequired?: boolean;
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Names of read-first rows to open on mount, in addition to whatever the
|
|
676
|
+
* user opens afterwards.
|
|
677
|
+
*
|
|
678
|
+
* Which rows are expanded is otherwise private to the engine, which is
|
|
679
|
+
* fine while expansion is only ever a click. It stops being fine when the
|
|
680
|
+
* expanded row is part of an address: a consumer whose field renders its
|
|
681
|
+
* own routable surface (a table whose rows open panes with their own URLs)
|
|
682
|
+
* can restore the pane from the URL, but not the row that has to be open
|
|
683
|
+
* for the pane to exist at all — so a pasted or reloaded link lands on a
|
|
684
|
+
* collapsed form.
|
|
685
|
+
*
|
|
686
|
+
* Applied once, on the first render where the schema has rows, so an
|
|
687
|
+
* async-loaded schema is covered. It never re-expands a row the user has
|
|
688
|
+
* since collapsed, and it does not participate in `expandMode: 'single'`
|
|
689
|
+
* accordion collapsing — the caller is naming a starting point, not
|
|
690
|
+
* driving the state. Only a remount re-arms it. No-op in classic
|
|
691
|
+
* (non-compact) mode.
|
|
692
|
+
*/
|
|
693
|
+
initialExpandedOptions?: string[];
|
|
501
694
|
}
|
|
502
695
|
|
|
503
696
|
// Option types rendered full-width (IDE Options parity, commit 8e6b7781).
|
|
@@ -522,9 +715,12 @@ export const FormEngine = ({
|
|
|
522
715
|
allowTemplates = true,
|
|
523
716
|
interfaceContext,
|
|
524
717
|
templateFieldProps,
|
|
718
|
+
forceDropdown,
|
|
525
719
|
showTypeToggle = true,
|
|
526
720
|
compact,
|
|
527
721
|
compactFlush = false,
|
|
722
|
+
compactNested = false,
|
|
723
|
+
compactPanelProps,
|
|
528
724
|
commitMode = 'immediate',
|
|
529
725
|
expandMode = 'single',
|
|
530
726
|
onCommit,
|
|
@@ -533,9 +729,35 @@ export const FormEngine = ({
|
|
|
533
729
|
optionsLoader,
|
|
534
730
|
onValidityChange,
|
|
535
731
|
optionActions,
|
|
732
|
+
optionActionsCollapse = 'auto',
|
|
536
733
|
componentOverrides,
|
|
734
|
+
rendererOnlyUiTypes,
|
|
735
|
+
inheritedFromParent,
|
|
736
|
+
autoFocusFirstRequired,
|
|
737
|
+
initialExpandedOptions,
|
|
537
738
|
...rest
|
|
538
739
|
}: IFormEngineProps) => {
|
|
740
|
+
// Built-ins + whatever the consumer declared for its own injected editors.
|
|
741
|
+
const isRendererOnly = useMemo(
|
|
742
|
+
() => createRendererOnlyUiTypeCheck(rendererOnlyUiTypes),
|
|
743
|
+
[JSON.stringify(rendererOnlyUiTypes)]
|
|
744
|
+
);
|
|
745
|
+
// Pointer CAPABILITY and viewport WIDTH are different questions and both
|
|
746
|
+
// matter here: a hover-gated action is unreachable without a hover, and a
|
|
747
|
+
// phone-width row has no space for a button strip either way. Both now come
|
|
748
|
+
// from Reqore's context (one subscription for the whole app) rather than a
|
|
749
|
+
// reqraft-local matchMedia hook.
|
|
750
|
+
//
|
|
751
|
+
// `isHoverCapable` defaults to `true` in Reqore, but fall back explicitly so
|
|
752
|
+
// an older Reqore — where the property does not exist — degrades to "this
|
|
753
|
+
// pointer hovers" (the pre-existing behaviour) instead of collapsing every
|
|
754
|
+
// action into a menu for everyone.
|
|
755
|
+
const isHoverCapable = useReqoreProperty('isHoverCapable') ?? true;
|
|
756
|
+
const isMobile = useReqoreProperty('isMobile');
|
|
757
|
+
const collapseOptionActions =
|
|
758
|
+
optionActionsCollapse === 'always' ? true
|
|
759
|
+
: optionActionsCollapse === 'never' ? false
|
|
760
|
+
: !isHoverCapable || !!isMobile;
|
|
539
761
|
const [options, setOptions] = useState<IQorusFormSchema | undefined>(rest?.options || undefined);
|
|
540
762
|
// optionsLoader lifecycle: loading feeds the skeleton gate, error the banner.
|
|
541
763
|
const [optionsLoading, setOptionsLoading] = useState<boolean>(!!optionsLoader && !rest?.options);
|
|
@@ -560,6 +782,10 @@ export const FormEngine = ({
|
|
|
560
782
|
const [showInvalidOptionsOnly, setShowInvalidOptionsOnly] = useState<boolean>(false);
|
|
561
783
|
// Which options are expanded into their editor (several can be open at once).
|
|
562
784
|
const [expandedOptions, setExpandedOptions] = useState<string[]>([]);
|
|
785
|
+
// Remembers each row's last settled status box, so an actively-edited field
|
|
786
|
+
// stays put when its status flips (e.g. becomes valid) instead of jumping to
|
|
787
|
+
// another box mid-edit and stealing focus. Keyed by option name.
|
|
788
|
+
const settledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
|
|
563
789
|
// Measured form width (not viewport — the form lives in drawers/panels of
|
|
564
790
|
// arbitrary width) drives the stacked narrow layout.
|
|
565
791
|
const [compactWrapRef, { width: compactWrapWidth }] = useMeasure<HTMLDivElement>();
|
|
@@ -614,9 +840,18 @@ export const FormEngine = ({
|
|
|
614
840
|
const flashTimeout = useRef<ReturnType<typeof setTimeout>>();
|
|
615
841
|
const flashOptions = useCallback((optionNames: string[], scrollToFirst = false) => {
|
|
616
842
|
if (scrollToFirst && optionNames[0]) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
843
|
+
// Defer to the next frame: when this fires for a field that just changed
|
|
844
|
+
// panels, its row has only just re-mounted in the new box — scrolling in the
|
|
845
|
+
// same tick targets the stale (pre-move) layout, so the page doesn't budge.
|
|
846
|
+
// A rAF lets the new position settle first.
|
|
847
|
+
requestAnimationFrame(() => {
|
|
848
|
+
const target = document.querySelector<HTMLElement>(
|
|
849
|
+
`.readfirst-row[data-field="${optionNames[0]}"]`
|
|
850
|
+
);
|
|
851
|
+
if (typeof target?.scrollIntoView === 'function') {
|
|
852
|
+
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
853
|
+
}
|
|
854
|
+
});
|
|
620
855
|
}
|
|
621
856
|
setFlashedOptions(optionNames);
|
|
622
857
|
clearTimeout(flashTimeout.current);
|
|
@@ -627,6 +862,28 @@ export const FormEngine = ({
|
|
|
627
862
|
[flashOptions]
|
|
628
863
|
);
|
|
629
864
|
useEffect(() => () => clearTimeout(flashTimeout.current), []);
|
|
865
|
+
|
|
866
|
+
// Follow a field across panels: when its status bucket changes — e.g. you fill
|
|
867
|
+
// an optional field and it jumps to Set / Needs attention — scroll to its new
|
|
868
|
+
// row and flash it so it's easy to keep track of. `settledBucket` holds each
|
|
869
|
+
// field's current panel (frozen while the field is being edited, it re-buckets
|
|
870
|
+
// on collapse), so diffing it after every render catches the move the instant it
|
|
871
|
+
// lands in the new panel. Runs every render; the diff is cheap and only fires a
|
|
872
|
+
// scroll on an ACTUAL move of a non-expanded field.
|
|
873
|
+
const prevSettledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
|
|
874
|
+
useEffect(() => {
|
|
875
|
+
if (!compact) return;
|
|
876
|
+
const cur = settledBucket.current;
|
|
877
|
+
const prev = prevSettledBucket.current;
|
|
878
|
+
const moved = Object.keys(cur).find(
|
|
879
|
+
(name) => prev[name] && prev[name] !== cur[name] && !expandedOptions.includes(name)
|
|
880
|
+
);
|
|
881
|
+
prevSettledBucket.current = { ...cur };
|
|
882
|
+
if (moved) {
|
|
883
|
+
flashOptions([moved], true);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
630
887
|
const compactNarrow = !!compactWrapWidth && compactWrapWidth < 480;
|
|
631
888
|
// Info panels auto-open on Tier-1 content; the per-row user override sticks.
|
|
632
889
|
const [infoPanelOverrides, setInfoPanelOverrides] = useState<Record<string, boolean>>({});
|
|
@@ -639,7 +896,7 @@ export const FormEngine = ({
|
|
|
639
896
|
fields: TQorusForm | TQorusFlatForm;
|
|
640
897
|
meta?: IOptionsOnChangeMeta;
|
|
641
898
|
}>(() => ({
|
|
642
|
-
fields: fixOptions(value, options || {}),
|
|
899
|
+
fields: fixOptions(value, options || {}, undefined, isRendererOnly),
|
|
643
900
|
meta: undefined,
|
|
644
901
|
}));
|
|
645
902
|
const originalValue = useRef<any>();
|
|
@@ -732,7 +989,7 @@ export const FormEngine = ({
|
|
|
732
989
|
setOptions({});
|
|
733
990
|
return;
|
|
734
991
|
}
|
|
735
|
-
setLocalValue({ fields: fixOptions(value, data.data), meta: undefined });
|
|
992
|
+
setLocalValue({ fields: fixOptions(value, data.data, undefined, isRendererOnly), meta: undefined });
|
|
736
993
|
if (!operatorsUrl) {
|
|
737
994
|
setLoading(false);
|
|
738
995
|
}
|
|
@@ -777,7 +1034,7 @@ export const FormEngine = ({
|
|
|
777
1034
|
}
|
|
778
1035
|
setOptions(data.data);
|
|
779
1036
|
onOptionsLoaded?.(data.data);
|
|
780
|
-
setLocalValue({ fields: fixOptions({}, data.data), meta: undefined });
|
|
1037
|
+
setLocalValue({ fields: fixOptions({}, data.data, undefined, isRendererOnly), meta: undefined });
|
|
781
1038
|
})();
|
|
782
1039
|
}
|
|
783
1040
|
}, [url, customUrl]);
|
|
@@ -801,7 +1058,7 @@ export const FormEngine = ({
|
|
|
801
1058
|
}, [operatorsUrl]);
|
|
802
1059
|
|
|
803
1060
|
useUpdateEffect(() => {
|
|
804
|
-
const fixedValue = fixOptions(value, options || {});
|
|
1061
|
+
const fixedValue = fixOptions(value, options || {}, undefined, isRendererOnly);
|
|
805
1062
|
|
|
806
1063
|
// When the value we're receiving is the one we just emitted AND fixOptions has nothing to
|
|
807
1064
|
// meaningfully add or change, skip the update. This breaks the controlled-component loop for
|
|
@@ -825,19 +1082,21 @@ export const FormEngine = ({
|
|
|
825
1082
|
}
|
|
826
1083
|
|
|
827
1084
|
setLocalValue?.({ fields: fixedValue, meta: undefined });
|
|
828
|
-
}, [JSON.stringify(options), JSON.stringify(value)]);
|
|
1085
|
+
}, [JSON.stringify(options), JSON.stringify(value), isRendererOnly]);
|
|
829
1086
|
|
|
830
1087
|
const handleValueChange = useCallback(
|
|
831
1088
|
(optionName: string, val?: any, _type?: string, isFunction?: boolean) => {
|
|
832
1089
|
setLocalValue(({ fields = {} }) => {
|
|
833
|
-
const schemaType = (options?.[optionName]
|
|
834
|
-
options?.[optionName]?.type) as TQorusType;
|
|
1090
|
+
const schemaType = getOptionSchemaStorageType(options?.[optionName], isRendererOnly);
|
|
835
1091
|
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
836
1092
|
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
837
1093
|
const resolvedSchemaType =
|
|
838
|
-
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
839
|
-
((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
840
|
-
|
|
1094
|
+
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
1095
|
+
? ((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
1096
|
+
: (fields[optionName] as IQorusFormField)?.type === options?.[optionName]?.ui_type &&
|
|
1097
|
+
isRendererOnly((fields[optionName] as IQorusFormField)?.type)
|
|
1098
|
+
? schemaType
|
|
1099
|
+
: schemaType || ((fields[optionName] as IQorusFormField)?.type as TQorusType);
|
|
841
1100
|
const type =
|
|
842
1101
|
_type ||
|
|
843
1102
|
getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
|
|
@@ -911,6 +1170,14 @@ export const FormEngine = ({
|
|
|
911
1170
|
|
|
912
1171
|
onSingleOptionsChange?.(optionName, updatedValue[optionName]);
|
|
913
1172
|
|
|
1173
|
+
if (
|
|
1174
|
+
compact &&
|
|
1175
|
+
!readOnly &&
|
|
1176
|
+
shouldAutoCollapseCompactOption(options?.[optionName], val)
|
|
1177
|
+
) {
|
|
1178
|
+
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
914
1181
|
return {
|
|
915
1182
|
fields: updatedValue,
|
|
916
1183
|
meta,
|
|
@@ -920,6 +1187,9 @@ export const FormEngine = ({
|
|
|
920
1187
|
[
|
|
921
1188
|
onSingleOptionsChange,
|
|
922
1189
|
onDependableOptionChange,
|
|
1190
|
+
isRendererOnly,
|
|
1191
|
+
compact,
|
|
1192
|
+
readOnly,
|
|
923
1193
|
JSON.stringify(options),
|
|
924
1194
|
JSON.stringify(operators),
|
|
925
1195
|
]
|
|
@@ -1028,6 +1298,10 @@ export const FormEngine = ({
|
|
|
1028
1298
|
meta: undefined,
|
|
1029
1299
|
};
|
|
1030
1300
|
});
|
|
1301
|
+
// Collapse it too: a removed field drops back to the (collapsed) Optional box
|
|
1302
|
+
// as a quiet addable row — if it was being edited, that editor must close
|
|
1303
|
+
// rather than linger as an open editor for a field that's no longer added.
|
|
1304
|
+
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
1031
1305
|
}, []);
|
|
1032
1306
|
|
|
1033
1307
|
const handleAddOptionalFieldChange = useCallback(
|
|
@@ -1036,8 +1310,7 @@ export const FormEngine = ({
|
|
|
1036
1310
|
optionName as string,
|
|
1037
1311
|
getDefaultValue(options?.[optionName as string]),
|
|
1038
1312
|
getTypeAndCanBeNull(
|
|
1039
|
-
(options?.[optionName as string]
|
|
1040
|
-
options?.[optionName as string]?.type) as TQorusType,
|
|
1313
|
+
getOptionSchemaStorageType(options?.[optionName as string], isRendererOnly),
|
|
1041
1314
|
options?.[optionName as string]?.allowed_values
|
|
1042
1315
|
).type
|
|
1043
1316
|
);
|
|
@@ -1065,36 +1338,37 @@ export const FormEngine = ({
|
|
|
1065
1338
|
return newValue;
|
|
1066
1339
|
}
|
|
1067
1340
|
|
|
1068
|
-
const
|
|
1341
|
+
const rendererType = getType(
|
|
1069
1342
|
(options[optionName].ui_type || options[optionName].type) as TQorusType,
|
|
1070
1343
|
operators,
|
|
1071
1344
|
(option as IQorusFormField)?.op
|
|
1072
1345
|
);
|
|
1346
|
+
const storageType = getOptionFieldStorageType(
|
|
1347
|
+
optionName,
|
|
1348
|
+
(option as IQorusFormField)?.type,
|
|
1349
|
+
options,
|
|
1350
|
+
operators,
|
|
1351
|
+
(option as IQorusFormField)?.op,
|
|
1352
|
+
isRendererOnly
|
|
1353
|
+
);
|
|
1073
1354
|
|
|
1074
1355
|
if (!isPlainObject(option)) {
|
|
1075
1356
|
return {
|
|
1076
1357
|
...newValue,
|
|
1077
1358
|
[optionName]: {
|
|
1078
|
-
type:
|
|
1359
|
+
type: storageType || rendererType,
|
|
1079
1360
|
value: option,
|
|
1080
1361
|
},
|
|
1081
1362
|
};
|
|
1082
1363
|
}
|
|
1083
1364
|
|
|
1084
|
-
// any/auto: preserve the user-picked type; otherwise normalize to the
|
|
1085
|
-
// schema type (ui_type wins) so rendering and validation agree.
|
|
1086
|
-
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
1087
|
-
const effectiveType =
|
|
1088
|
-
isAnyLike && (option as IQorusFormField)?.type ?
|
|
1089
|
-
(option as IQorusFormField).type
|
|
1090
|
-
: schemaType;
|
|
1091
|
-
|
|
1092
1365
|
return {
|
|
1093
1366
|
...newValue,
|
|
1094
|
-
[optionName]: { ...(option as IQorusFormField), type:
|
|
1367
|
+
[optionName]: { ...(option as IQorusFormField), type: storageType || rendererType },
|
|
1095
1368
|
};
|
|
1096
1369
|
}, {});
|
|
1097
1370
|
}, [
|
|
1371
|
+
isRendererOnly,
|
|
1098
1372
|
JSON.stringify(fixedValue),
|
|
1099
1373
|
JSON.stringify(options),
|
|
1100
1374
|
unavailableOptionsCount.current,
|
|
@@ -1191,10 +1465,14 @@ export const FormEngine = ({
|
|
|
1191
1465
|
const fields: IFormFieldValidityData[] = reduce(
|
|
1192
1466
|
availableOptions,
|
|
1193
1467
|
(result, option, optionName) => {
|
|
1194
|
-
const type =
|
|
1195
|
-
|
|
1196
|
-
(
|
|
1197
|
-
|
|
1468
|
+
const type = getOptionFieldStorageType(
|
|
1469
|
+
optionName,
|
|
1470
|
+
(option as IQorusFormField).type,
|
|
1471
|
+
options,
|
|
1472
|
+
operators,
|
|
1473
|
+
(option as IQorusFormField).op,
|
|
1474
|
+
isRendererOnly
|
|
1475
|
+
);
|
|
1198
1476
|
const optionValue = (option as IQorusFormField).value;
|
|
1199
1477
|
|
|
1200
1478
|
const isRequired = options?.[optionName]?.required;
|
|
@@ -1236,8 +1514,10 @@ export const FormEngine = ({
|
|
|
1236
1514
|
invalidFields,
|
|
1237
1515
|
};
|
|
1238
1516
|
}, [
|
|
1517
|
+
isRendererOnly,
|
|
1239
1518
|
JSON.stringify(availableOptions),
|
|
1240
1519
|
JSON.stringify(options),
|
|
1520
|
+
JSON.stringify(operators),
|
|
1241
1521
|
JSON.stringify(localValue.fields),
|
|
1242
1522
|
]);
|
|
1243
1523
|
|
|
@@ -1256,8 +1536,14 @@ export const FormEngine = ({
|
|
|
1256
1536
|
showInvalidOptionsOnly &&
|
|
1257
1537
|
isOptionValid(
|
|
1258
1538
|
optionName,
|
|
1259
|
-
(
|
|
1260
|
-
|
|
1539
|
+
getOptionFieldStorageType(
|
|
1540
|
+
optionName,
|
|
1541
|
+
(option as IQorusFormField).type,
|
|
1542
|
+
options,
|
|
1543
|
+
operators,
|
|
1544
|
+
(option as IQorusFormField).op,
|
|
1545
|
+
isRendererOnly
|
|
1546
|
+
),
|
|
1261
1547
|
(option as IQorusFormField).value
|
|
1262
1548
|
)
|
|
1263
1549
|
) {
|
|
@@ -1267,7 +1553,89 @@ export const FormEngine = ({
|
|
|
1267
1553
|
},
|
|
1268
1554
|
{}
|
|
1269
1555
|
);
|
|
1270
|
-
}, [
|
|
1556
|
+
}, [
|
|
1557
|
+
showInvalidOptionsOnly,
|
|
1558
|
+
isRendererOnly,
|
|
1559
|
+
JSON.stringify(availableOptions),
|
|
1560
|
+
JSON.stringify(options),
|
|
1561
|
+
JSON.stringify(operators),
|
|
1562
|
+
]);
|
|
1563
|
+
|
|
1564
|
+
// Read-first STATUS / BOX for one option — lifted to component scope so the
|
|
1565
|
+
// status boxes (renderCompact) and the header's "needs attention" count share
|
|
1566
|
+
// exactly one definition. One-of group members travel together (bucket by the
|
|
1567
|
+
// group's satisfaction); everything else by its own status.
|
|
1568
|
+
const schemaMsgIntent = useCallback(
|
|
1569
|
+
(name: string): 'danger' | 'warning' | undefined => {
|
|
1570
|
+
const msgs = ((options?.[name] as { messages?: Array<{ intent?: string }> } | undefined)
|
|
1571
|
+
?.messages || []) as Array<{ intent?: string }>;
|
|
1572
|
+
if (msgs.some((m) => m.intent === 'danger')) return 'danger';
|
|
1573
|
+
if (msgs.some((m) => m.intent === 'warning')) return 'warning';
|
|
1574
|
+
return undefined;
|
|
1575
|
+
},
|
|
1576
|
+
[JSON.stringify(options)]
|
|
1577
|
+
);
|
|
1578
|
+
const getOptionStatus = useCallback(
|
|
1579
|
+
(name: string, hidden = false): TReadFirstStatus => {
|
|
1580
|
+
if (hidden) return 'optional';
|
|
1581
|
+
const schema = options?.[name];
|
|
1582
|
+
const type = getOptionFieldStorageType(
|
|
1583
|
+
name,
|
|
1584
|
+
(availableOptions as TQorusForm)?.[name]?.type,
|
|
1585
|
+
options,
|
|
1586
|
+
operators,
|
|
1587
|
+
(availableOptions as TQorusForm)?.[name]?.op,
|
|
1588
|
+
isRendererOnly
|
|
1589
|
+
);
|
|
1590
|
+
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1591
|
+
const empty = isOptionValueEmpty(value);
|
|
1592
|
+
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
1593
|
+
const required = !!(schema?.required || reqGroups.length);
|
|
1594
|
+
const covered =
|
|
1595
|
+
empty &&
|
|
1596
|
+
reqGroups.some((g) => {
|
|
1597
|
+
const by = requiredGroupsInfo.satisfiedBy[g];
|
|
1598
|
+
return !!by && by !== name;
|
|
1599
|
+
});
|
|
1600
|
+
const msgIntent = schemaMsgIntent(name);
|
|
1601
|
+
const invalid = (!empty && !isOptionValid(name, type, value)) || msgIntent === 'danger';
|
|
1602
|
+
return getReadFirstStatus({
|
|
1603
|
+
empty,
|
|
1604
|
+
required,
|
|
1605
|
+
covered,
|
|
1606
|
+
invalid,
|
|
1607
|
+
warned: msgIntent === 'warning',
|
|
1608
|
+
});
|
|
1609
|
+
},
|
|
1610
|
+
[
|
|
1611
|
+
isRendererOnly,
|
|
1612
|
+
JSON.stringify(options),
|
|
1613
|
+
JSON.stringify(availableOptions),
|
|
1614
|
+
JSON.stringify(operators),
|
|
1615
|
+
isOptionValid,
|
|
1616
|
+
requiredGroupsInfo,
|
|
1617
|
+
schemaMsgIntent,
|
|
1618
|
+
]
|
|
1619
|
+
);
|
|
1620
|
+
const getOptionBucket = useCallback(
|
|
1621
|
+
(name: string, hidden = false): 'attention' | 'set' | 'optional' => {
|
|
1622
|
+
if (!hidden) {
|
|
1623
|
+
const reqGroups = (options?.[name]?.required_groups as string[] | undefined) || [];
|
|
1624
|
+
if (reqGroups.length) {
|
|
1625
|
+
return reqGroups.some((g) => !requiredGroupsInfo.satisfiedBy[g]) ? 'attention' : 'set';
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return getReadFirstBucket(getOptionStatus(name, hidden));
|
|
1629
|
+
},
|
|
1630
|
+
[JSON.stringify(options), requiredGroupsInfo, getOptionStatus]
|
|
1631
|
+
);
|
|
1632
|
+
// How many fields are in the "Needs attention" box — drives the header link.
|
|
1633
|
+
const readFirstAttentionCount = useMemo(
|
|
1634
|
+
() =>
|
|
1635
|
+
Object.keys(availableOptions || {}).filter((name) => getOptionBucket(name) === 'attention')
|
|
1636
|
+
.length,
|
|
1637
|
+
[JSON.stringify(availableOptions), getOptionBucket]
|
|
1638
|
+
);
|
|
1271
1639
|
|
|
1272
1640
|
const getIntent = useCallback(
|
|
1273
1641
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -1346,6 +1714,120 @@ export const FormEngine = ({
|
|
|
1346
1714
|
[expandMode]
|
|
1347
1715
|
);
|
|
1348
1716
|
|
|
1717
|
+
// --- Caller-named starting rows (opt-in) ----------------------------------
|
|
1718
|
+
// `initialExpandedOptions` lets a consumer name the rows that must already
|
|
1719
|
+
// be open when the form first paints — the case where the expanded row is
|
|
1720
|
+
// part of an address rather than a click (see the prop's docs).
|
|
1721
|
+
//
|
|
1722
|
+
// One-shot for the same reason autofocus is: it fires on the first render
|
|
1723
|
+
// that actually has rows (so an async-loaded schema is covered) and then
|
|
1724
|
+
// never again, so a row the user collapses afterwards stays collapsed and a
|
|
1725
|
+
// later schema reload does not reopen it.
|
|
1726
|
+
const hasAppliedInitialExpansionRef = useRef(false);
|
|
1727
|
+
useEffect(() => {
|
|
1728
|
+
if (!initialExpandedOptions?.length || !compact || hasAppliedInitialExpansionRef.current) {
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
const names = Object.keys(availableOptions);
|
|
1732
|
+
if (!names.length) {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
hasAppliedInitialExpansionRef.current = true;
|
|
1736
|
+
const toExpand = initialExpandedOptions.filter((name) => names.includes(name));
|
|
1737
|
+
if (!toExpand.length) {
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
setExpandedOptions((prev) => [...prev, ...toExpand.filter((name) => !prev.includes(name))]);
|
|
1741
|
+
}, [initialExpandedOptions, compact, availableOptions]);
|
|
1742
|
+
|
|
1743
|
+
// --- First-attention-field autofocus (opt-in) -----------------------------
|
|
1744
|
+
// With `autoFocusFirstRequired`, drop the user straight into the first field
|
|
1745
|
+
// they must fix. We reuse the engine's own ordering (`availableOptions`), the
|
|
1746
|
+
// very same `getOptionBucket` the status boxes use to decide "needs attention"
|
|
1747
|
+
// (so we cover empty-required, unsatisfied one-of, AND filled-but-invalid rows
|
|
1748
|
+
// without ever drifting from what the user sees), and dependency gating
|
|
1749
|
+
// (`dependencyLockedNames`), then expand the target row through
|
|
1750
|
+
// `expandedOptions` — the row's editor-focus effect does the rest. No DOM
|
|
1751
|
+
// scraping, no synthetic clicks, no polling.
|
|
1752
|
+
//
|
|
1753
|
+
// Strictly one-shot: it fires the first time the form has focusable content
|
|
1754
|
+
// (so an async-loaded schema is still covered — the empty first pass doesn't
|
|
1755
|
+
// count), then never again for the life of this instance. A server-driven
|
|
1756
|
+
// field update or an in-place schema reload will NOT re-grab focus; only a
|
|
1757
|
+
// genuine remount (a fresh instance) auto-focuses again, which is the intended
|
|
1758
|
+
// on-mount behaviour.
|
|
1759
|
+
const hasAutoFocusedRef = useRef(false);
|
|
1760
|
+
// The field expanded programmatically for autofocus. A ref (not state) so
|
|
1761
|
+
// CompactRow's 60ms focus timer reads the current value regardless of render
|
|
1762
|
+
// batching; CompactRow focuses this one with `preventScroll` so an off-screen
|
|
1763
|
+
// (or below-the-fold) form is never scrolled into view on mount.
|
|
1764
|
+
const autoFocusNameRef = useRef<string | undefined>(undefined);
|
|
1765
|
+
useEffect(() => {
|
|
1766
|
+
if (!autoFocusFirstRequired || !compact || !options) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (hasAutoFocusedRef.current) {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const orderedNames = Object.keys(availableOptions);
|
|
1774
|
+
if (!orderedNames.length) {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// Never grab focus from a control the user is already in — including a field
|
|
1779
|
+
// *inside* this form. If a slow/async schema lets the user start typing
|
|
1780
|
+
// before this first-required scan runs, we must not steal their caret. On a
|
|
1781
|
+
// clean mount focus rests on the body, so the intended "drop into the first
|
|
1782
|
+
// field" still fires. Bailing here leaves `hasAutoFocusedRef` false, so it
|
|
1783
|
+
// retries once focus is free.
|
|
1784
|
+
const active = document.activeElement as HTMLElement | null;
|
|
1785
|
+
if (active && active !== document.body) {
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
const target = getFirstAttentionOptionName(orderedNames, (fieldName) => {
|
|
1790
|
+
const schema = options[fieldName];
|
|
1791
|
+
if (!schema) {
|
|
1792
|
+
return undefined;
|
|
1793
|
+
}
|
|
1794
|
+
return {
|
|
1795
|
+
focusable:
|
|
1796
|
+
!readOnly &&
|
|
1797
|
+
!schema.disabled &&
|
|
1798
|
+
!(schema as { readonly?: boolean }).readonly &&
|
|
1799
|
+
!dependencyLockedNames.includes(fieldName),
|
|
1800
|
+
// Single source of truth: the same bucket the read-first status boxes
|
|
1801
|
+
// show, so a filled-but-invalid required row is a target too.
|
|
1802
|
+
needsAttention: getOptionBucket(fieldName) === 'attention',
|
|
1803
|
+
};
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
// Mark done even when nothing needs attention, so later value edits or an
|
|
1807
|
+
// in-place schema reload never re-scan or re-focus. Only a remount arms it
|
|
1808
|
+
// again.
|
|
1809
|
+
hasAutoFocusedRef.current = true;
|
|
1810
|
+
|
|
1811
|
+
if (target) {
|
|
1812
|
+
// Set the ref before the state update so CompactRow's focus timer sees it.
|
|
1813
|
+
autoFocusNameRef.current = target;
|
|
1814
|
+
setExpandedOptions((prev) =>
|
|
1815
|
+
prev.includes(target) ? prev
|
|
1816
|
+
: expandMode === 'multi' ? [...prev, target]
|
|
1817
|
+
: [target]
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
}, [
|
|
1821
|
+
autoFocusFirstRequired,
|
|
1822
|
+
compact,
|
|
1823
|
+
options,
|
|
1824
|
+
availableOptions,
|
|
1825
|
+
readOnly,
|
|
1826
|
+
dependencyLockedNames,
|
|
1827
|
+
getOptionBucket,
|
|
1828
|
+
expandMode,
|
|
1829
|
+
]);
|
|
1830
|
+
|
|
1349
1831
|
// Read-first completion summary (how many shown options have a value set),
|
|
1350
1832
|
// surfaced as a progress meter at the top of the compact form.
|
|
1351
1833
|
const readFirstCompletion = useMemo(
|
|
@@ -1395,10 +1877,10 @@ export const FormEngine = ({
|
|
|
1395
1877
|
next[optionName] = value as IQorusFormField;
|
|
1396
1878
|
}
|
|
1397
1879
|
});
|
|
1398
|
-
return { fields: fixOptions(next, options || {}, operators), meta: undefined };
|
|
1880
|
+
return { fields: fixOptions(next, options || {}, operators, isRendererOnly), meta: undefined };
|
|
1399
1881
|
});
|
|
1400
1882
|
setRequiredOnly(false);
|
|
1401
|
-
}, [JSON.stringify(options), JSON.stringify(operators)]);
|
|
1883
|
+
}, [JSON.stringify(options), JSON.stringify(operators), isRendererOnly]);
|
|
1402
1884
|
|
|
1403
1885
|
const getCustomMenuTemplateItems = useCallback<(optionName: string) => TCustomTemplateItems>(
|
|
1404
1886
|
(optionName) => {
|
|
@@ -1428,22 +1910,72 @@ export const FormEngine = ({
|
|
|
1428
1910
|
suppressSchemaMessages?: boolean
|
|
1429
1911
|
) => {
|
|
1430
1912
|
const operatorParts = fixOperatorValue(other.op);
|
|
1913
|
+
// The RENDERER type for this row — which editor mounts. Distinct from the
|
|
1914
|
+
// storage type on the value envelope: a `richtext` field stores a string
|
|
1915
|
+
// but must render the richtext editor, so `ui_type` wins here.
|
|
1916
|
+
//
|
|
1917
|
+
// The exception is an any-like `ui_type`. Those schemas say
|
|
1918
|
+
// `ui_type: 'any'` precisely so the user can pick a concrete type, and
|
|
1919
|
+
// that pick lives on the field's stored `type` — letting 'any' win would
|
|
1920
|
+
// pin the row to the untyped editor forever.
|
|
1921
|
+
//
|
|
1922
|
+
// The trailing fallbacks keep the row rendering when a server-driven
|
|
1923
|
+
// schema has not arrived yet (`type` undefined) instead of crashing.
|
|
1924
|
+
const optionSchema = options?.[optionName];
|
|
1925
|
+
const schemaUiType = optionSchema?.ui_type as TQorusType;
|
|
1926
|
+
const uiTypeIsAnyLike = schemaUiType === 'any' || schemaUiType === 'auto';
|
|
1927
|
+
const resolvedType =
|
|
1928
|
+
(isFixedCompactAllowedValueOption(optionSchema)
|
|
1929
|
+
? getOptionSchemaStorageType(optionSchema, isRendererOnly)
|
|
1930
|
+
: schemaUiType && !uiTypeIsAnyLike
|
|
1931
|
+
? schemaUiType
|
|
1932
|
+
: undefined) ||
|
|
1933
|
+
type ||
|
|
1934
|
+
schemaUiType ||
|
|
1935
|
+
(optionSchema?.type as TQorusType) ||
|
|
1936
|
+
'any';
|
|
1937
|
+
const effectiveForceDropdown =
|
|
1938
|
+
forceDropdown ??
|
|
1939
|
+
(templateFieldProps as { forceDropdown?: boolean } | undefined)?.forceDropdown;
|
|
1940
|
+
|
|
1431
1941
|
return (
|
|
1432
1942
|
<>
|
|
1433
|
-
{(
|
|
1434
|
-
|
|
1943
|
+
{(() => {
|
|
1944
|
+
const schemaMsgs = (
|
|
1945
|
+
suppressSchemaMessages ?
|
|
1946
|
+
[]
|
|
1947
|
+
: (options?.[optionName] as any)?.messages || []) as {
|
|
1948
|
+
intent?: string;
|
|
1949
|
+
title?: string;
|
|
1950
|
+
content?: string;
|
|
1951
|
+
}[];
|
|
1952
|
+
if (!schemaMsgs.length) return null;
|
|
1953
|
+
const items = schemaMsgs.map(({ intent, title, content }, index) => (
|
|
1435
1954
|
<ReqoreMessage
|
|
1436
|
-
intent={intent}
|
|
1955
|
+
intent={intent as never}
|
|
1437
1956
|
title={title}
|
|
1438
1957
|
key={title || index}
|
|
1439
1958
|
opaque={false}
|
|
1440
1959
|
size='small'
|
|
1441
|
-
|
|
1960
|
+
// Compact: flat (no border) to match the read-row info panels;
|
|
1961
|
+
// classic forms keep the bordered, bottom-margined message.
|
|
1962
|
+
flat={compact || undefined}
|
|
1963
|
+
margin={compact ? undefined : 'bottom'}
|
|
1442
1964
|
>
|
|
1443
1965
|
{content}
|
|
1444
1966
|
</ReqoreMessage>
|
|
1445
|
-
)
|
|
1446
|
-
|
|
1967
|
+
));
|
|
1968
|
+
// Compact: stack them in a 4px-gap panel so a field's messages look
|
|
1969
|
+
// identical whether the row is collapsed (read panel) or expanded.
|
|
1970
|
+
return compact ?
|
|
1971
|
+
<div
|
|
1972
|
+
className='options-readfirst-info-panel'
|
|
1973
|
+
style={{ display: 'flex', flexFlow: 'column', gap: 4, marginBottom: 8 }}
|
|
1974
|
+
>
|
|
1975
|
+
{items}
|
|
1976
|
+
</div>
|
|
1977
|
+
: <>{items}</>;
|
|
1978
|
+
})()}
|
|
1447
1979
|
{operators && size(operators) ?
|
|
1448
1980
|
<>
|
|
1449
1981
|
<ReqoreControlGroup fill wrap className='operators'>
|
|
@@ -1498,6 +2030,46 @@ export const FormEngine = ({
|
|
|
1498
2030
|
<TemplateField
|
|
1499
2031
|
fluid
|
|
1500
2032
|
{...(options?.[optionName] as any)}
|
|
2033
|
+
// qorus#347-followup: resolve the field's `inherit_props` against
|
|
2034
|
+
// the CURRENT sibling values (with a fallback to
|
|
2035
|
+
// `inheritedFromParent` — the bag threaded in from an outer
|
|
2036
|
+
// FormEngine when this scope is an `arg_schema` sub-form),
|
|
2037
|
+
// threading each entry as a top-level prop. Each
|
|
2038
|
+
// `<prop-name>: <sibling-field-name>` mapping copies the
|
|
2039
|
+
// sibling's value onto the rendered field's renderer — e.g. a
|
|
2040
|
+
// `code-editor` with `inherit_props: { language: 'language' }`
|
|
2041
|
+
// picks up the live `language` value as a `language` prop without
|
|
2042
|
+
// a schema refetch. Spread AFTER `{...options?.[optionName]}` so
|
|
2043
|
+
// the runtime value wins over any schema-defined default of the
|
|
2044
|
+
// same key. Mirrored in qorus-ide's `systemOptions.tsx`; see the
|
|
2045
|
+
// CLAUDE.md rule there.
|
|
2046
|
+
{...resolveInheritProps(
|
|
2047
|
+
options?.[optionName]?.inherit_props,
|
|
2048
|
+
availableOptions,
|
|
2049
|
+
inheritedFromParent
|
|
2050
|
+
)}
|
|
2051
|
+
// qorus#347-followup (scope forwarding): merge accumulated
|
|
2052
|
+
// inheritance (`inheritedFromParent`) with THIS field's freshly
|
|
2053
|
+
// resolved inherit_props, and pass down as a single bag so any
|
|
2054
|
+
// nested `arg_schema` sub-form (mounted by AutoFormField for
|
|
2055
|
+
// `hash` / `free-hash` / list-of-hash fields) sees every value
|
|
2056
|
+
// the ancestor chain forwarded. This is the plumbing that lets a
|
|
2057
|
+
// service-method row's `body` sub-field pick up the parent
|
|
2058
|
+
// service form's `language` — the parent field declares
|
|
2059
|
+
// `inherit_props: { language: 'language' }`, the list renderer
|
|
2060
|
+
// forwards it into each row, and the row's body resolves against
|
|
2061
|
+
// the accumulated bag.
|
|
2062
|
+
inheritedFromParent={{
|
|
2063
|
+
...inheritedFromParent,
|
|
2064
|
+
...resolveInheritProps(
|
|
2065
|
+
options?.[optionName]?.inherit_props,
|
|
2066
|
+
availableOptions,
|
|
2067
|
+
inheritedFromParent
|
|
2068
|
+
),
|
|
2069
|
+
}}
|
|
2070
|
+
// Propagate compact so an arg_schema field renders a COMPACT sub-form
|
|
2071
|
+
// (consistent with the parent) rather than the classic FormEngine.
|
|
2072
|
+
compact={compact}
|
|
1501
2073
|
// SEAM: forwarded through TemplateField's rest-spread to AutoFormField,
|
|
1502
2074
|
// which renders consumer-injected editors by field type/ui_type.
|
|
1503
2075
|
componentOverrides={componentOverrides}
|
|
@@ -1506,16 +2078,27 @@ export const FormEngine = ({
|
|
|
1506
2078
|
// reqraft: form-level expression fields get the Visual/Text shell
|
|
1507
2079
|
// (DPQL text mode); opt out per-form via `templateFieldProps`.
|
|
1508
2080
|
allowTextExpressions
|
|
2081
|
+
// A fixed allowed-value field still needs its selector even when
|
|
2082
|
+
// arbitrary custom values are forbidden. TemplateField uses this
|
|
2083
|
+
// flag to decide whether to mount AutoFormField at all; treating
|
|
2084
|
+
// `supports_custom_values: false` as "no input" left dependent
|
|
2085
|
+
// delivery fields (Discord server/channel, for example) as empty
|
|
2086
|
+
// control groups. The allowed-values renderer itself enforces the
|
|
2087
|
+
// fixed catalogue and never exposes free-form entry.
|
|
1509
2088
|
allowCustomValues={
|
|
1510
|
-
|
|
2089
|
+
isFixedCompactAllowedValueOption(optionSchema) ||
|
|
2090
|
+
(options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any')
|
|
1511
2091
|
}
|
|
1512
2092
|
templates={templates.value}
|
|
1513
2093
|
{...getTypeAndCanBeNull(
|
|
1514
|
-
type
|
|
2094
|
+
// The RENDERER type picks the editor — the storage type lives on
|
|
2095
|
+
// the value envelope. Passing storage here rendered a `richtext`
|
|
2096
|
+
// field as a plain string input.
|
|
2097
|
+
resolvedType,
|
|
1515
2098
|
options?.[optionName]?.allowed_values,
|
|
1516
2099
|
other.op
|
|
1517
2100
|
)}
|
|
1518
|
-
ui_type={
|
|
2101
|
+
ui_type={resolvedType}
|
|
1519
2102
|
name={optionName}
|
|
1520
2103
|
uniqueName={`${uniqueName ? `${uniqueName}.` : `${name ? `${name}.` : ''}`}${optionName}`}
|
|
1521
2104
|
onChange={
|
|
@@ -1534,6 +2117,7 @@ export const FormEngine = ({
|
|
|
1534
2117
|
default_value={getDefaultValue(options?.[optionName])}
|
|
1535
2118
|
isDefaultTemplate={options?.[optionName]?.default_view === 'template'}
|
|
1536
2119
|
allowed_values={options?.[optionName]?.allowed_values}
|
|
2120
|
+
allowed_values_creatable={options?.[optionName]?.allowed_values_creatable}
|
|
1537
2121
|
disabled={
|
|
1538
2122
|
options?.[optionName]?.disabled ||
|
|
1539
2123
|
readOnly ||
|
|
@@ -1551,12 +2135,13 @@ export const FormEngine = ({
|
|
|
1551
2135
|
: undefined
|
|
1552
2136
|
}
|
|
1553
2137
|
{...templateFieldProps}
|
|
2138
|
+
forceDropdown={effectiveForceDropdown}
|
|
1554
2139
|
/>
|
|
1555
2140
|
<OptionFieldMessages
|
|
1556
2141
|
schema={options || {}}
|
|
1557
2142
|
allOptions={availableOptions}
|
|
1558
2143
|
name={optionName}
|
|
1559
|
-
option={{ type, ...other }}
|
|
2144
|
+
option={{ type: resolvedType, ...other }}
|
|
1560
2145
|
getType={getTypeForOption}
|
|
1561
2146
|
/>
|
|
1562
2147
|
{operators && size(operators) && size(other.op) ?
|
|
@@ -1571,7 +2156,7 @@ export const FormEngine = ({
|
|
|
1571
2156
|
intent='info'
|
|
1572
2157
|
label={
|
|
1573
2158
|
other.value ?
|
|
1574
|
-
|
|
2159
|
+
resolvedType === 'richtext' ?
|
|
1575
2160
|
richtextToString(other.value)
|
|
1576
2161
|
: JSON.stringify(other.value)
|
|
1577
2162
|
: ''
|
|
@@ -1594,6 +2179,7 @@ export const FormEngine = ({
|
|
|
1594
2179
|
uniqueName,
|
|
1595
2180
|
componentOverrides,
|
|
1596
2181
|
templateFieldProps,
|
|
2182
|
+
forceDropdown,
|
|
1597
2183
|
availableOptions,
|
|
1598
2184
|
fixedValue,
|
|
1599
2185
|
// Depend on the specific `rest` values used, not the whole `rest` object
|
|
@@ -1643,6 +2229,7 @@ export const FormEngine = ({
|
|
|
1643
2229
|
showFieldTypes,
|
|
1644
2230
|
showAllDescriptions,
|
|
1645
2231
|
expandedOptions,
|
|
2232
|
+
autoFocusNameRef,
|
|
1646
2233
|
highlightedOptions,
|
|
1647
2234
|
flashedOptions,
|
|
1648
2235
|
infoPanelOverrides,
|
|
@@ -1663,6 +2250,8 @@ export const FormEngine = ({
|
|
|
1663
2250
|
getTypeForOption,
|
|
1664
2251
|
isOptionValid,
|
|
1665
2252
|
confirmAction,
|
|
2253
|
+
optionActions,
|
|
2254
|
+
collapseOptionActions,
|
|
1666
2255
|
renderOption,
|
|
1667
2256
|
theme,
|
|
1668
2257
|
cText,
|
|
@@ -1687,6 +2276,7 @@ export const FormEngine = ({
|
|
|
1687
2276
|
showFieldTypes,
|
|
1688
2277
|
showAllDescriptions,
|
|
1689
2278
|
expandedOptions,
|
|
2279
|
+
autoFocusNameRef,
|
|
1690
2280
|
highlightedOptions,
|
|
1691
2281
|
flashedOptions,
|
|
1692
2282
|
infoPanelOverrides,
|
|
@@ -1707,6 +2297,8 @@ export const FormEngine = ({
|
|
|
1707
2297
|
getTypeForOption,
|
|
1708
2298
|
isOptionValid,
|
|
1709
2299
|
confirmAction,
|
|
2300
|
+
optionActions,
|
|
2301
|
+
collapseOptionActions,
|
|
1710
2302
|
renderOption,
|
|
1711
2303
|
theme,
|
|
1712
2304
|
cText,
|
|
@@ -1726,6 +2318,7 @@ export const FormEngine = ({
|
|
|
1726
2318
|
() => ({
|
|
1727
2319
|
readOnly,
|
|
1728
2320
|
invalidCount: size(validityData.invalidFields),
|
|
2321
|
+
attentionCount: readFirstAttentionCount,
|
|
1729
2322
|
completion: readFirstCompletion,
|
|
1730
2323
|
showInvalidOnly: showInvalidOptionsOnly,
|
|
1731
2324
|
onToggleInvalidOnly: handleToggleInvalidOnly,
|
|
@@ -1751,6 +2344,7 @@ export const FormEngine = ({
|
|
|
1751
2344
|
[
|
|
1752
2345
|
readOnly,
|
|
1753
2346
|
validityData,
|
|
2347
|
+
readFirstAttentionCount,
|
|
1754
2348
|
readFirstCompletion,
|
|
1755
2349
|
showInvalidOptionsOnly,
|
|
1756
2350
|
handleToggleInvalidOnly,
|
|
@@ -1776,7 +2370,13 @@ export const FormEngine = ({
|
|
|
1776
2370
|
]
|
|
1777
2371
|
);
|
|
1778
2372
|
|
|
1779
|
-
|
|
2373
|
+
// The engine's own toolbar action, plus whatever the consumer added through
|
|
2374
|
+
// `compactPanelProps.actions` — appended, so an outside action can never
|
|
2375
|
+
// knock the form's toolbar out of the header.
|
|
2376
|
+
const compactHeaderActions = useMemo(
|
|
2377
|
+
() => [{ as: CompactToolbar, responsive: false }, ...(compactPanelProps?.actions || [])],
|
|
2378
|
+
[compactPanelProps?.actions]
|
|
2379
|
+
);
|
|
1780
2380
|
const renderCompact = () => {
|
|
1781
2381
|
const headerBg = `${changeDarkness(getMainBackgroundColor(theme), 0.02)}${percentToHexAlpha(88)}`;
|
|
1782
2382
|
// Toolbar filters narrow the listed rows; the meter reflects the full set.
|
|
@@ -1809,15 +2409,18 @@ export const FormEngine = ({
|
|
|
1809
2409
|
pushRow(optionName, false);
|
|
1810
2410
|
}
|
|
1811
2411
|
});
|
|
1812
|
-
//
|
|
1813
|
-
//
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2412
|
+
// Surface EVERY not-yet-added optional field as an addable (hidden) row, so
|
|
2413
|
+
// the whole schema is browsable inline — they all land in the Optional box
|
|
2414
|
+
// (hidden ⇒ 'optional' bucket) instead of being buried in the Fields menu.
|
|
2415
|
+
// Narrowed by the same filters as the listed rows (search query + required-
|
|
2416
|
+
// only). availableOptions (listed) and filteredOptions (these) are disjoint —
|
|
2417
|
+
// the former is built from fixedValue keys, the latter excludes them — so a
|
|
2418
|
+
// field is never both a listed and a hidden row.
|
|
2419
|
+
forEach(filteredOptions, (_schema, optionName) => {
|
|
2420
|
+
if (matchesFilters(optionName)) {
|
|
2421
|
+
pushRow(optionName, true);
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
1821
2424
|
|
|
1822
2425
|
// User sort (Fields menu → "Sort by"), applied WITHIN each group so the
|
|
1823
2426
|
// group sections and the required-group rails are preserved. Schema order is
|
|
@@ -1832,7 +2435,14 @@ export const FormEngine = ({
|
|
|
1832
2435
|
const isFieldInvalid = (name: string) =>
|
|
1833
2436
|
!isOptionValid(
|
|
1834
2437
|
name,
|
|
1835
|
-
(
|
|
2438
|
+
getOptionFieldStorageType(
|
|
2439
|
+
name,
|
|
2440
|
+
(shownOptions as TQorusForm)[name]?.type,
|
|
2441
|
+
options,
|
|
2442
|
+
operators,
|
|
2443
|
+
(shownOptions as TQorusForm)[name]?.op,
|
|
2444
|
+
isRendererOnly
|
|
2445
|
+
),
|
|
1836
2446
|
(shownOptions as TQorusForm)[name]?.value
|
|
1837
2447
|
);
|
|
1838
2448
|
const comparator = (a: { name: string }, b: { name: string }): number => {
|
|
@@ -1863,6 +2473,67 @@ export const FormEngine = ({
|
|
|
1863
2473
|
return groupOrder.indexOf(a) - groupOrder.indexOf(b);
|
|
1864
2474
|
});
|
|
1865
2475
|
|
|
2476
|
+
// Read-first STATUS per row → one of three boxes (Needs attention / Set /
|
|
2477
|
+
// Optional), via the component-scope getOptionBucket (shared with the header
|
|
2478
|
+
// count and the row dot, so box ↔ dot ↔ header always agree). Buckets keep
|
|
2479
|
+
// schema-group order (thin sub-labels) and the required-group clustering: an
|
|
2480
|
+
// unmet one-of group's members all land in attention and still rail up.
|
|
2481
|
+
type TRowEntry = { name: string; hidden: boolean };
|
|
2482
|
+
type TBucketKey = 'attention' | 'set' | 'optional';
|
|
2483
|
+
const buckets: Record<TBucketKey, Record<string, TRowEntry[]>> = {
|
|
2484
|
+
attention: {},
|
|
2485
|
+
set: {},
|
|
2486
|
+
optional: {},
|
|
2487
|
+
};
|
|
2488
|
+
const bucketGroups: Record<TBucketKey, string[]> = { attention: [], set: [], optional: [] };
|
|
2489
|
+
// Freeze the box of any field currently being edited (or whose one-of group
|
|
2490
|
+
// has an edited member) to its last settled box — so finishing an edit that
|
|
2491
|
+
// flips its status doesn't remount it in another box and steal focus.
|
|
2492
|
+
const stableBucketOf = (entry: TRowEntry): TBucketKey => {
|
|
2493
|
+
const fresh = getOptionBucket(entry.name, entry.hidden);
|
|
2494
|
+
const groupBeingEdited =
|
|
2495
|
+
!entry.hidden &&
|
|
2496
|
+
((options?.[entry.name]?.required_groups as string[] | undefined) || []).some((g) =>
|
|
2497
|
+
(requiredGroupsInfo.members[g] || []).some((m) => expandedOptions.includes(m))
|
|
2498
|
+
);
|
|
2499
|
+
if (!entry.hidden && (expandedOptions.includes(entry.name) || groupBeingEdited)) {
|
|
2500
|
+
const memo = settledBucket.current[entry.name];
|
|
2501
|
+
if (memo) return memo;
|
|
2502
|
+
}
|
|
2503
|
+
settledBucket.current[entry.name] = fresh;
|
|
2504
|
+
return fresh;
|
|
2505
|
+
};
|
|
2506
|
+
groupKeys.forEach((groupName) => {
|
|
2507
|
+
grouped[groupName].forEach((entry) => {
|
|
2508
|
+
const b = stableBucketOf(entry);
|
|
2509
|
+
if (!buckets[b][groupName]) {
|
|
2510
|
+
buckets[b][groupName] = [];
|
|
2511
|
+
bucketGroups[b].push(groupName);
|
|
2512
|
+
}
|
|
2513
|
+
buckets[b][groupName].push(entry);
|
|
2514
|
+
});
|
|
2515
|
+
});
|
|
2516
|
+
const bucketCount = (b: TBucketKey) =>
|
|
2517
|
+
bucketGroups[b].reduce((n, g) => n + buckets[b][g].length, 0);
|
|
2518
|
+
// 'general' / 'optional' are the SYNTHETIC fallback group keys getOptionGroup
|
|
2519
|
+
// assigns to fields with no explicit `group` — printing a "General"/"Optional"
|
|
2520
|
+
// sub-label for those is just noise, so suppress it. BUT a consumer may also
|
|
2521
|
+
// use 'general' as a REAL group (defining it in the `groups` prop and tagging
|
|
2522
|
+
// fields with `group: 'general'`); in that case it's a named group like any
|
|
2523
|
+
// other and DOES get its sub-label.
|
|
2524
|
+
const showGroupSubLabel = (groupName: string) =>
|
|
2525
|
+
(groupName !== 'general' && groupName !== 'optional') || !!groups?.[groupName];
|
|
2526
|
+
const STATUS_BOXES: Array<{
|
|
2527
|
+
key: TBucketKey;
|
|
2528
|
+
label: string;
|
|
2529
|
+
intent?: 'warning' | 'success';
|
|
2530
|
+
icon: IReqoreIconName;
|
|
2531
|
+
}> = [
|
|
2532
|
+
{ key: 'attention', label: 'Needs attention', intent: 'warning', icon: 'ErrorWarningLine' },
|
|
2533
|
+
{ key: 'set', label: 'Set', intent: 'success', icon: 'CheckLine' },
|
|
2534
|
+
{ key: 'optional', label: 'Optional', icon: 'CheckboxBlankCircleLine' },
|
|
2535
|
+
];
|
|
2536
|
+
|
|
1866
2537
|
// Build the rows for one group: contiguous required-group members are pulled
|
|
1867
2538
|
// together at the first member's slot and rendered as a connected rail (flat
|
|
1868
2539
|
// rows — no wrapper — so the value surface applies normally; the rail + nodes
|
|
@@ -1880,7 +2551,7 @@ export const FormEngine = ({
|
|
|
1880
2551
|
optionField={
|
|
1881
2552
|
entry.hidden ?
|
|
1882
2553
|
({
|
|
1883
|
-
type: (options?.[entry.name]
|
|
2554
|
+
type: getOptionSchemaStorageType(options?.[entry.name], isRendererOnly),
|
|
1884
2555
|
value: undefined,
|
|
1885
2556
|
} as IQorusFormField)
|
|
1886
2557
|
: ((shownOptions as TQorusForm)[entry.name] as IQorusFormField)
|
|
@@ -1891,7 +2562,9 @@ export const FormEngine = ({
|
|
|
1891
2562
|
clusterLast={clusterLast}
|
|
1892
2563
|
/>
|
|
1893
2564
|
);
|
|
1894
|
-
|
|
2565
|
+
// (Clustering runs in narrow mode too now — the "One of the below is
|
|
2566
|
+
// required" box wraps the members regardless of width; it no longer relies
|
|
2567
|
+
// on a contiguous rail.)
|
|
1895
2568
|
const emitted = new Set<string>();
|
|
1896
2569
|
const groupOf = (name: string) =>
|
|
1897
2570
|
(options?.[name]?.required_groups as string[] | undefined)?.[0];
|
|
@@ -1902,9 +2575,27 @@ export const FormEngine = ({
|
|
|
1902
2575
|
const memberEntries = names.filter((e) => !e.hidden && groupOf(e.name) === grp);
|
|
1903
2576
|
if (memberEntries.length < 2) return renderRow(entry, false);
|
|
1904
2577
|
emitted.add(grp);
|
|
1905
|
-
|
|
2578
|
+
const railed = memberEntries.map((e, idx) =>
|
|
1906
2579
|
renderRow(e, true, idx === 0, idx === memberEntries.length - 1)
|
|
1907
2580
|
);
|
|
2581
|
+
// An UNMET one-of group gets the explicit "One of the below is required"
|
|
2582
|
+
// box (the Focus cluster). A met group needs no banner — the rail + the
|
|
2583
|
+
// "Covers"/"Covered by" chips already say which member satisfies it.
|
|
2584
|
+
if (requiredGroupsInfo.satisfiedBy[grp]) return railed;
|
|
2585
|
+
return (
|
|
2586
|
+
<StyledRequiredClusterBox
|
|
2587
|
+
key={grp}
|
|
2588
|
+
className='options-readfirst-required-cluster'
|
|
2589
|
+
$border={`${cWarning}33`}
|
|
2590
|
+
$tint={`${cWarning}0d`}
|
|
2591
|
+
>
|
|
2592
|
+
<StyledRequiredClusterHeader $color={cWarning}>
|
|
2593
|
+
<ReqoreIcon icon='LinkM' size='11px' style={{ color: cWarning }} />
|
|
2594
|
+
One of the below is required
|
|
2595
|
+
</StyledRequiredClusterHeader>
|
|
2596
|
+
{railed}
|
|
2597
|
+
</StyledRequiredClusterBox>
|
|
2598
|
+
);
|
|
1908
2599
|
});
|
|
1909
2600
|
};
|
|
1910
2601
|
|
|
@@ -1922,19 +2613,37 @@ export const FormEngine = ({
|
|
|
1922
2613
|
<StyledCompactWrap
|
|
1923
2614
|
ref={setCompactWrap}
|
|
1924
2615
|
className='options-readfirst-scroll'
|
|
1925
|
-
|
|
2616
|
+
// A nested sub-form sits flush inside the parent's card — no outer
|
|
2617
|
+
// gutter (the card already provides the breathing room).
|
|
2618
|
+
$flush={compactFlush || compactNested}
|
|
1926
2619
|
>
|
|
1927
2620
|
<StyledCompactPanel
|
|
1928
|
-
|
|
2621
|
+
// The top-level form scrolls, so its toolbar STICKS and carries a
|
|
2622
|
+
// dark blurred backdrop so content ghosts cleanly beneath it. A
|
|
2623
|
+
// nested (arg_schema) sub-form owns no scroll context — drop the
|
|
2624
|
+
// sticky, the backdrop, and the stacking context so its header is
|
|
2625
|
+
// transparent inside the parent's card.
|
|
2626
|
+
$headerBg={compactNested ? 'transparent' : headerBg}
|
|
2627
|
+
$nested={compactNested}
|
|
1929
2628
|
flat
|
|
1930
|
-
|
|
1931
|
-
|
|
2629
|
+
raised
|
|
2630
|
+
minimal
|
|
2631
|
+
// No panel background: the form sits transparently on whatever
|
|
2632
|
+
// hosts it (page, drawer, or — for an arg_schema field — the
|
|
2633
|
+
// parent's edit card) instead of stacking its own dark surface.
|
|
2634
|
+
// The status boxes keep their own tints; the sticky toolbar keeps
|
|
2635
|
+
// its blurred header via the $headerBg override.
|
|
2636
|
+
stickyHeader={!compactNested}
|
|
2637
|
+
// Consumer overrides land last so they win over every default
|
|
2638
|
+
// above; `actions` and `contentStyle` below merge rather than
|
|
2639
|
+
// replace (see `compactPanelProps`).
|
|
2640
|
+
{...compactPanelProps}
|
|
1932
2641
|
actions={compactHeaderActions}
|
|
1933
2642
|
contentStyle={{
|
|
1934
2643
|
display: 'flex',
|
|
1935
2644
|
flexFlow: 'column',
|
|
1936
2645
|
gap: '10px',
|
|
1937
|
-
|
|
2646
|
+
...compactPanelProps?.contentStyle,
|
|
1938
2647
|
}}
|
|
1939
2648
|
>
|
|
1940
2649
|
{size(groupKeys) === 0 ?
|
|
@@ -1943,78 +2652,61 @@ export const FormEngine = ({
|
|
|
1943
2652
|
</ReqoreMessage>
|
|
1944
2653
|
: null}
|
|
1945
2654
|
|
|
1946
|
-
{
|
|
1947
|
-
const
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2655
|
+
{STATUS_BOXES.map((box) => {
|
|
2656
|
+
const groupsInBox = bucketGroups[box.key];
|
|
2657
|
+
const count = bucketCount(box.key);
|
|
2658
|
+
if (!count) return null;
|
|
2659
|
+
const accent =
|
|
2660
|
+
box.key === 'attention' ? cWarning
|
|
2661
|
+
: box.key === 'set' ? cSuccess
|
|
2662
|
+
: cMuted;
|
|
2663
|
+
// The muted "Optional" box reads as a quieter, recessed
|
|
2664
|
+
// surface — a touch darker than the page rather than the
|
|
2665
|
+
// faint grey tint the accent would give.
|
|
2666
|
+
const boxBg =
|
|
2667
|
+
box.key === 'optional' ?
|
|
2668
|
+
changeDarkness(getMainBackgroundColor(theme), 0.06)
|
|
2669
|
+
: undefined;
|
|
1960
2670
|
return (
|
|
1961
|
-
<
|
|
1962
|
-
|
|
2671
|
+
<StyledStatusBox
|
|
2672
|
+
$accent={accent}
|
|
2673
|
+
$bg={boxBg}
|
|
2674
|
+
key={box.key}
|
|
1963
2675
|
flat
|
|
1964
2676
|
minimal
|
|
1965
2677
|
collapseButtonProps={{ flat: true, minimal: true, size: 'small' }}
|
|
1966
2678
|
collapsible
|
|
2679
|
+
// The Optional box now holds every not-yet-added field, so
|
|
2680
|
+
// it starts COLLAPSED to keep the form focused on what's in
|
|
2681
|
+
// use. But a SEARCH must surface matching addable fields —
|
|
2682
|
+
// and ReqorePanel unmounts collapsed content — so force it
|
|
2683
|
+
// open whenever a query is active. (isCollapsed is the
|
|
2684
|
+
// panel's controllable state; manual toggling still works
|
|
2685
|
+
// when no query is set.)
|
|
2686
|
+
isCollapsed={box.key === 'optional' && !query}
|
|
1967
2687
|
label={
|
|
1968
2688
|
<StyledGroupHeader>
|
|
1969
|
-
<ReqoreP effect={{ weight: 'bold' }} size='
|
|
1970
|
-
{
|
|
2689
|
+
<ReqoreP effect={{ weight: 'bold' }} size='normal'>
|
|
2690
|
+
{box.label}
|
|
1971
2691
|
</ReqoreP>
|
|
1972
|
-
<
|
|
1973
|
-
|
|
1974
|
-
readOnly
|
|
1975
|
-
size='tiny'
|
|
2692
|
+
<ReqoreTag
|
|
2693
|
+
size='small'
|
|
1976
2694
|
minimal
|
|
1977
|
-
flat
|
|
1978
2695
|
compact
|
|
1979
|
-
|
|
1980
|
-
{
|
|
1981
|
-
: invalidCount ?
|
|
1982
|
-
{
|
|
1983
|
-
label: `${invalidCount} to resolve`,
|
|
1984
|
-
intent: 'warning' as const,
|
|
1985
|
-
icon: 'ErrorWarningLine' as const,
|
|
1986
|
-
}
|
|
1987
|
-
: {
|
|
1988
|
-
label: 'All set',
|
|
1989
|
-
intent: 'success' as const,
|
|
1990
|
-
icon: 'CheckLine' as const,
|
|
1991
|
-
})}
|
|
2696
|
+
intent={box.intent}
|
|
2697
|
+
label={String(count)}
|
|
1992
2698
|
/>
|
|
1993
2699
|
</StyledGroupHeader>
|
|
1994
2700
|
}
|
|
1995
|
-
icon={
|
|
2701
|
+
icon={box.icon}
|
|
1996
2702
|
className='options-readfirst-group'
|
|
1997
2703
|
padded={false}
|
|
1998
2704
|
contentStyle={{ padding: '4px 4px 6px' }}
|
|
1999
2705
|
>
|
|
2000
|
-
{
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
// Indent to the same content line as the rows (StyledGroupBody's
|
|
2005
|
-
// `margin-left` clamp) so the subtitle sits tucked under the group
|
|
2006
|
-
// name instead of at the panel edge — and clears the group's
|
|
2007
|
-
// vertical rule (left:16px) rather than crossing it.
|
|
2008
|
-
style={{
|
|
2009
|
-
marginTop: 2,
|
|
2010
|
-
marginBottom: 8,
|
|
2011
|
-
marginLeft: GROUP_INDENT,
|
|
2012
|
-
paddingRight: 10,
|
|
2013
|
-
}}
|
|
2014
|
-
>
|
|
2015
|
-
{groupConfig.subtitle}
|
|
2016
|
-
</ReqoreP>
|
|
2017
|
-
: null}
|
|
2706
|
+
{/* ONE group body per box: every field block (and the
|
|
2707
|
+
thin schema sub-labels) is a direct flex child, so the
|
|
2708
|
+
inter-field gap is identical everywhere — including
|
|
2709
|
+
across schema-group boundaries. */}
|
|
2018
2710
|
<StyledGroupBody
|
|
2019
2711
|
$divider={cDivider}
|
|
2020
2712
|
$hover={cHover}
|
|
@@ -2024,9 +2716,35 @@ export const FormEngine = ({
|
|
|
2024
2716
|
$lineColor={cGroupLine}
|
|
2025
2717
|
className={compactNarrow ? 'readfirst-narrow' : undefined}
|
|
2026
2718
|
>
|
|
2027
|
-
{
|
|
2719
|
+
{groupsInBox.map((groupName) => {
|
|
2720
|
+
const groupConfig = groups?.[groupName];
|
|
2721
|
+
return (
|
|
2722
|
+
<React.Fragment key={groupName}>
|
|
2723
|
+
{showGroupSubLabel(groupName) ?
|
|
2724
|
+
<StyledStatusBoxGroupLabel>
|
|
2725
|
+
{getOptionGroupLabel(groupName, groups)}
|
|
2726
|
+
</StyledStatusBoxGroupLabel>
|
|
2727
|
+
: null}
|
|
2728
|
+
{showGroupSubLabel(groupName) && groupConfig?.subtitle ?
|
|
2729
|
+
<ReqoreP
|
|
2730
|
+
size='small'
|
|
2731
|
+
effect={{ opacity: 0.6 }}
|
|
2732
|
+
style={{
|
|
2733
|
+
marginTop: 2,
|
|
2734
|
+
marginBottom: 8,
|
|
2735
|
+
marginLeft: GROUP_INDENT,
|
|
2736
|
+
paddingRight: 10,
|
|
2737
|
+
}}
|
|
2738
|
+
>
|
|
2739
|
+
{groupConfig.subtitle}
|
|
2740
|
+
</ReqoreP>
|
|
2741
|
+
: null}
|
|
2742
|
+
{renderGroupRows(buckets[box.key][groupName])}
|
|
2743
|
+
</React.Fragment>
|
|
2744
|
+
);
|
|
2745
|
+
})}
|
|
2028
2746
|
</StyledGroupBody>
|
|
2029
|
-
</
|
|
2747
|
+
</StyledStatusBox>
|
|
2030
2748
|
);
|
|
2031
2749
|
})}
|
|
2032
2750
|
</StyledCompactPanel>
|
|
@@ -2294,13 +3012,11 @@ export const FormEngine = ({
|
|
|
2294
3012
|
// option's schema as context). The consumer (the IDE) injects
|
|
2295
3013
|
// it; same factory pattern as the ExpressionBuilder's
|
|
2296
3014
|
// `extraActions`.
|
|
2297
|
-
...(
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
})
|
|
2303
|
-
: (optionActions ?? [])),
|
|
3015
|
+
...resolveOptionActions(optionActions, {
|
|
3016
|
+
name: optionName,
|
|
3017
|
+
schema: options[optionName],
|
|
3018
|
+
value: availableOptions?.[optionName] as TOption,
|
|
3019
|
+
}),
|
|
2304
3020
|
{
|
|
2305
3021
|
size: 'tiny',
|
|
2306
3022
|
icon: 'FullscreenLine',
|