@qoretechnologies/reqraft 0.10.57 → 0.10.58-pr.129.g2af0cf5
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/design/FORM_ENGINE_COMPACT.md +23 -5
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +2 -2
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +71 -62
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +6 -3
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +16 -20
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +16 -49
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.js +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -1
- package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -1
- package/dist/components/form/engine/variants/variantModel.js +8 -3
- package/dist/components/form/engine/variants/variantModel.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +22 -13
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +8 -0
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.js +8 -0
- package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +4 -1
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/binary/Binary.d.ts.map +1 -1
- package/dist/components/form/fields/binary/Binary.js +10 -0
- package/dist/components/form/fields/binary/Binary.js.map +1 -1
- package/dist/components/form/fields/emittedValues.d.ts +38 -0
- package/dist/components/form/fields/emittedValues.d.ts.map +1 -0
- package/dist/components/form/fields/emittedValues.js +67 -0
- package/dist/components/form/fields/emittedValues.js.map +1 -0
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +24 -8
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/number/Number.d.ts.map +1 -1
- package/dist/components/form/fields/number/Number.js +9 -0
- package/dist/components/form/fields/number/Number.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +10 -0
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +3 -1
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +8 -0
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +21 -5
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.js +24 -8
- package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.d.ts +5 -4
- package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.js +27 -2
- package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
- package/dist/components/supportTicket/meta.d.ts +16 -0
- package/dist/components/supportTicket/meta.d.ts.map +1 -1
- package/dist/components/supportTicket/meta.js +6 -1
- package/dist/components/supportTicket/meta.js.map +1 -1
- package/dist/components/supportTicket/useLoadedImages.d.ts +18 -0
- package/dist/components/supportTicket/useLoadedImages.d.ts.map +1 -0
- package/dist/components/supportTicket/useLoadedImages.js +72 -0
- package/dist/components/supportTicket/useLoadedImages.js.map +1 -0
- package/dist/components/ticketReferences/TicketReferences.d.ts +9 -5
- package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -1
- package/dist/components/ticketReferences/TicketReferences.js +22 -9
- package/dist/components/ticketReferences/TicketReferences.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts +10 -6
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +2 -2
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/dist/helpers/allowedValues.d.ts +71 -0
- package/dist/helpers/allowedValues.d.ts.map +1 -0
- package/dist/helpers/allowedValues.js +183 -0
- package/dist/helpers/allowedValues.js.map +1 -0
- package/dist/helpers/common.d.ts +7 -0
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +16 -1
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/expressions.d.ts +13 -0
- package/dist/helpers/expressions.d.ts.map +1 -1
- package/dist/helpers/expressions.js +23 -1
- package/dist/helpers/expressions.js.map +1 -1
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +14 -0
- package/dist/helpers/validations.js.map +1 -1
- package/dist/utils/fetch.d.ts +4 -0
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +6 -1
- package/dist/utils/fetch.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactToolbar.tsx +8 -3
- package/src/components/form/engine/FormEngine.stories.tsx +206 -13
- package/src/components/form/engine/FormEngine.tsx +70 -57
- package/src/components/form/engine/OptionFieldMessages.tsx +7 -5
- package/src/components/form/engine/readFirst.ts +19 -50
- package/src/components/form/engine/variants/VariantCalmTable.tsx +3 -1
- package/src/components/form/engine/variants/variantModel.ts +10 -3
- package/src/components/form/expressions/builder/index.tsx +15 -2
- package/src/components/form/fields/array/ArrayAuto.tsx +10 -1
- package/src/components/form/fields/array/ArrayAutoField.tsx +10 -1
- package/src/components/form/fields/auto/AutoFormField.tsx +4 -1
- package/src/components/form/fields/binary/Binary.tsx +10 -0
- package/src/components/form/fields/emittedValues.ts +62 -0
- package/src/components/form/fields/long-string/LongString.tsx +28 -8
- package/src/components/form/fields/number/Number.tsx +9 -0
- package/src/components/form/fields/rich-text/RichText.tsx +10 -0
- package/src/components/form/fields/template/TemplateField.tsx +4 -1
- package/src/components/smartEditor/useLspSession.ts +8 -0
- package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +86 -0
- package/src/components/supportTicket/InterfaceReferenceTags.tsx +44 -11
- package/src/components/supportTicket/ReferencePicker.stories.tsx +65 -0
- package/src/components/supportTicket/ReferencePicker.tsx +26 -5
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +5 -2
- package/src/components/supportTicket/__fixtures__/kindMarks.ts +28 -0
- package/src/components/supportTicket/meta.ts +21 -0
- package/src/components/supportTicket/useLoadedImages.ts +73 -0
- package/src/components/ticketReferences/TicketReferences.tsx +27 -7
- package/src/components/ticketThread/TicketThread.tsx +11 -3
- package/src/helpers/allowedValues.ts +216 -0
- package/src/helpers/common.ts +14 -0
- package/src/helpers/expressions.ts +25 -0
- package/src/helpers/validations.ts +16 -0
- package/src/utils/fetch.ts +10 -1
|
@@ -2153,6 +2153,114 @@ export const CompactWithholdsFieldsWithUnmetDependencies: Story = {
|
|
|
2153
2153
|
},
|
|
2154
2154
|
};
|
|
2155
2155
|
|
|
2156
|
+
/** A check whose kind decides which of the rest apply — the guided check's shape. */
|
|
2157
|
+
const lockedRequiredCheckOptions = {
|
|
2158
|
+
kind: {
|
|
2159
|
+
type: 'string',
|
|
2160
|
+
ui_type: 'string',
|
|
2161
|
+
display_name: 'Kind',
|
|
2162
|
+
short_desc: 'What the check compares',
|
|
2163
|
+
preselected: true,
|
|
2164
|
+
allowed_values: [
|
|
2165
|
+
{ value: { type: 'string', value: 'equals' }, display_name: 'Equals' },
|
|
2166
|
+
{ value: { type: 'string', value: 'between' }, display_name: 'Between' },
|
|
2167
|
+
{ value: { type: 'string', value: 'exists' }, display_name: 'Exists' },
|
|
2168
|
+
],
|
|
2169
|
+
},
|
|
2170
|
+
expected: {
|
|
2171
|
+
type: 'string',
|
|
2172
|
+
ui_type: 'string',
|
|
2173
|
+
display_name: 'Expected value',
|
|
2174
|
+
short_desc: 'Applies once a kind is picked',
|
|
2175
|
+
required: true,
|
|
2176
|
+
depends_on: ['kind'],
|
|
2177
|
+
},
|
|
2178
|
+
min: {
|
|
2179
|
+
type: 'int',
|
|
2180
|
+
ui_type: 'int',
|
|
2181
|
+
display_name: 'Minimum',
|
|
2182
|
+
short_desc: 'Applies to Between alone',
|
|
2183
|
+
required: true,
|
|
2184
|
+
depends_on: ['kind=between'],
|
|
2185
|
+
},
|
|
2186
|
+
max: {
|
|
2187
|
+
type: 'int',
|
|
2188
|
+
ui_type: 'int',
|
|
2189
|
+
display_name: 'Maximum',
|
|
2190
|
+
short_desc: 'Applies to Between alone',
|
|
2191
|
+
required: true,
|
|
2192
|
+
depends_on: ['kind=between'],
|
|
2193
|
+
},
|
|
2194
|
+
} as IOptionsSchema;
|
|
2195
|
+
|
|
2196
|
+
export const CompactLockedRequiredNeedsNoAttention: Story = {
|
|
2197
|
+
parameters: {
|
|
2198
|
+
docs: {
|
|
2199
|
+
description: {
|
|
2200
|
+
story:
|
|
2201
|
+
'Renders a check whose Expected value, Minimum and Maximum are required but locked until a kind is picked. A locked field is not listed under "Needs attention": the author can do nothing with it yet. Each stays on the form, locked, among the optional fields, saying what unlocks it. With no kind picked, nothing needs attention and the header reads Ready: the locked fields do not apply yet.',
|
|
2202
|
+
},
|
|
2203
|
+
},
|
|
2204
|
+
},
|
|
2205
|
+
args: {
|
|
2206
|
+
compact: true,
|
|
2207
|
+
minColumnWidth: '300px',
|
|
2208
|
+
options: lockedRequiredCheckOptions,
|
|
2209
|
+
value: {} as IOptions,
|
|
2210
|
+
},
|
|
2211
|
+
play: async () => {
|
|
2212
|
+
await _testsWaitForText('Expected value');
|
|
2213
|
+
await waitFor(
|
|
2214
|
+
() =>
|
|
2215
|
+
expect(
|
|
2216
|
+
document.querySelector(
|
|
2217
|
+
'.readfirst-row[data-field="expected"] .options-readfirst-lock-deps'
|
|
2218
|
+
)
|
|
2219
|
+
).toBeTruthy(),
|
|
2220
|
+
{ timeout: 10000 }
|
|
2221
|
+
);
|
|
2222
|
+
await expect(document.querySelector('[data-field="min"]')).toBeTruthy();
|
|
2223
|
+
await expect(document.querySelector('[data-field="max"]')).toBeTruthy();
|
|
2224
|
+
await _testsWaitForTextToNotExist('Needs attention');
|
|
2225
|
+
// Nothing the author can fill in is missing, so the form is Ready — the
|
|
2226
|
+
// locked fields do not apply until a kind is picked.
|
|
2227
|
+
await _testsWaitForText('Ready');
|
|
2228
|
+
await _testsWaitForTextToNotExist('Incomplete');
|
|
2229
|
+
},
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
export const CompactUnlockedRequiredNeedsAttention: Story = {
|
|
2233
|
+
parameters: {
|
|
2234
|
+
docs: {
|
|
2235
|
+
description: {
|
|
2236
|
+
story:
|
|
2237
|
+
'Renders the same check with the kind set to Between. Every field that kind unlocks is required and empty, so Expected value, Minimum and Maximum are all listed under "Needs attention" and the header reads Incomplete (never Draft: nothing has been changed).',
|
|
2238
|
+
},
|
|
2239
|
+
},
|
|
2240
|
+
},
|
|
2241
|
+
args: {
|
|
2242
|
+
compact: true,
|
|
2243
|
+
minColumnWidth: '300px',
|
|
2244
|
+
options: lockedRequiredCheckOptions,
|
|
2245
|
+
value: { kind: { type: 'string', value: 'between' } } as IOptions,
|
|
2246
|
+
},
|
|
2247
|
+
play: async () => {
|
|
2248
|
+
await _testsWaitForText('Needs attention');
|
|
2249
|
+
await _testsWaitForText('Incomplete');
|
|
2250
|
+
await _testsWaitForTextToNotExist('Draft');
|
|
2251
|
+
const attention = [...document.querySelectorAll('.options-readfirst-group')].find((group) =>
|
|
2252
|
+
(group.textContent || '').startsWith('Needs attention')
|
|
2253
|
+
);
|
|
2254
|
+
await expect(attention).toBeTruthy();
|
|
2255
|
+
for (const field of ['expected', 'min', 'max']) {
|
|
2256
|
+
await waitFor(
|
|
2257
|
+
() => expect(attention!.querySelector(`[data-field="${field}"]`)).toBeTruthy(),
|
|
2258
|
+
{ timeout: 10000 }
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
},
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2156
2264
|
export const CompactConditionalMessage: Story = {
|
|
2157
2265
|
parameters: {
|
|
2158
2266
|
docs: {
|
|
@@ -2616,6 +2724,63 @@ const _expandOptionalBox = async () => {
|
|
|
2616
2724
|
}
|
|
2617
2725
|
};
|
|
2618
2726
|
|
|
2727
|
+
/**
|
|
2728
|
+
* Put one row of the Optional box on screen: open the box, and its "N more
|
|
2729
|
+
* optional fields" fold if the row is behind it. A required field locked by an
|
|
2730
|
+
* unmet dependency waits here rather than under "Needs attention", and a box
|
|
2731
|
+
* that shows its preselected rows folds the rest.
|
|
2732
|
+
*/
|
|
2733
|
+
const _revealOptionalRow = async (field: string): Promise<{ refold: () => Promise<void> }> => {
|
|
2734
|
+
await _expandOptionalBox();
|
|
2735
|
+
const row = () => document.querySelector(`.readfirst-row[data-field="${field}"]`);
|
|
2736
|
+
const opensFold = !row();
|
|
2737
|
+
if (opensFold) {
|
|
2738
|
+
await _testsClickButton({ selector: '.options-readfirst-more' });
|
|
2739
|
+
}
|
|
2740
|
+
await waitFor(() => expect(row()).toBeTruthy(), { timeout: 10000 });
|
|
2741
|
+
return {
|
|
2742
|
+
/** Close the fold again if this opened it, so the form is laid out as before. */
|
|
2743
|
+
refold: async () => {
|
|
2744
|
+
if (opensFold) {
|
|
2745
|
+
await fireEvent.click(document.querySelector('.options-readfirst-more') as HTMLElement);
|
|
2746
|
+
await waitFor(() => expect(row()).toBeNull(), { timeout: 10000 });
|
|
2747
|
+
}
|
|
2748
|
+
},
|
|
2749
|
+
};
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
/** Every scrolled element in the page, with its scroll offsets. */
|
|
2753
|
+
const _scrollPositions = () =>
|
|
2754
|
+
new Map(
|
|
2755
|
+
[document.scrollingElement, ...Array.from(document.querySelectorAll('*'))]
|
|
2756
|
+
.filter((element): element is Element => !!element)
|
|
2757
|
+
.map((element) => [element, [element.scrollTop, element.scrollLeft] as const])
|
|
2758
|
+
);
|
|
2759
|
+
|
|
2760
|
+
/**
|
|
2761
|
+
* Run `check` and leave the page scrolled exactly as it was. Opening a box or a
|
|
2762
|
+
* fold to reach a row moves focus and scrolls the frame, and a story whose
|
|
2763
|
+
* snapshot is its reference frame must not end somewhere else.
|
|
2764
|
+
*/
|
|
2765
|
+
const _withoutScrolling = async (check: () => Promise<void>) => {
|
|
2766
|
+
const before = _scrollPositions();
|
|
2767
|
+
await check();
|
|
2768
|
+
const elements = [document.scrollingElement, ...Array.from(document.querySelectorAll('*'))];
|
|
2769
|
+
for (const element of elements) {
|
|
2770
|
+
if (!element) {
|
|
2771
|
+
continue;
|
|
2772
|
+
}
|
|
2773
|
+
const [top, left] = before.get(element) ?? [0, 0];
|
|
2774
|
+
if (element.scrollTop !== top || element.scrollLeft !== left) {
|
|
2775
|
+
element.scrollTop = top;
|
|
2776
|
+
element.scrollLeft = left;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
(document.activeElement as HTMLElement | null)?.blur?.();
|
|
2780
|
+
const top = before.get(document.scrollingElement as Element)?.[0] ?? 0;
|
|
2781
|
+
await waitFor(() => expect(document.scrollingElement?.scrollTop ?? 0).toBe(top));
|
|
2782
|
+
};
|
|
2783
|
+
|
|
2619
2784
|
// CompactSchema plus one optional (non-preselected) field, to exercise the
|
|
2620
2785
|
// "Fields" menu add / select-all / reset actions.
|
|
2621
2786
|
const CompactFieldsMenuSchema: Record<string, TCompactField> = {
|
|
@@ -2785,9 +2950,10 @@ export const CompactReadOnly: Story = {
|
|
|
2785
2950
|
},
|
|
2786
2951
|
play: async () => {
|
|
2787
2952
|
await _testsWaitForText('order-fulfilment');
|
|
2788
|
-
// Nothing that reports progress: no meter, no
|
|
2953
|
+
// Nothing that reports progress: no meter, no Incomplete/Ready, no boxes.
|
|
2789
2954
|
expect(document.querySelector('.options-readfirst-completion')).toBeNull();
|
|
2790
2955
|
expect(document.querySelector('.options-readfirst-group')).toBeNull();
|
|
2956
|
+
await _testsWaitForTextToNotExist('Incomplete');
|
|
2791
2957
|
await _testsWaitForTextToNotExist('Draft');
|
|
2792
2958
|
await _testsWaitForTextToNotExist('Needs attention');
|
|
2793
2959
|
await _testsWaitForTextToNotExist('Optional');
|
|
@@ -2924,9 +3090,10 @@ export const CompactBasic: Story = {
|
|
|
2924
3090
|
value: basicFormValue,
|
|
2925
3091
|
},
|
|
2926
3092
|
play: async () => {
|
|
2927
|
-
// Unresolved required/invalid fields → the header
|
|
2928
|
-
//
|
|
2929
|
-
await _testsWaitForText('
|
|
3093
|
+
// Unresolved required/invalid fields → the header says Incomplete. Never
|
|
3094
|
+
// "Draft": nobody has changed this form, and Draft means unsaved changes.
|
|
3095
|
+
await _testsWaitForText('Incomplete');
|
|
3096
|
+
await _testsWaitForTextToNotExist('Draft');
|
|
2930
3097
|
// Several asserted/clicked fields (Disabled option, …) are empty optionals in
|
|
2931
3098
|
// the collapsed Optional box — open it so they're on screen.
|
|
2932
3099
|
await _expandOptionalBox();
|
|
@@ -2955,7 +3122,9 @@ export const CompactBasic: Story = {
|
|
|
2955
3122
|
).toBeFalsy();
|
|
2956
3123
|
|
|
2957
3124
|
// Dependency locks are navigable: the lock's popover lists the blockers
|
|
2958
|
-
// with their state; clicking one scrolls to + flashes it.
|
|
3125
|
+
// with their state; clicking one scrolls to + flashes it. The locked row is
|
|
3126
|
+
// required, but locked it needs no attention, so it waits in Optional.
|
|
3127
|
+
const lockedRow = await _revealOptionalRow('optionWithShortDescription');
|
|
2959
3128
|
const depLock = document.querySelector(
|
|
2960
3129
|
'.readfirst-row[data-field="optionWithShortDescription"] .options-readfirst-lock-deps'
|
|
2961
3130
|
) as HTMLElement;
|
|
@@ -2984,6 +3153,10 @@ export const CompactBasic: Story = {
|
|
|
2984
3153
|
{ timeout: 10000 }
|
|
2985
3154
|
);
|
|
2986
3155
|
await fireEvent.click(depLock); // close the popover
|
|
3156
|
+
// Fold the Optional box's extra rows back, so the rest of the play (and the
|
|
3157
|
+
// captured frame) sees the form laid out as before; once unlocked, the row
|
|
3158
|
+
// moves to Needs attention and is on screen again.
|
|
3159
|
+
await lockedRow.refold();
|
|
2987
3160
|
|
|
2988
3161
|
// Fulfilling the dependency UNLOCKS the dependent row and flashes it.
|
|
2989
3162
|
await _testsClickText('basicOption');
|
|
@@ -3612,7 +3785,7 @@ export const CompactSensitive: Story = {
|
|
|
3612
3785
|
};
|
|
3613
3786
|
|
|
3614
3787
|
// `rules: ['valid_identifier']` flows from the schema into validation: a bad
|
|
3615
|
-
// identifier marks the form invalid (banner +
|
|
3788
|
+
// identifier marks the form invalid (banner + Incomplete status).
|
|
3616
3789
|
export const CompactValidIdentifierRule: Story = {
|
|
3617
3790
|
parameters: {
|
|
3618
3791
|
docs: {
|
|
@@ -3641,7 +3814,7 @@ export const CompactValidIdentifierRule: Story = {
|
|
|
3641
3814
|
await _testsWaitForText('1-bad-identifier');
|
|
3642
3815
|
// The rules-driven validation marks the form as needing attention — the
|
|
3643
3816
|
// dedicated "Needs attention" box (and the header link) signal it.
|
|
3644
|
-
await _testsWaitForText('
|
|
3817
|
+
await _testsWaitForText('Incomplete');
|
|
3645
3818
|
await _testsWaitForText('Needs attention');
|
|
3646
3819
|
},
|
|
3647
3820
|
};
|
|
@@ -5801,12 +5974,12 @@ export const CompactRequiredGroups: Story = {
|
|
|
5801
5974
|
value: {} as IOptions,
|
|
5802
5975
|
},
|
|
5803
5976
|
play: async () => {
|
|
5804
|
-
// All three members show the required placeholder + the
|
|
5977
|
+
// All three members show the required placeholder + the Incomplete status. The two
|
|
5805
5978
|
// contiguous members (byHost/byFile in Connection) cluster into a rail, which
|
|
5806
5979
|
// carries the grouping in place of a chip; only the lone member (byUrl in
|
|
5807
5980
|
// General) keeps a "One of" chip — so exactly one chip, not three.
|
|
5808
5981
|
await _testsWaitForTextsCount('—', undefined, 3);
|
|
5809
|
-
await _testsWaitForText('
|
|
5982
|
+
await _testsWaitForText('Incomplete');
|
|
5810
5983
|
await _testsWaitForTextsCount('One of', undefined, 1);
|
|
5811
5984
|
|
|
5812
5985
|
// The chip is a ReqoreDropdown listing the siblings; selecting one flashes
|
|
@@ -5861,7 +6034,7 @@ export const CompactRequiredGroups: Story = {
|
|
|
5861
6034
|
await _testsClickButton({ selector: '.options-readfirst-done' });
|
|
5862
6035
|
await _testsWaitForText('https://example.com');
|
|
5863
6036
|
|
|
5864
|
-
// One fulfilled member satisfies the group → the
|
|
6037
|
+
// One fulfilled member satisfies the group → the status flips to Ready and the
|
|
5865
6038
|
// Once satisfied: the filled member keeps a "Covers" chip; the empty siblings
|
|
5866
6039
|
// show their "Covered by 'By URL'" note INLINE (not a chip), and no "One of"
|
|
5867
6040
|
// remains. So exactly one required-group chip stays (the coverer's).
|
|
@@ -6468,10 +6641,30 @@ export const CompactShowcase: Story = {
|
|
|
6468
6641
|
await expect(document.querySelectorAll('.options-readfirst-info-panel').length).toBeGreaterThan(
|
|
6469
6642
|
0
|
|
6470
6643
|
);
|
|
6471
|
-
// The unmet-dependency hint surfaces on the dependent field's row
|
|
6472
|
-
|
|
6473
|
-
|
|
6644
|
+
// The unmet-dependency hint surfaces on the dependent field's row — locked,
|
|
6645
|
+
// so in the Optional box rather than under "Needs attention". Not under
|
|
6646
|
+
// Needs attention is a DOM fact; reaching the row means opening the
|
|
6647
|
+
// Optional box's fold, which scrolls, so the frame is put back afterwards:
|
|
6648
|
+
// this story's snapshot is the form's top.
|
|
6649
|
+
const attentionBox = Array.from(document.querySelectorAll('.options-readfirst-group')).find(
|
|
6650
|
+
(group) => (group.textContent || '').startsWith('Needs attention')
|
|
6474
6651
|
);
|
|
6652
|
+
await expect(
|
|
6653
|
+
attentionBox?.querySelector('[data-field="optionWithShortDescription"]')
|
|
6654
|
+
).toBeFalsy();
|
|
6655
|
+
await _withoutScrolling(async () => {
|
|
6656
|
+
const lockedRow = await _revealOptionalRow('optionWithShortDescription');
|
|
6657
|
+
const optionalBox = Array.from(document.querySelectorAll('.options-readfirst-group')).find(
|
|
6658
|
+
(group) => (group.textContent || '').startsWith('Optional')
|
|
6659
|
+
);
|
|
6660
|
+
await expect(
|
|
6661
|
+
optionalBox?.querySelector('[data-field="optionWithShortDescription"]')
|
|
6662
|
+
).toBeTruthy();
|
|
6663
|
+
await _testsWaitForText(
|
|
6664
|
+
'This field is disabled because some dependencies are not fulfilled: "basicOption"'
|
|
6665
|
+
);
|
|
6666
|
+
await lockedRow.refold();
|
|
6667
|
+
});
|
|
6475
6668
|
// ONE info mechanism: no subtitle lines, no badge — just the panel + ⓘ.
|
|
6476
6669
|
await expect(document.querySelectorAll('.options-readfirst-subtitle')).toHaveLength(0);
|
|
6477
6670
|
await expect(document.querySelectorAll('.options-readfirst-info-badge')).toHaveLength(0);
|
|
@@ -76,7 +76,6 @@ import {
|
|
|
76
76
|
ITemplateFieldProps,
|
|
77
77
|
TCustomTemplateItems,
|
|
78
78
|
TemplateField,
|
|
79
|
-
isValueTemplate,
|
|
80
79
|
} from '../fields/template/TemplateField';
|
|
81
80
|
import { CompactRow } from './CompactRow';
|
|
82
81
|
import { FormFieldsSkeleton } from './FormFieldsSkeleton';
|
|
@@ -115,7 +114,6 @@ import { OptionsHelpDialog } from './OptionsHelpDialog';
|
|
|
115
114
|
import { firstDeclaredType, isUntypedOptionType } from '../../../helpers/optionUiTypes';
|
|
116
115
|
import {
|
|
117
116
|
TReadFirstStatus,
|
|
118
|
-
findAllowedValueOption,
|
|
119
117
|
getFirstAttentionOptionName,
|
|
120
118
|
getOptionGroup,
|
|
121
119
|
getOptionGroupLabel,
|
|
@@ -522,24 +520,15 @@ export const fixOptions = (
|
|
|
522
520
|
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
523
521
|
) {
|
|
524
522
|
obj = option.default_value as IQorusFormField;
|
|
525
|
-
} else if (
|
|
526
|
-
option.default_view === 'expression' &&
|
|
527
|
-
(fixedValue[name] as IQorusFormField)?.value === undefined
|
|
528
|
-
) {
|
|
529
|
-
// A field the schema declares as opening in expression mode starts as an
|
|
530
|
-
// EMPTY expression, not as its raw default. `isDefaultFunction` already
|
|
531
|
-
// makes the renderer open that way, so without this the data disagreed
|
|
532
|
-
// with what the operator was looking at: an expression editor whose
|
|
533
|
-
// value was still the plain default, and no `is_expression` on the field
|
|
534
|
-
// until the first edit.
|
|
535
|
-
obj = {
|
|
536
|
-
type,
|
|
537
|
-
value: {
|
|
538
|
-
args: [],
|
|
539
|
-
},
|
|
540
|
-
is_expression: true,
|
|
541
|
-
};
|
|
542
523
|
} else {
|
|
524
|
+
// A field declared with `default_view: 'expression'` is NOT seeded with
|
|
525
|
+
// an empty expression here. This runs on every load, so a seed written
|
|
526
|
+
// here changed a saved object merely by opening it: an existing Qog
|
|
527
|
+
// state gained `{args: []}` the moment its panel opened, and the editor
|
|
528
|
+
// autosaved a draft nobody made. The renderer already opens such a
|
|
529
|
+
// field in expression mode from the schema (`isDefaultFunction`), the
|
|
530
|
+
// first edit stores `is_expression`, and a host that creates a NEW
|
|
531
|
+
// object seeds the expression itself, at creation.
|
|
543
532
|
obj = {
|
|
544
533
|
type,
|
|
545
534
|
value:
|
|
@@ -596,34 +585,15 @@ export const fixOptions = (
|
|
|
596
585
|
};
|
|
597
586
|
}
|
|
598
587
|
|
|
599
|
-
// A value that is not one of the declared choices is
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
// the
|
|
607
|
-
//
|
|
608
|
-
// saying it has no reference values to offer right now (the app-action
|
|
609
|
-
// catalogue ships exactly that, with an
|
|
610
|
-
// `option_reference_values_unavailable` message attached). An empty array
|
|
611
|
-
// is truthy, so testing the array itself made every such option erase the
|
|
612
|
-
// operator's stored value on the first render, before the
|
|
613
|
-
// connection-refreshed schema arrived to say otherwise — and the emptied
|
|
614
|
-
// form then autosaved over the draft, so the value was gone for good.
|
|
615
|
-
// Only an option that actually declares choices can have a value that is
|
|
616
|
-
// not one of them.
|
|
617
|
-
if (
|
|
618
|
-
newOption.value !== undefined &&
|
|
619
|
-
options?.[optionName]?.allowed_values?.length &&
|
|
620
|
-
!findAllowedValueOption(newOption.value, options?.[optionName]) &&
|
|
621
|
-
!isValueTemplate(newOption.value) &&
|
|
622
|
-
!options?.[optionName]?.multiselect &&
|
|
623
|
-
!options?.[optionName]?.allowed_values_creatable
|
|
624
|
-
) {
|
|
625
|
-
newOption.value = undefined;
|
|
626
|
-
}
|
|
588
|
+
// A value that is not one of the declared choices is KEPT. It used to be
|
|
589
|
+
// dropped here, on load, and the dropped value was three times a real
|
|
590
|
+
// one — a bare-declared choice, a document naming a choice, a value whose
|
|
591
|
+
// choices had not arrived yet — that the emptied form then autosaved, so
|
|
592
|
+
// it was gone for good. Whether a value is one of the choices is now the
|
|
593
|
+
// validator's question (`getUnlistedChoiceReason`, asked through
|
|
594
|
+
// `validateFieldWithResult`): such a field is invalid, sits under "Needs
|
|
595
|
+
// attention" saying which value is not a choice, and the form reports
|
|
596
|
+
// itself invalid so the host blocks submitting it.
|
|
627
597
|
|
|
628
598
|
if (
|
|
629
599
|
newOption.value &&
|
|
@@ -1869,8 +1839,13 @@ const FormEngineImpl = ({
|
|
|
1869
1839
|
) ?
|
|
1870
1840
|
schemaType
|
|
1871
1841
|
: schemaType || ((fields[optionName] as IQorusFormField)?.type as TQorusType);
|
|
1842
|
+
// A field reports back the type it was drawn as, which for a renderer-only editor (markdown, cron,
|
|
1843
|
+
// dpql, ...) is the editor's name rather than the type the value is stored as. Storing that name
|
|
1844
|
+
// sent a description as `{type: "markdown"}`, which the server decodes by type and so parsed as
|
|
1845
|
+
// YAML. A type picked for an untyped field is never renderer-only, so it still wins.
|
|
1846
|
+
const emittedType = _type && !isRendererOnly(_type as TQorusType) ? _type : undefined;
|
|
1872
1847
|
const type =
|
|
1873
|
-
|
|
1848
|
+
emittedType ||
|
|
1874
1849
|
getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
|
|
1875
1850
|
|
|
1876
1851
|
if (!(fields as TQorusForm)[optionName]) {
|
|
@@ -1969,7 +1944,15 @@ const FormEngineImpl = ({
|
|
|
1969
1944
|
|
|
1970
1945
|
onSingleOptionsChange?.(optionName, updatedValue[optionName]);
|
|
1971
1946
|
|
|
1972
|
-
if (
|
|
1947
|
+
if (
|
|
1948
|
+
compact &&
|
|
1949
|
+
!readOnly &&
|
|
1950
|
+
shouldAutoCollapseCompactOption(
|
|
1951
|
+
options?.[optionName],
|
|
1952
|
+
val,
|
|
1953
|
+
!!(updatedValue[optionName] as { is_expression?: boolean } | undefined)?.is_expression
|
|
1954
|
+
)
|
|
1955
|
+
) {
|
|
1973
1956
|
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
1974
1957
|
}
|
|
1975
1958
|
|
|
@@ -2324,10 +2307,13 @@ const FormEngineImpl = ({
|
|
|
2324
2307
|
|
|
2325
2308
|
const isOptionValid = useCallback(
|
|
2326
2309
|
(optionName: string, type: TQorusType, optionValue: any) => {
|
|
2310
|
+
const isEmpty = optionValue === undefined || optionValue === '';
|
|
2311
|
+
// An empty field locked by an unmet `depends_on` does not apply, so it is
|
|
2312
|
+
// not missing — the same rule the read-first status follows.
|
|
2327
2313
|
if (
|
|
2328
|
-
|
|
2329
|
-
!options?.[optionName]?.required_groups
|
|
2330
|
-
|
|
2314
|
+
isEmpty &&
|
|
2315
|
+
((!options?.[optionName]?.required && !options?.[optionName]?.required_groups) ||
|
|
2316
|
+
dependencyLockedNames.includes(optionName))
|
|
2331
2317
|
) {
|
|
2332
2318
|
return true;
|
|
2333
2319
|
}
|
|
@@ -2342,7 +2328,12 @@ const FormEngineImpl = ({
|
|
|
2342
2328
|
isFunction: (availableOptions?.[optionName] as { is_expression?: boolean })?.is_expression,
|
|
2343
2329
|
} as any);
|
|
2344
2330
|
},
|
|
2345
|
-
[
|
|
2331
|
+
[
|
|
2332
|
+
JSON.stringify(options),
|
|
2333
|
+
JSON.stringify(availableOptions),
|
|
2334
|
+
JSON.stringify(localValue.fields),
|
|
2335
|
+
dependencyLockedNames,
|
|
2336
|
+
]
|
|
2346
2337
|
);
|
|
2347
2338
|
|
|
2348
2339
|
const getValidityData = useCallback((): IFormValidityData => {
|
|
@@ -2366,7 +2357,17 @@ const FormEngineImpl = ({
|
|
|
2366
2357
|
|
|
2367
2358
|
let validation: IValidationResult;
|
|
2368
2359
|
|
|
2369
|
-
|
|
2360
|
+
/* A required field locked by an unmet `depends_on` does not apply
|
|
2361
|
+
while it is locked: it is not in "Needs attention", and an empty one
|
|
2362
|
+
does not make the form incomplete either. Otherwise a check whose
|
|
2363
|
+
kind is Equals could never be saved, because Minimum — required for
|
|
2364
|
+
Between alone — is empty; and a form whose only gaps are fields
|
|
2365
|
+
nobody can fill in would say Incomplete with nothing to act on. A
|
|
2366
|
+
locked field that still HOLDS a value is validated as before. */
|
|
2367
|
+
if (
|
|
2368
|
+
isEmpty &&
|
|
2369
|
+
((!isRequired && !hasRequiredGroups) || dependencyLockedNames.includes(optionName))
|
|
2370
|
+
) {
|
|
2370
2371
|
validation = { isValid: true, reasons: [] };
|
|
2371
2372
|
} else {
|
|
2372
2373
|
validation = validateFieldWithResult(getType(type), optionValue, {
|
|
@@ -2404,6 +2405,7 @@ const FormEngineImpl = ({
|
|
|
2404
2405
|
JSON.stringify(options),
|
|
2405
2406
|
JSON.stringify(operators),
|
|
2406
2407
|
JSON.stringify(localValue.fields),
|
|
2408
|
+
dependencyLockedNames,
|
|
2407
2409
|
]);
|
|
2408
2410
|
|
|
2409
2411
|
const validityData = useMemo(() => getValidityData(), [getValidityData]);
|
|
@@ -2487,7 +2489,14 @@ const FormEngineImpl = ({
|
|
|
2487
2489
|
// sub-form it summarises said 0/3 set.
|
|
2488
2490
|
const empty = isOptionValueEmpty(value) || isUnsetSchemaHash(value, schema);
|
|
2489
2491
|
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
2490
|
-
|
|
2492
|
+
// Required only while it applies. A field locked by an unmet `depends_on`
|
|
2493
|
+
// renders disabled with what unlocks it; nothing the author can do to it
|
|
2494
|
+
// is a next step — the next step is the field it depends on, which carries
|
|
2495
|
+
// its own status. Counting it here put a guided check's locked Path /
|
|
2496
|
+
// Minimum / Maximum under "Needs attention" before its kind was even
|
|
2497
|
+
// picked. The moment the dependency holds, it is required again.
|
|
2498
|
+
const required =
|
|
2499
|
+
!dependencyLockedNames.includes(name) && !!(schema?.required || reqGroups.length);
|
|
2491
2500
|
const covered =
|
|
2492
2501
|
empty &&
|
|
2493
2502
|
reqGroups.some((g) => {
|
|
@@ -2512,11 +2521,15 @@ const FormEngineImpl = ({
|
|
|
2512
2521
|
isOptionValid,
|
|
2513
2522
|
requiredGroupsInfo,
|
|
2514
2523
|
schemaMsgIntent,
|
|
2524
|
+
dependencyLockedNames,
|
|
2515
2525
|
]
|
|
2516
2526
|
);
|
|
2517
2527
|
const getOptionBucket = useCallback(
|
|
2518
2528
|
(name: string, hidden = false): 'attention' | 'set' | 'optional' => {
|
|
2519
|
-
|
|
2529
|
+
// A locked one-of member cannot be the one that satisfies its group, so it
|
|
2530
|
+
// does not travel with the group into "Needs attention"; its own status
|
|
2531
|
+
// (not required while locked) decides its box.
|
|
2532
|
+
if (!hidden && !dependencyLockedNames.includes(name)) {
|
|
2520
2533
|
const reqGroups = (options?.[name]?.required_groups as string[] | undefined) || [];
|
|
2521
2534
|
if (reqGroups.length) {
|
|
2522
2535
|
return reqGroups.some((g) => !requiredGroupsInfo.satisfiedBy[g]) ? 'attention' : 'set';
|
|
@@ -2524,7 +2537,7 @@ const FormEngineImpl = ({
|
|
|
2524
2537
|
}
|
|
2525
2538
|
return getReadFirstBucket(getOptionStatus(name, hidden));
|
|
2526
2539
|
},
|
|
2527
|
-
[JSON.stringify(options), requiredGroupsInfo, getOptionStatus]
|
|
2540
|
+
[JSON.stringify(options), requiredGroupsInfo, getOptionStatus, dependencyLockedNames]
|
|
2528
2541
|
);
|
|
2529
2542
|
// How many fields are in the "Needs attention" box — drives the header link.
|
|
2530
2543
|
const readFirstAttentionCount = useMemo(
|
|
@@ -358,6 +358,9 @@ export const getOptionFieldMessages = ({
|
|
|
358
358
|
}: IOptionFieldMessagesProps): IReqoreTagProps[] => {
|
|
359
359
|
const optionSchema = schema[name];
|
|
360
360
|
const result: IReqoreTagProps[] = [];
|
|
361
|
+
const locked =
|
|
362
|
+
!!optionSchema?.depends_on &&
|
|
363
|
+
!hasAllDependenciesFullfilled(optionSchema.depends_on, allOptions, schema);
|
|
361
364
|
|
|
362
365
|
if (option.value || option.value === false || option.value === 0 || option.value === null) {
|
|
363
366
|
/* A concrete type the VALUE carries beats an untyped schema.
|
|
@@ -392,7 +395,9 @@ export const getOptionFieldMessages = ({
|
|
|
392
395
|
});
|
|
393
396
|
}
|
|
394
397
|
} else {
|
|
395
|
-
|
|
398
|
+
// A locked field is not asked for: the dependency message below says what
|
|
399
|
+
// unlocks it, and until then there is nothing to fill in.
|
|
400
|
+
if (optionSchema?.required && !untouched && !locked) {
|
|
396
401
|
result.push({ label: 'This field is required', intent: 'danger' });
|
|
397
402
|
}
|
|
398
403
|
|
|
@@ -413,10 +418,7 @@ export const getOptionFieldMessages = ({
|
|
|
413
418
|
}
|
|
414
419
|
}
|
|
415
420
|
|
|
416
|
-
if (
|
|
417
|
-
optionSchema?.depends_on &&
|
|
418
|
-
!hasAllDependenciesFullfilled(optionSchema.depends_on, allOptions, schema)
|
|
419
|
-
) {
|
|
421
|
+
if (locked) {
|
|
420
422
|
const dependsOn = describeDependencies(optionSchema.depends_on, schema);
|
|
421
423
|
|
|
422
424
|
result.push({
|
|
@@ -11,6 +11,7 @@ import { IExpressionValue } from '../expressions/types';
|
|
|
11
11
|
import yaml from 'js-yaml';
|
|
12
12
|
import { formatTimeoutValue, richtextToString } from '../../../helpers/common';
|
|
13
13
|
import { firstDeclaredType } from '../../../helpers/optionUiTypes';
|
|
14
|
+
import { findAllowedValueOption } from '../../../helpers/allowedValues';
|
|
14
15
|
|
|
15
16
|
/** "N noun" with naive pluralisation ("1 item", "2 items"). */
|
|
16
17
|
const pluralize = (count: number, noun: string): string =>
|
|
@@ -244,65 +245,33 @@ export const isCompactBooleanOption = (schema: TQorusFormFieldSchema | undefined
|
|
|
244
245
|
return type === 'bool' || type === 'boolean';
|
|
245
246
|
};
|
|
246
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Whether a change completes a compact row, so its editor can close.
|
|
250
|
+
*
|
|
251
|
+
* Only a VALUE does: Yes or No, or one of a fixed list. An expression is
|
|
252
|
+
* built one step at a time - an operation, then each of its arguments - and
|
|
253
|
+
* every step is a change; closing the editor on the first one left a bool
|
|
254
|
+
* field's condition at `null > null`, with no way to fill in its arguments
|
|
255
|
+
* short of opening the row again for each of them.
|
|
256
|
+
*
|
|
257
|
+
* @param isExpression the row now holds an expression (`is_expression`)
|
|
258
|
+
*/
|
|
247
259
|
export const shouldAutoCollapseCompactOption = (
|
|
248
260
|
schema: TQorusFormFieldSchema | undefined,
|
|
249
|
-
value: unknown
|
|
261
|
+
value: unknown,
|
|
262
|
+
isExpression?: boolean
|
|
250
263
|
): boolean =>
|
|
264
|
+
!isExpression &&
|
|
251
265
|
!isOptionValueEmpty(value) &&
|
|
252
266
|
(isCompactBooleanOption(schema) || isFixedCompactAllowedValueOption(schema));
|
|
253
267
|
|
|
254
268
|
/**
|
|
255
269
|
* Prefer the matching allowed_values entry's display_name (fallback `name`)
|
|
256
|
-
* over the raw stored value.
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* each element, not the list itself — so a multi-select that was missing here
|
|
260
|
-
* printed what it stores rather than what you picked: `orders, batch` for
|
|
261
|
-
* fields whose picker reads "Orders, Batch". The gap shows worst exactly where
|
|
262
|
-
* allowed values earn their keep, since the stored form is often not readable
|
|
263
|
-
* at all (a permission code, `PO_REQUIRE_TYPES`, an app-specific id).
|
|
264
|
-
*
|
|
265
|
-
* Exported (as `findAllowedValueOption`) because the form engine's value
|
|
266
|
-
* validation has to answer the SAME question — "is this stored value one of the
|
|
267
|
-
* declared choices?" — and answering it differently loses data. An
|
|
268
|
-
* `allowed_values` entry is written three ways: an envelope (`{value: {type,
|
|
269
|
-
* value}}`), a bare value (`{value: 'default'}`) or a named entry
|
|
270
|
-
* (`{name: 'default'}`). This has always accepted all three; the engine's
|
|
271
|
-
* clearing guard accepted only the first and the third, so a schema using the
|
|
272
|
-
* bare form had its value ERASED on load while this function went on rendering
|
|
273
|
-
* the display name for it — the row read "Default RBAC" collapsed and "—" when
|
|
274
|
-
* opened, and the value was gone from the submitted data.
|
|
270
|
+
* over the raw stored value. The predicate lives in `helpers/allowedValues`,
|
|
271
|
+
* where the validator uses it too, so a value the row can name is exactly a
|
|
272
|
+
* value the form accepts.
|
|
275
273
|
*/
|
|
276
|
-
export
|
|
277
|
-
value: unknown,
|
|
278
|
-
schema?: TQorusFormFieldSchema
|
|
279
|
-
): any | undefined => {
|
|
280
|
-
const s = schema as
|
|
281
|
-
{ allowed_values?: any[]; element_allowed_values?: any[]; items?: any[] } | undefined;
|
|
282
|
-
const options =
|
|
283
|
-
(s?.allowed_values?.length && s.allowed_values) ||
|
|
284
|
-
(s?.element_allowed_values?.length && s.element_allowed_values) ||
|
|
285
|
-
s?.items;
|
|
286
|
-
if (!options?.length) {
|
|
287
|
-
return undefined;
|
|
288
|
-
}
|
|
289
|
-
// A stored element can be the bare value or the typed `{type, value}`
|
|
290
|
-
// envelope the form engine round-trips; match either against either.
|
|
291
|
-
const stored =
|
|
292
|
-
value && typeof value === 'object' && 'value' in (value as Record<string, unknown>) ?
|
|
293
|
-
(value as Record<string, unknown>).value
|
|
294
|
-
: value;
|
|
295
|
-
|
|
296
|
-
return options.find(
|
|
297
|
-
(option) =>
|
|
298
|
-
option?.value?.value === value ||
|
|
299
|
-
option?.value === value ||
|
|
300
|
-
option?.name === value ||
|
|
301
|
-
option?.value?.value === stored ||
|
|
302
|
-
option?.value === stored ||
|
|
303
|
-
option?.name === stored
|
|
304
|
-
);
|
|
305
|
-
};
|
|
274
|
+
export { findAllowedValueOption };
|
|
306
275
|
|
|
307
276
|
const getAllowedValueLabel = (
|
|
308
277
|
value: unknown,
|
|
@@ -112,7 +112,9 @@ const Meter = ({ form }: { form: TVariantForm }) => {
|
|
|
112
112
|
color: (s.attention ? c.warning : c.success) as never,
|
|
113
113
|
}}
|
|
114
114
|
>
|
|
115
|
-
{
|
|
115
|
+
{/* What the form lacks, not whether it has unsaved changes:
|
|
116
|
+
"Draft" is the batched mode's per-row chip alone. */}
|
|
117
|
+
{s.attention ? 'Incomplete' : 'Ready'}
|
|
116
118
|
</ReqoreP>
|
|
117
119
|
<span>
|
|
118
120
|
{s.set} of {s.total} set
|