@sanity/workflow-studio-plugin 0.5.0 → 0.21.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.
@@ -1,22 +1,6 @@
1
1
  "use strict";
2
2
 
3
- var jsxRuntime = require("react/jsx-runtime"), ui = require("@sanity/ui"), react = require("react"), router = require("sanity/router"), index = require("./index.cjs"), ArrowLeft = require("@sanity/icons/ArrowLeft"), workflowDiagram = require("@sanity/workflow-diagram"), workflowEngine = require("@sanity/workflow-engine"), Add = require("@sanity/icons/Add"), Search = require("@sanity/icons/Search"), Checkmark = require("@sanity/icons/Checkmark"), Close = require("@sanity/icons/Close"), Filter = require("@sanity/icons/Filter"), workflowComponents = require("@sanity/workflow-components"), endOfDay = require("date-fns/endOfDay"), reactDom = require("react-dom"), sanity = require("sanity"), Calendar = require("@sanity/icons/Calendar"), workflowStudio = require("@sanity/workflow-studio");
4
-
5
- function SpinnerSlot({busy: busy}) {
6
- /* @__PURE__ */
7
- return jsxRuntime.jsx(ui.Flex, {
8
- align: "center",
9
- flex: "none",
10
- justify: "center",
11
- style: {
12
- width: 19,
13
- height: 19
14
- },
15
- children: busy ? /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, {
16
- muted: !0
17
- }) : null
18
- });
19
- }
3
+ var jsxRuntime = require("react/jsx-runtime"), ui = require("@sanity/ui"), workflowReact = require("@sanity/workflow-react"), react = require("react"), router = require("sanity/router"), index = require("./index.cjs"), ArrowLeft = require("@sanity/icons/ArrowLeft"), workflowDiagram = require("@sanity/workflow-diagram"), workflowEngine = require("@sanity/workflow-engine"), Add = require("@sanity/icons/Add"), Search = require("@sanity/icons/Search"), sanity = require("sanity"), Checkmark = require("@sanity/icons/Checkmark"), Close = require("@sanity/icons/Close"), Filter = require("@sanity/icons/Filter"), workflowComponents = require("@sanity/workflow-components"), endOfDay = require("date-fns/endOfDay"), reactDom = require("react-dom"), Calendar = require("@sanity/icons/Calendar"), workflowStudio = require("@sanity/workflow-studio");
20
4
 
21
5
  function documentRefsOf(instance) {
22
6
  const seen = /* @__PURE__ */ new Map;
@@ -34,14 +18,19 @@ function InstanceTaskLists({entry: entry, onOpenActivity: onOpenActivity}) {
34
18
  onOpenActivity: onOpenActivity
35
19
  }),
36
20
  /* @__PURE__ */ jsxRuntime.jsx(index.TodoItemsList, {
37
- entry: entry
21
+ entry: entry,
22
+ surface: "tool-instance-detail"
38
23
  }) ]
39
24
  })
40
25
  });
41
26
  }
42
27
 
43
28
  function WorkflowInstanceDetail({instanceId: instanceId, onBack: onBack}) {
44
- const entry = index.useWorkflowInstanceEntry(instanceId), [graceOver, setGraceOver] = react.useState(!1);
29
+ const entry = index.useWorkflowInstanceEntry(instanceId);
30
+ index.useLogEventOnMount(index.WorkflowInstanceDetailViewed, {
31
+ instanceId: instanceId
32
+ });
33
+ const [graceOver, setGraceOver] = react.useState(!1);
45
34
  return react.useEffect(() => {
46
35
  const timer = setTimeout(() => setGraceOver(!0), 2500);
47
36
  return () => clearTimeout(timer);
@@ -135,7 +124,8 @@ function InstanceDetailPanel({entry: entry, onBack: onBack}) {
135
124
  breadcrumb: index.instanceBreadcrumb(instance, definition),
136
125
  definition: definition,
137
126
  entry: entry,
138
- onClose: () => setOpenActivity(null)
127
+ onClose: () => setOpenActivity(null),
128
+ source: "tool-instance-detail"
139
129
  }) : null ]
140
130
  });
141
131
  }
@@ -181,7 +171,7 @@ function DetailHeader({entry: entry, definition: definition, onBack: onBack}) {
181
171
  }) : /* @__PURE__ */ jsxRuntime.jsx(index.AbortedBadge, {
182
172
  instance: instance
183
173
  }),
184
- /* @__PURE__ */ jsxRuntime.jsx(SpinnerSlot, {
174
+ /* @__PURE__ */ jsxRuntime.jsx(index.SpinnerSlot, {
185
175
  busy: !ready || index.isEvaluationStale(entry)
186
176
  }) ]
187
177
  })
@@ -276,7 +266,8 @@ function NewWorkflowButton() {
276
266
  onPick: definition => {
277
267
  setOpen(!1), openStartDialog({
278
268
  definition: definition.name,
279
- label: definition.title
269
+ label: definition.title,
270
+ source: "tool"
280
271
  });
281
272
  }
282
273
  }) : null,
@@ -383,7 +374,8 @@ function ToolActivityDialog({target: target, onClose: onClose}) {
383
374
  definition: definition,
384
375
  document: documentRefsOf(entry.instance)[0],
385
376
  entry: entry,
386
- onClose: onClose
377
+ onClose: onClose,
378
+ source: "tool-task-list"
387
379
  }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Dialog, {
388
380
  header: "Loading activity…",
389
381
  id: "workflow-activity-detail",
@@ -398,13 +390,154 @@ function ToolActivityDialog({target: target, onClose: onClose}) {
398
390
  });
399
391
  }
400
392
 
393
+ const GROUP_DEFAULT_OPEN = !0;
394
+
395
+ function instanceBandDefaultOpen(segment) {
396
+ return segment === "for-me";
397
+ }
398
+
399
+ const UNGROUPED_BAND_KEY = "section:no-document", PAYLOAD_VERSION = 1, MAX_ENTRIES = 200;
400
+
401
+ function bandStateStorageKey(scope) {
402
+ return `sanity.workflows.tool.bands:${scope}`;
403
+ }
404
+
405
+ function isRecord(value) {
406
+ return typeof value == "object" && value !== null && !Array.isArray(value);
407
+ }
408
+
409
+ function bandEntryOf(value) {
410
+ if (!isRecord(value)) return;
411
+ const {open: open, touched: touched} = value;
412
+ if (!(typeof open != "boolean" || typeof touched != "number") && Number.isFinite(touched)) return {
413
+ open: open,
414
+ touched: touched
415
+ };
416
+ }
417
+
418
+ function parsePayload(raw) {
419
+ try {
420
+ const parsed = JSON.parse(raw);
421
+ if (!isRecord(parsed) || parsed.version !== PAYLOAD_VERSION || !isRecord(parsed.bands)) return;
422
+ const entries = [];
423
+ for (const [key, value] of Object.entries(parsed.bands)) {
424
+ const entry = bandEntryOf(value);
425
+ if (entry === void 0) return;
426
+ entries.push([ key, entry ]);
427
+ }
428
+ return Object.fromEntries(entries);
429
+ } catch {
430
+ return;
431
+ }
432
+ }
433
+
434
+ function readStored(storage, storageKey) {
435
+ if (!storage) return {
436
+ entries: void 0,
437
+ corrupt: !1
438
+ };
439
+ let raw;
440
+ try {
441
+ raw = storage.getItem(storageKey);
442
+ } catch {
443
+ return {
444
+ entries: void 0,
445
+ corrupt: !1
446
+ };
447
+ }
448
+ if (raw === null) return {
449
+ entries: {},
450
+ corrupt: !1
451
+ };
452
+ const entries = parsePayload(raw);
453
+ return {
454
+ entries: entries,
455
+ corrupt: entries === void 0
456
+ };
457
+ }
458
+
459
+ function writeStored(args) {
460
+ if (!args.storage) return !1;
461
+ try {
462
+ return args.storage.setItem(args.storageKey, JSON.stringify({
463
+ version: PAYLOAD_VERSION,
464
+ bands: args.entries
465
+ })), !0;
466
+ } catch {
467
+ return !1;
468
+ }
469
+ }
470
+
471
+ function baseEntries(args) {
472
+ return args.stored === void 0 ? args.held : args.unpersisted ? mergeByTouched(args.stored, args.held) : args.stored;
473
+ }
474
+
475
+ function mergeByTouched(stored, held) {
476
+ const merged = {
477
+ ...stored
478
+ };
479
+ for (const [key, entry] of Object.entries(held)) {
480
+ const persisted = merged[key];
481
+ (persisted === void 0 || entry.touched >= persisted.touched) && (merged[key] = entry);
482
+ }
483
+ return merged;
484
+ }
485
+
486
+ function prune(entries) {
487
+ const held = Object.entries(entries);
488
+ return held.length <= MAX_ENTRIES ? entries : Object.fromEntries(held.sort(([, a], [, b]) => b.touched - a.touched).slice(0, MAX_ENTRIES));
489
+ }
490
+
491
+ function togglesOf(entries) {
492
+ return new Map(Object.entries(entries).map(([key, entry]) => [ key, entry.open ]));
493
+ }
494
+
495
+ function defaultStorage() {
496
+ try {
497
+ return globalThis.localStorage;
498
+ } catch {
499
+ return;
500
+ }
501
+ }
502
+
503
+ function createBandStateStore(args) {
504
+ const storage = args.storage ?? defaultStorage(), initial = readStored(storage, args.storageKey);
505
+ initial.corrupt && writeStored({
506
+ storage: storage,
507
+ storageKey: args.storageKey,
508
+ entries: {}
509
+ });
510
+ let held = initial.entries ?? {}, unpersisted = !1;
511
+ return {
512
+ read: () => togglesOf(held),
513
+ setOpen(key, open) {
514
+ const stored = readStored(storage, args.storageKey).entries, base = baseEntries({
515
+ stored: stored,
516
+ held: held,
517
+ unpersisted: unpersisted
518
+ });
519
+ return held = prune({
520
+ ...base,
521
+ [key]: {
522
+ open: open,
523
+ touched: Date.now()
524
+ }
525
+ }), unpersisted = !writeStored({
526
+ storage: storage,
527
+ storageKey: args.storageKey,
528
+ entries: held
529
+ }), togglesOf(held);
530
+ }
531
+ };
532
+ }
533
+
401
534
  function datesOf(state) {
402
535
  return state.filter(entry => entry._type === "date" || entry._type === "datetime").map(entry => entry.value).filter(value => typeof value == "string" && value !== "");
403
536
  }
404
537
 
405
538
  function spawnOrigins(instances) {
406
539
  const origins = /* @__PURE__ */ new Map;
407
- for (const parent of instances) for (const row of parent.subworkflows ?? []) origins.set(workflowEngine.extractDocumentId(row.ref.id), {
540
+ for (const parent of instances) for (const row of parent.subworkflows ?? []) origins.set(workflowEngine.toBareId(row.ref.id), {
408
541
  parent: parent,
409
542
  activityName: row.activity,
410
543
  cohortStage: row.cohortStage
@@ -414,7 +547,7 @@ function spawnOrigins(instances) {
414
547
 
415
548
  function rootOf(instance, byId) {
416
549
  const rootRef = instance.ancestors[0];
417
- return rootRef ? byId.get(workflowEngine.extractDocumentId(rootRef.id)) ?? instance : instance;
550
+ return rootRef ? byId.get(workflowEngine.toBareId(rootRef.id)) ?? instance : instance;
418
551
  }
419
552
 
420
553
  function taskRowsOf(args) {
@@ -555,6 +688,223 @@ function taskRowRender(row, entry) {
555
688
  };
556
689
  }
557
690
 
691
+ const NO_ORPHANS = {
692
+ groupKeys: /* @__PURE__ */ new Set,
693
+ workflows: []
694
+ };
695
+
696
+ function localBareIds(group, contentResource) {
697
+ const ids = group.documents.map(doc => index.gdrLocality(doc.id, contentResource));
698
+ if (!ids.some(locality => !locality.parsed || !locality.local)) return ids.flatMap(locality => locality.parsed ? [ locality.parsed.documentId ] : []);
699
+ }
700
+
701
+ function orphanProbeIds(groups, contentResource) {
702
+ const ids = /* @__PURE__ */ new Set;
703
+ for (const group of groups.withDocuments) for (const id of localBareIds(group, contentResource) ?? []) ids.add(id);
704
+ return [ ...ids ].sort();
705
+ }
706
+
707
+ function orphanVerdict(args) {
708
+ const groupKeys = /* @__PURE__ */ new Set, workflows = /* @__PURE__ */ new Map;
709
+ for (const group of args.groups.withDocuments) {
710
+ const ids = localBareIds(group, args.contentResource);
711
+ if (!(ids === void 0 || !ids.every(id => args.missing.has(id)))) {
712
+ groupKeys.add(group.key);
713
+ for (const band of group.instances) workflows.set(band.instanceId, {
714
+ instanceId: band.instanceId,
715
+ title: band.title,
716
+ documentIds: ids
717
+ });
718
+ }
719
+ }
720
+ return groupKeys.size === 0 ? NO_ORPHANS : {
721
+ groupKeys: groupKeys,
722
+ workflows: [ ...workflows.values() ]
723
+ };
724
+ }
725
+
726
+ function hideOrphanedGroups(groups, orphaned) {
727
+ return orphaned.size === 0 ? groups : cutTaskGroups(groups, {
728
+ keepRow: () => !0,
729
+ keepGroup: group => !orphaned.has(group.key)
730
+ });
731
+ }
732
+
733
+ const DRAFT_PREFIX = "drafts.";
734
+
735
+ async function missingDocIds(client, ids) {
736
+ const present = await client.fetch("*[_id in $ids || _id in $drafts]._id", {
737
+ ids: ids,
738
+ drafts: ids.map(id => `${DRAFT_PREFIX}${id}`)
739
+ }, {
740
+ perspective: "raw"
741
+ }), 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", {
742
+ version: `versions.*.${id}`
743
+ }, {
744
+ perspective: "raw"
745
+ })));
746
+ return new Set(candidates.filter((_, index2) => versions[index2] === null));
747
+ }
748
+
749
+ async function settleOrphans(args) {
750
+ 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));
751
+ let settled = 0;
752
+ const failures = [];
753
+ for (const workflow of args.workflows.filter(stillOrphaned)) try {
754
+ await args.abort({
755
+ instanceId: workflow.instanceId,
756
+ reason: "Subject document deleted"
757
+ }), settled += 1;
758
+ } catch (err) {
759
+ failures.push({
760
+ workflow: workflow,
761
+ message: index.describeError(err)
762
+ });
763
+ }
764
+ return {
765
+ settled: settled,
766
+ skipped: args.workflows.filter(workflow => !stillOrphaned(workflow)),
767
+ failures: failures
768
+ };
769
+ }
770
+
771
+ function workflowTally(workflows) {
772
+ const counts = /* @__PURE__ */ new Map;
773
+ for (const workflow of workflows) counts.set(workflow.title, (counts.get(workflow.title) ?? 0) + 1);
774
+ return [ ...counts ].map(([title, count]) => count === 1 ? title : `${title} ×${count}`);
775
+ }
776
+
777
+ function settleToast(outcome) {
778
+ const total = outcome.settled + outcome.skipped.length + outcome.failures.length;
779
+ if (outcome.skipped.length === 0 && outcome.failures.length === 0) return {
780
+ status: "success",
781
+ title: outcome.settled === 1 ? "Cleaned up 1 workflow" : `Cleaned up ${outcome.settled} workflows`
782
+ };
783
+ const lines = [ ...outcome.skipped.map(workflow => `${workflow.title}: its document exists again — left running`), ...outcome.failures.map(failure => `${failure.workflow.title}: ${failure.message}`) ];
784
+ return {
785
+ status: "warning",
786
+ title: `Cleaned up ${outcome.settled} of ${total} workflows`,
787
+ description: lines.join(`\n`)
788
+ };
789
+ }
790
+
791
+ function recheckFailedToast(err) {
792
+ return {
793
+ status: "error",
794
+ title: "Nothing was cleaned up — the documents could not be re-checked",
795
+ description: index.describeError(err)
796
+ };
797
+ }
798
+
799
+ function OrphanedWorkflowsNote({orphans: orphans}) {
800
+ const [confirming, setConfirming] = react.useState(!1), count = orphans.workflows.length;
801
+ if (count === 0) return null;
802
+ 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.`;
803
+ /* @__PURE__ */
804
+ return jsxRuntime.jsxs(jsxRuntime.Fragment, {
805
+ children: [
806
+ /* @__PURE__ */ jsxRuntime.jsx(index.CautionNote, {
807
+ action: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, {
808
+ fontSize: 1,
809
+ mode: "ghost",
810
+ onClick: () => setConfirming(!0),
811
+ padding: 2,
812
+ text: "Clean up…"
813
+ }),
814
+ label: label
815
+ }), confirming ? /* @__PURE__ */ jsxRuntime.jsx(SettleOrphansDialog, {
816
+ onClose: () => setConfirming(!1),
817
+ workflows: orphans.workflows
818
+ }) : null ]
819
+ });
820
+ }
821
+
822
+ function SettleOrphansDialog({workflows: workflows, onClose: onClose}) {
823
+ const {engine: engine} = index.useWorkflowContext(), client = sanity.useClient({
824
+ apiVersion: index.WORKFLOW_API_VERSION
825
+ }), toast = index.useClosableToast(), [pending, setPending] = react.useState(!1), confirm = async () => {
826
+ setPending(!0);
827
+ try {
828
+ const outcome = await settleOrphans({
829
+ workflows: workflows,
830
+ probeMissing: ids => missingDocIds(client, ids),
831
+ abort: args => engine.abortInstance(args)
832
+ });
833
+ toast.push(settleToast(outcome));
834
+ } catch (err) {
835
+ toast.push(recheckFailedToast(err));
836
+ }
837
+ onClose();
838
+ };
839
+ /* @__PURE__ */
840
+ return jsxRuntime.jsx(ui.Dialog, {
841
+ footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
842
+ padding: 3,
843
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
844
+ gap: 2,
845
+ justify: "flex-end",
846
+ children: [
847
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, {
848
+ disabled: pending,
849
+ fontSize: 1,
850
+ mode: "bleed",
851
+ onClick: onClose,
852
+ padding: 2,
853
+ text: "Cancel"
854
+ }),
855
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, {
856
+ fontSize: 1,
857
+ loading: pending,
858
+ onClick: () => {
859
+ confirm();
860
+ },
861
+ padding: 2,
862
+ text: workflows.length === 1 ? "Clean up 1 workflow" : `Clean up ${workflows.length} workflows`
863
+ }) ]
864
+ })
865
+ }),
866
+ header: "Clean up workflows",
867
+ id: "settle-orphaned-workflows",
868
+ onClose: onClose,
869
+ width: 0,
870
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
871
+ padding: 4,
872
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
873
+ gap: 4,
874
+ children: [
875
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
876
+ muted: !0,
877
+ size: 1,
878
+ children: "The following workflows exist for documents which have since been deleted:"
879
+ }),
880
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, {
881
+ as: "ul",
882
+ gap: 2,
883
+ style: {
884
+ listStyle: "disc",
885
+ margin: 0,
886
+ paddingLeft: "1.25em"
887
+ },
888
+ children: workflowTally(workflows).map(line => /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
889
+ as: "li",
890
+ size: 1,
891
+ style: {
892
+ display: "list-item"
893
+ },
894
+ weight: "medium",
895
+ children: line
896
+ }, line))
897
+ }),
898
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
899
+ muted: !0,
900
+ size: 1,
901
+ children: "Cleaning up these workflows will close them."
902
+ }) ]
903
+ })
904
+ })
905
+ });
906
+ }
907
+
558
908
  function taskFilterOptions(groups) {
559
909
  const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
560
910
  for (const group of groups.withDocuments) {
@@ -571,8 +921,33 @@ function taskFilterOptions(groups) {
571
921
  };
572
922
  }
573
923
 
924
+ function activeEntries(filters) {
925
+ return Object.entries(filters).filter(([, value]) => value !== void 0 && (!Array.isArray(value) || value.length > 0));
926
+ }
927
+
574
928
  function hasActiveFilters(filters) {
575
- return Object.values(filters).some(value => value !== void 0 && (!Array.isArray(value) || value.length > 0));
929
+ return activeEntries(filters).length > 0;
930
+ }
931
+
932
+ function sameFilters(a, b) {
933
+ 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);
934
+ }
935
+
936
+ function sameValueSet(a, b) {
937
+ const av = a ?? [], bv = b ?? [];
938
+ return av.length === bv.length && av.every(value => bv.includes(value));
939
+ }
940
+
941
+ function sameDate(a, b) {
942
+ return typeof a == "object" && typeof b == "object" ? a.before === b.before : a === b;
943
+ }
944
+
945
+ function taskFilterTelemetry(filters) {
946
+ const entries = activeEntries(filters);
947
+ return {
948
+ filterKinds: entries.map(([key]) => key).sort(),
949
+ filterCount: entries.reduce((n, [, value]) => n + (Array.isArray(value) ? value.length : 1), 0)
950
+ };
576
951
  }
577
952
 
578
953
  function toggledValue(args) {
@@ -727,7 +1102,12 @@ function useDocPreviewTitles(documents) {
727
1102
  const HITS_CAP = 6;
728
1103
 
729
1104
  function TaskFilterMenu({filters: filters, options: options, onChange: onChange}) {
730
- const [open, setOpen] = react.useState(!1), active = hasActiveFilters(filters);
1105
+ const [open, setOpen] = react.useState(!1), telemetry = workflowReact.useWorkflowTelemetry(), openSnapshot = react.useRef(void 0), openMenu = () => {
1106
+ openSnapshot.current = filters, setOpen(!0);
1107
+ }, close = () => {
1108
+ setOpen(!1), openSnapshot.current !== void 0 && !sameFilters(openSnapshot.current, filters) && telemetry.log(index.WorkflowTaskFiltersApplied, taskFilterTelemetry(filters)),
1109
+ openSnapshot.current = void 0;
1110
+ }, active = hasActiveFilters(filters);
731
1111
  /* @__PURE__ */
732
1112
  return jsxRuntime.jsx(index.DismissablePopover, {
733
1113
  content: open ? /* @__PURE__ */ jsxRuntime.jsx(FilterPanel, {
@@ -735,7 +1115,7 @@ function TaskFilterMenu({filters: filters, options: options, onChange: onChange}
735
1115
  onChange: onChange,
736
1116
  options: options
737
1117
  }) : null,
738
- onDismiss: () => setOpen(!1),
1118
+ onDismiss: close,
739
1119
  open: open,
740
1120
  overflow: "visible",
741
1121
  children: /* @__PURE__ */ jsxRuntime.jsx(index.HoverHint, {
@@ -746,7 +1126,7 @@ function TaskFilterMenu({filters: filters, options: options, onChange: onChange}
746
1126
  fontSize: 1,
747
1127
  icon: Filter.FilterIcon,
748
1128
  mode: "bleed",
749
- onClick: () => setOpen(v => !v),
1129
+ onClick: () => open ? close() : openMenu(),
750
1130
  padding: 2,
751
1131
  selected: active || open
752
1132
  })
@@ -1088,7 +1468,7 @@ function DocumentField({documents: documents, activeIds: activeIds, onToggle: on
1088
1468
  if (documents.length === 0) return null;
1089
1469
  const rows = documents.map(doc => ({
1090
1470
  key: doc.id,
1091
- text: `${titles.get(doc.id) ?? ""} ${doc.type} ${workflowEngine.extractDocumentId(doc.id)}`,
1471
+ text: `${titles.get(doc.id) ?? ""} ${doc.type} ${workflowEngine.toBareId(doc.id)}`,
1092
1472
  node: /* @__PURE__ */ jsxRuntime.jsx(index.DocRefFace, {
1093
1473
  gdr: doc
1094
1474
  })
@@ -1173,7 +1553,7 @@ function useDocumentChipLabels(documentIds, documents) {
1173
1553
  const schema = sanity.useSchema(), activeDocs = documents.filter(doc => documentIds.includes(doc.id)), {titles: titles, resolved: resolved} = useDocPreviewTitles(activeDocs), labels = /* @__PURE__ */ new Map;
1174
1554
  for (const doc of activeDocs) {
1175
1555
  if (!resolved.has(doc.id)) continue;
1176
- const label = titles.get(doc.id) ?? schemaTypeTitle(schema, doc.type) ?? workflowEngine.extractDocumentId(doc.id);
1556
+ const label = titles.get(doc.id) ?? schemaTypeTitle(schema, doc.type) ?? workflowEngine.toBareId(doc.id);
1177
1557
  labels.set(doc.id, label);
1178
1558
  }
1179
1559
  return labels;
@@ -1242,7 +1622,7 @@ function ActivityDateControl({instanceId: instanceId, state: state, dates: dates
1242
1622
  });
1243
1623
  const kind = index.dateControlKind(state), commit = (mode, value) => {
1244
1624
  state.kind === "editable" && save(() => editFieldFor(instanceId, {
1245
- target: index.editTargetOf(state.field),
1625
+ target: workflowReact.editFieldTarget(state.field),
1246
1626
  ...mode === "set" ? {
1247
1627
  mode: mode,
1248
1628
  value: value
@@ -1300,6 +1680,28 @@ function ActivityDateControl({instanceId: instanceId, state: state, dates: dates
1300
1680
  });
1301
1681
  }
1302
1682
 
1683
+ const PREWARM_MARGIN = "600px 0px";
1684
+
1685
+ function useNearViewport() {
1686
+ const [element, setElement] = react.useState(null), [near, setNear] = react.useState(!1);
1687
+ return react.useEffect(() => {
1688
+ if (element === null) return;
1689
+ if (typeof IntersectionObserver > "u") {
1690
+ setNear(!0);
1691
+ return;
1692
+ }
1693
+ const observer = new IntersectionObserver(entries => {
1694
+ setNear(entries.at(-1)?.isIntersecting ?? !1);
1695
+ }, {
1696
+ rootMargin: PREWARM_MARGIN
1697
+ });
1698
+ return observer.observe(element), () => observer.disconnect();
1699
+ }, [ element ]), {
1700
+ ref: setElement,
1701
+ near: near
1702
+ };
1703
+ }
1704
+
1303
1705
  function RefChips({refs: refs, max: max = 2}) {
1304
1706
  if (refs.length === 0) return null;
1305
1707
  const shown = refs.slice(0, max), overflow = refs.length - shown.length;
@@ -1321,15 +1723,17 @@ function RefChips({refs: refs, max: max = 2}) {
1321
1723
 
1322
1724
  const EMPTY_NOTE = "No tasks match — adjust the filters, or start a workflow", UNGROUPED_TITLE = "Workflows without Studio documents";
1323
1725
 
1324
- function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1726
+ function TaskGroupList({bands: bands, groups: groups, instanceDefaultOpen: instanceDefaultOpen, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1325
1727
  return groups.withDocuments.length === 0 && groups.withoutDocuments.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
1326
1728
  text: EMPTY_NOTE
1327
1729
  }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
1328
1730
  gap: 2,
1329
1731
  children: [ groups.withDocuments.map(group => /* @__PURE__ */ jsxRuntime.jsx(index.CollapsibleBand, {
1330
1732
  background: !0,
1733
+ onToggle: () => bands.toggle(group.key, GROUP_DEFAULT_OPEN),
1734
+ open: bands.isOpen(group.key, GROUP_DEFAULT_OPEN),
1331
1735
  sticky: !0,
1332
- title: workflowEngine.extractDocumentId(group.document.id),
1736
+ title: workflowEngine.toBareId(group.document.id),
1333
1737
  header: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, {
1334
1738
  align: "center",
1335
1739
  flex: "none",
@@ -1342,6 +1746,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
1342
1746
  })
1343
1747
  }),
1344
1748
  children: /* @__PURE__ */ jsxRuntime.jsx(InstanceBands, {
1749
+ bands: bands,
1750
+ instanceDefaultOpen: instanceDefaultOpen,
1345
1751
  instances: group.instances,
1346
1752
  onOpenTask: onOpenTask,
1347
1753
  onOpenWorkflow: onOpenWorkflow,
@@ -1349,6 +1755,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
1349
1755
  })
1350
1756
  }, group.key)), groups.withoutDocuments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(index.CollapsibleBand, {
1351
1757
  background: !0,
1758
+ onToggle: () => bands.toggle(UNGROUPED_BAND_KEY, GROUP_DEFAULT_OPEN),
1759
+ open: bands.isOpen(UNGROUPED_BAND_KEY, GROUP_DEFAULT_OPEN),
1352
1760
  sticky: !0,
1353
1761
  title: UNGROUPED_TITLE,
1354
1762
  header: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
@@ -1357,6 +1765,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
1357
1765
  children: UNGROUPED_TITLE
1358
1766
  }),
1359
1767
  children: /* @__PURE__ */ jsxRuntime.jsx(InstanceBands, {
1768
+ bands: bands,
1769
+ instanceDefaultOpen: instanceDefaultOpen,
1360
1770
  instances: groups.withoutDocuments,
1361
1771
  onOpenTask: onOpenTask,
1362
1772
  onOpenWorkflow: onOpenWorkflow,
@@ -1374,12 +1784,14 @@ function targetOf(row) {
1374
1784
  };
1375
1785
  }
1376
1786
 
1377
- function InstanceBands({instances: instances, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1787
+ function InstanceBands({bands: bands, instanceDefaultOpen: instanceDefaultOpen, instances: instances, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1378
1788
  /* @__PURE__ */
1379
1789
  return jsxRuntime.jsx(ui.Stack, {
1380
1790
  gap: 1,
1381
1791
  children: instances.map(band => /* @__PURE__ */ jsxRuntime.jsx(InstanceBand, {
1382
1792
  band: band,
1793
+ bands: bands,
1794
+ instanceDefaultOpen: instanceDefaultOpen,
1383
1795
  onOpenTask: onOpenTask,
1384
1796
  onOpenWorkflow: onOpenWorkflow,
1385
1797
  showTerminalActions: showTerminalActions
@@ -1387,10 +1799,12 @@ function InstanceBands({instances: instances, onOpenTask: onOpenTask, onOpenWork
1387
1799
  });
1388
1800
  }
1389
1801
 
1390
- function InstanceBand({band: band, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1802
+ function InstanceBand({band: band, bands: bands, instanceDefaultOpen: instanceDefaultOpen, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1391
1803
  const {resolvePathFromState: resolvePathFromState} = router.useRouter();
1392
1804
  /* @__PURE__ */
1393
1805
  return jsxRuntime.jsx(index.CollapsibleBand, {
1806
+ onToggle: () => bands.toggle(band.instanceId, instanceDefaultOpen),
1807
+ open: bands.isOpen(band.instanceId, instanceDefaultOpen),
1394
1808
  title: band.title,
1395
1809
  header: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, {
1396
1810
  children: [
@@ -1452,26 +1866,86 @@ function TaskRows({rows: rows, onOpenTask: onOpenTask, showTerminalActions: show
1452
1866
  }
1453
1867
 
1454
1868
  function TaskActivityRow({row: row, onOpen: onOpen, showTerminalActions: showTerminalActions}) {
1455
- const entry = index.useWorkflowInstanceEntry(row.instanceId), render = taskRowRender(row, entry);
1869
+ const {ref: ref, near: near} = useNearViewport(), entry = index.useWorkflowInstanceEntry(row.instanceId, {
1870
+ evaluate: near
1871
+ }), render = taskRowRender(row, entry);
1456
1872
  /* @__PURE__ */
1457
- return jsxRuntime.jsx(index.StaleLock, {
1458
- stale: render.stale,
1459
- children: /* @__PURE__ */ jsxRuntime.jsx(index.ActivityRow, {
1460
- assignState: render.assignState,
1461
- assigneeIds: render.assigneeIds,
1462
- dateControl: /* @__PURE__ */ jsxRuntime.jsx(ActivityDateControl, {
1463
- dates: render.dates,
1873
+ return jsxRuntime.jsx(ui.Box, {
1874
+ ref: ref,
1875
+ children: /* @__PURE__ */ jsxRuntime.jsx(index.StaleLock, {
1876
+ stale: render.stale,
1877
+ children: /* @__PURE__ */ jsxRuntime.jsx(index.ActivityRow, {
1878
+ assignState: render.assignState,
1879
+ assigneeIds: render.assigneeIds,
1880
+ dateControl: /* @__PURE__ */ jsxRuntime.jsx(ActivityDateControl, {
1881
+ dates: render.dates,
1882
+ instanceId: row.instanceId,
1883
+ state: render.dateState
1884
+ }),
1885
+ face: render.face,
1464
1886
  instanceId: row.instanceId,
1465
- state: render.dateState
1466
- }),
1467
- face: render.face,
1468
- instanceId: row.instanceId,
1469
- onOpen: onOpen,
1470
- terminalActions: showTerminalActions ? render.terminalActions : []
1887
+ onOpen: onOpen,
1888
+ terminalActions: showTerminalActions ? render.terminalActions : []
1889
+ })
1471
1890
  })
1472
1891
  });
1473
1892
  }
1474
1893
 
1894
+ function useBandOpenState(args) {
1895
+ const {segment: segment, scope: scope} = args, store = react.useMemo(() => createBandStateStore({
1896
+ storageKey: bandStateStorageKey(scope)
1897
+ }), [ scope ]), [held, setHeld] = react.useState(() => ({
1898
+ store: store,
1899
+ toggles: store.read()
1900
+ }));
1901
+ held.store !== store && setHeld({
1902
+ store: store,
1903
+ toggles: store.read()
1904
+ });
1905
+ const qualify = key => `${segment}/${key}`, isOpen = (key, defaultOpen) => held.toggles.get(qualify(key)) ?? defaultOpen;
1906
+ return {
1907
+ isOpen: isOpen,
1908
+ toggle: (key, defaultOpen) => {
1909
+ setHeld({
1910
+ store: store,
1911
+ toggles: store.setOpen(qualify(key), !isOpen(key, defaultOpen))
1912
+ });
1913
+ }
1914
+ };
1915
+ }
1916
+
1917
+ const NO_MISSING = /* @__PURE__ */ new Set;
1918
+
1919
+ function sameIdSet(a, b) {
1920
+ if (a.size !== b.size) return !1;
1921
+ for (const id of a) if (!b.has(id)) return !1;
1922
+ return !0;
1923
+ }
1924
+
1925
+ function useOrphanedWorkflows(groups) {
1926
+ const {actualTypes: actualTypes, binding: binding} = index.useWorkflowContext(), contentResource = binding.contentResource, key = react.useMemo(() => orphanProbeIds(groups, contentResource), [ groups, contentResource ]).join("|"), [missing, setMissing] = react.useState(NO_MISSING);
1927
+ return react.useEffect(() => {
1928
+ if (key === "") {
1929
+ setMissing(NO_MISSING);
1930
+ return;
1931
+ }
1932
+ const tracked = key.split("|"), releases = tracked.map(id => actualTypes.track(id)), recompute = () => {
1933
+ const next = new Set(tracked.filter(id => actualTypes.read(id) === null));
1934
+ setMissing(current => sameIdSet(current, next) ? current : next);
1935
+ };
1936
+ recompute();
1937
+ const unsubscribe = actualTypes.subscribe(recompute);
1938
+ return () => {
1939
+ unsubscribe();
1940
+ for (const release of releases) release();
1941
+ };
1942
+ }, [ actualTypes, key ]), react.useMemo(() => missing.size === 0 ? NO_ORPHANS : orphanVerdict({
1943
+ groups: groups,
1944
+ contentResource: contentResource,
1945
+ missing: missing
1946
+ }), [ groups, contentResource, missing ]);
1947
+ }
1948
+
1475
1949
  const INSTANCE_CAP = 200, NEWEST_INSTANCES = {
1476
1950
  includeCompleted: !0,
1477
1951
  limit: INSTANCE_CAP
@@ -1494,18 +1968,21 @@ function useToolInstances() {
1494
1968
  }, [ instances, loading, unreadable ]);
1495
1969
  }
1496
1970
 
1497
- function TasksTab({filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, mineOnly: mineOnly, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, truncated: truncated, unreadable: unreadable}) {
1498
- const [filters, setFilters] = react.useState({}), groups = react.useMemo(() => deriveTaskGroups({
1971
+ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, segment: segment, truncated: truncated, unreadable: unreadable}) {
1972
+ const mineOnly = segment === "for-me", {binding: binding} = index.useWorkflowContext(), bands = useBandOpenState({
1973
+ segment: segment,
1974
+ scope: binding.engineResource.id
1975
+ }), [filters, setFilters] = react.useState({}), groups = react.useMemo(() => deriveTaskGroups({
1499
1976
  instances: instances,
1500
1977
  identity: identity
1501
- }), [ instances, identity ]), options = react.useMemo(() => taskFilterOptions(groups), [ groups ]), visible = react.useMemo(() => {
1978
+ }), [ instances, identity ]), orphans = useOrphanedWorkflows(groups), listed = react.useMemo(() => hideOrphanedGroups(groups, orphans.groupKeys), [ groups, orphans.groupKeys ]), options = react.useMemo(() => taskFilterOptions(listed), [ listed ]), visible = react.useMemo(() => {
1502
1979
  const filtered = applyTaskFilters({
1503
- groups: groups,
1980
+ groups: listed,
1504
1981
  filters: filters,
1505
1982
  now: /* @__PURE__ */ new Date
1506
1983
  });
1507
1984
  return mineOnly ? onlyMyTasks(filtered) : filtered;
1508
- }, [ groups, filters, mineOnly ]);
1985
+ }, [ listed, filters, mineOnly ]);
1509
1986
  return loading ?
1510
1987
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
1511
1988
  paddingLeft: 2,
@@ -1526,8 +2003,13 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
1526
2003
  /* @__PURE__ */ jsxRuntime.jsx(index.UnreadableDocsNote, {
1527
2004
  unreadable: unreadable
1528
2005
  }),
2006
+ /* @__PURE__ */ jsxRuntime.jsx(OrphanedWorkflowsNote, {
2007
+ orphans: orphans
2008
+ }),
1529
2009
  /* @__PURE__ */ jsxRuntime.jsx(TaskGroupList, {
2010
+ bands: bands,
1530
2011
  groups: visible,
2012
+ instanceDefaultOpen: instanceBandDefaultOpen(segment),
1531
2013
  onOpenTask: onOpenTask,
1532
2014
  onOpenWorkflow: onOpenWorkflow,
1533
2015
  showTerminalActions: mineOnly
@@ -1568,7 +2050,12 @@ function carryingSearchParams(state, searchParams) {
1568
2050
  }
1569
2051
 
1570
2052
  function WorkflowsToolRoot() {
1571
- const router$1 = router.useRouter(), {seedInstance: seedInstance} = index.useWorkflowContext(), instanceId = typeof router$1.state.instanceId == "string" ? router$1.state.instanceId : void 0;
2053
+ const router$1 = router.useRouter(), {seedInstance: seedInstance} = index.useWorkflowContext(), {tab: tab} = useToolTab();
2054
+ index.useLogEventOnMount(index.WorkflowToolOpened, {
2055
+ tab: tab,
2056
+ via: "open"
2057
+ });
2058
+ const instanceId = typeof router$1.state.instanceId == "string" ? router$1.state.instanceId : void 0;
1572
2059
  return instanceId ? /* @__PURE__ */ jsxRuntime.jsx(WorkflowInstanceDetail, {
1573
2060
  instanceId: instanceId,
1574
2061
  onBack: () => router$1.navigate(carryingSearchParams({}, router$1.state._searchParams))
@@ -1582,7 +2069,7 @@ function WorkflowsToolRoot() {
1582
2069
  }
1583
2070
 
1584
2071
  function WorkflowsHome({onOpenInstance: onOpenInstance}) {
1585
- const {tab: tab, setTab: setTab} = useToolTab(), instances = useToolInstances(), identity = index.useAssignmentIdentity(), [openTask, setOpenTask] = react.useState(null), openInstance = target => {
2072
+ const {tab: tab, setTab: setTab} = useToolTab(), telemetry = workflowReact.useWorkflowTelemetry(), instances = useToolInstances(), identity = index.useAssignmentIdentity(), [openTask, setOpenTask] = react.useState(null), openInstance = target => {
1586
2073
  if (typeof target != "string") return onOpenInstance(target);
1587
2074
  const held = [ ...instances.inFlight, ...instances.settled ].find(i => i._id === target);
1588
2075
  onOpenInstance(held ?? target);
@@ -1626,7 +2113,12 @@ function WorkflowsHome({onOpenInstance: onOpenInstance}) {
1626
2113
  /* @__PURE__ */ jsxRuntime.jsx(index.TabSwitch, {
1627
2114
  ariaControls: "workflows-tool-panel",
1628
2115
  idPrefix: "workflows-tool-tab",
1629
- onSelect: setTab,
2116
+ onSelect: next => {
2117
+ next !== tab && telemetry.log(index.WorkflowToolOpened, {
2118
+ tab: next,
2119
+ via: "tab-switch"
2120
+ }), setTab(next);
2121
+ },
1630
2122
  options: TOOL_TABS,
1631
2123
  selected: tab
1632
2124
  }),
@@ -1657,9 +2149,9 @@ function WorkflowsHome({onOpenInstance: onOpenInstance}) {
1657
2149
  identity: identity,
1658
2150
  instances: [ ...instances.inFlight, ...instances.settled ],
1659
2151
  loading: instances.loading,
1660
- mineOnly: tab === "for-me",
1661
2152
  onOpenTask: setOpenTask,
1662
2153
  onOpenWorkflow: openInstance,
2154
+ segment: tab,
1663
2155
  truncated: instances.truncated,
1664
2156
  unreadable: instances.unreadable
1665
2157
  })