@sanity/workflow-studio-plugin 0.20.0 → 0.22.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/CHANGELOG.md +123 -0
- package/README.md +9 -1
- package/dist/_chunks-cjs/index.cjs +611 -290
- package/dist/_chunks-cjs/workflows-tool-root.cjs +504 -23
- package/dist/_chunks-es/index.js +607 -292
- package/dist/_chunks-es/workflows-tool-root.js +508 -25
- package/package.json +16 -14
|
@@ -2,11 +2,13 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
|
2
2
|
|
|
3
3
|
import { Stack, Box, Flex, Text, Button, Card, TextInput, Dialog, Badge, useTheme_v2, Container, TabPanel } from "@sanity/ui";
|
|
4
4
|
|
|
5
|
+
import { useWorkflowTelemetry, editFieldTarget } from "@sanity/workflow-react";
|
|
6
|
+
|
|
5
7
|
import { useState, useEffect, useMemo, useRef } from "react";
|
|
6
8
|
|
|
7
9
|
import { useRouter } from "sanity/router";
|
|
8
10
|
|
|
9
|
-
import { instanceNotice, StaleLock, ActivitiesList, TodoItemsList, isEvaluationStale, useWorkflowInstanceEntry, InvalidDocNotice, LoadingRow, useDefinition, openActivityGone, GroupHeading, ActivityDetailDialog, instanceBreadcrumb, HoverHint, instanceTitle, formatDate, StageChip, stageTitle, AbortedBadge, SpinnerSlot, DocPreviewLink, useWorkflowContext, readDeployedDefinitions, describeError, DismissablePopover, definitionSnapshotOf, findActivity, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isActivityAssignedTo, assigneeUserIdsOf, isOpenActivityStatus, committedRowFace, activityRowProps,
|
|
11
|
+
import { instanceNotice, StaleLock, ActivitiesList, TodoItemsList, isEvaluationStale, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, InvalidDocNotice, LoadingRow, useDefinition, openActivityGone, GroupHeading, ActivityDetailDialog, instanceBreadcrumb, HoverHint, instanceTitle, formatDate, StageChip, stageTitle, AbortedBadge, SpinnerSlot, DocPreviewLink, useWorkflowContext, readDeployedDefinitions, describeError, DismissablePopover, definitionSnapshotOf, findActivity, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isActivityAssignedTo, assigneeUserIdsOf, isOpenActivityStatus, committedRowFace, activityRowProps, gdrLocality, CautionNote, WORKFLOW_API_VERSION, useClosableToast, parseStoredDateValue, useUserDisplays, useProjectMembers, WorkflowTaskFiltersApplied, DocRefFace, useSaveField, dateControlValue, stopRowMouseDown, stopRowClick, parseDateFieldValue, serializeDateFieldValue, dateControlKind, CollapsibleBand, LinkChip, BreadcrumbTail, StageFace, TrailingHairline, ActivityRow, UnreadableDocsNote, useAssignmentIdentity, TabSwitch, WorkflowToolOpened, workflowsTabCodec } from "./index.js";
|
|
10
12
|
|
|
11
13
|
import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
|
|
12
14
|
|
|
@@ -18,6 +20,8 @@ import { AddIcon } from "@sanity/icons/Add";
|
|
|
18
20
|
|
|
19
21
|
import { SearchIcon } from "@sanity/icons/Search";
|
|
20
22
|
|
|
23
|
+
import { useClient, useDocumentPreviewStore, useSchema } from "sanity";
|
|
24
|
+
|
|
21
25
|
import { CheckmarkIcon } from "@sanity/icons/Checkmark";
|
|
22
26
|
|
|
23
27
|
import { CloseIcon } from "@sanity/icons/Close";
|
|
@@ -30,8 +34,6 @@ import { endOfDay } from "date-fns/endOfDay";
|
|
|
30
34
|
|
|
31
35
|
import { createPortal } from "react-dom";
|
|
32
36
|
|
|
33
|
-
import { useDocumentPreviewStore, useSchema } from "sanity";
|
|
34
|
-
|
|
35
37
|
import { CalendarIcon } from "@sanity/icons/Calendar";
|
|
36
38
|
|
|
37
39
|
import { useWorkflowInstances } from "@sanity/workflow-studio";
|
|
@@ -52,14 +54,19 @@ function InstanceTaskLists({entry: entry, onOpenActivity: onOpenActivity}) {
|
|
|
52
54
|
onOpenActivity: onOpenActivity
|
|
53
55
|
}),
|
|
54
56
|
/* @__PURE__ */ jsx(TodoItemsList, {
|
|
55
|
-
entry: entry
|
|
57
|
+
entry: entry,
|
|
58
|
+
surface: "tool-instance-detail"
|
|
56
59
|
}) ]
|
|
57
60
|
})
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
function WorkflowInstanceDetail({instanceId: instanceId, onBack: onBack}) {
|
|
62
|
-
const entry = useWorkflowInstanceEntry(instanceId)
|
|
65
|
+
const entry = useWorkflowInstanceEntry(instanceId);
|
|
66
|
+
useLogEventOnMount(WorkflowInstanceDetailViewed, {
|
|
67
|
+
instanceId: instanceId
|
|
68
|
+
});
|
|
69
|
+
const [graceOver, setGraceOver] = useState(!1);
|
|
63
70
|
return useEffect(() => {
|
|
64
71
|
const timer = setTimeout(() => setGraceOver(!0), 2500);
|
|
65
72
|
return () => clearTimeout(timer);
|
|
@@ -153,7 +160,8 @@ function InstanceDetailPanel({entry: entry, onBack: onBack}) {
|
|
|
153
160
|
breadcrumb: instanceBreadcrumb(instance, definition),
|
|
154
161
|
definition: definition,
|
|
155
162
|
entry: entry,
|
|
156
|
-
onClose: () => setOpenActivity(null)
|
|
163
|
+
onClose: () => setOpenActivity(null),
|
|
164
|
+
source: "tool-instance-detail"
|
|
157
165
|
}) : null ]
|
|
158
166
|
});
|
|
159
167
|
}
|
|
@@ -294,7 +302,8 @@ function NewWorkflowButton() {
|
|
|
294
302
|
onPick: definition => {
|
|
295
303
|
setOpen(!1), openStartDialog({
|
|
296
304
|
definition: definition.name,
|
|
297
|
-
label: definition.title
|
|
305
|
+
label: definition.title,
|
|
306
|
+
source: "tool"
|
|
298
307
|
});
|
|
299
308
|
}
|
|
300
309
|
}) : null,
|
|
@@ -401,7 +410,8 @@ function ToolActivityDialog({target: target, onClose: onClose}) {
|
|
|
401
410
|
definition: definition,
|
|
402
411
|
document: documentRefsOf(entry.instance)[0],
|
|
403
412
|
entry: entry,
|
|
404
|
-
onClose: onClose
|
|
413
|
+
onClose: onClose,
|
|
414
|
+
source: "tool-task-list"
|
|
405
415
|
}) : /* @__PURE__ */ jsx(Dialog, {
|
|
406
416
|
header: "Loading activity…",
|
|
407
417
|
id: "workflow-activity-detail",
|
|
@@ -416,6 +426,147 @@ function ToolActivityDialog({target: target, onClose: onClose}) {
|
|
|
416
426
|
});
|
|
417
427
|
}
|
|
418
428
|
|
|
429
|
+
const GROUP_DEFAULT_OPEN = !0;
|
|
430
|
+
|
|
431
|
+
function instanceBandDefaultOpen(segment) {
|
|
432
|
+
return segment === "for-me";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const UNGROUPED_BAND_KEY = "section:no-document", PAYLOAD_VERSION = 1, MAX_ENTRIES = 200;
|
|
436
|
+
|
|
437
|
+
function bandStateStorageKey(scope) {
|
|
438
|
+
return `sanity.workflows.tool.bands:${scope}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function isRecord(value) {
|
|
442
|
+
return typeof value == "object" && value !== null && !Array.isArray(value);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function bandEntryOf(value) {
|
|
446
|
+
if (!isRecord(value)) return;
|
|
447
|
+
const {open: open, touched: touched} = value;
|
|
448
|
+
if (!(typeof open != "boolean" || typeof touched != "number") && Number.isFinite(touched)) return {
|
|
449
|
+
open: open,
|
|
450
|
+
touched: touched
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function parsePayload(raw) {
|
|
455
|
+
try {
|
|
456
|
+
const parsed = JSON.parse(raw);
|
|
457
|
+
if (!isRecord(parsed) || parsed.version !== PAYLOAD_VERSION || !isRecord(parsed.bands)) return;
|
|
458
|
+
const entries = [];
|
|
459
|
+
for (const [key, value] of Object.entries(parsed.bands)) {
|
|
460
|
+
const entry = bandEntryOf(value);
|
|
461
|
+
if (entry === void 0) return;
|
|
462
|
+
entries.push([ key, entry ]);
|
|
463
|
+
}
|
|
464
|
+
return Object.fromEntries(entries);
|
|
465
|
+
} catch {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function readStored(storage, storageKey) {
|
|
471
|
+
if (!storage) return {
|
|
472
|
+
entries: void 0,
|
|
473
|
+
corrupt: !1
|
|
474
|
+
};
|
|
475
|
+
let raw;
|
|
476
|
+
try {
|
|
477
|
+
raw = storage.getItem(storageKey);
|
|
478
|
+
} catch {
|
|
479
|
+
return {
|
|
480
|
+
entries: void 0,
|
|
481
|
+
corrupt: !1
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (raw === null) return {
|
|
485
|
+
entries: {},
|
|
486
|
+
corrupt: !1
|
|
487
|
+
};
|
|
488
|
+
const entries = parsePayload(raw);
|
|
489
|
+
return {
|
|
490
|
+
entries: entries,
|
|
491
|
+
corrupt: entries === void 0
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function writeStored(args) {
|
|
496
|
+
if (!args.storage) return !1;
|
|
497
|
+
try {
|
|
498
|
+
return args.storage.setItem(args.storageKey, JSON.stringify({
|
|
499
|
+
version: PAYLOAD_VERSION,
|
|
500
|
+
bands: args.entries
|
|
501
|
+
})), !0;
|
|
502
|
+
} catch {
|
|
503
|
+
return !1;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function baseEntries(args) {
|
|
508
|
+
return args.stored === void 0 ? args.held : args.unpersisted ? mergeByTouched(args.stored, args.held) : args.stored;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function mergeByTouched(stored, held) {
|
|
512
|
+
const merged = {
|
|
513
|
+
...stored
|
|
514
|
+
};
|
|
515
|
+
for (const [key, entry] of Object.entries(held)) {
|
|
516
|
+
const persisted = merged[key];
|
|
517
|
+
(persisted === void 0 || entry.touched >= persisted.touched) && (merged[key] = entry);
|
|
518
|
+
}
|
|
519
|
+
return merged;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function prune(entries) {
|
|
523
|
+
const held = Object.entries(entries);
|
|
524
|
+
return held.length <= MAX_ENTRIES ? entries : Object.fromEntries(held.sort(([, a], [, b]) => b.touched - a.touched).slice(0, MAX_ENTRIES));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function togglesOf(entries) {
|
|
528
|
+
return new Map(Object.entries(entries).map(([key, entry]) => [ key, entry.open ]));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function defaultStorage() {
|
|
532
|
+
try {
|
|
533
|
+
return globalThis.localStorage;
|
|
534
|
+
} catch {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function createBandStateStore(args) {
|
|
540
|
+
const storage = args.storage ?? defaultStorage(), initial = readStored(storage, args.storageKey);
|
|
541
|
+
initial.corrupt && writeStored({
|
|
542
|
+
storage: storage,
|
|
543
|
+
storageKey: args.storageKey,
|
|
544
|
+
entries: {}
|
|
545
|
+
});
|
|
546
|
+
let held = initial.entries ?? {}, unpersisted = !1;
|
|
547
|
+
return {
|
|
548
|
+
read: () => togglesOf(held),
|
|
549
|
+
setOpen(key, open) {
|
|
550
|
+
const stored = readStored(storage, args.storageKey).entries, base = baseEntries({
|
|
551
|
+
stored: stored,
|
|
552
|
+
held: held,
|
|
553
|
+
unpersisted: unpersisted
|
|
554
|
+
});
|
|
555
|
+
return held = prune({
|
|
556
|
+
...base,
|
|
557
|
+
[key]: {
|
|
558
|
+
open: open,
|
|
559
|
+
touched: Date.now()
|
|
560
|
+
}
|
|
561
|
+
}), unpersisted = !writeStored({
|
|
562
|
+
storage: storage,
|
|
563
|
+
storageKey: args.storageKey,
|
|
564
|
+
entries: held
|
|
565
|
+
}), togglesOf(held);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
419
570
|
function datesOf(state) {
|
|
420
571
|
return state.filter(entry => entry._type === "date" || entry._type === "datetime").map(entry => entry.value).filter(value => typeof value == "string" && value !== "");
|
|
421
572
|
}
|
|
@@ -573,6 +724,223 @@ function taskRowRender(row, entry) {
|
|
|
573
724
|
};
|
|
574
725
|
}
|
|
575
726
|
|
|
727
|
+
const NO_ORPHANS = {
|
|
728
|
+
groupKeys: /* @__PURE__ */ new Set,
|
|
729
|
+
workflows: []
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
function localBareIds(group, contentResource) {
|
|
733
|
+
const ids = group.documents.map(doc => gdrLocality(doc.id, contentResource));
|
|
734
|
+
if (!ids.some(locality => !locality.parsed || !locality.local)) return ids.flatMap(locality => locality.parsed ? [ locality.parsed.documentId ] : []);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function orphanProbeIds(groups, contentResource) {
|
|
738
|
+
const ids = /* @__PURE__ */ new Set;
|
|
739
|
+
for (const group of groups.withDocuments) for (const id of localBareIds(group, contentResource) ?? []) ids.add(id);
|
|
740
|
+
return [ ...ids ].sort();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function orphanVerdict(args) {
|
|
744
|
+
const groupKeys = /* @__PURE__ */ new Set, workflows = /* @__PURE__ */ new Map;
|
|
745
|
+
for (const group of args.groups.withDocuments) {
|
|
746
|
+
const ids = localBareIds(group, args.contentResource);
|
|
747
|
+
if (!(ids === void 0 || !ids.every(id => args.missing.has(id)))) {
|
|
748
|
+
groupKeys.add(group.key);
|
|
749
|
+
for (const band of group.instances) workflows.set(band.instanceId, {
|
|
750
|
+
instanceId: band.instanceId,
|
|
751
|
+
title: band.title,
|
|
752
|
+
documentIds: ids
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return groupKeys.size === 0 ? NO_ORPHANS : {
|
|
757
|
+
groupKeys: groupKeys,
|
|
758
|
+
workflows: [ ...workflows.values() ]
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function hideOrphanedGroups(groups, orphaned) {
|
|
763
|
+
return orphaned.size === 0 ? groups : cutTaskGroups(groups, {
|
|
764
|
+
keepRow: () => !0,
|
|
765
|
+
keepGroup: group => !orphaned.has(group.key)
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const DRAFT_PREFIX = "drafts.";
|
|
770
|
+
|
|
771
|
+
async function missingDocIds(client, ids) {
|
|
772
|
+
const present = await client.fetch("*[_id in $ids || _id in $drafts]._id", {
|
|
773
|
+
ids: ids,
|
|
774
|
+
drafts: ids.map(id => `${DRAFT_PREFIX}${id}`)
|
|
775
|
+
}, {
|
|
776
|
+
perspective: "raw"
|
|
777
|
+
}), found = new Set(present.map(id => id.startsWith(DRAFT_PREFIX) ? id.slice(DRAFT_PREFIX.length) : id)), candidates = ids.filter(id => !found.has(id)), versions = await Promise.all(candidates.map(id => client.fetch("*[_id in path($version)][0]._id", {
|
|
778
|
+
version: `versions.*.${id}`
|
|
779
|
+
}, {
|
|
780
|
+
perspective: "raw"
|
|
781
|
+
})));
|
|
782
|
+
return new Set(candidates.filter((_, index) => versions[index] === null));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async function settleOrphans(args) {
|
|
786
|
+
const ids = [ ...new Set(args.workflows.flatMap(workflow => workflow.documentIds)) ].sort(), missing = await args.probeMissing(ids), stillOrphaned = workflow => workflow.documentIds.every(id => missing.has(id));
|
|
787
|
+
let settled = 0;
|
|
788
|
+
const failures = [];
|
|
789
|
+
for (const workflow of args.workflows.filter(stillOrphaned)) try {
|
|
790
|
+
await args.abort({
|
|
791
|
+
instanceId: workflow.instanceId,
|
|
792
|
+
reason: "Subject document deleted"
|
|
793
|
+
}), settled += 1;
|
|
794
|
+
} catch (err) {
|
|
795
|
+
failures.push({
|
|
796
|
+
workflow: workflow,
|
|
797
|
+
message: describeError(err)
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
settled: settled,
|
|
802
|
+
skipped: args.workflows.filter(workflow => !stillOrphaned(workflow)),
|
|
803
|
+
failures: failures
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function workflowTally(workflows) {
|
|
808
|
+
const counts = /* @__PURE__ */ new Map;
|
|
809
|
+
for (const workflow of workflows) counts.set(workflow.title, (counts.get(workflow.title) ?? 0) + 1);
|
|
810
|
+
return [ ...counts ].map(([title, count]) => count === 1 ? title : `${title} ×${count}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function settleToast(outcome) {
|
|
814
|
+
const total = outcome.settled + outcome.skipped.length + outcome.failures.length;
|
|
815
|
+
if (outcome.skipped.length === 0 && outcome.failures.length === 0) return {
|
|
816
|
+
status: "success",
|
|
817
|
+
title: outcome.settled === 1 ? "Cleaned up 1 workflow" : `Cleaned up ${outcome.settled} workflows`
|
|
818
|
+
};
|
|
819
|
+
const lines = [ ...outcome.skipped.map(workflow => `${workflow.title}: its document exists again — left running`), ...outcome.failures.map(failure => `${failure.workflow.title}: ${failure.message}`) ];
|
|
820
|
+
return {
|
|
821
|
+
status: "warning",
|
|
822
|
+
title: `Cleaned up ${outcome.settled} of ${total} workflows`,
|
|
823
|
+
description: lines.join(`\n`)
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function recheckFailedToast(err) {
|
|
828
|
+
return {
|
|
829
|
+
status: "error",
|
|
830
|
+
title: "Nothing was cleaned up — the documents could not be re-checked",
|
|
831
|
+
description: describeError(err)
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function OrphanedWorkflowsNote({orphans: orphans}) {
|
|
836
|
+
const [confirming, setConfirming] = useState(!1), count = orphans.workflows.length;
|
|
837
|
+
if (count === 0) return null;
|
|
838
|
+
const label = count === 1 ? "1 workflow is attached to a deleted document — its tasks aren’t listed." : `${count} workflows are attached to deleted documents — their tasks aren’t listed.`;
|
|
839
|
+
/* @__PURE__ */
|
|
840
|
+
return jsxs(Fragment, {
|
|
841
|
+
children: [
|
|
842
|
+
/* @__PURE__ */ jsx(CautionNote, {
|
|
843
|
+
action: /* @__PURE__ */ jsx(Button, {
|
|
844
|
+
fontSize: 1,
|
|
845
|
+
mode: "ghost",
|
|
846
|
+
onClick: () => setConfirming(!0),
|
|
847
|
+
padding: 2,
|
|
848
|
+
text: "Clean up…"
|
|
849
|
+
}),
|
|
850
|
+
label: label
|
|
851
|
+
}), confirming ? /* @__PURE__ */ jsx(SettleOrphansDialog, {
|
|
852
|
+
onClose: () => setConfirming(!1),
|
|
853
|
+
workflows: orphans.workflows
|
|
854
|
+
}) : null ]
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function SettleOrphansDialog({workflows: workflows, onClose: onClose}) {
|
|
859
|
+
const {engine: engine} = useWorkflowContext(), client = useClient({
|
|
860
|
+
apiVersion: WORKFLOW_API_VERSION
|
|
861
|
+
}), toast = useClosableToast(), [pending, setPending] = useState(!1), confirm = async () => {
|
|
862
|
+
setPending(!0);
|
|
863
|
+
try {
|
|
864
|
+
const outcome = await settleOrphans({
|
|
865
|
+
workflows: workflows,
|
|
866
|
+
probeMissing: ids => missingDocIds(client, ids),
|
|
867
|
+
abort: args => engine.abortInstance(args)
|
|
868
|
+
});
|
|
869
|
+
toast.push(settleToast(outcome));
|
|
870
|
+
} catch (err) {
|
|
871
|
+
toast.push(recheckFailedToast(err));
|
|
872
|
+
}
|
|
873
|
+
onClose();
|
|
874
|
+
};
|
|
875
|
+
/* @__PURE__ */
|
|
876
|
+
return jsx(Dialog, {
|
|
877
|
+
footer: /* @__PURE__ */ jsx(Box, {
|
|
878
|
+
padding: 3,
|
|
879
|
+
children: /* @__PURE__ */ jsxs(Flex, {
|
|
880
|
+
gap: 2,
|
|
881
|
+
justify: "flex-end",
|
|
882
|
+
children: [
|
|
883
|
+
/* @__PURE__ */ jsx(Button, {
|
|
884
|
+
disabled: pending,
|
|
885
|
+
fontSize: 1,
|
|
886
|
+
mode: "bleed",
|
|
887
|
+
onClick: onClose,
|
|
888
|
+
padding: 2,
|
|
889
|
+
text: "Cancel"
|
|
890
|
+
}),
|
|
891
|
+
/* @__PURE__ */ jsx(Button, {
|
|
892
|
+
fontSize: 1,
|
|
893
|
+
loading: pending,
|
|
894
|
+
onClick: () => {
|
|
895
|
+
confirm();
|
|
896
|
+
},
|
|
897
|
+
padding: 2,
|
|
898
|
+
text: workflows.length === 1 ? "Clean up 1 workflow" : `Clean up ${workflows.length} workflows`
|
|
899
|
+
}) ]
|
|
900
|
+
})
|
|
901
|
+
}),
|
|
902
|
+
header: "Clean up workflows",
|
|
903
|
+
id: "settle-orphaned-workflows",
|
|
904
|
+
onClose: onClose,
|
|
905
|
+
width: 0,
|
|
906
|
+
children: /* @__PURE__ */ jsx(Box, {
|
|
907
|
+
padding: 4,
|
|
908
|
+
children: /* @__PURE__ */ jsxs(Stack, {
|
|
909
|
+
gap: 4,
|
|
910
|
+
children: [
|
|
911
|
+
/* @__PURE__ */ jsx(Text, {
|
|
912
|
+
muted: !0,
|
|
913
|
+
size: 1,
|
|
914
|
+
children: "The following workflows exist for documents which have since been deleted:"
|
|
915
|
+
}),
|
|
916
|
+
/* @__PURE__ */ jsx(Stack, {
|
|
917
|
+
as: "ul",
|
|
918
|
+
gap: 2,
|
|
919
|
+
style: {
|
|
920
|
+
listStyle: "disc",
|
|
921
|
+
margin: 0,
|
|
922
|
+
paddingLeft: "1.25em"
|
|
923
|
+
},
|
|
924
|
+
children: workflowTally(workflows).map(line => /* @__PURE__ */ jsx(Text, {
|
|
925
|
+
as: "li",
|
|
926
|
+
size: 1,
|
|
927
|
+
style: {
|
|
928
|
+
display: "list-item"
|
|
929
|
+
},
|
|
930
|
+
weight: "medium",
|
|
931
|
+
children: line
|
|
932
|
+
}, line))
|
|
933
|
+
}),
|
|
934
|
+
/* @__PURE__ */ jsx(Text, {
|
|
935
|
+
muted: !0,
|
|
936
|
+
size: 1,
|
|
937
|
+
children: "Cleaning up these workflows will close them."
|
|
938
|
+
}) ]
|
|
939
|
+
})
|
|
940
|
+
})
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
576
944
|
function taskFilterOptions(groups) {
|
|
577
945
|
const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
|
|
578
946
|
for (const group of groups.withDocuments) {
|
|
@@ -589,8 +957,33 @@ function taskFilterOptions(groups) {
|
|
|
589
957
|
};
|
|
590
958
|
}
|
|
591
959
|
|
|
960
|
+
function activeEntries(filters) {
|
|
961
|
+
return Object.entries(filters).filter(([, value]) => value !== void 0 && (!Array.isArray(value) || value.length > 0));
|
|
962
|
+
}
|
|
963
|
+
|
|
592
964
|
function hasActiveFilters(filters) {
|
|
593
|
-
return
|
|
965
|
+
return activeEntries(filters).length > 0;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function sameFilters(a, b) {
|
|
969
|
+
return sameValueSet(a.statuses, b.statuses) && sameValueSet(a.workflows, b.workflows) && sameValueSet(a.documentIds, b.documentIds) && sameValueSet(a.assigneeIds, b.assigneeIds) && sameDate(a.date, b.date);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function sameValueSet(a, b) {
|
|
973
|
+
const av = a ?? [], bv = b ?? [];
|
|
974
|
+
return av.length === bv.length && av.every(value => bv.includes(value));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function sameDate(a, b) {
|
|
978
|
+
return typeof a == "object" && typeof b == "object" ? a.before === b.before : a === b;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function taskFilterTelemetry(filters) {
|
|
982
|
+
const entries = activeEntries(filters);
|
|
983
|
+
return {
|
|
984
|
+
filterKinds: entries.map(([key]) => key).sort(),
|
|
985
|
+
filterCount: entries.reduce((n, [, value]) => n + (Array.isArray(value) ? value.length : 1), 0)
|
|
986
|
+
};
|
|
594
987
|
}
|
|
595
988
|
|
|
596
989
|
function toggledValue(args) {
|
|
@@ -745,7 +1138,12 @@ function useDocPreviewTitles(documents) {
|
|
|
745
1138
|
const HITS_CAP = 6;
|
|
746
1139
|
|
|
747
1140
|
function TaskFilterMenu({filters: filters, options: options, onChange: onChange}) {
|
|
748
|
-
const [open, setOpen] = useState(!1),
|
|
1141
|
+
const [open, setOpen] = useState(!1), telemetry = useWorkflowTelemetry(), openSnapshot = useRef(void 0), openMenu = () => {
|
|
1142
|
+
openSnapshot.current = filters, setOpen(!0);
|
|
1143
|
+
}, close = () => {
|
|
1144
|
+
setOpen(!1), openSnapshot.current !== void 0 && !sameFilters(openSnapshot.current, filters) && telemetry.log(WorkflowTaskFiltersApplied, taskFilterTelemetry(filters)),
|
|
1145
|
+
openSnapshot.current = void 0;
|
|
1146
|
+
}, active = hasActiveFilters(filters);
|
|
749
1147
|
/* @__PURE__ */
|
|
750
1148
|
return jsx(DismissablePopover, {
|
|
751
1149
|
content: open ? /* @__PURE__ */ jsx(FilterPanel, {
|
|
@@ -753,7 +1151,7 @@ function TaskFilterMenu({filters: filters, options: options, onChange: onChange}
|
|
|
753
1151
|
onChange: onChange,
|
|
754
1152
|
options: options
|
|
755
1153
|
}) : null,
|
|
756
|
-
onDismiss:
|
|
1154
|
+
onDismiss: close,
|
|
757
1155
|
open: open,
|
|
758
1156
|
overflow: "visible",
|
|
759
1157
|
children: /* @__PURE__ */ jsx(HoverHint, {
|
|
@@ -764,7 +1162,7 @@ function TaskFilterMenu({filters: filters, options: options, onChange: onChange}
|
|
|
764
1162
|
fontSize: 1,
|
|
765
1163
|
icon: FilterIcon,
|
|
766
1164
|
mode: "bleed",
|
|
767
|
-
onClick: () =>
|
|
1165
|
+
onClick: () => open ? close() : openMenu(),
|
|
768
1166
|
padding: 2,
|
|
769
1167
|
selected: active || open
|
|
770
1168
|
})
|
|
@@ -1260,7 +1658,7 @@ function ActivityDateControl({instanceId: instanceId, state: state, dates: dates
|
|
|
1260
1658
|
});
|
|
1261
1659
|
const kind = dateControlKind(state), commit = (mode, value) => {
|
|
1262
1660
|
state.kind === "editable" && save(() => editFieldFor(instanceId, {
|
|
1263
|
-
target:
|
|
1661
|
+
target: editFieldTarget(state.field),
|
|
1264
1662
|
...mode === "set" ? {
|
|
1265
1663
|
mode: mode,
|
|
1266
1664
|
value: value
|
|
@@ -1361,13 +1759,15 @@ function RefChips({refs: refs, max: max = 2}) {
|
|
|
1361
1759
|
|
|
1362
1760
|
const EMPTY_NOTE = "No tasks match — adjust the filters, or start a workflow", UNGROUPED_TITLE = "Workflows without Studio documents";
|
|
1363
1761
|
|
|
1364
|
-
function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
|
|
1762
|
+
function TaskGroupList({bands: bands, groups: groups, instanceDefaultOpen: instanceDefaultOpen, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
|
|
1365
1763
|
return groups.withDocuments.length === 0 && groups.withoutDocuments.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
|
|
1366
1764
|
text: EMPTY_NOTE
|
|
1367
1765
|
}) : /* @__PURE__ */ jsxs(Stack, {
|
|
1368
1766
|
gap: 2,
|
|
1369
1767
|
children: [ groups.withDocuments.map(group => /* @__PURE__ */ jsx(CollapsibleBand, {
|
|
1370
1768
|
background: !0,
|
|
1769
|
+
onToggle: () => bands.toggle(group.key, GROUP_DEFAULT_OPEN),
|
|
1770
|
+
open: bands.isOpen(group.key, GROUP_DEFAULT_OPEN),
|
|
1371
1771
|
sticky: !0,
|
|
1372
1772
|
title: toBareId(group.document.id),
|
|
1373
1773
|
header: /* @__PURE__ */ jsx(Flex, {
|
|
@@ -1382,6 +1782,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
|
|
|
1382
1782
|
})
|
|
1383
1783
|
}),
|
|
1384
1784
|
children: /* @__PURE__ */ jsx(InstanceBands, {
|
|
1785
|
+
bands: bands,
|
|
1786
|
+
instanceDefaultOpen: instanceDefaultOpen,
|
|
1385
1787
|
instances: group.instances,
|
|
1386
1788
|
onOpenTask: onOpenTask,
|
|
1387
1789
|
onOpenWorkflow: onOpenWorkflow,
|
|
@@ -1389,6 +1791,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
|
|
|
1389
1791
|
})
|
|
1390
1792
|
}, group.key)), groups.withoutDocuments.length > 0 ? /* @__PURE__ */ jsx(CollapsibleBand, {
|
|
1391
1793
|
background: !0,
|
|
1794
|
+
onToggle: () => bands.toggle(UNGROUPED_BAND_KEY, GROUP_DEFAULT_OPEN),
|
|
1795
|
+
open: bands.isOpen(UNGROUPED_BAND_KEY, GROUP_DEFAULT_OPEN),
|
|
1392
1796
|
sticky: !0,
|
|
1393
1797
|
title: UNGROUPED_TITLE,
|
|
1394
1798
|
header: /* @__PURE__ */ jsx(Text, {
|
|
@@ -1397,6 +1801,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
|
|
|
1397
1801
|
children: UNGROUPED_TITLE
|
|
1398
1802
|
}),
|
|
1399
1803
|
children: /* @__PURE__ */ jsx(InstanceBands, {
|
|
1804
|
+
bands: bands,
|
|
1805
|
+
instanceDefaultOpen: instanceDefaultOpen,
|
|
1400
1806
|
instances: groups.withoutDocuments,
|
|
1401
1807
|
onOpenTask: onOpenTask,
|
|
1402
1808
|
onOpenWorkflow: onOpenWorkflow,
|
|
@@ -1414,12 +1820,14 @@ function targetOf(row) {
|
|
|
1414
1820
|
};
|
|
1415
1821
|
}
|
|
1416
1822
|
|
|
1417
|
-
function InstanceBands({instances: instances, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
|
|
1823
|
+
function InstanceBands({bands: bands, instanceDefaultOpen: instanceDefaultOpen, instances: instances, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
|
|
1418
1824
|
/* @__PURE__ */
|
|
1419
1825
|
return jsx(Stack, {
|
|
1420
1826
|
gap: 1,
|
|
1421
1827
|
children: instances.map(band => /* @__PURE__ */ jsx(InstanceBand, {
|
|
1422
1828
|
band: band,
|
|
1829
|
+
bands: bands,
|
|
1830
|
+
instanceDefaultOpen: instanceDefaultOpen,
|
|
1423
1831
|
onOpenTask: onOpenTask,
|
|
1424
1832
|
onOpenWorkflow: onOpenWorkflow,
|
|
1425
1833
|
showTerminalActions: showTerminalActions
|
|
@@ -1427,10 +1835,12 @@ function InstanceBands({instances: instances, onOpenTask: onOpenTask, onOpenWork
|
|
|
1427
1835
|
});
|
|
1428
1836
|
}
|
|
1429
1837
|
|
|
1430
|
-
function InstanceBand({band: band, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
|
|
1838
|
+
function InstanceBand({band: band, bands: bands, instanceDefaultOpen: instanceDefaultOpen, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
|
|
1431
1839
|
const {resolvePathFromState: resolvePathFromState} = useRouter();
|
|
1432
1840
|
/* @__PURE__ */
|
|
1433
1841
|
return jsx(CollapsibleBand, {
|
|
1842
|
+
onToggle: () => bands.toggle(band.instanceId, instanceDefaultOpen),
|
|
1843
|
+
open: bands.isOpen(band.instanceId, instanceDefaultOpen),
|
|
1434
1844
|
title: band.title,
|
|
1435
1845
|
header: /* @__PURE__ */ jsxs(Fragment, {
|
|
1436
1846
|
children: [
|
|
@@ -1517,6 +1927,61 @@ function TaskActivityRow({row: row, onOpen: onOpen, showTerminalActions: showTer
|
|
|
1517
1927
|
});
|
|
1518
1928
|
}
|
|
1519
1929
|
|
|
1930
|
+
function useBandOpenState(args) {
|
|
1931
|
+
const {segment: segment, scope: scope} = args, store = useMemo(() => createBandStateStore({
|
|
1932
|
+
storageKey: bandStateStorageKey(scope)
|
|
1933
|
+
}), [ scope ]), [held, setHeld] = useState(() => ({
|
|
1934
|
+
store: store,
|
|
1935
|
+
toggles: store.read()
|
|
1936
|
+
}));
|
|
1937
|
+
held.store !== store && setHeld({
|
|
1938
|
+
store: store,
|
|
1939
|
+
toggles: store.read()
|
|
1940
|
+
});
|
|
1941
|
+
const qualify = key => `${segment}/${key}`, isOpen = (key, defaultOpen) => held.toggles.get(qualify(key)) ?? defaultOpen;
|
|
1942
|
+
return {
|
|
1943
|
+
isOpen: isOpen,
|
|
1944
|
+
toggle: (key, defaultOpen) => {
|
|
1945
|
+
setHeld({
|
|
1946
|
+
store: store,
|
|
1947
|
+
toggles: store.setOpen(qualify(key), !isOpen(key, defaultOpen))
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
const NO_MISSING = /* @__PURE__ */ new Set;
|
|
1954
|
+
|
|
1955
|
+
function sameIdSet(a, b) {
|
|
1956
|
+
if (a.size !== b.size) return !1;
|
|
1957
|
+
for (const id of a) if (!b.has(id)) return !1;
|
|
1958
|
+
return !0;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function useOrphanedWorkflows(groups) {
|
|
1962
|
+
const {actualTypes: actualTypes, binding: binding} = useWorkflowContext(), contentResource = binding.contentResource, key = useMemo(() => orphanProbeIds(groups, contentResource), [ groups, contentResource ]).join("|"), [missing, setMissing] = useState(NO_MISSING);
|
|
1963
|
+
return useEffect(() => {
|
|
1964
|
+
if (key === "") {
|
|
1965
|
+
setMissing(NO_MISSING);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
const tracked = key.split("|"), releases = tracked.map(id => actualTypes.track(id)), recompute = () => {
|
|
1969
|
+
const next = new Set(tracked.filter(id => actualTypes.read(id) === null));
|
|
1970
|
+
setMissing(current => sameIdSet(current, next) ? current : next);
|
|
1971
|
+
};
|
|
1972
|
+
recompute();
|
|
1973
|
+
const unsubscribe = actualTypes.subscribe(recompute);
|
|
1974
|
+
return () => {
|
|
1975
|
+
unsubscribe();
|
|
1976
|
+
for (const release of releases) release();
|
|
1977
|
+
};
|
|
1978
|
+
}, [ actualTypes, key ]), useMemo(() => missing.size === 0 ? NO_ORPHANS : orphanVerdict({
|
|
1979
|
+
groups: groups,
|
|
1980
|
+
contentResource: contentResource,
|
|
1981
|
+
missing: missing
|
|
1982
|
+
}), [ groups, contentResource, missing ]);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1520
1985
|
const INSTANCE_CAP = 200, NEWEST_INSTANCES = {
|
|
1521
1986
|
includeCompleted: !0,
|
|
1522
1987
|
limit: INSTANCE_CAP
|
|
@@ -1539,18 +2004,21 @@ function useToolInstances() {
|
|
|
1539
2004
|
}, [ instances, loading, unreadable ]);
|
|
1540
2005
|
}
|
|
1541
2006
|
|
|
1542
|
-
function TasksTab({filterSlot: filterSlot, instances: instances, loading: loading, identity: identity,
|
|
1543
|
-
const
|
|
2007
|
+
function TasksTab({filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, segment: segment, truncated: truncated, unreadable: unreadable}) {
|
|
2008
|
+
const mineOnly = segment === "for-me", {binding: binding} = useWorkflowContext(), bands = useBandOpenState({
|
|
2009
|
+
segment: segment,
|
|
2010
|
+
scope: binding.engineResource.id
|
|
2011
|
+
}), [filters, setFilters] = useState({}), groups = useMemo(() => deriveTaskGroups({
|
|
1544
2012
|
instances: instances,
|
|
1545
2013
|
identity: identity
|
|
1546
|
-
}), [ instances, identity ]),
|
|
2014
|
+
}), [ instances, identity ]), orphans = useOrphanedWorkflows(groups), listed = useMemo(() => hideOrphanedGroups(groups, orphans.groupKeys), [ groups, orphans.groupKeys ]), options = useMemo(() => taskFilterOptions(listed), [ listed ]), visible = useMemo(() => {
|
|
1547
2015
|
const filtered = applyTaskFilters({
|
|
1548
|
-
groups:
|
|
2016
|
+
groups: listed,
|
|
1549
2017
|
filters: filters,
|
|
1550
2018
|
now: /* @__PURE__ */ new Date
|
|
1551
2019
|
});
|
|
1552
2020
|
return mineOnly ? onlyMyTasks(filtered) : filtered;
|
|
1553
|
-
}, [
|
|
2021
|
+
}, [ listed, filters, mineOnly ]);
|
|
1554
2022
|
return loading ?
|
|
1555
2023
|
/* @__PURE__ */ jsx(Box, {
|
|
1556
2024
|
paddingLeft: 2,
|
|
@@ -1571,8 +2039,13 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
|
|
|
1571
2039
|
/* @__PURE__ */ jsx(UnreadableDocsNote, {
|
|
1572
2040
|
unreadable: unreadable
|
|
1573
2041
|
}),
|
|
2042
|
+
/* @__PURE__ */ jsx(OrphanedWorkflowsNote, {
|
|
2043
|
+
orphans: orphans
|
|
2044
|
+
}),
|
|
1574
2045
|
/* @__PURE__ */ jsx(TaskGroupList, {
|
|
2046
|
+
bands: bands,
|
|
1575
2047
|
groups: visible,
|
|
2048
|
+
instanceDefaultOpen: instanceBandDefaultOpen(segment),
|
|
1576
2049
|
onOpenTask: onOpenTask,
|
|
1577
2050
|
onOpenWorkflow: onOpenWorkflow,
|
|
1578
2051
|
showTerminalActions: mineOnly
|
|
@@ -1613,7 +2086,12 @@ function carryingSearchParams(state, searchParams) {
|
|
|
1613
2086
|
}
|
|
1614
2087
|
|
|
1615
2088
|
function WorkflowsToolRoot() {
|
|
1616
|
-
const router = useRouter(), {seedInstance: seedInstance} = useWorkflowContext(),
|
|
2089
|
+
const router = useRouter(), {seedInstance: seedInstance} = useWorkflowContext(), {tab: tab} = useToolTab();
|
|
2090
|
+
useLogEventOnMount(WorkflowToolOpened, {
|
|
2091
|
+
tab: tab,
|
|
2092
|
+
via: "open"
|
|
2093
|
+
});
|
|
2094
|
+
const instanceId = typeof router.state.instanceId == "string" ? router.state.instanceId : void 0;
|
|
1617
2095
|
return instanceId ? /* @__PURE__ */ jsx(WorkflowInstanceDetail, {
|
|
1618
2096
|
instanceId: instanceId,
|
|
1619
2097
|
onBack: () => router.navigate(carryingSearchParams({}, router.state._searchParams))
|
|
@@ -1627,7 +2105,7 @@ function WorkflowsToolRoot() {
|
|
|
1627
2105
|
}
|
|
1628
2106
|
|
|
1629
2107
|
function WorkflowsHome({onOpenInstance: onOpenInstance}) {
|
|
1630
|
-
const {tab: tab, setTab: setTab} = useToolTab(), instances = useToolInstances(), identity = useAssignmentIdentity(), [openTask, setOpenTask] = useState(null), openInstance = target => {
|
|
2108
|
+
const {tab: tab, setTab: setTab} = useToolTab(), telemetry = useWorkflowTelemetry(), instances = useToolInstances(), identity = useAssignmentIdentity(), [openTask, setOpenTask] = useState(null), openInstance = target => {
|
|
1631
2109
|
if (typeof target != "string") return onOpenInstance(target);
|
|
1632
2110
|
const held = [ ...instances.inFlight, ...instances.settled ].find(i => i._id === target);
|
|
1633
2111
|
onOpenInstance(held ?? target);
|
|
@@ -1671,7 +2149,12 @@ function WorkflowsHome({onOpenInstance: onOpenInstance}) {
|
|
|
1671
2149
|
/* @__PURE__ */ jsx(TabSwitch, {
|
|
1672
2150
|
ariaControls: "workflows-tool-panel",
|
|
1673
2151
|
idPrefix: "workflows-tool-tab",
|
|
1674
|
-
onSelect:
|
|
2152
|
+
onSelect: next => {
|
|
2153
|
+
next !== tab && telemetry.log(WorkflowToolOpened, {
|
|
2154
|
+
tab: next,
|
|
2155
|
+
via: "tab-switch"
|
|
2156
|
+
}), setTab(next);
|
|
2157
|
+
},
|
|
1675
2158
|
options: TOOL_TABS,
|
|
1676
2159
|
selected: tab
|
|
1677
2160
|
}),
|
|
@@ -1702,9 +2185,9 @@ function WorkflowsHome({onOpenInstance: onOpenInstance}) {
|
|
|
1702
2185
|
identity: identity,
|
|
1703
2186
|
instances: [ ...instances.inFlight, ...instances.settled ],
|
|
1704
2187
|
loading: instances.loading,
|
|
1705
|
-
mineOnly: tab === "for-me",
|
|
1706
2188
|
onOpenTask: setOpenTask,
|
|
1707
2189
|
onOpenWorkflow: openInstance,
|
|
2190
|
+
segment: tab,
|
|
1708
2191
|
truncated: instances.truncated,
|
|
1709
2192
|
unreadable: instances.unreadable
|
|
1710
2193
|
})
|