@isi-ui7/bos7-shared 0.2.6 → 0.2.7

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.
@@ -12,6 +12,7 @@ import {
12
12
  Toggle,
13
13
  } from "@carbon/react";
14
14
  import { LookupInput } from "@isi-ui7/lookup-input";
15
+ import type { T_LookupTblStruct } from "@isi-ui7/lookup-input";
15
16
  import { EditableTable } from "@isi-ui7/editable-table";
16
17
  import { useI18n } from "@isi-ui7/i18n";
17
18
  import type { Ui7Locale } from "@isi-ui7/i18n";
@@ -91,6 +92,39 @@ function makeDisplayRenderer(
91
92
  }
92
93
  return value ? labels.toggleOn : labels.toggleOff;
93
94
  }
95
+ if (field.type === "select") {
96
+ // Resolve the option label so read-only mode shows "Active" rather than
97
+ // the stored code "active". Falls back to the raw value when no option
98
+ // matches (e.g. a legacy value no longer in the options list).
99
+ const match = (field.options ?? []).find((o) => String(o.value) === asString(value));
100
+ if (match) return match.label;
101
+ return asString(value) || labels.emptyValue;
102
+ }
103
+ if (field.type === "date") {
104
+ const raw = asString(value);
105
+ if (!raw) return labels.emptyValue;
106
+ const d = new Date(raw);
107
+ if (Number.isNaN(d.getTime())) return raw;
108
+ const nloc = locale === "en" ? "en-GB" : "id-ID";
109
+ return new Intl.DateTimeFormat(nloc, { day: "2-digit", month: "short", year: "numeric" }).format(d);
110
+ }
111
+ if (field.type === "datetime") {
112
+ // ISO timestamp → locale-aware "18 Jun 2026, 10:23" (24h). Echoes the
113
+ // raw value if unparseable. Edit mode falls through to a text input.
114
+ const raw = asString(value);
115
+ if (!raw) return labels.emptyValue;
116
+ const d = new Date(raw);
117
+ if (Number.isNaN(d.getTime())) return raw;
118
+ const nloc = locale === "en" ? "en-GB" : "id-ID";
119
+ return new Intl.DateTimeFormat(nloc, {
120
+ day: "2-digit",
121
+ month: "short",
122
+ year: "numeric",
123
+ hour: "2-digit",
124
+ minute: "2-digit",
125
+ hour12: false,
126
+ }).format(d);
127
+ }
94
128
  if (field.type === "lookup") {
95
129
  // View/read-only mode mirrors the LookupInput initialDisplay logic:
96
130
  // when initialDisplayFields is set and all referenced FormData
@@ -338,7 +372,6 @@ function NumericField({
338
372
  size={size}
339
373
  disabled={disabled}
340
374
  inputMode={numericKind === "integer" ? "numeric" : "decimal"}
341
- light
342
375
  invalid={invalid}
343
376
  invalidText={invalidText}
344
377
  placeholder={placeholder}
@@ -461,7 +494,10 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
461
494
  style={{
462
495
  padding: "1rem 1.125rem",
463
496
  border: "1px solid var(--cds-border-subtle, #e0e0e0)",
464
- background: "var(--cds-layer-01, #ffffff)",
497
+ // Section sits flat on the page background (white) and is set
498
+ // apart by its border line only — fields carry the contrasting
499
+ // fill instead (see read-only box + non-light Carbon inputs).
500
+ background: "var(--cds-background, #ffffff)",
465
501
  }}
466
502
  >
467
503
  {showSectionHeader && (section.title || section.description) ? (
@@ -570,7 +606,40 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
570
606
  newRowFactory={detail.newRowFactory}
571
607
  maxRows={detail.maxRows}
572
608
  readOnly={effectiveReadonly}
609
+ lockExistingRows={detail.lockExistingRows}
573
610
  validateRow={detail.validateRow}
611
+ lookupCellRenderer={(args) => {
612
+ const ct = args.column.columnType;
613
+ if (ct.type !== "lookup") return null;
614
+ let initialDisplay: string | undefined;
615
+ if (ct.initialDisplayFields?.length) {
616
+ const sep = ct.initialDisplaySeparator ?? " ";
617
+ const parts = ct.initialDisplayFields.map((k) => asString(args.row[k]));
618
+ if (parts.every((p) => p !== "")) initialDisplay = parts.join(sep);
619
+ }
620
+ return (
621
+ <LookupInput
622
+ id={`et-${String(field.key)}-${args.rowIndex}-${args.column.field}`}
623
+ labelText=""
624
+ value={args.value}
625
+ initialDisplay={initialDisplay}
626
+ onChange={() => {}}
627
+ onDataSelected={(picked: Record<string, unknown>) => {
628
+ const patch = ct.patch
629
+ ? ct.patch(picked)
630
+ : { [args.column.field]: asString(picked[args.column.field]) };
631
+ args.onPatch(patch);
632
+ }}
633
+ dataSource="api"
634
+ lookupAPI={ct.lookupAPI}
635
+ lookupDataStructure={ct.lookupDataStructure as Record<string, T_LookupTblStruct>}
636
+ lookupFieldTitles={ct.lookupFieldTitles ?? {}}
637
+ displayFieldNames={ct.displayFieldNames ?? []}
638
+ popupWidthPx={ct.popupWidthPx}
639
+ readOnly={args.readOnly}
640
+ />
641
+ );
642
+ }}
574
643
  />
575
644
  </FieldShell>
576
645
  </div>,
@@ -585,12 +654,18 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
585
654
  <div
586
655
  className={`${UI7_FORM_VISUAL_CLASSNAMES.control} ${UI7_FORM_VISUAL_CLASSNAMES.controlReadonly}`}
587
656
  style={{
588
- minHeight: UI7_FORM_VISUAL_TOKENS.controlHeight,
657
+ // Match the editable control height for the active
658
+ // density (compact = 2rem) so read-only fields line
659
+ // up with create/edit inputs instead of being 40px.
660
+ minHeight: dt.controlHeight,
589
661
  display: "flex",
590
662
  alignItems: "center",
591
663
  fontSize: UI7_FORM_VISUAL_TOKENS.inputFontSize,
592
664
  lineHeight: UI7_FORM_VISUAL_TOKENS.inputLineHeight,
593
665
  background: "var(--ui7-form-readonly-background, #f4f4f4)",
666
+ // No bottom rule for non-editable fields — the gray
667
+ // fill alone sets them apart from the white section;
668
+ // an input-style underline would imply editability.
594
669
  cursor: "var(--ui7-form-readonly-cursor, default)",
595
670
  width: "100%",
596
671
  padding: "0 0.75rem",
@@ -695,7 +770,6 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
695
770
  value={asString(rawValue)}
696
771
  onChange={(e) => setValue(field.key, e.target.value as TData[keyof TData])}
697
772
  disabled={disabled}
698
- light
699
773
  invalid={Boolean(invalidText)}
700
774
  invalidText={invalidText}
701
775
  >
@@ -873,7 +947,6 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
873
947
  disabled={disabled}
874
948
  maxLength={field.maxLength}
875
949
  inputMode="tel"
876
- light
877
950
  invalid={Boolean(invalidText)}
878
951
  invalidText={invalidText}
879
952
  placeholder={field.placeholder}
@@ -897,7 +970,6 @@ export function SchemaFormRenderer<TData extends Record<string, unknown>>({
897
970
  disabled={disabled}
898
971
  maxLength={field.maxLength}
899
972
  inputMode={field.inputMode}
900
- light
901
973
  invalid={Boolean(invalidText)}
902
974
  invalidText={invalidText}
903
975
  placeholder={field.placeholder}
package/src/form-types.ts CHANGED
@@ -31,11 +31,17 @@ export type FieldValidation<TData extends Record<string, unknown>> = {
31
31
  // ── Field ─────────────────────────────────────────────────────────────────────
32
32
 
33
33
  export type FormFieldType =
34
- | "text" | "textarea" | "number" | "date"
34
+ | "text" | "textarea" | "number" | "date" | "datetime"
35
35
  | "select" | "lookup" | "checkbox" | "toggle"
36
36
  | "detail-rows";
37
37
 
38
- export type FormFieldOption = { value: string; label: string };
38
+ /**
39
+ * Select option. `value` may be a number for numeric-coded columns (e.g.
40
+ * kolektibilitas 1–5); the renderer compares via `String(value)` and the
41
+ * native `<select>` still round-trips strings, so `format`/`onDataPatch`
42
+ * should not assume the runtime type survives an edit.
43
+ */
44
+ export type FormFieldOption = { value: string | number; label: string };
39
45
 
40
46
  export type FormFieldLookupConfig<TData extends Record<string, unknown>> = {
41
47
  lookupAPI: string;
@@ -98,6 +104,13 @@ export type DetailRowsConfig = {
98
104
  index: number,
99
105
  ) => Record<string, string>;
100
106
  maxRows?: number;
107
+ /**
108
+ * Assignment/master-detail mode: rows loaded from the server render
109
+ * delete-only (read-only cells, no lookup picker) while rows added in the
110
+ * current session stay editable. Defaults to `false` (normal edit-in-place).
111
+ * Forwarded to `<EditableTable lockExistingRows>`.
112
+ */
113
+ lockExistingRows?: boolean;
101
114
  };
102
115
 
103
116
  export type FormField<TData extends Record<string, unknown>> = {
@@ -250,7 +250,12 @@ function buildColumnType(schema: JSONSchema, xui: XUi | undefined, t: T): Editab
250
250
  return { type: "number", min: schema.minimum, max: schema.maximum };
251
251
  }
252
252
  if (type === "select") {
253
- return { type: "select", options: buildOptions(schema, xui, t) ?? [] };
253
+ // EditableTable select options are value:string only coerce, since
254
+ // FormFieldOption.value may be a number.
255
+ return {
256
+ type: "select",
257
+ options: (buildOptions(schema, xui, t) ?? []).map((o) => ({ value: String(o.value), label: o.label })),
258
+ };
254
259
  }
255
260
  if (type === "date") return { type: "date" };
256
261
  return { type: "text", maxLength: schema.maxLength, placeholder: xui?.placeholder };
@@ -100,7 +100,7 @@ export interface AppShellLayoutProps {
100
100
  notificationsViewAllTarget?: string;
101
101
  hideMenuButton?: boolean;
102
102
  disableAppsMenu?: boolean;
103
- sideNavMode?: "drilldown" | "overlay";
103
+ sideNavMode?: "drilldown" | "overlay" | "expand";
104
104
  sideNavItems?: ShellNavItemLike[];
105
105
  }
106
106
 
@@ -364,7 +364,7 @@ function ShellInner({
364
364
  notificationsViewAllTarget: string;
365
365
  hideMenuButton: boolean;
366
366
  disableAppsMenu: boolean;
367
- sideNavMode: "drilldown" | "overlay";
367
+ sideNavMode: "drilldown" | "overlay" | "expand";
368
368
  sideNavItems: ShellNavItemLike[];
369
369
  notifBadgeCount: number;
370
370
  userName?: string | null;