@qoretechnologies/reqraft 0.10.61 → 0.10.62-pr.137.g86641aa
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/dpqlEditor/DpqlEditor.d.ts.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.js +8 -3
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +5 -0
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +17 -8
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +65 -15
- 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 +12 -1
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +2 -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/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +5 -1
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/expressions/ExpressionField.d.ts +21 -1
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +72 -14
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +16 -3
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/expressionOperands.d.ts +16 -0
- package/dist/components/form/expressions/expressionOperands.d.ts.map +1 -0
- package/dist/components/form/expressions/expressionOperands.js +97 -0
- package/dist/components/form/expressions/expressionOperands.js.map +1 -0
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +15 -0
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +2 -0
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +24 -5
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/textOfExpression.d.ts +18 -0
- package/dist/components/form/expressions/textOfExpression.d.ts.map +1 -0
- package/dist/components/form/expressions/textOfExpression.js +37 -0
- package/dist/components/form/expressions/textOfExpression.js.map +1 -0
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +7 -7
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +8 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +10 -10
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts +6 -0
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +280 -55
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/writtenAsText.d.ts +57 -0
- package/dist/components/form/fields/template/writtenAsText.d.ts.map +1 -0
- package/dist/components/form/fields/template/writtenAsText.js +203 -0
- package/dist/components/form/fields/template/writtenAsText.js.map +1 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +7 -1
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +6 -0
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +7 -6
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/helpers/templates.d.ts +18 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +30 -1
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +98 -18
- package/dist/helpers/validations.js.map +1 -1
- package/package.json +1 -1
- package/src/components/dpqlEditor/DpqlEditor.tsx +11 -18
- package/src/components/dpqlEditor/types.ts +5 -0
- package/src/components/form/engine/CompactRow.tsx +31 -10
- package/src/components/form/engine/FormEngine.stories.tsx +656 -18
- package/src/components/form/engine/FormEngine.tsx +64 -22
- package/src/components/form/engine/FormEngineRemote.stories.tsx +5 -0
- package/src/components/form/engine/OptionFieldMessages.tsx +12 -1
- package/src/components/form/engine/compactRowContext.ts +2 -0
- package/src/components/form/engine/readFirst.ts +7 -1
- package/src/components/form/expressions/ExpressionField.stories.tsx +57 -9
- package/src/components/form/expressions/ExpressionField.tsx +179 -60
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +8 -1
- package/src/components/form/expressions/builder/index.tsx +16 -3
- package/src/components/form/expressions/dpqlMockLanguage.ts +7 -2
- package/src/components/form/expressions/expressionOperands.ts +74 -0
- package/src/components/form/expressions/mockExpressions.ts +16 -4
- package/src/components/form/expressions/renderExpressionToText.ts +26 -9
- package/src/components/form/expressions/textOfExpression.ts +39 -0
- package/src/components/form/expressions/useRenderExpression.ts +9 -1
- package/src/components/form/fields/number/Number.stories.tsx +8 -7
- package/src/components/form/fields/rich-text/RichText.tsx +18 -10
- package/src/components/form/fields/template/TemplateField.stories.tsx +486 -18
- package/src/components/form/fields/template/TemplateField.tsx +506 -62
- package/src/components/form/fields/template/writtenAsText.ts +217 -0
- package/src/components/form/index.tsx +6 -0
- package/src/components/smartEditor/useLspSession.ts +15 -10
- package/src/helpers/templates.ts +35 -0
- package/src/helpers/validations.ts +83 -12
|
@@ -1378,6 +1378,8 @@ const FormEngineImpl = ({
|
|
|
1378
1378
|
// stays put when its status flips (e.g. becomes valid) instead of jumping to
|
|
1379
1379
|
// another box mid-edit and stealing focus. Keyed by option name.
|
|
1380
1380
|
const settledBucket = useRef<Record<string, 'attention' | 'set' | 'optional'>>({});
|
|
1381
|
+
// the one field that says where it moves when it is closed: see `heldOptions`
|
|
1382
|
+
const movesToNoteFor = useRef<string | null | undefined>(undefined);
|
|
1381
1383
|
// Measured form width (not viewport — the form lives in drawers/panels of
|
|
1382
1384
|
// arbitrary width) drives the stacked narrow layout.
|
|
1383
1385
|
const [compactWrapRef, { width: compactWrapWidth }] = useMeasure<HTMLDivElement>();
|
|
@@ -1425,9 +1427,7 @@ const FormEngineImpl = ({
|
|
|
1425
1427
|
setCompactToolbarHeight(0);
|
|
1426
1428
|
return undefined;
|
|
1427
1429
|
}
|
|
1428
|
-
const header = wrap.querySelector<HTMLElement>(
|
|
1429
|
-
':scope > .reqore-panel > .reqore-panel-title'
|
|
1430
|
-
);
|
|
1430
|
+
const header = wrap.querySelector<HTMLElement>(':scope > .reqore-panel > .reqore-panel-title');
|
|
1431
1431
|
if (!header) {
|
|
1432
1432
|
setCompactToolbarHeight(0);
|
|
1433
1433
|
return undefined;
|
|
@@ -2148,7 +2148,6 @@ const FormEngineImpl = ({
|
|
|
2148
2148
|
return newValue;
|
|
2149
2149
|
}
|
|
2150
2150
|
|
|
2151
|
-
|
|
2152
2151
|
const rendererType = getType(
|
|
2153
2152
|
(options[optionName].ui_type || options[optionName].type) as TQorusType,
|
|
2154
2153
|
operators,
|
|
@@ -2392,29 +2391,39 @@ const FormEngineImpl = ({
|
|
|
2392
2391
|
const isEmpty = optionValue === undefined || optionValue === '';
|
|
2393
2392
|
|
|
2394
2393
|
let validation: IValidationResult;
|
|
2395
|
-
|
|
2396
|
-
|
|
2394
|
+
/* A field whose host gives its own reason (`invalid_reason`) is invalid while it does, whatever the
|
|
2395
|
+
field makes of its value: the host knows something the field cannot - that a field it names
|
|
2396
|
+
is not in the record, that text around a field is not a whole number - and says it in the field,
|
|
2397
|
+
in place of the field's own message (`getOptionFieldMessages`). */
|
|
2398
|
+
const hostReason = (options?.[optionName] as { invalid_reason?: string } | undefined)
|
|
2399
|
+
?.invalid_reason;
|
|
2400
|
+
|
|
2401
|
+
if (hostReason) {
|
|
2402
|
+
validation = { isValid: false, reason: hostReason, reasons: [hostReason] };
|
|
2403
|
+
} else {
|
|
2404
|
+
/* A required field locked by an unmet `depends_on` does not apply
|
|
2397
2405
|
while it is locked: it is not in "Needs attention", and an empty one
|
|
2398
2406
|
does not make the form incomplete either. Otherwise a check whose
|
|
2399
2407
|
kind is Equals could never be saved, because Minimum — required for
|
|
2400
2408
|
Between alone — is empty; and a form whose only gaps are fields
|
|
2401
2409
|
nobody can fill in would say Incomplete with nothing to act on. A
|
|
2402
2410
|
locked field that still HOLDS a value is validated as before. */
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2411
|
+
if (
|
|
2412
|
+
isEmpty &&
|
|
2413
|
+
((!isRequired && !hasRequiredGroups) || dependencyLockedNames.includes(optionName))
|
|
2414
|
+
) {
|
|
2415
|
+
validation = { isValid: true, reasons: [] };
|
|
2416
|
+
} else {
|
|
2417
|
+
validation = validateFieldWithResult(getType(type), optionValue, {
|
|
2418
|
+
has_to_have_value: true,
|
|
2419
|
+
optionSchema: options,
|
|
2420
|
+
options: availableOptions,
|
|
2421
|
+
...options?.[optionName],
|
|
2422
|
+
// The expression flag lives on the field value, not the schema.
|
|
2423
|
+
isFunction: (option as { is_expression?: boolean }).is_expression,
|
|
2424
|
+
hasOwnEditor: isRendererOnly(options?.[optionName]?.ui_type as TQorusType),
|
|
2425
|
+
} as any);
|
|
2426
|
+
}
|
|
2418
2427
|
}
|
|
2419
2428
|
|
|
2420
2429
|
result.push({
|
|
@@ -3103,6 +3112,9 @@ const FormEngineImpl = ({
|
|
|
3103
3112
|
// reqraft: form-level expression fields get the Visual/Text shell
|
|
3104
3113
|
// (DPQL text mode); opt out per-form via `templateFieldProps`.
|
|
3105
3114
|
allowTextExpressions
|
|
3115
|
+
// a form's options enter their value on tabs: Value · Expression · Visual, or Value · Template
|
|
3116
|
+
// where only templates are taken (qorus#646); opt out per-form via `templateFieldProps`
|
|
3117
|
+
valueTabs
|
|
3106
3118
|
// A fixed allowed-value field still needs its selector even when
|
|
3107
3119
|
// arbitrary custom values are forbidden. TemplateField uses this
|
|
3108
3120
|
// flag to decide whether to mount AutoFormField at all; treating
|
|
@@ -3270,9 +3282,35 @@ const FormEngineImpl = ({
|
|
|
3270
3282
|
// The closure surface the extracted CompactRow reads through context. Refs and
|
|
3271
3283
|
// setters are stable; the state/memo/handler fields change identity as they do
|
|
3272
3284
|
// today, so a row re-renders exactly when its inputs do.
|
|
3285
|
+
/* An open field is held in the box it was opened in, so finishing an edit does not remount it elsewhere
|
|
3286
|
+
and take the focus (see `stableBucketOf`). Held, it says where it now belongs - otherwise a value
|
|
3287
|
+
already right sat under "Needs attention" until ✓ was pressed, with nothing saying why (David's review
|
|
3288
|
+
of qorus#646). Keyed by option name: the box it moves to when closed.
|
|
3289
|
+
|
|
3290
|
+
It is said once per form, on the first field held this way, and on no field once that one is closed:
|
|
3291
|
+
the user knows it from then on, and saying it on every field takes room (qlip build 20261008-083118;
|
|
3292
|
+
David's decision). `movesToNoteFor` is that field - undefined until one is held, null once it is
|
|
3293
|
+
closed. */
|
|
3294
|
+
const heldEntries = expandedOptions
|
|
3295
|
+
.map((name) => [name, settledBucket.current[name], getOptionBucket(name)] as const)
|
|
3296
|
+
.filter(([, held, now]) => !!held && held !== now)
|
|
3297
|
+
.map(([name, , now]) => [name, now] as const);
|
|
3298
|
+
if (movesToNoteFor.current === undefined && heldEntries.length) {
|
|
3299
|
+
movesToNoteFor.current = heldEntries[0][0];
|
|
3300
|
+
} else if (movesToNoteFor.current && !expandedOptions.includes(movesToNoteFor.current)) {
|
|
3301
|
+
movesToNoteFor.current = null;
|
|
3302
|
+
}
|
|
3303
|
+
const heldOptionsKey = JSON.stringify(
|
|
3304
|
+
Object.fromEntries(heldEntries.filter(([name]) => name === movesToNoteFor.current))
|
|
3305
|
+
);
|
|
3306
|
+
const heldOptions = useMemo<Record<string, 'attention' | 'set' | 'optional'>>(
|
|
3307
|
+
() => JSON.parse(heldOptionsKey),
|
|
3308
|
+
[heldOptionsKey]
|
|
3309
|
+
);
|
|
3273
3310
|
const compactRowContextValue = useMemo<ICompactRowContext>(
|
|
3274
3311
|
() => ({
|
|
3275
3312
|
templates: templates.value,
|
|
3313
|
+
heldOptions,
|
|
3276
3314
|
readOnly,
|
|
3277
3315
|
onReadOnlyActivate,
|
|
3278
3316
|
commitMode,
|
|
@@ -3322,6 +3360,7 @@ const FormEngineImpl = ({
|
|
|
3322
3360
|
}),
|
|
3323
3361
|
[
|
|
3324
3362
|
templates.value,
|
|
3363
|
+
heldOptions,
|
|
3325
3364
|
readOnly,
|
|
3326
3365
|
commitMode,
|
|
3327
3366
|
expandMode,
|
|
@@ -4245,7 +4284,10 @@ const FormEngineImpl = ({
|
|
|
4245
4284
|
tested. Rendered as `data-wait` on the placeholder so a page can be asked
|
|
4246
4285
|
what it is waiting FOR instead of only that it is waiting. */
|
|
4247
4286
|
const waitReason =
|
|
4248
|
-
rest.skeleton ?
|
|
4287
|
+
rest.skeleton ?
|
|
4288
|
+
rest.skeletonReason ?
|
|
4289
|
+
`host:${rest.skeletonReason}`
|
|
4290
|
+
: 'host'
|
|
4249
4291
|
: templates.loading ? 'templates'
|
|
4250
4292
|
: typesLoading ? 'types'
|
|
4251
4293
|
: optionsLoading ? 'options'
|
|
@@ -633,6 +633,11 @@ export const InjectedOptionActionsEditCard: Story = {
|
|
|
633
633
|
document.querySelector('.options-readfirst-card .option-branch-action')
|
|
634
634
|
).toBeTruthy();
|
|
635
635
|
});
|
|
636
|
+
|
|
637
|
+
// the card's name is centred on its actions, not above them (David's review of qorus#646)
|
|
638
|
+
const header = document.querySelector('.options-readfirst-card-header') as HTMLElement;
|
|
639
|
+
const [heading, actions] = [...header.children].map((el) => el.getBoundingClientRect());
|
|
640
|
+
await expect(Math.abs(heading.top + heading.height / 2 - (actions.top + actions.height / 2))).toBeLessThanOrEqual(2);
|
|
636
641
|
},
|
|
637
642
|
};
|
|
638
643
|
|
|
@@ -356,7 +356,16 @@ export const getOptionFieldMessages = ({
|
|
|
356
356
|
!!optionSchema?.depends_on &&
|
|
357
357
|
!hasAllDependenciesFullfilled(optionSchema.depends_on, allOptions, schema);
|
|
358
358
|
|
|
359
|
-
|
|
359
|
+
/* The host's own words for what is wrong with this field (`invalid_reason`) are said in place of the
|
|
360
|
+
field's own reason and its "This field is required": a host that has a sentence of its own, in the
|
|
361
|
+
user's language, showed it beside the field's generic one, two messages for one problem (qorus#646).
|
|
362
|
+
Required groups and dependencies still say what they say. */
|
|
363
|
+
const hostReason = (optionSchema as { invalid_reason?: string } | undefined)?.invalid_reason;
|
|
364
|
+
const hasValue = option.value || option.value === false || option.value === 0 || option.value === null;
|
|
365
|
+
|
|
366
|
+
if (hostReason) {
|
|
367
|
+
result.push({ label: hostReason, intent: 'danger' });
|
|
368
|
+
} else if (hasValue) {
|
|
360
369
|
/* A concrete type the VALUE carries beats an untyped schema.
|
|
361
370
|
`auto`/`any` declare no type — the author picks one (Binary, Date, …)
|
|
362
371
|
and it is recorded beside the value. Reading the schema first validated
|
|
@@ -396,7 +405,9 @@ export const getOptionFieldMessages = ({
|
|
|
396
405
|
if (optionSchema?.required && !locked) {
|
|
397
406
|
result.push({ label: 'This field is required', intent: 'danger' });
|
|
398
407
|
}
|
|
408
|
+
}
|
|
399
409
|
|
|
410
|
+
if (!hasValue) {
|
|
400
411
|
if (
|
|
401
412
|
optionSchema?.required_groups &&
|
|
402
413
|
!validateOptionWithRequiredGroups(allOptions, schema, optionSchema.required_groups)
|
|
@@ -69,6 +69,8 @@ export interface ICompactRowContext {
|
|
|
69
69
|
// display name in the read-first chip.
|
|
70
70
|
templates?: IReqoreFormTemplates;
|
|
71
71
|
expandedOptions: string[];
|
|
72
|
+
/** Open fields held in the box they were opened in, with the box each moves to when it is closed. */
|
|
73
|
+
heldOptions?: Record<string, 'attention' | 'set' | 'optional'>;
|
|
72
74
|
highlightedOptions: string[];
|
|
73
75
|
flashedOptions: string[];
|
|
74
76
|
infoPanelOverrides: Record<string, boolean>;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
TQorusFormFieldSchema,
|
|
5
5
|
TQorusType,
|
|
6
6
|
} from '@qoretechnologies/ts-toolkit';
|
|
7
|
+
import { isValueTemplate } from '../../../helpers/templates';
|
|
7
8
|
import { isRendererOnlyUiType } from './rendererTypes';
|
|
8
9
|
import { isEmptyUiEnvelope, isUiEncodedValue } from './_structuredData/structuredData';
|
|
9
10
|
import { renderExpressionToText } from '../expressions/renderExpressionToText';
|
|
@@ -642,7 +643,12 @@ export const formatOptionValue = (
|
|
|
642
643
|
|
|
643
644
|
const type = getValueType(option, schema);
|
|
644
645
|
|
|
645
|
-
|
|
646
|
+
/* A template is the field it names, in a yes/no as in any type: it is not "Yes" because it is not
|
|
647
|
+
empty. Only a value that is true or false reads as Yes or No (qorus#646). */
|
|
648
|
+
if (
|
|
649
|
+
(type === 'bool' || type === 'boolean') &&
|
|
650
|
+
!(typeof value === 'string' && isValueTemplate(value))
|
|
651
|
+
) {
|
|
646
652
|
return value ? 'Yes' : 'No';
|
|
647
653
|
}
|
|
648
654
|
|
|
@@ -659,6 +659,37 @@ export const TextMode: Story = {
|
|
|
659
659
|
},
|
|
660
660
|
};
|
|
661
661
|
|
|
662
|
+
/** A comparison built in the Visual view and left unfinished, seen in the Text view. */
|
|
663
|
+
export const TextModeOfAnUnfinishedExpression: Story = {
|
|
664
|
+
args: {
|
|
665
|
+
value: {
|
|
666
|
+
is_expression: true,
|
|
667
|
+
// ">" with its first operand switched to "Use Expression", nothing chosen, and its second empty
|
|
668
|
+
value: { exp: '>', args: [{ value: { args: [null] }, is_expression: true }, {}] },
|
|
669
|
+
},
|
|
670
|
+
defaultMode: 'text',
|
|
671
|
+
},
|
|
672
|
+
parameters: {
|
|
673
|
+
docs: {
|
|
674
|
+
description: {
|
|
675
|
+
story:
|
|
676
|
+
'Renders ExpressionField in Text mode over a comparison left unfinished in the Visual view: an operand switched to Use Expression with no operation chosen, and an empty one. The text shows a hole for each, "… > …", as the row summary does.',
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
async play({ canvasElement }) {
|
|
681
|
+
await waitFor(
|
|
682
|
+
() => {
|
|
683
|
+
const el = canvasElement.querySelector('[contenteditable="true"]');
|
|
684
|
+
expect(el?.textContent).toBe('… > …');
|
|
685
|
+
},
|
|
686
|
+
{ timeout: 6000 }
|
|
687
|
+
);
|
|
688
|
+
expect(canvasElement.textContent).not.toContain('[object Object]');
|
|
689
|
+
await waitForLspIdle(canvasElement);
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
|
|
662
693
|
/**
|
|
663
694
|
* A plain `bool` field with `supports_expressions` and no value — starts as
|
|
664
695
|
* the normal field; the "Use Expression" toggle (More menu) switches it.
|
|
@@ -757,10 +788,10 @@ const settle = (root: HTMLElement, quietMs = 400): Promise<void> =>
|
|
|
757
788
|
});
|
|
758
789
|
|
|
759
790
|
/** Open a compact row — a read-first row shows its label, not its editor. */
|
|
760
|
-
const openCompactRow = async (canvasElement: HTMLElement): Promise<void> => {
|
|
791
|
+
const openCompactRow = async (canvasElement: HTMLElement, rowLabel = 'Value'): Promise<void> => {
|
|
761
792
|
const label = (await waitFor(
|
|
762
793
|
() => {
|
|
763
|
-
const el = within(canvasElement).queryByText(
|
|
794
|
+
const el = within(canvasElement).queryByText(rowLabel);
|
|
764
795
|
if (!el) throw new Error('row not rendered');
|
|
765
796
|
return el as HTMLElement;
|
|
766
797
|
},
|
|
@@ -1108,7 +1139,7 @@ export const FieldMenuStaysInTheToolbar: Story = {
|
|
|
1108
1139
|
docs: {
|
|
1109
1140
|
description: {
|
|
1110
1141
|
story:
|
|
1111
|
-
"
|
|
1142
|
+
"Types 1 + 2 on a compact row whose field also has templates: the field enters its value on tabs (Value · Expression · Visual), and the expression moves it to Expression. The row shows ONE More menu — the field publishes its items into the row's ⋮ rather than drawing its own — and that menu sits in the tabs' row, not part-way down the editor.",
|
|
1112
1143
|
},
|
|
1113
1144
|
},
|
|
1114
1145
|
},
|
|
@@ -1128,7 +1159,7 @@ export const FieldMenuStaysInTheToolbar: Story = {
|
|
|
1128
1159
|
expected: {
|
|
1129
1160
|
type: 'string',
|
|
1130
1161
|
ui_type: 'string',
|
|
1131
|
-
display_name: '
|
|
1162
|
+
display_name: 'Expected',
|
|
1132
1163
|
preselected: true,
|
|
1133
1164
|
supports_expressions: true,
|
|
1134
1165
|
supports_templates: true,
|
|
@@ -1150,8 +1181,25 @@ export const FieldMenuStaysInTheToolbar: Story = {
|
|
|
1150
1181
|
);
|
|
1151
1182
|
},
|
|
1152
1183
|
async play({ canvasElement }) {
|
|
1153
|
-
await openCompactRow(canvasElement);
|
|
1154
|
-
await
|
|
1184
|
+
await openCompactRow(canvasElement, 'Expected');
|
|
1185
|
+
const input = (await waitFor(
|
|
1186
|
+
() => {
|
|
1187
|
+
const el = canvasElement.querySelector('.value-tab-text [contenteditable="true"]');
|
|
1188
|
+
if (!el) throw new Error('field not ready');
|
|
1189
|
+
return el as HTMLElement;
|
|
1190
|
+
},
|
|
1191
|
+
{ timeout: 10000 }
|
|
1192
|
+
)) as HTMLElement;
|
|
1193
|
+
await userEvent.click(input);
|
|
1194
|
+
await userEvent.type(input, '1 + 2');
|
|
1195
|
+
// the Value tab never holds an expression: the field moves to Expression, in the tabs' row
|
|
1196
|
+
await waitFor(
|
|
1197
|
+
() =>
|
|
1198
|
+
expect(
|
|
1199
|
+
canvasElement.querySelector('.value-tab[aria-pressed="true"]')?.getAttribute('data-tab')
|
|
1200
|
+
).toBe('Expression'),
|
|
1201
|
+
{ timeout: 10000 }
|
|
1202
|
+
);
|
|
1155
1203
|
|
|
1156
1204
|
// One menu per control: the row's ⋮, not a second one drawn by the field.
|
|
1157
1205
|
expect(canvasElement.querySelector('.template-more')).toBeNull();
|
|
@@ -1159,9 +1207,9 @@ export const FieldMenuStaysInTheToolbar: Story = {
|
|
|
1159
1207
|
expect(menus).toHaveLength(1);
|
|
1160
1208
|
const menu = menus[0] as HTMLElement;
|
|
1161
1209
|
|
|
1162
|
-
// The toolbar row is the
|
|
1163
|
-
const
|
|
1164
|
-
const toolbar =
|
|
1210
|
+
// The toolbar row is the field's first line — its tabs.
|
|
1211
|
+
const tabs = canvasElement.querySelector('.value-tabs') as HTMLElement;
|
|
1212
|
+
const toolbar = tabs.getBoundingClientRect();
|
|
1165
1213
|
const control = menu.getBoundingClientRect();
|
|
1166
1214
|
|
|
1167
1215
|
/* Reported as numbers: "the menu is in the wrong place" is not something a
|