@isi-ui7/bos7-shared 0.2.6 → 0.2.8

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.
@@ -63,6 +63,8 @@ export type CrudListSchema = {
63
63
  showSort?: boolean;
64
64
  /** Enable search bar. Defaults to true. */
65
65
  showSearch?: boolean;
66
+ /** Endpoint for LookupInput (defaults to apiPath if omitted). In Pattern B both DataTable and LookupInput share the same /query endpoint. */
67
+ lookupApiPath?: string;
66
68
  /**
67
69
  * Level 2: schema-driven confirmation modals for custom popup actions.
68
70
  * Keyed by popup menu item id. Actions not listed here fall through to onCustomAction (Level 1).
@@ -19,9 +19,15 @@ export type FieldValidation<TData extends Record<string, unknown>> = {
19
19
  /** Array of validators — compose Rules.* or custom functions. Runs after built-in rules. */
20
20
  validators?: FieldValidatorFn<TData>[];
21
21
  };
22
- export type FormFieldType = "text" | "textarea" | "number" | "date" | "select" | "lookup" | "checkbox" | "toggle" | "detail-rows";
22
+ export type FormFieldType = "text" | "textarea" | "number" | "date" | "datetime" | "select" | "lookup" | "checkbox" | "toggle" | "detail-rows";
23
+ /**
24
+ * Select option. `value` may be a number for numeric-coded columns (e.g.
25
+ * kolektibilitas 1–5); the renderer compares via `String(value)` and the
26
+ * native `<select>` still round-trips strings, so `format`/`onDataPatch`
27
+ * should not assume the runtime type survives an edit.
28
+ */
23
29
  export type FormFieldOption = {
24
- value: string;
30
+ value: string | number;
25
31
  label: string;
26
32
  };
27
33
  export type FormFieldLookupConfig<TData extends Record<string, unknown>> = {
@@ -90,6 +96,13 @@ export type DetailRowsConfig = {
90
96
  newRowFactory: () => Record<string, unknown>;
91
97
  validateRow?: (row: Record<string, unknown>, index: number) => Record<string, string>;
92
98
  maxRows?: number;
99
+ /**
100
+ * Assignment/master-detail mode: rows loaded from the server render
101
+ * delete-only (read-only cells, no lookup picker) while rows added in the
102
+ * current session stay editable. Defaults to `false` (normal edit-in-place).
103
+ * Forwarded to `<EditableTable lockExistingRows>`.
104
+ */
105
+ lockExistingRows?: boolean;
93
106
  };
94
107
  export type FormField<TData extends Record<string, unknown>> = {
95
108
  key: keyof TData;
package/dist/index.js CHANGED
@@ -20872,7 +20872,10 @@ function Nx(e, t, r) {
20872
20872
  return (a == null ? void 0 : a.kind) === "currency" ? {
20873
20873
  type: "currency",
20874
20874
  precision: a.defaultPrecision ?? qa.defaultPrecision
20875
- } : (a == null ? void 0 : a.kind) === "percent" ? { type: "number", decimals: a.maxFractionDigits ?? 2, min: e.minimum, max: e.maximum } : (a == null ? void 0 : a.kind) === "integer" || e.type === "integer" ? { type: "number", decimals: 0, min: e.minimum, max: e.maximum } : n === "number" ? { type: "number", min: e.minimum, max: e.maximum } : n === "select" ? { type: "select", options: mc(e, t, r) ?? [] } : n === "date" ? { type: "date" } : { type: "text", maxLength: e.maxLength, placeholder: t == null ? void 0 : t.placeholder };
20875
+ } : (a == null ? void 0 : a.kind) === "percent" ? { type: "number", decimals: a.maxFractionDigits ?? 2, min: e.minimum, max: e.maximum } : (a == null ? void 0 : a.kind) === "integer" || e.type === "integer" ? { type: "number", decimals: 0, min: e.minimum, max: e.maximum } : n === "number" ? { type: "number", min: e.minimum, max: e.maximum } : n === "select" ? {
20876
+ type: "select",
20877
+ options: (mc(e, t, r) ?? []).map((i) => ({ value: String(i.value), label: i.label }))
20878
+ } : n === "date" ? { type: "date" } : { type: "text", maxLength: e.maxLength, placeholder: t == null ? void 0 : t.placeholder };
20876
20879
  }
20877
20880
  function Ix(e, t) {
20878
20881
  const r = e.items;
@@ -78,7 +78,7 @@ export interface AppShellLayoutProps {
78
78
  notificationsViewAllTarget?: string;
79
79
  hideMenuButton?: boolean;
80
80
  disableAppsMenu?: boolean;
81
- sideNavMode?: "drilldown" | "overlay";
81
+ sideNavMode?: "drilldown" | "overlay" | "expand";
82
82
  sideNavItems?: ShellNavItemLike[];
83
83
  }
84
84
  export declare function AppShellLayout({ children, deps, navItems, markActive, appNameFallback, auth7UiUrlFallback, portalUrlFallback, roleFallback, appSwitchMode, resolveNotificationHref, notificationsViewAllHref, notificationsViewAllTarget, hideMenuButton, disableAppsMenu, sideNavMode, sideNavItems, }: AppShellLayoutProps): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isi-ui7/bos7-shared",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Shared auth7 and layout primitives for bos7 applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -99,14 +99,14 @@
99
99
  "next": "16.1.1",
100
100
  "@carbon/react": "^1.97.0",
101
101
  "@carbon/icons-react": "^11.71.0",
102
- "@isi-ui7/i18n": "0.2.0",
103
102
  "@isi-ui7/corporate-themes": "0.2.4",
104
103
  "@isi-ui7/ui-shell": "0.2.4",
105
- "@isi-ui7/modal-manager": "0.2.1",
106
- "@isi-ui7/realtime": "0.2.2",
107
- "@isi-ui7/data-table": "0.2.2",
108
- "@isi-ui7/lookup-input": "0.2.2",
109
- "@isi-ui7/editable-table": "0.2.2"
104
+ "@isi-ui7/i18n": "0.2.1",
105
+ "@isi-ui7/modal-manager": "0.2.2",
106
+ "@isi-ui7/realtime": "0.2.3",
107
+ "@isi-ui7/data-table": "0.2.3",
108
+ "@isi-ui7/lookup-input": "0.2.3",
109
+ "@isi-ui7/editable-table": "0.2.3"
110
110
  },
111
111
  "dependencies": {
112
112
  "@carbon/icons-react": "^11.71.0",
@@ -119,9 +119,9 @@
119
119
  "@opentelemetry/sdk-node": "^0.57.0",
120
120
  "@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
121
121
  "@opentelemetry/auto-instrumentations-node": "^0.57.0",
122
- "@isi-ui7/data-table": "0.2.2",
123
- "@isi-ui7/lookup-input": "0.2.2",
124
- "@isi-ui7/editable-table": "0.2.2"
122
+ "@isi-ui7/data-table": "0.2.3",
123
+ "@isi-ui7/lookup-input": "0.2.3",
124
+ "@isi-ui7/editable-table": "0.2.3"
125
125
  },
126
126
  "scripts": {
127
127
  "build": "vite build",
package/src/crud-types.ts CHANGED
@@ -44,6 +44,8 @@ export type CrudListSchema = {
44
44
  showSort?: boolean;
45
45
  /** Enable search bar. Defaults to true. */
46
46
  showSearch?: boolean;
47
+ /** Endpoint for LookupInput (defaults to apiPath if omitted). In Pattern B both DataTable and LookupInput share the same /query endpoint. */
48
+ lookupApiPath?: string;
47
49
  /**
48
50
  * Level 2: schema-driven confirmation modals for custom popup actions.
49
51
  * Keyed by popup menu item id. Actions not listed here fall through to onCustomAction (Level 1).
@@ -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;