@qoretechnologies/reqraft 0.10.18 → 0.10.20
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/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +25 -11
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +6 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +44 -18
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +1 -1
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +7 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +32 -1
- package/dist/components/form/engine/readFirst.js.map +1 -1
- 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.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +3 -3
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- 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/package.json +3 -3
- package/src/components/form/engine/CompactRow.tsx +46 -29
- package/src/components/form/engine/FormEngine.stories.tsx +99 -2
- package/src/components/form/engine/FormEngine.tsx +51 -8
- package/src/components/form/engine/compactRowStyles.ts +2 -1
- package/src/components/form/engine/readFirst.ts +42 -0
- package/src/components/form/expressions/ExpressionField.stories.tsx +10 -0
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +26 -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.tsx +10 -1
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -36
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +1 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +2 -0
- package/src/components/form/fields/boolean/Boolean.stories.tsx +34 -0
- package/src/components/form/fields/boolean/Boolean.tsx +1 -1
- package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +14 -10
- package/src/components/form/fields/select/Select.stories.tsx +4 -0
- package/src/components/form/fields/select/Select.tsx +38 -12
- package/src/components/form/fields/select/SelectCollection.tsx +78 -17
- package/src/stores/currentUser/currentUser.stories.tsx +32 -0
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
getHashEntries,
|
|
60
60
|
getReadFirstStatus,
|
|
61
61
|
getValueType,
|
|
62
|
+
isFixedCompactAllowedValueOption,
|
|
62
63
|
isOptionValueEmpty,
|
|
63
64
|
optionHasImages,
|
|
64
65
|
} from './readFirst';
|
|
@@ -95,7 +96,7 @@ const COMPACT_COMPLEX_TYPES = new Set([
|
|
|
95
96
|
|
|
96
97
|
// Card editors that hold a SINGLE value through one text-bearing input — their
|
|
97
98
|
// built-in ReqoreInput clear duplicates the card's own "Clear value" action, so
|
|
98
|
-
// we suppress it (the card
|
|
99
|
+
// we suppress it (the card trash action is the single source). Structural/multi-input
|
|
99
100
|
// editors (hash, list, schema…) and polymorphic ones (auto/any) are NOT listed:
|
|
100
101
|
// their per-sub-field clears are distinct affordances and must stay, while the
|
|
101
102
|
// card ✕ clears the whole value. (Operators force a card on a scalar value, so
|
|
@@ -520,7 +521,11 @@ export const CompactRow = memo(
|
|
|
520
521
|
/>
|
|
521
522
|
</span>
|
|
522
523
|
: null;
|
|
523
|
-
const
|
|
524
|
+
const fixedAllowedValueOption = isFixedCompactAllowedValueOption(schema);
|
|
525
|
+
const editType =
|
|
526
|
+
(fixedAllowedValueOption
|
|
527
|
+
? (schema?.type as string) || (schema?.ui_type as string)
|
|
528
|
+
: (schema?.ui_type as string) || (schema?.type as string)) ?? '';
|
|
524
529
|
// Scalars edit in place inside the row; complex fields (tall or nested
|
|
525
530
|
// editors) still open the expanded card below.
|
|
526
531
|
const inlineEditable =
|
|
@@ -573,8 +578,9 @@ export const CompactRow = memo(
|
|
|
573
578
|
// — distinct from the read-row "Remove field" (which deletes an optional
|
|
574
579
|
// field entirely). Once cleared, `changed` flips on and `hasValue` off, so
|
|
575
580
|
// this button gives way to `revertButton` in the same slot — the morph the
|
|
576
|
-
// edit row's actions are designed around.
|
|
577
|
-
//
|
|
581
|
+
// edit row's actions are designed around. Clearing is destructive even
|
|
582
|
+
// though it can be reverted before saving, so both the trigger and its
|
|
583
|
+
// confirmation use danger intent.
|
|
578
584
|
//
|
|
579
585
|
// Only editors with NO built-in clear of their own get this button —
|
|
580
586
|
// toggles and fixed-choice pickers. The text/number/date inputs already
|
|
@@ -587,6 +593,17 @@ export const CompactRow = memo(
|
|
|
587
593
|
// CREATABLE one still renders the raw input (with its own ✕), so it keeps
|
|
588
594
|
// its clear — matching the dispatch in AutoFormField/TemplateField.
|
|
589
595
|
(!!size(schema?.allowed_values) && !schema?.allowed_values_creatable);
|
|
596
|
+
const confirmClearValue = (event?: React.MouseEvent) => {
|
|
597
|
+
event?.stopPropagation();
|
|
598
|
+
confirmAction({
|
|
599
|
+
title: 'Clear value',
|
|
600
|
+
description: `Clear the current value of "${label}"?`,
|
|
601
|
+
confirmLabel: 'Clear value',
|
|
602
|
+
confirmButtonIntent: 'danger',
|
|
603
|
+
intent: 'danger',
|
|
604
|
+
onConfirm: () => handleValueChange(optionName, undefined),
|
|
605
|
+
});
|
|
606
|
+
};
|
|
590
607
|
const clearValueButton =
|
|
591
608
|
hasValue && !readOnly && editorLacksOwnClear ?
|
|
592
609
|
<ReqoreButton
|
|
@@ -594,12 +611,11 @@ export const CompactRow = memo(
|
|
|
594
611
|
size='small'
|
|
595
612
|
flat
|
|
596
613
|
minimal
|
|
597
|
-
icon='
|
|
614
|
+
icon='DeleteBinLine'
|
|
615
|
+
intent='danger'
|
|
616
|
+
aria-label='Clear value'
|
|
598
617
|
tooltip='Clear value'
|
|
599
|
-
onClick={
|
|
600
|
-
e.stopPropagation();
|
|
601
|
-
handleValueChange(optionName, undefined);
|
|
602
|
-
}}
|
|
618
|
+
onClick={confirmClearValue}
|
|
603
619
|
/>
|
|
604
620
|
: null;
|
|
605
621
|
// Batched commit: a changed row is a draft until Save — mark it with the
|
|
@@ -619,6 +635,21 @@ export const CompactRow = memo(
|
|
|
619
635
|
effect={{ uppercase: true, spaced: 1 }}
|
|
620
636
|
/>
|
|
621
637
|
: null;
|
|
638
|
+
const isPassiveCloseAction = fixedAllowedValueOption || readOnly;
|
|
639
|
+
const closeExpandedFieldButton = (
|
|
640
|
+
<ReqoreButton
|
|
641
|
+
className='options-readfirst-done'
|
|
642
|
+
size='small'
|
|
643
|
+
intent={!isPassiveCloseAction ? 'success' : undefined}
|
|
644
|
+
flat={isPassiveCloseAction}
|
|
645
|
+
minimal={isPassiveCloseAction}
|
|
646
|
+
fixed
|
|
647
|
+
icon={isPassiveCloseAction ? 'CloseLine' : 'CheckLine'}
|
|
648
|
+
aria-label={isPassiveCloseAction ? 'Close field' : 'Done'}
|
|
649
|
+
tooltip={isPassiveCloseAction ? 'Close field' : 'Done'}
|
|
650
|
+
onClick={() => toggleExpandedOption(optionName)}
|
|
651
|
+
/>
|
|
652
|
+
);
|
|
622
653
|
|
|
623
654
|
// Info tiers: Tier 1 (danger/warning + dependency hints) must be visible
|
|
624
655
|
// without interaction; Tier 2 (info/success, default notes) sits behind ⓘ.
|
|
@@ -860,15 +891,7 @@ export const CompactRow = memo(
|
|
|
860
891
|
renderInjectedOptionAction(action, index)
|
|
861
892
|
)}
|
|
862
893
|
{moreMenu}
|
|
863
|
-
|
|
864
|
-
className='options-readfirst-done'
|
|
865
|
-
size='small'
|
|
866
|
-
intent='success'
|
|
867
|
-
fixed
|
|
868
|
-
icon='CheckLine'
|
|
869
|
-
tooltip='Done'
|
|
870
|
-
onClick={collapse}
|
|
871
|
-
/>
|
|
894
|
+
{closeExpandedFieldButton}
|
|
872
895
|
</StyledRowActions>
|
|
873
896
|
{/* Cluster node is rendered LAST (it's absolutely positioned, so DOM
|
|
874
897
|
order doesn't move it) — keeping it out of the leading cells lets
|
|
@@ -1021,25 +1044,19 @@ export const CompactRow = memo(
|
|
|
1021
1044
|
{hasValue && !readOnly ?
|
|
1022
1045
|
<ReqoreButton
|
|
1023
1046
|
size='small'
|
|
1024
|
-
icon='
|
|
1047
|
+
icon='DeleteBinLine'
|
|
1025
1048
|
minimal
|
|
1026
1049
|
flat
|
|
1027
1050
|
fixed
|
|
1051
|
+
intent='danger'
|
|
1028
1052
|
className='options-readfirst-clear'
|
|
1053
|
+
aria-label='Clear value'
|
|
1029
1054
|
tooltip='Clear value'
|
|
1030
|
-
onClick={
|
|
1055
|
+
onClick={confirmClearValue}
|
|
1031
1056
|
/>
|
|
1032
1057
|
: null}
|
|
1033
1058
|
{moreMenu}
|
|
1034
|
-
|
|
1035
|
-
size='small'
|
|
1036
|
-
icon={readOnly ? 'CloseLine' : 'CheckLine'}
|
|
1037
|
-
intent='success'
|
|
1038
|
-
fixed
|
|
1039
|
-
className='options-readfirst-done'
|
|
1040
|
-
tooltip={readOnly ? 'Close' : 'Done'}
|
|
1041
|
-
onClick={() => toggleExpandedOption(optionName)}
|
|
1042
|
-
/>
|
|
1059
|
+
{closeExpandedFieldButton}
|
|
1043
1060
|
</ReqoreControlGroup>
|
|
1044
1061
|
</div>
|
|
1045
1062
|
{/* Same fullscreen focused-editing affordance as the classic cards —
|
|
@@ -5,6 +5,7 @@ import { Meta, StoryObj } from '@storybook/react-vite';
|
|
|
5
5
|
import { ChangeEvent, useState } from 'react';
|
|
6
6
|
import { expect, fireEvent, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
7
7
|
import { validateField } from '../../../helpers/validations';
|
|
8
|
+
import { defaultQorusTypes } from '../../../hooks/useQorusTypes';
|
|
8
9
|
import {
|
|
9
10
|
_testsChangeRichText,
|
|
10
11
|
_testsChangeStringField,
|
|
@@ -90,6 +91,29 @@ const meta: Meta<typeof FormEngine> = {
|
|
|
90
91
|
chromatic: {
|
|
91
92
|
viewports: [2560],
|
|
92
93
|
},
|
|
94
|
+
// `useQorusTypes` resolves the type catalogue as `size(data) ? data : defaultQorusTypes`,
|
|
95
|
+
// so a reachable, authenticated instance *replaces* the built-in list rather than
|
|
96
|
+
// supplementing it. These stories never opt into live data (no `live: true`), but the
|
|
97
|
+
// request fires anyway — which made `Option With Any Type` pass locally (401 → built-in
|
|
98
|
+
// list, so "Boolean" exists) and fail in CI, where the token is valid and the server's
|
|
99
|
+
// list decides the labels. Pin the catalogue so the type names the plays click are ours.
|
|
100
|
+
mockData: [
|
|
101
|
+
{
|
|
102
|
+
// `query()` builds `${instance}api/latest/${url}`, and the hook's url is
|
|
103
|
+
// `/system/qorus-type-info` — hence the doubled slash. Both spellings are listed
|
|
104
|
+
// so the mock keeps matching if that leading slash is ever dropped.
|
|
105
|
+
url: 'https://hq.qoretechnologies.com:8092/api/latest//system/qorus-type-info',
|
|
106
|
+
method: 'GET',
|
|
107
|
+
status: 200,
|
|
108
|
+
response: defaultQorusTypes,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
url: 'https://hq.qoretechnologies.com:8092/api/latest/system/qorus-type-info',
|
|
112
|
+
method: 'GET',
|
|
113
|
+
status: 200,
|
|
114
|
+
response: defaultQorusTypes,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
93
117
|
},
|
|
94
118
|
render: ({ value, onChange, ...rest }: IFormEngineProps) => {
|
|
95
119
|
const [val, setValue] = useState(value);
|
|
@@ -622,7 +646,7 @@ export const OptionWithAnyType: Story = {
|
|
|
622
646
|
// and switch it to a specific custom type (Boolean).
|
|
623
647
|
await _testsOpenTemplateMenu(4);
|
|
624
648
|
await _testsClickButton({ label: 'Set Custom Value' });
|
|
625
|
-
await _testsClickButton({ label: '
|
|
649
|
+
await _testsClickButton({ label: 'Boolean' });
|
|
626
650
|
},
|
|
627
651
|
};
|
|
628
652
|
|
|
@@ -3361,6 +3385,57 @@ const langImg = (color: string, letter: string): string =>
|
|
|
3361
3385
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="${color}"/><text x="16" y="23" font-size="20" fill="white" text-anchor="middle" font-family="sans-serif">${letter}</text></svg>`
|
|
3362
3386
|
)}`;
|
|
3363
3387
|
|
|
3388
|
+
const assertFixedChoiceCanCloseWithoutClearing = async (field: string, expectedLabel: string) => {
|
|
3389
|
+
const cardSelector = `.options-readfirst-card[data-field="${field}"]`;
|
|
3390
|
+
const rowSelector = `.readfirst-row[data-field="${field}"]`;
|
|
3391
|
+
|
|
3392
|
+
let card: HTMLElement | null = null;
|
|
3393
|
+
await waitFor(
|
|
3394
|
+
() => {
|
|
3395
|
+
card = document.querySelector(cardSelector);
|
|
3396
|
+
expect(card).toBeTruthy();
|
|
3397
|
+
},
|
|
3398
|
+
{ timeout: 10000 }
|
|
3399
|
+
);
|
|
3400
|
+
|
|
3401
|
+
const clearButton = card!.querySelector(
|
|
3402
|
+
'.options-readfirst-clear[aria-label="Clear value"]'
|
|
3403
|
+
) as HTMLElement;
|
|
3404
|
+
const closeButton = card!.querySelector(
|
|
3405
|
+
'.options-readfirst-done[aria-label="Close field"]'
|
|
3406
|
+
) as HTMLElement;
|
|
3407
|
+
await expect(clearButton).toBeInTheDocument();
|
|
3408
|
+
await expect(closeButton).toBeInTheDocument();
|
|
3409
|
+
// Clear is destructive and visually distinct from the adjacent passive
|
|
3410
|
+
// actions. It must not mutate the value until the Reqore confirmation is
|
|
3411
|
+
// accepted; cancelling keeps both the value and expanded editor intact.
|
|
3412
|
+
await expect(getComputedStyle(clearButton).color).not.toBe(getComputedStyle(closeButton).color);
|
|
3413
|
+
await fireEvent.click(clearButton);
|
|
3414
|
+
await waitFor(() => expect(document.querySelector('.reqore-confirmation-modal')).toBeTruthy(), {
|
|
3415
|
+
timeout: 10000,
|
|
3416
|
+
});
|
|
3417
|
+
await expect(document.querySelector('.reqore-confirmation-modal')?.textContent).toContain(
|
|
3418
|
+
'Clear value'
|
|
3419
|
+
);
|
|
3420
|
+
await _testsClickButton({ label: 'Cancel' });
|
|
3421
|
+
await waitFor(() => expect(document.querySelector('.reqore-confirmation-modal')).toBeNull());
|
|
3422
|
+
await expect(document.querySelector(cardSelector)).toBeTruthy();
|
|
3423
|
+
|
|
3424
|
+
await fireEvent.click(closeButton);
|
|
3425
|
+
await waitFor(() => expect(document.querySelector(cardSelector)).toBeNull(), {
|
|
3426
|
+
timeout: 10000,
|
|
3427
|
+
});
|
|
3428
|
+
|
|
3429
|
+
const row = document.querySelector(rowSelector) as HTMLElement;
|
|
3430
|
+
expect(row?.textContent).toContain(expectedLabel);
|
|
3431
|
+
|
|
3432
|
+
// Re-open so the story's visual snapshot still covers the expanded fixed-choice card.
|
|
3433
|
+
await fireEvent.click(row);
|
|
3434
|
+
await waitFor(() => expect(document.querySelector(cardSelector)).toBeTruthy(), {
|
|
3435
|
+
timeout: 10000,
|
|
3436
|
+
});
|
|
3437
|
+
};
|
|
3438
|
+
|
|
3364
3439
|
/**
|
|
3365
3440
|
* Enum field with per-value images (the IDE `language` field shape: `type:
|
|
3366
3441
|
* 'enum'` + `items: [{ value, title, image }]`). Read-first shows the selected
|
|
@@ -3426,6 +3501,7 @@ export const CompactEnumWithImages: Story = {
|
|
|
3426
3501
|
},
|
|
3427
3502
|
{ timeout: 10000 }
|
|
3428
3503
|
);
|
|
3504
|
+
await assertFixedChoiceCanCloseWithoutClearing('lang', 'Qore');
|
|
3429
3505
|
},
|
|
3430
3506
|
};
|
|
3431
3507
|
|
|
@@ -3502,6 +3578,7 @@ export const CompactEnumRichtextValue: Story = {
|
|
|
3502
3578
|
},
|
|
3503
3579
|
{ timeout: 10000 }
|
|
3504
3580
|
);
|
|
3581
|
+
await assertFixedChoiceCanCloseWithoutClearing('lang', 'Qore');
|
|
3505
3582
|
},
|
|
3506
3583
|
};
|
|
3507
3584
|
|
|
@@ -4163,8 +4240,28 @@ export const CompactFieldTypesEditing: Story = {
|
|
|
4163
4240
|
editRow('enabled').querySelector('.options-readfirst-revert')
|
|
4164
4241
|
).not.toBeInTheDocument();
|
|
4165
4242
|
|
|
4166
|
-
// Clear
|
|
4243
|
+
// Clear is destructive: cancelling its Reqore confirmation leaves the
|
|
4244
|
+
// value untouched, while confirming empties it and swaps Clear for Revert.
|
|
4245
|
+
await fireEvent.click(editRow('enabled').querySelector('.options-readfirst-clear')!);
|
|
4246
|
+
await waitFor(() => expect(document.querySelector('.reqore-confirmation-modal')).toBeTruthy(), {
|
|
4247
|
+
timeout: 10000,
|
|
4248
|
+
});
|
|
4249
|
+
await _testsClickButton({ label: 'Cancel' });
|
|
4250
|
+
await waitFor(() => expect(document.querySelector('.reqore-confirmation-modal')).toBeNull());
|
|
4251
|
+
await expect(editRow('enabled').querySelector('.options-readfirst-clear')).toBeInTheDocument();
|
|
4252
|
+
|
|
4167
4253
|
await fireEvent.click(editRow('enabled').querySelector('.options-readfirst-clear')!);
|
|
4254
|
+
let confirmationModal: HTMLElement | null = null;
|
|
4255
|
+
await waitFor(
|
|
4256
|
+
() => {
|
|
4257
|
+
confirmationModal = document.querySelector('.reqore-confirmation-modal');
|
|
4258
|
+
expect(confirmationModal).toBeTruthy();
|
|
4259
|
+
},
|
|
4260
|
+
{ timeout: 10000 }
|
|
4261
|
+
);
|
|
4262
|
+
await userEvent.click(
|
|
4263
|
+
within(confirmationModal!).getByRole('button', { name: 'Clear value' })
|
|
4264
|
+
);
|
|
4168
4265
|
await waitFor(() => {
|
|
4169
4266
|
expect(editRow('enabled').querySelector('.options-readfirst-clear')).not.toBeInTheDocument();
|
|
4170
4267
|
expect(editRow('enabled').querySelector('.options-readfirst-revert')).toBeInTheDocument();
|
|
@@ -101,6 +101,8 @@ import {
|
|
|
101
101
|
getReadFirstCompletion,
|
|
102
102
|
getReadFirstStatus,
|
|
103
103
|
isOptionValueEmpty,
|
|
104
|
+
isFixedCompactAllowedValueOption,
|
|
105
|
+
shouldAutoCollapseCompactOption,
|
|
104
106
|
} from './readFirst';
|
|
105
107
|
|
|
106
108
|
// Re-export types for consumers
|
|
@@ -281,7 +283,9 @@ const getOptionSchemaStorageType = (
|
|
|
281
283
|
option?: TQorusFormFieldSchema,
|
|
282
284
|
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
283
285
|
): TQorusType =>
|
|
284
|
-
((
|
|
286
|
+
((isFixedCompactAllowedValueOption(option)
|
|
287
|
+
? option?.type || option?.ui_type
|
|
288
|
+
: isRendererOnly(option?.ui_type)
|
|
285
289
|
? option?.type || option?.ui_type
|
|
286
290
|
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
287
291
|
|
|
@@ -555,6 +559,11 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
555
559
|
/** Opt-in: fetch global templates from `system/getContextData` for this context. */
|
|
556
560
|
interfaceContext?: string;
|
|
557
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;
|
|
558
567
|
showTypeToggle?: boolean;
|
|
559
568
|
/**
|
|
560
569
|
* Compact (read-first) mode: each option renders as a row with its formatted
|
|
@@ -706,6 +715,7 @@ export const FormEngine = ({
|
|
|
706
715
|
allowTemplates = true,
|
|
707
716
|
interfaceContext,
|
|
708
717
|
templateFieldProps,
|
|
718
|
+
forceDropdown,
|
|
709
719
|
showTypeToggle = true,
|
|
710
720
|
compact,
|
|
711
721
|
compactFlush = false,
|
|
@@ -835,9 +845,12 @@ export const FormEngine = ({
|
|
|
835
845
|
// same tick targets the stale (pre-move) layout, so the page doesn't budge.
|
|
836
846
|
// A rAF lets the new position settle first.
|
|
837
847
|
requestAnimationFrame(() => {
|
|
838
|
-
document
|
|
839
|
-
|
|
840
|
-
|
|
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
|
+
}
|
|
841
854
|
});
|
|
842
855
|
}
|
|
843
856
|
setFlashedOptions(optionNames);
|
|
@@ -1157,6 +1170,14 @@ export const FormEngine = ({
|
|
|
1157
1170
|
|
|
1158
1171
|
onSingleOptionsChange?.(optionName, updatedValue[optionName]);
|
|
1159
1172
|
|
|
1173
|
+
if (
|
|
1174
|
+
compact &&
|
|
1175
|
+
!readOnly &&
|
|
1176
|
+
shouldAutoCollapseCompactOption(options?.[optionName], val)
|
|
1177
|
+
) {
|
|
1178
|
+
setExpandedOptions((prev) => prev.filter((name) => name !== optionName));
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1160
1181
|
return {
|
|
1161
1182
|
fields: updatedValue,
|
|
1162
1183
|
meta,
|
|
@@ -1167,6 +1188,8 @@ export const FormEngine = ({
|
|
|
1167
1188
|
onSingleOptionsChange,
|
|
1168
1189
|
onDependableOptionChange,
|
|
1169
1190
|
isRendererOnly,
|
|
1191
|
+
compact,
|
|
1192
|
+
readOnly,
|
|
1170
1193
|
JSON.stringify(options),
|
|
1171
1194
|
JSON.stringify(operators),
|
|
1172
1195
|
]
|
|
@@ -1898,14 +1921,23 @@ export const FormEngine = ({
|
|
|
1898
1921
|
//
|
|
1899
1922
|
// The trailing fallbacks keep the row rendering when a server-driven
|
|
1900
1923
|
// schema has not arrived yet (`type` undefined) instead of crashing.
|
|
1901
|
-
const
|
|
1924
|
+
const optionSchema = options?.[optionName];
|
|
1925
|
+
const schemaUiType = optionSchema?.ui_type as TQorusType;
|
|
1902
1926
|
const uiTypeIsAnyLike = schemaUiType === 'any' || schemaUiType === 'auto';
|
|
1903
1927
|
const resolvedType =
|
|
1904
|
-
(
|
|
1928
|
+
(isFixedCompactAllowedValueOption(optionSchema)
|
|
1929
|
+
? getOptionSchemaStorageType(optionSchema, isRendererOnly)
|
|
1930
|
+
: schemaUiType && !uiTypeIsAnyLike
|
|
1931
|
+
? schemaUiType
|
|
1932
|
+
: undefined) ||
|
|
1905
1933
|
type ||
|
|
1906
1934
|
schemaUiType ||
|
|
1907
|
-
(
|
|
1935
|
+
(optionSchema?.type as TQorusType) ||
|
|
1908
1936
|
'any';
|
|
1937
|
+
const effectiveForceDropdown =
|
|
1938
|
+
forceDropdown ??
|
|
1939
|
+
(templateFieldProps as { forceDropdown?: boolean } | undefined)?.forceDropdown;
|
|
1940
|
+
|
|
1909
1941
|
return (
|
|
1910
1942
|
<>
|
|
1911
1943
|
{(() => {
|
|
@@ -2046,8 +2078,16 @@ export const FormEngine = ({
|
|
|
2046
2078
|
// reqraft: form-level expression fields get the Visual/Text shell
|
|
2047
2079
|
// (DPQL text mode); opt out per-form via `templateFieldProps`.
|
|
2048
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.
|
|
2049
2088
|
allowCustomValues={
|
|
2050
|
-
|
|
2089
|
+
isFixedCompactAllowedValueOption(optionSchema) ||
|
|
2090
|
+
(options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any')
|
|
2051
2091
|
}
|
|
2052
2092
|
templates={templates.value}
|
|
2053
2093
|
{...getTypeAndCanBeNull(
|
|
@@ -2077,6 +2117,7 @@ export const FormEngine = ({
|
|
|
2077
2117
|
default_value={getDefaultValue(options?.[optionName])}
|
|
2078
2118
|
isDefaultTemplate={options?.[optionName]?.default_view === 'template'}
|
|
2079
2119
|
allowed_values={options?.[optionName]?.allowed_values}
|
|
2120
|
+
allowed_values_creatable={options?.[optionName]?.allowed_values_creatable}
|
|
2080
2121
|
disabled={
|
|
2081
2122
|
options?.[optionName]?.disabled ||
|
|
2082
2123
|
readOnly ||
|
|
@@ -2094,6 +2135,7 @@ export const FormEngine = ({
|
|
|
2094
2135
|
: undefined
|
|
2095
2136
|
}
|
|
2096
2137
|
{...templateFieldProps}
|
|
2138
|
+
forceDropdown={effectiveForceDropdown}
|
|
2097
2139
|
/>
|
|
2098
2140
|
<OptionFieldMessages
|
|
2099
2141
|
schema={options || {}}
|
|
@@ -2137,6 +2179,7 @@ export const FormEngine = ({
|
|
|
2137
2179
|
uniqueName,
|
|
2138
2180
|
componentOverrides,
|
|
2139
2181
|
templateFieldProps,
|
|
2182
|
+
forceDropdown,
|
|
2140
2183
|
availableOptions,
|
|
2141
2184
|
fixedValue,
|
|
2142
2185
|
// Depend on the specific `rest` values used, not the whole `rest` object
|
|
@@ -220,7 +220,8 @@ export const StyledEditCard = styled.div<{ $bg: string; $border: string }>`
|
|
|
220
220
|
|
|
221
221
|
/* Single-value editors (operators, long-string, markdown, richtext, byte-size)
|
|
222
222
|
carry the card's own "Clear value" action, so suppress the value input's
|
|
223
|
-
built-in ReqoreInput clear — ReqoreInput has no prop to hide it, and
|
|
223
|
+
built-in ReqoreInput clear — ReqoreInput has no prop to hide it, and a
|
|
224
|
+
separate input ✕ plus the row-level trash action
|
|
224
225
|
read as a duplicate. Multi-input cards (hash/list/…) are not marked single,
|
|
225
226
|
so their per-sub-field clears stay. */
|
|
226
227
|
&.options-readfirst-card-single .reqore-clear-input-button {
|
|
@@ -87,6 +87,48 @@ export const isOptionValueEmpty = (value: unknown): boolean =>
|
|
|
87
87
|
value === '' ||
|
|
88
88
|
(Array.isArray(value) && value.length === 0);
|
|
89
89
|
|
|
90
|
+
export const isFixedCompactAllowedValueOption = (
|
|
91
|
+
schema: TQorusFormFieldSchema | undefined
|
|
92
|
+
): boolean => {
|
|
93
|
+
if (!schema) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const selectableSchema = schema as TQorusFormFieldSchema & { items?: unknown[] };
|
|
98
|
+
const options = selectableSchema.allowed_values?.length
|
|
99
|
+
? selectableSchema.allowed_values
|
|
100
|
+
: selectableSchema.items;
|
|
101
|
+
|
|
102
|
+
return !!(
|
|
103
|
+
options?.length &&
|
|
104
|
+
!schema.allowed_values_creatable &&
|
|
105
|
+
!schema.multiselect &&
|
|
106
|
+
!schema.arg_schema
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const shouldAutoCollapseCompactAllowedValueOption = (
|
|
111
|
+
schema: TQorusFormFieldSchema | undefined,
|
|
112
|
+
value: unknown
|
|
113
|
+
): boolean => !isOptionValueEmpty(value) && isFixedCompactAllowedValueOption(schema);
|
|
114
|
+
|
|
115
|
+
/** Boolean fields have three meaningful states: unset, Yes, and No. Once the
|
|
116
|
+
* user chooses either boolean value in compact mode, the choice is complete
|
|
117
|
+
* and the inline editor can close without a separate confirmation. */
|
|
118
|
+
export const isCompactBooleanOption = (
|
|
119
|
+
schema: TQorusFormFieldSchema | undefined
|
|
120
|
+
): boolean => {
|
|
121
|
+
const type = (schema?.ui_type || schema?.type)?.toLowerCase();
|
|
122
|
+
return type === 'bool' || type === 'boolean';
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const shouldAutoCollapseCompactOption = (
|
|
126
|
+
schema: TQorusFormFieldSchema | undefined,
|
|
127
|
+
value: unknown
|
|
128
|
+
): boolean =>
|
|
129
|
+
!isOptionValueEmpty(value) &&
|
|
130
|
+
(isCompactBooleanOption(schema) || isFixedCompactAllowedValueOption(schema));
|
|
131
|
+
|
|
90
132
|
/** Prefer the matching allowed_values entry's display_name (fallback `name`)
|
|
91
133
|
* over the raw stored value. */
|
|
92
134
|
const findAllowedOption = (value: unknown, schema?: TQorusFormFieldSchema): any | undefined => {
|
|
@@ -23,6 +23,16 @@ const SAMPLE: IExpression = {
|
|
|
23
23
|
const meta = {
|
|
24
24
|
component: ExpressionField,
|
|
25
25
|
title: 'Components/Form/Expressions/ExpressionField',
|
|
26
|
+
parameters: {
|
|
27
|
+
mockData: [
|
|
28
|
+
{
|
|
29
|
+
url: 'https://hq.qoretechnologies.com:8092/api/latest/system?action=expressions&context=ui',
|
|
30
|
+
method: 'GET',
|
|
31
|
+
status: 200,
|
|
32
|
+
response: mockExpressions,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
26
36
|
args: {
|
|
27
37
|
onChange: fn(),
|
|
28
38
|
expressions: mockExpressions,
|
|
@@ -16,7 +16,8 @@ import { ExpressionBuilder } from './index';
|
|
|
16
16
|
// --- inlined test helpers (equivalents of the IDE `../Tests/utils`) ---------
|
|
17
17
|
|
|
18
18
|
/** Count regular expression cards (`.expression`). */
|
|
19
|
-
const expressionCount = (
|
|
19
|
+
const expressionCount = (scope: ParentNode = document) =>
|
|
20
|
+
scope.querySelectorAll('.expression').length;
|
|
20
21
|
|
|
21
22
|
/** Click a ReqoreButton by visible label (anywhere in the document). */
|
|
22
23
|
const clickButtonByLabel = async (label: string) => {
|
|
@@ -26,14 +27,14 @@ const clickButtonByLabel = async (label: string) => {
|
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
/** Click a button by CSS selector. */
|
|
29
|
-
const clickSelector = async (selector: string, nth = 0) => {
|
|
30
|
-
const els =
|
|
30
|
+
const clickSelector = async (selector: string, nth = 0, scope: ParentNode = document) => {
|
|
31
|
+
const els = scope.querySelectorAll(selector);
|
|
31
32
|
await fireEvent.click(els[nth]);
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
/** Wait for some text to appear in the document. */
|
|
35
|
-
const waitForText = async (text: string | RegExp) => {
|
|
36
|
-
const body = within(
|
|
36
|
+
const waitForText = async (text: string | RegExp, scope: HTMLElement = document.body) => {
|
|
37
|
+
const body = within(scope);
|
|
37
38
|
await waitFor(() => expect(body.queryAllByText(text)[0]).toBeTruthy(), { timeout: 10000 });
|
|
38
39
|
};
|
|
39
40
|
|
|
@@ -44,15 +45,19 @@ const waitForText = async (text: string | RegExp) => {
|
|
|
44
45
|
* toolkit `Select` opens a `SelectFieldCollection` modal (not an inline
|
|
45
46
|
* dropdown) — the item is a collection button with the display name as text.
|
|
46
47
|
*/
|
|
47
|
-
const selectOperation = async (
|
|
48
|
-
|
|
48
|
+
const selectOperation = async (
|
|
49
|
+
currentLabel: string,
|
|
50
|
+
itemLabel: string,
|
|
51
|
+
ownerDocument: Document = document
|
|
52
|
+
) => {
|
|
53
|
+
const body = within(ownerDocument.body);
|
|
49
54
|
// Open the picker (placeholder when empty, or the current operation name).
|
|
50
55
|
const trigger = await body.findByText(currentLabel, undefined, { timeout: 10000 });
|
|
51
56
|
await fireEvent.click(trigger);
|
|
52
57
|
// Click the item inside the now-open collection modal.
|
|
53
58
|
const item = await waitFor(
|
|
54
59
|
() => {
|
|
55
|
-
const el = within(
|
|
60
|
+
const el = within(ownerDocument.body).queryAllByText(itemLabel)[0];
|
|
56
61
|
if (!el) throw new Error(`collection item "${itemLabel}" not ready`);
|
|
57
62
|
return el;
|
|
58
63
|
},
|
|
@@ -121,8 +126,8 @@ export const Default: Story = {
|
|
|
121
126
|
},
|
|
122
127
|
},
|
|
123
128
|
},
|
|
124
|
-
play: async () => {
|
|
125
|
-
await waitFor(() => expect(expressionCount()).toBe(1), { timeout: 10000 });
|
|
129
|
+
play: async ({ canvasElement }) => {
|
|
130
|
+
await waitFor(() => expect(expressionCount(canvasElement)).toBe(1), { timeout: 10000 });
|
|
126
131
|
},
|
|
127
132
|
};
|
|
128
133
|
|
|
@@ -630,19 +635,19 @@ export const ExpressionCanBeWrapped: Story = {
|
|
|
630
635
|
},
|
|
631
636
|
},
|
|
632
637
|
},
|
|
633
|
-
play: async () => {
|
|
638
|
+
play: async ({ canvasElement }) => {
|
|
634
639
|
// Seeded base op + its operand render.
|
|
635
|
-
await waitFor(() => expect(expressionCount()).toBe(1), { timeout: 10000 });
|
|
636
|
-
await waitForText('Value To Convert');
|
|
640
|
+
await waitFor(() => expect(expressionCount(canvasElement)).toBe(1), { timeout: 10000 });
|
|
641
|
+
await waitForText('Value To Convert', canvasElement);
|
|
637
642
|
|
|
638
643
|
// Hover the card to reveal the floating "Wrap" picker, then wrap it in
|
|
639
644
|
// `Join List Elements` — the wrapped op becomes its first operand and the
|
|
640
645
|
// outer card shows "List To Join". The card count goes 1 → 2.
|
|
641
|
-
await userEvent.hover(
|
|
646
|
+
await userEvent.hover(canvasElement.querySelectorAll('.expression')[0]);
|
|
642
647
|
await sleep(500);
|
|
643
|
-
await selectOperation('Wrap', 'Join List Elements');
|
|
644
|
-
await waitForText('List To Join');
|
|
645
|
-
await waitFor(() => expect(expressionCount()).toBe(2), { timeout: 10000 });
|
|
648
|
+
await selectOperation('Wrap', 'Join List Elements', canvasElement.ownerDocument);
|
|
649
|
+
await waitForText('List To Join', canvasElement);
|
|
650
|
+
await waitFor(() => expect(expressionCount(canvasElement)).toBe(2), { timeout: 10000 });
|
|
646
651
|
},
|
|
647
652
|
};
|
|
648
653
|
|
|
@@ -662,11 +667,13 @@ export const ExpressionCanBeUnwrapped: Story = {
|
|
|
662
667
|
await ExpressionCanBeWrapped.play!(args);
|
|
663
668
|
|
|
664
669
|
// Unwrap the outer op (keep the child) — the `.expression-unwrap` action.
|
|
665
|
-
await userEvent.hover(
|
|
670
|
+
await userEvent.hover(args.canvasElement.querySelectorAll('.expression')[0]);
|
|
666
671
|
await sleep(500);
|
|
672
|
+
// The card's actions are ReQore *floating* actions, so they are portalled out
|
|
673
|
+
// of the canvas — this one lookup stays document-scoped on purpose.
|
|
667
674
|
await clickSelector('.expression-unwrap');
|
|
668
675
|
|
|
669
|
-
await waitFor(() => expect(expressionCount()).toBe(1), { timeout: 10000 });
|
|
676
|
+
await waitFor(() => expect(expressionCount(args.canvasElement)).toBe(1), { timeout: 10000 });
|
|
670
677
|
},
|
|
671
678
|
};
|
|
672
679
|
|
|
@@ -651,7 +651,10 @@ export const Expression = ({
|
|
|
651
651
|
return value.value?.args?.some((arg) => arg?.types_mismatch);
|
|
652
652
|
}, [JSON.stringify(value)]);
|
|
653
653
|
|
|
654
|
-
|
|
654
|
+
// The type hook is seeded with the complete built-in catalogue and refreshes
|
|
655
|
+
// it in the background. Do not hide an otherwise usable expression behind
|
|
656
|
+
// a skeleton while that optional refresh is in flight.
|
|
657
|
+
if (expressions.loading || (types.loading && !size(types.value))) {
|
|
655
658
|
return <ReqorePanelSkeleton size='small' />;
|
|
656
659
|
}
|
|
657
660
|
|
|
@@ -687,6 +690,11 @@ export const Expression = ({
|
|
|
687
690
|
fluid={false}
|
|
688
691
|
fixed={true}
|
|
689
692
|
flat
|
|
693
|
+
// The operation catalogue is long and every entry carries a description,
|
|
694
|
+
// so it belongs in the searchable collection modal rather than an inline
|
|
695
|
+
// dropdown. `SelectFormField` defaults `forceDropdown` to `true`, which
|
|
696
|
+
// would otherwise collapse this picker into the dropdown presentation.
|
|
697
|
+
forceDropdown={false}
|
|
690
698
|
minimal
|
|
691
699
|
items={defaultItems}
|
|
692
700
|
onChange={handleOperatorChange}
|
|
@@ -732,6 +740,9 @@ export const Expression = ({
|
|
|
732
740
|
transparent: false,
|
|
733
741
|
fixed: true,
|
|
734
742
|
minimal: false,
|
|
743
|
+
// Same catalogue as the operation selector above — keep the collection
|
|
744
|
+
// modal rather than the `forceDropdown` default's inline dropdown.
|
|
745
|
+
forceDropdown: false,
|
|
735
746
|
items: defaultItems,
|
|
736
747
|
hideItemCount: true,
|
|
737
748
|
onChange: handleWrapExpressionClick,
|
|
@@ -84,7 +84,10 @@ export const useExpressions = (
|
|
|
84
84
|
|
|
85
85
|
return {
|
|
86
86
|
expressions,
|
|
87
|
-
|
|
87
|
+
// `extra` is deliberately not loaded without an expressions URL. The
|
|
88
|
+
// generic fetch hook keeps an unstarted request in its initial loading
|
|
89
|
+
// state, so only include it when that request is actually enabled.
|
|
90
|
+
loading: system.loading || (!!expressionsUrl && extra.loading),
|
|
88
91
|
error: errorOverride ?? system.error ?? extra.error,
|
|
89
92
|
reload: () => {
|
|
90
93
|
system.load();
|