@finos/legend-query-builder 4.16.20 → 4.16.21

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 (58) hide show
  1. package/lib/components/__test-utils__/QueryBuilderComponentTestUtils.d.ts +21 -1
  2. package/lib/components/__test-utils__/QueryBuilderComponentTestUtils.d.ts.map +1 -1
  3. package/lib/components/__test-utils__/QueryBuilderComponentTestUtils.js +16 -2
  4. package/lib/components/__test-utils__/QueryBuilderComponentTestUtils.js.map +1 -1
  5. package/lib/components/fetch-structure/QueryBuilderPostFilterPanel.js +1 -1
  6. package/lib/components/fetch-structure/QueryBuilderPostFilterPanel.js.map +1 -1
  7. package/lib/components/filter/QueryBuilderFilterPanel.js +1 -1
  8. package/lib/components/filter/QueryBuilderFilterPanel.js.map +1 -1
  9. package/lib/components/shared/BasicValueSpecificationEditor.d.ts +155 -4
  10. package/lib/components/shared/BasicValueSpecificationEditor.d.ts.map +1 -1
  11. package/lib/components/shared/BasicValueSpecificationEditor.js +239 -158
  12. package/lib/components/shared/BasicValueSpecificationEditor.js.map +1 -1
  13. package/lib/components/shared/CustomDatePicker.d.ts +8 -55
  14. package/lib/components/shared/CustomDatePicker.d.ts.map +1 -1
  15. package/lib/components/shared/CustomDatePicker.js +31 -415
  16. package/lib/components/shared/CustomDatePicker.js.map +1 -1
  17. package/lib/components/shared/CustomDatePickerHelper.d.ts +145 -0
  18. package/lib/components/shared/CustomDatePickerHelper.d.ts.map +1 -0
  19. package/lib/components/shared/CustomDatePickerHelper.js +517 -0
  20. package/lib/components/shared/CustomDatePickerHelper.js.map +1 -0
  21. package/lib/components/shared/QueryBuilderVariableSelector.js +1 -1
  22. package/lib/components/shared/QueryBuilderVariableSelector.js.map +1 -1
  23. package/lib/components/shared/V1_BasicValueSpecificationEditor.d.ts +36 -0
  24. package/lib/components/shared/V1_BasicValueSpecificationEditor.d.ts.map +1 -0
  25. package/lib/components/shared/V1_BasicValueSpecificationEditor.js +166 -0
  26. package/lib/components/shared/V1_BasicValueSpecificationEditor.js.map +1 -0
  27. package/lib/index.css +1 -1
  28. package/lib/index.d.ts +3 -0
  29. package/lib/index.d.ts.map +1 -1
  30. package/lib/index.js +3 -0
  31. package/lib/index.js.map +1 -1
  32. package/lib/package.json +1 -1
  33. package/lib/stores/shared/V1_ValueSpecificationEditorHelper.d.ts +23 -0
  34. package/lib/stores/shared/V1_ValueSpecificationEditorHelper.d.ts.map +1 -0
  35. package/lib/stores/shared/V1_ValueSpecificationEditorHelper.js +69 -0
  36. package/lib/stores/shared/V1_ValueSpecificationEditorHelper.js.map +1 -0
  37. package/lib/stores/shared/V1_ValueSpecificationModifierHelper.d.ts +20 -0
  38. package/lib/stores/shared/V1_ValueSpecificationModifierHelper.d.ts.map +1 -0
  39. package/lib/stores/shared/V1_ValueSpecificationModifierHelper.js +38 -0
  40. package/lib/stores/shared/V1_ValueSpecificationModifierHelper.js.map +1 -0
  41. package/lib/stores/shared/ValueSpecificationEditorHelper.d.ts +4 -1
  42. package/lib/stores/shared/ValueSpecificationEditorHelper.d.ts.map +1 -1
  43. package/lib/stores/shared/ValueSpecificationEditorHelper.js +23 -2
  44. package/lib/stores/shared/ValueSpecificationEditorHelper.js.map +1 -1
  45. package/package.json +5 -5
  46. package/src/components/__test-utils__/QueryBuilderComponentTestUtils.tsx +101 -12
  47. package/src/components/fetch-structure/QueryBuilderPostFilterPanel.tsx +1 -1
  48. package/src/components/filter/QueryBuilderFilterPanel.tsx +1 -1
  49. package/src/components/shared/BasicValueSpecificationEditor.tsx +1417 -1085
  50. package/src/components/shared/CustomDatePicker.tsx +136 -902
  51. package/src/components/shared/CustomDatePickerHelper.ts +984 -0
  52. package/src/components/shared/QueryBuilderVariableSelector.tsx +1 -1
  53. package/src/components/shared/V1_BasicValueSpecificationEditor.tsx +396 -0
  54. package/src/index.ts +5 -0
  55. package/src/stores/shared/V1_ValueSpecificationEditorHelper.ts +113 -0
  56. package/src/stores/shared/V1_ValueSpecificationModifierHelper.ts +76 -0
  57. package/src/stores/shared/ValueSpecificationEditorHelper.ts +71 -2
  58. package/tsconfig.json +4 -0
@@ -40,26 +40,29 @@ import {
40
40
  } from '@finos/legend-art';
41
41
  import {
42
42
  type Enum,
43
- type Type,
44
- type ValueSpecification,
45
- type PureModel,
46
43
  type ObserverContext,
47
- PrimitiveInstanceValue,
44
+ type PureModel,
45
+ type ValueSpecification,
48
46
  CollectionInstanceValue,
49
- EnumValueInstanceValue,
50
- INTERNAL__PropagatedValue,
51
- SimpleFunctionExpression,
52
- VariableExpression,
47
+ Enumeration,
53
48
  EnumValueExplicitReference,
54
- PrimitiveType,
55
- PRIMITIVE_TYPE,
56
- GenericTypeExplicitReference,
49
+ EnumValueInstanceValue,
57
50
  GenericType,
58
- Enumeration,
51
+ GenericTypeExplicitReference,
59
52
  getMultiplicityDescription,
60
- matchFunctionName,
61
- isSubType,
53
+ getPrimitiveTypeInstanceFromEnum,
62
54
  InstanceValue,
55
+ INTERNAL__PropagatedValue,
56
+ isSubType,
57
+ matchFunctionName,
58
+ PRIMITIVE_TYPE,
59
+ PrimitiveInstanceValue,
60
+ PrimitiveType,
61
+ SimpleFunctionExpression,
62
+ Type,
63
+ VariableExpression,
64
+ observe_ValueSpecification,
65
+ V1_PackageableType,
63
66
  } from '@finos/legend-graph';
64
67
  import {
65
68
  type DebouncedFunc,
@@ -72,7 +75,6 @@ import {
72
75
  isNonEmptyString,
73
76
  parseCSVString,
74
77
  uniq,
75
- at,
76
78
  } from '@finos/legend-shared';
77
79
  import { flowResult } from 'mobx';
78
80
  import { observer } from 'mobx-react-lite';
@@ -86,8 +88,8 @@ import React, {
86
88
  import {
87
89
  instanceValue_setValue,
88
90
  instanceValue_setValues,
91
+ valueSpecification_setGenericType,
89
92
  } from '../../stores/shared/ValueSpecificationModifierHelper.js';
90
- import { CustomDatePicker } from './CustomDatePicker.js';
91
93
  import { QUERY_BUILDER_SUPPORTED_FUNCTIONS } from '../../graph/QueryBuilderMetaModelConst.js';
92
94
  import {
93
95
  isValidInstanceValue,
@@ -96,12 +98,25 @@ import {
96
98
  import { evaluate } from 'mathjs';
97
99
  import { isUsedDateFunctionSupportedInFormMode } from '../../stores/QueryBuilderStateBuilder.js';
98
100
  import {
101
+ buildPrimitiveInstanceValue,
99
102
  convertTextToEnum,
100
103
  convertTextToPrimitiveInstanceValue,
101
104
  getValueSpecificationStringValue,
102
105
  } from '../../stores/shared/ValueSpecificationEditorHelper.js';
103
-
104
- type TypeCheckOption = {
106
+ import { CustomDatePicker } from './CustomDatePicker.js';
107
+ import {
108
+ type CustomDatePickerValueSpecification,
109
+ type CustomDatePickerUpdateValueSpecification,
110
+ CustomDateOption,
111
+ buildPureAdjustDateFunction,
112
+ CustomFirstDayOfOption,
113
+ buildPureDateFunctionExpression,
114
+ CustomPreviousDayOfWeekOption,
115
+ DatePickerOption,
116
+ } from './CustomDatePickerHelper.js';
117
+ import type { V1_TypeCheckOption } from './V1_BasicValueSpecificationEditor.js';
118
+
119
+ export type TypeCheckOption = {
105
120
  expectedType: Type;
106
121
  /**
107
122
  * Indicates if a strict type-matching will happen.
@@ -109,6 +124,10 @@ type TypeCheckOption = {
109
124
  * for example we can assign a Float to an Integer, a
110
125
  * Date to a DateTime. With this flag set to `true`
111
126
  * we will not allow this.
127
+ *
128
+ * For example, if `match=true`, it means that options in the
129
+ * date-capability-dropdown which are not returning type DateTime
130
+ * will be filtered out.
112
131
  */
113
132
  match?: boolean;
114
133
  };
@@ -223,461 +242,498 @@ const VariableExpressionParameterEditor = observer(
223
242
  },
224
243
  );
225
244
 
226
- const StringPrimitiveInstanceValueEditor = observer(
227
- forwardRef<
228
- HTMLInputElement | SelectComponent,
229
- {
230
- valueSpecification: PrimitiveInstanceValue;
231
- className?: string | undefined;
232
- setValueSpecification: (val: ValueSpecification) => void;
233
- resetValue: () => void;
234
- selectorConfig?: BasicValueSpecificationEditorSelectorConfig | undefined;
235
- observerContext: ObserverContext;
236
- handleBlur?: (() => void) | undefined;
237
- handleKeyDown?: React.KeyboardEventHandler<HTMLDivElement> | undefined;
238
- }
239
- >(function StringPrimitiveInstanceValueEditor(props, ref) {
240
- const {
241
- valueSpecification,
242
- className,
243
- resetValue,
244
- setValueSpecification,
245
- selectorConfig,
246
- observerContext,
247
- handleBlur,
248
- handleKeyDown,
249
- } = props;
250
- const useSelector = Boolean(selectorConfig);
251
- const applicationStore = useApplicationStore();
252
- const value = valueSpecification.values[0] as string | null;
253
- const updateValueSpec = (val: string): void => {
254
- instanceValue_setValue(valueSpecification, val, 0, observerContext);
255
- setValueSpecification(valueSpecification);
256
- };
257
- const changeInputValue: React.ChangeEventHandler<HTMLInputElement> = (
258
- event,
259
- ) => {
260
- updateValueSpec(event.target.value);
261
- };
262
- // custom select
263
- const selectedValue = value ? { value: value, label: value } : null;
264
- const reloadValuesFunc = selectorConfig?.reloadValues;
265
- const changeValue = (
266
- val: null | { value: number | string; label: string },
267
- ): void => {
268
- const newValue = val === null ? '' : val.value.toString();
269
- updateValueSpec(newValue);
270
- };
271
- const handleInputChange = (
272
- inputValue: string,
273
- actionChange: InputActionData,
274
- ): void => {
275
- if (actionChange.action === 'input-change') {
276
- updateValueSpec(inputValue);
277
- reloadValuesFunc?.cancel();
278
- const reloadValuesFuncTransformation = reloadValuesFunc?.(inputValue);
279
- if (reloadValuesFuncTransformation) {
280
- flowResult(reloadValuesFuncTransformation).catch(
281
- applicationStore.alertUnhandledError,
282
- );
283
- }
284
- }
285
- if (actionChange.action === 'input-blur') {
286
- reloadValuesFunc?.cancel();
287
- selectorConfig?.cleanUpReloadValues?.();
288
- }
289
- };
290
- const isLoading = selectorConfig?.isLoading;
291
- const queryOptions = selectorConfig?.values?.length
292
- ? selectorConfig.values.map((e) => ({
293
- value: e,
294
- label: e.toString(),
295
- }))
296
- : undefined;
297
- const noOptionsMessage =
298
- selectorConfig?.values === undefined ? (): null => null : undefined;
299
- const resetButtonName = `reset-${valueSpecification.hashCode}`;
300
- const inputName = `input-${valueSpecification.hashCode}`;
301
-
302
- const onBlur = (
303
- event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
304
- ): void => {
305
- if (
306
- event.relatedTarget?.name !== resetButtonName &&
307
- event.relatedTarget?.name !== inputName
308
- ) {
309
- handleBlur?.();
310
- }
311
- };
245
+ /**
246
+ * This is the base interface for primitive instance value editors (non-collection values).
247
+ * The interface is made generic so that it can support various types of objects that hold the value
248
+ * to be edited (currently, we just use this for ValueSpecification and V1_ValueSpecification).
249
+ *
250
+ * T represents the type of the object that holds the value to be edited (i.e. ValueSpecification or V1_ValueSpecification).
251
+ * U represents the type of data that the object holds.
252
+ *
253
+ * valueSelector: callback that handles extracting the data value from the object.
254
+ * updateValueSpecification: callback that takes the valueSpecification object and the new value and handles updating
255
+ * the object with the new value.
256
+ * errorChecker: optional callback that should return true if the valueSpecification is invalid.
257
+ */
258
+ export interface PrimitiveInstanceValueEditorProps<
259
+ T,
260
+ U extends string | number | boolean | Enum | null,
261
+ > {
262
+ valueSpecification: T;
263
+ valueSelector: (val: T) => U;
264
+ updateValueSpecification: (valueSpecification: T, value: U) => void;
265
+ errorChecker?: (valueSpecification: T) => boolean;
266
+ resetValue: () => void;
267
+ handleBlur?: (() => void) | undefined;
268
+ handleKeyDown?: React.KeyboardEventHandler<HTMLDivElement> | undefined;
269
+ className?: string | undefined;
270
+ }
312
271
 
313
- return (
314
- <div className={clsx('value-spec-editor', className)} onBlur={onBlur}>
315
- {useSelector ? (
316
- <CustomSelectorInput
317
- className="value-spec-editor__enum-selector"
318
- options={queryOptions}
319
- onChange={changeValue}
320
- value={selectedValue}
321
- inputValue={value ?? ''}
322
- onInputChange={handleInputChange}
323
- darkMode={
324
- !applicationStore.layoutService
325
- .TEMPORARY__isLightColorThemeEnabled
326
- }
327
- isLoading={isLoading}
328
- allowCreateWhileLoading={true}
329
- noOptionsMessage={noOptionsMessage}
330
- components={{
331
- DropdownIndicator: null,
332
- }}
333
- hasError={!isValidInstanceValue(valueSpecification)}
334
- placeholder={value === '' ? '(empty)' : undefined}
335
- inputRef={ref as React.Ref<SelectComponent>}
336
- onKeyDown={
337
- handleKeyDown as React.KeyboardEventHandler<HTMLDivElement>
338
- }
339
- inputName={inputName}
340
- />
341
- ) : (
342
- <InputWithInlineValidation
343
- className="panel__content__form__section__input value-spec-editor__input"
344
- spellCheck={false}
345
- value={value ?? ''}
346
- placeholder={value === '' ? '(empty)' : undefined}
347
- onChange={changeInputValue}
348
- ref={ref as React.Ref<HTMLInputElement>}
349
- error={
350
- !isValidInstanceValue(valueSpecification)
351
- ? 'Invalid String value'
352
- : undefined
353
- }
354
- onKeyDown={handleKeyDown}
355
- name={inputName}
356
- />
357
- )}
358
- <button
359
- className="value-spec-editor__reset-btn"
360
- name={resetButtonName}
361
- title="Reset"
362
- onClick={resetValue}
363
- >
364
- <RefreshIcon />
365
- </button>
366
- </div>
367
- );
368
- }),
369
- );
272
+ export interface BasicValueSpecificationEditorSelectorSearchConfig {
273
+ values: string[] | undefined;
274
+ isLoading: boolean;
275
+ reloadValues:
276
+ | DebouncedFunc<(inputValue: string) => GeneratorFn<void>>
277
+ | undefined;
278
+ cleanUpReloadValues?: () => void;
279
+ }
370
280
 
371
- const BooleanPrimitiveInstanceValueEditor = observer(
372
- (props: {
373
- valueSpecification: PrimitiveInstanceValue;
374
- className?: string | undefined;
375
- resetValue: () => void;
376
- setValueSpecification: (val: ValueSpecification) => void;
377
- observerContext: ObserverContext;
378
- }) => {
379
- const {
380
- valueSpecification,
381
- className,
382
- resetValue,
383
- setValueSpecification,
384
- observerContext,
385
- } = props;
386
- const value = valueSpecification.values[0] as boolean;
387
- const toggleValue = (): void => {
388
- instanceValue_setValue(valueSpecification, !value, 0, observerContext);
389
- setValueSpecification(valueSpecification);
390
- };
281
+ export interface BasicValueSpecificationEditorSelectorConfig {
282
+ optionCustomization?: { rowHeight?: number | undefined } | undefined;
283
+ }
391
284
 
392
- return (
393
- <div className={clsx('value-spec-editor', className)}>
394
- <button
395
- className={clsx('value-spec-editor__toggler__btn', {
396
- 'value-spec-editor__toggler__btn--toggled': value,
397
- })}
398
- onClick={toggleValue}
399
- >
400
- {value ? <CheckSquareIcon /> : <SquareIcon />}
401
- </button>
402
- <button
403
- className="value-spec-editor__reset-btn"
404
- name="Reset"
405
- title="Reset"
406
- onClick={resetValue}
407
- >
408
- <RefreshIcon />
409
- </button>
410
- </div>
411
- );
412
- },
413
- );
285
+ interface StringPrimitiveInstanceValueEditorProps<T>
286
+ extends PrimitiveInstanceValueEditorProps<T, string | null> {
287
+ selectorSearchConfig?:
288
+ | BasicValueSpecificationEditorSelectorSearchConfig
289
+ | undefined;
290
+ selectorConfig?: BasicValueSpecificationEditorSelectorConfig | undefined;
291
+ }
414
292
 
415
- const NumberPrimitiveInstanceValueEditor = observer(
416
- forwardRef<
417
- HTMLInputElement,
418
- {
419
- valueSpecification: PrimitiveInstanceValue;
420
- isInteger: boolean;
421
- className?: string | undefined;
422
- resetValue: () => void;
423
- setValueSpecification: (val: ValueSpecification) => void;
424
- observerContext: ObserverContext;
425
- handleBlur?: (() => void) | undefined;
426
- handleKeyDown?:
427
- | ((event: React.KeyboardEvent<HTMLInputElement>) => void)
428
- | undefined;
429
- }
430
- >(function NumberPrimitiveInstanceValueEditor(props, ref) {
431
- const {
432
- valueSpecification,
433
- isInteger,
434
- className,
435
- resetValue,
436
- setValueSpecification,
437
- observerContext,
438
- handleBlur,
439
- handleKeyDown,
440
- } = props;
441
- const [value, setValue] = useState(
442
- valueSpecification.values[0] === null
443
- ? ''
444
- : (valueSpecification.values[0] as number).toString(),
445
- );
446
- const inputRef = useRef<HTMLInputElement>(null);
447
- useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);
448
- const numericValue = value
449
- ? isInteger
450
- ? Number.parseInt(Number(value).toString(), 10)
451
- : Number(value)
452
- : null;
453
-
454
- const updateValueSpecIfValid = (val: string): void => {
455
- if (val) {
456
- const parsedValue = isInteger
457
- ? Number.parseInt(Number(val).toString(), 10)
458
- : Number(val);
459
- if (
460
- !isNaN(parsedValue) &&
461
- parsedValue !== valueSpecification.values[0]
462
- ) {
463
- instanceValue_setValue(
464
- valueSpecification,
465
- parsedValue,
466
- 0,
467
- observerContext,
468
- );
469
- setValueSpecification(valueSpecification);
470
- }
471
- } else {
472
- resetValue();
293
+ // eslint-disable-next-line comma-spacing
294
+ const StringPrimitiveInstanceValueEditorInner = <T,>(
295
+ props: StringPrimitiveInstanceValueEditorProps<T>,
296
+ ref: React.ForwardedRef<HTMLInputElement | SelectComponent | null>,
297
+ ): React.ReactElement => {
298
+ const {
299
+ valueSpecification,
300
+ valueSelector,
301
+ updateValueSpecification,
302
+ errorChecker,
303
+ resetValue,
304
+ handleBlur,
305
+ handleKeyDown,
306
+ className,
307
+ selectorSearchConfig,
308
+ selectorConfig,
309
+ } = props;
310
+ const useSelector = Boolean(selectorSearchConfig);
311
+ const applicationStore = useApplicationStore();
312
+ const value = valueSelector(valueSpecification);
313
+ const changeInputValue: React.ChangeEventHandler<HTMLInputElement> = (
314
+ event,
315
+ ) => {
316
+ updateValueSpecification(valueSpecification, event.target.value);
317
+ };
318
+ // custom select
319
+ const selectedValue = value ? { value: value, label: value } : null;
320
+ const reloadValuesFunc = selectorSearchConfig?.reloadValues;
321
+ const changeValue = (
322
+ val: null | { value: number | string; label: string },
323
+ ): void => {
324
+ const newValue = val === null ? '' : val.value.toString();
325
+ updateValueSpecification(valueSpecification, newValue);
326
+ };
327
+ const handleInputChange = (
328
+ inputValue: string,
329
+ actionChange: InputActionData,
330
+ ): void => {
331
+ if (actionChange.action === 'input-change') {
332
+ updateValueSpecification(valueSpecification, inputValue);
333
+ reloadValuesFunc?.cancel();
334
+ const reloadValuesFuncTransformation = reloadValuesFunc?.(inputValue);
335
+ if (reloadValuesFuncTransformation) {
336
+ flowResult(reloadValuesFuncTransformation).catch(
337
+ applicationStore.alertUnhandledError,
338
+ );
473
339
  }
474
- };
340
+ }
341
+ if (actionChange.action === 'input-blur') {
342
+ reloadValuesFunc?.cancel();
343
+ selectorSearchConfig?.cleanUpReloadValues?.();
344
+ }
345
+ };
346
+ const isLoading = selectorSearchConfig?.isLoading;
347
+ const queryOptions = selectorSearchConfig?.values?.length
348
+ ? selectorSearchConfig.values.map((e) => ({
349
+ value: e,
350
+ label: e.toString(),
351
+ }))
352
+ : undefined;
353
+ const noOptionsMessage =
354
+ selectorSearchConfig?.values === undefined ? (): null => null : undefined;
355
+ const resetButtonName = `reset-${valueSelector(valueSpecification)}`;
356
+ const inputName = `input-${valueSelector(valueSpecification)}`;
357
+
358
+ const onBlur = (
359
+ event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
360
+ ): void => {
361
+ if (
362
+ event.relatedTarget?.name !== resetButtonName &&
363
+ event.relatedTarget?.name !== inputName
364
+ ) {
365
+ handleBlur?.();
366
+ }
367
+ };
475
368
 
476
- const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (
477
- event,
478
- ) => {
479
- setValue(event.target.value);
480
- updateValueSpecIfValid(event.target.value);
481
- };
369
+ return (
370
+ <div className={clsx('value-spec-editor', className)} onBlur={onBlur}>
371
+ {useSelector ? (
372
+ <CustomSelectorInput
373
+ className="value-spec-editor__enum-selector"
374
+ options={queryOptions}
375
+ onChange={changeValue}
376
+ value={selectedValue}
377
+ inputValue={value ?? ''}
378
+ onInputChange={handleInputChange}
379
+ darkMode={
380
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
381
+ }
382
+ isLoading={isLoading}
383
+ allowCreateWhileLoading={true}
384
+ noOptionsMessage={noOptionsMessage}
385
+ components={{
386
+ DropdownIndicator: null,
387
+ }}
388
+ hasError={errorChecker?.(valueSpecification)}
389
+ placeholder={value === '' ? '(empty)' : undefined}
390
+ inputRef={ref as React.Ref<SelectComponent>}
391
+ onKeyDown={
392
+ handleKeyDown as React.KeyboardEventHandler<HTMLDivElement>
393
+ }
394
+ inputName={inputName}
395
+ optionCustomization={selectorConfig?.optionCustomization}
396
+ />
397
+ ) : (
398
+ <InputWithInlineValidation
399
+ className="panel__content__form__section__input value-spec-editor__input"
400
+ spellCheck={false}
401
+ value={value ?? ''}
402
+ placeholder={value === '' ? '(empty)' : undefined}
403
+ onChange={changeInputValue}
404
+ ref={ref as React.Ref<HTMLInputElement>}
405
+ error={
406
+ errorChecker?.(valueSpecification)
407
+ ? 'Invalid String value'
408
+ : undefined
409
+ }
410
+ onKeyDown={handleKeyDown}
411
+ name={inputName}
412
+ />
413
+ )}
414
+ <button
415
+ className="value-spec-editor__reset-btn"
416
+ name={resetButtonName}
417
+ title="Reset"
418
+ onClick={resetValue}
419
+ >
420
+ <RefreshIcon />
421
+ </button>
422
+ </div>
423
+ );
424
+ };
482
425
 
483
- // Support expression evaluation
484
- const calculateExpression = (): void => {
485
- if (numericValue !== null && isNaN(numericValue)) {
486
- // If the value is not a number, try to evaluate it as an expression
487
- try {
488
- const calculatedValue = guaranteeIsNumber(evaluate(value));
489
- updateValueSpecIfValid(calculatedValue.toString());
490
- setValue(calculatedValue.toString());
491
- } catch {
492
- // If we fail to evaluate the expression, we just keep the previous value
493
- const prevValue =
494
- valueSpecification.values[0] !== null &&
495
- valueSpecification.values[0] !== undefined
496
- ? valueSpecification.values[0].toString()
497
- : '';
498
- updateValueSpecIfValid(prevValue);
499
- setValue(prevValue);
500
- }
501
- } else if (numericValue !== null) {
502
- // If numericValue is a number, update the value spec
503
- updateValueSpecIfValid(numericValue.toString());
504
- setValue(numericValue.toString());
505
- } else {
506
- // If numericValue is null, reset the value spec
507
- resetValue();
508
- }
509
- };
426
+ export const StringPrimitiveInstanceValueEditor = observer(
427
+ forwardRef(StringPrimitiveInstanceValueEditorInner) as <T>(
428
+ props: StringPrimitiveInstanceValueEditorProps<T> & {
429
+ ref: React.ForwardedRef<HTMLInputElement | SelectComponent | null>;
430
+ },
431
+ ) => ReturnType<typeof StringPrimitiveInstanceValueEditorInner>,
432
+ );
510
433
 
511
- const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
512
- if (event.code === 'Enter') {
513
- calculateExpression();
514
- inputRef.current?.focus();
515
- } else if (event.code === 'Escape') {
516
- inputRef.current?.select();
517
- }
518
- };
434
+ type BooleanInstanceValueEditorProps<T> = PrimitiveInstanceValueEditorProps<
435
+ T,
436
+ boolean
437
+ >;
519
438
 
520
- useEffect(() => {
521
- if (
522
- numericValue !== null &&
523
- !isNaN(numericValue) &&
524
- numericValue !== valueSpecification.values[0]
525
- ) {
526
- const valueFromValueSpec =
527
- valueSpecification.values[0] !== null
528
- ? (valueSpecification.values[0] as number).toString()
529
- : '';
530
- setValue(valueFromValueSpec);
531
- }
532
- }, [numericValue, valueSpecification]);
439
+ // eslint-disable-next-line comma-spacing
440
+ const BooleanInstanceValueEditorInner = <T,>(
441
+ props: BooleanInstanceValueEditorProps<T>,
442
+ ): React.ReactElement => {
443
+ const {
444
+ valueSpecification,
445
+ valueSelector,
446
+ updateValueSpecification,
447
+ resetValue,
448
+ className,
449
+ } = props;
450
+ const value = valueSelector(valueSpecification);
451
+ const toggleValue = (): void => {
452
+ updateValueSpecification(valueSpecification, !value);
453
+ };
454
+
455
+ return (
456
+ <div className={clsx('value-spec-editor', className)}>
457
+ <button
458
+ role="checkbox"
459
+ className={clsx('value-spec-editor__toggler__btn', {
460
+ 'value-spec-editor__toggler__btn--toggled': value,
461
+ })}
462
+ onClick={toggleValue}
463
+ >
464
+ {value ? <CheckSquareIcon /> : <SquareIcon />}
465
+ </button>
466
+ <button
467
+ className="value-spec-editor__reset-btn"
468
+ name="Reset"
469
+ title="Reset"
470
+ onClick={resetValue}
471
+ >
472
+ <RefreshIcon />
473
+ </button>
474
+ </div>
475
+ );
476
+ };
477
+
478
+ export const BooleanPrimitiveInstanceValueEditor = observer(
479
+ BooleanInstanceValueEditorInner as <T>(
480
+ props: BooleanInstanceValueEditorProps<T>,
481
+ ) => ReturnType<typeof BooleanInstanceValueEditorInner>,
482
+ );
533
483
 
534
- const resetButtonName = `reset-${valueSpecification.hashCode}`;
535
- const inputName = `input-${valueSpecification.hashCode}`;
536
- const calculateButtonName = `calculate-${valueSpecification.hashCode}`;
484
+ interface NumberPrimitiveInstanceValueEditorProps<T>
485
+ extends PrimitiveInstanceValueEditorProps<T, number | null> {
486
+ isInteger: boolean;
487
+ }
537
488
 
538
- const onBlur = (
539
- event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
540
- ): void => {
489
+ // eslint-disable-next-line comma-spacing
490
+ const NumberPrimitiveInstanceValueEditorInner = <T,>(
491
+ props: NumberPrimitiveInstanceValueEditorProps<T>,
492
+ ref: React.ForwardedRef<HTMLInputElement>,
493
+ ): React.ReactElement => {
494
+ const {
495
+ valueSpecification,
496
+ valueSelector,
497
+ updateValueSpecification,
498
+ errorChecker,
499
+ resetValue,
500
+ handleBlur,
501
+ handleKeyDown,
502
+ className,
503
+ isInteger,
504
+ } = props;
505
+ const [value, setValue] = useState(
506
+ valueSelector(valueSpecification)?.toString() ?? '',
507
+ );
508
+ const inputRef = useRef<HTMLInputElement>(null);
509
+ useImperativeHandle(ref, () => inputRef.current as HTMLInputElement, []);
510
+ const numericValue = value
511
+ ? isInteger
512
+ ? Number.parseInt(Number(value).toString(), 10)
513
+ : Number(value)
514
+ : null;
515
+
516
+ const updateValueSpecIfValid = (val: string): void => {
517
+ if (val) {
518
+ const parsedValue = isInteger
519
+ ? Number.parseInt(Number(val).toString(), 10)
520
+ : Number(val);
541
521
  if (
542
- event.relatedTarget?.name !== resetButtonName &&
543
- event.relatedTarget?.name !== inputName &&
544
- event.relatedTarget?.name !== calculateButtonName
522
+ !isNaN(parsedValue) &&
523
+ parsedValue !== valueSelector(valueSpecification)
545
524
  ) {
546
- handleBlur?.();
525
+ updateValueSpecification(valueSpecification, parsedValue);
547
526
  }
548
- };
527
+ } else {
528
+ resetValue();
529
+ }
530
+ };
531
+
532
+ const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = (
533
+ event,
534
+ ) => {
535
+ setValue(event.target.value);
536
+ updateValueSpecIfValid(event.target.value);
537
+ };
538
+
539
+ // Support expression evaluation
540
+ const calculateExpression = (): void => {
541
+ if (numericValue !== null && isNaN(numericValue)) {
542
+ // If the value is not a number, try to evaluate it as an expression
543
+ try {
544
+ const calculatedValue = guaranteeIsNumber(evaluate(value));
545
+ updateValueSpecIfValid(calculatedValue.toString());
546
+ setValue(calculatedValue.toString());
547
+ } catch {
548
+ // If we fail to evaluate the expression, we just keep the previous value
549
+ const prevValue = valueSelector(valueSpecification)?.toString() ?? '';
550
+ updateValueSpecIfValid(prevValue);
551
+ setValue(prevValue);
552
+ }
553
+ } else if (numericValue !== null) {
554
+ // If numericValue is a number, update the value spec
555
+ updateValueSpecIfValid(numericValue.toString());
556
+ setValue(numericValue.toString());
557
+ } else {
558
+ // If numericValue is null, reset the value spec
559
+ resetValue();
560
+ }
561
+ };
562
+
563
+ const onKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
564
+ if (event.code === 'Enter') {
565
+ calculateExpression();
566
+ inputRef.current?.focus();
567
+ } else if (event.code === 'Escape') {
568
+ inputRef.current?.select();
569
+ }
570
+ };
549
571
 
550
- return (
551
- <div className={clsx('value-spec-editor', className)} onBlur={onBlur}>
552
- <div className="value-spec-editor__number__input-container">
553
- <input
554
- ref={inputRef}
555
- className={clsx(
556
- 'panel__content__form__section__input',
557
- 'value-spec-editor__input',
558
- 'value-spec-editor__number__input',
559
- {
560
- 'value-spec-editor__number__input--error':
561
- !isValidInstanceValue(valueSpecification),
562
- },
563
- )}
564
- spellCheck={false}
565
- type="text" // NOTE: we leave this as text so that we can support expression evaluation
566
- inputMode="numeric"
567
- value={value}
568
- onChange={handleInputChange}
569
- onBlur={calculateExpression}
570
- onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
571
- onKeyDown(event);
572
- handleKeyDown?.(event);
573
- }}
574
- name={inputName}
575
- />
576
- <div className="value-spec-editor__number__actions">
577
- <button
578
- className="value-spec-editor__number__action"
579
- title="Evaluate Expression (Enter)"
580
- name={calculateButtonName}
581
- onClick={calculateExpression}
582
- >
583
- <CalculateIcon />
584
- </button>
585
- </div>
572
+ useEffect(() => {
573
+ if (
574
+ numericValue !== null &&
575
+ !isNaN(numericValue) &&
576
+ numericValue !== valueSelector(valueSpecification)
577
+ ) {
578
+ const valueFromValueSpec =
579
+ valueSelector(valueSpecification) !== null
580
+ ? (valueSelector(valueSpecification) as number).toString()
581
+ : '';
582
+ setValue(valueFromValueSpec);
583
+ }
584
+ }, [numericValue, valueSpecification, valueSelector]);
585
+
586
+ const resetButtonName = `reset-${valueSelector(valueSpecification)}`;
587
+ const inputName = `input-${valueSelector(valueSpecification)}`;
588
+ const calculateButtonName = `calculate-${valueSelector(valueSpecification)}`;
589
+
590
+ const onBlur = (
591
+ event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
592
+ ): void => {
593
+ if (
594
+ event.relatedTarget?.name !== resetButtonName &&
595
+ event.relatedTarget?.name !== inputName &&
596
+ event.relatedTarget?.name !== calculateButtonName
597
+ ) {
598
+ handleBlur?.();
599
+ }
600
+ };
601
+
602
+ return (
603
+ <div className={clsx('value-spec-editor', className)} onBlur={onBlur}>
604
+ <div className="value-spec-editor__number__input-container">
605
+ <input
606
+ ref={inputRef}
607
+ className={clsx(
608
+ 'panel__content__form__section__input',
609
+ 'value-spec-editor__input',
610
+ 'value-spec-editor__number__input',
611
+ {
612
+ 'value-spec-editor__number__input--error':
613
+ errorChecker?.(valueSpecification),
614
+ },
615
+ )}
616
+ spellCheck={false}
617
+ type="text" // NOTE: we leave this as text so that we can support expression evaluation
618
+ inputMode="numeric"
619
+ value={value}
620
+ onChange={handleInputChange}
621
+ onBlur={calculateExpression}
622
+ onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
623
+ onKeyDown(event);
624
+ handleKeyDown?.(event);
625
+ }}
626
+ name={inputName}
627
+ />
628
+ <div className="value-spec-editor__number__actions">
629
+ <button
630
+ className="value-spec-editor__number__action"
631
+ title="Evaluate Expression (Enter)"
632
+ name={calculateButtonName}
633
+ onClick={calculateExpression}
634
+ >
635
+ <CalculateIcon />
636
+ </button>
586
637
  </div>
587
- <button
588
- className="value-spec-editor__reset-btn"
589
- name={resetButtonName}
590
- title="Reset"
591
- onClick={resetValue}
592
- >
593
- <RefreshIcon />
594
- </button>
595
638
  </div>
596
- );
597
- }),
639
+ <button
640
+ className="value-spec-editor__reset-btn"
641
+ name={resetButtonName}
642
+ title="Reset"
643
+ onClick={resetValue}
644
+ >
645
+ <RefreshIcon />
646
+ </button>
647
+ </div>
648
+ );
649
+ };
650
+
651
+ export const NumberPrimitiveInstanceValueEditor = observer(
652
+ forwardRef(NumberPrimitiveInstanceValueEditorInner) as <T>(
653
+ props: NumberPrimitiveInstanceValueEditorProps<T> & {
654
+ ref: React.ForwardedRef<HTMLInputElement>;
655
+ },
656
+ ) => ReturnType<typeof NumberPrimitiveInstanceValueEditorInner>,
598
657
  );
599
658
 
600
- const EnumValueInstanceValueEditor = observer(
601
- (props: {
602
- valueSpecification: EnumValueInstanceValue;
603
- className?: string | undefined;
604
- setValueSpecification: (val: ValueSpecification) => void;
605
- resetValue: () => void;
606
- observerContext: ObserverContext;
607
- handleBlur?: (() => void) | undefined;
608
- }) => {
609
- const {
610
- valueSpecification,
611
- className,
612
- resetValue,
613
- setValueSpecification,
614
- observerContext,
615
- handleBlur,
616
- } = props;
617
- const applicationStore = useApplicationStore();
618
- const enumType = guaranteeType(
619
- valueSpecification.genericType?.value.rawType,
620
- Enumeration,
621
- );
622
- const enumValue =
623
- valueSpecification.values[0] === undefined
624
- ? null
625
- : valueSpecification.values[0].value;
626
- const options = enumType.values.map((value) => ({
627
- label: value.name,
628
- value: value,
629
- }));
630
- const resetButtonName = `reset-${valueSpecification.hashCode}`;
631
- const inputName = `input-${valueSpecification.hashCode}`;
659
+ /**
660
+ * Generic interface for handling editing enum values. The editor component
661
+ * expects an options array which contains the list of possible enum values.
662
+ */
663
+ interface EnumInstanceValueEditorProps<T>
664
+ extends PrimitiveInstanceValueEditorProps<T, string | null> {
665
+ options: { label: string; value: string }[];
666
+ selectorConfig?: BasicValueSpecificationEditorSelectorConfig | undefined;
667
+ }
632
668
 
633
- const changeValue = (val: { value: Enum; label: string }): void => {
634
- instanceValue_setValue(
635
- valueSpecification,
636
- EnumValueExplicitReference.create(val.value),
637
- 0,
638
- observerContext,
639
- );
640
- setValueSpecification(valueSpecification);
669
+ // eslint-disable-next-line comma-spacing
670
+ const EnumInstanceValueEditorInner = <T,>(
671
+ props: EnumInstanceValueEditorProps<T>,
672
+ ): React.ReactElement => {
673
+ const {
674
+ valueSpecification,
675
+ valueSelector,
676
+ updateValueSpecification,
677
+ errorChecker,
678
+ resetValue,
679
+ handleBlur,
680
+ options,
681
+ className,
682
+ selectorConfig,
683
+ } = props;
684
+ const applicationStore = useApplicationStore();
685
+ const enumValue = valueSelector(valueSpecification);
686
+ const resetButtonName = `reset-${valueSelector(valueSpecification)}`;
687
+ const inputName = `input-${valueSelector(valueSpecification)}`;
688
+
689
+ const changeValue = (val: { value: string; label: string }): void => {
690
+ updateValueSpecification(valueSpecification, val.value);
691
+ handleBlur?.();
692
+ };
693
+
694
+ const onBlur = (
695
+ event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
696
+ ): void => {
697
+ if (
698
+ event.relatedTarget?.name !== resetButtonName &&
699
+ event.relatedTarget?.name !== inputName
700
+ ) {
641
701
  handleBlur?.();
642
- };
702
+ }
703
+ };
643
704
 
644
- const onBlur = (
645
- event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
646
- ): void => {
647
- if (
648
- event.relatedTarget?.name !== resetButtonName &&
649
- event.relatedTarget?.name !== inputName
650
- ) {
651
- handleBlur?.();
652
- }
653
- };
705
+ return (
706
+ <div className={clsx('value-spec-editor', className)} onBlur={onBlur}>
707
+ <CustomSelectorInput
708
+ className="value-spec-editor__enum-selector"
709
+ options={options}
710
+ onChange={changeValue}
711
+ value={enumValue ? { value: enumValue, label: enumValue } : null}
712
+ darkMode={
713
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
714
+ }
715
+ hasError={errorChecker?.(valueSpecification)}
716
+ placeholder="Select value"
717
+ autoFocus={true}
718
+ inputName={inputName}
719
+ optionCustomization={selectorConfig?.optionCustomization}
720
+ />
721
+ <button
722
+ className="value-spec-editor__reset-btn"
723
+ name={resetButtonName}
724
+ title="Reset"
725
+ onClick={resetValue}
726
+ >
727
+ <RefreshIcon />
728
+ </button>
729
+ </div>
730
+ );
731
+ };
654
732
 
655
- return (
656
- <div className={clsx('value-spec-editor', className)} onBlur={onBlur}>
657
- <CustomSelectorInput
658
- className="value-spec-editor__enum-selector"
659
- options={options}
660
- onChange={changeValue}
661
- value={enumValue ? { value: enumValue, label: enumValue.name } : null}
662
- darkMode={
663
- !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
664
- }
665
- hasError={!isValidInstanceValue(valueSpecification)}
666
- placeholder="Select value"
667
- autoFocus={true}
668
- inputName={inputName}
669
- />
670
- <button
671
- className="value-spec-editor__reset-btn"
672
- name={resetButtonName}
673
- title="Reset"
674
- onClick={resetValue}
675
- >
676
- <RefreshIcon />
677
- </button>
678
- </div>
679
- );
680
- },
733
+ export const EnumInstanceValueEditor = observer(
734
+ EnumInstanceValueEditorInner as <T>(
735
+ props: EnumInstanceValueEditorProps<T>,
736
+ ) => ReturnType<typeof EnumInstanceValueEditorInner>,
681
737
  );
682
738
 
683
739
  const stringifyValue = (values: ValueSpecification[]): string => {
@@ -698,7 +754,7 @@ const stringifyValue = (values: ValueSpecification[]): string => {
698
754
  ]).trim();
699
755
  };
700
756
 
701
- const getPlaceHolder = (expectedType: Type): string => {
757
+ const getPlaceHolder = (expectedType: Type | V1_PackageableType): string => {
702
758
  if (expectedType instanceof PrimitiveType) {
703
759
  switch (expectedType.path) {
704
760
  case PRIMITIVE_TYPE.DATE:
@@ -709,674 +765,734 @@ const getPlaceHolder = (expectedType: Type): string => {
709
765
  default:
710
766
  return 'Add';
711
767
  }
768
+ } else if (expectedType instanceof V1_PackageableType) {
769
+ switch (expectedType.fullPath) {
770
+ case PRIMITIVE_TYPE.DATE:
771
+ case PRIMITIVE_TYPE.STRICTDATE:
772
+ return 'yyyy-mm-dd';
773
+ case PRIMITIVE_TYPE.DATETIME:
774
+ case PRIMITIVE_TYPE.STRICTTIME:
775
+ return 'yyyy-mm-ddThh:mm:ss';
776
+ default:
777
+ return 'Add';
778
+ }
779
+ } else {
780
+ throw new Error(`Cannot get placeholder for type ${expectedType}`);
712
781
  }
713
- return 'Add';
714
782
  };
715
783
 
716
- interface BasicValueSpecificationEditorSelectorConfig {
717
- values: string[] | undefined;
718
- isLoading: boolean;
719
- reloadValues:
720
- | DebouncedFunc<(inputValue: string) => GeneratorFn<void>>
784
+ /**
785
+ * This is the base interface for collection primitive instance value editors.
786
+ * The interface is made generic so that it can support various types of objects that hold the value
787
+ * to be edited (currently, we just use this for CollectionInstanceValue and V1_Collection).
788
+ *
789
+ * T represents the type of the objects held in the collection (i.e. ValueSpecification or V1_ValueSpecification).
790
+ * U represents the interface of the collection object (i.e. CollectionInstanceValue or V1_Collection). Currently,
791
+ * this only supports collection objects that hold their data in a property called values.
792
+ *
793
+ * updateValueSpecification: callback that takes the collection object and the new values and handles updating
794
+ * the collection object with the new values.
795
+ * convertTextToValueSpecification: callback that takes a string and converts it to the expected valueSpecification type.
796
+ * convertValueSpecificationToText: callback that takes a valueSpecification and converts it to a string.
797
+ * expectedType: the expected type of the values in the collection.
798
+ * errorChecker: optional callback that should return true if the valueSpecification is invalid.
799
+ */
800
+ interface PrimitiveCollectionInstanceValueEditorProps<
801
+ T,
802
+ U extends { values: T[] },
803
+ > {
804
+ valueSpecification: U;
805
+ updateValueSpecification: (valueSpecification: U, values: T[]) => void;
806
+ convertTextToValueSpecification: (
807
+ type: Type | V1_PackageableType,
808
+ text: string,
809
+ ) => T | null;
810
+ convertValueSpecificationToText: (
811
+ valueSpecification: T,
812
+ ) => string | undefined;
813
+ expectedType: Type | V1_PackageableType;
814
+ saveEdit: () => void;
815
+ selectorSearchConfig?:
816
+ | BasicValueSpecificationEditorSelectorSearchConfig
721
817
  | undefined;
722
- cleanUpReloadValues?: () => void;
818
+ selectorConfig?: BasicValueSpecificationEditorSelectorConfig | undefined;
819
+ errorChecker?: (valueSpecification: U) => boolean;
820
+ className?: string | undefined;
723
821
  }
724
822
 
725
- const PrimitiveCollectionInstanceValueEditor = observer(
726
- (props: {
727
- valueSpecification: CollectionInstanceValue;
728
- expectedType: Type;
729
- saveEdit: () => void;
730
- selectorConfig?: BasicValueSpecificationEditorSelectorConfig | undefined;
731
- observerContext: ObserverContext;
732
- }) => {
733
- const {
734
- valueSpecification,
735
- expectedType,
736
- saveEdit,
737
- selectorConfig,
738
- observerContext,
739
- } = props;
823
+ const PrimitiveCollectionInstanceValueEditorInner = <
824
+ T,
825
+ U extends { values: T[] },
826
+ >(
827
+ props: PrimitiveCollectionInstanceValueEditorProps<T, U>,
828
+ ): React.ReactElement => {
829
+ const {
830
+ valueSpecification,
831
+ convertTextToValueSpecification,
832
+ convertValueSpecificationToText,
833
+ updateValueSpecification,
834
+ saveEdit,
835
+ selectorSearchConfig,
836
+ selectorConfig,
837
+ expectedType,
838
+ } = props;
740
839
 
741
- // local state and variables
742
- const applicationStore = useApplicationStore();
743
- const inputRef = useRef(null);
744
- const [inputValue, setInputValue] = useState('');
745
- const [inputValueIsError, setInputValueIsError] = useState(false);
746
- const [selectedOptions, setSelectedOptions] = useState<
747
- { label: string; value: string }[]
748
- >(
749
- valueSpecification.values
750
- .map((valueSpec) =>
751
- getValueSpecificationStringValue(valueSpec, applicationStore),
752
- )
753
- .filter(isNonEmptyString)
754
- .map((value) => ({
755
- label: value,
756
- value,
757
- })),
758
- );
840
+ // local state and variables
841
+ const applicationStore = useApplicationStore();
842
+ const inputRef = useRef(null);
843
+ const [inputValue, setInputValue] = useState('');
844
+ const [inputValueIsError, setInputValueIsError] = useState(false);
845
+ const [selectedOptions, setSelectedOptions] = useState<
846
+ { label: string; value: string }[]
847
+ >(
848
+ valueSpecification.values
849
+ .filter((value) => guaranteeNonNullable(value))
850
+ .map(convertValueSpecificationToText)
851
+ .filter(isNonEmptyString)
852
+ .map((value) => ({
853
+ label: value,
854
+ value,
855
+ })),
856
+ );
759
857
 
760
- // typehead search setup
761
- const isTypeaheadSearchEnabled =
762
- expectedType === PrimitiveType.STRING && Boolean(selectorConfig);
763
- const reloadValuesFunc = isTypeaheadSearchEnabled
764
- ? selectorConfig?.reloadValues
765
- : undefined;
766
- const cleanUpReloadValuesFunc = isTypeaheadSearchEnabled
767
- ? selectorConfig?.cleanUpReloadValues
768
- : undefined;
769
- const isLoading = isTypeaheadSearchEnabled
770
- ? selectorConfig?.isLoading
858
+ // typehead search setup
859
+ const isTypeaheadSearchEnabled =
860
+ expectedType === PrimitiveType.STRING && Boolean(selectorSearchConfig);
861
+ const reloadValuesFunc = isTypeaheadSearchEnabled
862
+ ? selectorSearchConfig?.reloadValues
863
+ : undefined;
864
+ const cleanUpReloadValuesFunc = isTypeaheadSearchEnabled
865
+ ? selectorSearchConfig?.cleanUpReloadValues
866
+ : undefined;
867
+ const isLoading = isTypeaheadSearchEnabled
868
+ ? selectorSearchConfig?.isLoading
869
+ : undefined;
870
+ const queryOptions =
871
+ isTypeaheadSearchEnabled && selectorSearchConfig?.values?.length
872
+ ? selectorSearchConfig.values.map((e) => ({
873
+ value: e,
874
+ label: e.toString(),
875
+ }))
771
876
  : undefined;
772
- const queryOptions =
773
- isTypeaheadSearchEnabled && selectorConfig?.values?.length
774
- ? selectorConfig.values.map((e) => ({
775
- value: e,
776
- label: e.toString(),
777
- }))
778
- : undefined;
779
- const noMatchMessage =
780
- isTypeaheadSearchEnabled && isLoading ? 'Loading...' : undefined;
781
- const copyButtonName = `copy-${valueSpecification.hashCode}`;
782
- const inputName = `input-${valueSpecification.hashCode}`;
783
-
784
- // helper functions
785
- const buildOptionForValueSpec = (
786
- value: ValueSpecification,
787
- ): { label: string; value: string } => {
788
- const stringValue = guaranteeNonNullable(
789
- getValueSpecificationStringValue(value, applicationStore),
790
- );
791
- return {
792
- label: stringValue,
793
- value: stringValue,
794
- };
877
+ const noMatchMessage =
878
+ isTypeaheadSearchEnabled && isLoading ? 'Loading...' : undefined;
879
+ const copyButtonName = `copy-${valueSpecification.values[0] ? convertValueSpecificationToText(valueSpecification.values[0]) : ''}`;
880
+ const inputName = `input-${valueSpecification.values[0] ? convertValueSpecificationToText(valueSpecification.values[0]) : ''}`;
881
+
882
+ // helper functions
883
+ const buildOptionForValueSpec = (
884
+ value: T,
885
+ ): { label: string; value: string } => {
886
+ const stringValue = guaranteeNonNullable(
887
+ convertValueSpecificationToText(value),
888
+ );
889
+ return {
890
+ label: stringValue,
891
+ value: stringValue,
795
892
  };
893
+ };
796
894
 
797
- const isValueAlreadySelected = (value: string): boolean =>
798
- selectedOptions.map((option) => option.value).includes(value);
799
-
800
- /**
801
- * NOTE: We attempt to be less disruptive here by not throwing errors left and right, instead
802
- * we simply return null for values which are not valid or parsable. But perhaps, we can consider
803
- * passing in logger or notifier to give the users some idea of what went wrong instead of ignoring
804
- * their input.
805
- */
806
- const convertInputValueToValueSpec = (): ValueSpecification | null => {
807
- const trimmedInputValue = inputValue.trim();
808
-
809
- if (trimmedInputValue.length) {
810
- const newValueSpec = convertTextToPrimitiveInstanceValue(
811
- expectedType,
812
- trimmedInputValue,
813
- observerContext,
814
- );
895
+ const isValueAlreadySelected = (value: string): boolean =>
896
+ selectedOptions.map((option) => option.value).includes(value);
815
897
 
816
- if (
817
- newValueSpec === null ||
818
- getValueSpecificationStringValue(newValueSpec, applicationStore) ===
819
- undefined ||
820
- isValueAlreadySelected(
821
- guaranteeNonNullable(
822
- getValueSpecificationStringValue(newValueSpec, applicationStore),
823
- ),
824
- )
825
- ) {
826
- return null;
827
- }
828
-
829
- return newValueSpec;
830
- }
831
- return null;
832
- };
898
+ /**
899
+ * NOTE: We attempt to be less disruptive here by not throwing errors left and right, instead
900
+ * we simply return null for values which are not valid or parsable. But perhaps, we can consider
901
+ * passing in logger or notifier to give the users some idea of what went wrong instead of ignoring
902
+ * their input.
903
+ */
904
+ const convertInputValueToValueSpec = (): T | null => {
905
+ const trimmedInputValue = inputValue.trim();
833
906
 
834
- const addInputValueToSelectedOptions = (): void => {
835
- const newValueSpec = convertInputValueToValueSpec();
836
-
837
- if (newValueSpec !== null) {
838
- setSelectedOptions([
839
- ...selectedOptions,
840
- buildOptionForValueSpec(newValueSpec),
841
- ]);
842
- setInputValue('');
843
- reloadValuesFunc?.cancel();
844
- } else if (inputValue.trim().length) {
845
- setInputValueIsError(true);
846
- }
847
- };
907
+ if (trimmedInputValue.length) {
908
+ const newValueSpec = convertTextToValueSpecification(
909
+ expectedType,
910
+ trimmedInputValue,
911
+ );
848
912
 
849
- // event handlers
850
- const changeValue = (
851
- newSelectedOptions: { value: string; label: string }[],
852
- actionChange: SelectActionData<{ value: string; label: string }>,
853
- ): void => {
854
- setSelectedOptions(newSelectedOptions);
855
- if (actionChange.action === 'select-option') {
856
- setInputValue('');
857
- } else if (
858
- actionChange.action === 'remove-value' &&
859
- actionChange.removedValue.value === inputValue
913
+ if (
914
+ newValueSpec === null ||
915
+ convertValueSpecificationToText(newValueSpec) === undefined ||
916
+ isValueAlreadySelected(
917
+ guaranteeNonNullable(convertValueSpecificationToText(newValueSpec)),
918
+ )
860
919
  ) {
861
- setInputValueIsError(false);
920
+ return null;
862
921
  }
863
- };
864
922
 
865
- const handleInputChange = (
866
- newInputValue: string,
867
- actionChange: InputActionData,
868
- ): void => {
869
- if (actionChange.action === 'input-change') {
870
- setInputValue(newInputValue);
871
- setInputValueIsError(false);
872
- reloadValuesFunc?.cancel();
873
- const reloadValuesFuncTransformation =
874
- reloadValuesFunc?.(newInputValue);
875
- if (reloadValuesFuncTransformation) {
876
- flowResult(reloadValuesFuncTransformation).catch(
877
- applicationStore.alertUnhandledError,
878
- );
879
- }
880
- }
881
- if (actionChange.action === 'input-blur') {
882
- reloadValuesFunc?.cancel();
883
- cleanUpReloadValuesFunc?.();
884
- }
885
- };
886
-
887
- const copyValueToClipboard = async () =>
888
- navigator.clipboard.writeText(
889
- selectedOptions.map((option) => option.value).join(','),
890
- );
891
-
892
- const updateValueSpecAndSaveEdit = (): void => {
893
- const newValueSpec = convertInputValueToValueSpec();
894
- const finalSelectedOptions =
895
- newValueSpec !== null
896
- ? [...selectedOptions, buildOptionForValueSpec(newValueSpec)]
897
- : selectedOptions;
898
- instanceValue_setValues(
899
- valueSpecification,
900
- finalSelectedOptions
901
- .map((option) => option.value)
902
- .map((value) =>
903
- convertTextToPrimitiveInstanceValue(
904
- expectedType,
905
- value,
906
- observerContext,
907
- ),
908
- )
909
- .filter(isNonNullable),
910
- observerContext,
911
- );
912
- saveEdit();
913
- };
923
+ return newValueSpec;
924
+ }
925
+ return null;
926
+ };
914
927
 
915
- const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (
916
- event,
917
- ) => {
918
- if ((event.key === 'Enter' || event.key === ',') && !event.shiftKey) {
919
- addInputValueToSelectedOptions();
920
- event.preventDefault();
921
- }
922
- };
928
+ const addInputValueToSelectedOptions = (): void => {
929
+ const newValueSpec = convertInputValueToValueSpec();
923
930
 
924
- const handlePaste: React.ClipboardEventHandler<HTMLInputElement> = (
925
- event,
926
- ) => {
927
- const pastedText = event.clipboardData.getData('text');
928
- const parsedData = parseCSVString(pastedText);
929
- if (!parsedData) {
930
- return;
931
- }
932
- const newValues = uniq(
933
- uniq(parsedData)
934
- .map((value) => {
935
- const newValueSpec = convertTextToPrimitiveInstanceValue(
936
- expectedType,
937
- value,
938
- observerContext,
939
- );
940
- return newValueSpec
941
- ? getValueSpecificationStringValue(newValueSpec, applicationStore)
942
- : null;
943
- })
944
- .filter(isNonNullable),
945
- ).filter((value) => !isValueAlreadySelected(value));
931
+ if (newValueSpec !== null) {
946
932
  setSelectedOptions([
947
933
  ...selectedOptions,
948
- ...newValues.map((value) => ({ label: value, value })),
934
+ buildOptionForValueSpec(newValueSpec),
949
935
  ]);
950
- event.preventDefault();
951
- };
952
-
953
- const onBlur = (
954
- event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
955
- ): void => {
956
- if (
957
- event.relatedTarget?.name !== copyButtonName &&
958
- event.relatedTarget?.name !== inputName
959
- ) {
960
- updateValueSpecAndSaveEdit();
936
+ setInputValue('');
937
+ reloadValuesFunc?.cancel();
938
+ } else if (inputValue.trim().length) {
939
+ setInputValueIsError(true);
940
+ }
941
+ };
942
+
943
+ // event handlers
944
+ const changeValue = (
945
+ newSelectedOptions: { value: string; label: string }[],
946
+ actionChange: SelectActionData<{ value: string; label: string }>,
947
+ ): void => {
948
+ setSelectedOptions(newSelectedOptions);
949
+ if (actionChange.action === 'select-option') {
950
+ setInputValue('');
951
+ } else if (
952
+ actionChange.action === 'remove-value' &&
953
+ actionChange.removedValue.value === inputValue
954
+ ) {
955
+ setInputValueIsError(false);
956
+ }
957
+ };
958
+
959
+ const handleInputChange = (
960
+ newInputValue: string,
961
+ actionChange: InputActionData,
962
+ ): void => {
963
+ if (actionChange.action === 'input-change') {
964
+ setInputValue(newInputValue);
965
+ setInputValueIsError(false);
966
+ reloadValuesFunc?.cancel();
967
+ const reloadValuesFuncTransformation = reloadValuesFunc?.(newInputValue);
968
+ if (reloadValuesFuncTransformation) {
969
+ flowResult(reloadValuesFuncTransformation).catch(
970
+ applicationStore.alertUnhandledError,
971
+ );
961
972
  }
962
- };
973
+ }
974
+ if (actionChange.action === 'input-blur') {
975
+ reloadValuesFunc?.cancel();
976
+ cleanUpReloadValuesFunc?.();
977
+ }
978
+ };
963
979
 
964
- return (
965
- <div className="value-spec-editor" onBlur={onBlur}>
966
- <CustomSelectorInput
967
- className={clsx('value-spec-editor__primitive-collection-selector', {
968
- 'value-spec-editor__primitive-collection-selector--error':
969
- inputValueIsError,
970
- })}
971
- options={queryOptions}
972
- inputValue={inputValue}
973
- isMulti={true}
974
- menuIsOpen={
975
- isTypeaheadSearchEnabled &&
976
- inputValue.length >= DEFAULT_TYPEAHEAD_SEARCH_MINIMUM_SEARCH_LENGTH
977
- }
978
- autoFocus={true}
979
- inputRef={inputRef}
980
- onChange={changeValue}
981
- onInputChange={handleInputChange}
982
- onKeyDown={handleKeyDown}
983
- onPaste={handlePaste}
984
- value={selectedOptions}
985
- darkMode={
986
- !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
987
- }
988
- isLoading={isLoading}
989
- noMatchMessage={noMatchMessage}
990
- placeholder={getPlaceHolder(expectedType)}
991
- components={{
992
- DropdownIndicator: null,
993
- }}
994
- inputName={inputName}
995
- />
996
- <button
997
- className="value-spec-editor__list-editor__copy-button"
998
- // eslint-disable-next-line no-void
999
- onClick={() => void copyValueToClipboard()}
1000
- name={copyButtonName}
1001
- title="Copy values to clipboard"
1002
- >
1003
- <CopyIcon />
1004
- </button>
1005
- <button
1006
- className="value-spec-editor__list-editor__save-button btn--dark"
1007
- name="Save"
1008
- title="Save"
1009
- onClick={updateValueSpecAndSaveEdit}
1010
- >
1011
- <SaveIcon />
1012
- </button>
1013
- </div>
980
+ const copyValueToClipboard = async () =>
981
+ navigator.clipboard.writeText(
982
+ selectedOptions.map((option) => option.value).join(','),
1014
983
  );
1015
- },
984
+
985
+ const updateValueSpecAndSaveEdit = (): void => {
986
+ const newValueSpec = convertInputValueToValueSpec();
987
+ const finalSelectedOptions =
988
+ newValueSpec !== null
989
+ ? [...selectedOptions, buildOptionForValueSpec(newValueSpec)]
990
+ : selectedOptions;
991
+ const finalFormattedSelectedOptions = finalSelectedOptions
992
+ .map((option) => option.value)
993
+ .map((value) => convertTextToValueSpecification(expectedType, value))
994
+ .filter(isNonNullable);
995
+ updateValueSpecification(valueSpecification, finalFormattedSelectedOptions);
996
+ saveEdit();
997
+ };
998
+
999
+ const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => {
1000
+ if ((event.key === 'Enter' || event.key === ',') && !event.shiftKey) {
1001
+ addInputValueToSelectedOptions();
1002
+ event.preventDefault();
1003
+ }
1004
+ };
1005
+
1006
+ const handlePaste: React.ClipboardEventHandler<HTMLInputElement> = (
1007
+ event,
1008
+ ) => {
1009
+ const pastedText = event.clipboardData.getData('text');
1010
+ const parsedData = parseCSVString(pastedText);
1011
+ if (!parsedData) {
1012
+ return;
1013
+ }
1014
+ const newValues = uniq(
1015
+ uniq(parsedData)
1016
+ .map((value) => {
1017
+ const newValueSpec = convertTextToValueSpecification(
1018
+ expectedType,
1019
+ value,
1020
+ );
1021
+ return newValueSpec
1022
+ ? convertValueSpecificationToText(newValueSpec)
1023
+ : null;
1024
+ })
1025
+ .filter(isNonNullable),
1026
+ ).filter((value) => !isValueAlreadySelected(value));
1027
+ setSelectedOptions([
1028
+ ...selectedOptions,
1029
+ ...newValues.map((value) => ({ label: value, value })),
1030
+ ]);
1031
+ event.preventDefault();
1032
+ };
1033
+
1034
+ const onBlur = (
1035
+ event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
1036
+ ): void => {
1037
+ if (
1038
+ event.relatedTarget?.name !== copyButtonName &&
1039
+ event.relatedTarget?.name !== inputName
1040
+ ) {
1041
+ updateValueSpecAndSaveEdit();
1042
+ }
1043
+ };
1044
+
1045
+ return (
1046
+ <div className="value-spec-editor" onBlur={onBlur}>
1047
+ <CustomSelectorInput
1048
+ className={clsx('value-spec-editor__primitive-collection-selector', {
1049
+ 'value-spec-editor__primitive-collection-selector--error':
1050
+ inputValueIsError,
1051
+ })}
1052
+ options={queryOptions}
1053
+ inputValue={inputValue}
1054
+ isMulti={true}
1055
+ menuIsOpen={
1056
+ isTypeaheadSearchEnabled &&
1057
+ inputValue.length >= DEFAULT_TYPEAHEAD_SEARCH_MINIMUM_SEARCH_LENGTH
1058
+ }
1059
+ autoFocus={true}
1060
+ inputRef={inputRef}
1061
+ onChange={changeValue}
1062
+ onInputChange={handleInputChange}
1063
+ onKeyDown={handleKeyDown}
1064
+ onPaste={handlePaste}
1065
+ value={selectedOptions}
1066
+ darkMode={
1067
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
1068
+ }
1069
+ isLoading={isLoading}
1070
+ noMatchMessage={noMatchMessage}
1071
+ placeholder={getPlaceHolder(expectedType)}
1072
+ components={{
1073
+ DropdownIndicator: null,
1074
+ }}
1075
+ inputName={inputName}
1076
+ optionCustomization={selectorConfig?.optionCustomization}
1077
+ />
1078
+ <button
1079
+ className="value-spec-editor__list-editor__copy-button"
1080
+ // eslint-disable-next-line no-void
1081
+ onClick={() => void copyValueToClipboard()}
1082
+ name={copyButtonName}
1083
+ title="Copy values to clipboard"
1084
+ >
1085
+ <CopyIcon />
1086
+ </button>
1087
+ <button
1088
+ className="value-spec-editor__list-editor__save-button btn--dark"
1089
+ name="Save"
1090
+ title="Save"
1091
+ onClick={updateValueSpecAndSaveEdit}
1092
+ >
1093
+ <SaveIcon />
1094
+ </button>
1095
+ </div>
1096
+ );
1097
+ };
1098
+
1099
+ export const PrimitiveCollectionInstanceValueEditor = observer(
1100
+ PrimitiveCollectionInstanceValueEditorInner as <T, U extends { values: T[] }>(
1101
+ props: PrimitiveCollectionInstanceValueEditorProps<T, U>,
1102
+ ) => ReturnType<typeof PrimitiveCollectionInstanceValueEditorInner>,
1016
1103
  );
1017
1104
 
1018
- const EnumCollectionInstanceValueEditor = observer(
1019
- (props: {
1020
- valueSpecification: CollectionInstanceValue;
1021
- observerContext: ObserverContext;
1022
- saveEdit: () => void;
1023
- }) => {
1024
- const { valueSpecification, observerContext, saveEdit } = props;
1105
+ interface EnumCollectionInstanceValueEditorProps<T, U extends { values: T[] }>
1106
+ extends PrimitiveCollectionInstanceValueEditorProps<T, U> {
1107
+ enumOptions: { label: string; value: string }[] | undefined;
1108
+ }
1025
1109
 
1026
- // local state and variables
1027
- const applicationStore = useApplicationStore();
1028
- const enumType = guaranteeType(
1029
- valueSpecification.genericType?.value.rawType,
1030
- Enumeration,
1031
- );
1032
- const [inputValue, setInputValue] = useState('');
1033
- const [inputValueIsError, setInputValueIsError] = useState(false);
1034
- const [selectedOptions, setSelectedOptions] = useState<
1035
- { label: string; value: Enum }[]
1036
- >(
1037
- (valueSpecification.values as EnumValueInstanceValue[])
1038
- .filter((valueSpec) => valueSpec.values[0]?.value !== undefined)
1039
- .map((valueSpec) => ({
1040
- label: at(valueSpec.values, 0).value.name,
1041
- value: at(valueSpec.values, 0).value,
1042
- })),
1043
- );
1110
+ const EnumCollectionInstanceValueEditorInner = <T, U extends { values: T[] }>(
1111
+ props: EnumCollectionInstanceValueEditorProps<T, U>,
1112
+ ): React.ReactElement => {
1113
+ const {
1114
+ valueSpecification,
1115
+ convertTextToValueSpecification,
1116
+ convertValueSpecificationToText,
1117
+ updateValueSpecification,
1118
+ saveEdit,
1119
+ expectedType,
1120
+ enumOptions,
1121
+ selectorConfig,
1122
+ } = props;
1123
+
1124
+ guaranteeNonNullable(
1125
+ enumOptions,
1126
+ 'Must pass enum options to EnumCollectionInstanceValueEditor',
1127
+ );
1044
1128
 
1045
- const availableOptions = enumType.values
1046
- .filter(
1047
- (value) =>
1048
- !selectedOptions.some(
1049
- (selectedValue) => selectedValue.value.name === value.name,
1050
- ),
1051
- )
1129
+ // local state and variables
1130
+ const applicationStore = useApplicationStore();
1131
+ const [inputValue, setInputValue] = useState('');
1132
+ const [inputValueIsError, setInputValueIsError] = useState(false);
1133
+ const [selectedOptions, setSelectedOptions] = useState<
1134
+ { label: string; value: string }[]
1135
+ >(
1136
+ valueSpecification.values
1137
+ .filter((value) => guaranteeNonNullable(value))
1138
+ .map(convertValueSpecificationToText)
1139
+ .filter(isNonEmptyString)
1052
1140
  .map((value) => ({
1053
- label: value.name,
1054
- value: value,
1055
- }));
1056
-
1057
- const copyButtonName = `copy-${valueSpecification.hashCode}`;
1058
- const inputName = `input-${valueSpecification.hashCode}`;
1059
-
1060
- // helper functions
1061
- const isValueAlreadySelected = (value: Enum): boolean =>
1062
- selectedOptions.map((option) => option.value).includes(value);
1063
-
1064
- /**
1065
- * NOTE: We attempt to be less disruptive here by not throwing errors left and right, instead
1066
- * we simply return null for values which are not valid or parsable. But perhaps, we can consider
1067
- * passing in logger or notifier to give the users some idea of what went wrong instead of ignoring
1068
- * their input.
1069
- */
1070
- const convertInputValueToEnum = (): Enum | null => {
1071
- const trimmedInputValue = inputValue.trim();
1072
-
1073
- if (trimmedInputValue.length) {
1074
- const newEnum = convertTextToEnum(trimmedInputValue, enumType);
1075
-
1076
- if (newEnum === undefined || isValueAlreadySelected(newEnum)) {
1077
- return null;
1078
- }
1141
+ label: value,
1142
+ value,
1143
+ })),
1144
+ );
1079
1145
 
1080
- return newEnum;
1081
- }
1082
- return null;
1083
- };
1146
+ const availableOptions = enumOptions?.filter(
1147
+ (value) =>
1148
+ !selectedOptions.some(
1149
+ (selectedValue) => selectedValue.value === value.value,
1150
+ ),
1151
+ );
1084
1152
 
1085
- const addInputValueToSelectedOptions = (): void => {
1086
- const newEnum = convertInputValueToEnum();
1087
-
1088
- if (newEnum !== null) {
1089
- setSelectedOptions([
1090
- ...selectedOptions,
1091
- {
1092
- label: newEnum.name,
1093
- value: newEnum,
1094
- },
1095
- ]);
1096
- setInputValue('');
1097
- } else if (inputValue.trim().length) {
1098
- setInputValueIsError(true);
1099
- }
1100
- };
1153
+ const copyButtonName = `copy-${valueSpecification.values[0] ? convertValueSpecificationToText(valueSpecification.values[0]) : ''}`;
1154
+ const inputName = `input-${valueSpecification.values[0] ? convertValueSpecificationToText(valueSpecification.values[0]) : ''}`;
1101
1155
 
1102
- // event handlers
1103
- const changeValue = (
1104
- newSelectedOptions: { value: Enum; label: string }[],
1105
- actionChange: SelectActionData<{ value: Enum; label: string }>,
1106
- ): void => {
1107
- setSelectedOptions(newSelectedOptions);
1108
- if (actionChange.action === 'select-option') {
1109
- setInputValue('');
1110
- } else if (
1111
- actionChange.action === 'remove-value' &&
1112
- actionChange.removedValue.value.name === inputValue
1113
- ) {
1114
- setInputValueIsError(false);
1115
- }
1116
- };
1156
+ // helper functions
1157
+ const isValueAlreadySelected = (value: string): boolean =>
1158
+ selectedOptions.map((option) => option.value).includes(value);
1117
1159
 
1118
- const handleInputChange = (
1119
- newInputValue: string,
1120
- actionChange: InputActionData,
1121
- ): void => {
1122
- if (actionChange.action === 'input-change') {
1123
- setInputValue(newInputValue);
1124
- setInputValueIsError(false);
1125
- }
1126
- };
1160
+ /**
1161
+ * NOTE: We attempt to be less disruptive here by not throwing errors left and right, instead
1162
+ * we simply return null for values which are not valid or parsable. But perhaps, we can consider
1163
+ * passing in logger or notifier to give the users some idea of what went wrong instead of ignoring
1164
+ * their input.
1165
+ */
1166
+ const convertInputValueToEnum = (): string | null => {
1167
+ const trimmedInputValue = inputValue.trim();
1127
1168
 
1128
- const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (
1129
- event,
1130
- ) => {
1131
- if ((event.key === 'Enter' || event.key === ',') && !event.shiftKey) {
1132
- addInputValueToSelectedOptions();
1133
- event.preventDefault();
1134
- }
1135
- };
1169
+ if (
1170
+ !trimmedInputValue.length ||
1171
+ isValueAlreadySelected(trimmedInputValue) ||
1172
+ !enumOptions?.some((option) => option.value === trimmedInputValue)
1173
+ ) {
1174
+ return null;
1175
+ }
1136
1176
 
1137
- const handlePaste: React.ClipboardEventHandler<HTMLInputElement> = (
1138
- event,
1139
- ) => {
1140
- const pastedText = event.clipboardData.getData('text');
1141
- const parsedData = parseCSVString(pastedText);
1142
- if (!parsedData) {
1143
- return;
1144
- }
1145
- const newValues = uniq(
1146
- uniq(parsedData)
1147
- .map((value) => convertTextToEnum(value, enumType))
1148
- .filter(isNonNullable),
1149
- ).filter((value) => !isValueAlreadySelected(value));
1177
+ return trimmedInputValue;
1178
+ };
1179
+
1180
+ const addInputValueToSelectedOptions = (): void => {
1181
+ const newEnum = convertInputValueToEnum();
1182
+
1183
+ if (newEnum !== null) {
1150
1184
  setSelectedOptions([
1151
1185
  ...selectedOptions,
1152
- ...newValues.map((value) => ({ label: value.name, value })),
1186
+ {
1187
+ label: newEnum,
1188
+ value: newEnum,
1189
+ },
1153
1190
  ]);
1154
- event.preventDefault();
1155
- };
1191
+ setInputValue('');
1192
+ } else if (inputValue.trim().length) {
1193
+ setInputValueIsError(true);
1194
+ }
1195
+ };
1196
+
1197
+ // event handlers
1198
+ const changeValue = (
1199
+ newSelectedOptions: { value: string; label: string }[],
1200
+ actionChange: SelectActionData<{ value: string; label: string }>,
1201
+ ): void => {
1202
+ setSelectedOptions(newSelectedOptions);
1203
+ if (actionChange.action === 'select-option') {
1204
+ setInputValue('');
1205
+ } else if (
1206
+ actionChange.action === 'remove-value' &&
1207
+ actionChange.removedValue.value === inputValue
1208
+ ) {
1209
+ setInputValueIsError(false);
1210
+ }
1211
+ };
1212
+
1213
+ const handleInputChange = (
1214
+ newInputValue: string,
1215
+ actionChange: InputActionData,
1216
+ ): void => {
1217
+ if (actionChange.action === 'input-change') {
1218
+ setInputValue(newInputValue);
1219
+ setInputValueIsError(false);
1220
+ }
1221
+ };
1156
1222
 
1157
- const copyValueToClipboard = async () =>
1158
- navigator.clipboard.writeText(
1159
- selectedOptions.map((option) => option.value.name).join(','),
1160
- );
1223
+ const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => {
1224
+ if ((event.key === 'Enter' || event.key === ',') && !event.shiftKey) {
1225
+ addInputValueToSelectedOptions();
1226
+ event.preventDefault();
1227
+ }
1228
+ };
1229
+
1230
+ const handlePaste: React.ClipboardEventHandler<HTMLInputElement> = (
1231
+ event,
1232
+ ) => {
1233
+ const pastedText = event.clipboardData.getData('text');
1234
+ const parsedData = parseCSVString(pastedText);
1235
+ if (!parsedData) {
1236
+ return;
1237
+ }
1238
+ const newValues = uniq(
1239
+ uniq(parsedData).filter((value) =>
1240
+ enumOptions?.some((option) => option.value === value),
1241
+ ),
1242
+ ).filter((value) => !isValueAlreadySelected(value));
1243
+ setSelectedOptions([
1244
+ ...selectedOptions,
1245
+ ...newValues.map((value) => ({ label: value, value })),
1246
+ ]);
1247
+ event.preventDefault();
1248
+ };
1249
+
1250
+ const copyValueToClipboard = async () =>
1251
+ navigator.clipboard.writeText(
1252
+ selectedOptions.map((option) => option.value).join(','),
1253
+ );
1161
1254
 
1162
- const updateValueSpecAndSaveEdit = (): void => {
1163
- const result = selectedOptions
1164
- .map((value) => {
1165
- const enumValueInstanceValue = new EnumValueInstanceValue(
1166
- GenericTypeExplicitReference.create(new GenericType(enumType)),
1167
- );
1168
- instanceValue_setValues(
1169
- enumValueInstanceValue,
1170
- [EnumValueExplicitReference.create(value.value)],
1171
- observerContext,
1172
- );
1173
- return enumValueInstanceValue;
1174
- })
1175
- .filter(isNonNullable);
1176
- instanceValue_setValues(valueSpecification, result, observerContext);
1177
- saveEdit();
1178
- };
1255
+ const updateValueSpecAndSaveEdit = (): void => {
1256
+ const result = selectedOptions
1257
+ .map((option) => option.value)
1258
+ .map((value) => convertTextToValueSpecification(expectedType, value))
1259
+ .filter(isNonNullable);
1260
+ updateValueSpecification(valueSpecification, result);
1261
+ saveEdit();
1262
+ };
1263
+
1264
+ const onBlur = (
1265
+ event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
1266
+ ): void => {
1267
+ if (
1268
+ event.relatedTarget?.name !== copyButtonName &&
1269
+ event.relatedTarget?.name !== inputName
1270
+ ) {
1271
+ updateValueSpecAndSaveEdit();
1272
+ }
1273
+ };
1179
1274
 
1180
- const onBlur = (
1181
- event: React.FocusEvent<HTMLInputElement, HTMLButtonElement>,
1182
- ): void => {
1183
- if (
1184
- event.relatedTarget?.name !== copyButtonName &&
1185
- event.relatedTarget?.name !== inputName
1186
- ) {
1187
- updateValueSpecAndSaveEdit();
1188
- }
1189
- };
1275
+ return (
1276
+ <div className="value-spec-editor" onBlur={onBlur}>
1277
+ <CustomSelectorInput
1278
+ className={clsx('value-spec-editor__enum-collection-selector', {
1279
+ 'value-spec-editor__enum-collection-selector--error':
1280
+ inputValueIsError,
1281
+ })}
1282
+ options={availableOptions}
1283
+ inputValue={inputValue}
1284
+ isMulti={true}
1285
+ autoFocus={true}
1286
+ onChange={changeValue}
1287
+ onInputChange={handleInputChange}
1288
+ onKeyDown={handleKeyDown}
1289
+ onPaste={handlePaste}
1290
+ value={selectedOptions}
1291
+ darkMode={
1292
+ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
1293
+ }
1294
+ placeholder="Add"
1295
+ menuIsOpen={true}
1296
+ inputName={inputName}
1297
+ optionCustomization={selectorConfig?.optionCustomization}
1298
+ />
1299
+ <button
1300
+ className="value-spec-editor__list-editor__copy-button"
1301
+ // eslint-disable-next-line no-void
1302
+ onClick={() => void copyValueToClipboard()}
1303
+ name={copyButtonName}
1304
+ title="Copy values to clipboard"
1305
+ >
1306
+ <CopyIcon />
1307
+ </button>
1308
+ <button
1309
+ className="value-spec-editor__list-editor__save-button btn--dark"
1310
+ name="Save"
1311
+ title="Save"
1312
+ onClick={updateValueSpecAndSaveEdit}
1313
+ >
1314
+ <SaveIcon />
1315
+ </button>
1316
+ </div>
1317
+ );
1318
+ };
1190
1319
 
1191
- return (
1192
- <div className="value-spec-editor" onBlur={onBlur}>
1193
- <CustomSelectorInput
1194
- className={clsx('value-spec-editor__enum-collection-selector', {
1195
- 'value-spec-editor__enum-collection-selector--error':
1196
- inputValueIsError,
1197
- })}
1198
- options={availableOptions}
1199
- inputValue={inputValue}
1200
- isMulti={true}
1201
- autoFocus={true}
1202
- onChange={changeValue}
1203
- onInputChange={handleInputChange}
1204
- onKeyDown={handleKeyDown}
1205
- onPaste={handlePaste}
1206
- value={selectedOptions}
1207
- darkMode={
1208
- !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled
1209
- }
1210
- placeholder="Add"
1211
- menuIsOpen={true}
1212
- inputName={inputName}
1213
- />
1214
- <button
1215
- className="value-spec-editor__list-editor__copy-button"
1216
- // eslint-disable-next-line no-void
1217
- onClick={() => void copyValueToClipboard()}
1218
- name={copyButtonName}
1219
- title="Copy values to clipboard"
1220
- >
1221
- <CopyIcon />
1222
- </button>
1223
- <button
1224
- className="value-spec-editor__list-editor__save-button btn--dark"
1225
- name="Save"
1226
- title="Save"
1227
- onClick={updateValueSpecAndSaveEdit}
1228
- >
1229
- <SaveIcon />
1230
- </button>
1231
- </div>
1232
- );
1233
- },
1320
+ export const EnumCollectionInstanceValueEditor = observer(
1321
+ EnumCollectionInstanceValueEditorInner as <T, U extends { values: T[] }>(
1322
+ props: EnumCollectionInstanceValueEditorProps<T, U>,
1323
+ ) => ReturnType<typeof EnumCollectionInstanceValueEditorInner>,
1234
1324
  );
1235
1325
 
1236
1326
  const COLLECTION_PREVIEW_CHAR_LIMIT = 50;
1237
1327
 
1238
- const CollectionValueInstanceValueEditor = observer(
1239
- (props: {
1240
- valueSpecification: CollectionInstanceValue;
1241
- graph: PureModel;
1242
- expectedType: Type;
1243
- className?: string | undefined;
1244
- setValueSpecification: (val: ValueSpecification) => void;
1245
- selectorConfig?: BasicValueSpecificationEditorSelectorConfig | undefined;
1246
- observerContext: ObserverContext;
1247
- }) => {
1248
- const {
1249
- valueSpecification,
1250
- expectedType,
1251
- className,
1252
- setValueSpecification,
1253
- selectorConfig,
1254
- observerContext,
1255
- } = props;
1328
+ interface CollectionValueInstanceValueEditorProps<T, U extends { values: T[] }>
1329
+ extends Omit<PrimitiveCollectionInstanceValueEditorProps<T, U>, 'saveEdit'>,
1330
+ Omit<EnumCollectionInstanceValueEditorProps<T, U>, 'saveEdit'> {
1331
+ stringifyCollectionValueSpecification: (valueSpecification: U) => string;
1332
+ }
1256
1333
 
1257
- const [editable, setEditable] = useState(false);
1258
- const valueText = stringifyValue(valueSpecification.values);
1259
- const previewText = `List(${
1260
- valueSpecification.values.length === 0
1261
- ? 'empty'
1262
- : valueSpecification.values.length
1263
- })${
1264
- valueSpecification.values.length === 0
1265
- ? ''
1266
- : `: ${
1267
- valueText.length > COLLECTION_PREVIEW_CHAR_LIMIT
1268
- ? `${valueText.substring(0, COLLECTION_PREVIEW_CHAR_LIMIT)}...`
1269
- : valueText
1270
- }`
1271
- }`;
1272
- const enableEdit = (): void => setEditable(true);
1273
- const saveEdit = (): void => {
1274
- if (editable) {
1275
- setEditable(false);
1276
- setValueSpecification(valueSpecification);
1277
- }
1278
- };
1334
+ const CollectionValueInstanceValueEditorInner = <T, U extends { values: T[] }>(
1335
+ props: CollectionValueInstanceValueEditorProps<T, U>,
1336
+ ): React.ReactElement => {
1337
+ const {
1338
+ valueSpecification,
1339
+ convertTextToValueSpecification,
1340
+ convertValueSpecificationToText,
1341
+ updateValueSpecification,
1342
+ stringifyCollectionValueSpecification,
1343
+ errorChecker,
1344
+ className,
1345
+ selectorSearchConfig,
1346
+ selectorConfig,
1347
+ expectedType,
1348
+ enumOptions,
1349
+ } = props;
1279
1350
 
1351
+ const [editable, setEditable] = useState(false);
1352
+ const valueText = stringifyCollectionValueSpecification(valueSpecification);
1353
+ const previewText = `List(${
1354
+ valueSpecification.values.length === 0
1355
+ ? 'empty'
1356
+ : valueSpecification.values.length
1357
+ })${
1358
+ valueSpecification.values.length === 0
1359
+ ? ''
1360
+ : `: ${
1361
+ valueText.length > COLLECTION_PREVIEW_CHAR_LIMIT
1362
+ ? `${valueText.substring(0, COLLECTION_PREVIEW_CHAR_LIMIT)}...`
1363
+ : valueText
1364
+ }`
1365
+ }`;
1366
+ const enableEdit = (): void => setEditable(true);
1367
+ const saveEdit = (): void => {
1280
1368
  if (editable) {
1281
- return (
1282
- <>
1283
- <div className={clsx('value-spec-editor', className)}>
1284
- {expectedType instanceof Enumeration ? (
1285
- <EnumCollectionInstanceValueEditor
1286
- valueSpecification={valueSpecification}
1287
- observerContext={observerContext}
1288
- saveEdit={saveEdit}
1289
- />
1290
- ) : (
1291
- <PrimitiveCollectionInstanceValueEditor
1292
- valueSpecification={valueSpecification}
1293
- expectedType={expectedType}
1294
- saveEdit={saveEdit}
1295
- selectorConfig={selectorConfig}
1296
- observerContext={observerContext}
1297
- />
1298
- )}
1299
- </div>
1300
- </>
1301
- );
1369
+ setEditable(false);
1302
1370
  }
1371
+ };
1372
+
1373
+ if (editable) {
1303
1374
  return (
1375
+ <>
1376
+ <div className={clsx('value-spec-editor', className)}>
1377
+ {enumOptions !== undefined ? (
1378
+ <EnumCollectionInstanceValueEditor<T, U>
1379
+ valueSpecification={valueSpecification}
1380
+ updateValueSpecification={updateValueSpecification}
1381
+ convertTextToValueSpecification={convertTextToValueSpecification}
1382
+ convertValueSpecificationToText={convertValueSpecificationToText}
1383
+ expectedType={expectedType}
1384
+ saveEdit={saveEdit}
1385
+ enumOptions={enumOptions}
1386
+ selectorConfig={selectorConfig}
1387
+ />
1388
+ ) : (
1389
+ <PrimitiveCollectionInstanceValueEditor<T, U>
1390
+ valueSpecification={valueSpecification}
1391
+ updateValueSpecification={updateValueSpecification}
1392
+ convertTextToValueSpecification={convertTextToValueSpecification}
1393
+ convertValueSpecificationToText={convertValueSpecificationToText}
1394
+ expectedType={expectedType}
1395
+ saveEdit={saveEdit}
1396
+ selectorSearchConfig={selectorSearchConfig}
1397
+ selectorConfig={selectorConfig}
1398
+ />
1399
+ )}
1400
+ </div>
1401
+ </>
1402
+ );
1403
+ }
1404
+ return (
1405
+ <div
1406
+ className={clsx('value-spec-editor', className)}
1407
+ onClick={enableEdit}
1408
+ title="Click to edit"
1409
+ >
1304
1410
  <div
1305
- className={clsx('value-spec-editor', className)}
1306
- onClick={enableEdit}
1307
- title="Click to edit"
1411
+ className={clsx('value-spec-editor__list-editor__preview', {
1412
+ 'value-spec-editor__list-editor__preview--error':
1413
+ errorChecker?.(valueSpecification),
1414
+ })}
1308
1415
  >
1309
- <div
1310
- className={clsx('value-spec-editor__list-editor__preview', {
1311
- 'value-spec-editor__list-editor__preview--error':
1312
- !isValidInstanceValue(valueSpecification),
1313
- })}
1314
- >
1315
- {previewText}
1316
- </div>
1317
- <button className="value-spec-editor__list-editor__edit-icon">
1318
- <PencilIcon />
1319
- </button>
1416
+ {previewText}
1320
1417
  </div>
1321
- );
1322
- },
1418
+ <button className="value-spec-editor__list-editor__edit-icon">
1419
+ <PencilIcon />
1420
+ </button>
1421
+ </div>
1422
+ );
1423
+ };
1424
+
1425
+ export const CollectionValueInstanceValueEditor = observer(
1426
+ CollectionValueInstanceValueEditorInner as <T, U extends { values: T[] }>(
1427
+ props: CollectionValueInstanceValueEditorProps<T, U>,
1428
+ ) => ReturnType<typeof CollectionValueInstanceValueEditorInner>,
1323
1429
  );
1324
1430
 
1325
1431
  const UnsupportedValueSpecificationEditor: React.FC = () => (
1326
1432
  <div className="value-spec-editor--unsupported">unsupported</div>
1327
1433
  );
1328
1434
 
1329
- const DateInstanceValueEditor = observer(
1330
- (props: {
1331
- valueSpecification: PrimitiveInstanceValue | SimpleFunctionExpression;
1332
- graph: PureModel;
1333
- observerContext: ObserverContext;
1334
- typeCheckOption: TypeCheckOption;
1335
- className?: string | undefined;
1336
- setValueSpecification: (val: ValueSpecification) => void;
1337
- resetValue: () => void;
1338
- handleBlur?: (() => void) | undefined;
1339
- displayAsEditableValue?: boolean | undefined;
1340
- }) => {
1341
- const {
1342
- valueSpecification,
1343
- setValueSpecification,
1344
- graph,
1345
- observerContext,
1346
- typeCheckOption,
1347
- resetValue,
1348
- handleBlur,
1349
- displayAsEditableValue,
1350
- } = props;
1435
+ interface DateInstanceValueEditorProps<
1436
+ T extends CustomDatePickerValueSpecification | undefined,
1437
+ > extends Omit<
1438
+ PrimitiveInstanceValueEditorProps<T, string | null>,
1439
+ 'updateValueSpecification'
1440
+ > {
1441
+ updateValueSpecification: CustomDatePickerUpdateValueSpecification<T>;
1442
+ typeCheckOption: TypeCheckOption | V1_TypeCheckOption;
1443
+ displayAsEditableValue?: boolean | undefined;
1444
+ }
1351
1445
 
1352
- return (
1353
- <div className="value-spec-editor">
1354
- <CustomDatePicker
1355
- valueSpecification={valueSpecification}
1356
- graph={graph}
1357
- observerContext={observerContext}
1358
- typeCheckOption={typeCheckOption}
1359
- setValueSpecification={setValueSpecification}
1360
- hasError={
1361
- valueSpecification instanceof PrimitiveInstanceValue &&
1362
- !isValidInstanceValue(valueSpecification)
1363
- }
1364
- handleBlur={handleBlur}
1365
- displayAsEditableValue={displayAsEditableValue}
1366
- />
1367
- {!displayAsEditableValue && (
1368
- <button
1369
- className="value-spec-editor__reset-btn"
1370
- name="Reset"
1371
- title="Reset"
1372
- onClick={resetValue}
1373
- >
1374
- <RefreshIcon />
1375
- </button>
1376
- )}
1377
- </div>
1378
- );
1379
- },
1446
+ const DateInstanceValueEditorInner = <
1447
+ T extends CustomDatePickerValueSpecification | undefined,
1448
+ >(
1449
+ props: DateInstanceValueEditorProps<T>,
1450
+ ): React.ReactElement => {
1451
+ const {
1452
+ valueSpecification,
1453
+ valueSelector,
1454
+ updateValueSpecification,
1455
+ resetValue,
1456
+ handleBlur,
1457
+ typeCheckOption,
1458
+ displayAsEditableValue,
1459
+ className,
1460
+ } = props;
1461
+
1462
+ return (
1463
+ <div className={clsx('value-spec-editor', className)}>
1464
+ <CustomDatePicker<T>
1465
+ valueSpecification={valueSpecification}
1466
+ valueSelector={valueSelector}
1467
+ typeCheckOption={typeCheckOption}
1468
+ updateValueSpecification={updateValueSpecification}
1469
+ hasError={
1470
+ valueSpecification instanceof PrimitiveInstanceValue &&
1471
+ !isValidInstanceValue(valueSpecification)
1472
+ }
1473
+ handleBlur={handleBlur}
1474
+ displayAsEditableValue={displayAsEditableValue}
1475
+ />
1476
+ {!displayAsEditableValue && (
1477
+ <button
1478
+ className="value-spec-editor__reset-btn"
1479
+ name="Reset"
1480
+ title="Reset"
1481
+ onClick={resetValue}
1482
+ >
1483
+ <RefreshIcon />
1484
+ </button>
1485
+ )}
1486
+ </div>
1487
+ );
1488
+ };
1489
+
1490
+ export const DateInstanceValueEditor = observer(
1491
+ DateInstanceValueEditorInner as <
1492
+ T extends CustomDatePickerValueSpecification | undefined,
1493
+ >(
1494
+ props: DateInstanceValueEditorProps<T>,
1495
+ ) => ReturnType<typeof DateInstanceValueEditorInner>,
1380
1496
  );
1381
1497
 
1382
1498
  /**
@@ -1396,6 +1512,9 @@ export const BasicValueSpecificationEditor = forwardRef<
1396
1512
  setValueSpecification: (val: ValueSpecification) => void;
1397
1513
  resetValue: () => void;
1398
1514
  isConstant?: boolean | undefined;
1515
+ selectorSearchConfig?:
1516
+ | BasicValueSpecificationEditorSelectorSearchConfig
1517
+ | undefined;
1399
1518
  selectorConfig?: BasicValueSpecificationEditorSelectorConfig | undefined;
1400
1519
  handleBlur?: (() => void) | undefined;
1401
1520
  handleKeyDown?:
@@ -1403,7 +1522,7 @@ export const BasicValueSpecificationEditor = forwardRef<
1403
1522
  | undefined;
1404
1523
  displayDateEditorAsEditableValue?: boolean | undefined;
1405
1524
  }
1406
- >(function _BasicValueSpecificationEditor(props, ref) {
1525
+ >(function BasicValueSpecificationEditorInner(props, ref) {
1407
1526
  const {
1408
1527
  className,
1409
1528
  valueSpecification,
@@ -1412,37 +1531,128 @@ export const BasicValueSpecificationEditor = forwardRef<
1412
1531
  typeCheckOption,
1413
1532
  setValueSpecification,
1414
1533
  resetValue,
1534
+ selectorSearchConfig,
1415
1535
  selectorConfig,
1416
1536
  isConstant,
1417
1537
  handleBlur,
1418
1538
  handleKeyDown,
1419
1539
  displayDateEditorAsEditableValue,
1420
1540
  } = props;
1541
+
1542
+ const applicationStore = useApplicationStore();
1543
+
1544
+ const errorChecker = (_valueSpecification: InstanceValue) =>
1545
+ !isValidInstanceValue(_valueSpecification);
1546
+ const dateValueSelector = (
1547
+ _valueSpecification: SimpleFunctionExpression | PrimitiveInstanceValue,
1548
+ ): string | null => {
1549
+ return _valueSpecification instanceof SimpleFunctionExpression
1550
+ ? ''
1551
+ : (_valueSpecification.values[0] as string | null);
1552
+ };
1553
+ const dateUpdateValueSpecification: CustomDatePickerUpdateValueSpecification<
1554
+ SimpleFunctionExpression | PrimitiveInstanceValue | undefined
1555
+ > = (_valueSpecification, value, options): void => {
1556
+ if (value instanceof CustomDateOption) {
1557
+ setValueSpecification(
1558
+ buildPureAdjustDateFunction(value, graph, observerContext),
1559
+ );
1560
+ } else if (value instanceof CustomFirstDayOfOption) {
1561
+ setValueSpecification(
1562
+ buildPureDateFunctionExpression(value, graph, observerContext),
1563
+ );
1564
+ } else if (value instanceof CustomPreviousDayOfWeekOption) {
1565
+ setValueSpecification(
1566
+ buildPureDateFunctionExpression(value, graph, observerContext),
1567
+ );
1568
+ } else if (value instanceof DatePickerOption) {
1569
+ setValueSpecification(
1570
+ buildPureDateFunctionExpression(value, graph, observerContext),
1571
+ );
1572
+ } else {
1573
+ if (_valueSpecification instanceof SimpleFunctionExpression) {
1574
+ setValueSpecification(
1575
+ buildPrimitiveInstanceValue(
1576
+ graph,
1577
+ guaranteeNonNullable(options?.primitiveTypeEnum),
1578
+ value,
1579
+ observerContext,
1580
+ ),
1581
+ );
1582
+ } else if (_valueSpecification instanceof InstanceValue) {
1583
+ instanceValue_setValue(_valueSpecification, value, 0, observerContext);
1584
+ if (
1585
+ _valueSpecification.genericType.value.rawType.path !==
1586
+ guaranteeNonNullable(options?.primitiveTypeEnum)
1587
+ ) {
1588
+ valueSpecification_setGenericType(
1589
+ _valueSpecification,
1590
+ GenericTypeExplicitReference.create(
1591
+ new GenericType(
1592
+ getPrimitiveTypeInstanceFromEnum(
1593
+ guaranteeNonNullable(options?.primitiveTypeEnum),
1594
+ ),
1595
+ ),
1596
+ ),
1597
+ );
1598
+ }
1599
+ setValueSpecification(_valueSpecification);
1600
+ } else if (options?.primitiveTypeEnum === PRIMITIVE_TYPE.LATESTDATE) {
1601
+ setValueSpecification(
1602
+ buildPrimitiveInstanceValue(
1603
+ graph,
1604
+ PRIMITIVE_TYPE.LATESTDATE,
1605
+ value,
1606
+ observerContext,
1607
+ ),
1608
+ );
1609
+ }
1610
+ }
1611
+ };
1612
+
1421
1613
  if (valueSpecification instanceof PrimitiveInstanceValue) {
1422
1614
  const _type = valueSpecification.genericType.value.rawType;
1615
+
1616
+ // eslint-disable-next-line comma-spacing
1617
+ const valueSelector = <T,>(val: PrimitiveInstanceValue): T =>
1618
+ val.values[0] as T;
1619
+ // eslint-disable-next-line comma-spacing
1620
+ const updateValueSpecification = <T,>(
1621
+ _valueSpecification: PrimitiveInstanceValue,
1622
+ value: T,
1623
+ ) => {
1624
+ instanceValue_setValue(_valueSpecification, value, 0, observerContext);
1625
+ setValueSpecification(_valueSpecification);
1626
+ };
1423
1627
  switch (_type.path) {
1424
1628
  case PRIMITIVE_TYPE.STRING:
1425
1629
  return (
1426
- <StringPrimitiveInstanceValueEditor
1630
+ <StringPrimitiveInstanceValueEditor<PrimitiveInstanceValue>
1427
1631
  valueSpecification={valueSpecification}
1428
- setValueSpecification={setValueSpecification}
1632
+ valueSelector={valueSelector}
1633
+ updateValueSpecification={updateValueSpecification}
1634
+ errorChecker={errorChecker}
1429
1635
  className={className}
1430
1636
  resetValue={resetValue}
1637
+ selectorSearchConfig={selectorSearchConfig}
1431
1638
  selectorConfig={selectorConfig}
1432
- observerContext={observerContext}
1433
- ref={ref}
1639
+ ref={
1640
+ ref as React.ForwardedRef<
1641
+ HTMLInputElement | SelectComponent | null
1642
+ >
1643
+ }
1434
1644
  handleBlur={handleBlur}
1435
1645
  handleKeyDown={handleKeyDown}
1436
1646
  />
1437
1647
  );
1438
1648
  case PRIMITIVE_TYPE.BOOLEAN:
1439
1649
  return (
1440
- <BooleanPrimitiveInstanceValueEditor
1650
+ <BooleanPrimitiveInstanceValueEditor<PrimitiveInstanceValue>
1441
1651
  valueSpecification={valueSpecification}
1442
- setValueSpecification={setValueSpecification}
1652
+ valueSelector={valueSelector}
1653
+ updateValueSpecification={updateValueSpecification}
1443
1654
  className={className}
1444
1655
  resetValue={resetValue}
1445
- observerContext={observerContext}
1446
1656
  />
1447
1657
  );
1448
1658
  case PRIMITIVE_TYPE.NUMBER:
@@ -1452,13 +1662,14 @@ export const BasicValueSpecificationEditor = forwardRef<
1452
1662
  case PRIMITIVE_TYPE.BYTE:
1453
1663
  case PRIMITIVE_TYPE.INTEGER:
1454
1664
  return (
1455
- <NumberPrimitiveInstanceValueEditor
1665
+ <NumberPrimitiveInstanceValueEditor<PrimitiveInstanceValue>
1456
1666
  valueSpecification={valueSpecification}
1667
+ valueSelector={valueSelector}
1457
1668
  isInteger={_type.path === PRIMITIVE_TYPE.INTEGER}
1458
- setValueSpecification={setValueSpecification}
1669
+ updateValueSpecification={updateValueSpecification}
1670
+ errorChecker={errorChecker}
1459
1671
  className={className}
1460
1672
  resetValue={resetValue}
1461
- observerContext={observerContext}
1462
1673
  ref={ref}
1463
1674
  handleBlur={handleBlur}
1464
1675
  handleKeyDown={handleKeyDown}
@@ -1469,48 +1680,150 @@ export const BasicValueSpecificationEditor = forwardRef<
1469
1680
  case PRIMITIVE_TYPE.DATETIME:
1470
1681
  case PRIMITIVE_TYPE.LATESTDATE:
1471
1682
  return (
1472
- <DateInstanceValueEditor
1683
+ <DateInstanceValueEditor<
1684
+ SimpleFunctionExpression | PrimitiveInstanceValue
1685
+ >
1473
1686
  valueSpecification={valueSpecification}
1474
- graph={graph}
1475
- observerContext={observerContext}
1687
+ valueSelector={dateValueSelector}
1476
1688
  typeCheckOption={typeCheckOption}
1477
1689
  className={className}
1478
- setValueSpecification={setValueSpecification}
1690
+ updateValueSpecification={dateUpdateValueSpecification}
1479
1691
  resetValue={resetValue}
1480
1692
  handleBlur={handleBlur}
1481
1693
  displayAsEditableValue={displayDateEditorAsEditableValue}
1694
+ errorChecker={(_valueSpecification) =>
1695
+ _valueSpecification instanceof PrimitiveInstanceValue &&
1696
+ errorChecker(_valueSpecification)
1697
+ }
1482
1698
  />
1483
1699
  );
1484
1700
  default:
1485
1701
  return <UnsupportedValueSpecificationEditor />;
1486
1702
  }
1487
1703
  } else if (valueSpecification instanceof EnumValueInstanceValue) {
1704
+ const enumType = guaranteeType(
1705
+ valueSpecification.genericType?.value.rawType,
1706
+ Enumeration,
1707
+ );
1708
+ const options = enumType.values.map((value) => ({
1709
+ label: value.name,
1710
+ value: value.name,
1711
+ }));
1488
1712
  return (
1489
- <EnumValueInstanceValueEditor
1713
+ <EnumInstanceValueEditor<EnumValueInstanceValue>
1490
1714
  valueSpecification={valueSpecification}
1715
+ valueSelector={(val) =>
1716
+ val.values[0] === undefined ? null : val.values[0].value.name
1717
+ }
1718
+ options={options}
1491
1719
  className={className}
1492
1720
  resetValue={resetValue}
1493
- setValueSpecification={setValueSpecification}
1494
- observerContext={observerContext}
1721
+ updateValueSpecification={(
1722
+ _valueSpecification: EnumValueInstanceValue,
1723
+ value: string | null,
1724
+ ) => {
1725
+ const enumValue = guaranteeNonNullable(
1726
+ enumType.values.find((val: Enum) => val.name === value),
1727
+ `Unable to find enum value ${value} in enumeration ${enumType.name}`,
1728
+ );
1729
+ instanceValue_setValue(
1730
+ _valueSpecification,
1731
+ EnumValueExplicitReference.create(enumValue),
1732
+ 0,
1733
+ observerContext,
1734
+ );
1735
+ setValueSpecification(_valueSpecification);
1736
+ }}
1737
+ errorChecker={(_valueSpecification: EnumValueInstanceValue) =>
1738
+ !isValidInstanceValue(_valueSpecification)
1739
+ }
1495
1740
  handleBlur={handleBlur}
1741
+ selectorConfig={selectorConfig}
1496
1742
  />
1497
1743
  );
1498
1744
  } else if (
1499
1745
  valueSpecification instanceof CollectionInstanceValue &&
1500
1746
  valueSpecification.genericType
1501
1747
  ) {
1748
+ const updateValueSpecification = (
1749
+ collectionValueSpecification: CollectionInstanceValue,
1750
+ valueSpecifications: ValueSpecification[],
1751
+ ) => {
1752
+ instanceValue_setValues(
1753
+ collectionValueSpecification,
1754
+ valueSpecifications,
1755
+ observerContext,
1756
+ );
1757
+ setValueSpecification(collectionValueSpecification);
1758
+ };
1759
+ const convertTextToValueSpecification = (
1760
+ type: Type | V1_PackageableType,
1761
+ text: string,
1762
+ ): ValueSpecification | null => {
1763
+ if (type instanceof Enumeration) {
1764
+ const enumValue = convertTextToEnum(text, type);
1765
+ if (enumValue) {
1766
+ const enumValueInstanceValue = new EnumValueInstanceValue(
1767
+ GenericTypeExplicitReference.create(new GenericType(type)),
1768
+ );
1769
+ instanceValue_setValues(
1770
+ enumValueInstanceValue,
1771
+ [EnumValueExplicitReference.create(enumValue)],
1772
+ observerContext,
1773
+ );
1774
+ return observe_ValueSpecification(
1775
+ enumValueInstanceValue,
1776
+ observerContext,
1777
+ );
1778
+ }
1779
+ } else {
1780
+ const primitiveVal = convertTextToPrimitiveInstanceValue(
1781
+ guaranteeType(type, Type),
1782
+ text,
1783
+ observerContext,
1784
+ );
1785
+ if (primitiveVal) {
1786
+ return observe_ValueSpecification(primitiveVal, observerContext);
1787
+ }
1788
+ }
1789
+ return null;
1790
+ };
1791
+ const enumOptions =
1792
+ typeCheckOption.expectedType instanceof Enumeration
1793
+ ? typeCheckOption.expectedType.values.map((enumValue) => ({
1794
+ label: enumValue.name,
1795
+ value: enumValue.name,
1796
+ }))
1797
+ : undefined;
1502
1798
  // NOTE: since when we fill in the arguments, `[]` (or `nullish` value in Pure)
1503
1799
  // is used for parameters we don't handle, we should not attempt to support empty collection
1504
1800
  // without generic type here as that is equivalent to `[]`
1505
1801
  return (
1506
- <CollectionValueInstanceValueEditor
1802
+ <CollectionValueInstanceValueEditor<
1803
+ ValueSpecification,
1804
+ CollectionInstanceValue
1805
+ >
1507
1806
  valueSpecification={valueSpecification}
1508
- graph={graph}
1807
+ updateValueSpecification={updateValueSpecification}
1509
1808
  expectedType={typeCheckOption.expectedType}
1510
1809
  className={className}
1511
- setValueSpecification={setValueSpecification}
1810
+ selectorSearchConfig={selectorSearchConfig}
1512
1811
  selectorConfig={selectorConfig}
1513
- observerContext={observerContext}
1812
+ stringifyCollectionValueSpecification={(
1813
+ collectionValueSpecification: CollectionInstanceValue,
1814
+ ) => stringifyValue(collectionValueSpecification.values)}
1815
+ errorChecker={errorChecker}
1816
+ convertValueSpecificationToText={(
1817
+ _valueSpecification: ValueSpecification,
1818
+ ) =>
1819
+ getValueSpecificationStringValue(
1820
+ _valueSpecification,
1821
+ applicationStore,
1822
+ { omitEnumOwnerName: true },
1823
+ )
1824
+ }
1825
+ convertTextToValueSpecification={convertTextToValueSpecification}
1826
+ enumOptions={enumOptions}
1514
1827
  />
1515
1828
  );
1516
1829
  }
@@ -1536,19 +1849,22 @@ export const BasicValueSpecificationEditor = forwardRef<
1536
1849
  handleBlur={handleBlur}
1537
1850
  handleKeyDown={handleKeyDown}
1538
1851
  displayDateEditorAsEditableValue={displayDateEditorAsEditableValue}
1852
+ selectorSearchConfig={selectorSearchConfig}
1853
+ selectorConfig={selectorConfig}
1539
1854
  />
1540
1855
  );
1541
1856
  } else if (valueSpecification instanceof SimpleFunctionExpression) {
1542
1857
  if (isSubType(typeCheckOption.expectedType, PrimitiveType.DATE)) {
1543
1858
  if (isUsedDateFunctionSupportedInFormMode(valueSpecification)) {
1544
1859
  return (
1545
- <DateInstanceValueEditor
1860
+ <DateInstanceValueEditor<
1861
+ SimpleFunctionExpression | PrimitiveInstanceValue
1862
+ >
1546
1863
  valueSpecification={valueSpecification}
1547
- graph={graph}
1548
- observerContext={observerContext}
1864
+ valueSelector={dateValueSelector}
1549
1865
  typeCheckOption={typeCheckOption}
1550
1866
  className={className}
1551
- setValueSpecification={setValueSpecification}
1867
+ updateValueSpecification={dateUpdateValueSpecification}
1552
1868
  resetValue={resetValue}
1553
1869
  handleBlur={handleBlur}
1554
1870
  displayAsEditableValue={displayDateEditorAsEditableValue}
@@ -1580,14 +1896,25 @@ export const BasicValueSpecificationEditor = forwardRef<
1580
1896
  return (
1581
1897
  <NumberPrimitiveInstanceValueEditor
1582
1898
  valueSpecification={simplifiedValue}
1899
+ valueSelector={(val) => val.values[0] as number}
1583
1900
  isInteger={
1584
1901
  simplifiedValue.genericType.value.rawType ===
1585
1902
  PrimitiveType.INTEGER
1586
1903
  }
1587
- setValueSpecification={setValueSpecification}
1904
+ updateValueSpecification={(
1905
+ _valueSpecification: PrimitiveInstanceValue,
1906
+ value: number | null,
1907
+ ) => {
1908
+ instanceValue_setValue(
1909
+ _valueSpecification,
1910
+ value,
1911
+ 0,
1912
+ observerContext,
1913
+ );
1914
+ setValueSpecification(_valueSpecification);
1915
+ }}
1588
1916
  className={className}
1589
1917
  resetValue={resetValue}
1590
- observerContext={observerContext}
1591
1918
  ref={ref}
1592
1919
  handleBlur={handleBlur}
1593
1920
  handleKeyDown={handleKeyDown}
@@ -1608,6 +1935,9 @@ export const EditableBasicValueSpecificationEditor = observer(
1608
1935
  observerContext: ObserverContext;
1609
1936
  typeCheckOption: TypeCheckOption;
1610
1937
  resetValue: () => void;
1938
+ selectorSearchConfig?:
1939
+ | BasicValueSpecificationEditorSelectorSearchConfig
1940
+ | undefined;
1611
1941
  selectorConfig?: BasicValueSpecificationEditorSelectorConfig | undefined;
1612
1942
  isConstant?: boolean;
1613
1943
  initializeAsEditable?: boolean;
@@ -1619,6 +1949,7 @@ export const EditableBasicValueSpecificationEditor = observer(
1619
1949
  observerContext,
1620
1950
  typeCheckOption,
1621
1951
  resetValue,
1952
+ selectorSearchConfig,
1622
1953
  selectorConfig,
1623
1954
  isConstant,
1624
1955
  initializeAsEditable,
@@ -1664,6 +1995,7 @@ export const EditableBasicValueSpecificationEditor = observer(
1664
1995
  observerContext={observerContext}
1665
1996
  typeCheckOption={typeCheckOption}
1666
1997
  resetValue={resetValue}
1998
+ selectorSearchConfig={selectorSearchConfig}
1667
1999
  selectorConfig={selectorConfig}
1668
2000
  isConstant={isConstant}
1669
2001
  ref={inputRef}