@happyvertical/smrt-content 0.43.2 → 0.43.4

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.
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Shared content-list data adapter.
3
+ *
4
+ * Every ContentList presentation (grid, detailed, compact) and every
5
+ * agent-addressable data surface resolves rows, columns, filters, sorting, and
6
+ * per-row action eligibility here, so switching presentation can never change
7
+ * which rows exist or what may be done to them.
8
+ *
9
+ * The module owns no transport, no DOM, and no routing: callers supply the
10
+ * content array, a `getViewHref` resolver, and a `DataTableController` that
11
+ * holds the serializable view state.
12
+ *
13
+ * Query modes are `manual`: this adapter, not the renderer, applies search,
14
+ * filters, sorting, and paging, so a card view and the compact table can never
15
+ * disagree about the visible rows. #2452 replaces the local implementation of
16
+ * that transform with a server query behind the same contract.
17
+ */
18
+ import { compareDataTableRowIds, createDataTableController, defaultSort, getNestedValue, } from '@happyvertical/smrt-ui/data';
19
+ /** Stable surface identity for the default mounted content list. */
20
+ export const CONTENT_LIST_SURFACE_ID = 'content-list';
21
+ /** Descriptor and view-state schema version owned by this adapter. */
22
+ export const CONTENT_LIST_SCHEMA_VERSION = 1;
23
+ /** Row identity column. Selection and expansion address rows by this value. */
24
+ export const CONTENT_LIST_ROW_KEY = 'id';
25
+ /** Stable filter ids dispatched by the toolbar and accepted from a surface. */
26
+ export const CONTENT_LIST_TYPE_FILTER_ID = 'type';
27
+ export const CONTENT_LIST_STATUS_FILTER_ID = 'status';
28
+ /** Prefix for rows the source array could not identify durably. */
29
+ const UNIDENTIFIED_ROW_PREFIX = 'content-list:unidentified:';
30
+ /** Columns rendered by the compact table and published to a data surface. */
31
+ export const CONTENT_LIST_VISIBLE_COLUMN_IDS = [
32
+ 'type',
33
+ 'title',
34
+ 'author',
35
+ 'status',
36
+ 'state',
37
+ 'publish',
38
+ 'updated',
39
+ 'site',
40
+ ];
41
+ /**
42
+ * `description` is searched but never rendered or published: it participates in
43
+ * local search so the rebuilt list keeps the legacy search reach.
44
+ */
45
+ export const CONTENT_LIST_HIDDEN_COLUMN_IDS = [
46
+ 'description',
47
+ ];
48
+ export const CONTENT_LIST_COLUMN_IDS = [
49
+ ...CONTENT_LIST_VISIBLE_COLUMN_IDS,
50
+ ...CONTENT_LIST_HIDDEN_COLUMN_IDS,
51
+ ];
52
+ /**
53
+ * Structural columns the compact table owns. They carry no query capability and
54
+ * are never published to a data surface, but the controller still has to know
55
+ * them: column order is reconciled from its known column ids, so leaving them
56
+ * out would push selection and actions behind every data column.
57
+ */
58
+ export const CONTENT_LIST_SELECTION_COLUMN_ID = 'select';
59
+ export const CONTENT_LIST_ACTIONS_COLUMN_ID = 'actions';
60
+ /** Every column of the compact table, in render order. */
61
+ export const CONTENT_LIST_TABLE_COLUMN_IDS = [
62
+ CONTENT_LIST_SELECTION_COLUMN_ID,
63
+ ...CONTENT_LIST_COLUMN_IDS,
64
+ CONTENT_LIST_ACTIONS_COLUMN_ID,
65
+ ];
66
+ const DEFAULT_COLUMN_LABELS = {
67
+ type: 'Type',
68
+ title: 'Title',
69
+ author: 'Author',
70
+ status: 'Status',
71
+ state: 'State',
72
+ publish: 'Publish',
73
+ updated: 'Updated',
74
+ site: 'Site',
75
+ description: 'Description',
76
+ };
77
+ /**
78
+ * The `ContentData` field each published column reads. Column ids are stable
79
+ * public identifiers and do not always match the field name, so the mapping is
80
+ * explicit — advertising a field that does not exist would mislead an adapter
81
+ * that maps a descriptor onto the model. `site` is derived from `url`/`source`
82
+ * and therefore names no single field.
83
+ */
84
+ const CONTENT_LIST_COLUMN_FIELD_NAMES = {
85
+ type: 'type',
86
+ title: 'title',
87
+ author: 'author',
88
+ status: 'status',
89
+ state: 'state',
90
+ publish: 'publish_date',
91
+ updated: 'updatedAt',
92
+ };
93
+ const DEFAULT_ACTION_LABELS = {
94
+ view: 'View',
95
+ edit: 'Edit',
96
+ delete: 'Delete',
97
+ };
98
+ const DEFAULT_SURFACE_LIMITS = {
99
+ maxQueryRows: 200,
100
+ maxQueryBytes: 50_000,
101
+ maxSelectionSize: 200,
102
+ };
103
+ /** Local table commands a mounted content list accepts from a data surface. */
104
+ const CONTENT_LIST_CONTROLS = [
105
+ { id: 'set-search', label: 'Search contents' },
106
+ { id: 'set-filters', label: 'Filter contents' },
107
+ { id: 'set-sorting', label: 'Sort contents' },
108
+ { id: 'toggle-sorting', label: 'Toggle column sorting' },
109
+ { id: 'set-page', label: 'Change page' },
110
+ { id: 'set-page-size', label: 'Change page size' },
111
+ { id: 'set-selected-rows', label: 'Replace the row selection' },
112
+ { id: 'toggle-row-selection', label: 'Toggle one row selection' },
113
+ { id: 'reset', label: 'Reset the list view' },
114
+ { id: 'focus', label: 'Focus the list' },
115
+ { id: 'reveal', label: 'Scroll the list into view' },
116
+ { id: 'highlight', label: 'Highlight the list' },
117
+ ];
118
+ function getTextValue(value) {
119
+ return typeof value === 'string' ? value : '';
120
+ }
121
+ /** Normalizes a content type for filtering; unknown values become `content`. */
122
+ export function normalizeContentType(value) {
123
+ return getTextValue(value).trim().toLowerCase() || 'content';
124
+ }
125
+ /** Normalizes a status/state token for filtering and badge variants. */
126
+ export function normalizeContentToken(value) {
127
+ return getTextValue(value).trim().toLowerCase();
128
+ }
129
+ export function contentTypeLabel(value) {
130
+ switch (normalizeContentType(value)) {
131
+ case 'article':
132
+ return 'Article';
133
+ case 'mirror':
134
+ return 'Mirror';
135
+ case 'document':
136
+ return 'Document';
137
+ default:
138
+ return 'Content';
139
+ }
140
+ }
141
+ /** Badge variant for a status; unrecognized statuses degrade to `unknown`. */
142
+ export function contentStatusVariant(value) {
143
+ switch (normalizeContentToken(value)) {
144
+ case 'published':
145
+ return 'published';
146
+ case 'draft':
147
+ return 'draft';
148
+ case 'archived':
149
+ return 'archived';
150
+ default:
151
+ return 'unknown';
152
+ }
153
+ }
154
+ /** Badge variant for a workflow state; unrecognized states degrade to `unknown`. */
155
+ export function contentStateVariant(value) {
156
+ switch (normalizeContentToken(value)) {
157
+ case 'highlighted':
158
+ return 'highlighted';
159
+ case 'active':
160
+ return 'active';
161
+ case 'deprecated':
162
+ return 'deprecated';
163
+ default:
164
+ return 'unknown';
165
+ }
166
+ }
167
+ function hostnameOf(url) {
168
+ try {
169
+ return new URL(url).hostname;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ function resolveSite(content) {
176
+ const url = getTextValue(content.url);
177
+ if (url) {
178
+ const hostname = hostnameOf(url);
179
+ if (hostname)
180
+ return hostname;
181
+ }
182
+ return getTextValue(content.source);
183
+ }
184
+ /** Renders an ISO timestamp as a stable calendar date, or echoes free text. */
185
+ export function formatContentListDate(value) {
186
+ const text = getTextValue(value);
187
+ if (!text)
188
+ return '';
189
+ const parsed = new Date(text);
190
+ return Number.isNaN(parsed.getTime())
191
+ ? text
192
+ : parsed.toISOString().slice(0, 10);
193
+ }
194
+ /**
195
+ * Resolves the rows every presentation renders.
196
+ *
197
+ * A content without a durable id — or one repeating an id an earlier row
198
+ * already claimed — still renders, keyed by its position, but is marked
199
+ * unidentified so it never enters a selection that outlives the current order.
200
+ */
201
+ export function toContentListRows(contents) {
202
+ const claimed = new Set();
203
+ return contents.map((content, index) => {
204
+ const declaredId = getTextValue(content.id);
205
+ const identified = declaredId.length > 0 && !claimed.has(declaredId);
206
+ if (identified)
207
+ claimed.add(declaredId);
208
+ const type = normalizeContentType(content.type);
209
+ const status = normalizeContentToken(content.status);
210
+ const state = normalizeContentToken(content.state);
211
+ const publish = getTextValue(content.publish_date);
212
+ const updated = getTextValue(content.updatedAt);
213
+ return {
214
+ id: identified ? declaredId : `${UNIDENTIFIED_ROW_PREFIX}${index}`,
215
+ identified,
216
+ content,
217
+ type,
218
+ typeLabel: contentTypeLabel(content.type),
219
+ title: getTextValue(content.title) || 'Untitled content',
220
+ description: getTextValue(content.description),
221
+ author: getTextValue(content.author),
222
+ status,
223
+ statusLabel: getTextValue(content.status),
224
+ state,
225
+ stateLabel: getTextValue(content.state),
226
+ publish,
227
+ publishLabel: formatContentListDate(publish),
228
+ updated,
229
+ updatedLabel: formatContentListDate(updated),
230
+ site: resolveSite(content),
231
+ };
232
+ });
233
+ }
234
+ /** Row ids that may take part in selection. Unidentified rows are excluded. */
235
+ export function selectableContentListRowIds(rows) {
236
+ return rows.filter((row) => row.identified).map((row) => row.id);
237
+ }
238
+ /**
239
+ * Resolves a selection to rows. Unidentified rows are dropped rather than
240
+ * throwing, so one malformed content can never break a bulk workflow (#2453).
241
+ */
242
+ export function resolveSelectedContentListRows(rows, state) {
243
+ const selected = new Set(state.selectedRowIds.map((rowId) => String(rowId)));
244
+ return rows.filter((row) => row.identified && selected.has(String(row.id)));
245
+ }
246
+ /** The durable contents behind the current selection. */
247
+ export function resolveSelectedContents(rows, state) {
248
+ return resolveSelectedContentListRows(rows, state).map((row) => row.content);
249
+ }
250
+ /**
251
+ * Column metadata shared by the compact table and the local query helpers.
252
+ * Callers add cell snippets; they must not change ids, accessors, or the
253
+ * searchable/filterable/sortable flags the descriptor is derived from.
254
+ */
255
+ export function buildContentListColumns(labels = {}) {
256
+ const label = (id) => labels[id] ?? DEFAULT_COLUMN_LABELS[id];
257
+ return [
258
+ {
259
+ id: 'type',
260
+ label: label('type'),
261
+ accessor: 'type',
262
+ sortable: true,
263
+ searchable: false,
264
+ width: '8rem',
265
+ },
266
+ {
267
+ id: 'title',
268
+ label: label('title'),
269
+ accessor: 'title',
270
+ sortable: true,
271
+ },
272
+ {
273
+ id: 'author',
274
+ label: label('author'),
275
+ accessor: 'author',
276
+ sortable: true,
277
+ },
278
+ {
279
+ id: 'status',
280
+ label: label('status'),
281
+ accessor: 'status',
282
+ sortable: true,
283
+ searchable: false,
284
+ role: 'status',
285
+ },
286
+ {
287
+ id: 'state',
288
+ label: label('state'),
289
+ accessor: 'state',
290
+ sortable: true,
291
+ searchable: false,
292
+ role: 'status',
293
+ },
294
+ {
295
+ id: 'publish',
296
+ label: label('publish'),
297
+ accessor: 'publish',
298
+ sortable: true,
299
+ searchable: false,
300
+ },
301
+ {
302
+ id: 'updated',
303
+ label: label('updated'),
304
+ accessor: 'updated',
305
+ sortable: true,
306
+ searchable: false,
307
+ },
308
+ {
309
+ id: 'site',
310
+ label: label('site'),
311
+ accessor: 'site',
312
+ sortable: true,
313
+ searchable: false,
314
+ },
315
+ {
316
+ id: 'description',
317
+ label: label('description'),
318
+ accessor: 'description',
319
+ sortable: false,
320
+ filterable: false,
321
+ hidden: true,
322
+ },
323
+ ];
324
+ }
325
+ /**
326
+ * One normalizer per filter column, so a filter built by the toolbar, by the
327
+ * `type` lock, and by a restored view all compare equal.
328
+ */
329
+ export function normalizeContentListFilterValue(columnId, value) {
330
+ return columnId === CONTENT_LIST_TYPE_FILTER_ID
331
+ ? normalizeContentType(value)
332
+ : normalizeContentToken(value);
333
+ }
334
+ /** Builds the declarative filter set for the two toolbar filters. */
335
+ export function contentListFilters(values) {
336
+ const filters = [];
337
+ if (values.type?.trim()) {
338
+ filters.push({
339
+ columnId: CONTENT_LIST_TYPE_FILTER_ID,
340
+ operator: 'equals',
341
+ value: normalizeContentListFilterValue(CONTENT_LIST_TYPE_FILTER_ID, values.type),
342
+ });
343
+ }
344
+ if (values.status?.trim()) {
345
+ filters.push({
346
+ columnId: CONTENT_LIST_STATUS_FILTER_ID,
347
+ operator: 'equals',
348
+ value: normalizeContentListFilterValue(CONTENT_LIST_STATUS_FILTER_ID, values.status),
349
+ });
350
+ }
351
+ return filters;
352
+ }
353
+ /** Reads one filter's value, or `null` when the filter is not applied. */
354
+ export function readContentListFilter(state, columnId) {
355
+ const filter = state.filters.find((candidate) => candidate.columnId === columnId);
356
+ return typeof filter?.value === 'string' ? filter.value : null;
357
+ }
358
+ /**
359
+ * True when a column is filtered to exactly the given value and nothing else.
360
+ *
361
+ * A locked filter has to be checked as a whole rather than by reading one
362
+ * value: a `notEquals` on the locked value, or a second filter on the same
363
+ * column, would otherwise satisfy a value-only comparison while selecting rows
364
+ * the lock is meant to exclude.
365
+ */
366
+ export function isContentListFilterExactly(state, columnId, value) {
367
+ const applied = state.filters.filter((filter) => filter.columnId === columnId);
368
+ return (applied.length === 1 &&
369
+ applied[0].operator === 'equals' &&
370
+ applied[0].value === normalizeContentListFilterValue(columnId, value));
371
+ }
372
+ /**
373
+ * Replaces one filter while preserving the others, so locking the type filter
374
+ * never discards a status the operator chose.
375
+ *
376
+ * A blank value clears the filter: normalizing whitespace into an `equals ''`
377
+ * filter would silently exclude every row instead.
378
+ */
379
+ export function applyContentListFilter(controller, columnId, value) {
380
+ const current = controller
381
+ .getState()
382
+ .filters.filter((filter) => filter.columnId !== columnId);
383
+ const requested = typeof value === 'string' ? value.trim() : '';
384
+ const next = requested
385
+ ? [
386
+ ...current,
387
+ {
388
+ columnId,
389
+ operator: 'equals',
390
+ value: normalizeContentListFilterValue(columnId, requested),
391
+ },
392
+ ]
393
+ : current;
394
+ controller.dispatch({ type: 'setFilters', filters: next });
395
+ }
396
+ export function createContentListController(options = {}) {
397
+ return createDataTableController({
398
+ columnIds: CONTENT_LIST_TABLE_COLUMN_IDS,
399
+ hiddenColumnIds: CONTENT_LIST_HIDDEN_COLUMN_IDS,
400
+ // This adapter owns the transform in every presentation, so the renderer
401
+ // must not apply a second, subtly different pass over the same rows.
402
+ modes: { filtering: 'manual', sorting: 'manual', pagination: 'manual' },
403
+ initialState: {
404
+ search: options.search ?? '',
405
+ filters: contentListFilters({
406
+ type: options.type ?? null,
407
+ status: options.status ?? null,
408
+ }),
409
+ sorting: options.sorting ? [...options.sorting] : [],
410
+ pageSize: options.pageSize ?? null,
411
+ },
412
+ onStateChange: options.onStateChange,
413
+ });
414
+ }
415
+ function textValue(value) {
416
+ if (value === null || value === undefined)
417
+ return '';
418
+ if (typeof value === 'string')
419
+ return value.toLowerCase();
420
+ if (typeof value === 'number' || typeof value === 'boolean') {
421
+ return String(value).toLowerCase();
422
+ }
423
+ return JSON.stringify(value)?.toLowerCase() ?? '';
424
+ }
425
+ function sameFilterValue(left, right) {
426
+ if (left === right)
427
+ return true;
428
+ if (left === null ||
429
+ left === undefined ||
430
+ right === null ||
431
+ right === undefined) {
432
+ return false;
433
+ }
434
+ return textValue(left) === textValue(right);
435
+ }
436
+ function compareFilterValues(left, right) {
437
+ if (typeof left === 'number' && typeof right === 'number') {
438
+ return left === right ? 0 : left < right ? -1 : 1;
439
+ }
440
+ const leftText = textValue(left);
441
+ const rightText = textValue(right);
442
+ return leftText === rightText ? 0 : leftText < rightText ? -1 : 1;
443
+ }
444
+ /**
445
+ * The single declarative-filter evaluator. It follows DataTable's operator
446
+ * semantics so a persisted or agent-issued filter behaves the same here as it
447
+ * would in a locally filtered table.
448
+ */
449
+ function matchesContentListFilter(row, column, filter) {
450
+ if (column.filterable === false)
451
+ return true;
452
+ const value = getNestedValue(row, String(column.accessor ?? column.id));
453
+ const expected = filter.value;
454
+ const valueText = textValue(value);
455
+ const expectedText = textValue(expected);
456
+ switch (filter.operator) {
457
+ case 'equals':
458
+ return sameFilterValue(value, expected);
459
+ case 'notEquals':
460
+ return !sameFilterValue(value, expected);
461
+ case 'contains':
462
+ return valueText.includes(expectedText);
463
+ case 'notContains':
464
+ return !valueText.includes(expectedText);
465
+ case 'startsWith':
466
+ return valueText.startsWith(expectedText);
467
+ case 'endsWith':
468
+ return valueText.endsWith(expectedText);
469
+ case 'in':
470
+ return (Array.isArray(expected) &&
471
+ expected.some((entry) => sameFilterValue(value, entry)));
472
+ case 'notIn':
473
+ return (Array.isArray(expected) &&
474
+ !expected.some((entry) => sameFilterValue(value, entry)));
475
+ case 'gt':
476
+ return compareFilterValues(value, expected) > 0;
477
+ case 'gte':
478
+ return compareFilterValues(value, expected) >= 0;
479
+ case 'lt':
480
+ return compareFilterValues(value, expected) < 0;
481
+ case 'lte':
482
+ return compareFilterValues(value, expected) <= 0;
483
+ case 'isNull':
484
+ return value === null || value === undefined;
485
+ case 'isNotNull':
486
+ return value !== null && value !== undefined;
487
+ default:
488
+ return false;
489
+ }
490
+ }
491
+ /**
492
+ * Applies search, declarative filters, and sorting once for every
493
+ * presentation. Pagination stays separate so a caller can report the unpaged
494
+ * result count (DataTable's `totalRows`) alongside the current page.
495
+ */
496
+ export function selectContentListRows(rows, state, columns = buildContentListColumns()) {
497
+ const search = state.search.trim().toLowerCase();
498
+ const filtered = rows.filter((row) => {
499
+ if (search &&
500
+ !columns.some((column) => column.searchable !== false &&
501
+ textValue(getNestedValue(row, String(column.accessor ?? column.id))).includes(search))) {
502
+ return false;
503
+ }
504
+ return state.filters.every((filter) => {
505
+ const column = columns.find((candidate) => candidate.id === filter.columnId);
506
+ return Boolean(column && matchesContentListFilter(row, column, filter));
507
+ });
508
+ });
509
+ if (state.sorting.length === 0)
510
+ return filtered;
511
+ return filtered.slice().sort((left, right) => {
512
+ for (const rule of state.sorting) {
513
+ const column = columns.find((candidate) => candidate.id === rule.columnId);
514
+ if (!column)
515
+ continue;
516
+ const result = defaultSort(left, right, String(column.accessor ?? column.id), rule.direction);
517
+ if (result !== 0)
518
+ return result;
519
+ }
520
+ return compareDataTableRowIds(left.id, right.id);
521
+ });
522
+ }
523
+ /** Slices the current page. An unset page size keeps the whole result. */
524
+ export function paginateContentListRows(rows, state) {
525
+ if (!state.pageSize)
526
+ return [...rows];
527
+ const start = (state.page - 1) * state.pageSize;
528
+ return rows.slice(start, start + state.pageSize);
529
+ }
530
+ /**
531
+ * Resolves the host-owned view link. An unknown subtype — or a resolver that
532
+ * throws on one — degrades to plain text rather than a dead link.
533
+ */
534
+ export function resolveContentHref(content, getViewHref) {
535
+ if (!getViewHref)
536
+ return null;
537
+ try {
538
+ const href = getViewHref(content);
539
+ return typeof href === 'string' && href.length > 0 ? href : null;
540
+ }
541
+ catch {
542
+ return null;
543
+ }
544
+ }
545
+ /**
546
+ * Per-row action eligibility, used identically by all three presentations.
547
+ * `view` requires a resolvable href, so unpublished content never renders one.
548
+ */
549
+ export function contentListRowActions(row, options = {}) {
550
+ const actions = [];
551
+ if (resolveContentHref(row.content, options.getViewHref))
552
+ actions.push('view');
553
+ if (options.canEdit !== false)
554
+ actions.push('edit');
555
+ if (options.canDelete !== false)
556
+ actions.push('delete');
557
+ return actions;
558
+ }
559
+ function surfaceColumn(id, labels, order, column) {
560
+ const capabilities = [
561
+ 'read',
562
+ 'filter',
563
+ 'sort',
564
+ 'project',
565
+ ];
566
+ if (column.searchable !== false)
567
+ capabilities.push('search');
568
+ const fieldName = CONTENT_LIST_COLUMN_FIELD_NAMES[id];
569
+ return {
570
+ id,
571
+ label: labels[id] ?? DEFAULT_COLUMN_LABELS[id],
572
+ capabilities,
573
+ ...(fieldName ? { fieldName } : {}),
574
+ visibility: 'basic',
575
+ order,
576
+ role: column.role === 'status' ? 'status' : 'data',
577
+ };
578
+ }
579
+ /**
580
+ * Builds the discovery contract for a mounted content list. Only rendered
581
+ * columns are published: the search-only `description` column stays private.
582
+ */
583
+ export function buildContentListSurfaceDescriptor(options = {}) {
584
+ const columnLabels = options.columnLabels ?? {};
585
+ const actionLabels = options.actionLabels ?? {};
586
+ const columns = buildContentListColumns(columnLabels);
587
+ const visibleColumns = CONTENT_LIST_VISIBLE_COLUMN_IDS.map((id, index) => {
588
+ const column = columns.find((candidate) => candidate.id === id);
589
+ if (!column) {
590
+ throw new Error(`Missing content list column definition: ${id}`);
591
+ }
592
+ return surfaceColumn(id, columnLabels, index, column);
593
+ });
594
+ // The row-key column must be declared even though the table renders identity
595
+ // through Svelte keys rather than a visible column.
596
+ const rowKeyColumn = {
597
+ id: CONTENT_LIST_ROW_KEY,
598
+ label: options.rowKeyLabel ?? 'Content id',
599
+ capabilities: ['read', 'project'],
600
+ fieldName: CONTENT_LIST_ROW_KEY,
601
+ role: 'row-key',
602
+ };
603
+ const columnIds = visibleColumns.map((column) => column.id);
604
+ const searchableColumnIds = visibleColumns
605
+ .filter((column) => column.capabilities.includes('search'))
606
+ .map((column) => column.id);
607
+ const actions = [
608
+ {
609
+ id: 'view',
610
+ label: actionLabels.view ?? DEFAULT_ACTION_LABELS.view,
611
+ selectionScopes: ['explicit-ids'],
612
+ columnIds: ['title'],
613
+ },
614
+ {
615
+ id: 'edit',
616
+ label: actionLabels.edit ?? DEFAULT_ACTION_LABELS.edit,
617
+ selectionScopes: ['explicit-ids'],
618
+ },
619
+ {
620
+ id: 'delete',
621
+ label: actionLabels.delete ?? DEFAULT_ACTION_LABELS.delete,
622
+ sensitivity: 'sensitive',
623
+ selectionScopes: ['explicit-ids', 'current-page'],
624
+ requiresConfirmation: true,
625
+ },
626
+ ];
627
+ return {
628
+ version: 1,
629
+ identity: {
630
+ surfaceId: options.surfaceId ?? CONTENT_LIST_SURFACE_ID,
631
+ kind: 'table',
632
+ ...(options.subject ? { subject: options.subject } : {}),
633
+ },
634
+ schemaVersion: CONTENT_LIST_SCHEMA_VERSION,
635
+ label: options.label ?? 'Contents',
636
+ ...(options.description ? { description: options.description } : {}),
637
+ rowKey: CONTENT_LIST_ROW_KEY,
638
+ columns: [rowKeyColumn, ...visibleColumns],
639
+ query: {
640
+ modes: ['rows', 'count'],
641
+ projectableColumnIds: [CONTENT_LIST_ROW_KEY, ...columnIds],
642
+ searchableColumnIds,
643
+ filterableColumnIds: columnIds,
644
+ sortableColumnIds: columnIds,
645
+ },
646
+ controls: CONTENT_LIST_CONTROLS.map((control) => ({ ...control })),
647
+ actions,
648
+ limits: { ...DEFAULT_SURFACE_LIMITS, ...options.limits },
649
+ };
650
+ }
@@ -40,8 +40,39 @@ export declare const M: {
40
40
  readonly 'content.contributor_manager.trust_level': "content.contributor_manager.trust_level";
41
41
  readonly 'content.contributor_manager.save_contributor': "content.contributor_manager.save_contributor";
42
42
  readonly 'content.content_list.search_placeholder': "content.content_list.search_placeholder";
43
+ readonly 'content.content_list.search_label': "content.content_list.search_label";
43
44
  readonly 'content.content_list.all_types': "content.content_list.all_types";
44
45
  readonly 'content.content_list.all_statuses': "content.content_list.all_statuses";
46
+ readonly 'content.content_list.filter_type': "content.content_list.filter_type";
47
+ readonly 'content.content_list.filter_status': "content.content_list.filter_status";
48
+ readonly 'content.content_list.type_articles': "content.content_list.type_articles";
49
+ readonly 'content.content_list.type_documents': "content.content_list.type_documents";
50
+ readonly 'content.content_list.type_mirrors': "content.content_list.type_mirrors";
51
+ readonly 'content.content_list.status_published': "content.content_list.status_published";
52
+ readonly 'content.content_list.status_draft': "content.content_list.status_draft";
53
+ readonly 'content.content_list.status_archived': "content.content_list.status_archived";
54
+ readonly 'content.content_list.view_mode': "content.content_list.view_mode";
55
+ readonly 'content.content_list.table_caption': "content.content_list.table_caption";
56
+ readonly 'content.content_list.column_type': "content.content_list.column_type";
57
+ readonly 'content.content_list.column_title': "content.content_list.column_title";
58
+ readonly 'content.content_list.column_author': "content.content_list.column_author";
59
+ readonly 'content.content_list.column_status': "content.content_list.column_status";
60
+ readonly 'content.content_list.column_state': "content.content_list.column_state";
61
+ readonly 'content.content_list.column_publish': "content.content_list.column_publish";
62
+ readonly 'content.content_list.column_updated': "content.content_list.column_updated";
63
+ readonly 'content.content_list.column_site': "content.content_list.column_site";
64
+ readonly 'content.content_list.actions_column': "content.content_list.actions_column";
65
+ readonly 'content.content_list.select_all': "content.content_list.select_all";
66
+ readonly 'content.content_list.select_row': "content.content_list.select_row";
67
+ readonly 'content.content_list.deselect_row': "content.content_list.deselect_row";
68
+ readonly 'content.content_list.row_not_selectable': "content.content_list.row_not_selectable";
69
+ readonly 'content.content_list.selection_count': "content.content_list.selection_count";
70
+ readonly 'content.content_list.clear_selection': "content.content_list.clear_selection";
71
+ readonly 'content.content_list.loading': "content.content_list.loading";
72
+ readonly 'content.content_list.refreshing': "content.content_list.refreshing";
73
+ readonly 'content.content_list.pagination': "content.content_list.pagination";
74
+ readonly 'content.content_list.error_title': "content.content_list.error_title";
75
+ readonly 'content.content_list.retry': "content.content_list.retry";
45
76
  readonly 'content.content_list.grid_view': "content.content_list.grid_view";
46
77
  readonly 'content.content_list.detailed_list': "content.content_list.detailed_list";
47
78
  readonly 'content.content_list.compact_list': "content.content_list.compact_list";
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.contribution.d.ts","sourceRoot":"","sources":["../../src/svelte/i18n.contribution.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsFZ,CAAC"}
1
+ {"version":3,"file":"i18n.contribution.d.ts","sourceRoot":"","sources":["../../src/svelte/i18n.contribution.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsHZ,CAAC"}