@qoretechnologies/reqraft 0.10.51 → 0.10.52

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.
Files changed (50) hide show
  1. package/dist/components/form/expressions/ExpressionField.d.ts +4 -2
  2. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  3. package/dist/components/form/expressions/ExpressionField.js +2 -2
  4. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  5. package/dist/components/form/expressions/builder/argumentWrapper.d.ts +13 -2
  6. package/dist/components/form/expressions/builder/argumentWrapper.d.ts.map +1 -1
  7. package/dist/components/form/expressions/builder/argumentWrapper.js +61 -3
  8. package/dist/components/form/expressions/builder/argumentWrapper.js.map +1 -1
  9. package/dist/components/form/expressions/builder/index.d.ts +4 -2
  10. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  11. package/dist/components/form/expressions/builder/index.js +96 -15
  12. package/dist/components/form/expressions/builder/index.js.map +1 -1
  13. package/dist/components/form/expressions/builder/moveToPositionStrip.d.ts +19 -0
  14. package/dist/components/form/expressions/builder/moveToPositionStrip.d.ts.map +1 -0
  15. package/dist/components/form/expressions/builder/moveToPositionStrip.js +62 -0
  16. package/dist/components/form/expressions/builder/moveToPositionStrip.js.map +1 -0
  17. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  18. package/dist/components/form/expressions/mockExpressions.js +18 -0
  19. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  20. package/dist/components/form/expressions/types.d.ts +8 -0
  21. package/dist/components/form/expressions/types.d.ts.map +1 -1
  22. package/dist/components/form/fields/schema-definition/CatalogNodeEditor.js +3 -2
  23. package/dist/components/form/fields/schema-definition/CatalogNodeEditor.js.map +1 -1
  24. package/dist/components/form/fields/schema-definition/MigrationsTab.js +3 -3
  25. package/dist/components/form/fields/schema-definition/MigrationsTab.js.map +1 -1
  26. package/dist/components/form/fields/schema-definition/helpers.d.ts +0 -2
  27. package/dist/components/form/fields/schema-definition/helpers.d.ts.map +1 -1
  28. package/dist/components/form/fields/schema-definition/helpers.js +1 -20
  29. package/dist/components/form/fields/schema-definition/helpers.js.map +1 -1
  30. package/dist/components/form/fields/template/TemplateField.d.ts +33 -4
  31. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  32. package/dist/components/form/fields/template/TemplateField.js +57 -7
  33. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  34. package/dist/helpers/common.d.ts +4 -0
  35. package/dist/helpers/common.d.ts.map +1 -1
  36. package/dist/helpers/common.js +30 -1
  37. package/dist/helpers/common.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/components/form/expressions/ExpressionField.tsx +5 -1
  40. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +386 -0
  41. package/src/components/form/expressions/builder/argumentWrapper.tsx +137 -11
  42. package/src/components/form/expressions/builder/index.tsx +153 -5
  43. package/src/components/form/expressions/builder/moveToPositionStrip.tsx +156 -0
  44. package/src/components/form/expressions/mockExpressions.ts +18 -0
  45. package/src/components/form/expressions/types.ts +10 -0
  46. package/src/components/form/fields/schema-definition/CatalogNodeEditor.tsx +1 -1
  47. package/src/components/form/fields/schema-definition/MigrationsTab.tsx +1 -1
  48. package/src/components/form/fields/schema-definition/helpers.ts +0 -9
  49. package/src/components/form/fields/template/TemplateField.tsx +112 -4
  50. package/src/helpers/common.ts +31 -0
@@ -15,12 +15,14 @@ import {
15
15
  TReqoreHexColor,
16
16
  } from '@qoretechnologies/reqore/dist/components/Effect';
17
17
  import { IReqorePanelAction } from '@qoretechnologies/reqore/dist/components/Panel';
18
+ import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
18
19
  import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
19
20
  import { TQorusType } from '@qoretechnologies/ts-toolkit';
20
21
  import { clone, cloneDeep, get, isArray, set, size, unset } from 'lodash';
21
22
  import { darken, rgba } from 'polished';
22
- import React, { useCallback, useMemo, useState } from 'react';
23
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
23
24
  import styled from 'styled-components';
25
+ import { moveItem } from '../../../../helpers/common';
24
26
  import { areQorusTypesCompatible, getArgumentType } from '../../../../helpers/expressions';
25
27
  import { findTemplate } from '../../../../helpers/templates';
26
28
  import { validateField, validateFieldWithResult } from '../../../../helpers/validations';
@@ -29,12 +31,18 @@ import { useReqraftStorage } from '../../../../hooks/useStorage/useStorage';
29
31
  import { useTemplates } from '../../../../hooks/useTemplates';
30
32
  import { AutoFormField as auto } from '../../fields/auto/AutoFormField';
31
33
  import { SelectFormField as Select } from '../../fields/select/Select';
32
- import { TCustomTemplateItems, TemplateField } from '../../fields/template/TemplateField';
34
+ import {
35
+ ITemplateMenuActions,
36
+ TCustomTemplateItems,
37
+ TemplateField,
38
+ } from '../../fields/template/TemplateField';
33
39
  import {
34
40
  ExpressionDefaultValue,
35
41
  IExpression,
36
42
  IExpressionSchema,
37
43
  IExpressionSchemaArg,
44
+ TExpressionReorder,
45
+ TExpressionReorderSurface,
38
46
  } from '../types';
39
47
  import { useExpressions } from '../useExpressions';
40
48
  import { ExpressionArgumentDetail } from './argumentDetail';
@@ -42,6 +50,7 @@ import { ExpressionBuilderArgumentWrapper } from './argumentWrapper';
42
50
  import { ConfirmMismatchedTypesModal } from './confirmMismatchedTypesModal';
43
51
  import { ConfirmUnsupportedTypeModal } from './confirmUnsupportedTypeModal';
44
52
  import { ExpressionItem } from './item';
53
+ import { ExpressionMoveToPositionStrip } from './moveToPositionStrip';
45
54
  import { ExpressionRenderTemplate } from './renderTemplate';
46
55
 
47
56
  const noopUseRegisterHintView = (...args: unknown[]): void => {
@@ -60,6 +69,8 @@ export interface IExpressionBuilderProps {
60
69
  group?: '&&' | '||';
61
70
  onChange?: (value: IExpression, remove?: boolean) => void;
62
71
  onValueChange?: (value: IExpression, path: string, remove?: boolean) => void;
72
+ /** Operand reordering for varargs expressions — see `TExpressionReorder`. Default `true`. */
73
+ reorder?: TExpressionReorder;
63
74
  readOnly?: boolean;
64
75
  id?: string;
65
76
  expressions?: IExpressionSchema[];
@@ -416,6 +427,63 @@ export const Expression = ({
416
427
  [onValueChange, JSON.stringify(value), path]
417
428
  );
418
429
 
430
+ const reorderSurfaces = useMemo((): TExpressionReorderSurface[] => {
431
+ const reorder = props.reorder ?? true;
432
+
433
+ if (reorder === false) {
434
+ return [];
435
+ }
436
+
437
+ return reorder === true ? ['dragHandle', 'overflowMenu'] : reorder;
438
+ }, [props.reorder]);
439
+ // Bumped on every move and folded into the operand keys: the operands are
440
+ // keyed by slot, and a slot's TemplateField keeps template / text-mode
441
+ // state that only resyncs one way — remounting hands every slot a field
442
+ // seeded from the operand that now lives there.
443
+ const [argsGeneration, setArgsGeneration] = useState(0);
444
+ const dragArg = useRef<number | null>(null);
445
+ const [dragOverArg, setDragOverArg] = useState<number | null>(null);
446
+
447
+ const moveVarArg = useCallback(
448
+ (from: number, to: number) => {
449
+ setArgsGeneration((generation) => generation + 1);
450
+ onValueChange(
451
+ {
452
+ ...value,
453
+ value: {
454
+ ...value.value,
455
+ args: moveItem(value.value.args, from, to),
456
+ },
457
+ },
458
+ path
459
+ );
460
+ },
461
+ [onValueChange, JSON.stringify(value), path]
462
+ );
463
+
464
+ const handleArgDragStart = useCallback((index: number) => {
465
+ dragArg.current = index;
466
+ }, []);
467
+ const handleArgDragOver = useCallback((index: number) => {
468
+ setDragOverArg((current) => (current === index ? current : index));
469
+ }, []);
470
+ const handleArgDragEnd = useCallback(() => {
471
+ dragArg.current = null;
472
+ setDragOverArg(null);
473
+ }, []);
474
+ const handleArgDrop = useCallback(
475
+ (index: number) => {
476
+ const from = dragArg.current;
477
+
478
+ if (from !== null && from !== index) {
479
+ moveVarArg(from, index);
480
+ }
481
+
482
+ handleArgDragEnd();
483
+ },
484
+ [moveVarArg, handleArgDragEnd]
485
+ );
486
+
419
487
  const updateMultipleArgs = useCallback(
420
488
  (args: IExpression[]) => {
421
489
  onValueChange(
@@ -478,6 +546,59 @@ export const Expression = ({
478
546
  ? firstArgSchema?.ui_type
479
547
  : firstArgument?.type || type || 'context';
480
548
  let restOfArgs = selectedExpression?.args.slice(1);
549
+ const argCount = size(value.value?.args);
550
+ const canReorderArgs =
551
+ !!selectedExpression?.varargs && argCount > 1 && !readOnly && size(reorderSurfaces) > 0;
552
+ const argReorderSurfaces = canReorderArgs ? reorderSurfaces : undefined;
553
+ const isDragOverArg = (index: number) =>
554
+ dragOverArg === index && dragArg.current !== null && dragArg.current !== index;
555
+
556
+ // The `⋮` menu's "Move Argument" section, one per operand. The wording is
557
+ // axis-free on purpose — the operands wrap into a column on narrow screens.
558
+ const argMenuActions = useMemo((): ITemplateMenuActions[] | undefined => {
559
+ if (!canReorderArgs || !reorderSurfaces.includes('overflowMenu')) {
560
+ return undefined;
561
+ }
562
+
563
+ const moves: [string, string, IReqoreIconName, (index: number) => number][] = [
564
+ ['before', 'Move before', 'ArrowGoBackLine', (index) => index - 1],
565
+ ['after', 'Move after', 'ArrowGoForwardLine', (index) => index + 1],
566
+ ['start', 'Move to start', 'SkipBackLine', () => 0],
567
+ ['end', 'Move to end', 'SkipForwardLine', () => argCount - 1],
568
+ ];
569
+
570
+ return Array.from({ length: argCount }, (_, index) => ({
571
+ label: 'Move Argument',
572
+ icon: 'ArrowLeftRightLine',
573
+ items: [
574
+ ...moves.map(([key, label, icon, target]) => {
575
+ const to = target(index);
576
+
577
+ return {
578
+ label,
579
+ icon,
580
+ className: `expression-arg-move-${key}`,
581
+ disabled: to === index || to < 0 || to >= argCount,
582
+ onClick: () => moveVarArg(index, to),
583
+ };
584
+ }),
585
+ // Any position in one pick — what a ten-part concat needs.
586
+ {
587
+ isCustom: true as const,
588
+ content: (closePopover) => (
589
+ <ExpressionMoveToPositionStrip
590
+ index={index}
591
+ count={argCount}
592
+ onMoveTo={(to) => {
593
+ moveVarArg(index, to);
594
+ closePopover?.();
595
+ }}
596
+ />
597
+ ),
598
+ },
599
+ ],
600
+ }));
601
+ }, [canReorderArgs, reorderSurfaces, argCount, moveVarArg]);
481
602
 
482
603
  if (selectedExpression?.varargs) {
483
604
  restOfArgs = [
@@ -880,6 +1001,15 @@ export const Expression = ({
880
1001
  schema={firstArgSchema}
881
1002
  onTypeChange={handleUpdateTypeChange}
882
1003
  readOnly={readOnly}
1004
+ reorder={argReorderSurfaces}
1005
+ argIndex={0}
1006
+ argCount={argCount}
1007
+ dragOver={isDragOverArg(0)}
1008
+ onMoveArg={moveVarArg}
1009
+ onArgDragStart={handleArgDragStart}
1010
+ onArgDragOver={handleArgDragOver}
1011
+ onArgDrop={handleArgDrop}
1012
+ onArgDragEnd={handleArgDragEnd}
883
1013
  >
884
1014
  <TemplateField
885
1015
  component={auto}
@@ -905,7 +1035,7 @@ export const Expression = ({
905
1035
  !firstArgSchema?.element_allowed_values
906
1036
  }
907
1037
  level={level + 1}
908
- key={`${firstParamType}${selectedExpression?.name}`}
1038
+ key={`${argsGeneration}-${firstParamType}${selectedExpression?.name}`}
909
1039
  type={firstParamType}
910
1040
  defaultType={
911
1041
  firstArgument?.allowed_values || firstArgument?.element_allowed_values
@@ -955,6 +1085,8 @@ export const Expression = ({
955
1085
  );
956
1086
  })
957
1087
  }
1088
+ menuActions={argMenuActions?.[0]}
1089
+ reorder={props.reorder}
958
1090
  expressions={props.expressions as any}
959
1091
  expressions_url={expressionsUrl}
960
1092
  />
@@ -964,7 +1096,9 @@ export const Expression = ({
964
1096
  )}
965
1097
  {selectedExpression
966
1098
  ? restOfArgs?.map((arg, index) => (
967
- <React.Fragment key={`${index}-${arg.ui_type}-${arg.display_name}`}>
1099
+ <React.Fragment
1100
+ key={`${argsGeneration}-${index}-${arg.ui_type}-${arg.display_name}`}
1101
+ >
968
1102
  {arg?.label_before && <ExpressionArgumentDetail label={arg.label_before} />}
969
1103
  <ExpressionBuilderArgumentWrapper
970
1104
  readOnly={readOnly}
@@ -985,6 +1119,15 @@ export const Expression = ({
985
1119
  removeVarArg(index + 1);
986
1120
  }}
987
1121
  hasMultipleArgs={selectedExpression.varargs && size(rest) > 1}
1122
+ reorder={argReorderSurfaces}
1123
+ argIndex={index + 1}
1124
+ argCount={argCount}
1125
+ dragOver={isDragOverArg(index + 1)}
1126
+ onMoveArg={moveVarArg}
1127
+ onArgDragStart={handleArgDragStart}
1128
+ onArgDragOver={handleArgDragOver}
1129
+ onArgDrop={handleArgDrop}
1130
+ onArgDragEnd={handleArgDragEnd}
988
1131
  >
989
1132
  <TemplateField
990
1133
  minimal
@@ -995,7 +1138,7 @@ export const Expression = ({
995
1138
  level={level + 1}
996
1139
  allowFunctions={!arg?.allowed_values && !arg?.element_allowed_values}
997
1140
  isFunction={rest[index]?.is_expression}
998
- key={`${index}-${arg.ui_type}-${arg.display_name}-${level + 1}`}
1141
+ key={`${argsGeneration}-${index}-${arg.ui_type}-${arg.display_name}-${level + 1}`}
999
1142
  // An empty argument has no value to take a type from; fall
1000
1143
  // back to the type the catalogue declares for it, as the
1001
1144
  // first-argument mount already does. Without this an
@@ -1043,6 +1186,8 @@ export const Expression = ({
1043
1186
  type.name === 'context' ? undefined : (type.name as string)
1044
1187
  );
1045
1188
  })}
1189
+ menuActions={argMenuActions?.[index + 1]}
1190
+ reorder={props.reorder}
1046
1191
  expressions={props.expressions as any}
1047
1192
  expressions_url={expressionsUrl}
1048
1193
  allowed_values={arg?.allowed_values}
@@ -1103,6 +1248,7 @@ export const ExpressionBuilder = ({
1103
1248
  expressionsUrl,
1104
1249
  serverHandled,
1105
1250
  extraActions,
1251
+ reorder,
1106
1252
  }: IExpressionBuilderProps) => {
1107
1253
  const templates = useTemplates(!isChild, localTemplates);
1108
1254
  const theme = useReqoreTheme();
@@ -1248,6 +1394,7 @@ export const ExpressionBuilder = ({
1248
1394
  expressionsUrl={expressionsUrl}
1249
1395
  serverHandled={serverHandled}
1250
1396
  extraActions={extraActions}
1397
+ reorder={reorder}
1251
1398
  />
1252
1399
  {index < size(value.value.args) - 1 && (
1253
1400
  <ExpressionArgumentDetail
@@ -1285,6 +1432,7 @@ export const ExpressionBuilder = ({
1285
1432
  id={level === 0 && index === 0 ? 'expression-builder' : undefined}
1286
1433
  serverHandled={serverHandled}
1287
1434
  extraActions={extraActions}
1435
+ reorder={reorder}
1288
1436
  />
1289
1437
  </ReqoreErrorBoundary>
1290
1438
  );
@@ -0,0 +1,156 @@
1
+ import {
2
+ ReqoreButton,
3
+ ReqoreControlGroup,
4
+ ReqoreDropdown,
5
+ ReqoreInput,
6
+ ReqoreMenuItem,
7
+ } from '@qoretechnologies/reqore';
8
+ import { memo, useState } from 'react';
9
+ import { ordinal } from '../../../../helpers/common';
10
+
11
+ /** Positions offered as one-click segments before the typed field takes over. */
12
+ export const QUICK_POSITIONS = 4;
13
+ /** Up to this many operands, a single position dropdown replaces the strip. */
14
+ export const PICKER_POSITIONS = 3;
15
+
16
+ export interface IExpressionMoveToPositionStripProps {
17
+ index: number;
18
+ count: number;
19
+ onMoveTo: (to: number) => void;
20
+ }
21
+
22
+ const positionItems = (index: number, from: number, count: number) =>
23
+ Array.from({ length: count - from }, (_, i) => {
24
+ const to = from + i;
25
+
26
+ return {
27
+ label: ordinal(to + 1),
28
+ className: `expression-arg-move-to-${to + 1}`,
29
+ selected: to === index,
30
+ disabled: to === index,
31
+ value: to,
32
+ };
33
+ });
34
+
35
+ /**
36
+ * The "Move to position" row of the ⋮ menu's "Move Argument" section: an
37
+ * icon and label in the shape of the rows above it. Up to three operands the
38
+ * position sits beside the label as a small dropdown; past that the strip
39
+ * follows under the label — the first four positions as segments, a number
40
+ * field with an arrow, and a "More" list of the rest.
41
+ */
42
+ export const ExpressionMoveToPositionStrip = memo(
43
+ ({ index, count, onMoveTo }: IExpressionMoveToPositionStripProps) => {
44
+ const [draft, setDraft] = useState('');
45
+ const target = Number(draft);
46
+ const canGo =
47
+ draft !== '' &&
48
+ Number.isInteger(target) &&
49
+ target >= 1 &&
50
+ target <= count &&
51
+ target - 1 !== index;
52
+ const go = () => canGo && onMoveTo(target - 1);
53
+ const quick = Math.min(QUICK_POSITIONS, count);
54
+ // The label row: a menu item like its siblings, but not a control itself.
55
+ const label = (
56
+ <ReqoreMenuItem
57
+ icon='ListOrdered'
58
+ label='Move to position'
59
+ readOnly
60
+ fluid={false}
61
+ style={{ cursor: 'default' }}
62
+ />
63
+ );
64
+
65
+ if (count <= PICKER_POSITIONS) {
66
+ return (
67
+ <ReqoreControlGroup
68
+ size='small'
69
+ gapSize='small'
70
+ verticalAlign='center'
71
+ className='expression-arg-move-to'
72
+ >
73
+ {label}
74
+ <ReqoreDropdown
75
+ compact
76
+ className='expression-arg-move-to-picker'
77
+ label={ordinal(index + 1)}
78
+ caretPosition='right'
79
+ items={positionItems(index, 0, count).map((item) => ({
80
+ ...item,
81
+ onClick: () => onMoveTo(item.value),
82
+ }))}
83
+ />
84
+ </ReqoreControlGroup>
85
+ );
86
+ }
87
+
88
+ return (
89
+ <ReqoreControlGroup vertical gapSize='small' className='expression-arg-move-to'>
90
+ {label}
91
+ <ReqoreControlGroup
92
+ size='small'
93
+ gapSize='small'
94
+ fluid={false}
95
+ verticalAlign='center'
96
+ style={{ padding: '0 10px 6px 32px' }}
97
+ >
98
+ <ReqoreControlGroup stack size='small' fluid={false}>
99
+ {Array.from({ length: quick }, (_, to) => (
100
+ <ReqoreButton
101
+ key={to}
102
+ compact
103
+ textAlign='center'
104
+ className={`expression-arg-move-to-${to + 1}`}
105
+ active={to === index}
106
+ disabled={to === index}
107
+ label={String(to + 1)}
108
+ tooltip={to === index ? 'Current position' : `Move to ${ordinal(to + 1)}`}
109
+ onClick={() => onMoveTo(to)}
110
+ />
111
+ ))}
112
+ </ReqoreControlGroup>
113
+ {count > QUICK_POSITIONS && (
114
+ <>
115
+ <ReqoreControlGroup stack size='small' fluid={false}>
116
+ <ReqoreInput
117
+ type='number'
118
+ size='small'
119
+ className='expression-arg-move-to-input'
120
+ placeholder={`${QUICK_POSITIONS + 1}–${count}`}
121
+ value={draft}
122
+ min={1}
123
+ max={count}
124
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
125
+ setDraft(event.target.value)
126
+ }
127
+ onKeyDown={(event: React.KeyboardEvent) => event.key === 'Enter' && go()}
128
+ style={{ width: 64 }}
129
+ />
130
+ <ReqoreButton
131
+ compact
132
+ className='expression-arg-move-to-go'
133
+ icon='ArrowRightLine'
134
+ tooltip='Move to that position'
135
+ disabled={!canGo}
136
+ onClick={go}
137
+ />
138
+ </ReqoreControlGroup>
139
+ <ReqoreDropdown
140
+ compact
141
+ className='expression-arg-move-to-more'
142
+ label='More'
143
+ caretPosition='right'
144
+ filterable={count - QUICK_POSITIONS > 8}
145
+ items={positionItems(index, QUICK_POSITIONS, count).map((item) => ({
146
+ ...item,
147
+ onClick: () => onMoveTo(item.value),
148
+ }))}
149
+ />
150
+ </>
151
+ )}
152
+ </ReqoreControlGroup>
153
+ </ReqoreControlGroup>
154
+ );
155
+ }
156
+ );
@@ -169,6 +169,24 @@ export const mockExpressions: IExpressionSchema[] = [
169
169
  groups: ['Math'],
170
170
  args: [{ name: 'int', display_name: 'Value', ui_type: 'int', required: true }],
171
171
  },
172
+ // --- Varargs (string) ----------------------------------------------------
173
+ // String twin of `+` — the reorder stories seed this one because "first",
174
+ // "second", "third" read as an order where int operands don't.
175
+ {
176
+ name: 'concat',
177
+ display_name: 'Concatenate Strings',
178
+ short_desc: 'Returns all arguments joined into one string',
179
+ desc: 'Returns all arguments joined into one string, in argument order. Accepts a variable number of string operands.',
180
+ symbol: 'concat',
181
+ type: 2,
182
+ subtype: 1,
183
+ return_type: 'string',
184
+ ui_return_type: 'string',
185
+ varargs: true,
186
+ min_args: 1,
187
+ groups: ['String'],
188
+ args: [{ name: 'softstring', display_name: 'Value', ui_type: 'string', required: true }],
189
+ },
172
190
  // --- Function expressions ------------------------------------------------
173
191
  // Single-arg type-conversion function. Used as the wrap/unwrap base: a
174
192
  // boolean-returning op with one operand ("Value To Convert").
@@ -83,3 +83,13 @@ export interface IExpressionSchema {
83
83
  return_type_first_arg?: boolean;
84
84
  return_type_arg_priority?: string[];
85
85
  }
86
+
87
+ /** Where a varargs expression offers operand reordering. */
88
+ export type TExpressionReorderSurface = 'overflowMenu' | 'dragHandle' | 'positionPicker';
89
+
90
+ /**
91
+ * `true` (the default) renders the drag grip together with the "Move Argument"
92
+ * section in the field's `⋮` menu; `false` turns reordering off; a list
93
+ * renders exactly those surfaces.
94
+ */
95
+ export type TExpressionReorder = boolean | TExpressionReorderSurface[];
@@ -45,8 +45,8 @@ import {
45
45
  CatalogLeafForm,
46
46
  ICatalogLeafFormNameField,
47
47
  } from './CatalogLeafForm';
48
+ import { moveItem } from '../../../../helpers/common';
48
49
  import {
49
- moveItem,
50
50
  removeKey,
51
51
  renameKey,
52
52
  SECTION_ICONS,
@@ -18,7 +18,7 @@ import {
18
18
  import { memo, useCallback } from 'react';
19
19
  import { Hint } from './Hint';
20
20
  import { CatalogGroupBody } from './CatalogNodeEditor';
21
- import { moveItem } from './helpers';
21
+ import { moveItem } from '../../../../helpers/common';
22
22
  import { ICatalogList } from './types';
23
23
 
24
24
  export interface IMigrationsTabProps {
@@ -38,15 +38,6 @@ export const renameKey = <T>(
38
38
  return next;
39
39
  };
40
40
 
41
- /** Immutably moves a list element from `from` to `to`. */
42
- export const moveItem = <T>(list: T[], from: number, to: number): T[] => {
43
- if (from === to || from < 0 || to < 0 || from >= list.length) return list;
44
- const next = [...list];
45
- const [item] = next.splice(from, 1);
46
- next.splice(Math.min(to, next.length), 0, item);
47
- return next;
48
- };
49
-
50
41
  /**
51
42
  * Picks a unique key for a new map entry — `base`, then `base_2`,
52
43
  * `base_3`, … — so "Add" never silently overwrites an existing entry.