@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
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import {
|
|
10
10
|
ReqoreButton,
|
|
11
11
|
ReqoreControlGroup,
|
|
12
|
+
ReqoreDropdown,
|
|
12
13
|
ReqoreMessage,
|
|
13
14
|
ReqoreVerticalSpacer,
|
|
14
15
|
} from '@qoretechnologies/reqore';
|
|
@@ -20,6 +21,7 @@ import { TDpqlFields } from '../../dpqlEditor/types';
|
|
|
20
21
|
import { ExpressionBuilder, IExpressionBuilderProps } from './builder';
|
|
21
22
|
import { DpqlRendering } from './DpqlRendering';
|
|
22
23
|
import { IExpression, IExpressionSchema, IExpressionValue, TExpressionReorder } from './types';
|
|
24
|
+
import { incompleteExpressionText, serializableExpression } from './textOfExpression';
|
|
23
25
|
import { useExpressions } from './useExpressions';
|
|
24
26
|
import { useRenderExpression } from './useRenderExpression';
|
|
25
27
|
import { isUntypedOptionType } from '../../../helpers/optionUiTypes';
|
|
@@ -80,6 +82,26 @@ export interface IExpressionFieldProps {
|
|
|
80
82
|
fields?: TDpqlFields;
|
|
81
83
|
/** Initial editor mode (default `visual`). */
|
|
82
84
|
defaultMode?: TExpressionMode;
|
|
85
|
+
/**
|
|
86
|
+
* The Text view's text when the value is not an expression yet: a template or a literal the field held
|
|
87
|
+
* when "Use Template / Expression" opened it. Nothing is emitted until it is edited.
|
|
88
|
+
*/
|
|
89
|
+
initialText?: string;
|
|
90
|
+
/**
|
|
91
|
+
* The field holds a plain value the text stands for - a template or a literal written in the Text view
|
|
92
|
+
* (stored as itself, not as an expression). The expression is then gone from `value` but the field is not
|
|
93
|
+
* cleared, so the text being written stays.
|
|
94
|
+
*/
|
|
95
|
+
heldAsValue?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* The view a host's own tabs ask for (Value · Expression · Visual, qorus#646). Followed as the field's own
|
|
98
|
+
* Visual / Text toggle is: leaving the Text view parses what is written first, so nothing is lost.
|
|
99
|
+
*/
|
|
100
|
+
requestedMode?: TExpressionMode;
|
|
101
|
+
/** The host draws the views as tabs of its own: the field's Visual / Text toggle is not drawn. */
|
|
102
|
+
hideModeToggle?: boolean;
|
|
103
|
+
/** No language server: the Text view cannot work, and the field shows the Visual view instead. */
|
|
104
|
+
onTextUnavailable?: () => void;
|
|
83
105
|
/**
|
|
84
106
|
* SEAM (reqraft): the host's per-`ui_type` editors, forwarded to the
|
|
85
107
|
* builder's operand fields. Without them an operand typed with one of the
|
|
@@ -107,6 +129,11 @@ export const ExpressionField = memo(
|
|
|
107
129
|
fields,
|
|
108
130
|
recordType,
|
|
109
131
|
defaultMode = 'visual',
|
|
132
|
+
initialText,
|
|
133
|
+
heldAsValue,
|
|
134
|
+
requestedMode,
|
|
135
|
+
hideModeToggle,
|
|
136
|
+
onTextUnavailable,
|
|
110
137
|
size,
|
|
111
138
|
componentOverrides,
|
|
112
139
|
reorder,
|
|
@@ -124,7 +151,15 @@ export const ExpressionField = memo(
|
|
|
124
151
|
|
|
125
152
|
// Text mode state. `text` is the DPQL string the editor shows; the AST
|
|
126
153
|
// (`value`) stays the source of truth, kept in sync via parse-on-edit.
|
|
127
|
-
const [text, setText] = useState('');
|
|
154
|
+
const [text, setText] = useState(initialText ?? '');
|
|
155
|
+
/* No language server to parse or write the text: the Text view cannot work, so the field shows the
|
|
156
|
+
Visual view, which needs none, and says why (qorus#646). */
|
|
157
|
+
const [textUnavailable, setTextUnavailable] = useState(false);
|
|
158
|
+
const handleTextUnavailable = useCallback(() => {
|
|
159
|
+
setTextUnavailable(true);
|
|
160
|
+
setMode('visual');
|
|
161
|
+
onTextUnavailable?.();
|
|
162
|
+
}, [onTextUnavailable]);
|
|
128
163
|
/* The text the current AST was parsed FROM.
|
|
129
164
|
*
|
|
130
165
|
* The preview renders the AST, and the AST only moves on a SUCCESSFUL
|
|
@@ -173,9 +208,14 @@ export const ExpressionField = memo(
|
|
|
173
208
|
* noise the message below is written to avoid.
|
|
174
209
|
*/
|
|
175
210
|
const readTypeCheck = useCallback(
|
|
176
|
-
(result?: {
|
|
177
|
-
|
|
178
|
-
|
|
211
|
+
(result?: {
|
|
212
|
+
type_compatible?: boolean;
|
|
213
|
+
auto_coercible?: boolean;
|
|
214
|
+
coercion_may_fail?: boolean;
|
|
215
|
+
inferred_type?: string;
|
|
216
|
+
target_type?: string;
|
|
217
|
+
suggested_fix?: { text: string };
|
|
218
|
+
}): void => {
|
|
179
219
|
if (!result || result.type_compatible === undefined) {
|
|
180
220
|
setTypeCheck(null);
|
|
181
221
|
return;
|
|
@@ -245,12 +285,17 @@ export const ExpressionField = memo(
|
|
|
245
285
|
hadExpression.current = true;
|
|
246
286
|
return;
|
|
247
287
|
}
|
|
288
|
+
// a template or a literal written here is held as itself: the text stands for it, and stays
|
|
289
|
+
if (heldAsValue) {
|
|
290
|
+
hadExpression.current = false;
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
248
293
|
if (hadExpression.current) {
|
|
249
294
|
hadExpression.current = false;
|
|
250
295
|
userTypedRef.current = false;
|
|
251
296
|
setText('');
|
|
252
297
|
}
|
|
253
|
-
}, [ast]);
|
|
298
|
+
}, [ast, heldAsValue]);
|
|
254
299
|
|
|
255
300
|
// Text mode: parse the DPQL into the AST (debounced).
|
|
256
301
|
/* The session may not be attached when the debounce fires — the editor
|
|
@@ -329,7 +374,20 @@ export const ExpressionField = memo(
|
|
|
329
374
|
}
|
|
330
375
|
if (seededRef.current || userTypedRef.current) return undefined;
|
|
331
376
|
if (!ast?.exp) return undefined;
|
|
332
|
-
|
|
377
|
+
/* Only what DPQL can write is serialized. An operand whose operation is not
|
|
378
|
+
chosen yet, or that is not filled in, has no text; handed to serialize as
|
|
379
|
+
it was, it came back as the server's (or a stand-in's) rendering of a hash:
|
|
380
|
+
`{args=(null)} > {type=int}`. The Text view shows the expression with a hole
|
|
381
|
+
for each part not filled in instead - `… > …`, as the row's summary does -
|
|
382
|
+
and serialize gets custom Text values as the strings or templates they are. */
|
|
383
|
+
const seedAst = serializableExpression(ast);
|
|
384
|
+
if (!seedAst) {
|
|
385
|
+
const holes = incompleteExpressionText(ast, expressions);
|
|
386
|
+
seededRef.current = true;
|
|
387
|
+
setText(holes);
|
|
388
|
+
setAstText(holes);
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
333
391
|
let cancelled = false;
|
|
334
392
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
335
393
|
let tries = 0;
|
|
@@ -349,7 +407,23 @@ export const ExpressionField = memo(
|
|
|
349
407
|
cancelled = true;
|
|
350
408
|
if (timer) clearTimeout(timer);
|
|
351
409
|
};
|
|
352
|
-
}, [mode, ast]);
|
|
410
|
+
}, [mode, ast, expressions]);
|
|
411
|
+
|
|
412
|
+
/** The field's templates, its catalogue's entries, for the Text view's picker. */
|
|
413
|
+
const templateItems = useMemo(
|
|
414
|
+
() => (localTemplates?.items ?? []) as NonNullable<IReqoreFormTemplates['items']>,
|
|
415
|
+
[localTemplates]
|
|
416
|
+
);
|
|
417
|
+
/* A template chosen from the picker goes in after the text, as a chip, and is parsed as typed text is:
|
|
418
|
+
the editor keeps no caret the field can reach, so the end of the text is where it is put. */
|
|
419
|
+
const insertTemplate = useCallback(
|
|
420
|
+
(item: { value?: unknown }) => {
|
|
421
|
+
if (typeof item?.value !== 'string' || !item.value) return;
|
|
422
|
+
const current = text.trimEnd();
|
|
423
|
+
handleDpqlChange(current ? `${current} ${item.value}` : item.value);
|
|
424
|
+
},
|
|
425
|
+
[text, handleDpqlChange]
|
|
426
|
+
);
|
|
353
427
|
|
|
354
428
|
// Switch to Text: the seeding effect above serializes the AST once
|
|
355
429
|
// the editor's session is up.
|
|
@@ -370,6 +444,13 @@ export const ExpressionField = memo(
|
|
|
370
444
|
setMode('visual');
|
|
371
445
|
}, [mode, text, onChange, targetType, readTypeCheck]);
|
|
372
446
|
|
|
447
|
+
// the host's tabs: followed as the toggle is (see `requestedMode`)
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (!requestedMode || requestedMode === mode) return;
|
|
450
|
+
if (requestedMode === 'visual') void enterVisualMode();
|
|
451
|
+
else if (!textUnavailable) enterTextMode();
|
|
452
|
+
}, [requestedMode]);
|
|
453
|
+
|
|
373
454
|
useEffect(
|
|
374
455
|
() => () => {
|
|
375
456
|
if (parseTimer.current) clearTimeout(parseTimer.current);
|
|
@@ -394,39 +475,64 @@ export const ExpressionField = memo(
|
|
|
394
475
|
className='expression-field'
|
|
395
476
|
data-preview={mode === 'text' ? previewState : undefined}
|
|
396
477
|
>
|
|
397
|
-
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
478
|
+
{hideModeToggle ? null : (
|
|
479
|
+
<ReqoreControlGroup gapSize='small' fluid>
|
|
480
|
+
<ReqoreButton
|
|
481
|
+
icon='NodeTree'
|
|
482
|
+
active={mode === 'visual'}
|
|
483
|
+
onClick={enterVisualMode}
|
|
484
|
+
disabled={readOnly}
|
|
485
|
+
size={size as any}
|
|
486
|
+
>
|
|
487
|
+
Visual
|
|
488
|
+
</ReqoreButton>
|
|
489
|
+
<ReqoreButton
|
|
490
|
+
icon='CodeLine'
|
|
491
|
+
active={mode === 'text'}
|
|
492
|
+
onClick={enterTextMode}
|
|
493
|
+
disabled={readOnly || textUnavailable}
|
|
494
|
+
tooltip={
|
|
495
|
+
textUnavailable ? 'The expression language server is not available' : undefined
|
|
496
|
+
}
|
|
497
|
+
size={size as any}
|
|
498
|
+
>
|
|
499
|
+
Text
|
|
500
|
+
</ReqoreButton>
|
|
501
|
+
</ReqoreControlGroup>
|
|
502
|
+
)}
|
|
417
503
|
|
|
418
|
-
{mode === 'text' ?
|
|
504
|
+
{mode === 'text' ?
|
|
419
505
|
<>
|
|
420
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
506
|
+
<ReqoreControlGroup gapSize='small' fluid verticalAlign='flex-start'>
|
|
507
|
+
<DpqlEditor
|
|
508
|
+
ref={dpqlRef}
|
|
509
|
+
value={text}
|
|
510
|
+
onChange={handleDpqlChange}
|
|
511
|
+
provider={provider}
|
|
512
|
+
recordType={recordType}
|
|
513
|
+
fields={fields}
|
|
514
|
+
// the field's own templates name the chips as its catalogue names them
|
|
515
|
+
templates={localTemplates}
|
|
516
|
+
readOnly={readOnly}
|
|
517
|
+
height='48px'
|
|
518
|
+
onUnavailable={handleTextUnavailable}
|
|
519
|
+
/>
|
|
520
|
+
{/* The field's templates and fields, as its catalogue names them, inserted as chips - next to
|
|
521
|
+
the server's `$` completion, which offers every context but not these names. */}
|
|
522
|
+
{templateItems.length && !readOnly ?
|
|
523
|
+
<ReqoreDropdown
|
|
524
|
+
className='expression-text-template-picker'
|
|
525
|
+
icon='MoneyDollarCircleLine'
|
|
526
|
+
aria-label='Insert a template'
|
|
527
|
+
tooltip='Insert a template'
|
|
528
|
+
size={size as any}
|
|
529
|
+
filterable
|
|
530
|
+
fixed
|
|
531
|
+
items={templateItems}
|
|
532
|
+
onItemSelect={insertTemplate}
|
|
533
|
+
/>
|
|
534
|
+
: null}
|
|
535
|
+
</ReqoreControlGroup>
|
|
430
536
|
{/* What the server said about the result's type, when the field
|
|
431
537
|
declares one to check against.
|
|
432
538
|
|
|
@@ -449,12 +555,12 @@ export const ExpressionField = memo(
|
|
|
449
555
|
{typeCheck.mayFail ?
|
|
450
556
|
' That conversion is attempted rather than guaranteed, so the value itself is checked when it runs.'
|
|
451
557
|
: ''}
|
|
452
|
-
{typeCheck.fix ?
|
|
558
|
+
{typeCheck.fix ?
|
|
453
559
|
<>
|
|
454
560
|
<ReqoreVerticalSpacer height={6} />
|
|
455
561
|
<DpqlRendering text={typeCheck.fix} data-testid='expression-type-fix' />
|
|
456
562
|
</>
|
|
457
|
-
|
|
563
|
+
: null}
|
|
458
564
|
</ReqoreMessage>
|
|
459
565
|
)}
|
|
460
566
|
|
|
@@ -471,30 +577,43 @@ export const ExpressionField = memo(
|
|
|
471
577
|
|
|
472
578
|
An empty query has nothing to render either, so the box appears
|
|
473
579
|
once there is a result rather than holding a placeholder. */}
|
|
474
|
-
{previewState === 'shown' ?
|
|
580
|
+
{previewState === 'shown' ?
|
|
475
581
|
<ReqoreMessage intent='info' size='small' title='Preview' flat opaque={false}>
|
|
476
582
|
<DpqlRendering text={preview} data-testid='expression-preview' />
|
|
477
583
|
</ReqoreMessage>
|
|
478
|
-
|
|
584
|
+
: null}
|
|
479
585
|
</>
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
586
|
+
: <>
|
|
587
|
+
{textUnavailable ?
|
|
588
|
+
<ReqoreMessage
|
|
589
|
+
intent='muted'
|
|
590
|
+
size='small'
|
|
591
|
+
flat
|
|
592
|
+
opaque={false}
|
|
593
|
+
className='expression-text-unavailable'
|
|
594
|
+
>
|
|
595
|
+
The Text view needs the expression language server, which is not available: the
|
|
596
|
+
expression is built here instead.
|
|
597
|
+
</ReqoreMessage>
|
|
598
|
+
: null}
|
|
599
|
+
<ExpressionBuilder
|
|
600
|
+
value={value}
|
|
601
|
+
onChange={(v, remove) => onChange(v, remove)}
|
|
602
|
+
expressions={expressions}
|
|
603
|
+
// Default to `auto` when the field declares no return type, so a
|
|
604
|
+
// valid expression renders `muted` rather than `danger` (the
|
|
605
|
+
// builder's intent requires a truthy returnType).
|
|
606
|
+
returnType={(returnType ?? type ?? 'auto') as any}
|
|
607
|
+
readOnly={readOnly}
|
|
608
|
+
localTemplates={localTemplates ?? { items: [] }}
|
|
609
|
+
serverHandled={serverHandled}
|
|
610
|
+
componentOverrides={componentOverrides}
|
|
611
|
+
extraActions={extraActions}
|
|
612
|
+
size={size}
|
|
613
|
+
reorder={reorder}
|
|
614
|
+
/>
|
|
615
|
+
</>
|
|
616
|
+
}
|
|
498
617
|
</ReqoreControlGroup>
|
|
499
618
|
);
|
|
500
619
|
}
|
|
@@ -200,12 +200,19 @@ export const WithSimpleValue: Story = {
|
|
|
200
200
|
docs: {
|
|
201
201
|
description: {
|
|
202
202
|
story:
|
|
203
|
-
'Renders the ExpressionBuilder holding a single "contains" expression with three arguments — the card shows the operation and each operand slot.',
|
|
203
|
+
'Renders the ExpressionBuilder holding a single "contains" expression with three arguments — the card shows the operation and each operand slot. Each text value is 150px wide, the one holding a template as well as the one holding text: in a row with room, a text field is no narrower than that.',
|
|
204
204
|
},
|
|
205
205
|
},
|
|
206
206
|
},
|
|
207
207
|
play: async () => {
|
|
208
208
|
await waitFor(() => expect(expressionCount()).toBe(1), { timeout: 10000 });
|
|
209
|
+
await waitFor(() =>
|
|
210
|
+
expect(
|
|
211
|
+
[...document.querySelectorAll<HTMLElement>('.expression-arg [data-slate-editor]')].map((editor) =>
|
|
212
|
+
Math.round((editor.closest('.reqore-panel') as HTMLElement).getBoundingClientRect().width)
|
|
213
|
+
)
|
|
214
|
+
).toEqual([150, 150])
|
|
215
|
+
);
|
|
209
216
|
},
|
|
210
217
|
};
|
|
211
218
|
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
isTextOrDateType,
|
|
33
33
|
} from '../../../../helpers/expressions';
|
|
34
34
|
import { addMissingExpressionArgs } from '../argumentPresence';
|
|
35
|
+
import { isUntypedOptionType } from '../../../../helpers/optionUiTypes';
|
|
35
36
|
import { findTemplate } from '../../../../helpers/templates';
|
|
36
37
|
import { validateField, validateFieldWithResult } from '../../../../helpers/validations';
|
|
37
38
|
import { IQorusTypeObject, useQorusTypes } from '../../../../hooks/useQorusTypes';
|
|
@@ -520,9 +521,16 @@ export const Expression = ({
|
|
|
520
521
|
}, [onValueChange, path]);
|
|
521
522
|
|
|
522
523
|
const firstArgSchema = selectedExpression?.args[0];
|
|
524
|
+
/* An empty untyped operand is of the type the catalogue declares for it, as the other operands are:
|
|
525
|
+
falling back to the builder's own type gave the first value of a condition (the builder's `bool`)
|
|
526
|
+
or of an untyped builder (`context`) a type it is not, which rendered as "Unknown type!" once an
|
|
527
|
+
untyped operand was written as text. */
|
|
523
528
|
const firstParamType = firstArgument?.is_expression
|
|
524
529
|
? firstArgSchema?.ui_type
|
|
525
|
-
: firstArgument?.type ||
|
|
530
|
+
: firstArgument?.type ||
|
|
531
|
+
(isUntypedOptionType(firstArgSchema?.ui_type) ? firstArgSchema?.ui_type : undefined) ||
|
|
532
|
+
type ||
|
|
533
|
+
'context';
|
|
526
534
|
let restOfArgs = selectedExpression?.args.slice(1);
|
|
527
535
|
const argCount = size(value.value?.args);
|
|
528
536
|
const canReorderArgs =
|
|
@@ -1129,7 +1137,10 @@ export const Expression = ({
|
|
|
1129
1137
|
allowCustomValues={
|
|
1130
1138
|
!!firstArgument?.allowed_values ||
|
|
1131
1139
|
!!firstArgument?.element_allowed_values ||
|
|
1132
|
-
|
|
1140
|
+
// an untyped operand is written as text from the start, as a typed one is once
|
|
1141
|
+
// it has a type: a template chosen into it is a chip, with text around it
|
|
1142
|
+
((!!firstArgument?.type || isUntypedOptionType(firstArgSchema?.ui_type)) &&
|
|
1143
|
+
!serverExpression)
|
|
1133
1144
|
}
|
|
1134
1145
|
filterTemplatesFunc={(templates) => {
|
|
1135
1146
|
// PORT NOTE: `?.` on `items` added — with no templates at all
|
|
@@ -1242,7 +1253,9 @@ export const Expression = ({
|
|
|
1242
1253
|
!!arg.allowed_values ||
|
|
1243
1254
|
!!arg.element_allowed_values ||
|
|
1244
1255
|
!!rest[index]?.type ||
|
|
1245
|
-
!!arg.default_value
|
|
1256
|
+
!!arg.default_value ||
|
|
1257
|
+
// an untyped operand is written as text from the start (see the first operand)
|
|
1258
|
+
isUntypedOptionType(arg.ui_type)
|
|
1246
1259
|
}
|
|
1247
1260
|
onChange={((_name, value, type, isFunction) => {
|
|
1248
1261
|
updateArg(value, index + 1, isFunction ? undefined : type, isFunction);
|
|
@@ -391,8 +391,13 @@ export const mockParseDpql = (text: string, target?: string): IDpqlMockParseResu
|
|
|
391
391
|
const tokens = tokenize(trimmed);
|
|
392
392
|
const node = parseTokens(tokens, trimmed.length + 1);
|
|
393
393
|
// A lone value is wrapped in `value(...)`, which is how the server tells a
|
|
394
|
-
// literal apart from an expression
|
|
395
|
-
|
|
394
|
+
// literal apart from an expression; a lone template reference in `template(...)`,
|
|
395
|
+
// as the server's parser wraps one (Qore DpqlParser: DP_EXPR_TEMPLATE).
|
|
396
|
+
const isTemplate = !!(node.value as { tmpl_context?: unknown } | undefined)?.tmpl_context;
|
|
397
|
+
const root: IDpqlMockNode =
|
|
398
|
+
node.exp ? node
|
|
399
|
+
: isTemplate ? { exp: 'template', args: [node] }
|
|
400
|
+
: { exp: 'value', args: [node] };
|
|
396
401
|
const inferred = inferDpqlMockType(root);
|
|
397
402
|
return {
|
|
398
403
|
success: true,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright 2026 Qore Technologies, s.r.o.
|
|
2
|
+
// The operands of an expression as the Visual view stores them, read as what they hold.
|
|
3
|
+
//
|
|
4
|
+
// The Visual view stores operands in the shapes its editors produce, and two of
|
|
5
|
+
// them are not values at all:
|
|
6
|
+
//
|
|
7
|
+
// - an operand switched to "Use Expression" whose operation is not chosen yet
|
|
8
|
+
// (`{ is_expression: true, value: { args } }`, no `exp`), and an operand not
|
|
9
|
+
// filled in (`{}`, `{ type }`): there is nothing in them yet;
|
|
10
|
+
// - a custom Text value, which is a rich-text document (a list of Slate nodes),
|
|
11
|
+
// not a string.
|
|
12
|
+
//
|
|
13
|
+
// Everything that writes an expression as text reads its operands here - the
|
|
14
|
+
// Text view's `dpql/serialize`, the read-first row summary, and an application's
|
|
15
|
+
// own summaries - so none of them prints a hash or `[object Object]` for them.
|
|
16
|
+
// Pure: no transport/socket import.
|
|
17
|
+
import { richtextToSegments, richtextToString } from '../../../helpers/common';
|
|
18
|
+
import { IExpression, IExpressionValue } from './types';
|
|
19
|
+
|
|
20
|
+
const isObject = (value: unknown): value is Record<string, any> =>
|
|
21
|
+
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
|
|
23
|
+
/** A rich-text value as the operand it holds, or `undefined` when it is empty. */
|
|
24
|
+
const richtextOperand = (value: unknown): IExpression | undefined => {
|
|
25
|
+
const segments = richtextToSegments(value as never).filter(
|
|
26
|
+
(segment) => segment.kind === 'tag' || segment.text.trim()
|
|
27
|
+
);
|
|
28
|
+
if (!segments.length) return undefined;
|
|
29
|
+
// a single template is that template, as a template picked from the list is
|
|
30
|
+
if (segments.length === 1 && segments[0].kind === 'tag') {
|
|
31
|
+
return { type: 'auto', value: segments[0].value };
|
|
32
|
+
}
|
|
33
|
+
return { type: 'string', value: richtextToString(value as never) };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One operand as what it holds: a literal, a `{ type, value }`, or a nested
|
|
38
|
+
* expression whose own operands are read the same way; `undefined` when it is
|
|
39
|
+
* not filled in yet (an operation not chosen, a value not given).
|
|
40
|
+
*
|
|
41
|
+
* A nested expression whose operation is chosen but whose operands are not all
|
|
42
|
+
* filled in is kept, with its missing operands as `undefined`, so a summary can
|
|
43
|
+
* write what is there.
|
|
44
|
+
*/
|
|
45
|
+
export const expressionOperand = (arg: unknown): unknown => {
|
|
46
|
+
if (arg === undefined || arg === null) return undefined;
|
|
47
|
+
if (!isObject(arg)) return arg;
|
|
48
|
+
if (arg.is_expression) {
|
|
49
|
+
if (!isObject(arg.value) || !arg.value.exp) return undefined;
|
|
50
|
+
return { ...arg, value: expressionWithOperands(arg.value as IExpressionValue) };
|
|
51
|
+
}
|
|
52
|
+
if ('exp' in arg) return arg.exp ? expressionWithOperands(arg as IExpressionValue) : undefined;
|
|
53
|
+
if (!('value' in arg) || arg.value === undefined) return undefined;
|
|
54
|
+
if (arg.type === 'richtext') return richtextOperand(arg.value);
|
|
55
|
+
return arg;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** The expression with each operand read as what it holds (`undefined` where none is given yet). */
|
|
59
|
+
export const expressionWithOperands = (value: IExpressionValue): IExpressionValue => ({
|
|
60
|
+
...value,
|
|
61
|
+
args: (value.args ?? []).map(expressionOperand) as IExpression[],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Whether every part of the expression is filled in: an operation, and each of its operands. */
|
|
65
|
+
export const isExpressionComplete = (value: IExpressionValue | undefined): boolean => {
|
|
66
|
+
if (!value?.exp) return false;
|
|
67
|
+
return (value.args ?? []).every((arg) => {
|
|
68
|
+
const operand = expressionOperand(arg);
|
|
69
|
+
if (operand === undefined) return false;
|
|
70
|
+
if (isObject(operand) && operand.is_expression) return isExpressionComplete(operand.value as IExpressionValue);
|
|
71
|
+
if (isObject(operand) && 'exp' in operand) return isExpressionComplete(operand as IExpressionValue);
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
};
|
|
@@ -8,10 +8,7 @@ import { IExpressionSchema } from './types';
|
|
|
8
8
|
|
|
9
9
|
// String comparators share this arg shape: a richtext value (with the
|
|
10
10
|
// operator word as `label_after`), a richtext operand, and an Ignore Case flag.
|
|
11
|
-
const stringArgs = (
|
|
12
|
-
operandLabel: string,
|
|
13
|
-
operatorWord: string
|
|
14
|
-
): IExpressionSchema['args'] => [
|
|
11
|
+
const stringArgs = (operandLabel: string, operatorWord: string): IExpressionSchema['args'] => [
|
|
15
12
|
{
|
|
16
13
|
name: 'softstring',
|
|
17
14
|
display_name: 'String Value',
|
|
@@ -36,6 +33,21 @@ const comparatorArgs = (operatorWord: string): IExpressionSchema['args'] => [
|
|
|
36
33
|
];
|
|
37
34
|
|
|
38
35
|
export const mockExpressions: IExpressionSchema[] = [
|
|
36
|
+
// As the server defines it (qorus Classes/QorusExpressions.qc QO_VALUE): what a value written alone parses
|
|
37
|
+
// into, so a value the Visual view starts from is shown as itself.
|
|
38
|
+
{
|
|
39
|
+
name: 'value',
|
|
40
|
+
display_name: 'Value',
|
|
41
|
+
short_desc: 'Evaluates and returns a field reference value',
|
|
42
|
+
desc: 'Evaluates the given field reference and returns its value; this allows field references to be used as standalone expressions',
|
|
43
|
+
symbol: '',
|
|
44
|
+
type: 1,
|
|
45
|
+
subtype: 1,
|
|
46
|
+
return_type: 'any',
|
|
47
|
+
ui_return_type: 'any',
|
|
48
|
+
varargs: false,
|
|
49
|
+
args: [{ name: 'any', display_name: 'Value', ui_type: 'any', required: true }],
|
|
50
|
+
},
|
|
39
51
|
{
|
|
40
52
|
name: '==',
|
|
41
53
|
display_name: 'Logical Equals',
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
// can wait uses it: Explain and the Text view's Preview wait for the server's
|
|
5
5
|
// `dpql/renderExpression` (see `useRenderExpression`). Pure: no
|
|
6
6
|
// transport/socket import, so it stays out of the LSP/nanoid dependency graph.
|
|
7
|
+
import { expressionOperand } from './expressionOperands';
|
|
7
8
|
import { IExpression, IExpressionSchema, IExpressionValue } from './types';
|
|
8
9
|
|
|
10
|
+
/** How a part not filled in yet is written: an operation not chosen, a value not given. */
|
|
11
|
+
export const EXPRESSION_HOLE = '…';
|
|
12
|
+
|
|
9
13
|
/** A symbol made only of non-word characters renders infix (`a == b`). */
|
|
10
14
|
const isOperatorSymbol = (symbol?: string): boolean =>
|
|
11
15
|
!!symbol && /^[^\w\s]+$/.test(symbol);
|
|
@@ -109,15 +113,28 @@ const renderInContext = (
|
|
|
109
113
|
// parentheses and commas already delimit its arguments.
|
|
110
114
|
const isInfix = isLogicalGroup || (args.length === 2 && isOperatorSymbol(symbol));
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
/* Each operand as what it holds (expressionOperands): a custom Text value is the
|
|
117
|
+
template or the words in it, and one not filled in yet - an operation not chosen,
|
|
118
|
+
a value not given - is a hole, not `null` and not the editor's rich-text document. */
|
|
119
|
+
const parts = args.map((raw: IExpression, argn: number) => {
|
|
120
|
+
const arg = expressionOperand(raw) as IExpression | undefined;
|
|
121
|
+
if (arg === undefined) return EXPRESSION_HOLE;
|
|
122
|
+
if (arg?.is_expression && arg.value?.exp) {
|
|
123
|
+
return renderInContext(
|
|
124
|
+
arg.value,
|
|
125
|
+
expressions,
|
|
126
|
+
isInfix ? argumentContext(value.exp as string, argn) : PREC_LOOSEST
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (arg && typeof arg === 'object' && 'exp' in arg) {
|
|
130
|
+
return renderInContext(
|
|
131
|
+
arg as IExpressionValue,
|
|
132
|
+
expressions,
|
|
133
|
+
isInfix ? argumentContext(value.exp as string, argn) : PREC_LOOSEST
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return renderLiteral(arg && typeof arg === 'object' && 'value' in arg ? arg.value : arg);
|
|
137
|
+
});
|
|
121
138
|
|
|
122
139
|
if (!isInfix) {
|
|
123
140
|
return `${symbol}(${parts.join(', ')})`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Copyright 2026 Qore Technologies, s.r.o.
|
|
2
|
+
// What the Text view is given for an expression built in the Visual view.
|
|
3
|
+
//
|
|
4
|
+
// `dpql/serialize` was handed the AST as the Visual view stores it, and wrote what
|
|
5
|
+
// it could of operands that are not DPQL (see expressionOperands): the server
|
|
6
|
+
// `{args=(null)} > {type=int}`, a stand-in `undefined([object Object]) > [object Object]`.
|
|
7
|
+
// The Text view serializes only an expression that is filled in, with its operands
|
|
8
|
+
// read as what they hold, and shows one that is not with a hole for each missing part.
|
|
9
|
+
import { expressionOperand, expressionWithOperands, isExpressionComplete } from './expressionOperands';
|
|
10
|
+
import { EXPRESSION_HOLE, renderExpressionToText } from './renderExpressionToText';
|
|
11
|
+
import { IExpressionSchema, IExpressionValue } from './types';
|
|
12
|
+
|
|
13
|
+
export { EXPRESSION_HOLE };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The expression as `dpql/serialize` can write it, or `undefined` when part of it
|
|
17
|
+
* is not filled in yet (an operation not chosen, an operand left empty).
|
|
18
|
+
*/
|
|
19
|
+
export const serializableExpression = (
|
|
20
|
+
value: IExpressionValue | undefined
|
|
21
|
+
): IExpressionValue | undefined => (isExpressionComplete(value) ? expressionWithOperands(value!) : undefined);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An expression that is not filled in yet, as text: what is there, and a
|
|
25
|
+
* {@link EXPRESSION_HOLE} for each part that is not - `… > …` for a comparison
|
|
26
|
+
* whose values are not chosen, as the read-first row writes it. Not DPQL; the
|
|
27
|
+
* Text view shows it until the author writes the rest.
|
|
28
|
+
*/
|
|
29
|
+
export const incompleteExpressionText = (
|
|
30
|
+
value: IExpressionValue | undefined,
|
|
31
|
+
expressions: IExpressionSchema[] = []
|
|
32
|
+
): string =>
|
|
33
|
+
value?.exp
|
|
34
|
+
? renderExpressionToText(value, expressions)
|
|
35
|
+
// a field of the record, as DPQL names it
|
|
36
|
+
.replace(/"\$record:\{([^}]+)\}"/g, '@$1')
|
|
37
|
+
: EXPRESSION_HOLE;
|
|
38
|
+
|
|
39
|
+
export { expressionOperand };
|
|
@@ -20,6 +20,7 @@ import { nanoid } from 'nanoid';
|
|
|
20
20
|
import { useCallback, useEffect, useRef } from 'react';
|
|
21
21
|
import { ReqraftLspClient } from '../../../utils/lspClient';
|
|
22
22
|
import { IExpressionValue } from './types';
|
|
23
|
+
import { expressionWithOperands, isExpressionComplete } from './expressionOperands';
|
|
23
24
|
|
|
24
25
|
/* The synchronous summary a collapsed row prints — see `readFirst.ts`. It is
|
|
25
26
|
not a stand-in for this rendering and is never shown in its place. */
|
|
@@ -137,7 +138,14 @@ export const useRenderExpression = (): IUseRenderExpressionResult => {
|
|
|
137
138
|
pendingRef.current = null;
|
|
138
139
|
if (!job) return;
|
|
139
140
|
lastRunRef.current = Date.now();
|
|
140
|
-
|
|
141
|
+
/* Only what can be rendered is asked for, with its operands read as what they
|
|
142
|
+
hold (expressionOperands) - as the Text view's serialize is. An expression
|
|
143
|
+
with a part not filled in yet (an operation not chosen, a value not given)
|
|
144
|
+
has no rendering; sent as it was, the stories' stand-in server threw on it
|
|
145
|
+
and the real one writes its operands as hashes. */
|
|
146
|
+
const outcome = isExpressionComplete(job.value)
|
|
147
|
+
? await renderViaServer(expressionWithOperands(job.value))
|
|
148
|
+
: null;
|
|
141
149
|
job.resolvers.forEach((r) => r(outcome));
|
|
142
150
|
};
|
|
143
151
|
|