@trackunit/custom-field-components 2.23.13 → 2.23.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +182 -110
- package/index.esm.js +185 -113
- package/package.json +3 -5
- package/src/StringListField/StringListField.d.ts +7 -5
- package/src/getValidationRules.d.ts +3 -5
- package/src/StringListField/AddStringListField.d.ts +0 -13
package/index.cjs.js
CHANGED
|
@@ -6,22 +6,17 @@ var reactFormComponents = require('@trackunit/react-form-components');
|
|
|
6
6
|
var sharedUtils = require('@trackunit/shared-utils');
|
|
7
7
|
var reactHookForm = require('react-hook-form');
|
|
8
8
|
var react = require('react');
|
|
9
|
-
var reactComponents = require('@trackunit/react-components');
|
|
10
|
-
var reactCoreHooks = require('@trackunit/react-core-hooks');
|
|
11
|
-
var reactModal = require('@trackunit/react-modal');
|
|
12
9
|
|
|
13
10
|
var defaultTranslations = {
|
|
14
11
|
"customfield.input.email": "Email",
|
|
15
12
|
"customfield.input.webaddress": "Web Address",
|
|
16
|
-
"customfield.stringlistfield.
|
|
17
|
-
"customfield.stringlistfield.
|
|
18
|
-
"customfield.stringlistfield.
|
|
19
|
-
"customfield.stringlistfield.
|
|
20
|
-
"customfield.stringlistfield.
|
|
21
|
-
"customfield.stringlistfield.
|
|
22
|
-
"customfield.stringlistfield.
|
|
23
|
-
"customfield.stringlistfield.discard": "Discard",
|
|
24
|
-
"customfield.stringlistfield.noValuesAddedYet": "No values added yet",
|
|
13
|
+
"customfield.stringlistfield.counter": "{{count}}/{{maximumItems}}",
|
|
14
|
+
"customfield.stringlistfield.duplicate": "This value has already been added",
|
|
15
|
+
"customfield.stringlistfield.itemMaximumLength": "Each value can be at most {{itemMaximumLength}} characters",
|
|
16
|
+
"customfield.stringlistfield.itemMinimumLength": "Each value must be at least {{itemMinimumLength}} characters",
|
|
17
|
+
"customfield.stringlistfield.maximumItems": "You can add at most {{maximumItems}} values",
|
|
18
|
+
"customfield.stringlistfield.pattern": "Value is not in the expected format",
|
|
19
|
+
"customfield.stringlistfield.placeholder": "Add {{label}}",
|
|
25
20
|
"customfield.unit.ACRE": "acre",
|
|
26
21
|
"customfield.unit.AMPERE": "A",
|
|
27
22
|
"customfield.unit.AMPERE_HOUR": "Ah",
|
|
@@ -252,73 +247,183 @@ const DropdownCustomField = ({ defaultValue, "data-testid": dataTestId, onChange
|
|
|
252
247
|
};
|
|
253
248
|
|
|
254
249
|
/**
|
|
255
|
-
*
|
|
250
|
+
* A labeled multi-value field for string-list custom fields.
|
|
256
251
|
*/
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
onClose: () => {
|
|
261
|
-
setInputValue("");
|
|
262
|
-
setIsEditing(false);
|
|
263
|
-
},
|
|
264
|
-
});
|
|
252
|
+
const StringListField = ({ defaultValue = [], label, description, helpText, errorMessage, disabled = false, validationRules, id, control, "data-testid": dataTestId, itemMinimumLength, itemMaximumLength, maximumItems, pattern, className, }) => {
|
|
253
|
+
const [t] = useTranslation();
|
|
254
|
+
const normalizeStringListItem = react.useCallback((raw) => raw.trim().replace(/\s+/g, " "), []);
|
|
265
255
|
const [inputValue, setInputValue] = react.useState("");
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
256
|
+
const [reportItemMinimumLength, setReportItemMinimumLength] = react.useState(false);
|
|
257
|
+
const inputValueRef = react.useRef("");
|
|
258
|
+
const selectTestId = dataTestId ?? id;
|
|
259
|
+
const itemPattern = react.useMemo(() => {
|
|
260
|
+
if (!pattern) {
|
|
261
|
+
return null;
|
|
272
262
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
react.useEffect(() => {
|
|
281
|
-
setLocalValue(initialValue);
|
|
282
|
-
}, [initialValue]);
|
|
283
|
-
const handleBlur = react.useCallback(() => {
|
|
284
|
-
if (localValue !== initialValue) {
|
|
285
|
-
onSave(localValue);
|
|
286
|
-
}
|
|
287
|
-
}, [localValue, initialValue, onSave]);
|
|
288
|
-
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsx(reactFormComponents.TextBaseInput, { "data-testid": `${id}-${index}-input`, disabled: disabled, onBlur: handleBlur, onChange: e => setLocalValue(e.target.value), value: localValue }), jsxRuntime.jsx(reactComponents.IconButton, { disabled: disabled, icon: jsxRuntime.jsx(reactComponents.Icon, { name: "Trash", size: "small" }), onClick: onClickDelete, title: t("customfield.stringlistfield.deleteField.PrimaryActionLabel"), variant: "secondary-danger" })] }));
|
|
289
|
-
};
|
|
290
|
-
/**
|
|
291
|
-
* A custom field that displays a list of strings.
|
|
292
|
-
*/
|
|
293
|
-
const StringListField = ({ defaultValue = [], label, description, disabled = false, validationRules, id, control, register, setValue, "data-testid": dataTestId, ...props }) => {
|
|
294
|
-
const [isEditing, setIsEditing] = react.useState(false);
|
|
295
|
-
const { confirm } = reactCoreHooks.useConfirmationDialog();
|
|
296
|
-
const { t } = useTranslation();
|
|
263
|
+
try {
|
|
264
|
+
return new RegExp(pattern);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}, [pattern]);
|
|
297
270
|
return (jsxRuntime.jsx(reactHookForm.Controller, { control: control, defaultValue: defaultValue, name: id, render: ({ field }) => {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
271
|
+
const values = Array.isArray(field.value) ? field.value : [];
|
|
272
|
+
const atCap = typeof maximumItems === "number" && values.length >= maximumItems;
|
|
273
|
+
const rejection = (() => {
|
|
274
|
+
const item = normalizeStringListItem(inputValue);
|
|
275
|
+
if (item === "") {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
if (typeof itemMaximumLength === "number" && item.length > itemMaximumLength) {
|
|
279
|
+
return "itemMaximumLength";
|
|
280
|
+
}
|
|
281
|
+
if (reportItemMinimumLength && typeof itemMinimumLength === "number" && item.length < itemMinimumLength) {
|
|
282
|
+
return "itemMinimumLength";
|
|
283
|
+
}
|
|
284
|
+
if (itemPattern && !itemPattern.test(item)) {
|
|
285
|
+
return "pattern";
|
|
286
|
+
}
|
|
287
|
+
if (atCap) {
|
|
288
|
+
return "maximumItems";
|
|
289
|
+
}
|
|
290
|
+
if (values.some(existing => existing.toLowerCase() === item.toLowerCase())) {
|
|
291
|
+
return "duplicate";
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
})();
|
|
295
|
+
const standingHelpText = (() => {
|
|
296
|
+
if (!disabled && atCap && typeof maximumItems === "number") {
|
|
297
|
+
return t("customfield.stringlistfield.maximumItems", { maximumItems });
|
|
298
|
+
}
|
|
299
|
+
return helpText ?? description;
|
|
300
|
+
})();
|
|
301
|
+
const resolvedHelpText = (() => {
|
|
302
|
+
switch (rejection) {
|
|
303
|
+
case "duplicate":
|
|
304
|
+
return t("customfield.stringlistfield.duplicate");
|
|
305
|
+
case "maximumItems":
|
|
306
|
+
return t("customfield.stringlistfield.maximumItems", { maximumItems });
|
|
307
|
+
case "itemMaximumLength":
|
|
308
|
+
return t("customfield.stringlistfield.itemMaximumLength", { itemMaximumLength });
|
|
309
|
+
case "itemMinimumLength":
|
|
310
|
+
return t("customfield.stringlistfield.itemMinimumLength", { itemMinimumLength });
|
|
311
|
+
case "pattern":
|
|
312
|
+
return t("customfield.stringlistfield.pattern");
|
|
313
|
+
case null:
|
|
314
|
+
return errorMessage ?? standingHelpText;
|
|
315
|
+
default:
|
|
316
|
+
return sharedUtils.exhaustiveCheck(rejection);
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
const selectedOptions = values.map(item => ({
|
|
320
|
+
label: item,
|
|
321
|
+
value: item,
|
|
322
|
+
}));
|
|
323
|
+
const tryAddItem = (raw) => {
|
|
324
|
+
if (disabled) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const item = normalizeStringListItem(raw);
|
|
328
|
+
if (item === "") {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (typeof itemMaximumLength === "number" && item.length > itemMaximumLength) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (typeof itemMinimumLength === "number" && item.length < itemMinimumLength) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (itemPattern && !itemPattern.test(item)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (typeof maximumItems === "number" && values.length >= maximumItems) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (values.some(existing => existing.toLowerCase() === item.toLowerCase())) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
inputValueRef.current = "";
|
|
347
|
+
setInputValue("");
|
|
348
|
+
field.onChange([...values, item]);
|
|
301
349
|
};
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
newValues[index] = value;
|
|
305
|
-
field.onChange(newValues);
|
|
350
|
+
const onCreateOption = (raw) => {
|
|
351
|
+
tryAddItem(raw);
|
|
306
352
|
};
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
handleRemoveValue(index);
|
|
353
|
+
const onSelectChange = (selected, actionMeta) => {
|
|
354
|
+
if (disabled || actionMeta.action === "create-option") {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const incoming = selected.map(option => option.value);
|
|
358
|
+
const kept = values.filter(existing => incoming.includes(existing));
|
|
359
|
+
const added = incoming.filter(next => !values.some(existing => existing.toLowerCase() === next.toLowerCase()));
|
|
360
|
+
if (added.length > 0 && typeof maximumItems === "number" && kept.length + added.length > maximumItems) {
|
|
361
|
+
return;
|
|
317
362
|
}
|
|
363
|
+
field.onChange([...kept, ...added]);
|
|
318
364
|
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
365
|
+
const onKeyDown = (event) => {
|
|
366
|
+
const raw = inputValueRef.current;
|
|
367
|
+
const item = normalizeStringListItem(raw);
|
|
368
|
+
if (event.key === " " && item === "") {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
const next = `${raw} `;
|
|
371
|
+
inputValueRef.current = next;
|
|
372
|
+
setInputValue(next);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (event.key !== "Enter") {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (item === "") {
|
|
379
|
+
if (raw !== "") {
|
|
380
|
+
event.preventDefault();
|
|
381
|
+
inputValueRef.current = "";
|
|
382
|
+
setInputValue("");
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
if (typeof itemMinimumLength === "number" && item.length < itemMinimumLength) {
|
|
388
|
+
setReportItemMinimumLength(true);
|
|
389
|
+
}
|
|
390
|
+
tryAddItem(raw);
|
|
391
|
+
};
|
|
392
|
+
const onInputChange = (nextValue, meta) => {
|
|
393
|
+
if (meta.action === "input-change") {
|
|
394
|
+
setReportItemMinimumLength(false);
|
|
395
|
+
inputValueRef.current = nextValue;
|
|
396
|
+
setInputValue(nextValue);
|
|
397
|
+
return nextValue;
|
|
398
|
+
}
|
|
399
|
+
const draft = inputValueRef.current;
|
|
400
|
+
const item = normalizeStringListItem(draft);
|
|
401
|
+
if (meta.action === "input-blur" &&
|
|
402
|
+
item !== "" &&
|
|
403
|
+
typeof itemMinimumLength === "number" &&
|
|
404
|
+
item.length < itemMinimumLength) {
|
|
405
|
+
setReportItemMinimumLength(true);
|
|
406
|
+
}
|
|
407
|
+
const isRejectedDraft = item !== "" &&
|
|
408
|
+
((typeof itemMaximumLength === "number" && item.length > itemMaximumLength) ||
|
|
409
|
+
(typeof itemMinimumLength === "number" && item.length < itemMinimumLength) ||
|
|
410
|
+
(itemPattern !== null && !itemPattern.test(item)) ||
|
|
411
|
+
(typeof maximumItems === "number" && values.length >= maximumItems) ||
|
|
412
|
+
values.some(existing => existing.toLowerCase() === item.toLowerCase()));
|
|
413
|
+
if (isRejectedDraft) {
|
|
414
|
+
return draft;
|
|
415
|
+
}
|
|
416
|
+
if (meta.action === "input-blur" && item !== "") {
|
|
417
|
+
tryAddItem(draft);
|
|
418
|
+
return "";
|
|
419
|
+
}
|
|
420
|
+
inputValueRef.current = nextValue;
|
|
421
|
+
setInputValue(nextValue);
|
|
422
|
+
return nextValue;
|
|
423
|
+
};
|
|
424
|
+
return (jsxRuntime.jsx(reactFormComponents.FormGroup, { className: className, "data-testid": dataTestId ? `${dataTestId}-field` : undefined, helpAddon: typeof maximumItems === "number"
|
|
425
|
+
? t("customfield.stringlistfield.counter", { count: values.length, maximumItems })
|
|
426
|
+
: undefined, helpText: resolvedHelpText, htmlFor: id, isInvalid: rejection !== null || Boolean(errorMessage), label: label, children: jsxRuntime.jsx(reactFormComponents.CreatableSelect, { "data-testid": selectTestId, disabled: disabled, hasError: rejection !== null || Boolean(errorMessage), hideSelectedOptions: false, inputId: id, inputValue: inputValue, isMulti: true, label: label, onChange: onSelectChange, onCreateOption: onCreateOption, onInputChange: onInputChange, onKeyDown: onKeyDown, options: selectedOptions, placeholder: t("customfield.stringlistfield.placeholder", { label }), value: selectedOptions }) }));
|
|
322
427
|
}, rules: validationRules }));
|
|
323
428
|
};
|
|
324
429
|
|
|
@@ -447,44 +552,11 @@ const getDropdownValidationRules = (_definition) => {
|
|
|
447
552
|
return getDefaultValidationRules();
|
|
448
553
|
};
|
|
449
554
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
* @param {GetStringListValidationRulesDefinition} definition - The definition of the string list field.
|
|
453
|
-
* @returns {ValidationRules} The generated validation rules.
|
|
555
|
+
* String-list item limits are enforced when a value is added. React Hook Form's
|
|
556
|
+
* minLength, maxLength, and pattern rules apply to the array itself, so they are not used here.
|
|
454
557
|
*/
|
|
455
|
-
const getStringListValidationRules = (
|
|
456
|
-
|
|
457
|
-
const itemMinLength = definition.itemMinimumLength !== null && definition.itemMinimumLength !== undefined
|
|
458
|
-
? {
|
|
459
|
-
minLength: {
|
|
460
|
-
value: definition.itemMinimumLength,
|
|
461
|
-
message: `minimum length for each item is ${definition.itemMinimumLength}`,
|
|
462
|
-
},
|
|
463
|
-
}
|
|
464
|
-
: undefined;
|
|
465
|
-
const itemMaxLength = definition.itemMaximumLength !== null && definition.itemMaximumLength !== undefined
|
|
466
|
-
? {
|
|
467
|
-
maxLength: {
|
|
468
|
-
value: definition.itemMaximumLength,
|
|
469
|
-
message: `maximum length for each item is ${definition.itemMaximumLength}`,
|
|
470
|
-
},
|
|
471
|
-
}
|
|
472
|
-
: undefined;
|
|
473
|
-
const pattern = definition.pattern
|
|
474
|
-
? {
|
|
475
|
-
pattern: {
|
|
476
|
-
value: RegExp(definition.pattern),
|
|
477
|
-
message: `Must match pattern: ${definition.pattern}`,
|
|
478
|
-
},
|
|
479
|
-
}
|
|
480
|
-
: undefined;
|
|
481
|
-
return definition.uiEditable
|
|
482
|
-
? {
|
|
483
|
-
...itemMaxLength,
|
|
484
|
-
...itemMinLength,
|
|
485
|
-
...pattern,
|
|
486
|
-
}
|
|
487
|
-
: defaultRules;
|
|
558
|
+
const getStringListValidationRules = (_definition) => {
|
|
559
|
+
return {};
|
|
488
560
|
};
|
|
489
561
|
/**
|
|
490
562
|
* Generates validation rules for a date field.
|
|
@@ -609,8 +681,8 @@ const useCustomFieldResolver = ({ field, definition, register, setValue, formSta
|
|
|
609
681
|
return (jsxRuntime.jsx(DateCustomField, { "data-testid": `${key}`, defaultValue: value, disabled: !isEditableCombined, errorMessage: errorMessage, helpText: description, id: uniqueIdentifier, label: title || "", readOnly: !isEditableCombined, register: validation.register, setValue: validation.setValue, title: title || "", validationRules: rules }));
|
|
610
682
|
}
|
|
611
683
|
case "StringListFieldDefinition": {
|
|
612
|
-
const rules = getStringListValidationRules(
|
|
613
|
-
return (jsxRuntime.jsx(StringListField, { control: control, "data-testid": key, defaultValue: value ?? undefined, description: description ?? "", disabled: !isEditableCombined, errorMessage: errorMessage, id: uniqueIdentifier, label: title || "",
|
|
684
|
+
const rules = getStringListValidationRules();
|
|
685
|
+
return (jsxRuntime.jsx(StringListField, { control: control, "data-testid": key, defaultValue: value ?? undefined, description: description ?? "", disabled: !isEditableCombined, errorMessage: errorMessage, id: uniqueIdentifier, itemMaximumLength: def.itemMaximumLength, itemMinimumLength: def.itemMinimumLength, label: title || "", maximumItems: def.maximumItems, pattern: def.pattern, validationRules: rules }));
|
|
614
686
|
}
|
|
615
687
|
case "JsonFieldDefinition":
|
|
616
688
|
case "FileFieldDefinition": {
|
package/index.esm.js
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import { jsx
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
2
|
import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-library-translation';
|
|
3
|
-
import { FormGroup, Checkbox, toISODateStringUTC, DateField, MultiSelectField, SelectField,
|
|
3
|
+
import { FormGroup, Checkbox, toISODateStringUTC, DateField, MultiSelectField, SelectField, CreatableSelect, useGetPhoneValidationRules, NumberField, TextField, PhoneField, ActionButton } from '@trackunit/react-form-components';
|
|
4
4
|
import { uuidv4, exhaustiveCheck } from '@trackunit/shared-utils';
|
|
5
5
|
import { Controller, useWatch } from 'react-hook-form';
|
|
6
|
-
import { useState, useEffect, useCallback, createElement, useMemo } from 'react';
|
|
7
|
-
import { Card, CardHeader, Tooltip, CardBody, Text, CardFooter, Button, IconButton, Icon } from '@trackunit/react-components';
|
|
8
|
-
import { useConfirmationDialog } from '@trackunit/react-core-hooks';
|
|
9
|
-
import { useModal, Modal, ModalHeader, ModalBody, ModalFooter } from '@trackunit/react-modal';
|
|
6
|
+
import { useState, useEffect, useCallback, createElement, useMemo, useRef } from 'react';
|
|
10
7
|
|
|
11
8
|
var defaultTranslations = {
|
|
12
9
|
"customfield.input.email": "Email",
|
|
13
10
|
"customfield.input.webaddress": "Web Address",
|
|
14
|
-
"customfield.stringlistfield.
|
|
15
|
-
"customfield.stringlistfield.
|
|
16
|
-
"customfield.stringlistfield.
|
|
17
|
-
"customfield.stringlistfield.
|
|
18
|
-
"customfield.stringlistfield.
|
|
19
|
-
"customfield.stringlistfield.
|
|
20
|
-
"customfield.stringlistfield.
|
|
21
|
-
"customfield.stringlistfield.discard": "Discard",
|
|
22
|
-
"customfield.stringlistfield.noValuesAddedYet": "No values added yet",
|
|
11
|
+
"customfield.stringlistfield.counter": "{{count}}/{{maximumItems}}",
|
|
12
|
+
"customfield.stringlistfield.duplicate": "This value has already been added",
|
|
13
|
+
"customfield.stringlistfield.itemMaximumLength": "Each value can be at most {{itemMaximumLength}} characters",
|
|
14
|
+
"customfield.stringlistfield.itemMinimumLength": "Each value must be at least {{itemMinimumLength}} characters",
|
|
15
|
+
"customfield.stringlistfield.maximumItems": "You can add at most {{maximumItems}} values",
|
|
16
|
+
"customfield.stringlistfield.pattern": "Value is not in the expected format",
|
|
17
|
+
"customfield.stringlistfield.placeholder": "Add {{label}}",
|
|
23
18
|
"customfield.unit.ACRE": "acre",
|
|
24
19
|
"customfield.unit.AMPERE": "A",
|
|
25
20
|
"customfield.unit.AMPERE_HOUR": "Ah",
|
|
@@ -250,73 +245,183 @@ const DropdownCustomField = ({ defaultValue, "data-testid": dataTestId, onChange
|
|
|
250
245
|
};
|
|
251
246
|
|
|
252
247
|
/**
|
|
253
|
-
*
|
|
248
|
+
* A labeled multi-value field for string-list custom fields.
|
|
254
249
|
*/
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
onClose: () => {
|
|
259
|
-
setInputValue("");
|
|
260
|
-
setIsEditing(false);
|
|
261
|
-
},
|
|
262
|
-
});
|
|
250
|
+
const StringListField = ({ defaultValue = [], label, description, helpText, errorMessage, disabled = false, validationRules, id, control, "data-testid": dataTestId, itemMinimumLength, itemMaximumLength, maximumItems, pattern, className, }) => {
|
|
251
|
+
const [t] = useTranslation();
|
|
252
|
+
const normalizeStringListItem = useCallback((raw) => raw.trim().replace(/\s+/g, " "), []);
|
|
263
253
|
const [inputValue, setInputValue] = useState("");
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
254
|
+
const [reportItemMinimumLength, setReportItemMinimumLength] = useState(false);
|
|
255
|
+
const inputValueRef = useRef("");
|
|
256
|
+
const selectTestId = dataTestId ?? id;
|
|
257
|
+
const itemPattern = useMemo(() => {
|
|
258
|
+
if (!pattern) {
|
|
259
|
+
return null;
|
|
270
260
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
useEffect(() => {
|
|
279
|
-
setLocalValue(initialValue);
|
|
280
|
-
}, [initialValue]);
|
|
281
|
-
const handleBlur = useCallback(() => {
|
|
282
|
-
if (localValue !== initialValue) {
|
|
283
|
-
onSave(localValue);
|
|
284
|
-
}
|
|
285
|
-
}, [localValue, initialValue, onSave]);
|
|
286
|
-
return (jsxs("div", { className: "flex items-center gap-2", children: [jsx(TextBaseInput, { "data-testid": `${id}-${index}-input`, disabled: disabled, onBlur: handleBlur, onChange: e => setLocalValue(e.target.value), value: localValue }), jsx(IconButton, { disabled: disabled, icon: jsx(Icon, { name: "Trash", size: "small" }), onClick: onClickDelete, title: t("customfield.stringlistfield.deleteField.PrimaryActionLabel"), variant: "secondary-danger" })] }));
|
|
287
|
-
};
|
|
288
|
-
/**
|
|
289
|
-
* A custom field that displays a list of strings.
|
|
290
|
-
*/
|
|
291
|
-
const StringListField = ({ defaultValue = [], label, description, disabled = false, validationRules, id, control, register, setValue, "data-testid": dataTestId, ...props }) => {
|
|
292
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
293
|
-
const { confirm } = useConfirmationDialog();
|
|
294
|
-
const { t } = useTranslation();
|
|
261
|
+
try {
|
|
262
|
+
return new RegExp(pattern);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}, [pattern]);
|
|
295
268
|
return (jsx(Controller, { control: control, defaultValue: defaultValue, name: id, render: ({ field }) => {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
269
|
+
const values = Array.isArray(field.value) ? field.value : [];
|
|
270
|
+
const atCap = typeof maximumItems === "number" && values.length >= maximumItems;
|
|
271
|
+
const rejection = (() => {
|
|
272
|
+
const item = normalizeStringListItem(inputValue);
|
|
273
|
+
if (item === "") {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (typeof itemMaximumLength === "number" && item.length > itemMaximumLength) {
|
|
277
|
+
return "itemMaximumLength";
|
|
278
|
+
}
|
|
279
|
+
if (reportItemMinimumLength && typeof itemMinimumLength === "number" && item.length < itemMinimumLength) {
|
|
280
|
+
return "itemMinimumLength";
|
|
281
|
+
}
|
|
282
|
+
if (itemPattern && !itemPattern.test(item)) {
|
|
283
|
+
return "pattern";
|
|
284
|
+
}
|
|
285
|
+
if (atCap) {
|
|
286
|
+
return "maximumItems";
|
|
287
|
+
}
|
|
288
|
+
if (values.some(existing => existing.toLowerCase() === item.toLowerCase())) {
|
|
289
|
+
return "duplicate";
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
})();
|
|
293
|
+
const standingHelpText = (() => {
|
|
294
|
+
if (!disabled && atCap && typeof maximumItems === "number") {
|
|
295
|
+
return t("customfield.stringlistfield.maximumItems", { maximumItems });
|
|
296
|
+
}
|
|
297
|
+
return helpText ?? description;
|
|
298
|
+
})();
|
|
299
|
+
const resolvedHelpText = (() => {
|
|
300
|
+
switch (rejection) {
|
|
301
|
+
case "duplicate":
|
|
302
|
+
return t("customfield.stringlistfield.duplicate");
|
|
303
|
+
case "maximumItems":
|
|
304
|
+
return t("customfield.stringlistfield.maximumItems", { maximumItems });
|
|
305
|
+
case "itemMaximumLength":
|
|
306
|
+
return t("customfield.stringlistfield.itemMaximumLength", { itemMaximumLength });
|
|
307
|
+
case "itemMinimumLength":
|
|
308
|
+
return t("customfield.stringlistfield.itemMinimumLength", { itemMinimumLength });
|
|
309
|
+
case "pattern":
|
|
310
|
+
return t("customfield.stringlistfield.pattern");
|
|
311
|
+
case null:
|
|
312
|
+
return errorMessage ?? standingHelpText;
|
|
313
|
+
default:
|
|
314
|
+
return exhaustiveCheck(rejection);
|
|
315
|
+
}
|
|
316
|
+
})();
|
|
317
|
+
const selectedOptions = values.map(item => ({
|
|
318
|
+
label: item,
|
|
319
|
+
value: item,
|
|
320
|
+
}));
|
|
321
|
+
const tryAddItem = (raw) => {
|
|
322
|
+
if (disabled) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const item = normalizeStringListItem(raw);
|
|
326
|
+
if (item === "") {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (typeof itemMaximumLength === "number" && item.length > itemMaximumLength) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (typeof itemMinimumLength === "number" && item.length < itemMinimumLength) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (itemPattern && !itemPattern.test(item)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (typeof maximumItems === "number" && values.length >= maximumItems) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (values.some(existing => existing.toLowerCase() === item.toLowerCase())) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
inputValueRef.current = "";
|
|
345
|
+
setInputValue("");
|
|
346
|
+
field.onChange([...values, item]);
|
|
299
347
|
};
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
newValues[index] = value;
|
|
303
|
-
field.onChange(newValues);
|
|
348
|
+
const onCreateOption = (raw) => {
|
|
349
|
+
tryAddItem(raw);
|
|
304
350
|
};
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
handleRemoveValue(index);
|
|
351
|
+
const onSelectChange = (selected, actionMeta) => {
|
|
352
|
+
if (disabled || actionMeta.action === "create-option") {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const incoming = selected.map(option => option.value);
|
|
356
|
+
const kept = values.filter(existing => incoming.includes(existing));
|
|
357
|
+
const added = incoming.filter(next => !values.some(existing => existing.toLowerCase() === next.toLowerCase()));
|
|
358
|
+
if (added.length > 0 && typeof maximumItems === "number" && kept.length + added.length > maximumItems) {
|
|
359
|
+
return;
|
|
315
360
|
}
|
|
361
|
+
field.onChange([...kept, ...added]);
|
|
316
362
|
};
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
363
|
+
const onKeyDown = (event) => {
|
|
364
|
+
const raw = inputValueRef.current;
|
|
365
|
+
const item = normalizeStringListItem(raw);
|
|
366
|
+
if (event.key === " " && item === "") {
|
|
367
|
+
event.preventDefault();
|
|
368
|
+
const next = `${raw} `;
|
|
369
|
+
inputValueRef.current = next;
|
|
370
|
+
setInputValue(next);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (event.key !== "Enter") {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (item === "") {
|
|
377
|
+
if (raw !== "") {
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
inputValueRef.current = "";
|
|
380
|
+
setInputValue("");
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
event.preventDefault();
|
|
385
|
+
if (typeof itemMinimumLength === "number" && item.length < itemMinimumLength) {
|
|
386
|
+
setReportItemMinimumLength(true);
|
|
387
|
+
}
|
|
388
|
+
tryAddItem(raw);
|
|
389
|
+
};
|
|
390
|
+
const onInputChange = (nextValue, meta) => {
|
|
391
|
+
if (meta.action === "input-change") {
|
|
392
|
+
setReportItemMinimumLength(false);
|
|
393
|
+
inputValueRef.current = nextValue;
|
|
394
|
+
setInputValue(nextValue);
|
|
395
|
+
return nextValue;
|
|
396
|
+
}
|
|
397
|
+
const draft = inputValueRef.current;
|
|
398
|
+
const item = normalizeStringListItem(draft);
|
|
399
|
+
if (meta.action === "input-blur" &&
|
|
400
|
+
item !== "" &&
|
|
401
|
+
typeof itemMinimumLength === "number" &&
|
|
402
|
+
item.length < itemMinimumLength) {
|
|
403
|
+
setReportItemMinimumLength(true);
|
|
404
|
+
}
|
|
405
|
+
const isRejectedDraft = item !== "" &&
|
|
406
|
+
((typeof itemMaximumLength === "number" && item.length > itemMaximumLength) ||
|
|
407
|
+
(typeof itemMinimumLength === "number" && item.length < itemMinimumLength) ||
|
|
408
|
+
(itemPattern !== null && !itemPattern.test(item)) ||
|
|
409
|
+
(typeof maximumItems === "number" && values.length >= maximumItems) ||
|
|
410
|
+
values.some(existing => existing.toLowerCase() === item.toLowerCase()));
|
|
411
|
+
if (isRejectedDraft) {
|
|
412
|
+
return draft;
|
|
413
|
+
}
|
|
414
|
+
if (meta.action === "input-blur" && item !== "") {
|
|
415
|
+
tryAddItem(draft);
|
|
416
|
+
return "";
|
|
417
|
+
}
|
|
418
|
+
inputValueRef.current = nextValue;
|
|
419
|
+
setInputValue(nextValue);
|
|
420
|
+
return nextValue;
|
|
421
|
+
};
|
|
422
|
+
return (jsx(FormGroup, { className: className, "data-testid": dataTestId ? `${dataTestId}-field` : undefined, helpAddon: typeof maximumItems === "number"
|
|
423
|
+
? t("customfield.stringlistfield.counter", { count: values.length, maximumItems })
|
|
424
|
+
: undefined, helpText: resolvedHelpText, htmlFor: id, isInvalid: rejection !== null || Boolean(errorMessage), label: label, children: jsx(CreatableSelect, { "data-testid": selectTestId, disabled: disabled, hasError: rejection !== null || Boolean(errorMessage), hideSelectedOptions: false, inputId: id, inputValue: inputValue, isMulti: true, label: label, onChange: onSelectChange, onCreateOption: onCreateOption, onInputChange: onInputChange, onKeyDown: onKeyDown, options: selectedOptions, placeholder: t("customfield.stringlistfield.placeholder", { label }), value: selectedOptions }) }));
|
|
320
425
|
}, rules: validationRules }));
|
|
321
426
|
};
|
|
322
427
|
|
|
@@ -445,44 +550,11 @@ const getDropdownValidationRules = (_definition) => {
|
|
|
445
550
|
return getDefaultValidationRules();
|
|
446
551
|
};
|
|
447
552
|
/**
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
* @param {GetStringListValidationRulesDefinition} definition - The definition of the string list field.
|
|
451
|
-
* @returns {ValidationRules} The generated validation rules.
|
|
553
|
+
* String-list item limits are enforced when a value is added. React Hook Form's
|
|
554
|
+
* minLength, maxLength, and pattern rules apply to the array itself, so they are not used here.
|
|
452
555
|
*/
|
|
453
|
-
const getStringListValidationRules = (
|
|
454
|
-
|
|
455
|
-
const itemMinLength = definition.itemMinimumLength !== null && definition.itemMinimumLength !== undefined
|
|
456
|
-
? {
|
|
457
|
-
minLength: {
|
|
458
|
-
value: definition.itemMinimumLength,
|
|
459
|
-
message: `minimum length for each item is ${definition.itemMinimumLength}`,
|
|
460
|
-
},
|
|
461
|
-
}
|
|
462
|
-
: undefined;
|
|
463
|
-
const itemMaxLength = definition.itemMaximumLength !== null && definition.itemMaximumLength !== undefined
|
|
464
|
-
? {
|
|
465
|
-
maxLength: {
|
|
466
|
-
value: definition.itemMaximumLength,
|
|
467
|
-
message: `maximum length for each item is ${definition.itemMaximumLength}`,
|
|
468
|
-
},
|
|
469
|
-
}
|
|
470
|
-
: undefined;
|
|
471
|
-
const pattern = definition.pattern
|
|
472
|
-
? {
|
|
473
|
-
pattern: {
|
|
474
|
-
value: RegExp(definition.pattern),
|
|
475
|
-
message: `Must match pattern: ${definition.pattern}`,
|
|
476
|
-
},
|
|
477
|
-
}
|
|
478
|
-
: undefined;
|
|
479
|
-
return definition.uiEditable
|
|
480
|
-
? {
|
|
481
|
-
...itemMaxLength,
|
|
482
|
-
...itemMinLength,
|
|
483
|
-
...pattern,
|
|
484
|
-
}
|
|
485
|
-
: defaultRules;
|
|
556
|
+
const getStringListValidationRules = (_definition) => {
|
|
557
|
+
return {};
|
|
486
558
|
};
|
|
487
559
|
/**
|
|
488
560
|
* Generates validation rules for a date field.
|
|
@@ -607,8 +679,8 @@ const useCustomFieldResolver = ({ field, definition, register, setValue, formSta
|
|
|
607
679
|
return (jsx(DateCustomField, { "data-testid": `${key}`, defaultValue: value, disabled: !isEditableCombined, errorMessage: errorMessage, helpText: description, id: uniqueIdentifier, label: title || "", readOnly: !isEditableCombined, register: validation.register, setValue: validation.setValue, title: title || "", validationRules: rules }));
|
|
608
680
|
}
|
|
609
681
|
case "StringListFieldDefinition": {
|
|
610
|
-
const rules = getStringListValidationRules(
|
|
611
|
-
return (jsx(StringListField, { control: control, "data-testid": key, defaultValue: value ?? undefined, description: description ?? "", disabled: !isEditableCombined, errorMessage: errorMessage, id: uniqueIdentifier, label: title || "",
|
|
682
|
+
const rules = getStringListValidationRules();
|
|
683
|
+
return (jsx(StringListField, { control: control, "data-testid": key, defaultValue: value ?? undefined, description: description ?? "", disabled: !isEditableCombined, errorMessage: errorMessage, id: uniqueIdentifier, itemMaximumLength: def.itemMaximumLength, itemMinimumLength: def.itemMinimumLength, label: title || "", maximumItems: def.maximumItems, pattern: def.pattern, validationRules: rules }));
|
|
612
684
|
}
|
|
613
685
|
case "JsonFieldDefinition":
|
|
614
686
|
case "FileFieldDefinition": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/custom-field-components",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.14",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -8,12 +8,10 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"react-select": "^5.10.2",
|
|
11
|
-
"@trackunit/react-form-components": "2.10.
|
|
11
|
+
"@trackunit/react-form-components": "2.10.8",
|
|
12
12
|
"@trackunit/shared-utils": "1.19.4",
|
|
13
|
-
"@trackunit/custom-field-api": "2.24.
|
|
13
|
+
"@trackunit/custom-field-api": "2.24.14",
|
|
14
14
|
"@trackunit/react-components": "3.8.1",
|
|
15
|
-
"@trackunit/react-modal": "2.10.7",
|
|
16
|
-
"@trackunit/react-core-hooks": "1.25.4",
|
|
17
15
|
"@trackunit/i18n-library-translation": "2.8.4",
|
|
18
16
|
"@trackunit/iris-app-runtime-core-api": "1.21.4"
|
|
19
17
|
},
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { CommonProps } from "@trackunit/react-components";
|
|
2
2
|
import { ReactElement } from "react";
|
|
3
|
-
import { Control, FieldValues, RegisterOptions
|
|
3
|
+
import { Control, FieldValues, RegisterOptions } from "react-hook-form";
|
|
4
4
|
export interface StringListFieldProps extends CommonProps {
|
|
5
5
|
defaultValue?: Array<string>;
|
|
6
6
|
label: string;
|
|
7
7
|
helpText?: string;
|
|
8
8
|
errorMessage?: string;
|
|
9
|
-
register: UseFormRegister<FieldValues>;
|
|
10
|
-
setValue: UseFormSetValue<FieldValues>;
|
|
11
9
|
description?: string;
|
|
12
10
|
disabled?: boolean;
|
|
13
11
|
validationRules: RegisterOptions;
|
|
14
12
|
id: string;
|
|
15
13
|
control: Control<FieldValues>;
|
|
14
|
+
itemMinimumLength?: number | null;
|
|
15
|
+
itemMaximumLength?: number | null;
|
|
16
|
+
maximumItems?: number | null;
|
|
17
|
+
pattern?: string | null;
|
|
16
18
|
}
|
|
17
19
|
/**
|
|
18
|
-
* A
|
|
20
|
+
* A labeled multi-value field for string-list custom fields.
|
|
19
21
|
*/
|
|
20
|
-
export declare const StringListField: ({ defaultValue, label, description, disabled, validationRules, id, control,
|
|
22
|
+
export declare const StringListField: ({ defaultValue, label, description, helpText, errorMessage, disabled, validationRules, id, control, "data-testid": dataTestId, itemMinimumLength, itemMaximumLength, maximumItems, pattern, className, }: StringListFieldProps) => ReactElement;
|
|
@@ -58,12 +58,10 @@ interface GetStringListValidationRulesDefinition extends CommonValidationRulesDe
|
|
|
58
58
|
maximumItems?: number | null;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param {GetStringListValidationRulesDefinition} definition - The definition of the string list field.
|
|
64
|
-
* @returns {ValidationRules} The generated validation rules.
|
|
61
|
+
* String-list item limits are enforced when a value is added. React Hook Form's
|
|
62
|
+
* minLength, maxLength, and pattern rules apply to the array itself, so they are not used here.
|
|
65
63
|
*/
|
|
66
|
-
export declare const getStringListValidationRules: (
|
|
64
|
+
export declare const getStringListValidationRules: (_definition: GetStringListValidationRulesDefinition) => ValidationRules;
|
|
67
65
|
/**
|
|
68
66
|
* Generates validation rules for a date field.
|
|
69
67
|
*
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { ReactElement } from "react";
|
|
2
|
-
interface AddStringListFieldProps {
|
|
3
|
-
id: string;
|
|
4
|
-
label: string;
|
|
5
|
-
isEditing: boolean;
|
|
6
|
-
setIsEditing: (isEditing: boolean) => void;
|
|
7
|
-
onAdd: (newItem: string) => void;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Modal for adding a new string list field
|
|
11
|
-
*/
|
|
12
|
-
export declare const AddStringListField: ({ id, label, isEditing, setIsEditing, onAdd, }: AddStringListFieldProps) => ReactElement;
|
|
13
|
-
export {};
|