@happyvertical/smrt-content 0.43.9 → 0.44.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.
- package/agents/content-list.md +69 -0
- package/dist/chunks/content-corrections-BxfA3tW1.js +107 -0
- package/dist/chunks/content-corrections-BxfA3tW1.js.map +1 -0
- package/dist/chunks/content-query-B4cHfrig.js +4092 -0
- package/dist/chunks/content-query-B4cHfrig.js.map +1 -0
- package/dist/chunks/content-reviews-BWrYEQc6.js +124 -0
- package/dist/chunks/content-reviews-BWrYEQc6.js.map +1 -0
- package/dist/chunks/content-versions-OEYQVrUf.js +284 -0
- package/dist/chunks/content-versions-OEYQVrUf.js.map +1 -0
- package/dist/chunks/contents-BrGxWH3s.js +612 -0
- package/dist/chunks/contents-BrGxWH3s.js.map +1 -0
- package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/content-chat-handlers.d.ts +1 -1
- package/dist/content-chat-handlers.d.ts.map +1 -1
- package/dist/content-governance.d.ts +2 -0
- package/dist/content-governance.d.ts.map +1 -1
- package/dist/content.d.ts +2 -2
- package/dist/content.d.ts.map +1 -1
- package/dist/index.js +142 -4717
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +142 -5
- package/dist/server/content-list-actions.d.ts +123 -0
- package/dist/server/content-list-actions.d.ts.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +874 -0
- package/dist/server.js.map +1 -0
- package/dist/smrt-knowledge.json +96 -9
- package/dist/svelte/components/ContentAgentChat.svelte +10 -0
- package/dist/svelte/components/ContentAgentChat.svelte.d.ts +10 -0
- package/dist/svelte/components/ContentAgentChat.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentBodyEditor.svelte +9 -0
- package/dist/svelte/components/ContentBodyEditor.svelte.d.ts +9 -0
- package/dist/svelte/components/ContentBodyEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentBodyRenderer.svelte +3 -0
- package/dist/svelte/components/ContentBodyRenderer.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentBodyRenderer.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentClaimAuditTool.svelte +3 -0
- package/dist/svelte/components/ContentClaimAuditTool.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentClaimAuditTool.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentContributionForm.svelte +7 -0
- package/dist/svelte/components/ContentContributionForm.svelte.d.ts +7 -0
- package/dist/svelte/components/ContentContributionForm.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentContributionInbox.svelte +8 -0
- package/dist/svelte/components/ContentContributionInbox.svelte.d.ts +8 -0
- package/dist/svelte/components/ContentContributionInbox.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentContributionPortal.svelte +5 -0
- package/dist/svelte/components/ContentContributionPortal.svelte.d.ts +5 -0
- package/dist/svelte/components/ContentContributionPortal.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentContributionTypeManager.svelte +3 -0
- package/dist/svelte/components/ContentContributionTypeManager.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentContributionTypeManager.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentContributorManager.svelte +3 -0
- package/dist/svelte/components/ContentContributorManager.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentContributorManager.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentCorrectionsTool.svelte +4 -0
- package/dist/svelte/components/ContentCorrectionsTool.svelte.d.ts +4 -0
- package/dist/svelte/components/ContentCorrectionsTool.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentEditor.svelte +16 -0
- package/dist/svelte/components/ContentEditor.svelte.d.ts +16 -0
- package/dist/svelte/components/ContentEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte +4 -0
- package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte.d.ts +4 -0
- package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentGovernanceManager.svelte +3 -0
- package/dist/svelte/components/ContentGovernanceManager.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentGovernanceManager.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentGovernancePanel.svelte +16 -0
- package/dist/svelte/components/ContentGovernancePanel.svelte.d.ts +16 -0
- package/dist/svelte/components/ContentGovernancePanel.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentGovernancePolicyEditor.svelte +3 -0
- package/dist/svelte/components/ContentGovernancePolicyEditor.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentGovernancePolicyEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentGovernanceProfileEditor.svelte +4 -0
- package/dist/svelte/components/ContentGovernanceProfileEditor.svelte.d.ts +4 -0
- package/dist/svelte/components/ContentGovernanceProfileEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentGovernanceTool.svelte +5 -0
- package/dist/svelte/components/ContentGovernanceTool.svelte.d.ts +5 -0
- package/dist/svelte/components/ContentGovernanceTool.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentImageBrowser.svelte +10 -0
- package/dist/svelte/components/ContentImageBrowser.svelte.d.ts +10 -0
- package/dist/svelte/components/ContentImageBrowser.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentImageChooser.svelte +4 -0
- package/dist/svelte/components/ContentImageChooser.svelte.d.ts +4 -0
- package/dist/svelte/components/ContentImageChooser.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentList.svelte +1036 -24
- package/dist/svelte/components/ContentList.svelte.d.ts +11 -0
- package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentMetadataFields.svelte +2 -0
- package/dist/svelte/components/ContentMetadataFields.svelte.d.ts +2 -0
- package/dist/svelte/components/ContentMetadataFields.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentReferencesPanel.svelte +3 -0
- package/dist/svelte/components/ContentReferencesPanel.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentReferencesPanel.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentReviewStatusTray.svelte +5 -0
- package/dist/svelte/components/ContentReviewStatusTray.svelte.d.ts +5 -0
- package/dist/svelte/components/ContentReviewStatusTray.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentStatusFields.svelte +2 -0
- package/dist/svelte/components/ContentStatusFields.svelte.d.ts +2 -0
- package/dist/svelte/components/ContentStatusFields.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentTitleField.svelte +4 -0
- package/dist/svelte/components/ContentTitleField.svelte.d.ts +4 -0
- package/dist/svelte/components/ContentTitleField.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentTransparencyReport.svelte +3 -0
- package/dist/svelte/components/ContentTransparencyReport.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentTransparencyReport.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentTransparencyTool.svelte +3 -0
- package/dist/svelte/components/ContentTransparencyTool.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentTransparencyTool.svelte.d.ts.map +1 -1
- package/dist/svelte/components/ContentVersionsTool.svelte +3 -0
- package/dist/svelte/components/ContentVersionsTool.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentVersionsTool.svelte.d.ts.map +1 -1
- package/dist/svelte/components/GovernedContentEditor.svelte +20 -0
- package/dist/svelte/components/GovernedContentEditor.svelte.d.ts +20 -0
- package/dist/svelte/components/GovernedContentEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.d.ts +3 -1
- package/dist/svelte/content-list-controller.d.ts.map +1 -1
- package/dist/svelte/content-list-controller.js +15 -1
- package/dist/svelte/content-list-workflows.d.ts +87 -0
- package/dist/svelte/content-list-workflows.d.ts.map +1 -0
- package/dist/svelte/content-list-workflows.js +199 -0
- package/dist/svelte/i18n.contribution.d.ts +109 -0
- package/dist/svelte/i18n.contribution.d.ts.map +1 -1
- package/dist/svelte/i18n.contribution.js +109 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +2 -0
- package/dist/svelte/routes/ContentContributionsRoute.svelte +4 -0
- package/dist/svelte/routes/ContentContributionsRoute.svelte.d.ts +4 -0
- package/dist/svelte/routes/ContentContributionsRoute.svelte.d.ts.map +1 -1
- package/dist/svelte/routes/ContentFactsRoute.svelte +5 -0
- package/dist/svelte/routes/ContentFactsRoute.svelte.d.ts +5 -0
- package/dist/svelte/routes/ContentFactsRoute.svelte.d.ts.map +1 -1
- package/dist/svelte/routes/ContentGovernanceRoute.svelte +4 -0
- package/dist/svelte/routes/ContentGovernanceRoute.svelte.d.ts +4 -0
- package/dist/svelte/routes/ContentGovernanceRoute.svelte.d.ts.map +1 -1
- package/dist/svelte/routes/ContentWorkspaceRoute.svelte +5 -0
- package/dist/svelte/routes/ContentWorkspaceRoute.svelte.d.ts +5 -0
- package/dist/svelte/routes/ContentWorkspaceRoute.svelte.d.ts.map +1 -1
- package/dist/svelte/routes/PublishedArticleRoute.svelte +3 -0
- package/dist/svelte/routes/PublishedArticleRoute.svelte.d.ts +3 -0
- package/dist/svelte/routes/PublishedArticleRoute.svelte.d.ts.map +1 -1
- package/package.json +20 -14
- package/dist/chunks/content-transparency-Bh6axaZj.js +0 -606
- package/dist/chunks/content-transparency-Bh6axaZj.js.map +0 -1
|
@@ -33,6 +33,8 @@ export interface ContentListUrlStateBinding {
|
|
|
33
33
|
<script lang="ts">
|
|
34
34
|
import {
|
|
35
35
|
DataTable,
|
|
36
|
+
type DataSurfaceActionResult,
|
|
37
|
+
type DataSurfaceIdentity,
|
|
36
38
|
type DataTableColumn,
|
|
37
39
|
type DataTableViewState,
|
|
38
40
|
} from '@happyvertical/smrt-ui/data';
|
|
@@ -74,6 +76,7 @@ import {
|
|
|
74
76
|
} from '../content-list-controller.js';
|
|
75
77
|
import {
|
|
76
78
|
CONTENT_LIST_QUERY_DEFAULT_PAGE_SIZE,
|
|
79
|
+
type ContentListDataQueryRequest,
|
|
77
80
|
type ContentListQueryDrop,
|
|
78
81
|
type ContentListQueryDropReason,
|
|
79
82
|
type ContentListQueryNotices,
|
|
@@ -94,6 +97,7 @@ import {
|
|
|
94
97
|
type ContentListJob,
|
|
95
98
|
type ContentListJobBinding,
|
|
96
99
|
type ContentListJobSnapshot,
|
|
100
|
+
type ContentListJobTarget,
|
|
97
101
|
} from '../content-list-runtime.js';
|
|
98
102
|
import {
|
|
99
103
|
type ContentListSavedView,
|
|
@@ -101,6 +105,15 @@ import {
|
|
|
101
105
|
restoreContentListSavedView,
|
|
102
106
|
toContentListSavedViewInput,
|
|
103
107
|
} from '../content-list-saved-views.js';
|
|
108
|
+
import {
|
|
109
|
+
type ContentListWorkflowBinding,
|
|
110
|
+
ContentListWorkflowError,
|
|
111
|
+
type ContentListWorkflowErrorReason,
|
|
112
|
+
type ContentListWorkflowId,
|
|
113
|
+
type ContentListWorkflowRequest,
|
|
114
|
+
CONTENT_LIST_WORKFLOW_OPTIONS,
|
|
115
|
+
contentListWorkflowOutcomes,
|
|
116
|
+
} from '../content-list-workflows.js';
|
|
104
117
|
import {
|
|
105
118
|
applyContentListViewState,
|
|
106
119
|
type ContentListStateDrop,
|
|
@@ -114,10 +127,170 @@ import ImageThumbnail from './ImageThumbnail.svelte';
|
|
|
114
127
|
|
|
115
128
|
const { t } = useI18n();
|
|
116
129
|
|
|
130
|
+
const WORKFLOW_LABEL_MESSAGES: Record<
|
|
131
|
+
ContentListWorkflowId,
|
|
132
|
+
keyof typeof M
|
|
133
|
+
> = {
|
|
134
|
+
'move-to-trash': M['content.content_list.workflow_move_to_trash'],
|
|
135
|
+
'mark-draft': M['content.content_list.workflow_mark_draft'],
|
|
136
|
+
'submit-review': M['content.content_list.workflow_submit_review'],
|
|
137
|
+
publish: M['content.content_list.workflow_publish'],
|
|
138
|
+
archive: M['content.content_list.workflow_archive'],
|
|
139
|
+
restore: M['content.content_list.workflow_restore'],
|
|
140
|
+
'automated-review': M['content.content_list.workflow_automated_review'],
|
|
141
|
+
'format-body': M['content.content_list.workflow_format_body'],
|
|
142
|
+
categorize: M['content.content_list.workflow_categorize'],
|
|
143
|
+
optimize: M['content.content_list.workflow_optimize'],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const WORKFLOW_CONSEQUENCE_MESSAGES: Record<string, keyof typeof M> = {
|
|
147
|
+
'Content leaves active views and becomes restorable from trash.':
|
|
148
|
+
M['content.content_list.workflow_consequence_trash'],
|
|
149
|
+
'Published content is no longer public.':
|
|
150
|
+
M['content.content_list.workflow_consequence_unpublished'],
|
|
151
|
+
'Content enters the review queue.':
|
|
152
|
+
M['content.content_list.workflow_consequence_review_queue'],
|
|
153
|
+
'Content becomes public and may create a publication snapshot.':
|
|
154
|
+
M['content.content_list.workflow_consequence_published'],
|
|
155
|
+
'Content leaves trash.':
|
|
156
|
+
M['content.content_list.workflow_consequence_leave_trash'],
|
|
157
|
+
'Publishing makes content public.':
|
|
158
|
+
M['content.content_list.workflow_consequence_restore_published'],
|
|
159
|
+
'Creates a review result and may create a review version.':
|
|
160
|
+
M['content.content_list.workflow_consequence_review_result'],
|
|
161
|
+
'The persisted content body may change.':
|
|
162
|
+
M['content.content_list.workflow_consequence_body_change'],
|
|
163
|
+
'The content category changes.':
|
|
164
|
+
M['content.content_list.workflow_consequence_category_change'],
|
|
165
|
+
'Content fields may be revised or completed.':
|
|
166
|
+
M['content.content_list.workflow_consequence_fields_change'],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const WORKFLOW_REASON_MESSAGES: Record<string, keyof typeof M> = {
|
|
170
|
+
not_deleted: M['content.content_list.workflow_reason_not_deleted'],
|
|
171
|
+
publish_readiness_failed:
|
|
172
|
+
M['content.content_list.workflow_reason_publish_readiness_failed'],
|
|
173
|
+
deleted: M['content.content_list.workflow_reason_deleted'],
|
|
174
|
+
already_draft: M['content.content_list.workflow_reason_already_draft'],
|
|
175
|
+
requires_draft: M['content.content_list.workflow_reason_requires_draft'],
|
|
176
|
+
invalid_status: M['content.content_list.workflow_reason_invalid_status'],
|
|
177
|
+
already_archived: M['content.content_list.workflow_reason_already_archived'],
|
|
178
|
+
governance_unavailable:
|
|
179
|
+
M['content.content_list.workflow_reason_governance_unavailable'],
|
|
180
|
+
review_policy_unavailable:
|
|
181
|
+
M['content.content_list.workflow_reason_review_policy_unavailable'],
|
|
182
|
+
review_kind_mismatch:
|
|
183
|
+
M['content.content_list.workflow_reason_review_kind_mismatch'],
|
|
184
|
+
handler_unavailable:
|
|
185
|
+
M['content.content_list.workflow_reason_handler_unavailable'],
|
|
186
|
+
requires_draft_or_review:
|
|
187
|
+
M['content.content_list.workflow_reason_requires_draft_or_review'],
|
|
188
|
+
not_found_or_denied:
|
|
189
|
+
M['content.content_list.workflow_reason_not_found_or_denied'],
|
|
190
|
+
row_revision_drifted:
|
|
191
|
+
M['content.content_list.workflow_reason_row_revision_drifted'],
|
|
192
|
+
permission_denied:
|
|
193
|
+
M['content.content_list.workflow_reason_permission_denied'],
|
|
194
|
+
not_found: M['content.content_list.workflow_reason_not_found'],
|
|
195
|
+
stale_query_fingerprint:
|
|
196
|
+
M['content.content_list.workflow_reason_stale_query_fingerprint'],
|
|
197
|
+
matching_count_drifted:
|
|
198
|
+
M['content.content_list.workflow_reason_matching_count_drifted'],
|
|
199
|
+
limit_exceeded: M['content.content_list.workflow_reason_limit_exceeded'],
|
|
200
|
+
invalid_request: M['content.content_list.workflow_reason_invalid_request'],
|
|
201
|
+
invalid_payload: M['content.content_list.workflow_reason_invalid_payload'],
|
|
202
|
+
confirmation_required:
|
|
203
|
+
M['content.content_list.workflow_reason_confirmation_required'],
|
|
204
|
+
stale_preview: M['content.content_list.workflow_reason_stale_preview'],
|
|
205
|
+
denied: M['content.content_list.workflow_reason_denied'],
|
|
206
|
+
unknown_permission:
|
|
207
|
+
M['content.content_list.workflow_reason_unknown_permission'],
|
|
208
|
+
execution_failed:
|
|
209
|
+
M['content.content_list.workflow_reason_execution_failed'],
|
|
210
|
+
idempotency_conflict:
|
|
211
|
+
M['content.content_list.workflow_reason_idempotency_conflict'],
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const WORKFLOW_TRANSPORT_MESSAGES: Record<
|
|
215
|
+
ContentListWorkflowErrorReason,
|
|
216
|
+
keyof typeof M
|
|
217
|
+
> = {
|
|
218
|
+
network_failure: M['content.content_list.workflow_transport_network_failure'],
|
|
219
|
+
invalid_json: M['content.content_list.workflow_transport_invalid_json'],
|
|
220
|
+
http_error: M['content.content_list.workflow_transport_http_error'],
|
|
221
|
+
invalid_result: M['content.content_list.workflow_transport_invalid_result'],
|
|
222
|
+
invalid_job_status:
|
|
223
|
+
M['content.content_list.workflow_transport_invalid_job_status'],
|
|
224
|
+
invalid_job_result:
|
|
225
|
+
M['content.content_list.workflow_transport_invalid_job_result'],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
function workflowLabel(id: ContentListWorkflowId): string {
|
|
229
|
+
return t(WORKFLOW_LABEL_MESSAGES[id]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function workflowConsequenceLabel(consequence: string): string {
|
|
233
|
+
return t(
|
|
234
|
+
WORKFLOW_CONSEQUENCE_MESSAGES[consequence] ??
|
|
235
|
+
M['content.content_list.workflow_consequence_unknown'],
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function workflowReasonLabel(
|
|
240
|
+
reason: string | undefined,
|
|
241
|
+
fallback: keyof typeof M,
|
|
242
|
+
params?: Record<string, string | number>,
|
|
243
|
+
): string {
|
|
244
|
+
return t((reason && WORKFLOW_REASON_MESSAGES[reason]) ?? fallback, params);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function workflowExceptionMessage(
|
|
248
|
+
error: unknown,
|
|
249
|
+
fallback: keyof typeof M,
|
|
250
|
+
): string {
|
|
251
|
+
if (error instanceof ContentListWorkflowError) {
|
|
252
|
+
return t(WORKFLOW_TRANSPORT_MESSAGES[error.reason], {
|
|
253
|
+
status: error.status ?? '',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return t(fallback);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function workflowScopeLabel(scope: string): string {
|
|
260
|
+
switch (scope) {
|
|
261
|
+
case 'explicit-ids':
|
|
262
|
+
return t(M['content.content_list.workflow_scope_explicit_ids']);
|
|
263
|
+
case 'current-page':
|
|
264
|
+
return t(M['content.content_list.workflow_scope_current_page']);
|
|
265
|
+
case 'all-matching':
|
|
266
|
+
return t(M['content.content_list.workflow_scope_all_matching']);
|
|
267
|
+
default:
|
|
268
|
+
return scope;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function workflowJobStatusLabel(status: string): string {
|
|
273
|
+
switch (status) {
|
|
274
|
+
case 'queued':
|
|
275
|
+
return t(M['content.content_list.job_queued']);
|
|
276
|
+
case 'running':
|
|
277
|
+
return t(M['content.content_list.job_running']);
|
|
278
|
+
case 'succeeded':
|
|
279
|
+
return t(M['content.content_list.job_succeeded']);
|
|
280
|
+
case 'failed':
|
|
281
|
+
return t(M['content.content_list.job_failed']);
|
|
282
|
+
case 'cancelled':
|
|
283
|
+
return t(M['content.content_list.workflow_status_cancelled']);
|
|
284
|
+
default:
|
|
285
|
+
return status;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
117
289
|
/** One reported refusal, from a restore or from the query translation. */
|
|
118
290
|
type ContentListDropNotice = ContentListStateDrop | ContentListQueryDrop;
|
|
119
291
|
|
|
120
292
|
interface Props {
|
|
293
|
+
/** Base URL for content API requests. */
|
|
121
294
|
apiBaseUrl?: string;
|
|
122
295
|
/**
|
|
123
296
|
* Client-side rows. Ignored when `query` is supplied — the server then owns
|
|
@@ -125,12 +298,19 @@ interface Props {
|
|
|
125
298
|
* source of truth.
|
|
126
299
|
*/
|
|
127
300
|
contents?: ContentData[];
|
|
301
|
+
/** Filter content by this type; no filtering applied when omitted. */
|
|
128
302
|
type?: string;
|
|
303
|
+
/** Initial view layout mode for the content list. */
|
|
129
304
|
defaultViewMode?: ContentListViewMode;
|
|
305
|
+
/** Invoked when the user requests to edit a content item. */
|
|
130
306
|
onEdit: (content: ContentData) => void;
|
|
307
|
+
/** Invoked when the user requests to delete a content item. */
|
|
131
308
|
onDelete: (content: ContentData) => void;
|
|
309
|
+
/** Invoked when the user requests to create new content. */
|
|
132
310
|
onAdd: () => void;
|
|
311
|
+
/** Optional UI controls rendered above the content list. */
|
|
133
312
|
controls?: Snippet;
|
|
313
|
+
/** Returns a URL to view the published content; null hides the view link. */
|
|
134
314
|
getViewHref?: (content: ContentData) => string | null;
|
|
135
315
|
/** Announced uniformly by every presentation; #2455 extends it. */
|
|
136
316
|
loading?: boolean;
|
|
@@ -153,6 +333,8 @@ interface Props {
|
|
|
153
333
|
savedViews?: ContentListSavedViewStore;
|
|
154
334
|
/** Shared background-workflow state. The same binding guards submissions. */
|
|
155
335
|
jobs?: ContentListJobBinding;
|
|
336
|
+
/** Opt-in, authenticated preview/apply client for bulk workflows (#2453). */
|
|
337
|
+
workflows?: ContentListWorkflowBinding;
|
|
156
338
|
}
|
|
157
339
|
|
|
158
340
|
let {
|
|
@@ -173,6 +355,7 @@ let {
|
|
|
173
355
|
urlState = undefined,
|
|
174
356
|
savedViews = undefined,
|
|
175
357
|
jobs = undefined,
|
|
358
|
+
workflows = undefined,
|
|
176
359
|
}: Props = $props();
|
|
177
360
|
|
|
178
361
|
const initialQuery = untrack(() => query);
|
|
@@ -391,6 +574,34 @@ $effect(() => {
|
|
|
391
574
|
}
|
|
392
575
|
});
|
|
393
576
|
});
|
|
577
|
+
let settledWorkflowQuery = $state<ContentListDataQueryRequest | null>(null);
|
|
578
|
+
let settledWorkflowFingerprint = $state<string | null>(null);
|
|
579
|
+
let settledWorkflowRevision = $state<string | null>(null);
|
|
580
|
+
let selectedWorkflow = $state<ContentListWorkflowId>('mark-draft');
|
|
581
|
+
let workflowCategory = $state('');
|
|
582
|
+
let workflowRestoreStatus = $state<'draft' | 'review' | 'published'>('draft');
|
|
583
|
+
let workflowFormat = $state<'markdown' | 'html'>('markdown');
|
|
584
|
+
let workflowReviewKind = $state('');
|
|
585
|
+
let workflowPolicyKey = $state('');
|
|
586
|
+
let workflowInstructions = $state('');
|
|
587
|
+
let workflowPending = $state(false);
|
|
588
|
+
let pageHeaderSelectionOnly = $state(false);
|
|
589
|
+
let workflowPreview = $state<import('@happyvertical/smrt-ui/data').DataSurfaceActionResult | null>(null);
|
|
590
|
+
let workflowResult = $state<import('@happyvertical/smrt-ui/data').DataSurfaceActionResult | null>(null);
|
|
591
|
+
let workflowError = $state<string | null>(null);
|
|
592
|
+
let workflowConfirmOpen = $state(false);
|
|
593
|
+
let workflowIntentAtPreview = '';
|
|
594
|
+
let workflowIdempotencyKey = '';
|
|
595
|
+
let workflowQueuedJobs = $state<
|
|
596
|
+
Array<{
|
|
597
|
+
intent: string;
|
|
598
|
+
jobId: string;
|
|
599
|
+
actionId: ContentListWorkflowId;
|
|
600
|
+
requestId: string;
|
|
601
|
+
identity: DataSurfaceIdentity;
|
|
602
|
+
target: ContentListJobTarget;
|
|
603
|
+
}>
|
|
604
|
+
>([]);
|
|
394
605
|
|
|
395
606
|
$effect(() =>
|
|
396
607
|
controller.subscribe((transition) => {
|
|
@@ -637,6 +848,36 @@ function refreshQuery(): void {
|
|
|
637
848
|
})
|
|
638
849
|
.catch(() => undefined);
|
|
639
850
|
}
|
|
851
|
+
|
|
852
|
+
function settleWorkflowQueryResult(
|
|
853
|
+
result: unknown,
|
|
854
|
+
request: ContentListDataQueryRequest,
|
|
855
|
+
signature: string,
|
|
856
|
+
): void {
|
|
857
|
+
settledSignature = signature;
|
|
858
|
+
resultNotices = readContentListQueryNotices(result);
|
|
859
|
+
if (result !== null && typeof result === 'object' && !Array.isArray(result)) {
|
|
860
|
+
const envelope = result as Record<string, unknown>;
|
|
861
|
+
settledWorkflowQuery = request;
|
|
862
|
+
settledWorkflowFingerprint =
|
|
863
|
+
typeof envelope.queryFingerprint === 'string'
|
|
864
|
+
? envelope.queryFingerprint
|
|
865
|
+
: null;
|
|
866
|
+
const freshness = envelope.freshness;
|
|
867
|
+
settledWorkflowRevision =
|
|
868
|
+
freshness !== null &&
|
|
869
|
+
typeof freshness === 'object' &&
|
|
870
|
+
!Array.isArray(freshness) &&
|
|
871
|
+
typeof (freshness as Record<string, unknown>).asOf === 'string'
|
|
872
|
+
? String((freshness as Record<string, unknown>).asOf)
|
|
873
|
+
: null;
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
settledWorkflowQuery = null;
|
|
877
|
+
settledWorkflowFingerprint = null;
|
|
878
|
+
settledWorkflowRevision = null;
|
|
879
|
+
}
|
|
880
|
+
|
|
640
881
|
/**
|
|
641
882
|
* A retry re-reads the same query, so its answer replaces the rendered rows —
|
|
642
883
|
* and therefore has to replace the completeness flags that describe them too.
|
|
@@ -648,13 +889,20 @@ function refreshQuery(): void {
|
|
|
648
889
|
function retryQuery(): void {
|
|
649
890
|
if (!queryBinding) return;
|
|
650
891
|
const signature = executedSignature;
|
|
892
|
+
const request = executedWorkflowQuery;
|
|
651
893
|
void queryBinding
|
|
652
894
|
.retry()
|
|
653
895
|
.then((result) => {
|
|
654
896
|
// `retry()` resolves undefined when there is no request to repeat, and a
|
|
655
897
|
// newer query may have superseded this one while it was in flight.
|
|
656
|
-
if (
|
|
657
|
-
|
|
898
|
+
if (
|
|
899
|
+
result === undefined ||
|
|
900
|
+
request === null ||
|
|
901
|
+
executedSignature !== signature ||
|
|
902
|
+
signature === undefined
|
|
903
|
+
)
|
|
904
|
+
return;
|
|
905
|
+
settleWorkflowQueryResult(result, request, signature);
|
|
658
906
|
})
|
|
659
907
|
.catch(() => undefined);
|
|
660
908
|
}
|
|
@@ -680,9 +928,9 @@ const querySignature = $derived(
|
|
|
680
928
|
);
|
|
681
929
|
|
|
682
930
|
let executedSignature: string | undefined;
|
|
931
|
+
let executedWorkflowQuery: ContentListDataQueryRequest | null = null;
|
|
683
932
|
let publishedUrlSignature: string | undefined;
|
|
684
933
|
|
|
685
|
-
|
|
686
934
|
$effect(() => {
|
|
687
935
|
const signature = querySignature;
|
|
688
936
|
untrack(() => {
|
|
@@ -727,15 +975,16 @@ $effect(() => {
|
|
|
727
975
|
// unhandled one. The resolved envelope carries the server's completeness
|
|
728
976
|
// flags, which the binding itself does not expose.
|
|
729
977
|
activeQueryKey = contentListQueryRequestKey(translated.request);
|
|
978
|
+
executedWorkflowQuery = translated.request;
|
|
730
979
|
void queryBinding
|
|
731
980
|
.execute(translated.request)
|
|
732
981
|
.then((result) => {
|
|
733
982
|
// A newer query may have superseded this one while it was in flight.
|
|
734
983
|
if (executedSignature !== signature) return;
|
|
735
984
|
// The binding's rows and total now describe THIS query, so the page
|
|
736
|
-
// may be clamped against them
|
|
737
|
-
|
|
738
|
-
|
|
985
|
+
// may be clamped against them and workflows may bind to the exact
|
|
986
|
+
// settled request.
|
|
987
|
+
settleWorkflowQueryResult(result, translated.request, signature);
|
|
739
988
|
})
|
|
740
989
|
.catch(() => undefined);
|
|
741
990
|
}
|
|
@@ -833,15 +1082,48 @@ const identifiedRowKeys = $derived(
|
|
|
833
1082
|
const selectablePageRowIds = $derived(
|
|
834
1083
|
selectableContentListRowIds(pageRows.filter((row) => !rowPending(row))),
|
|
835
1084
|
);
|
|
1085
|
+
const allMatchingSelected = $derived(tableState.selection.scope === 'allMatching');
|
|
836
1086
|
const allPageSelected = $derived(
|
|
837
|
-
|
|
838
|
-
selectablePageRowIds.
|
|
1087
|
+
allMatchingSelected ||
|
|
1088
|
+
(selectablePageRowIds.length > 0 &&
|
|
1089
|
+
selectablePageRowIds.every((rowId) => selectedRowKeys.has(String(rowId)))),
|
|
839
1090
|
);
|
|
840
1091
|
const somePageSelected = $derived(
|
|
841
1092
|
!allPageSelected &&
|
|
842
1093
|
selectablePageRowIds.some((rowId) => selectedRowKeys.has(String(rowId))),
|
|
843
1094
|
);
|
|
844
|
-
const selectedCount = $derived(
|
|
1095
|
+
const selectedCount = $derived(
|
|
1096
|
+
tableState.selection.scope === 'allMatching'
|
|
1097
|
+
? tableState.selection.expectedCount
|
|
1098
|
+
: tableState.selectedRowIds.length,
|
|
1099
|
+
);
|
|
1100
|
+
const exactMatchingCount = $derived(
|
|
1101
|
+
settledSignature === querySignature
|
|
1102
|
+
? contentListQueryExactTotal(queryBinding?.total)
|
|
1103
|
+
: undefined,
|
|
1104
|
+
);
|
|
1105
|
+
const canSelectAllMatching = $derived(
|
|
1106
|
+
Boolean(
|
|
1107
|
+
workflows &&
|
|
1108
|
+
serverBacked &&
|
|
1109
|
+
settledWorkflowQuery &&
|
|
1110
|
+
settledWorkflowFingerprint &&
|
|
1111
|
+
settledWorkflowRevision &&
|
|
1112
|
+
exactMatchingCount !== undefined &&
|
|
1113
|
+
exactMatchingCount > 0 &&
|
|
1114
|
+
exactMatchingCount <= (workflows?.maxSelectionSize ?? 200),
|
|
1115
|
+
),
|
|
1116
|
+
);
|
|
1117
|
+
const workflowPayloadValid = $derived(
|
|
1118
|
+
selectedWorkflow !== 'categorize' || workflowCategory.trim().length > 0,
|
|
1119
|
+
);
|
|
1120
|
+
const workflowDuplicateQueued = $derived(
|
|
1121
|
+
workflowQueuedJobs.some((job) => job.intent === workflowIntentSignature()),
|
|
1122
|
+
);
|
|
1123
|
+
const workflowQueuedJob = $derived(
|
|
1124
|
+
workflowQueuedJobs.find((job) => job.intent === workflowIntentSignature()) ??
|
|
1125
|
+
workflowQueuedJobs[0],
|
|
1126
|
+
);
|
|
845
1127
|
|
|
846
1128
|
/** Synthetic ids the adapter minted for rows that carry no durable identity. */
|
|
847
1129
|
const unidentifiedRowKeys = $derived(
|
|
@@ -976,7 +1258,22 @@ const surfaceOptions = $derived(
|
|
|
976
1258
|
registry: dataSurface.registry,
|
|
977
1259
|
descriptor:
|
|
978
1260
|
dataSurface.descriptor ??
|
|
979
|
-
|
|
1261
|
+
(() => {
|
|
1262
|
+
const descriptor = buildContentListSurfaceDescriptor({
|
|
1263
|
+
columnLabels,
|
|
1264
|
+
includeWorkflows: workflows !== undefined,
|
|
1265
|
+
});
|
|
1266
|
+
return {
|
|
1267
|
+
...descriptor,
|
|
1268
|
+
identity: workflows?.identity ?? descriptor.identity,
|
|
1269
|
+
limits: {
|
|
1270
|
+
...descriptor.limits,
|
|
1271
|
+
...(workflows?.maxSelectionSize === undefined
|
|
1272
|
+
? {}
|
|
1273
|
+
: { maxSelectionSize: workflows.maxSelectionSize }),
|
|
1274
|
+
},
|
|
1275
|
+
};
|
|
1276
|
+
})(),
|
|
980
1277
|
}
|
|
981
1278
|
: undefined,
|
|
982
1279
|
);
|
|
@@ -1154,14 +1451,19 @@ async function deleteSelectedView() {
|
|
|
1154
1451
|
}
|
|
1155
1452
|
|
|
1156
1453
|
function isSelected(row: ContentListRow): boolean {
|
|
1157
|
-
return selectedRowKeys.has(String(row.id));
|
|
1454
|
+
return allMatchingSelected || selectedRowKeys.has(String(row.id));
|
|
1158
1455
|
}
|
|
1159
1456
|
|
|
1160
1457
|
function rowPending(row: ContentListRow): boolean {
|
|
1161
1458
|
return (
|
|
1162
1459
|
jobSnapshot.pendingRowIds.has(String(row.id)) ||
|
|
1163
1460
|
(activeQueryKey !== undefined &&
|
|
1164
|
-
jobSnapshot.pendingQueryKeys.has(activeQueryKey))
|
|
1461
|
+
jobSnapshot.pendingQueryKeys.has(activeQueryKey)) ||
|
|
1462
|
+
workflowQueuedJobs.some((job) =>
|
|
1463
|
+
job.target.kind === 'query'
|
|
1464
|
+
? activeQueryKey !== undefined && job.target.queryKey === activeQueryKey
|
|
1465
|
+
: job.target.rowIds.includes(String(row.id)),
|
|
1466
|
+
)
|
|
1165
1467
|
);
|
|
1166
1468
|
}
|
|
1167
1469
|
|
|
@@ -1185,29 +1487,558 @@ function formattedLastUpdated(value: number): string {
|
|
|
1185
1487
|
}
|
|
1186
1488
|
|
|
1187
1489
|
function toggleRow(row: ContentListRow) {
|
|
1188
|
-
if (
|
|
1490
|
+
if (
|
|
1491
|
+
workflowPending ||
|
|
1492
|
+
!row.identified ||
|
|
1493
|
+
rowPending(row) ||
|
|
1494
|
+
allMatchingSelected
|
|
1495
|
+
)
|
|
1496
|
+
return;
|
|
1497
|
+
pageHeaderSelectionOnly = false;
|
|
1189
1498
|
controller.dispatch({ type: 'toggleRowSelection', rowId: row.id });
|
|
1190
1499
|
}
|
|
1191
1500
|
|
|
1192
1501
|
function togglePageSelection() {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1502
|
+
if (workflowPending) return;
|
|
1503
|
+
if (allMatchingSelected) {
|
|
1504
|
+
clearSelection();
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
const rowKey = (rowId: string | number) =>
|
|
1508
|
+
`${typeof rowId}:${String(rowId)}`;
|
|
1509
|
+
const pageKeys = new Set(selectablePageRowIds.map(rowKey));
|
|
1510
|
+
if (allPageSelected) {
|
|
1511
|
+
pageHeaderSelectionOnly = false;
|
|
1512
|
+
controller.dispatch({
|
|
1513
|
+
type: 'setSelectedRows',
|
|
1514
|
+
rowIds: tableState.selectedRowIds.filter(
|
|
1515
|
+
(rowId) => !pageKeys.has(rowKey(rowId)),
|
|
1197
1516
|
),
|
|
1517
|
+
});
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const selected = new Map(
|
|
1521
|
+
tableState.selectedRowIds.map((rowId) => [rowKey(rowId), rowId]),
|
|
1198
1522
|
);
|
|
1523
|
+
pageHeaderSelectionOnly =
|
|
1524
|
+
serverBacked && tableState.selectedRowIds.length === 0;
|
|
1525
|
+
for (const rowId of selectablePageRowIds) selected.set(rowKey(rowId), rowId);
|
|
1199
1526
|
controller.dispatch({
|
|
1200
1527
|
type: 'setSelectedRows',
|
|
1201
|
-
rowIds:
|
|
1202
|
-
|
|
1203
|
-
|
|
1528
|
+
rowIds: [...selected.values()],
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function selectAllMatching() {
|
|
1533
|
+
if (
|
|
1534
|
+
workflowPending ||
|
|
1535
|
+
!canSelectAllMatching ||
|
|
1536
|
+
!settledWorkflowFingerprint ||
|
|
1537
|
+
!settledWorkflowRevision ||
|
|
1538
|
+
exactMatchingCount === undefined
|
|
1539
|
+
) return;
|
|
1540
|
+
pageHeaderSelectionOnly = false;
|
|
1541
|
+
controller.dispatch({
|
|
1542
|
+
type: 'selectAllMatching',
|
|
1543
|
+
queryFingerprint: settledWorkflowFingerprint,
|
|
1544
|
+
queryRevision: settledWorkflowRevision,
|
|
1545
|
+
expectedCount: exactMatchingCount,
|
|
1204
1546
|
});
|
|
1205
1547
|
}
|
|
1206
1548
|
|
|
1207
1549
|
function clearSelection() {
|
|
1550
|
+
if (workflowPending) return;
|
|
1551
|
+
pageHeaderSelectionOnly = false;
|
|
1208
1552
|
controller.dispatch({ type: 'setSelectedRows', rowIds: [] });
|
|
1209
1553
|
}
|
|
1210
1554
|
|
|
1555
|
+
function workflowPayload(): Record<string, string> | undefined {
|
|
1556
|
+
switch (selectedWorkflow) {
|
|
1557
|
+
case 'categorize':
|
|
1558
|
+
return { category: workflowCategory.trim() };
|
|
1559
|
+
case 'restore':
|
|
1560
|
+
return { status: workflowRestoreStatus };
|
|
1561
|
+
case 'format-body':
|
|
1562
|
+
return { format: workflowFormat };
|
|
1563
|
+
case 'automated-review': {
|
|
1564
|
+
const payload: Record<string, string> = {};
|
|
1565
|
+
if (workflowReviewKind.trim()) payload.kind = workflowReviewKind.trim();
|
|
1566
|
+
if (workflowPolicyKey.trim()) payload.policyKey = workflowPolicyKey.trim();
|
|
1567
|
+
return payload;
|
|
1568
|
+
}
|
|
1569
|
+
case 'optimize':
|
|
1570
|
+
return workflowInstructions.trim()
|
|
1571
|
+
? { instructions: workflowInstructions.trim() }
|
|
1572
|
+
: {};
|
|
1573
|
+
default:
|
|
1574
|
+
return {};
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function workflowSelection(): ContentListWorkflowRequest['selection'] | null {
|
|
1579
|
+
const selection = tableState.selection;
|
|
1580
|
+
if (selection.scope === 'allMatching') {
|
|
1581
|
+
return { scope: 'all-matching', queryFingerprint: selection.queryFingerprint };
|
|
1582
|
+
}
|
|
1583
|
+
const currentPageKeys = new Set(pageRows.map((row) => String(row.id)));
|
|
1584
|
+
const selectablePageKeys = new Set(selectablePageRowIds.map(String));
|
|
1585
|
+
const effectiveRowIds =
|
|
1586
|
+
selection.scope === 'page' || pageHeaderSelectionOnly
|
|
1587
|
+
? selection.rowIds.filter((rowId) => {
|
|
1588
|
+
const key = String(rowId);
|
|
1589
|
+
return !currentPageKeys.has(key) || selectablePageKeys.has(key);
|
|
1590
|
+
})
|
|
1591
|
+
: selection.rowIds;
|
|
1592
|
+
if (
|
|
1593
|
+
serverBacked &&
|
|
1594
|
+
pageHeaderSelectionOnly &&
|
|
1595
|
+
selectablePageRowIds.length === pageRows.length &&
|
|
1596
|
+
effectiveRowIds.length === selectablePageRowIds.length &&
|
|
1597
|
+
selectablePageRowIds.every((rowId) =>
|
|
1598
|
+
effectiveRowIds.some(
|
|
1599
|
+
(selectedId) => String(selectedId) === String(rowId),
|
|
1600
|
+
),
|
|
1601
|
+
)
|
|
1602
|
+
) {
|
|
1603
|
+
return { scope: 'current-page' };
|
|
1604
|
+
}
|
|
1605
|
+
if (effectiveRowIds.length === 0) return null;
|
|
1606
|
+
return { scope: 'explicit-ids', rowIds: effectiveRowIds };
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function workflowJobTarget(
|
|
1610
|
+
request: ContentListWorkflowRequest,
|
|
1611
|
+
): ContentListJobTarget {
|
|
1612
|
+
if (request.selection.scope === 'all-matching' && activeQueryKey !== undefined)
|
|
1613
|
+
return { kind: 'query', queryKey: activeQueryKey };
|
|
1614
|
+
if (request.selection.scope === 'explicit-ids')
|
|
1615
|
+
return { kind: 'rows', rowIds: request.selection.rowIds.map(String) };
|
|
1616
|
+
return { kind: 'rows', rowIds: selectablePageRowIds.map(String) };
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function workflowIntentSignature(): string {
|
|
1620
|
+
const selection = workflowSelection();
|
|
1621
|
+
if (selection?.scope === 'explicit-ids') {
|
|
1622
|
+
return JSON.stringify([
|
|
1623
|
+
selectedWorkflow,
|
|
1624
|
+
workflowPayload(),
|
|
1625
|
+
{
|
|
1626
|
+
scope: selection.scope,
|
|
1627
|
+
rowIds: [...selection.rowIds].sort((left, right) => {
|
|
1628
|
+
const leftKey = `${typeof left}:${String(left)}`;
|
|
1629
|
+
const rightKey = `${typeof right}:${String(right)}`;
|
|
1630
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
1631
|
+
}),
|
|
1632
|
+
},
|
|
1633
|
+
]);
|
|
1634
|
+
}
|
|
1635
|
+
return JSON.stringify([
|
|
1636
|
+
selectedWorkflow,
|
|
1637
|
+
workflowPayload(),
|
|
1638
|
+
tableState.selection,
|
|
1639
|
+
settledWorkflowFingerprint,
|
|
1640
|
+
querySignature,
|
|
1641
|
+
]);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function createWorkflowRequest(
|
|
1645
|
+
phase: 'preview' | 'apply',
|
|
1646
|
+
): ContentListWorkflowRequest | null {
|
|
1647
|
+
const selection = workflowSelection();
|
|
1648
|
+
if (!selection) return null;
|
|
1649
|
+
const queryRequired = selection.scope !== 'explicit-ids';
|
|
1650
|
+
if (
|
|
1651
|
+
queryRequired &&
|
|
1652
|
+
(!settledWorkflowQuery || settledSignature !== querySignature)
|
|
1653
|
+
) return null;
|
|
1654
|
+
if (
|
|
1655
|
+
selection.scope === 'all-matching' &&
|
|
1656
|
+
selection.queryFingerprint !== settledWorkflowFingerprint
|
|
1657
|
+
) return null;
|
|
1658
|
+
return {
|
|
1659
|
+
version: 1,
|
|
1660
|
+
requestId: globalThis.crypto?.randomUUID?.() ?? `content-workflow-${Date.now()}`,
|
|
1661
|
+
identity: workflows?.identity ?? { surfaceId: 'content-list', kind: 'table' },
|
|
1662
|
+
actionId: selectedWorkflow,
|
|
1663
|
+
phase,
|
|
1664
|
+
selection,
|
|
1665
|
+
payload: workflowPayload(),
|
|
1666
|
+
expectedRevision: workflows?.revision ?? 0,
|
|
1667
|
+
target: {
|
|
1668
|
+
...(queryRequired && settledWorkflowQuery
|
|
1669
|
+
? { query: settledWorkflowQuery }
|
|
1670
|
+
: {}),
|
|
1671
|
+
expectedCount:
|
|
1672
|
+
selection.scope === 'explicit-ids'
|
|
1673
|
+
? selection.rowIds.length
|
|
1674
|
+
: selectedCount,
|
|
1675
|
+
},
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function workflowIdentityMatches(
|
|
1680
|
+
actual: DataSurfaceIdentity | null | undefined,
|
|
1681
|
+
expected: DataSurfaceIdentity,
|
|
1682
|
+
): boolean {
|
|
1683
|
+
return (
|
|
1684
|
+
actual !== null &&
|
|
1685
|
+
actual !== undefined &&
|
|
1686
|
+
actual.surfaceId === expected.surfaceId &&
|
|
1687
|
+
actual.kind === expected.kind &&
|
|
1688
|
+
(expected.subject
|
|
1689
|
+
? actual.subject?.type === expected.subject.type &&
|
|
1690
|
+
actual.subject.id === expected.subject.id
|
|
1691
|
+
: actual.subject === undefined)
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function workflowResultMatchesRequest(
|
|
1696
|
+
result: DataSurfaceActionResult,
|
|
1697
|
+
request: ContentListWorkflowRequest,
|
|
1698
|
+
): boolean {
|
|
1699
|
+
return (
|
|
1700
|
+
result.version === 1 &&
|
|
1701
|
+
result.requestId === request.requestId &&
|
|
1702
|
+
result.phase === request.phase &&
|
|
1703
|
+
result.actionId === request.actionId &&
|
|
1704
|
+
workflowIdentityMatches(result.identity, request.identity)
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function workflowResultMessage(result: import('@happyvertical/smrt-ui/data').DataSurfaceActionResult): string {
|
|
1709
|
+
const details = result.details ?? {};
|
|
1710
|
+
const accepted = typeof details.accepted === 'number' ? details.accepted : 0;
|
|
1711
|
+
if (details.background === true) {
|
|
1712
|
+
return t(M['content.content_list.workflow_result_background'], {
|
|
1713
|
+
accepted,
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
const skipped = typeof details.skipped === 'number' ? details.skipped : 0;
|
|
1717
|
+
const failed = typeof details.failed === 'number' ? details.failed : 0;
|
|
1718
|
+
return t(M['content.content_list.workflow_result_summary'], {
|
|
1719
|
+
accepted,
|
|
1720
|
+
skipped,
|
|
1721
|
+
failed,
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
function applyWorkflowSelectionOutcomes(
|
|
1726
|
+
result: import('@happyvertical/smrt-ui/data').DataSurfaceActionResult,
|
|
1727
|
+
): boolean {
|
|
1728
|
+
const outcomes = contentListWorkflowOutcomes(result);
|
|
1729
|
+
const rowKey = (rowId: string | number) =>
|
|
1730
|
+
`${typeof rowId}:${String(rowId)}`;
|
|
1731
|
+
if (tableState.selection.scope === 'allMatching') {
|
|
1732
|
+
const uniqueOutcomes = new Set(outcomes.map((outcome) => rowKey(outcome.rowId)));
|
|
1733
|
+
if (
|
|
1734
|
+
outcomes.length !== tableState.selection.expectedCount ||
|
|
1735
|
+
uniqueOutcomes.size !== outcomes.length
|
|
1736
|
+
) {
|
|
1737
|
+
workflowError = t(
|
|
1738
|
+
M['content.content_list.workflow_incomplete_outcomes'],
|
|
1739
|
+
);
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
controller.dispatch({
|
|
1743
|
+
type: 'setSelectedRows',
|
|
1744
|
+
rowIds: outcomes
|
|
1745
|
+
.filter((outcome) => outcome.status !== 'accepted')
|
|
1746
|
+
.map((outcome) => outcome.rowId),
|
|
1747
|
+
});
|
|
1748
|
+
return true;
|
|
1749
|
+
}
|
|
1750
|
+
const accepted = new Set(
|
|
1751
|
+
outcomes
|
|
1752
|
+
.filter((outcome) => outcome.status === 'accepted')
|
|
1753
|
+
.map((outcome) => rowKey(outcome.rowId)),
|
|
1754
|
+
);
|
|
1755
|
+
controller.dispatch({
|
|
1756
|
+
type: 'setSelectedRows',
|
|
1757
|
+
rowIds: tableState.selectedRowIds.filter(
|
|
1758
|
+
(rowId) => !accepted.has(rowKey(rowId)),
|
|
1759
|
+
),
|
|
1760
|
+
});
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function workflowPreviewMessage(): string {
|
|
1765
|
+
const details = workflowPreview?.details ?? {};
|
|
1766
|
+
const count = typeof details.count === 'number' ? details.count : selectedCount;
|
|
1767
|
+
const labels = Array.isArray(details.representativeLabels)
|
|
1768
|
+
? details.representativeLabels.filter((entry): entry is string => typeof entry === 'string')
|
|
1769
|
+
: [];
|
|
1770
|
+
const consequences = Array.isArray(details.consequences)
|
|
1771
|
+
? details.consequences.filter((entry): entry is string => typeof entry === 'string')
|
|
1772
|
+
: [];
|
|
1773
|
+
const skipped = typeof details.skipped === 'number' ? details.skipped : 0;
|
|
1774
|
+
const resolvedScope =
|
|
1775
|
+
typeof details.resolvedScope === 'string'
|
|
1776
|
+
? details.resolvedScope
|
|
1777
|
+
: workflowSelection()?.scope;
|
|
1778
|
+
const ineligible = Array.isArray(details.ineligible)
|
|
1779
|
+
? details.ineligible.flatMap((entry) => {
|
|
1780
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
1781
|
+
return [];
|
|
1782
|
+
const outcome = entry as Record<string, unknown>;
|
|
1783
|
+
if (
|
|
1784
|
+
typeof outcome.rowId !== 'string' &&
|
|
1785
|
+
typeof outcome.rowId !== 'number'
|
|
1786
|
+
)
|
|
1787
|
+
return [];
|
|
1788
|
+
const reason =
|
|
1789
|
+
typeof outcome.reason === 'string'
|
|
1790
|
+
? ` (${workflowReasonLabel(
|
|
1791
|
+
outcome.reason,
|
|
1792
|
+
M['content.content_list.workflow_reason_unknown'],
|
|
1793
|
+
)})`
|
|
1794
|
+
: '';
|
|
1795
|
+
return [`${String(outcome.rowId)}${reason}`];
|
|
1796
|
+
})
|
|
1797
|
+
: [];
|
|
1798
|
+
return [
|
|
1799
|
+
resolvedScope
|
|
1800
|
+
? t(M['content.content_list.workflow_preview_scope'], {
|
|
1801
|
+
scope: workflowScopeLabel(resolvedScope),
|
|
1802
|
+
})
|
|
1803
|
+
: '',
|
|
1804
|
+
t(
|
|
1805
|
+
count === 1
|
|
1806
|
+
? M['content.content_list.workflow_preview_count_one']
|
|
1807
|
+
: M['content.content_list.workflow_preview_count_many'],
|
|
1808
|
+
{ count },
|
|
1809
|
+
),
|
|
1810
|
+
labels.length
|
|
1811
|
+
? t(M['content.content_list.workflow_preview_examples'], {
|
|
1812
|
+
labels: labels.join(', '),
|
|
1813
|
+
})
|
|
1814
|
+
: '',
|
|
1815
|
+
skipped
|
|
1816
|
+
? t(M['content.content_list.workflow_preview_ineligible_count'], {
|
|
1817
|
+
count: skipped,
|
|
1818
|
+
})
|
|
1819
|
+
: '',
|
|
1820
|
+
ineligible.length
|
|
1821
|
+
? t(M['content.content_list.workflow_preview_ineligible'], {
|
|
1822
|
+
items: ineligible.join(', '),
|
|
1823
|
+
})
|
|
1824
|
+
: '',
|
|
1825
|
+
...consequences.map(workflowConsequenceLabel),
|
|
1826
|
+
].filter(Boolean).join(' ');
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
async function previewWorkflow() {
|
|
1830
|
+
if (!workflows || workflowPending || workflowDuplicateQueued) return;
|
|
1831
|
+
const request = createWorkflowRequest('preview');
|
|
1832
|
+
if (!request) return;
|
|
1833
|
+
workflowPending = true;
|
|
1834
|
+
workflowError = null;
|
|
1835
|
+
workflowResult = null;
|
|
1836
|
+
workflowIdempotencyKey = '';
|
|
1837
|
+
try {
|
|
1838
|
+
const result = await workflows.client.preview(request);
|
|
1839
|
+
if (!workflowResultMatchesRequest(result, request)) {
|
|
1840
|
+
workflowError = t(M['content.content_list.workflow_preview_mismatch']);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (!result.ok) {
|
|
1844
|
+
workflowError = workflowReasonLabel(
|
|
1845
|
+
result.reason,
|
|
1846
|
+
M['content.content_list.workflow_preview_failed'],
|
|
1847
|
+
);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
workflowPreview = result;
|
|
1851
|
+
workflowIntentAtPreview = workflowIntentSignature();
|
|
1852
|
+
workflowIdempotencyKey =
|
|
1853
|
+
globalThis.crypto?.randomUUID?.() ?? `content-workflow-apply-${Date.now()}`;
|
|
1854
|
+
workflowConfirmOpen = true;
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
workflowError = workflowExceptionMessage(
|
|
1857
|
+
error,
|
|
1858
|
+
M['content.content_list.workflow_preview_failed'],
|
|
1859
|
+
);
|
|
1860
|
+
} finally {
|
|
1861
|
+
workflowPending = false;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
async function applyWorkflow() {
|
|
1866
|
+
if (!workflows || workflowPending || !workflowPreview?.confirmationToken) return;
|
|
1867
|
+
if (workflowIntentAtPreview !== workflowIntentSignature()) {
|
|
1868
|
+
workflowConfirmOpen = false;
|
|
1869
|
+
workflowPreview = null;
|
|
1870
|
+
workflowIdempotencyKey = '';
|
|
1871
|
+
workflowError = t(M['content.content_list.workflow_selection_changed']);
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
const request = createWorkflowRequest('apply');
|
|
1875
|
+
if (!request) return;
|
|
1876
|
+
request.confirmationToken = workflowPreview.confirmationToken;
|
|
1877
|
+
request.idempotencyKey = workflowIdempotencyKey;
|
|
1878
|
+
const appliedIntent = workflowIntentAtPreview;
|
|
1879
|
+
const appliedTarget = workflowJobTarget(request);
|
|
1880
|
+
workflowPending = true;
|
|
1881
|
+
workflowError = null;
|
|
1882
|
+
try {
|
|
1883
|
+
const result = await workflows.client.apply(request);
|
|
1884
|
+
if (!workflowResultMatchesRequest(result, request)) {
|
|
1885
|
+
workflowError = t(M['content.content_list.workflow_apply_mismatch']);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
if (appliedIntent !== workflowIntentSignature()) {
|
|
1889
|
+
if (
|
|
1890
|
+
result.ok &&
|
|
1891
|
+
result.details?.background === true &&
|
|
1892
|
+
typeof result.details.jobId === 'string' &&
|
|
1893
|
+
!workflowQueuedJobs.some((job) => job.intent === appliedIntent)
|
|
1894
|
+
) {
|
|
1895
|
+
workflowQueuedJobs = [
|
|
1896
|
+
...workflowQueuedJobs,
|
|
1897
|
+
{
|
|
1898
|
+
intent: appliedIntent,
|
|
1899
|
+
jobId: result.details.jobId,
|
|
1900
|
+
actionId: request.actionId,
|
|
1901
|
+
requestId:
|
|
1902
|
+
typeof result.details.jobRequestId === 'string'
|
|
1903
|
+
? result.details.jobRequestId
|
|
1904
|
+
: request.requestId,
|
|
1905
|
+
identity: request.identity,
|
|
1906
|
+
target: appliedTarget,
|
|
1907
|
+
},
|
|
1908
|
+
];
|
|
1909
|
+
}
|
|
1910
|
+
workflowError = t(M['content.content_list.workflow_changed_applying']);
|
|
1911
|
+
workflowPreview = null;
|
|
1912
|
+
workflowIdempotencyKey = '';
|
|
1913
|
+
workflowConfirmOpen = false;
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
workflowResult = result;
|
|
1917
|
+
if (!result.ok) {
|
|
1918
|
+
workflowError = workflowReasonLabel(
|
|
1919
|
+
result.reason,
|
|
1920
|
+
M['content.content_list.workflow_failed'],
|
|
1921
|
+
);
|
|
1922
|
+
workflowPreview = null;
|
|
1923
|
+
workflowIdempotencyKey = '';
|
|
1924
|
+
workflowConfirmOpen = false;
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (result.details?.background !== true) {
|
|
1928
|
+
applyWorkflowSelectionOutcomes(result);
|
|
1929
|
+
refreshQuery();
|
|
1930
|
+
} else if (
|
|
1931
|
+
typeof result.details.jobId === 'string' &&
|
|
1932
|
+
!workflowQueuedJobs.some((job) => job.intent === workflowIntentAtPreview)
|
|
1933
|
+
) {
|
|
1934
|
+
workflowQueuedJobs = [
|
|
1935
|
+
...workflowQueuedJobs,
|
|
1936
|
+
{
|
|
1937
|
+
intent: workflowIntentAtPreview,
|
|
1938
|
+
jobId: result.details.jobId,
|
|
1939
|
+
actionId: request.actionId,
|
|
1940
|
+
requestId:
|
|
1941
|
+
typeof result.details.jobRequestId === 'string'
|
|
1942
|
+
? result.details.jobRequestId
|
|
1943
|
+
: request.requestId,
|
|
1944
|
+
identity: request.identity,
|
|
1945
|
+
target: appliedTarget,
|
|
1946
|
+
},
|
|
1947
|
+
];
|
|
1948
|
+
}
|
|
1949
|
+
workflowPreview = null;
|
|
1950
|
+
workflowIdempotencyKey = '';
|
|
1951
|
+
workflowConfirmOpen = false;
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
workflowError = workflowExceptionMessage(
|
|
1954
|
+
error,
|
|
1955
|
+
M['content.content_list.workflow_failed'],
|
|
1956
|
+
);
|
|
1957
|
+
} finally {
|
|
1958
|
+
workflowPending = false;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
async function checkWorkflowJob() {
|
|
1963
|
+
if (!workflows?.client.status || !workflowQueuedJob || workflowPending) return;
|
|
1964
|
+
const queuedJob = workflowQueuedJob;
|
|
1965
|
+
workflowPending = true;
|
|
1966
|
+
workflowError = null;
|
|
1967
|
+
try {
|
|
1968
|
+
const job = await workflows.client.status(queuedJob.jobId);
|
|
1969
|
+
const matchesCurrentIntent =
|
|
1970
|
+
queuedJob.intent === workflowIntentSignature();
|
|
1971
|
+
if (job.status === 'queued' || job.status === 'running') {
|
|
1972
|
+
workflowError = t(M['content.content_list.workflow_job_still'], {
|
|
1973
|
+
id: job.jobId,
|
|
1974
|
+
status: workflowJobStatusLabel(job.status),
|
|
1975
|
+
});
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (job.status === 'succeeded' && !job.result) {
|
|
1979
|
+
workflowError = t(M['content.content_list.workflow_job_missing_result'], {
|
|
1980
|
+
id: job.jobId,
|
|
1981
|
+
});
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (
|
|
1985
|
+
job.result &&
|
|
1986
|
+
(job.result.version !== 1 ||
|
|
1987
|
+
job.result.phase !== 'apply' ||
|
|
1988
|
+
job.result.actionId !== queuedJob.actionId ||
|
|
1989
|
+
job.result.requestId !== queuedJob.requestId ||
|
|
1990
|
+
!workflowIdentityMatches(job.result.identity, queuedJob.identity))
|
|
1991
|
+
) {
|
|
1992
|
+
workflowError = t(M['content.content_list.workflow_job_wrong_workflow'], {
|
|
1993
|
+
id: job.jobId,
|
|
1994
|
+
});
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
workflowQueuedJobs = workflowQueuedJobs.filter(
|
|
1998
|
+
(queued) => queued.jobId !== job.jobId,
|
|
1999
|
+
);
|
|
2000
|
+
if (!matchesCurrentIntent) {
|
|
2001
|
+
if (job.status === 'succeeded' && job.result?.ok) refreshQuery();
|
|
2002
|
+
workflowError = t(
|
|
2003
|
+
M['content.content_list.workflow_changed_checking_job'],
|
|
2004
|
+
);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
if (job.status === 'succeeded' && job.result) {
|
|
2008
|
+
if (matchesCurrentIntent) workflowResult = job.result;
|
|
2009
|
+
if (!job.result.ok) {
|
|
2010
|
+
workflowError = workflowReasonLabel(
|
|
2011
|
+
job.result.reason,
|
|
2012
|
+
M['content.content_list.workflow_job_failed'],
|
|
2013
|
+
{ id: job.jobId },
|
|
2014
|
+
);
|
|
2015
|
+
} else if (matchesCurrentIntent) {
|
|
2016
|
+
applyWorkflowSelectionOutcomes(job.result);
|
|
2017
|
+
}
|
|
2018
|
+
if (job.result.ok) refreshQuery();
|
|
2019
|
+
}
|
|
2020
|
+
if (job.status !== 'succeeded') {
|
|
2021
|
+
workflowError = workflowReasonLabel(
|
|
2022
|
+
job.reason,
|
|
2023
|
+
M['content.content_list.workflow_job_status'],
|
|
2024
|
+
{ id: job.jobId, status: workflowJobStatusLabel(job.status) },
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
workflowError = workflowExceptionMessage(
|
|
2029
|
+
error,
|
|
2030
|
+
M['content.content_list.workflow_job_failed'],
|
|
2031
|
+
);
|
|
2032
|
+
} finally {
|
|
2033
|
+
workflowPending = false;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function cancelWorkflowConfirmation() {
|
|
2038
|
+
if (workflowPending) return;
|
|
2039
|
+
workflowConfirmOpen = false;
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1211
2042
|
function handlePageChange(page: number) {
|
|
1212
2043
|
controller.dispatch({ type: 'setPage', page });
|
|
1213
2044
|
}
|
|
@@ -1303,6 +2134,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1303
2134
|
<Checkbox
|
|
1304
2135
|
checked={allPageSelected}
|
|
1305
2136
|
indeterminate={somePageSelected}
|
|
2137
|
+
disabled={workflowPending}
|
|
1306
2138
|
aria-label={t(M['content.content_list.select_all'])}
|
|
1307
2139
|
onchange={togglePageSelection}
|
|
1308
2140
|
/>
|
|
@@ -1311,7 +2143,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1311
2143
|
{#snippet selectCell({ row }: { row: ContentListRow })}
|
|
1312
2144
|
<Checkbox
|
|
1313
2145
|
checked={isSelected(row)}
|
|
1314
|
-
disabled={!row.identified || rowPending(row)}
|
|
2146
|
+
disabled={workflowPending || !row.identified || rowPending(row) || allMatchingSelected}
|
|
1315
2147
|
aria-label={selectRowLabel(row)}
|
|
1316
2148
|
title={row.identified
|
|
1317
2149
|
? rowPending(row)
|
|
@@ -1526,6 +2358,18 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1526
2358
|
<rect x="3" y="14" width="7" height="7"></rect>
|
|
1527
2359
|
</svg>
|
|
1528
2360
|
</Button>
|
|
2361
|
+
|
|
2362
|
+
{#if workflowQueuedJobs.length > 0 && workflows?.client.status}
|
|
2363
|
+
<Button
|
|
2364
|
+
variant="ghost"
|
|
2365
|
+
size="sm"
|
|
2366
|
+
type="button"
|
|
2367
|
+
disabled={workflowPending}
|
|
2368
|
+
onclick={() => void checkWorkflowJob()}
|
|
2369
|
+
>
|
|
2370
|
+
{t(M['content.content_list.check_job'])}
|
|
2371
|
+
</Button>
|
|
2372
|
+
{/if}
|
|
1529
2373
|
<Button
|
|
1530
2374
|
variant="ghost"
|
|
1531
2375
|
size="sm"
|
|
@@ -1694,6 +2538,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1694
2538
|
<Checkbox
|
|
1695
2539
|
checked={allPageSelected}
|
|
1696
2540
|
indeterminate={somePageSelected}
|
|
2541
|
+
disabled={workflowPending}
|
|
1697
2542
|
aria-label={t(M['content.content_list.select_all'])}
|
|
1698
2543
|
onchange={togglePageSelection}
|
|
1699
2544
|
/>
|
|
@@ -1702,13 +2547,136 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1702
2547
|
{t(M['content.content_list.selection_count'], { count: selectedCount })}
|
|
1703
2548
|
</span>
|
|
1704
2549
|
{#if selectedCount > 0}
|
|
1705
|
-
<Button variant="ghost" size="sm" type="button" class="clear-selection" onclick={clearSelection}>
|
|
2550
|
+
<Button variant="ghost" size="sm" type="button" class="clear-selection" disabled={workflowPending} onclick={clearSelection}>
|
|
1706
2551
|
{t(M['content.content_list.clear_selection'])}
|
|
1707
2552
|
</Button>
|
|
1708
2553
|
{/if}
|
|
2554
|
+
{#if canSelectAllMatching && !allMatchingSelected && exactMatchingCount !== undefined && exactMatchingCount > selectedCount}
|
|
2555
|
+
<Button
|
|
2556
|
+
variant="ghost"
|
|
2557
|
+
size="sm"
|
|
2558
|
+
type="button"
|
|
2559
|
+
class="select-all-matching"
|
|
2560
|
+
disabled={workflowPending}
|
|
2561
|
+
onclick={selectAllMatching}
|
|
2562
|
+
>
|
|
2563
|
+
{t(M['content.content_list.select_all_matching'], {
|
|
2564
|
+
count: exactMatchingCount,
|
|
2565
|
+
})}
|
|
2566
|
+
</Button>
|
|
2567
|
+
{/if}
|
|
1709
2568
|
</div>
|
|
1710
2569
|
{/if}
|
|
1711
2570
|
|
|
2571
|
+
{#if workflows && (selectedCount > 0 || workflowError || workflowResult)}
|
|
2572
|
+
<section class="content-workflows" aria-label={t(M['content.content_list.bulk_workflows'])}>
|
|
2573
|
+
<Select
|
|
2574
|
+
aria-label={t(M['content.content_list.bulk_workflow'])}
|
|
2575
|
+
value={selectedWorkflow}
|
|
2576
|
+
disabled={workflowPending}
|
|
2577
|
+
onchange={(event: Event) => {
|
|
2578
|
+
selectedWorkflow = (event.currentTarget as HTMLSelectElement).value as ContentListWorkflowId;
|
|
2579
|
+
workflowPreview = null;
|
|
2580
|
+
workflowIdempotencyKey = '';
|
|
2581
|
+
workflowResult = null;
|
|
2582
|
+
workflowError = null;
|
|
2583
|
+
}}
|
|
2584
|
+
>
|
|
2585
|
+
{#each CONTENT_LIST_WORKFLOW_OPTIONS as option (option.id)}
|
|
2586
|
+
<option value={option.id}>{workflowLabel(option.id)}</option>
|
|
2587
|
+
{/each}
|
|
2588
|
+
</Select>
|
|
2589
|
+
|
|
2590
|
+
{#if selectedWorkflow === 'categorize'}
|
|
2591
|
+
<Input
|
|
2592
|
+
aria-label={t(M['content.content_list.category_path'])}
|
|
2593
|
+
placeholder={t(M['content.content_list.category_path'])}
|
|
2594
|
+
value={workflowCategory}
|
|
2595
|
+
disabled={workflowPending}
|
|
2596
|
+
oninput={(event: Event) => workflowCategory = (event.currentTarget as HTMLInputElement).value}
|
|
2597
|
+
/>
|
|
2598
|
+
{:else if selectedWorkflow === 'restore'}
|
|
2599
|
+
<Select
|
|
2600
|
+
aria-label={t(M['content.content_list.restore_status'])}
|
|
2601
|
+
value={workflowRestoreStatus}
|
|
2602
|
+
disabled={workflowPending}
|
|
2603
|
+
onchange={(event: Event) => workflowRestoreStatus = (event.currentTarget as HTMLSelectElement).value as typeof workflowRestoreStatus}
|
|
2604
|
+
>
|
|
2605
|
+
<option value="draft">{t(M['content.content_list.workflow_draft'])}</option>
|
|
2606
|
+
<option value="review">{t(M['content.content_list.workflow_review'])}</option>
|
|
2607
|
+
<option value="published">{t(M['content.content_list.workflow_published'])}</option>
|
|
2608
|
+
</Select>
|
|
2609
|
+
{:else if selectedWorkflow === 'format-body'}
|
|
2610
|
+
<Select
|
|
2611
|
+
aria-label={t(M['content.content_list.body_format'])}
|
|
2612
|
+
value={workflowFormat}
|
|
2613
|
+
disabled={workflowPending}
|
|
2614
|
+
onchange={(event: Event) => workflowFormat = (event.currentTarget as HTMLSelectElement).value as typeof workflowFormat}
|
|
2615
|
+
>
|
|
2616
|
+
<option value="markdown">{t(M['content.content_list.workflow_markdown'])}</option>
|
|
2617
|
+
<option value="html">{t(M['content.content_list.workflow_html'])}</option>
|
|
2618
|
+
</Select>
|
|
2619
|
+
{:else if selectedWorkflow === 'automated-review'}
|
|
2620
|
+
<Select
|
|
2621
|
+
aria-label={t(M['content.content_list.review_kind'])}
|
|
2622
|
+
value={workflowReviewKind}
|
|
2623
|
+
disabled={workflowPending}
|
|
2624
|
+
onchange={(event: Event) => workflowReviewKind = (event.currentTarget as HTMLSelectElement).value}
|
|
2625
|
+
>
|
|
2626
|
+
<option value="">{t(M['content.content_list.default_review_kind'])}</option>
|
|
2627
|
+
<option value="facts">{t(M['content.content_list.workflow_facts'])}</option>
|
|
2628
|
+
<option value="safety">{t(M['content.content_list.workflow_safety'])}</option>
|
|
2629
|
+
<option value="custom">{t(M['content.content_list.workflow_custom'])}</option>
|
|
2630
|
+
</Select>
|
|
2631
|
+
<Input
|
|
2632
|
+
aria-label={t(M['content.content_list.review_policy_key'])}
|
|
2633
|
+
placeholder={t(M['content.content_list.policy_key_optional'])}
|
|
2634
|
+
value={workflowPolicyKey}
|
|
2635
|
+
disabled={workflowPending}
|
|
2636
|
+
oninput={(event: Event) => workflowPolicyKey = (event.currentTarget as HTMLInputElement).value}
|
|
2637
|
+
/>
|
|
2638
|
+
{:else if selectedWorkflow === 'optimize'}
|
|
2639
|
+
<Input
|
|
2640
|
+
aria-label={t(M['content.content_list.optimization_instructions'])}
|
|
2641
|
+
placeholder={t(M['content.content_list.optimization_instructions_optional'])}
|
|
2642
|
+
value={workflowInstructions}
|
|
2643
|
+
disabled={workflowPending}
|
|
2644
|
+
oninput={(event: Event) => workflowInstructions = (event.currentTarget as HTMLInputElement).value}
|
|
2645
|
+
/>
|
|
2646
|
+
{/if}
|
|
2647
|
+
|
|
2648
|
+
<Button
|
|
2649
|
+
variant="ghost"
|
|
2650
|
+
size="sm"
|
|
2651
|
+
type="button"
|
|
2652
|
+
disabled={
|
|
2653
|
+
workflowPending || !workflowPayloadValid || workflowDuplicateQueued
|
|
2654
|
+
}
|
|
2655
|
+
onclick={() => void previewWorkflow()}
|
|
2656
|
+
>
|
|
2657
|
+
{workflowPending
|
|
2658
|
+
? t(M['content.content_list.workflow_working'])
|
|
2659
|
+
: workflowDuplicateQueued
|
|
2660
|
+
? t(M['content.content_list.workflow_job_queued'])
|
|
2661
|
+
: t(M['content.content_list.workflow_preview'])}
|
|
2662
|
+
</Button>
|
|
2663
|
+
|
|
2664
|
+
{#if workflowError}
|
|
2665
|
+
<p class="content-workflows__error" role="alert">{workflowError}</p>
|
|
2666
|
+
{/if}
|
|
2667
|
+
{#if workflowResult?.ok}
|
|
2668
|
+
<p class="content-workflows__result" role="status">
|
|
2669
|
+
{workflowResultMessage(workflowResult)}
|
|
2670
|
+
{#if typeof workflowResult.details?.jobId === 'string'}
|
|
2671
|
+
{t(M['content.content_list.job_queued_progress'], {
|
|
2672
|
+
id: workflowResult.details.jobId,
|
|
2673
|
+
})}
|
|
2674
|
+
{/if}
|
|
2675
|
+
</p>
|
|
2676
|
+
{/if}
|
|
2677
|
+
</section>
|
|
2678
|
+
{/if}
|
|
2679
|
+
|
|
1712
2680
|
{#if refreshing && viewMode !== 'compact'}
|
|
1713
2681
|
<!-- DataTable announces its own refresh; the card views need their own. -->
|
|
1714
2682
|
<p class="content-refreshing" role="status" aria-live="polite">
|
|
@@ -1754,7 +2722,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1754
2722
|
<div class="content-row__select">
|
|
1755
2723
|
<Checkbox
|
|
1756
2724
|
checked={isSelected(row)}
|
|
1757
|
-
disabled={!row.identified || rowPending(row)}
|
|
2725
|
+
disabled={workflowPending || !row.identified || rowPending(row) || allMatchingSelected}
|
|
1758
2726
|
aria-label={selectRowLabel(row)}
|
|
1759
2727
|
title={row.identified
|
|
1760
2728
|
? rowPending(row)
|
|
@@ -1848,7 +2816,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1848
2816
|
<div class="content-header__eyebrow">
|
|
1849
2817
|
<Checkbox
|
|
1850
2818
|
checked={isSelected(row)}
|
|
1851
|
-
disabled={!row.identified || rowPending(row)}
|
|
2819
|
+
disabled={workflowPending || !row.identified || rowPending(row) || allMatchingSelected}
|
|
1852
2820
|
aria-label={selectRowLabel(row)}
|
|
1853
2821
|
title={row.identified
|
|
1854
2822
|
? rowPending(row)
|
|
@@ -1945,6 +2913,23 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1945
2913
|
oncancel={cancelDelete}
|
|
1946
2914
|
/>
|
|
1947
2915
|
|
|
2916
|
+
<ConfirmDialog
|
|
2917
|
+
open={workflowConfirmOpen}
|
|
2918
|
+
title={t(M['content.content_list.workflow_confirm'], {
|
|
2919
|
+
workflow: selectedWorkflow
|
|
2920
|
+
? workflowLabel(selectedWorkflow)
|
|
2921
|
+
: t(M['content.content_list.workflow_noun']),
|
|
2922
|
+
})}
|
|
2923
|
+
message={workflowPreviewMessage()}
|
|
2924
|
+
confirmLabel={workflowPending
|
|
2925
|
+
? t(M['content.content_list.workflow_applying'])
|
|
2926
|
+
: t(M['content.content_list.workflow_apply'])}
|
|
2927
|
+
cancelLabel={t(M['content.content_list.cancel'])}
|
|
2928
|
+
destructive={CONTENT_LIST_WORKFLOW_OPTIONS.find((option) => option.id === selectedWorkflow)?.sensitivity === 'sensitive'}
|
|
2929
|
+
onconfirm={() => void applyWorkflow()}
|
|
2930
|
+
oncancel={cancelWorkflowConfirmation}
|
|
2931
|
+
/>
|
|
2932
|
+
|
|
1948
2933
|
<style>
|
|
1949
2934
|
.content-list-wrapper {
|
|
1950
2935
|
display: flex;
|
|
@@ -2059,6 +3044,33 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
2059
3044
|
cursor: pointer;
|
|
2060
3045
|
}
|
|
2061
3046
|
|
|
3047
|
+
.content-workflows {
|
|
3048
|
+
display: flex;
|
|
3049
|
+
align-items: center;
|
|
3050
|
+
flex-wrap: wrap;
|
|
3051
|
+
gap: 0.75rem;
|
|
3052
|
+
margin-bottom: 1rem;
|
|
3053
|
+
padding: 0.75rem;
|
|
3054
|
+
border: 1px solid var(--smrt-color-outline-variant);
|
|
3055
|
+
border-radius: 0.65rem;
|
|
3056
|
+
background: var(--smrt-color-surface-container-low);
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
.content-workflows__error,
|
|
3060
|
+
.content-workflows__result {
|
|
3061
|
+
flex-basis: 100%;
|
|
3062
|
+
margin: 0;
|
|
3063
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
.content-workflows__error {
|
|
3067
|
+
color: var(--smrt-color-error);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
.content-workflows__result {
|
|
3071
|
+
color: var(--smrt-color-on-surface-variant);
|
|
3072
|
+
}
|
|
3073
|
+
|
|
2062
3074
|
.content-header__eyebrow,
|
|
2063
3075
|
.content-row__eyebrow {
|
|
2064
3076
|
display: flex;
|