@sustaina/shared-ui 1.24.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -4,7 +4,6 @@ import { twMerge } from 'tailwind-merge';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import * as React4 from 'react';
6
6
  import React4__default, { forwardRef, useRef, useMemo, useCallback, isValidElement, useState, useEffect, useLayoutEffect, createElement } from 'react';
7
- import { format, isValid, parseISO, isAfter, compareAsc, parse } from 'date-fns';
8
7
  import { CircleX, CircleHelp, Undo, Redo, Bold, Italic, Underline, Strikethrough, Code, Pilcrow, Heading1, Heading2, Heading3, List as List$1, ListOrdered, Quote, CodeSquare, Link, Link2Off, Image as Image$1, AlignLeft, AlignCenter, AlignRight, XIcon, ChevronRight, CheckIcon, Triangle, CalendarIcon, X, Search, ChevronUp, ChevronDown, Minimize2, Maximize2, Plus, ChevronLeft, CircleUserRound, PanelLeftIcon, Bug, GripVertical, Info, CircleMinus, Minus } from 'lucide-react';
9
8
  import { createPortal } from 'react-dom';
10
9
  import * as SelectPrimitive from '@radix-ui/react-select';
@@ -12,6 +11,7 @@ import { useForm, FormProvider, Controller, useFormContext, useFormState, useFie
12
11
  import { Slot } from '@radix-ui/react-slot';
13
12
  import * as LabelPrimitive from '@radix-ui/react-label';
14
13
  import { cva } from 'class-variance-authority';
14
+ import { format, isValid, parseISO, isAfter, compareAsc, parse } from 'date-fns';
15
15
  import * as PopoverPrimitive from '@radix-ui/react-popover';
16
16
  import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
17
17
  import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
@@ -3421,6 +3421,66 @@ function getBuilder(fieldType) {
3421
3421
  return new JSONBuilder();
3422
3422
  }
3423
3423
  }
3424
+ var FILTER_FIELD_MAP = {
3425
+ timezone: "timezoneId",
3426
+ decimalSeparator: "decimalSeparatorId",
3427
+ country: "countryId",
3428
+ currency: "currencyId"
3429
+ };
3430
+ function transformFilterKeys(obj, fieldMap = FILTER_FIELD_MAP) {
3431
+ if (Array.isArray(obj)) {
3432
+ return obj.map((item) => transformFilterKeys(item, fieldMap));
3433
+ }
3434
+ if (obj && typeof obj === "object") {
3435
+ const newObj = {};
3436
+ for (const key in obj) {
3437
+ const mappedKey = fieldMap[key] ?? key;
3438
+ newObj[mappedKey] = transformFilterKeys(obj[key], fieldMap);
3439
+ }
3440
+ return newObj;
3441
+ }
3442
+ return obj;
3443
+ }
3444
+ var sanitizeInput = (val) => {
3445
+ if (!val) return val;
3446
+ if (typeof val !== "string") return "__INVALID_TYPE__";
3447
+ if (val.includes("\n") || val.includes("\r") || /[\u2028\u2029]/u.test(val))
3448
+ return "__INVALID_NEWLINE__";
3449
+ if (/\\\\/.test(val)) return "__INVALID_ESCAPE__";
3450
+ if (/\\(n|t|r|b|f|u[0-9a-fA-F]{4})/.test(val)) return "__INVALID_ESCAPE__";
3451
+ if (/\p{Cf}/u.test(val)) return "__INVALID_UNICODE_WHITESPACE__";
3452
+ if (/[\u00A0\u1680\u180E\u202F\u205F\u3000]/u.test(val)) return "__INVALID_UNICODE_WHITESPACE__";
3453
+ const trimmed = val.trim();
3454
+ if (/^\{.*\}$/s.test(trimmed) || /^\[.*\]$/s.test(trimmed)) return "__INVALID_JSON_LITERAL__";
3455
+ if (/\\\{/.test(val) || /\\\}/.test(val)) return "__INVALID_JSON_ESCAPE__";
3456
+ if (/[%_*~^]/.test(trimmed)) return "__INVALID_WILDCARD__";
3457
+ if (/[%><={}\\[\]"']/u.test(trimmed)) return "__INVALID_CHAR__";
3458
+ if (/\p{Cc}/u.test(val)) return "__INVALID_CONTROL_CHAR__";
3459
+ return trimmed.replace(/\s+/g, " ");
3460
+ };
3461
+ var numericTypes = ["number", "integer", "decimal"];
3462
+ var dateTypes = ["date", "datemonth"];
3463
+ var validateByFieldType = (value, fieldType) => {
3464
+ if (!value) return { valid: true };
3465
+ if (numericTypes.includes(fieldType)) {
3466
+ if (!/^\d+(\.\d+)?$/.test(value)) {
3467
+ return { valid: false, message: "Please enter a valid number." };
3468
+ }
3469
+ }
3470
+ if (fieldType === "boolean") {
3471
+ if (!["true", "false"].includes(value.toLowerCase())) {
3472
+ return { valid: false, message: "Please enter a boolean value (true/false)." };
3473
+ }
3474
+ }
3475
+ if (dateTypes.includes(fieldType)) {
3476
+ const normalized = fieldType === "datemonth" ? `${value}-01` : value;
3477
+ const parsed = parseISO(normalized);
3478
+ if (!isValid(parsed)) {
3479
+ return { valid: false, message: "Invalid date format." };
3480
+ }
3481
+ }
3482
+ return { valid: true };
3483
+ };
3424
3484
  var AdvanceSearch = ({
3425
3485
  fields,
3426
3486
  portalId,
@@ -3428,7 +3488,8 @@ var AdvanceSearch = ({
3428
3488
  limitRows = 4,
3429
3489
  onSearch,
3430
3490
  onClear,
3431
- shortDateFormat
3491
+ shortDateFormat,
3492
+ filterFieldMap = FILTER_FIELD_MAP
3432
3493
  }) => {
3433
3494
  const fieldsData = useMemo(() => {
3434
3495
  if (fields.length === 0) throw new Error("fields cannot be an empty array");
@@ -3480,72 +3541,75 @@ var AdvanceSearch = ({
3480
3541
  const onSubmit = useCallback(() => {
3481
3542
  const operatorValidation = {};
3482
3543
  rows.forEach((r) => {
3483
- const availableOperators = operatorsForField(r.fieldName);
3484
- if (!availableOperators.length || !availableOperators.includes(r.operator)) {
3544
+ const ops = operatorsForField(r.fieldName);
3545
+ if (!ops.length || !ops.includes(r.operator))
3485
3546
  operatorValidation[r.id] = "Please select an operator.";
3486
- }
3487
3547
  });
3488
3548
  setOperatorErrors(operatorValidation);
3489
- if (Object.keys(operatorValidation).length > 0) {
3490
- return;
3491
- }
3549
+ if (Object.keys(operatorValidation).length > 0) return;
3492
3550
  const currentValues = getValues();
3493
- let hasRangeError = false;
3494
- const rawRows = rows.map((r) => {
3495
- const startFieldName = `value_${r.id}`;
3496
- const startValue = currentValues[startFieldName] ?? "";
3551
+ let hasError = false;
3552
+ const processedRows = rows.map((r) => {
3553
+ const startField = `value_${r.id}`;
3554
+ const endField = `value2_${r.id}`;
3555
+ let v1 = currentValues[startField] ?? "";
3556
+ let v2 = currentValues[endField] ?? "";
3557
+ const s1 = sanitizeInput(v1);
3558
+ if (s1?.startsWith("__INVALID")) {
3559
+ hasError = true;
3560
+ setError(startField, { type: "validate", message: "Invalid input." });
3561
+ return null;
3562
+ }
3563
+ v1 = s1 || "";
3564
+ const valid1 = validateByFieldType(v1, r.fieldType);
3565
+ if (!valid1.valid) {
3566
+ hasError = true;
3567
+ setError(startField, { type: "validate", message: valid1.message });
3568
+ return null;
3569
+ }
3497
3570
  if (r.operator === "between") {
3498
- const endFieldName = `value2_${r.id}`;
3499
- const endValue = currentValues[endFieldName] ?? "";
3500
- if (startValue && endValue) {
3501
- const startDate = parseRangeValue(startValue, r.fieldType);
3502
- const endDate = parseRangeValue(endValue, r.fieldType);
3503
- if (startDate && endDate && isAfter(startDate, endDate)) {
3504
- hasRangeError = true;
3505
- setError(startFieldName, {
3506
- type: "validate",
3507
- message: "Start value must be before end value."
3508
- });
3509
- setError(endFieldName, {
3510
- type: "validate",
3511
- message: "End value must be after start value."
3512
- });
3513
- } else {
3514
- clearErrors([startFieldName, endFieldName]);
3571
+ const s2 = sanitizeInput(v2);
3572
+ if (s2?.startsWith("__INVALID")) {
3573
+ hasError = true;
3574
+ setError(endField, { type: "validate", message: "Invalid input." });
3575
+ return null;
3576
+ }
3577
+ v2 = s2 || "";
3578
+ const valid2 = validateByFieldType(v2, r.fieldType);
3579
+ if (!valid2.valid) {
3580
+ hasError = true;
3581
+ setError(endField, { type: "validate", message: valid2.message });
3582
+ return null;
3583
+ }
3584
+ if (v1 && v2 && ["date", "datemonth"].includes(r.fieldType)) {
3585
+ const d1 = parseRangeValue(v1, r.fieldType);
3586
+ const d2 = parseRangeValue(v2, r.fieldType);
3587
+ if (d1 && d2 && isAfter(d1, d2)) {
3588
+ hasError = true;
3589
+ setError(startField, { type: "validate", message: "Start value must be before end value." });
3590
+ setError(endField, { type: "validate", message: "End value must be after start value." });
3591
+ return null;
3515
3592
  }
3516
3593
  }
3517
- return {
3518
- ...r,
3519
- value: startValue ?? "",
3520
- value2: endValue ?? ""
3521
- };
3594
+ return { ...r, value: v1, value2: v2 };
3522
3595
  }
3523
- return {
3524
- ...r,
3525
- value: startValue ?? ""
3526
- };
3596
+ return { ...r, value: v1 };
3527
3597
  });
3528
- if (hasRangeError) {
3529
- return;
3530
- }
3598
+ if (hasError) return;
3599
+ const cleanedRows = processedRows.filter(Boolean);
3531
3600
  const param = {
3532
- AND: rawRows.map((r) => {
3533
- const builder = getBuilder(r.fieldType);
3534
- return builder.build(r);
3535
- }).filter(Boolean)
3601
+ AND: cleanedRows.map((r) => getBuilder(r.fieldType).build(r)).filter(Boolean)
3536
3602
  };
3537
- if (onSearch) {
3538
- onSearch(param);
3539
- }
3603
+ if (onSearch) onSearch(transformFilterKeys(param, filterFieldMap));
3540
3604
  }, [
3541
- clearErrors,
3542
- getValues,
3543
- onSearch,
3605
+ rows,
3544
3606
  operatorsForField,
3607
+ getValues,
3545
3608
  parseRangeValue,
3546
- rows,
3547
3609
  setError,
3548
- setOperatorErrors
3610
+ setOperatorErrors,
3611
+ filterFieldMap,
3612
+ onSearch
3549
3613
  ]);
3550
3614
  return /* @__PURE__ */ jsx(
3551
3615
  ExpandCollapse_default,
@@ -3583,9 +3647,7 @@ var AdvanceSearch = ({
3583
3647
  unregister(`value_${row.id}`);
3584
3648
  unregister(`value2_${row.id}`);
3585
3649
  },
3586
- onClearValue: (which) => {
3587
- clearValue(row.id, which);
3588
- },
3650
+ onClearValue: (which) => clearValue(row.id, which),
3589
3651
  disableAdd: limitRows !== void 0 && rows.length >= limitRows
3590
3652
  },
3591
3653
  row.id
@@ -3610,7 +3672,7 @@ var AdvanceSearch = ({
3610
3672
  Button,
3611
3673
  {
3612
3674
  type: "submit",
3613
- className: "w-full bg-[#379a2a] text-white hover:bg-[#2f7c21] md:w-auto md:min-w-[120px]",
3675
+ className: "w-full bg-sus-green-2 text-white hover:bg-[#2f7c21] md:w-auto md:min-w-[120px]",
3614
3676
  "data-testid": "advsearch-btn-search",
3615
3677
  children: "Search"
3616
3678
  }
@@ -6203,7 +6265,8 @@ var FormulaEditor = ({
6203
6265
  onChange,
6204
6266
  onSelectSuggestion,
6205
6267
  field,
6206
- fieldState
6268
+ fieldState,
6269
+ mode = "edit"
6207
6270
  }) => {
6208
6271
  const [isExpanded, setIsExpanded] = useState(false);
6209
6272
  const lastEmittedValueRef = useRef(null);
@@ -6220,27 +6283,22 @@ var FormulaEditor = ({
6220
6283
  const prefixMap = useMemo(() => buildPrefixMap(normalizedConfigs), [normalizedConfigs]);
6221
6284
  const configLookup = useMemo(() => {
6222
6285
  const lookup = /* @__PURE__ */ new Map();
6223
- normalizedConfigs.forEach((config) => {
6224
- lookup.set(config.prefix, config);
6225
- });
6286
+ normalizedConfigs.forEach((config) => lookup.set(config.prefix, config));
6226
6287
  return lookup;
6227
6288
  }, [normalizedConfigs]);
6228
6289
  const allowedOperators = useMemo(() => operators.map((operator) => operator.value), [operators]);
6229
6290
  const displayError = errorMessage ?? fieldState?.error?.message;
6230
6291
  const hasError = Boolean(displayError);
6231
- const isInteractionDisabled = Boolean(disabled || loading);
6292
+ const isEditorReadOnly = mode === "display";
6293
+ const isEditorDisabled = disabled || loading || isEditorReadOnly;
6232
6294
  const convertValueToContent = useCallback(
6233
6295
  (input) => {
6234
6296
  if (!input) return "";
6235
6297
  const trimmed = input.trim();
6236
6298
  if (!trimmed) return "";
6237
6299
  const parsedJSON = tryParseJSON(trimmed);
6238
- if (parsedJSON && parsedJSON.type === "doc") {
6239
- return parsedJSON;
6240
- }
6241
- if (looksLikeHTML(trimmed)) {
6242
- return input;
6243
- }
6300
+ if (parsedJSON && parsedJSON.type === "doc") return parsedJSON;
6301
+ if (looksLikeHTML(trimmed)) return input;
6244
6302
  return buildDocFromRaw(input, prefixMap, configLookup);
6245
6303
  },
6246
6304
  [configLookup, prefixMap]
@@ -6271,7 +6329,7 @@ var FormulaEditor = ({
6271
6329
  hasError ? "border border-destructive" : "border focus-visible:border-ring",
6272
6330
  "w-full rounded-lg bg-white px-4 py-3",
6273
6331
  "overflow-y-auto whitespace-pre-wrap wrap-break-word focus:outline-none",
6274
- isInteractionDisabled && "pointer-events-none opacity-60",
6332
+ isEditorDisabled && "pointer-events-none",
6275
6333
  editorClassName
6276
6334
  ),
6277
6335
  ...loading ? { "aria-busy": "true" } : {}
@@ -6280,8 +6338,8 @@ var FormulaEditor = ({
6280
6338
  });
6281
6339
  useEffect(() => {
6282
6340
  if (!editor) return;
6283
- editor.setEditable(!isInteractionDisabled);
6284
- }, [editor, isInteractionDisabled]);
6341
+ editor.setEditable(!isEditorDisabled);
6342
+ }, [editor, isEditorDisabled]);
6285
6343
  useEffect(() => {
6286
6344
  if (!editor || resolvedContent === void 0) return;
6287
6345
  if (ignorePropValueRef.current && typeof value === "string" && value === lastEmittedValueRef.current) {
@@ -6315,9 +6373,7 @@ var FormulaEditor = ({
6315
6373
  className: "relative",
6316
6374
  "aria-busy": loading,
6317
6375
  onFocus: () => {
6318
- if (editor && !editor.isFocused) {
6319
- editor.chain().focus().run();
6320
- }
6376
+ if (editor && !editor.isFocused) editor.chain().focus().run();
6321
6377
  },
6322
6378
  children: [
6323
6379
  /* @__PURE__ */ jsx(EditorContent, { editor }),
@@ -6329,14 +6385,14 @@ var FormulaEditor = ({
6329
6385
  spinnerClassName: "size-6 text-sus-blue-3"
6330
6386
  }
6331
6387
  ),
6332
- /* @__PURE__ */ jsx(
6388
+ !isEditorReadOnly && /* @__PURE__ */ jsx(
6333
6389
  Button,
6334
6390
  {
6335
6391
  type: "button",
6336
6392
  variant: "ghost",
6337
6393
  size: "icon",
6338
6394
  className: "absolute bottom-2 right-4 h-6 w-6 rounded-full bg-white shadow",
6339
- disabled: isInteractionDisabled,
6395
+ disabled: isEditorDisabled,
6340
6396
  onClick: () => setIsExpanded((prev) => !prev),
6341
6397
  children: isExpanded ? /* @__PURE__ */ jsx(Minimize2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Maximize2, { className: "h-4 w-4" })
6342
6398
  }
@@ -6345,13 +6401,13 @@ var FormulaEditor = ({
6345
6401
  }
6346
6402
  ),
6347
6403
  hasError && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", role: "alert", children: displayError }),
6348
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-2 py-2", children: operators.map((operator) => /* @__PURE__ */ jsx(
6404
+ mode === "edit" && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-2 py-2", children: operators.map((operator) => /* @__PURE__ */ jsx(
6349
6405
  Button,
6350
6406
  {
6351
6407
  type: "button",
6352
6408
  onClick: () => insertOperator(operator.value),
6353
6409
  className: "min-w-10 rounded-sm px-3 bg-sus-blue-3",
6354
- disabled: isInteractionDisabled,
6410
+ disabled: isEditorDisabled,
6355
6411
  children: operator.label
6356
6412
  },
6357
6413
  operator.value
@@ -6547,8 +6603,8 @@ var GridSettingsModal = ({
6547
6603
  }
6548
6604
  }, [isOpen, currentColumns, form]);
6549
6605
  const addColumn = async () => {
6550
- const isValid5 = await trigger("columns");
6551
- if (isValid5) {
6606
+ const isValid6 = await trigger("columns");
6607
+ if (isValid6) {
6552
6608
  append({ id: "" });
6553
6609
  requestAnimationFrame(() => {
6554
6610
  const container = scrollRef.current;