@globalbrain/sefirot 4.58.0 → 4.59.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.
@@ -63,9 +63,6 @@ export interface Props {
63
63
  // Whether to show advanced filters.
64
64
  canFilter?: boolean
65
65
 
66
- // Whether to show advanced sorting options.
67
- canSort?: boolean
68
-
69
66
  // Whether to hide the condition blocks.
70
67
  hideConditions?: boolean
71
68
 
@@ -129,12 +126,16 @@ export interface Props {
129
126
  // setup, so toggling it afterwards has no effect.
130
127
  urlSync?: boolean
131
128
 
132
- // Enable CRUD editing for the catalog. When set, fields the backend
133
- // marks `showOnUpdate` become inline-editable, clicking a row's id cell
134
- // opens the record sheet (view + per-field edit + delete), and the
135
- // `openCreate()` method / sheet create mode become available. The CRUD
136
- // endpoints are derived from `endpoint` by replacing the trailing
137
- // `/search` with `/update`, `/create`, and `/delete`.
129
+ // Enable CRUD editing for the catalog.
130
+ //
131
+ // Every catalog already renders a **read-only detail sheet**: clicking a
132
+ // row's index-field cell opens the record for viewing (each `showOnDetail`
133
+ // field, read-only). `editable` builds on that same sheet. When set, fields
134
+ // the backend marks `showOnUpdate` become inline-editable in the table, the
135
+ // sheet gains per-field editing and a delete button, and the `openCreate()`
136
+ // method / sheet create mode become available. The CRUD endpoints are derived
137
+ // from `endpoint` by replacing the trailing `/search` with `/update`,
138
+ // `/create`, and `/delete`.
138
139
  //
139
140
  // The row identifier (`indexField`, defaulting to `id`) is never
140
141
  // inline- or sheet-editable on an existing row: optimistically changing
@@ -142,27 +143,28 @@ export interface Props {
142
143
  // follow-up save/delete would address the not-yet-synced new id. It can
143
144
  // still be set on creation (via a `showOnCreate` field).
144
145
  //
145
- // An editable catalog must keep its index field among the rendered columns,
146
- // because the row's sheet opener and therefore the only way to view or
147
- // delete a record is the index-field cell. The default `id` identifier is
148
- // kept when `select` is empty (server defaults), so that case works as-is; a
149
- // *custom* `indexField`, however, must be listed in `select` explicitly (an
150
- // empty/default `select` drops it from the rendered columns, leaving the rows
151
- // with no opener).
146
+ // The sheet opener and therefore the only way to view, edit, or delete a
147
+ // record is the index-field cell, so that field must be a rendered column.
148
+ // The default `id` identifier is kept when `select` is empty (server
149
+ // defaults), so that case works as-is; a *custom* `indexField`, or `id`
150
+ // alongside an explicit `select`, must be listed in `select` explicitly (an
151
+ // empty/default `select` otherwise drops it, leaving the rows with no opener).
152
152
  //
153
153
  // Turning this on also enables `creatable`, `deletable`, and `inlineEditable`
154
154
  // by default; pass `false` to any of them to opt out.
155
155
  //
156
156
  // Pass a predicate `(record) => boolean` instead of `true` to allow editing
157
157
  // only some rows (e.g. from a per-record policy): the inline affordance and
158
- // the sheet's per-field edit are hidden for the rows it rejects.
158
+ // the sheet's per-field edit are hidden for the rows it rejects (the sheet
159
+ // still opens read-only).
159
160
  editable?: boolean | ((record: Record<string, any>) => boolean)
160
161
 
161
162
  // Whether records can be deleted from the sheet. Defaults to enabled for an
162
163
  // editable catalog. Pass `false` to hide the delete button entirely, or a
163
164
  // predicate `(record) => boolean` to allow deleting only some rows (e.g. from
164
- // a per-record policy). Delete is reachable only through the sheet, so this
165
- // has no effect unless the catalog is `editable`.
165
+ // a per-record policy). The delete button lives in the record sheet which
166
+ // opens for read-only viewing on any catalog but stays gated behind edit
167
+ // permission, so this has no effect unless the catalog is `editable`.
166
168
  deletable?: boolean | ((record: Record<string, any>) => boolean)
167
169
 
168
170
  // Whether new records can be created (enables create mode in the sheet and
@@ -182,7 +184,6 @@ export interface Props {
182
184
 
183
185
  const props = withDefaults(defineProps<Props>(), {
184
186
  canFilter: true,
185
- canSort: true,
186
187
  // Default these on so an editable catalog gets create / delete / inline edit
187
188
  // without opting in to each. A boolean prop can't tell "omitted" from `false`
188
189
  // (Vue casts an absent boolean to `false`), so they default on here and the
@@ -623,7 +624,8 @@ const tableIndexField = computed(() => {
623
624
  // address each row; when no `indexField` is configured the identifier
624
625
  // defaults to `id`. When the caller has no concrete select list, nothing
625
626
  // is added either — leaving the request empty lets the server use its own
626
- // defaults. See `indexField` prop docs above.
627
+ // defaults (which include `id`, so the read-only sheet still opens). See
628
+ // `indexField` prop docs above.
627
629
  function withIndexField(fields: string[]): string[] {
628
630
  if (fields.length === 0) { return [] }
629
631
  const field = props.indexField ?? (props.editable ? 'id' : null)
@@ -1029,6 +1031,19 @@ async function refreshCatalog(): Promise<void> {
1029
1031
  await forceRefresh()
1030
1032
  }
1031
1033
 
1034
+ // Close an open *view* sheet when the effective identifier itself changes (an
1035
+ // async `indexField` resolving from the `id` default to `slug`, say): the open
1036
+ // record was keyed under the old field and can't be re-matched to the new one
1037
+ // (its row lacks the new key, so `resolveId` would be `undefined`), which would
1038
+ // let a delete/save act on the wrong — or no — id. Watched on `idField` alone,
1039
+ // not the editable-gated source below: that source can't tell an identifier
1040
+ // change from `editable` merely switching on when both happen in the same flush,
1041
+ // and `editable` flipping on with the identifier unchanged must leave an open
1042
+ // read-only sheet alone (viewing is the default; its record is still valid).
1043
+ watch(idField, () => {
1044
+ if (sheet.state.value && sheetMode.value === 'view') { sheet.off() }
1045
+ })
1046
+
1032
1047
  // When the effective row identifier appears or changes while editing is enabled,
1033
1048
  // the loaded rows won't carry it until a refetch runs — so refetch here. Two
1034
1049
  // cases: `editable` resolving true after mount (async permissions), so
@@ -1037,16 +1052,10 @@ async function refreshCatalog(): Promise<void> {
1037
1052
  // identifier, `rowsCarryIndexField` keeps editing gated off, and selection keys
1038
1053
  // go `undefined` until an unrelated query change. A no-op when the identifier was
1039
1054
  // already present (same request input → cached result reused).
1040
- //
1041
- // Close an open record sheet first: it was identified under the old field and
1042
- // can't be reliably re-matched to the new one (its row lacks the new key, so
1043
- // `resolveId` would be `undefined`), which would let a delete/save act on the
1044
- // wrong — or no — id during and after the refetch.
1045
1055
  watch(
1046
1056
  () => (props.editable ? idField.value : null),
1047
1057
  (field, prev) => {
1048
1058
  if (!field || field === prev) { return }
1049
- if (sheet.state.value && sheetMode.value === 'view') { sheet.off() }
1050
1059
  // Supersede any search still in flight under the previous identifier state: it
1051
1060
  // was issued before the new id/index field, so its rows won't carry the new
1052
1061
  // key. If it resolved after this refetch it would assign those id-less rows
@@ -1468,14 +1477,24 @@ function editEnabled(): boolean {
1468
1477
  return !!props.editable && rowsCarryIndexField.value
1469
1478
  }
1470
1479
 
1480
+ // Catalog-level sheet gate: the read-only detail sheet is reachable on any
1481
+ // catalog — viewing is the default — as soon as the rows carry the index field
1482
+ // the opener resolves an id from. Editing, create, and delete stay gated on
1483
+ // `editable` (via `canEdit` / `canDelete` / the `creatable` getter), so a
1484
+ // non-editable catalog opens the same sheet read-only.
1485
+ function viewEnabled(): boolean {
1486
+ return rowsCarryIndexField.value
1487
+ }
1488
+
1471
1489
  // Per-record refinement: a predicate `editable`/`deletable` decides each row, a
1472
1490
  // boolean applies to all.
1473
1491
  function canEdit(record: Record<string, any>): boolean {
1474
1492
  return editEnabled() && (typeof props.editable === 'function' ? props.editable(record) : true)
1475
1493
  }
1476
1494
 
1477
- // Delete is a stronger action than edit and rides the same editable sheet, so a
1478
- // row must be editable before it can be deleted building on `canEdit` makes a
1495
+ // Delete is a stronger action than edit and lives in the record sheet (whose
1496
+ // delete button only renders when editable), so a row must be editable before it
1497
+ // can be deleted — building on `canEdit` makes a
1479
1498
  // per-record `editable` predicate gate delete too (a row it rejects is never
1480
1499
  // deletable). `deletable` then refines further: on unless explicitly `false`, or
1481
1500
  // its own per-record predicate.
@@ -1486,13 +1505,18 @@ function canDelete(record: Record<string, any>): boolean {
1486
1505
  provideLensEdit({
1487
1506
  // Getters so the injected context tracks prop changes after mount (e.g.
1488
1507
  // permissions resolving async, or a flag toggling `editable` off): LensTable
1489
- // gates inline editing and the id-cell sheet on `edit.editable`.
1508
+ // gates inline editing on `edit.editable` and the id-cell sheet opener on the
1509
+ // broader `edit.viewable`.
1490
1510
  //
1491
1511
  // Also gated on `rowsCarryIndexField`: when `editable` flips true after mount we
1492
1512
  // trigger a refetch to pull in the identifier, but it lands asynchronously —
1493
1513
  // keep editing off until the rows actually carry it, so a save in that window
1494
1514
  // can't `resolveId()` to `undefined`.
1495
1515
  get editable() { return editEnabled() },
1516
+ // `LensTable` gates the id-cell sheet opener on this: the read-only sheet is
1517
+ // reachable on any catalog once the rows carry the index field, independent of
1518
+ // `editable`.
1519
+ get viewable() { return viewEnabled() },
1496
1520
  get creatable() { return !!props.editable && props.creatable },
1497
1521
  // Use the same `__no_entity__` fallback as the search / CRUD requests so slot
1498
1522
  // side-channel saves (which read this) target the same entity, not an empty one.
@@ -1575,7 +1599,6 @@ defineExpose({
1575
1599
  :selected
1576
1600
  :show-query="!!(queryKeys && queryKeys.length > 0)"
1577
1601
  :show-filters="canFilter"
1578
- :show-sort="canSort"
1579
1602
  :is-condition-active="!hideConditions"
1580
1603
  :is-condition-disabled="!hasConditions"
1581
1604
  @search="onQuery"
@@ -1696,7 +1719,7 @@ defineExpose({
1696
1719
  </SModal>
1697
1720
 
1698
1721
  <LensSheet
1699
- v-if="editable && result?.fields"
1722
+ v-if="(editable || rowsCarryIndexField) && result?.fields"
1700
1723
  :open="sheet.state.value"
1701
1724
  :mode="sheetMode"
1702
1725
  :entity="entityName"
@@ -117,11 +117,14 @@ const columns = computedAsync(async () => {
117
117
 
118
118
  const column = field.tableColumn()
119
119
 
120
- // When editing is enabled, clicking the row-identifier cell opens the record
121
- // sheet (view + per-field edit + delete) instead of following a link. Keyed
122
- // off the configured index field for any field type a slug/code identifier
123
- // opens the sheet just like an `id` so other id-type columns (e.g. a
124
- // `company_id` reference link) keep their own navigation.
120
+ // The detail sheet is reachable on any catalog once the rows carry the index
121
+ // field (`edit.viewable`) viewing is the default; `editable` only adds the
122
+ // per-field edit / delete affordances inside it. So clicking the row-identifier
123
+ // cell opens the record sheet (read-only, or editable when the catalog is)
124
+ // instead of following a link. Keyed off the configured index field for any
125
+ // field type — a slug/code identifier opens the sheet just like an `id` — so
126
+ // other id-type columns (e.g. a `company_id` reference link) keep their own
127
+ // navigation.
125
128
  //
126
129
  // Exception: an `id` field whose server value carries a `path` renders that
127
130
  // path as a link; respect it so those rows navigate to the details page instead
@@ -130,7 +133,7 @@ const columns = computedAsync(async () => {
130
133
  // a `link` / `slack_message` identifier) is still turned into the sheet opener,
131
134
  // as is a column whose `cell` is undefined (falls straight through to the opener).
132
135
  if (
133
- edit?.editable
136
+ edit?.viewable
134
137
  && key === edit.indexField
135
138
  ) {
136
139
  const original = column.cell
@@ -424,9 +424,9 @@ function onEditorKeydown(event: KeyboardEvent) {
424
424
  display: flex;
425
425
  flex-direction: column;
426
426
  gap: 8px;
427
- padding: 8px;
427
+ padding: 10px;
428
428
  border: 1px solid var(--c-border);
429
- border-radius: 8px;
429
+ border-radius: 6px;
430
430
  background-color: var(--c-bg-1);
431
431
  box-shadow: var(--shadow-depth-3);
432
432
  }
@@ -147,7 +147,7 @@ const { validation, validate, reset } = useValidation(
147
147
 
148
148
  const editorStyle = computed(() => ({
149
149
  ...inset.value,
150
- width: `${Math.max(bounds.width.value, 240)}px`
150
+ width: `${Math.max(bounds.width.value, 224)}px`
151
151
  }))
152
152
 
153
153
  function start() {
@@ -352,9 +352,9 @@ function onEditorKeydown(event: KeyboardEvent) {
352
352
  display: flex;
353
353
  flex-direction: column;
354
354
  gap: 8px;
355
- padding: 8px;
355
+ padding: 10px;
356
356
  border: 1px solid var(--c-border);
357
- border-radius: 8px;
357
+ border-radius: 6px;
358
358
  background-color: var(--c-bg-1);
359
359
  box-shadow: var(--shadow-depth-3);
360
360
  }
@@ -10,6 +10,15 @@ export interface LensEditContext {
10
10
  /** Whether the catalog has editing enabled at all. */
11
11
  editable: boolean
12
12
 
13
+ /**
14
+ * Whether the detail sheet is reachable. Viewing is the default: true on any
15
+ * catalog once the rows carry the index field the opener resolves an id from,
16
+ * independent of `editable`. The index-field cell opens the sheet on this gate,
17
+ * while the mutation affordances inside stay gated on `canEdit` / `canDelete` /
18
+ * `creatable`, so a non-editable catalog opens a read-only sheet.
19
+ */
20
+ viewable: boolean
21
+
13
22
  /** Whether new records may be created. */
14
23
  creatable: boolean
15
24
 
@@ -94,3 +94,9 @@ function onChange(value: T) {
94
94
  <template v-if="$slots.info" #info><slot name="info" /></template>
95
95
  </SInputBase>
96
96
  </template>
97
+
98
+ <style scoped>
99
+ .container {
100
+ padding-left: 2px;
101
+ }
102
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "4.58.0",
3
+ "version": "4.59.0",
4
4
  "description": "Vue Components for Global Brain Design System.",
5
5
  "keywords": [
6
6
  "components",