@finos/legend-query-builder 4.16.20 → 4.16.22

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