@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.
@@ -1,6 +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");
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");
4
4
 
5
5
  function documentRefsOf(instance) {
6
6
  const seen = /* @__PURE__ */ new Map;
@@ -18,14 +18,19 @@ function InstanceTaskLists({entry: entry, onOpenActivity: onOpenActivity}) {
18
18
  onOpenActivity: onOpenActivity
19
19
  }),
20
20
  /* @__PURE__ */ jsxRuntime.jsx(index.TodoItemsList, {
21
- entry: entry
21
+ entry: entry,
22
+ surface: "tool-instance-detail"
22
23
  }) ]
23
24
  })
24
25
  });
25
26
  }
26
27
 
27
28
  function WorkflowInstanceDetail({instanceId: instanceId, onBack: onBack}) {
28
- 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);
29
34
  return react.useEffect(() => {
30
35
  const timer = setTimeout(() => setGraceOver(!0), 2500);
31
36
  return () => clearTimeout(timer);
@@ -119,7 +124,8 @@ function InstanceDetailPanel({entry: entry, onBack: onBack}) {
119
124
  breadcrumb: index.instanceBreadcrumb(instance, definition),
120
125
  definition: definition,
121
126
  entry: entry,
122
- onClose: () => setOpenActivity(null)
127
+ onClose: () => setOpenActivity(null),
128
+ source: "tool-instance-detail"
123
129
  }) : null ]
124
130
  });
125
131
  }
@@ -260,7 +266,8 @@ function NewWorkflowButton() {
260
266
  onPick: definition => {
261
267
  setOpen(!1), openStartDialog({
262
268
  definition: definition.name,
263
- label: definition.title
269
+ label: definition.title,
270
+ source: "tool"
264
271
  });
265
272
  }
266
273
  }) : null,
@@ -367,7 +374,8 @@ function ToolActivityDialog({target: target, onClose: onClose}) {
367
374
  definition: definition,
368
375
  document: documentRefsOf(entry.instance)[0],
369
376
  entry: entry,
370
- onClose: onClose
377
+ onClose: onClose,
378
+ source: "tool-task-list"
371
379
  }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Dialog, {
372
380
  header: "Loading activity…",
373
381
  id: "workflow-activity-detail",
@@ -382,6 +390,147 @@ function ToolActivityDialog({target: target, onClose: onClose}) {
382
390
  });
383
391
  }
384
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
+
385
534
  function datesOf(state) {
386
535
  return state.filter(entry => entry._type === "date" || entry._type === "datetime").map(entry => entry.value).filter(value => typeof value == "string" && value !== "");
387
536
  }
@@ -539,6 +688,223 @@ function taskRowRender(row, entry) {
539
688
  };
540
689
  }
541
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
+
542
908
  function taskFilterOptions(groups) {
543
909
  const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
544
910
  for (const group of groups.withDocuments) {
@@ -555,8 +921,33 @@ function taskFilterOptions(groups) {
555
921
  };
556
922
  }
557
923
 
924
+ function activeEntries(filters) {
925
+ return Object.entries(filters).filter(([, value]) => value !== void 0 && (!Array.isArray(value) || value.length > 0));
926
+ }
927
+
558
928
  function hasActiveFilters(filters) {
559
- 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
+ };
560
951
  }
561
952
 
562
953
  function toggledValue(args) {
@@ -711,7 +1102,12 @@ function useDocPreviewTitles(documents) {
711
1102
  const HITS_CAP = 6;
712
1103
 
713
1104
  function TaskFilterMenu({filters: filters, options: options, onChange: onChange}) {
714
- 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);
715
1111
  /* @__PURE__ */
716
1112
  return jsxRuntime.jsx(index.DismissablePopover, {
717
1113
  content: open ? /* @__PURE__ */ jsxRuntime.jsx(FilterPanel, {
@@ -719,7 +1115,7 @@ function TaskFilterMenu({filters: filters, options: options, onChange: onChange}
719
1115
  onChange: onChange,
720
1116
  options: options
721
1117
  }) : null,
722
- onDismiss: () => setOpen(!1),
1118
+ onDismiss: close,
723
1119
  open: open,
724
1120
  overflow: "visible",
725
1121
  children: /* @__PURE__ */ jsxRuntime.jsx(index.HoverHint, {
@@ -730,7 +1126,7 @@ function TaskFilterMenu({filters: filters, options: options, onChange: onChange}
730
1126
  fontSize: 1,
731
1127
  icon: Filter.FilterIcon,
732
1128
  mode: "bleed",
733
- onClick: () => setOpen(v => !v),
1129
+ onClick: () => open ? close() : openMenu(),
734
1130
  padding: 2,
735
1131
  selected: active || open
736
1132
  })
@@ -1226,7 +1622,7 @@ function ActivityDateControl({instanceId: instanceId, state: state, dates: dates
1226
1622
  });
1227
1623
  const kind = index.dateControlKind(state), commit = (mode, value) => {
1228
1624
  state.kind === "editable" && save(() => editFieldFor(instanceId, {
1229
- target: index.editTargetOf(state.field),
1625
+ target: workflowReact.editFieldTarget(state.field),
1230
1626
  ...mode === "set" ? {
1231
1627
  mode: mode,
1232
1628
  value: value
@@ -1327,13 +1723,15 @@ function RefChips({refs: refs, max: max = 2}) {
1327
1723
 
1328
1724
  const EMPTY_NOTE = "No tasks match — adjust the filters, or start a workflow", UNGROUPED_TITLE = "Workflows without Studio documents";
1329
1725
 
1330
- 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}) {
1331
1727
  return groups.withDocuments.length === 0 && groups.withoutDocuments.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
1332
1728
  text: EMPTY_NOTE
1333
1729
  }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
1334
1730
  gap: 2,
1335
1731
  children: [ groups.withDocuments.map(group => /* @__PURE__ */ jsxRuntime.jsx(index.CollapsibleBand, {
1336
1732
  background: !0,
1733
+ onToggle: () => bands.toggle(group.key, GROUP_DEFAULT_OPEN),
1734
+ open: bands.isOpen(group.key, GROUP_DEFAULT_OPEN),
1337
1735
  sticky: !0,
1338
1736
  title: workflowEngine.toBareId(group.document.id),
1339
1737
  header: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, {
@@ -1348,6 +1746,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
1348
1746
  })
1349
1747
  }),
1350
1748
  children: /* @__PURE__ */ jsxRuntime.jsx(InstanceBands, {
1749
+ bands: bands,
1750
+ instanceDefaultOpen: instanceDefaultOpen,
1351
1751
  instances: group.instances,
1352
1752
  onOpenTask: onOpenTask,
1353
1753
  onOpenWorkflow: onOpenWorkflow,
@@ -1355,6 +1755,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
1355
1755
  })
1356
1756
  }, group.key)), groups.withoutDocuments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(index.CollapsibleBand, {
1357
1757
  background: !0,
1758
+ onToggle: () => bands.toggle(UNGROUPED_BAND_KEY, GROUP_DEFAULT_OPEN),
1759
+ open: bands.isOpen(UNGROUPED_BAND_KEY, GROUP_DEFAULT_OPEN),
1358
1760
  sticky: !0,
1359
1761
  title: UNGROUPED_TITLE,
1360
1762
  header: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
@@ -1363,6 +1765,8 @@ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow:
1363
1765
  children: UNGROUPED_TITLE
1364
1766
  }),
1365
1767
  children: /* @__PURE__ */ jsxRuntime.jsx(InstanceBands, {
1768
+ bands: bands,
1769
+ instanceDefaultOpen: instanceDefaultOpen,
1366
1770
  instances: groups.withoutDocuments,
1367
1771
  onOpenTask: onOpenTask,
1368
1772
  onOpenWorkflow: onOpenWorkflow,
@@ -1380,12 +1784,14 @@ function targetOf(row) {
1380
1784
  };
1381
1785
  }
1382
1786
 
1383
- 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}) {
1384
1788
  /* @__PURE__ */
1385
1789
  return jsxRuntime.jsx(ui.Stack, {
1386
1790
  gap: 1,
1387
1791
  children: instances.map(band => /* @__PURE__ */ jsxRuntime.jsx(InstanceBand, {
1388
1792
  band: band,
1793
+ bands: bands,
1794
+ instanceDefaultOpen: instanceDefaultOpen,
1389
1795
  onOpenTask: onOpenTask,
1390
1796
  onOpenWorkflow: onOpenWorkflow,
1391
1797
  showTerminalActions: showTerminalActions
@@ -1393,10 +1799,12 @@ function InstanceBands({instances: instances, onOpenTask: onOpenTask, onOpenWork
1393
1799
  });
1394
1800
  }
1395
1801
 
1396
- 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}) {
1397
1803
  const {resolvePathFromState: resolvePathFromState} = router.useRouter();
1398
1804
  /* @__PURE__ */
1399
1805
  return jsxRuntime.jsx(index.CollapsibleBand, {
1806
+ onToggle: () => bands.toggle(band.instanceId, instanceDefaultOpen),
1807
+ open: bands.isOpen(band.instanceId, instanceDefaultOpen),
1400
1808
  title: band.title,
1401
1809
  header: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, {
1402
1810
  children: [
@@ -1483,6 +1891,61 @@ function TaskActivityRow({row: row, onOpen: onOpen, showTerminalActions: showTer
1483
1891
  });
1484
1892
  }
1485
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
+
1486
1949
  const INSTANCE_CAP = 200, NEWEST_INSTANCES = {
1487
1950
  includeCompleted: !0,
1488
1951
  limit: INSTANCE_CAP
@@ -1505,18 +1968,21 @@ function useToolInstances() {
1505
1968
  }, [ instances, loading, unreadable ]);
1506
1969
  }
1507
1970
 
1508
- function TasksTab({filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, mineOnly: mineOnly, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, truncated: truncated, unreadable: unreadable}) {
1509
- 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({
1510
1976
  instances: instances,
1511
1977
  identity: identity
1512
- }), [ 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(() => {
1513
1979
  const filtered = applyTaskFilters({
1514
- groups: groups,
1980
+ groups: listed,
1515
1981
  filters: filters,
1516
1982
  now: /* @__PURE__ */ new Date
1517
1983
  });
1518
1984
  return mineOnly ? onlyMyTasks(filtered) : filtered;
1519
- }, [ groups, filters, mineOnly ]);
1985
+ }, [ listed, filters, mineOnly ]);
1520
1986
  return loading ?
1521
1987
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
1522
1988
  paddingLeft: 2,
@@ -1537,8 +2003,13 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
1537
2003
  /* @__PURE__ */ jsxRuntime.jsx(index.UnreadableDocsNote, {
1538
2004
  unreadable: unreadable
1539
2005
  }),
2006
+ /* @__PURE__ */ jsxRuntime.jsx(OrphanedWorkflowsNote, {
2007
+ orphans: orphans
2008
+ }),
1540
2009
  /* @__PURE__ */ jsxRuntime.jsx(TaskGroupList, {
2010
+ bands: bands,
1541
2011
  groups: visible,
2012
+ instanceDefaultOpen: instanceBandDefaultOpen(segment),
1542
2013
  onOpenTask: onOpenTask,
1543
2014
  onOpenWorkflow: onOpenWorkflow,
1544
2015
  showTerminalActions: mineOnly
@@ -1579,7 +2050,12 @@ function carryingSearchParams(state, searchParams) {
1579
2050
  }
1580
2051
 
1581
2052
  function WorkflowsToolRoot() {
1582
- 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;
1583
2059
  return instanceId ? /* @__PURE__ */ jsxRuntime.jsx(WorkflowInstanceDetail, {
1584
2060
  instanceId: instanceId,
1585
2061
  onBack: () => router$1.navigate(carryingSearchParams({}, router$1.state._searchParams))
@@ -1593,7 +2069,7 @@ function WorkflowsToolRoot() {
1593
2069
  }
1594
2070
 
1595
2071
  function WorkflowsHome({onOpenInstance: onOpenInstance}) {
1596
- 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 => {
1597
2073
  if (typeof target != "string") return onOpenInstance(target);
1598
2074
  const held = [ ...instances.inFlight, ...instances.settled ].find(i => i._id === target);
1599
2075
  onOpenInstance(held ?? target);
@@ -1637,7 +2113,12 @@ function WorkflowsHome({onOpenInstance: onOpenInstance}) {
1637
2113
  /* @__PURE__ */ jsxRuntime.jsx(index.TabSwitch, {
1638
2114
  ariaControls: "workflows-tool-panel",
1639
2115
  idPrefix: "workflows-tool-tab",
1640
- onSelect: setTab,
2116
+ onSelect: next => {
2117
+ next !== tab && telemetry.log(index.WorkflowToolOpened, {
2118
+ tab: next,
2119
+ via: "tab-switch"
2120
+ }), setTab(next);
2121
+ },
1641
2122
  options: TOOL_TABS,
1642
2123
  selected: tab
1643
2124
  }),
@@ -1668,9 +2149,9 @@ function WorkflowsHome({onOpenInstance: onOpenInstance}) {
1668
2149
  identity: identity,
1669
2150
  instances: [ ...instances.inFlight, ...instances.settled ],
1670
2151
  loading: instances.loading,
1671
- mineOnly: tab === "for-me",
1672
2152
  onOpenTask: setOpenTask,
1673
2153
  onOpenWorkflow: openInstance,
2154
+ segment: tab,
1674
2155
  truncated: instances.truncated,
1675
2156
  unreadable: instances.unreadable
1676
2157
  })