@happyvertical/smrt-content 0.44.1 → 0.45.1
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/agents/content-list.md +32 -9
- package/dist/chunks/{content-query-D1bLBzMT.js → content-query-BZi_ZM_c.js} +4 -4
- package/dist/chunks/{content-query-D1bLBzMT.js.map → content-query-BZi_ZM_c.js.map} +1 -1
- package/dist/chunks/{content-versions-B0OUI1ut.js → content-versions-D97wSfi0.js} +2 -2
- package/dist/chunks/{content-versions-B0OUI1ut.js.map → content-versions-D97wSfi0.js.map} +1 -1
- package/dist/chunks/{contents-CRG2Snrb.js → contents-D-qco7wz.js} +2 -2
- package/dist/chunks/{contents-CRG2Snrb.js.map → contents-D-qco7wz.js.map} +1 -1
- package/dist/index.js +4 -4
- package/dist/manifest.json +2079 -4183
- package/dist/server/content-list-data-surface.d.ts +29 -0
- package/dist/server/content-list-data-surface.d.ts.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +60 -2
- package/dist/server.js.map +1 -1
- package/dist/smrt-knowledge.json +4230 -2461
- package/dist/svelte/components/ContentList.svelte +346 -19
- package/dist/svelte/components/ContentList.svelte.d.ts +1 -1
- package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentListLifecyclePanel.svelte +52 -6
- package/dist/svelte/components/ContentListLifecyclePanel.svelte.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.d.ts +10 -0
- package/dist/svelte/content-list-controller.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.js +111 -26
- package/dist/svelte/content-list-data-surface.d.ts +45 -0
- package/dist/svelte/content-list-data-surface.d.ts.map +1 -0
- package/dist/svelte/content-list-data-surface.js +196 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -0
- package/package.json +15 -15
|
@@ -50,6 +50,11 @@ import {
|
|
|
50
50
|
readContentListLifecycleSummary,
|
|
51
51
|
reconcileContentListLifecycleSelection,
|
|
52
52
|
} from '../content-list-lifecycle.js';
|
|
53
|
+
import {
|
|
54
|
+
registerContentListDataSurface,
|
|
55
|
+
type ContentListSurfaceContext,
|
|
56
|
+
type ContentListSurfaceHandle,
|
|
57
|
+
} from '../content-list-data-surface.js';
|
|
53
58
|
import {
|
|
54
59
|
applyContentListFilter,
|
|
55
60
|
buildContentListColumns,
|
|
@@ -133,6 +138,104 @@ import ImageThumbnail from './ImageThumbnail.svelte';
|
|
|
133
138
|
|
|
134
139
|
const { t } = useI18n();
|
|
135
140
|
|
|
141
|
+
const DEFAULT_CONTENT_LIST_SURFACE_IDENTITY: DataSurfaceIdentity = {
|
|
142
|
+
surfaceId: 'content-list',
|
|
143
|
+
kind: 'table',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function sameSurfaceIdentity(
|
|
147
|
+
left: DataSurfaceIdentity,
|
|
148
|
+
right: DataSurfaceIdentity,
|
|
149
|
+
): boolean {
|
|
150
|
+
return (
|
|
151
|
+
left.kind === right.kind &&
|
|
152
|
+
left.surfaceId === right.surfaceId &&
|
|
153
|
+
left.subject?.type === right.subject?.type &&
|
|
154
|
+
left.subject?.id === right.subject?.id
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveSurfaceAuthority(
|
|
159
|
+
lifecycle: ContentListLifecycleBinding | undefined,
|
|
160
|
+
workflows: ContentListWorkflowBinding | undefined,
|
|
161
|
+
): {
|
|
162
|
+
identity?: DataSurfaceIdentity;
|
|
163
|
+
maxSelectionSize?: number;
|
|
164
|
+
lifecycle?: ContentListLifecycleBinding;
|
|
165
|
+
workflows?: ContentListWorkflowBinding;
|
|
166
|
+
} {
|
|
167
|
+
const lifecycleIdentity = lifecycle
|
|
168
|
+
? (lifecycle.identity ?? DEFAULT_CONTENT_LIST_SURFACE_IDENTITY)
|
|
169
|
+
: undefined;
|
|
170
|
+
const workflowIdentity = workflows
|
|
171
|
+
? (workflows.identity ?? DEFAULT_CONTENT_LIST_SURFACE_IDENTITY)
|
|
172
|
+
: undefined;
|
|
173
|
+
if (
|
|
174
|
+
lifecycleIdentity &&
|
|
175
|
+
workflowIdentity &&
|
|
176
|
+
!sameSurfaceIdentity(lifecycleIdentity, workflowIdentity)
|
|
177
|
+
) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
'ContentList lifecycle and workflow bindings must use the same data surface identity',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const bindings = [lifecycle, workflows].filter(
|
|
183
|
+
(binding): binding is ContentListLifecycleBinding | ContentListWorkflowBinding =>
|
|
184
|
+
binding !== undefined,
|
|
185
|
+
);
|
|
186
|
+
const maxSelectionSize =
|
|
187
|
+
bindings.length > 0
|
|
188
|
+
? Math.min(...bindings.map((binding) => binding.maxSelectionSize ?? 200))
|
|
189
|
+
: undefined;
|
|
190
|
+
return {
|
|
191
|
+
...(lifecycleIdentity ?? workflowIdentity
|
|
192
|
+
? { identity: lifecycleIdentity ?? workflowIdentity }
|
|
193
|
+
: {}),
|
|
194
|
+
...(maxSelectionSize === undefined ? {} : { maxSelectionSize }),
|
|
195
|
+
...(lifecycle && lifecycleIdentity
|
|
196
|
+
? {
|
|
197
|
+
lifecycle: {
|
|
198
|
+
...lifecycle,
|
|
199
|
+
identity: lifecycleIdentity,
|
|
200
|
+
...(maxSelectionSize === undefined ? {} : { maxSelectionSize }),
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
: {}),
|
|
204
|
+
...(workflows && workflowIdentity
|
|
205
|
+
? {
|
|
206
|
+
workflows: {
|
|
207
|
+
...workflows,
|
|
208
|
+
identity: workflowIdentity,
|
|
209
|
+
...(maxSelectionSize === undefined ? {} : { maxSelectionSize }),
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function reconcileSurfaceDescriptor(
|
|
217
|
+
descriptor: import('@happyvertical/smrt-ui/data').DataSurfaceDescriptor,
|
|
218
|
+
authority: ReturnType<typeof resolveSurfaceAuthority>,
|
|
219
|
+
): import('@happyvertical/smrt-ui/data').DataSurfaceDescriptor {
|
|
220
|
+
if (
|
|
221
|
+
authority.identity &&
|
|
222
|
+
!sameSurfaceIdentity(descriptor.identity, authority.identity)
|
|
223
|
+
) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
'ContentList custom data surface descriptor must use the lifecycle and workflow identity',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
authority.maxSelectionSize !== undefined &&
|
|
230
|
+
descriptor.limits.maxSelectionSize !== authority.maxSelectionSize
|
|
231
|
+
) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
'ContentList custom data surface descriptor must use the lifecycle and workflow selection limit',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return descriptor;
|
|
237
|
+
}
|
|
238
|
+
|
|
136
239
|
const WORKFLOW_LABEL_MESSAGES: Record<
|
|
137
240
|
ContentListWorkflowId,
|
|
138
241
|
keyof typeof M
|
|
@@ -324,7 +427,7 @@ interface Props {
|
|
|
324
427
|
error?: string | null;
|
|
325
428
|
/** Retry affordance rendered with an error. */
|
|
326
429
|
onRetry?: () => void;
|
|
327
|
-
/** Opt-in agent addressability
|
|
430
|
+
/** Opt-in agent addressability shared by every list presentation. */
|
|
328
431
|
dataSurface?: ContentListDataSurface;
|
|
329
432
|
/**
|
|
330
433
|
* Opt-in server-backed rows (#2452). `bind()` is called exactly once, during
|
|
@@ -370,6 +473,10 @@ let {
|
|
|
370
473
|
lifecycleMode = 'active',
|
|
371
474
|
}: Props = $props();
|
|
372
475
|
|
|
476
|
+
const surfaceAuthority = $derived(
|
|
477
|
+
resolveSurfaceAuthority(lifecycle, workflows),
|
|
478
|
+
);
|
|
479
|
+
|
|
373
480
|
const initialQuery = untrack(() => query);
|
|
374
481
|
|
|
375
482
|
// One controller owns search, filters, sorting, paging, and selection for
|
|
@@ -471,6 +578,14 @@ let settledLifecycleQuery = $state<ContentListDataQueryRequest | undefined>(
|
|
|
471
578
|
undefined,
|
|
472
579
|
);
|
|
473
580
|
let settledLifecycleFingerprint = $state<string | undefined>(undefined);
|
|
581
|
+
let settledSurfaceMetadata = $state<
|
|
582
|
+
| {
|
|
583
|
+
signature: string;
|
|
584
|
+
queryFingerprint: string | null;
|
|
585
|
+
totalRows: number | null;
|
|
586
|
+
}
|
|
587
|
+
| undefined
|
|
588
|
+
>(undefined);
|
|
474
589
|
|
|
475
590
|
function toSearchParams(
|
|
476
591
|
input: URLSearchParams | string | null | undefined,
|
|
@@ -563,6 +678,10 @@ let completedJobs = $state<ContentListJob[]>([]);
|
|
|
563
678
|
let completedJobsOverflowed = $state(false);
|
|
564
679
|
let activeQueryKey = $state<string | undefined>(undefined);
|
|
565
680
|
let lifecycleRefreshPending = $state(false);
|
|
681
|
+
let surfaceRoot: HTMLDivElement | undefined;
|
|
682
|
+
let surfaceHighlighted = $state(false);
|
|
683
|
+
let surfaceHighlightTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
684
|
+
let surfaceHandle: ContentListSurfaceHandle | undefined;
|
|
566
685
|
|
|
567
686
|
// Job state is subscribed once so hosts can use a framework-free controller.
|
|
568
687
|
// Only transitions observed after the initial snapshot are completion events;
|
|
@@ -924,9 +1043,20 @@ function settleWorkflowQueryResult(
|
|
|
924
1043
|
signature: string,
|
|
925
1044
|
): void {
|
|
926
1045
|
settledSignature = signature;
|
|
1046
|
+
const envelope =
|
|
1047
|
+
result !== null && typeof result === 'object' && !Array.isArray(result)
|
|
1048
|
+
? (result as Record<string, unknown>)
|
|
1049
|
+
: undefined;
|
|
1050
|
+
settledSurfaceMetadata = {
|
|
1051
|
+
signature,
|
|
1052
|
+
queryFingerprint:
|
|
1053
|
+
typeof envelope?.queryFingerprint === 'string'
|
|
1054
|
+
? envelope.queryFingerprint
|
|
1055
|
+
: null,
|
|
1056
|
+
totalRows: surfaceMetadataTotalRows(envelope?.total),
|
|
1057
|
+
};
|
|
927
1058
|
resultNotices = readContentListQueryNotices(result);
|
|
928
|
-
if (
|
|
929
|
-
const envelope = result as Record<string, unknown>;
|
|
1059
|
+
if (envelope) {
|
|
930
1060
|
settledWorkflowQuery = request;
|
|
931
1061
|
settledWorkflowFingerprint =
|
|
932
1062
|
typeof envelope.queryFingerprint === 'string'
|
|
@@ -947,6 +1077,17 @@ function settleWorkflowQueryResult(
|
|
|
947
1077
|
settledWorkflowRevision = null;
|
|
948
1078
|
}
|
|
949
1079
|
|
|
1080
|
+
function surfaceMetadataTotalRows(total: unknown): number | null {
|
|
1081
|
+
if (!total || typeof total !== 'object' || Array.isArray(total)) return null;
|
|
1082
|
+
const record = total as Record<string, unknown>;
|
|
1083
|
+
return (record.kind === 'exact' || record.kind === 'estimated') &&
|
|
1084
|
+
typeof record.value === 'number' &&
|
|
1085
|
+
Number.isSafeInteger(record.value) &&
|
|
1086
|
+
record.value >= 0
|
|
1087
|
+
? record.value
|
|
1088
|
+
: null;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
950
1091
|
function settleLifecycleResult(
|
|
951
1092
|
result: unknown,
|
|
952
1093
|
request: ContentListDataQueryRequest | undefined,
|
|
@@ -1205,7 +1346,7 @@ const canSelectAllMatching = $derived(
|
|
|
1205
1346
|
settledWorkflowRevision &&
|
|
1206
1347
|
exactMatchingCount !== undefined &&
|
|
1207
1348
|
exactMatchingCount > 0 &&
|
|
1208
|
-
exactMatchingCount <= (
|
|
1349
|
+
exactMatchingCount <= (surfaceAuthority.maxSelectionSize ?? 200),
|
|
1209
1350
|
),
|
|
1210
1351
|
);
|
|
1211
1352
|
const workflowPayloadValid = $derived(
|
|
@@ -1385,20 +1526,30 @@ const surfaceOptions = $derived(
|
|
|
1385
1526
|
? {
|
|
1386
1527
|
registry: dataSurface.registry,
|
|
1387
1528
|
descriptor:
|
|
1388
|
-
dataSurface.descriptor
|
|
1529
|
+
dataSurface.descriptor
|
|
1530
|
+
? reconcileSurfaceDescriptor(
|
|
1531
|
+
dataSurface.descriptor,
|
|
1532
|
+
surfaceAuthority,
|
|
1533
|
+
)
|
|
1534
|
+
:
|
|
1389
1535
|
(() => {
|
|
1390
1536
|
const descriptor = buildContentListSurfaceDescriptor({
|
|
1391
1537
|
columnLabels,
|
|
1538
|
+
serverBacked: initialQuery !== undefined,
|
|
1539
|
+
lifecycle: lifecycle !== undefined,
|
|
1540
|
+
lifecycleMode,
|
|
1392
1541
|
includeWorkflows: workflows !== undefined,
|
|
1542
|
+
refresh: queryBinding?.refresh !== undefined,
|
|
1543
|
+
retry: retryHandler !== undefined,
|
|
1393
1544
|
});
|
|
1394
1545
|
return {
|
|
1395
1546
|
...descriptor,
|
|
1396
|
-
identity:
|
|
1547
|
+
identity: surfaceAuthority.identity ?? descriptor.identity,
|
|
1397
1548
|
limits: {
|
|
1398
1549
|
...descriptor.limits,
|
|
1399
|
-
...(
|
|
1550
|
+
...(surfaceAuthority.maxSelectionSize === undefined
|
|
1400
1551
|
? {}
|
|
1401
|
-
: { maxSelectionSize:
|
|
1552
|
+
: { maxSelectionSize: surfaceAuthority.maxSelectionSize }),
|
|
1402
1553
|
},
|
|
1403
1554
|
};
|
|
1404
1555
|
})(),
|
|
@@ -1440,6 +1591,164 @@ const queryWarnings = $derived<ReadonlyArray<string>>(
|
|
|
1440
1591
|
boundResultNotices?.warnings ??
|
|
1441
1592
|
resultNotices.warnings,
|
|
1442
1593
|
);
|
|
1594
|
+
|
|
1595
|
+
function surfaceLastUpdated(value: unknown): string | null {
|
|
1596
|
+
if (value instanceof Date) return value.toISOString();
|
|
1597
|
+
if (typeof value === 'string') return value;
|
|
1598
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1599
|
+
return new Date(value).toISOString();
|
|
1600
|
+
}
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const matchingSurfaceMetadata = $derived(
|
|
1605
|
+
serverBacked && settledSurfaceMetadata?.signature === querySignature
|
|
1606
|
+
? settledSurfaceMetadata
|
|
1607
|
+
: undefined,
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
const surfaceContext = $derived<ContentListSurfaceContext>({
|
|
1611
|
+
viewMode,
|
|
1612
|
+
queryFingerprint: serverBacked
|
|
1613
|
+
? (matchingSurfaceMetadata?.queryFingerprint ?? null)
|
|
1614
|
+
: null,
|
|
1615
|
+
totalRows: serverBacked
|
|
1616
|
+
? (matchingSurfaceMetadata?.totalRows ?? null)
|
|
1617
|
+
: Number.isSafeInteger(totalRowCount)
|
|
1618
|
+
? totalRowCount
|
|
1619
|
+
: null,
|
|
1620
|
+
freshness: {
|
|
1621
|
+
stale,
|
|
1622
|
+
refreshing,
|
|
1623
|
+
offline,
|
|
1624
|
+
lastUpdated: surfaceLastUpdated(lastUpdated),
|
|
1625
|
+
truncated: queryTruncated,
|
|
1626
|
+
warnings: queryWarnings,
|
|
1627
|
+
},
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
function focusSurface(): void {
|
|
1631
|
+
if (!surfaceRoot) return;
|
|
1632
|
+
surfaceRoot.tabIndex = -1;
|
|
1633
|
+
surfaceRoot.focus();
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// ContentList owns the registration so grid, detailed, compact, empty, and
|
|
1637
|
+
// loading presentations all expose one stable identity and revision stream.
|
|
1638
|
+
// This ref is deliberately non-reactive: registration input changes must
|
|
1639
|
+
// advance a stable identity revision, but normal command acknowledgements must
|
|
1640
|
+
// not themselves cause a registration teardown/replay window.
|
|
1641
|
+
const surfaceRevision = { value: undefined as number | undefined };
|
|
1642
|
+
|
|
1643
|
+
function acceptsSurfaceTableCommand(
|
|
1644
|
+
command: import('@happyvertical/smrt-ui/data').DataTableCommand,
|
|
1645
|
+
): boolean {
|
|
1646
|
+
if (command.type === 'setPageSize' && serverPageSize !== null) {
|
|
1647
|
+
if (command.pageSize === null || command.pageSize > maxPageSize) return false;
|
|
1648
|
+
}
|
|
1649
|
+
if (command.type === 'setPage') {
|
|
1650
|
+
if (command.page > maxReachablePage) return false;
|
|
1651
|
+
if (
|
|
1652
|
+
tableState.pageSize !== null &&
|
|
1653
|
+
clampableRowCount !== undefined &&
|
|
1654
|
+
// A server binding retains the previous query's exact total until its
|
|
1655
|
+
// replacement settles. That count cannot bound a command for the new
|
|
1656
|
+
// query: it may be smaller, and would reject a valid page before the
|
|
1657
|
+
// new query gets a chance to report its own total.
|
|
1658
|
+
(!serverBacked || settledSignature === querySignature) &&
|
|
1659
|
+
command.page > Math.max(1, Math.ceil(clampableRowCount / tableState.pageSize))
|
|
1660
|
+
)
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
if (command.type === 'setFilters') {
|
|
1664
|
+
if (
|
|
1665
|
+
lockedType !== null &&
|
|
1666
|
+
!isContentListFilterExactly(
|
|
1667
|
+
{ ...tableState, filters: command.filters },
|
|
1668
|
+
CONTENT_LIST_TYPE_FILTER_ID,
|
|
1669
|
+
lockedType,
|
|
1670
|
+
)
|
|
1671
|
+
)
|
|
1672
|
+
return false;
|
|
1673
|
+
if (
|
|
1674
|
+
lifecycleMode === 'trash' &&
|
|
1675
|
+
!isContentListFilterExactly(
|
|
1676
|
+
{ ...tableState, filters: command.filters },
|
|
1677
|
+
CONTENT_LIST_STATUS_FILTER_ID,
|
|
1678
|
+
'deleted',
|
|
1679
|
+
)
|
|
1680
|
+
)
|
|
1681
|
+
return false;
|
|
1682
|
+
}
|
|
1683
|
+
return command.type !== 'reset' || (lockedType === null && lifecycleMode !== 'trash');
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
$effect(() => {
|
|
1687
|
+
const surface = surfaceOptions;
|
|
1688
|
+
if (!surface) {
|
|
1689
|
+
surfaceHandle = undefined;
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
const handle = registerContentListDataSurface({
|
|
1693
|
+
...surface,
|
|
1694
|
+
controller,
|
|
1695
|
+
context: untrack(() => surfaceContext),
|
|
1696
|
+
initialRevision:
|
|
1697
|
+
surfaceRevision.value === undefined ? 0 : surfaceRevision.value + 1,
|
|
1698
|
+
onRevision: (revision) => {
|
|
1699
|
+
surfaceRevision.value = revision;
|
|
1700
|
+
},
|
|
1701
|
+
acceptsTableCommand: acceptsSurfaceTableCommand,
|
|
1702
|
+
setViewMode: (next) => {
|
|
1703
|
+
viewMode = next;
|
|
1704
|
+
},
|
|
1705
|
+
...(queryBinding?.refresh
|
|
1706
|
+
? {
|
|
1707
|
+
refresh: async () => {
|
|
1708
|
+
await refreshQuery();
|
|
1709
|
+
return true;
|
|
1710
|
+
},
|
|
1711
|
+
}
|
|
1712
|
+
: {}),
|
|
1713
|
+
...(retryHandler
|
|
1714
|
+
? {
|
|
1715
|
+
retry: async () => {
|
|
1716
|
+
await retryHandler();
|
|
1717
|
+
return true;
|
|
1718
|
+
},
|
|
1719
|
+
}
|
|
1720
|
+
: {}),
|
|
1721
|
+
focus: focusSurface,
|
|
1722
|
+
reveal: () => surfaceRoot?.scrollIntoView({ block: 'nearest' }),
|
|
1723
|
+
highlight: () => {
|
|
1724
|
+
if (surfaceHighlightTimeout !== undefined) {
|
|
1725
|
+
clearTimeout(surfaceHighlightTimeout);
|
|
1726
|
+
}
|
|
1727
|
+
surfaceHighlighted = true;
|
|
1728
|
+
const timeout = setTimeout(() => {
|
|
1729
|
+
if (surfaceHandle === handle) surfaceHighlighted = false;
|
|
1730
|
+
if (surfaceHighlightTimeout === timeout) {
|
|
1731
|
+
surfaceHighlightTimeout = undefined;
|
|
1732
|
+
}
|
|
1733
|
+
}, 1_000);
|
|
1734
|
+
surfaceHighlightTimeout = timeout;
|
|
1735
|
+
},
|
|
1736
|
+
});
|
|
1737
|
+
surfaceHandle = handle;
|
|
1738
|
+
return () => {
|
|
1739
|
+
if (surfaceHandle === handle) surfaceHandle = undefined;
|
|
1740
|
+
if (surfaceHighlightTimeout !== undefined) {
|
|
1741
|
+
clearTimeout(surfaceHighlightTimeout);
|
|
1742
|
+
surfaceHighlightTimeout = undefined;
|
|
1743
|
+
}
|
|
1744
|
+
surfaceHighlighted = false;
|
|
1745
|
+
handle.destroy();
|
|
1746
|
+
};
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
$effect(() => {
|
|
1750
|
+
surfaceHandle?.update(surfaceContext);
|
|
1751
|
+
});
|
|
1443
1752
|
/** Identity of the current set of refusals, so a dismissal is not permanent. */
|
|
1444
1753
|
const dropNoticeKey = $derived(
|
|
1445
1754
|
dropNotices.length > 0 || queryTruncated || queryWarnings.length > 0
|
|
@@ -1734,6 +2043,16 @@ function workflowSelection(): ContentListWorkflowRequest['selection'] | null {
|
|
|
1734
2043
|
return { scope: 'explicit-ids', rowIds: effectiveRowIds };
|
|
1735
2044
|
}
|
|
1736
2045
|
|
|
2046
|
+
function workflowSelectionCount(
|
|
2047
|
+
selection: ContentListWorkflowRequest['selection'],
|
|
2048
|
+
): number {
|
|
2049
|
+
if (selection.scope === 'explicit-ids') return selection.rowIds.length;
|
|
2050
|
+
if (selection.scope === 'current-page') return selectablePageRowIds.length;
|
|
2051
|
+
return tableState.selection.scope === 'allMatching'
|
|
2052
|
+
? tableState.selection.expectedCount
|
|
2053
|
+
: 0;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1737
2056
|
function workflowJobTarget(
|
|
1738
2057
|
request: ContentListWorkflowRequest,
|
|
1739
2058
|
): ContentListJobTarget {
|
|
@@ -1774,6 +2093,8 @@ function createWorkflowRequest(
|
|
|
1774
2093
|
): ContentListWorkflowRequest | null {
|
|
1775
2094
|
const selection = workflowSelection();
|
|
1776
2095
|
if (!selection) return null;
|
|
2096
|
+
const selectionCount = workflowSelectionCount(selection);
|
|
2097
|
+
if (selectionCount > (surfaceAuthority.maxSelectionSize ?? 200)) return null;
|
|
1777
2098
|
const queryRequired = selection.scope !== 'explicit-ids';
|
|
1778
2099
|
if (
|
|
1779
2100
|
queryRequired &&
|
|
@@ -1786,7 +2107,8 @@ function createWorkflowRequest(
|
|
|
1786
2107
|
return {
|
|
1787
2108
|
version: 1,
|
|
1788
2109
|
requestId: globalThis.crypto?.randomUUID?.() ?? `content-workflow-${Date.now()}`,
|
|
1789
|
-
identity:
|
|
2110
|
+
identity:
|
|
2111
|
+
surfaceAuthority.identity ?? DEFAULT_CONTENT_LIST_SURFACE_IDENTITY,
|
|
1790
2112
|
actionId: selectedWorkflow,
|
|
1791
2113
|
phase,
|
|
1792
2114
|
selection,
|
|
@@ -1796,10 +2118,7 @@ function createWorkflowRequest(
|
|
|
1796
2118
|
...(queryRequired && settledWorkflowQuery
|
|
1797
2119
|
? { query: settledWorkflowQuery }
|
|
1798
2120
|
: {}),
|
|
1799
|
-
expectedCount:
|
|
1800
|
-
selection.scope === 'explicit-ids'
|
|
1801
|
-
? selection.rowIds.length
|
|
1802
|
-
: selectedCount,
|
|
2121
|
+
expectedCount: selectionCount,
|
|
1803
2122
|
},
|
|
1804
2123
|
};
|
|
1805
2124
|
}
|
|
@@ -2380,7 +2699,11 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
2380
2699
|
</div>
|
|
2381
2700
|
{/snippet}
|
|
2382
2701
|
|
|
2383
|
-
<div
|
|
2702
|
+
<div
|
|
2703
|
+
class="content-list-wrapper"
|
|
2704
|
+
class:data-surface-highlighted={surfaceHighlighted}
|
|
2705
|
+
bind:this={surfaceRoot}
|
|
2706
|
+
>
|
|
2384
2707
|
|
|
2385
2708
|
<div class="content-controls">
|
|
2386
2709
|
<div class="search-filters">
|
|
@@ -2726,7 +3049,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
2726
3049
|
|
|
2727
3050
|
{#if lifecycle}
|
|
2728
3051
|
<ContentListLifecyclePanel
|
|
2729
|
-
binding={lifecycle}
|
|
3052
|
+
binding={surfaceAuthority.lifecycle ?? lifecycle}
|
|
2730
3053
|
mode={lifecycleMode}
|
|
2731
3054
|
selectedRowIds={tableState.selectedRowIds}
|
|
2732
3055
|
query={lifecycleQuery}
|
|
@@ -2855,9 +3178,9 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
2855
3178
|
|
|
2856
3179
|
{#if viewMode === 'compact'}
|
|
2857
3180
|
<!--
|
|
2858
|
-
The compact table stays mounted for empty and loading results
|
|
2859
|
-
|
|
2860
|
-
|
|
3181
|
+
The compact table stays mounted for empty and loading results so its
|
|
3182
|
+
table semantics and operator affordances remain stable across queries.
|
|
3183
|
+
ContentList itself owns the agent-addressable surface in every view.
|
|
2861
3184
|
-->
|
|
2862
3185
|
<div class="content-table-wrapper">
|
|
2863
3186
|
<DataTable
|
|
@@ -2870,7 +3193,6 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
2870
3193
|
loading={isLoading}
|
|
2871
3194
|
caption={t(M['content.content_list.table_caption'])}
|
|
2872
3195
|
rowLabel={(row: ContentListRow) => row.title}
|
|
2873
|
-
dataSurface={surfaceOptions}
|
|
2874
3196
|
empty={tableEmptyState}
|
|
2875
3197
|
/>
|
|
2876
3198
|
</div>
|
|
@@ -3108,6 +3430,11 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
3108
3430
|
width: 100%;
|
|
3109
3431
|
}
|
|
3110
3432
|
|
|
3433
|
+
.content-list-wrapper.data-surface-highlighted {
|
|
3434
|
+
outline: 2px solid var(--smrt-color-primary);
|
|
3435
|
+
outline-offset: 0.25rem;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3111
3438
|
.content-controls {
|
|
3112
3439
|
display: flex;
|
|
3113
3440
|
justify-content: space-between;
|
|
@@ -60,7 +60,7 @@ interface Props {
|
|
|
60
60
|
error?: string | null;
|
|
61
61
|
/** Retry affordance rendered with an error. */
|
|
62
62
|
onRetry?: () => void;
|
|
63
|
-
/** Opt-in agent addressability
|
|
63
|
+
/** Opt-in agent addressability shared by every list presentation. */
|
|
64
64
|
dataSurface?: ContentListDataSurface;
|
|
65
65
|
/**
|
|
66
66
|
* Opt-in server-backed rows (#2452). `bind()` is called exactly once, during
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContentList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentList.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,IAAI,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CACT,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,6BAA6B,KACjC,IAAI,CAAC;IACV,uEAAuE;IACvE,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACtC;AAcD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"ContentList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentList.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,IAAI,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CACT,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,6BAA6B,KACjC,IAAI,CAAC;IACV,uEAAuE;IACvE,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACtC;AAcD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AAUhF,OAAO,EASL,KAAK,sBAAsB,EAE3B,KAAK,mBAAmB,EAiBzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAOL,KAAK,sBAAsB,EAU5B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAGL,KAAK,qBAAqB,EAG3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAEL,KAAK,yBAAyB,EAG/B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,KAAK,0BAA0B,EAOhC,MAAM,8BAA8B,CAAC;AAiBtC,UAAU,KAAK;IACb,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,6DAA6D;IAC7D,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,+DAA+D;IAC/D,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACzC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,GAAG,IAAI,CAAC;IACtD,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qEAAqE;IACrE,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IACtC,oFAAoF;IACpF,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,qBAAqB,CAAC;IAC7B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,0BAA0B,CAAC;IACvC,6EAA6E;IAC7E,SAAS,CAAC,EAAE,2BAA2B,CAAC;IACxC,2EAA2E;IAC3E,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACpC;AA26FD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -42,7 +42,7 @@ let {
|
|
|
42
42
|
oncomplete,
|
|
43
43
|
}: Props = $props();
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
let controller = createContentListLifecycleController(untrack(() => binding));
|
|
46
46
|
let snapshot = $state<ContentListLifecycleSnapshot>(controller.snapshot());
|
|
47
47
|
let lastResult = $state<ContentListLifecycleSnapshot['summary']>();
|
|
48
48
|
let dialogOpen = $state(false);
|
|
@@ -50,10 +50,50 @@ let countConfirmation = $state('');
|
|
|
50
50
|
let restoreStatus = $state<ContentListRestoreStatus>('draft');
|
|
51
51
|
let intent = $state<'selected' | 'all-matching'>('selected');
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
let unsubscribe = controller.subscribe((next) => {
|
|
54
54
|
snapshot = next;
|
|
55
55
|
});
|
|
56
|
-
|
|
56
|
+
|
|
57
|
+
function sameBindingAuthority(
|
|
58
|
+
left: ContentListLifecycleBinding,
|
|
59
|
+
right: ContentListLifecycleBinding,
|
|
60
|
+
): boolean {
|
|
61
|
+
return (
|
|
62
|
+
left.client === right.client &&
|
|
63
|
+
left.client.preview === right.client.preview &&
|
|
64
|
+
left.client.apply === right.client.apply &&
|
|
65
|
+
left.revision === right.revision &&
|
|
66
|
+
left.maxSelectionSize === right.maxSelectionSize &&
|
|
67
|
+
left.identity?.kind === right.identity?.kind &&
|
|
68
|
+
left.identity?.surfaceId === right.identity?.surfaceId &&
|
|
69
|
+
left.identity?.subject?.type === right.identity?.subject?.type &&
|
|
70
|
+
left.identity?.subject?.id === right.identity?.subject?.id
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let installedBinding = untrack(() => binding);
|
|
75
|
+
|
|
76
|
+
function replaceController(nextBinding: ContentListLifecycleBinding): void {
|
|
77
|
+
unsubscribe();
|
|
78
|
+
controller.reset();
|
|
79
|
+
controller = createContentListLifecycleController(nextBinding);
|
|
80
|
+
snapshot = controller.snapshot();
|
|
81
|
+
lastResult = undefined;
|
|
82
|
+
dialogOpen = false;
|
|
83
|
+
countConfirmation = '';
|
|
84
|
+
unsubscribe = controller.subscribe((next) => {
|
|
85
|
+
snapshot = next;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
$effect(() => {
|
|
90
|
+
const nextBinding = binding;
|
|
91
|
+
if (sameBindingAuthority(installedBinding, nextBinding)) return;
|
|
92
|
+
replaceController(nextBinding);
|
|
93
|
+
installedBinding = nextBinding;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
onDestroy(() => unsubscribe());
|
|
57
97
|
|
|
58
98
|
$effect(() => {
|
|
59
99
|
controller.invalidate(viewKey);
|
|
@@ -70,6 +110,7 @@ const busy = $derived(
|
|
|
70
110
|
);
|
|
71
111
|
const selectedCount = $derived(selectedRowIds.length);
|
|
72
112
|
const selectionLimit = $derived(binding.maxSelectionSize ?? 200);
|
|
113
|
+
const selectedSelectionTooLarge = $derived(selectedCount > selectionLimit);
|
|
73
114
|
const matchingSelectionTooLarge = $derived(
|
|
74
115
|
exactMatchingCount !== undefined && exactMatchingCount > selectionLimit,
|
|
75
116
|
);
|
|
@@ -92,6 +133,11 @@ async function begin(
|
|
|
92
133
|
actionId: ContentListLifecycleActionId,
|
|
93
134
|
scope: 'selected' | 'all-matching' = 'selected',
|
|
94
135
|
) {
|
|
136
|
+
if (
|
|
137
|
+
(scope === 'selected' && selectedSelectionTooLarge) ||
|
|
138
|
+
(scope === 'all-matching' && matchingSelectionTooLarge)
|
|
139
|
+
)
|
|
140
|
+
return;
|
|
95
141
|
intent = scope;
|
|
96
142
|
countConfirmation = '';
|
|
97
143
|
dialogOpen = true;
|
|
@@ -136,7 +182,7 @@ function retryPreview() {
|
|
|
136
182
|
variant="ghost"
|
|
137
183
|
size="sm"
|
|
138
184
|
type="button"
|
|
139
|
-
disabled={selectedCount === 0 || busy}
|
|
185
|
+
disabled={selectedCount === 0 || selectedSelectionTooLarge || busy}
|
|
140
186
|
onclick={() => begin('move-to-trash')}
|
|
141
187
|
>{t(M['content.content_list.move_selected_to_trash'])}</Button>
|
|
142
188
|
{:else}
|
|
@@ -153,14 +199,14 @@ function retryPreview() {
|
|
|
153
199
|
variant="ghost"
|
|
154
200
|
size="sm"
|
|
155
201
|
type="button"
|
|
156
|
-
disabled={selectedCount === 0 || busy}
|
|
202
|
+
disabled={selectedCount === 0 || selectedSelectionTooLarge || busy}
|
|
157
203
|
onclick={() => begin('restore')}
|
|
158
204
|
>{t(M['content.content_list.restore_selected'])}</Button>
|
|
159
205
|
<Button
|
|
160
206
|
variant="ghost"
|
|
161
207
|
size="sm"
|
|
162
208
|
type="button"
|
|
163
|
-
disabled={selectedCount === 0 || busy}
|
|
209
|
+
disabled={selectedCount === 0 || selectedSelectionTooLarge || busy}
|
|
164
210
|
onclick={() => begin('permanent-delete')}
|
|
165
211
|
>{t(M['content.content_list.delete_selected_permanently'])}</Button>
|
|
166
212
|
<Button
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContentListLifecyclePanel.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentListLifecyclePanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EAEL,KAAK,2BAA2B,EAIjC,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAI5E,UAAU,KAAK;IACb,OAAO,EAAE,2BAA2B,CAAC;IACrC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,cAAc,EAAE,gBAAgB,EAAE,CAAC;IACnC,KAAK,CAAC,EAAE,2BAA2B,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;CACvD;
|
|
1
|
+
{"version":3,"file":"ContentListLifecyclePanel.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentListLifecyclePanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EAEL,KAAK,2BAA2B,EAIjC,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAI5E,UAAU,KAAK;IACb,OAAO,EAAE,2BAA2B,CAAC;IACrC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,cAAc,EAAE,gBAAgB,EAAE,CAAC;IACnC,KAAK,CAAC,EAAE,2BAA2B,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;CACvD;AAgSD,QAAA,MAAM,yBAAyB,2CAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
|
|
@@ -105,6 +105,16 @@ export interface ContentListSurfaceDescriptorOptions {
|
|
|
105
105
|
limits?: Partial<DataSurfaceLimits>;
|
|
106
106
|
/** Publish bulk workflow actions only when the mounted list can execute them. */
|
|
107
107
|
includeWorkflows?: boolean;
|
|
108
|
+
/** Narrows discovery to operations the server query translator can execute. */
|
|
109
|
+
serverBacked?: boolean;
|
|
110
|
+
/** Publish the server-authoritative lifecycle actions available to this list. */
|
|
111
|
+
lifecycle?: boolean;
|
|
112
|
+
/** Trash lists expose restore/delete rather than active editing actions. */
|
|
113
|
+
lifecycleMode?: 'active' | 'trash';
|
|
114
|
+
/** A mounted query can refresh the currently visible result. */
|
|
115
|
+
refresh?: boolean;
|
|
116
|
+
/** A mounted query or host callback can retry a failed result. */
|
|
117
|
+
retry?: boolean;
|
|
108
118
|
}
|
|
109
119
|
/**
|
|
110
120
|
* Opt-in agent addressability for the compact table. The host owns the
|