@happyvertical/smrt-content 0.43.8 → 0.43.10
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-4w62u5_0.js +4092 -0
- package/dist/chunks/content-query-4w62u5_0.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-Di3vcTgb.js +284 -0
- package/dist/chunks/content-versions-Di3vcTgb.js.map +1 -0
- package/dist/chunks/contents-BV8RoP_R.js +612 -0
- package/dist/chunks/contents-BV8RoP_R.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/ContentList.svelte +1028 -24
- package/dist/svelte/components/ContentList.svelte.d.ts +3 -0
- package/dist/svelte/components/ContentList.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/package.json +27 -22
- 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,6 +127,165 @@ 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
|
|
|
@@ -153,6 +325,8 @@ interface Props {
|
|
|
153
325
|
savedViews?: ContentListSavedViewStore;
|
|
154
326
|
/** Shared background-workflow state. The same binding guards submissions. */
|
|
155
327
|
jobs?: ContentListJobBinding;
|
|
328
|
+
/** Opt-in, authenticated preview/apply client for bulk workflows (#2453). */
|
|
329
|
+
workflows?: ContentListWorkflowBinding;
|
|
156
330
|
}
|
|
157
331
|
|
|
158
332
|
let {
|
|
@@ -173,6 +347,7 @@ let {
|
|
|
173
347
|
urlState = undefined,
|
|
174
348
|
savedViews = undefined,
|
|
175
349
|
jobs = undefined,
|
|
350
|
+
workflows = undefined,
|
|
176
351
|
}: Props = $props();
|
|
177
352
|
|
|
178
353
|
const initialQuery = untrack(() => query);
|
|
@@ -391,6 +566,34 @@ $effect(() => {
|
|
|
391
566
|
}
|
|
392
567
|
});
|
|
393
568
|
});
|
|
569
|
+
let settledWorkflowQuery = $state<ContentListDataQueryRequest | null>(null);
|
|
570
|
+
let settledWorkflowFingerprint = $state<string | null>(null);
|
|
571
|
+
let settledWorkflowRevision = $state<string | null>(null);
|
|
572
|
+
let selectedWorkflow = $state<ContentListWorkflowId>('mark-draft');
|
|
573
|
+
let workflowCategory = $state('');
|
|
574
|
+
let workflowRestoreStatus = $state<'draft' | 'review' | 'published'>('draft');
|
|
575
|
+
let workflowFormat = $state<'markdown' | 'html'>('markdown');
|
|
576
|
+
let workflowReviewKind = $state('');
|
|
577
|
+
let workflowPolicyKey = $state('');
|
|
578
|
+
let workflowInstructions = $state('');
|
|
579
|
+
let workflowPending = $state(false);
|
|
580
|
+
let pageHeaderSelectionOnly = $state(false);
|
|
581
|
+
let workflowPreview = $state<import('@happyvertical/smrt-ui/data').DataSurfaceActionResult | null>(null);
|
|
582
|
+
let workflowResult = $state<import('@happyvertical/smrt-ui/data').DataSurfaceActionResult | null>(null);
|
|
583
|
+
let workflowError = $state<string | null>(null);
|
|
584
|
+
let workflowConfirmOpen = $state(false);
|
|
585
|
+
let workflowIntentAtPreview = '';
|
|
586
|
+
let workflowIdempotencyKey = '';
|
|
587
|
+
let workflowQueuedJobs = $state<
|
|
588
|
+
Array<{
|
|
589
|
+
intent: string;
|
|
590
|
+
jobId: string;
|
|
591
|
+
actionId: ContentListWorkflowId;
|
|
592
|
+
requestId: string;
|
|
593
|
+
identity: DataSurfaceIdentity;
|
|
594
|
+
target: ContentListJobTarget;
|
|
595
|
+
}>
|
|
596
|
+
>([]);
|
|
394
597
|
|
|
395
598
|
$effect(() =>
|
|
396
599
|
controller.subscribe((transition) => {
|
|
@@ -637,6 +840,36 @@ function refreshQuery(): void {
|
|
|
637
840
|
})
|
|
638
841
|
.catch(() => undefined);
|
|
639
842
|
}
|
|
843
|
+
|
|
844
|
+
function settleWorkflowQueryResult(
|
|
845
|
+
result: unknown,
|
|
846
|
+
request: ContentListDataQueryRequest,
|
|
847
|
+
signature: string,
|
|
848
|
+
): void {
|
|
849
|
+
settledSignature = signature;
|
|
850
|
+
resultNotices = readContentListQueryNotices(result);
|
|
851
|
+
if (result !== null && typeof result === 'object' && !Array.isArray(result)) {
|
|
852
|
+
const envelope = result as Record<string, unknown>;
|
|
853
|
+
settledWorkflowQuery = request;
|
|
854
|
+
settledWorkflowFingerprint =
|
|
855
|
+
typeof envelope.queryFingerprint === 'string'
|
|
856
|
+
? envelope.queryFingerprint
|
|
857
|
+
: null;
|
|
858
|
+
const freshness = envelope.freshness;
|
|
859
|
+
settledWorkflowRevision =
|
|
860
|
+
freshness !== null &&
|
|
861
|
+
typeof freshness === 'object' &&
|
|
862
|
+
!Array.isArray(freshness) &&
|
|
863
|
+
typeof (freshness as Record<string, unknown>).asOf === 'string'
|
|
864
|
+
? String((freshness as Record<string, unknown>).asOf)
|
|
865
|
+
: null;
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
settledWorkflowQuery = null;
|
|
869
|
+
settledWorkflowFingerprint = null;
|
|
870
|
+
settledWorkflowRevision = null;
|
|
871
|
+
}
|
|
872
|
+
|
|
640
873
|
/**
|
|
641
874
|
* A retry re-reads the same query, so its answer replaces the rendered rows —
|
|
642
875
|
* and therefore has to replace the completeness flags that describe them too.
|
|
@@ -648,13 +881,20 @@ function refreshQuery(): void {
|
|
|
648
881
|
function retryQuery(): void {
|
|
649
882
|
if (!queryBinding) return;
|
|
650
883
|
const signature = executedSignature;
|
|
884
|
+
const request = executedWorkflowQuery;
|
|
651
885
|
void queryBinding
|
|
652
886
|
.retry()
|
|
653
887
|
.then((result) => {
|
|
654
888
|
// `retry()` resolves undefined when there is no request to repeat, and a
|
|
655
889
|
// newer query may have superseded this one while it was in flight.
|
|
656
|
-
if (
|
|
657
|
-
|
|
890
|
+
if (
|
|
891
|
+
result === undefined ||
|
|
892
|
+
request === null ||
|
|
893
|
+
executedSignature !== signature ||
|
|
894
|
+
signature === undefined
|
|
895
|
+
)
|
|
896
|
+
return;
|
|
897
|
+
settleWorkflowQueryResult(result, request, signature);
|
|
658
898
|
})
|
|
659
899
|
.catch(() => undefined);
|
|
660
900
|
}
|
|
@@ -680,9 +920,9 @@ const querySignature = $derived(
|
|
|
680
920
|
);
|
|
681
921
|
|
|
682
922
|
let executedSignature: string | undefined;
|
|
923
|
+
let executedWorkflowQuery: ContentListDataQueryRequest | null = null;
|
|
683
924
|
let publishedUrlSignature: string | undefined;
|
|
684
925
|
|
|
685
|
-
|
|
686
926
|
$effect(() => {
|
|
687
927
|
const signature = querySignature;
|
|
688
928
|
untrack(() => {
|
|
@@ -727,15 +967,16 @@ $effect(() => {
|
|
|
727
967
|
// unhandled one. The resolved envelope carries the server's completeness
|
|
728
968
|
// flags, which the binding itself does not expose.
|
|
729
969
|
activeQueryKey = contentListQueryRequestKey(translated.request);
|
|
970
|
+
executedWorkflowQuery = translated.request;
|
|
730
971
|
void queryBinding
|
|
731
972
|
.execute(translated.request)
|
|
732
973
|
.then((result) => {
|
|
733
974
|
// A newer query may have superseded this one while it was in flight.
|
|
734
975
|
if (executedSignature !== signature) return;
|
|
735
976
|
// The binding's rows and total now describe THIS query, so the page
|
|
736
|
-
// may be clamped against them
|
|
737
|
-
|
|
738
|
-
|
|
977
|
+
// may be clamped against them and workflows may bind to the exact
|
|
978
|
+
// settled request.
|
|
979
|
+
settleWorkflowQueryResult(result, translated.request, signature);
|
|
739
980
|
})
|
|
740
981
|
.catch(() => undefined);
|
|
741
982
|
}
|
|
@@ -833,15 +1074,48 @@ const identifiedRowKeys = $derived(
|
|
|
833
1074
|
const selectablePageRowIds = $derived(
|
|
834
1075
|
selectableContentListRowIds(pageRows.filter((row) => !rowPending(row))),
|
|
835
1076
|
);
|
|
1077
|
+
const allMatchingSelected = $derived(tableState.selection.scope === 'allMatching');
|
|
836
1078
|
const allPageSelected = $derived(
|
|
837
|
-
|
|
838
|
-
selectablePageRowIds.
|
|
1079
|
+
allMatchingSelected ||
|
|
1080
|
+
(selectablePageRowIds.length > 0 &&
|
|
1081
|
+
selectablePageRowIds.every((rowId) => selectedRowKeys.has(String(rowId)))),
|
|
839
1082
|
);
|
|
840
1083
|
const somePageSelected = $derived(
|
|
841
1084
|
!allPageSelected &&
|
|
842
1085
|
selectablePageRowIds.some((rowId) => selectedRowKeys.has(String(rowId))),
|
|
843
1086
|
);
|
|
844
|
-
const selectedCount = $derived(
|
|
1087
|
+
const selectedCount = $derived(
|
|
1088
|
+
tableState.selection.scope === 'allMatching'
|
|
1089
|
+
? tableState.selection.expectedCount
|
|
1090
|
+
: tableState.selectedRowIds.length,
|
|
1091
|
+
);
|
|
1092
|
+
const exactMatchingCount = $derived(
|
|
1093
|
+
settledSignature === querySignature
|
|
1094
|
+
? contentListQueryExactTotal(queryBinding?.total)
|
|
1095
|
+
: undefined,
|
|
1096
|
+
);
|
|
1097
|
+
const canSelectAllMatching = $derived(
|
|
1098
|
+
Boolean(
|
|
1099
|
+
workflows &&
|
|
1100
|
+
serverBacked &&
|
|
1101
|
+
settledWorkflowQuery &&
|
|
1102
|
+
settledWorkflowFingerprint &&
|
|
1103
|
+
settledWorkflowRevision &&
|
|
1104
|
+
exactMatchingCount !== undefined &&
|
|
1105
|
+
exactMatchingCount > 0 &&
|
|
1106
|
+
exactMatchingCount <= (workflows?.maxSelectionSize ?? 200),
|
|
1107
|
+
),
|
|
1108
|
+
);
|
|
1109
|
+
const workflowPayloadValid = $derived(
|
|
1110
|
+
selectedWorkflow !== 'categorize' || workflowCategory.trim().length > 0,
|
|
1111
|
+
);
|
|
1112
|
+
const workflowDuplicateQueued = $derived(
|
|
1113
|
+
workflowQueuedJobs.some((job) => job.intent === workflowIntentSignature()),
|
|
1114
|
+
);
|
|
1115
|
+
const workflowQueuedJob = $derived(
|
|
1116
|
+
workflowQueuedJobs.find((job) => job.intent === workflowIntentSignature()) ??
|
|
1117
|
+
workflowQueuedJobs[0],
|
|
1118
|
+
);
|
|
845
1119
|
|
|
846
1120
|
/** Synthetic ids the adapter minted for rows that carry no durable identity. */
|
|
847
1121
|
const unidentifiedRowKeys = $derived(
|
|
@@ -976,7 +1250,22 @@ const surfaceOptions = $derived(
|
|
|
976
1250
|
registry: dataSurface.registry,
|
|
977
1251
|
descriptor:
|
|
978
1252
|
dataSurface.descriptor ??
|
|
979
|
-
|
|
1253
|
+
(() => {
|
|
1254
|
+
const descriptor = buildContentListSurfaceDescriptor({
|
|
1255
|
+
columnLabels,
|
|
1256
|
+
includeWorkflows: workflows !== undefined,
|
|
1257
|
+
});
|
|
1258
|
+
return {
|
|
1259
|
+
...descriptor,
|
|
1260
|
+
identity: workflows?.identity ?? descriptor.identity,
|
|
1261
|
+
limits: {
|
|
1262
|
+
...descriptor.limits,
|
|
1263
|
+
...(workflows?.maxSelectionSize === undefined
|
|
1264
|
+
? {}
|
|
1265
|
+
: { maxSelectionSize: workflows.maxSelectionSize }),
|
|
1266
|
+
},
|
|
1267
|
+
};
|
|
1268
|
+
})(),
|
|
980
1269
|
}
|
|
981
1270
|
: undefined,
|
|
982
1271
|
);
|
|
@@ -1154,14 +1443,19 @@ async function deleteSelectedView() {
|
|
|
1154
1443
|
}
|
|
1155
1444
|
|
|
1156
1445
|
function isSelected(row: ContentListRow): boolean {
|
|
1157
|
-
return selectedRowKeys.has(String(row.id));
|
|
1446
|
+
return allMatchingSelected || selectedRowKeys.has(String(row.id));
|
|
1158
1447
|
}
|
|
1159
1448
|
|
|
1160
1449
|
function rowPending(row: ContentListRow): boolean {
|
|
1161
1450
|
return (
|
|
1162
1451
|
jobSnapshot.pendingRowIds.has(String(row.id)) ||
|
|
1163
1452
|
(activeQueryKey !== undefined &&
|
|
1164
|
-
jobSnapshot.pendingQueryKeys.has(activeQueryKey))
|
|
1453
|
+
jobSnapshot.pendingQueryKeys.has(activeQueryKey)) ||
|
|
1454
|
+
workflowQueuedJobs.some((job) =>
|
|
1455
|
+
job.target.kind === 'query'
|
|
1456
|
+
? activeQueryKey !== undefined && job.target.queryKey === activeQueryKey
|
|
1457
|
+
: job.target.rowIds.includes(String(row.id)),
|
|
1458
|
+
)
|
|
1165
1459
|
);
|
|
1166
1460
|
}
|
|
1167
1461
|
|
|
@@ -1185,29 +1479,558 @@ function formattedLastUpdated(value: number): string {
|
|
|
1185
1479
|
}
|
|
1186
1480
|
|
|
1187
1481
|
function toggleRow(row: ContentListRow) {
|
|
1188
|
-
if (
|
|
1482
|
+
if (
|
|
1483
|
+
workflowPending ||
|
|
1484
|
+
!row.identified ||
|
|
1485
|
+
rowPending(row) ||
|
|
1486
|
+
allMatchingSelected
|
|
1487
|
+
)
|
|
1488
|
+
return;
|
|
1489
|
+
pageHeaderSelectionOnly = false;
|
|
1189
1490
|
controller.dispatch({ type: 'toggleRowSelection', rowId: row.id });
|
|
1190
1491
|
}
|
|
1191
1492
|
|
|
1192
1493
|
function togglePageSelection() {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1494
|
+
if (workflowPending) return;
|
|
1495
|
+
if (allMatchingSelected) {
|
|
1496
|
+
clearSelection();
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const rowKey = (rowId: string | number) =>
|
|
1500
|
+
`${typeof rowId}:${String(rowId)}`;
|
|
1501
|
+
const pageKeys = new Set(selectablePageRowIds.map(rowKey));
|
|
1502
|
+
if (allPageSelected) {
|
|
1503
|
+
pageHeaderSelectionOnly = false;
|
|
1504
|
+
controller.dispatch({
|
|
1505
|
+
type: 'setSelectedRows',
|
|
1506
|
+
rowIds: tableState.selectedRowIds.filter(
|
|
1507
|
+
(rowId) => !pageKeys.has(rowKey(rowId)),
|
|
1197
1508
|
),
|
|
1509
|
+
});
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const selected = new Map(
|
|
1513
|
+
tableState.selectedRowIds.map((rowId) => [rowKey(rowId), rowId]),
|
|
1198
1514
|
);
|
|
1515
|
+
pageHeaderSelectionOnly =
|
|
1516
|
+
serverBacked && tableState.selectedRowIds.length === 0;
|
|
1517
|
+
for (const rowId of selectablePageRowIds) selected.set(rowKey(rowId), rowId);
|
|
1199
1518
|
controller.dispatch({
|
|
1200
1519
|
type: 'setSelectedRows',
|
|
1201
|
-
rowIds:
|
|
1202
|
-
|
|
1203
|
-
|
|
1520
|
+
rowIds: [...selected.values()],
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function selectAllMatching() {
|
|
1525
|
+
if (
|
|
1526
|
+
workflowPending ||
|
|
1527
|
+
!canSelectAllMatching ||
|
|
1528
|
+
!settledWorkflowFingerprint ||
|
|
1529
|
+
!settledWorkflowRevision ||
|
|
1530
|
+
exactMatchingCount === undefined
|
|
1531
|
+
) return;
|
|
1532
|
+
pageHeaderSelectionOnly = false;
|
|
1533
|
+
controller.dispatch({
|
|
1534
|
+
type: 'selectAllMatching',
|
|
1535
|
+
queryFingerprint: settledWorkflowFingerprint,
|
|
1536
|
+
queryRevision: settledWorkflowRevision,
|
|
1537
|
+
expectedCount: exactMatchingCount,
|
|
1204
1538
|
});
|
|
1205
1539
|
}
|
|
1206
1540
|
|
|
1207
1541
|
function clearSelection() {
|
|
1542
|
+
if (workflowPending) return;
|
|
1543
|
+
pageHeaderSelectionOnly = false;
|
|
1208
1544
|
controller.dispatch({ type: 'setSelectedRows', rowIds: [] });
|
|
1209
1545
|
}
|
|
1210
1546
|
|
|
1547
|
+
function workflowPayload(): Record<string, string> | undefined {
|
|
1548
|
+
switch (selectedWorkflow) {
|
|
1549
|
+
case 'categorize':
|
|
1550
|
+
return { category: workflowCategory.trim() };
|
|
1551
|
+
case 'restore':
|
|
1552
|
+
return { status: workflowRestoreStatus };
|
|
1553
|
+
case 'format-body':
|
|
1554
|
+
return { format: workflowFormat };
|
|
1555
|
+
case 'automated-review': {
|
|
1556
|
+
const payload: Record<string, string> = {};
|
|
1557
|
+
if (workflowReviewKind.trim()) payload.kind = workflowReviewKind.trim();
|
|
1558
|
+
if (workflowPolicyKey.trim()) payload.policyKey = workflowPolicyKey.trim();
|
|
1559
|
+
return payload;
|
|
1560
|
+
}
|
|
1561
|
+
case 'optimize':
|
|
1562
|
+
return workflowInstructions.trim()
|
|
1563
|
+
? { instructions: workflowInstructions.trim() }
|
|
1564
|
+
: {};
|
|
1565
|
+
default:
|
|
1566
|
+
return {};
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function workflowSelection(): ContentListWorkflowRequest['selection'] | null {
|
|
1571
|
+
const selection = tableState.selection;
|
|
1572
|
+
if (selection.scope === 'allMatching') {
|
|
1573
|
+
return { scope: 'all-matching', queryFingerprint: selection.queryFingerprint };
|
|
1574
|
+
}
|
|
1575
|
+
const currentPageKeys = new Set(pageRows.map((row) => String(row.id)));
|
|
1576
|
+
const selectablePageKeys = new Set(selectablePageRowIds.map(String));
|
|
1577
|
+
const effectiveRowIds =
|
|
1578
|
+
selection.scope === 'page' || pageHeaderSelectionOnly
|
|
1579
|
+
? selection.rowIds.filter((rowId) => {
|
|
1580
|
+
const key = String(rowId);
|
|
1581
|
+
return !currentPageKeys.has(key) || selectablePageKeys.has(key);
|
|
1582
|
+
})
|
|
1583
|
+
: selection.rowIds;
|
|
1584
|
+
if (
|
|
1585
|
+
serverBacked &&
|
|
1586
|
+
pageHeaderSelectionOnly &&
|
|
1587
|
+
selectablePageRowIds.length === pageRows.length &&
|
|
1588
|
+
effectiveRowIds.length === selectablePageRowIds.length &&
|
|
1589
|
+
selectablePageRowIds.every((rowId) =>
|
|
1590
|
+
effectiveRowIds.some(
|
|
1591
|
+
(selectedId) => String(selectedId) === String(rowId),
|
|
1592
|
+
),
|
|
1593
|
+
)
|
|
1594
|
+
) {
|
|
1595
|
+
return { scope: 'current-page' };
|
|
1596
|
+
}
|
|
1597
|
+
if (effectiveRowIds.length === 0) return null;
|
|
1598
|
+
return { scope: 'explicit-ids', rowIds: effectiveRowIds };
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function workflowJobTarget(
|
|
1602
|
+
request: ContentListWorkflowRequest,
|
|
1603
|
+
): ContentListJobTarget {
|
|
1604
|
+
if (request.selection.scope === 'all-matching' && activeQueryKey !== undefined)
|
|
1605
|
+
return { kind: 'query', queryKey: activeQueryKey };
|
|
1606
|
+
if (request.selection.scope === 'explicit-ids')
|
|
1607
|
+
return { kind: 'rows', rowIds: request.selection.rowIds.map(String) };
|
|
1608
|
+
return { kind: 'rows', rowIds: selectablePageRowIds.map(String) };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function workflowIntentSignature(): string {
|
|
1612
|
+
const selection = workflowSelection();
|
|
1613
|
+
if (selection?.scope === 'explicit-ids') {
|
|
1614
|
+
return JSON.stringify([
|
|
1615
|
+
selectedWorkflow,
|
|
1616
|
+
workflowPayload(),
|
|
1617
|
+
{
|
|
1618
|
+
scope: selection.scope,
|
|
1619
|
+
rowIds: [...selection.rowIds].sort((left, right) => {
|
|
1620
|
+
const leftKey = `${typeof left}:${String(left)}`;
|
|
1621
|
+
const rightKey = `${typeof right}:${String(right)}`;
|
|
1622
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
1623
|
+
}),
|
|
1624
|
+
},
|
|
1625
|
+
]);
|
|
1626
|
+
}
|
|
1627
|
+
return JSON.stringify([
|
|
1628
|
+
selectedWorkflow,
|
|
1629
|
+
workflowPayload(),
|
|
1630
|
+
tableState.selection,
|
|
1631
|
+
settledWorkflowFingerprint,
|
|
1632
|
+
querySignature,
|
|
1633
|
+
]);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function createWorkflowRequest(
|
|
1637
|
+
phase: 'preview' | 'apply',
|
|
1638
|
+
): ContentListWorkflowRequest | null {
|
|
1639
|
+
const selection = workflowSelection();
|
|
1640
|
+
if (!selection) return null;
|
|
1641
|
+
const queryRequired = selection.scope !== 'explicit-ids';
|
|
1642
|
+
if (
|
|
1643
|
+
queryRequired &&
|
|
1644
|
+
(!settledWorkflowQuery || settledSignature !== querySignature)
|
|
1645
|
+
) return null;
|
|
1646
|
+
if (
|
|
1647
|
+
selection.scope === 'all-matching' &&
|
|
1648
|
+
selection.queryFingerprint !== settledWorkflowFingerprint
|
|
1649
|
+
) return null;
|
|
1650
|
+
return {
|
|
1651
|
+
version: 1,
|
|
1652
|
+
requestId: globalThis.crypto?.randomUUID?.() ?? `content-workflow-${Date.now()}`,
|
|
1653
|
+
identity: workflows?.identity ?? { surfaceId: 'content-list', kind: 'table' },
|
|
1654
|
+
actionId: selectedWorkflow,
|
|
1655
|
+
phase,
|
|
1656
|
+
selection,
|
|
1657
|
+
payload: workflowPayload(),
|
|
1658
|
+
expectedRevision: workflows?.revision ?? 0,
|
|
1659
|
+
target: {
|
|
1660
|
+
...(queryRequired && settledWorkflowQuery
|
|
1661
|
+
? { query: settledWorkflowQuery }
|
|
1662
|
+
: {}),
|
|
1663
|
+
expectedCount:
|
|
1664
|
+
selection.scope === 'explicit-ids'
|
|
1665
|
+
? selection.rowIds.length
|
|
1666
|
+
: selectedCount,
|
|
1667
|
+
},
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function workflowIdentityMatches(
|
|
1672
|
+
actual: DataSurfaceIdentity | null | undefined,
|
|
1673
|
+
expected: DataSurfaceIdentity,
|
|
1674
|
+
): boolean {
|
|
1675
|
+
return (
|
|
1676
|
+
actual !== null &&
|
|
1677
|
+
actual !== undefined &&
|
|
1678
|
+
actual.surfaceId === expected.surfaceId &&
|
|
1679
|
+
actual.kind === expected.kind &&
|
|
1680
|
+
(expected.subject
|
|
1681
|
+
? actual.subject?.type === expected.subject.type &&
|
|
1682
|
+
actual.subject.id === expected.subject.id
|
|
1683
|
+
: actual.subject === undefined)
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function workflowResultMatchesRequest(
|
|
1688
|
+
result: DataSurfaceActionResult,
|
|
1689
|
+
request: ContentListWorkflowRequest,
|
|
1690
|
+
): boolean {
|
|
1691
|
+
return (
|
|
1692
|
+
result.version === 1 &&
|
|
1693
|
+
result.requestId === request.requestId &&
|
|
1694
|
+
result.phase === request.phase &&
|
|
1695
|
+
result.actionId === request.actionId &&
|
|
1696
|
+
workflowIdentityMatches(result.identity, request.identity)
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function workflowResultMessage(result: import('@happyvertical/smrt-ui/data').DataSurfaceActionResult): string {
|
|
1701
|
+
const details = result.details ?? {};
|
|
1702
|
+
const accepted = typeof details.accepted === 'number' ? details.accepted : 0;
|
|
1703
|
+
if (details.background === true) {
|
|
1704
|
+
return t(M['content.content_list.workflow_result_background'], {
|
|
1705
|
+
accepted,
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
const skipped = typeof details.skipped === 'number' ? details.skipped : 0;
|
|
1709
|
+
const failed = typeof details.failed === 'number' ? details.failed : 0;
|
|
1710
|
+
return t(M['content.content_list.workflow_result_summary'], {
|
|
1711
|
+
accepted,
|
|
1712
|
+
skipped,
|
|
1713
|
+
failed,
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function applyWorkflowSelectionOutcomes(
|
|
1718
|
+
result: import('@happyvertical/smrt-ui/data').DataSurfaceActionResult,
|
|
1719
|
+
): boolean {
|
|
1720
|
+
const outcomes = contentListWorkflowOutcomes(result);
|
|
1721
|
+
const rowKey = (rowId: string | number) =>
|
|
1722
|
+
`${typeof rowId}:${String(rowId)}`;
|
|
1723
|
+
if (tableState.selection.scope === 'allMatching') {
|
|
1724
|
+
const uniqueOutcomes = new Set(outcomes.map((outcome) => rowKey(outcome.rowId)));
|
|
1725
|
+
if (
|
|
1726
|
+
outcomes.length !== tableState.selection.expectedCount ||
|
|
1727
|
+
uniqueOutcomes.size !== outcomes.length
|
|
1728
|
+
) {
|
|
1729
|
+
workflowError = t(
|
|
1730
|
+
M['content.content_list.workflow_incomplete_outcomes'],
|
|
1731
|
+
);
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
controller.dispatch({
|
|
1735
|
+
type: 'setSelectedRows',
|
|
1736
|
+
rowIds: outcomes
|
|
1737
|
+
.filter((outcome) => outcome.status !== 'accepted')
|
|
1738
|
+
.map((outcome) => outcome.rowId),
|
|
1739
|
+
});
|
|
1740
|
+
return true;
|
|
1741
|
+
}
|
|
1742
|
+
const accepted = new Set(
|
|
1743
|
+
outcomes
|
|
1744
|
+
.filter((outcome) => outcome.status === 'accepted')
|
|
1745
|
+
.map((outcome) => rowKey(outcome.rowId)),
|
|
1746
|
+
);
|
|
1747
|
+
controller.dispatch({
|
|
1748
|
+
type: 'setSelectedRows',
|
|
1749
|
+
rowIds: tableState.selectedRowIds.filter(
|
|
1750
|
+
(rowId) => !accepted.has(rowKey(rowId)),
|
|
1751
|
+
),
|
|
1752
|
+
});
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function workflowPreviewMessage(): string {
|
|
1757
|
+
const details = workflowPreview?.details ?? {};
|
|
1758
|
+
const count = typeof details.count === 'number' ? details.count : selectedCount;
|
|
1759
|
+
const labels = Array.isArray(details.representativeLabels)
|
|
1760
|
+
? details.representativeLabels.filter((entry): entry is string => typeof entry === 'string')
|
|
1761
|
+
: [];
|
|
1762
|
+
const consequences = Array.isArray(details.consequences)
|
|
1763
|
+
? details.consequences.filter((entry): entry is string => typeof entry === 'string')
|
|
1764
|
+
: [];
|
|
1765
|
+
const skipped = typeof details.skipped === 'number' ? details.skipped : 0;
|
|
1766
|
+
const resolvedScope =
|
|
1767
|
+
typeof details.resolvedScope === 'string'
|
|
1768
|
+
? details.resolvedScope
|
|
1769
|
+
: workflowSelection()?.scope;
|
|
1770
|
+
const ineligible = Array.isArray(details.ineligible)
|
|
1771
|
+
? details.ineligible.flatMap((entry) => {
|
|
1772
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
1773
|
+
return [];
|
|
1774
|
+
const outcome = entry as Record<string, unknown>;
|
|
1775
|
+
if (
|
|
1776
|
+
typeof outcome.rowId !== 'string' &&
|
|
1777
|
+
typeof outcome.rowId !== 'number'
|
|
1778
|
+
)
|
|
1779
|
+
return [];
|
|
1780
|
+
const reason =
|
|
1781
|
+
typeof outcome.reason === 'string'
|
|
1782
|
+
? ` (${workflowReasonLabel(
|
|
1783
|
+
outcome.reason,
|
|
1784
|
+
M['content.content_list.workflow_reason_unknown'],
|
|
1785
|
+
)})`
|
|
1786
|
+
: '';
|
|
1787
|
+
return [`${String(outcome.rowId)}${reason}`];
|
|
1788
|
+
})
|
|
1789
|
+
: [];
|
|
1790
|
+
return [
|
|
1791
|
+
resolvedScope
|
|
1792
|
+
? t(M['content.content_list.workflow_preview_scope'], {
|
|
1793
|
+
scope: workflowScopeLabel(resolvedScope),
|
|
1794
|
+
})
|
|
1795
|
+
: '',
|
|
1796
|
+
t(
|
|
1797
|
+
count === 1
|
|
1798
|
+
? M['content.content_list.workflow_preview_count_one']
|
|
1799
|
+
: M['content.content_list.workflow_preview_count_many'],
|
|
1800
|
+
{ count },
|
|
1801
|
+
),
|
|
1802
|
+
labels.length
|
|
1803
|
+
? t(M['content.content_list.workflow_preview_examples'], {
|
|
1804
|
+
labels: labels.join(', '),
|
|
1805
|
+
})
|
|
1806
|
+
: '',
|
|
1807
|
+
skipped
|
|
1808
|
+
? t(M['content.content_list.workflow_preview_ineligible_count'], {
|
|
1809
|
+
count: skipped,
|
|
1810
|
+
})
|
|
1811
|
+
: '',
|
|
1812
|
+
ineligible.length
|
|
1813
|
+
? t(M['content.content_list.workflow_preview_ineligible'], {
|
|
1814
|
+
items: ineligible.join(', '),
|
|
1815
|
+
})
|
|
1816
|
+
: '',
|
|
1817
|
+
...consequences.map(workflowConsequenceLabel),
|
|
1818
|
+
].filter(Boolean).join(' ');
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
async function previewWorkflow() {
|
|
1822
|
+
if (!workflows || workflowPending || workflowDuplicateQueued) return;
|
|
1823
|
+
const request = createWorkflowRequest('preview');
|
|
1824
|
+
if (!request) return;
|
|
1825
|
+
workflowPending = true;
|
|
1826
|
+
workflowError = null;
|
|
1827
|
+
workflowResult = null;
|
|
1828
|
+
workflowIdempotencyKey = '';
|
|
1829
|
+
try {
|
|
1830
|
+
const result = await workflows.client.preview(request);
|
|
1831
|
+
if (!workflowResultMatchesRequest(result, request)) {
|
|
1832
|
+
workflowError = t(M['content.content_list.workflow_preview_mismatch']);
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
if (!result.ok) {
|
|
1836
|
+
workflowError = workflowReasonLabel(
|
|
1837
|
+
result.reason,
|
|
1838
|
+
M['content.content_list.workflow_preview_failed'],
|
|
1839
|
+
);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
workflowPreview = result;
|
|
1843
|
+
workflowIntentAtPreview = workflowIntentSignature();
|
|
1844
|
+
workflowIdempotencyKey =
|
|
1845
|
+
globalThis.crypto?.randomUUID?.() ?? `content-workflow-apply-${Date.now()}`;
|
|
1846
|
+
workflowConfirmOpen = true;
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
workflowError = workflowExceptionMessage(
|
|
1849
|
+
error,
|
|
1850
|
+
M['content.content_list.workflow_preview_failed'],
|
|
1851
|
+
);
|
|
1852
|
+
} finally {
|
|
1853
|
+
workflowPending = false;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
async function applyWorkflow() {
|
|
1858
|
+
if (!workflows || workflowPending || !workflowPreview?.confirmationToken) return;
|
|
1859
|
+
if (workflowIntentAtPreview !== workflowIntentSignature()) {
|
|
1860
|
+
workflowConfirmOpen = false;
|
|
1861
|
+
workflowPreview = null;
|
|
1862
|
+
workflowIdempotencyKey = '';
|
|
1863
|
+
workflowError = t(M['content.content_list.workflow_selection_changed']);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const request = createWorkflowRequest('apply');
|
|
1867
|
+
if (!request) return;
|
|
1868
|
+
request.confirmationToken = workflowPreview.confirmationToken;
|
|
1869
|
+
request.idempotencyKey = workflowIdempotencyKey;
|
|
1870
|
+
const appliedIntent = workflowIntentAtPreview;
|
|
1871
|
+
const appliedTarget = workflowJobTarget(request);
|
|
1872
|
+
workflowPending = true;
|
|
1873
|
+
workflowError = null;
|
|
1874
|
+
try {
|
|
1875
|
+
const result = await workflows.client.apply(request);
|
|
1876
|
+
if (!workflowResultMatchesRequest(result, request)) {
|
|
1877
|
+
workflowError = t(M['content.content_list.workflow_apply_mismatch']);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (appliedIntent !== workflowIntentSignature()) {
|
|
1881
|
+
if (
|
|
1882
|
+
result.ok &&
|
|
1883
|
+
result.details?.background === true &&
|
|
1884
|
+
typeof result.details.jobId === 'string' &&
|
|
1885
|
+
!workflowQueuedJobs.some((job) => job.intent === appliedIntent)
|
|
1886
|
+
) {
|
|
1887
|
+
workflowQueuedJobs = [
|
|
1888
|
+
...workflowQueuedJobs,
|
|
1889
|
+
{
|
|
1890
|
+
intent: appliedIntent,
|
|
1891
|
+
jobId: result.details.jobId,
|
|
1892
|
+
actionId: request.actionId,
|
|
1893
|
+
requestId:
|
|
1894
|
+
typeof result.details.jobRequestId === 'string'
|
|
1895
|
+
? result.details.jobRequestId
|
|
1896
|
+
: request.requestId,
|
|
1897
|
+
identity: request.identity,
|
|
1898
|
+
target: appliedTarget,
|
|
1899
|
+
},
|
|
1900
|
+
];
|
|
1901
|
+
}
|
|
1902
|
+
workflowError = t(M['content.content_list.workflow_changed_applying']);
|
|
1903
|
+
workflowPreview = null;
|
|
1904
|
+
workflowIdempotencyKey = '';
|
|
1905
|
+
workflowConfirmOpen = false;
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
workflowResult = result;
|
|
1909
|
+
if (!result.ok) {
|
|
1910
|
+
workflowError = workflowReasonLabel(
|
|
1911
|
+
result.reason,
|
|
1912
|
+
M['content.content_list.workflow_failed'],
|
|
1913
|
+
);
|
|
1914
|
+
workflowPreview = null;
|
|
1915
|
+
workflowIdempotencyKey = '';
|
|
1916
|
+
workflowConfirmOpen = false;
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (result.details?.background !== true) {
|
|
1920
|
+
applyWorkflowSelectionOutcomes(result);
|
|
1921
|
+
refreshQuery();
|
|
1922
|
+
} else if (
|
|
1923
|
+
typeof result.details.jobId === 'string' &&
|
|
1924
|
+
!workflowQueuedJobs.some((job) => job.intent === workflowIntentAtPreview)
|
|
1925
|
+
) {
|
|
1926
|
+
workflowQueuedJobs = [
|
|
1927
|
+
...workflowQueuedJobs,
|
|
1928
|
+
{
|
|
1929
|
+
intent: workflowIntentAtPreview,
|
|
1930
|
+
jobId: result.details.jobId,
|
|
1931
|
+
actionId: request.actionId,
|
|
1932
|
+
requestId:
|
|
1933
|
+
typeof result.details.jobRequestId === 'string'
|
|
1934
|
+
? result.details.jobRequestId
|
|
1935
|
+
: request.requestId,
|
|
1936
|
+
identity: request.identity,
|
|
1937
|
+
target: appliedTarget,
|
|
1938
|
+
},
|
|
1939
|
+
];
|
|
1940
|
+
}
|
|
1941
|
+
workflowPreview = null;
|
|
1942
|
+
workflowIdempotencyKey = '';
|
|
1943
|
+
workflowConfirmOpen = false;
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
workflowError = workflowExceptionMessage(
|
|
1946
|
+
error,
|
|
1947
|
+
M['content.content_list.workflow_failed'],
|
|
1948
|
+
);
|
|
1949
|
+
} finally {
|
|
1950
|
+
workflowPending = false;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
async function checkWorkflowJob() {
|
|
1955
|
+
if (!workflows?.client.status || !workflowQueuedJob || workflowPending) return;
|
|
1956
|
+
const queuedJob = workflowQueuedJob;
|
|
1957
|
+
workflowPending = true;
|
|
1958
|
+
workflowError = null;
|
|
1959
|
+
try {
|
|
1960
|
+
const job = await workflows.client.status(queuedJob.jobId);
|
|
1961
|
+
const matchesCurrentIntent =
|
|
1962
|
+
queuedJob.intent === workflowIntentSignature();
|
|
1963
|
+
if (job.status === 'queued' || job.status === 'running') {
|
|
1964
|
+
workflowError = t(M['content.content_list.workflow_job_still'], {
|
|
1965
|
+
id: job.jobId,
|
|
1966
|
+
status: workflowJobStatusLabel(job.status),
|
|
1967
|
+
});
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (job.status === 'succeeded' && !job.result) {
|
|
1971
|
+
workflowError = t(M['content.content_list.workflow_job_missing_result'], {
|
|
1972
|
+
id: job.jobId,
|
|
1973
|
+
});
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (
|
|
1977
|
+
job.result &&
|
|
1978
|
+
(job.result.version !== 1 ||
|
|
1979
|
+
job.result.phase !== 'apply' ||
|
|
1980
|
+
job.result.actionId !== queuedJob.actionId ||
|
|
1981
|
+
job.result.requestId !== queuedJob.requestId ||
|
|
1982
|
+
!workflowIdentityMatches(job.result.identity, queuedJob.identity))
|
|
1983
|
+
) {
|
|
1984
|
+
workflowError = t(M['content.content_list.workflow_job_wrong_workflow'], {
|
|
1985
|
+
id: job.jobId,
|
|
1986
|
+
});
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
workflowQueuedJobs = workflowQueuedJobs.filter(
|
|
1990
|
+
(queued) => queued.jobId !== job.jobId,
|
|
1991
|
+
);
|
|
1992
|
+
if (!matchesCurrentIntent) {
|
|
1993
|
+
if (job.status === 'succeeded' && job.result?.ok) refreshQuery();
|
|
1994
|
+
workflowError = t(
|
|
1995
|
+
M['content.content_list.workflow_changed_checking_job'],
|
|
1996
|
+
);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (job.status === 'succeeded' && job.result) {
|
|
2000
|
+
if (matchesCurrentIntent) workflowResult = job.result;
|
|
2001
|
+
if (!job.result.ok) {
|
|
2002
|
+
workflowError = workflowReasonLabel(
|
|
2003
|
+
job.result.reason,
|
|
2004
|
+
M['content.content_list.workflow_job_failed'],
|
|
2005
|
+
{ id: job.jobId },
|
|
2006
|
+
);
|
|
2007
|
+
} else if (matchesCurrentIntent) {
|
|
2008
|
+
applyWorkflowSelectionOutcomes(job.result);
|
|
2009
|
+
}
|
|
2010
|
+
if (job.result.ok) refreshQuery();
|
|
2011
|
+
}
|
|
2012
|
+
if (job.status !== 'succeeded') {
|
|
2013
|
+
workflowError = workflowReasonLabel(
|
|
2014
|
+
job.reason,
|
|
2015
|
+
M['content.content_list.workflow_job_status'],
|
|
2016
|
+
{ id: job.jobId, status: workflowJobStatusLabel(job.status) },
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
workflowError = workflowExceptionMessage(
|
|
2021
|
+
error,
|
|
2022
|
+
M['content.content_list.workflow_job_failed'],
|
|
2023
|
+
);
|
|
2024
|
+
} finally {
|
|
2025
|
+
workflowPending = false;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function cancelWorkflowConfirmation() {
|
|
2030
|
+
if (workflowPending) return;
|
|
2031
|
+
workflowConfirmOpen = false;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1211
2034
|
function handlePageChange(page: number) {
|
|
1212
2035
|
controller.dispatch({ type: 'setPage', page });
|
|
1213
2036
|
}
|
|
@@ -1303,6 +2126,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1303
2126
|
<Checkbox
|
|
1304
2127
|
checked={allPageSelected}
|
|
1305
2128
|
indeterminate={somePageSelected}
|
|
2129
|
+
disabled={workflowPending}
|
|
1306
2130
|
aria-label={t(M['content.content_list.select_all'])}
|
|
1307
2131
|
onchange={togglePageSelection}
|
|
1308
2132
|
/>
|
|
@@ -1311,7 +2135,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1311
2135
|
{#snippet selectCell({ row }: { row: ContentListRow })}
|
|
1312
2136
|
<Checkbox
|
|
1313
2137
|
checked={isSelected(row)}
|
|
1314
|
-
disabled={!row.identified || rowPending(row)}
|
|
2138
|
+
disabled={workflowPending || !row.identified || rowPending(row) || allMatchingSelected}
|
|
1315
2139
|
aria-label={selectRowLabel(row)}
|
|
1316
2140
|
title={row.identified
|
|
1317
2141
|
? rowPending(row)
|
|
@@ -1526,6 +2350,18 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1526
2350
|
<rect x="3" y="14" width="7" height="7"></rect>
|
|
1527
2351
|
</svg>
|
|
1528
2352
|
</Button>
|
|
2353
|
+
|
|
2354
|
+
{#if workflowQueuedJobs.length > 0 && workflows?.client.status}
|
|
2355
|
+
<Button
|
|
2356
|
+
variant="ghost"
|
|
2357
|
+
size="sm"
|
|
2358
|
+
type="button"
|
|
2359
|
+
disabled={workflowPending}
|
|
2360
|
+
onclick={() => void checkWorkflowJob()}
|
|
2361
|
+
>
|
|
2362
|
+
{t(M['content.content_list.check_job'])}
|
|
2363
|
+
</Button>
|
|
2364
|
+
{/if}
|
|
1529
2365
|
<Button
|
|
1530
2366
|
variant="ghost"
|
|
1531
2367
|
size="sm"
|
|
@@ -1694,6 +2530,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1694
2530
|
<Checkbox
|
|
1695
2531
|
checked={allPageSelected}
|
|
1696
2532
|
indeterminate={somePageSelected}
|
|
2533
|
+
disabled={workflowPending}
|
|
1697
2534
|
aria-label={t(M['content.content_list.select_all'])}
|
|
1698
2535
|
onchange={togglePageSelection}
|
|
1699
2536
|
/>
|
|
@@ -1702,13 +2539,136 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1702
2539
|
{t(M['content.content_list.selection_count'], { count: selectedCount })}
|
|
1703
2540
|
</span>
|
|
1704
2541
|
{#if selectedCount > 0}
|
|
1705
|
-
<Button variant="ghost" size="sm" type="button" class="clear-selection" onclick={clearSelection}>
|
|
2542
|
+
<Button variant="ghost" size="sm" type="button" class="clear-selection" disabled={workflowPending} onclick={clearSelection}>
|
|
1706
2543
|
{t(M['content.content_list.clear_selection'])}
|
|
1707
2544
|
</Button>
|
|
1708
2545
|
{/if}
|
|
2546
|
+
{#if canSelectAllMatching && !allMatchingSelected && exactMatchingCount !== undefined && exactMatchingCount > selectedCount}
|
|
2547
|
+
<Button
|
|
2548
|
+
variant="ghost"
|
|
2549
|
+
size="sm"
|
|
2550
|
+
type="button"
|
|
2551
|
+
class="select-all-matching"
|
|
2552
|
+
disabled={workflowPending}
|
|
2553
|
+
onclick={selectAllMatching}
|
|
2554
|
+
>
|
|
2555
|
+
{t(M['content.content_list.select_all_matching'], {
|
|
2556
|
+
count: exactMatchingCount,
|
|
2557
|
+
})}
|
|
2558
|
+
</Button>
|
|
2559
|
+
{/if}
|
|
1709
2560
|
</div>
|
|
1710
2561
|
{/if}
|
|
1711
2562
|
|
|
2563
|
+
{#if workflows && (selectedCount > 0 || workflowError || workflowResult)}
|
|
2564
|
+
<section class="content-workflows" aria-label={t(M['content.content_list.bulk_workflows'])}>
|
|
2565
|
+
<Select
|
|
2566
|
+
aria-label={t(M['content.content_list.bulk_workflow'])}
|
|
2567
|
+
value={selectedWorkflow}
|
|
2568
|
+
disabled={workflowPending}
|
|
2569
|
+
onchange={(event: Event) => {
|
|
2570
|
+
selectedWorkflow = (event.currentTarget as HTMLSelectElement).value as ContentListWorkflowId;
|
|
2571
|
+
workflowPreview = null;
|
|
2572
|
+
workflowIdempotencyKey = '';
|
|
2573
|
+
workflowResult = null;
|
|
2574
|
+
workflowError = null;
|
|
2575
|
+
}}
|
|
2576
|
+
>
|
|
2577
|
+
{#each CONTENT_LIST_WORKFLOW_OPTIONS as option (option.id)}
|
|
2578
|
+
<option value={option.id}>{workflowLabel(option.id)}</option>
|
|
2579
|
+
{/each}
|
|
2580
|
+
</Select>
|
|
2581
|
+
|
|
2582
|
+
{#if selectedWorkflow === 'categorize'}
|
|
2583
|
+
<Input
|
|
2584
|
+
aria-label={t(M['content.content_list.category_path'])}
|
|
2585
|
+
placeholder={t(M['content.content_list.category_path'])}
|
|
2586
|
+
value={workflowCategory}
|
|
2587
|
+
disabled={workflowPending}
|
|
2588
|
+
oninput={(event: Event) => workflowCategory = (event.currentTarget as HTMLInputElement).value}
|
|
2589
|
+
/>
|
|
2590
|
+
{:else if selectedWorkflow === 'restore'}
|
|
2591
|
+
<Select
|
|
2592
|
+
aria-label={t(M['content.content_list.restore_status'])}
|
|
2593
|
+
value={workflowRestoreStatus}
|
|
2594
|
+
disabled={workflowPending}
|
|
2595
|
+
onchange={(event: Event) => workflowRestoreStatus = (event.currentTarget as HTMLSelectElement).value as typeof workflowRestoreStatus}
|
|
2596
|
+
>
|
|
2597
|
+
<option value="draft">{t(M['content.content_list.workflow_draft'])}</option>
|
|
2598
|
+
<option value="review">{t(M['content.content_list.workflow_review'])}</option>
|
|
2599
|
+
<option value="published">{t(M['content.content_list.workflow_published'])}</option>
|
|
2600
|
+
</Select>
|
|
2601
|
+
{:else if selectedWorkflow === 'format-body'}
|
|
2602
|
+
<Select
|
|
2603
|
+
aria-label={t(M['content.content_list.body_format'])}
|
|
2604
|
+
value={workflowFormat}
|
|
2605
|
+
disabled={workflowPending}
|
|
2606
|
+
onchange={(event: Event) => workflowFormat = (event.currentTarget as HTMLSelectElement).value as typeof workflowFormat}
|
|
2607
|
+
>
|
|
2608
|
+
<option value="markdown">{t(M['content.content_list.workflow_markdown'])}</option>
|
|
2609
|
+
<option value="html">{t(M['content.content_list.workflow_html'])}</option>
|
|
2610
|
+
</Select>
|
|
2611
|
+
{:else if selectedWorkflow === 'automated-review'}
|
|
2612
|
+
<Select
|
|
2613
|
+
aria-label={t(M['content.content_list.review_kind'])}
|
|
2614
|
+
value={workflowReviewKind}
|
|
2615
|
+
disabled={workflowPending}
|
|
2616
|
+
onchange={(event: Event) => workflowReviewKind = (event.currentTarget as HTMLSelectElement).value}
|
|
2617
|
+
>
|
|
2618
|
+
<option value="">{t(M['content.content_list.default_review_kind'])}</option>
|
|
2619
|
+
<option value="facts">{t(M['content.content_list.workflow_facts'])}</option>
|
|
2620
|
+
<option value="safety">{t(M['content.content_list.workflow_safety'])}</option>
|
|
2621
|
+
<option value="custom">{t(M['content.content_list.workflow_custom'])}</option>
|
|
2622
|
+
</Select>
|
|
2623
|
+
<Input
|
|
2624
|
+
aria-label={t(M['content.content_list.review_policy_key'])}
|
|
2625
|
+
placeholder={t(M['content.content_list.policy_key_optional'])}
|
|
2626
|
+
value={workflowPolicyKey}
|
|
2627
|
+
disabled={workflowPending}
|
|
2628
|
+
oninput={(event: Event) => workflowPolicyKey = (event.currentTarget as HTMLInputElement).value}
|
|
2629
|
+
/>
|
|
2630
|
+
{:else if selectedWorkflow === 'optimize'}
|
|
2631
|
+
<Input
|
|
2632
|
+
aria-label={t(M['content.content_list.optimization_instructions'])}
|
|
2633
|
+
placeholder={t(M['content.content_list.optimization_instructions_optional'])}
|
|
2634
|
+
value={workflowInstructions}
|
|
2635
|
+
disabled={workflowPending}
|
|
2636
|
+
oninput={(event: Event) => workflowInstructions = (event.currentTarget as HTMLInputElement).value}
|
|
2637
|
+
/>
|
|
2638
|
+
{/if}
|
|
2639
|
+
|
|
2640
|
+
<Button
|
|
2641
|
+
variant="ghost"
|
|
2642
|
+
size="sm"
|
|
2643
|
+
type="button"
|
|
2644
|
+
disabled={
|
|
2645
|
+
workflowPending || !workflowPayloadValid || workflowDuplicateQueued
|
|
2646
|
+
}
|
|
2647
|
+
onclick={() => void previewWorkflow()}
|
|
2648
|
+
>
|
|
2649
|
+
{workflowPending
|
|
2650
|
+
? t(M['content.content_list.workflow_working'])
|
|
2651
|
+
: workflowDuplicateQueued
|
|
2652
|
+
? t(M['content.content_list.workflow_job_queued'])
|
|
2653
|
+
: t(M['content.content_list.workflow_preview'])}
|
|
2654
|
+
</Button>
|
|
2655
|
+
|
|
2656
|
+
{#if workflowError}
|
|
2657
|
+
<p class="content-workflows__error" role="alert">{workflowError}</p>
|
|
2658
|
+
{/if}
|
|
2659
|
+
{#if workflowResult?.ok}
|
|
2660
|
+
<p class="content-workflows__result" role="status">
|
|
2661
|
+
{workflowResultMessage(workflowResult)}
|
|
2662
|
+
{#if typeof workflowResult.details?.jobId === 'string'}
|
|
2663
|
+
{t(M['content.content_list.job_queued_progress'], {
|
|
2664
|
+
id: workflowResult.details.jobId,
|
|
2665
|
+
})}
|
|
2666
|
+
{/if}
|
|
2667
|
+
</p>
|
|
2668
|
+
{/if}
|
|
2669
|
+
</section>
|
|
2670
|
+
{/if}
|
|
2671
|
+
|
|
1712
2672
|
{#if refreshing && viewMode !== 'compact'}
|
|
1713
2673
|
<!-- DataTable announces its own refresh; the card views need their own. -->
|
|
1714
2674
|
<p class="content-refreshing" role="status" aria-live="polite">
|
|
@@ -1754,7 +2714,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1754
2714
|
<div class="content-row__select">
|
|
1755
2715
|
<Checkbox
|
|
1756
2716
|
checked={isSelected(row)}
|
|
1757
|
-
disabled={!row.identified || rowPending(row)}
|
|
2717
|
+
disabled={workflowPending || !row.identified || rowPending(row) || allMatchingSelected}
|
|
1758
2718
|
aria-label={selectRowLabel(row)}
|
|
1759
2719
|
title={row.identified
|
|
1760
2720
|
? rowPending(row)
|
|
@@ -1848,7 +2808,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1848
2808
|
<div class="content-header__eyebrow">
|
|
1849
2809
|
<Checkbox
|
|
1850
2810
|
checked={isSelected(row)}
|
|
1851
|
-
disabled={!row.identified || rowPending(row)}
|
|
2811
|
+
disabled={workflowPending || !row.identified || rowPending(row) || allMatchingSelected}
|
|
1852
2812
|
aria-label={selectRowLabel(row)}
|
|
1853
2813
|
title={row.identified
|
|
1854
2814
|
? rowPending(row)
|
|
@@ -1945,6 +2905,23 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
1945
2905
|
oncancel={cancelDelete}
|
|
1946
2906
|
/>
|
|
1947
2907
|
|
|
2908
|
+
<ConfirmDialog
|
|
2909
|
+
open={workflowConfirmOpen}
|
|
2910
|
+
title={t(M['content.content_list.workflow_confirm'], {
|
|
2911
|
+
workflow: selectedWorkflow
|
|
2912
|
+
? workflowLabel(selectedWorkflow)
|
|
2913
|
+
: t(M['content.content_list.workflow_noun']),
|
|
2914
|
+
})}
|
|
2915
|
+
message={workflowPreviewMessage()}
|
|
2916
|
+
confirmLabel={workflowPending
|
|
2917
|
+
? t(M['content.content_list.workflow_applying'])
|
|
2918
|
+
: t(M['content.content_list.workflow_apply'])}
|
|
2919
|
+
cancelLabel={t(M['content.content_list.cancel'])}
|
|
2920
|
+
destructive={CONTENT_LIST_WORKFLOW_OPTIONS.find((option) => option.id === selectedWorkflow)?.sensitivity === 'sensitive'}
|
|
2921
|
+
onconfirm={() => void applyWorkflow()}
|
|
2922
|
+
oncancel={cancelWorkflowConfirmation}
|
|
2923
|
+
/>
|
|
2924
|
+
|
|
1948
2925
|
<style>
|
|
1949
2926
|
.content-list-wrapper {
|
|
1950
2927
|
display: flex;
|
|
@@ -2059,6 +3036,33 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
|
|
|
2059
3036
|
cursor: pointer;
|
|
2060
3037
|
}
|
|
2061
3038
|
|
|
3039
|
+
.content-workflows {
|
|
3040
|
+
display: flex;
|
|
3041
|
+
align-items: center;
|
|
3042
|
+
flex-wrap: wrap;
|
|
3043
|
+
gap: 0.75rem;
|
|
3044
|
+
margin-bottom: 1rem;
|
|
3045
|
+
padding: 0.75rem;
|
|
3046
|
+
border: 1px solid var(--smrt-color-outline-variant);
|
|
3047
|
+
border-radius: 0.65rem;
|
|
3048
|
+
background: var(--smrt-color-surface-container-low);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
.content-workflows__error,
|
|
3052
|
+
.content-workflows__result {
|
|
3053
|
+
flex-basis: 100%;
|
|
3054
|
+
margin: 0;
|
|
3055
|
+
font-size: var(--smrt-typography-body-medium-size, 0.875rem);
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
.content-workflows__error {
|
|
3059
|
+
color: var(--smrt-color-error);
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
.content-workflows__result {
|
|
3063
|
+
color: var(--smrt-color-on-surface-variant);
|
|
3064
|
+
}
|
|
3065
|
+
|
|
2062
3066
|
.content-header__eyebrow,
|
|
2063
3067
|
.content-row__eyebrow {
|
|
2064
3068
|
display: flex;
|