@sanity/workflow-studio-plugin 0.28.0 → 0.29.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"), workflowReact = require("@sanity/workflow-react"), react = require("react"), router = require("sanity/router"), index = require("./index.cjs"), workflowEngine = require("@sanity/workflow-engine"), ArrowLeft = require("@sanity/icons/ArrowLeft"), Transfer = require("@sanity/icons/Transfer"), WarningOutline = require("@sanity/icons/WarningOutline"), pluralize = require("pluralize-esm"), sanity = require("sanity"), workflowStudio = require("@sanity/workflow-studio"), 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"), ChevronDown = require("@sanity/icons/ChevronDown"), ChevronRight = require("@sanity/icons/ChevronRight"), types = require("@sanity/types"), workflowDiagram = require("@sanity/workflow-diagram"), react$1 = require("motion/react");
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"), workflowEngine = require("@sanity/workflow-engine"), ArrowLeft = require("@sanity/icons/ArrowLeft"), Calendar = require("@sanity/icons/Calendar"), Clock = require("@sanity/icons/Clock"), Document = require("@sanity/icons/Document"), ErrorOutline = require("@sanity/icons/ErrorOutline"), Transfer = require("@sanity/icons/Transfer"), sanity = require("sanity"), workflowStudio = require("@sanity/workflow-studio"), pluralize = require("pluralize-esm"), 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"), ChevronDown = require("@sanity/icons/ChevronDown"), ChevronRight = require("@sanity/icons/ChevronRight"), types = require("@sanity/types"), Empty = require("@sanity/icons/Empty"), WarningOutline = require("@sanity/icons/WarningOutline"), workflowDiagram = require("@sanity/workflow-diagram"), react$1 = require("motion/react");
4
4
 
5
5
  function _interopDefaultCompat(e) {
6
6
  return e && typeof e == "object" && "default" in e ? e : {
@@ -49,11 +49,22 @@ function documentRefsOf(instance) {
49
49
  return [ ...seen.values() ];
50
50
  }
51
51
 
52
+ function rootInstanceOf(instance, byId) {
53
+ const rootRef = instance.ancestors[0];
54
+ return rootRef ? byId.get(workflowEngine.toBareId(rootRef.id)) ?? instance : instance;
55
+ }
56
+
57
+ const TOOL_PANEL_PADDING = 3, TOOL_HEADING_PADDING = 2, PANEL_STEP = 3;
58
+
52
59
  function TabStatusRow({children: children}) {
60
+ const firstLineOffset = index.useSpaceToken(PANEL_STEP) + index.useSpaceToken(TOOL_HEADING_PADDING);
53
61
  /* @__PURE__ */
54
62
  return jsxRuntime.jsx(ui.Box, {
55
- paddingLeft: 2,
56
- paddingY: 3,
63
+ paddingBottom: PANEL_STEP,
64
+ paddingLeft: TOOL_HEADING_PADDING,
65
+ style: {
66
+ paddingTop: firstLineOffset
67
+ },
57
68
  children: children
58
69
  });
59
70
  }
@@ -304,14 +315,24 @@ function InstanceDetailBody({entry: entry, framed: framed, subject: subject}) {
304
315
  },
305
316
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
306
317
  gap: 5,
307
- children: [ subject === "preview" ? /* @__PURE__ */ jsxRuntime.jsx(SubjectPreview, {
318
+ children: [
319
+ /* @__PURE__ */ jsxRuntime.jsx(index.InstanceFaultNote, {
320
+ entry: entry,
321
+ scope: {
322
+ kind: "run",
323
+ onOpenActivity: activityName => setOpenActivity({
324
+ stage: instance.currentStage,
325
+ activityName: activityName
326
+ })
327
+ }
328
+ }), subject === "preview" ? /* @__PURE__ */ jsxRuntime.jsx(SubjectPreview, {
308
329
  instance: instance
309
330
  }) : null,
310
331
  /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
311
332
  gap: 4,
312
333
  children: [
313
334
  /* @__PURE__ */ jsxRuntime.jsx(SectionHeading, {
314
- children: "Stages / Tasks"
335
+ children: "Stages"
315
336
  }),
316
337
  /* @__PURE__ */ jsxRuntime.jsx(index.InstanceSnapshotBody, {
317
338
  entry: entry,
@@ -393,12 +414,140 @@ function byResolvedTitle(titleOf, keyOf) {
393
414
  };
394
415
  }
395
416
 
417
+ function openActivitiesOf(instance) {
418
+ return (workflowEngine.findOpenStageEntry(instance)?.activities ?? []).filter(activity => index.isOpenActivityStatus(activity.status));
419
+ }
420
+
421
+ function overdueTaskCount(instance, now) {
422
+ return openActivitiesOf(instance).filter(activity => index.dueDatesOf(activity.fields ?? []).some(due => index.isDueDatePast(due, now))).length;
423
+ }
424
+
425
+ const NO_SIGNALS = {
426
+ activeCount: 0,
427
+ assignedToMe: 0,
428
+ documentCount: 0,
429
+ documentlessCount: 0,
430
+ initialStageDocumentCount: 0,
431
+ lastTransition: void 0,
432
+ overdueDocumentCount: 0,
433
+ overdueDocumentlessCount: 0,
434
+ stuckCount: 0
435
+ };
436
+
437
+ function noDefinitionSignals() {
438
+ return NO_SIGNALS;
439
+ }
440
+
441
+ function holdsWorkFor(instance, who) {
442
+ return openActivitiesOf(instance).some(activity => index.isActivityAssignedTo({
443
+ state: activity.fields ?? [],
444
+ who: who
445
+ }));
446
+ }
447
+
448
+ function arrivalOf(instance, byId) {
449
+ const stage = workflowEngine.findOpenStageEntry(instance);
450
+ if (stage !== void 0) return {
451
+ at: stage.enteredAt,
452
+ document: documentRefOf(instance, byId),
453
+ stage: index.stageTitle(index.definitionSnapshotOf(instance), stage.name)
454
+ };
455
+ }
456
+
457
+ function laterArrival(held, next) {
458
+ return next === void 0 ? held : held === void 0 || next.at > held.at ? next : held;
459
+ }
460
+
461
+ function documentRefOf(instance, byId) {
462
+ return documentRefsOf(rootInstanceOf(instance, byId))[0];
463
+ }
464
+
465
+ const NO_DRAFT = {
466
+ ...NO_SIGNALS,
467
+ documents: /* @__PURE__ */ new Set,
468
+ initialStageDocuments: /* @__PURE__ */ new Set,
469
+ overdueDocuments: /* @__PURE__ */ new Set
470
+ };
471
+
472
+ function withDocument(held, document2) {
473
+ return document2 === void 0 ? held : new Set(held).add(document2);
474
+ }
475
+
476
+ function atInitialStage(instance) {
477
+ const snapshot = index.definitionSnapshotOf(instance);
478
+ return snapshot !== void 0 && instance.currentStage === snapshot.initialStage;
479
+ }
480
+
481
+ function fold(args) {
482
+ const {byId: byId, held: held, instance: instance, now: now, who: who} = args, stuck = workflowEngine.documentStuckCause({
483
+ instance: instance,
484
+ definition: index.definitionSnapshotOf(instance)
485
+ }), document2 = documentRefOf(instance, byId)?.id, overdue = overdueTaskCount(instance, now) > 0;
486
+ return {
487
+ activeCount: held.activeCount + 1,
488
+ assignedToMe: held.assignedToMe + (who !== void 0 && holdsWorkFor(instance, who) ? 1 : 0),
489
+ documentlessCount: held.documentlessCount + (document2 === void 0 ? 1 : 0),
490
+ documents: withDocument(held.documents, document2),
491
+ initialStageDocuments: atInitialStage(instance) ? withDocument(held.initialStageDocuments, document2) : held.initialStageDocuments,
492
+ lastTransition: laterArrival(held.lastTransition, arrivalOf(instance, byId)),
493
+ overdueDocumentlessCount: held.overdueDocumentlessCount + (overdue && document2 === void 0 ? 1 : 0),
494
+ overdueDocuments: overdue ? withDocument(held.overdueDocuments, document2) : held.overdueDocuments,
495
+ stuckCount: held.stuckCount + (stuck === void 0 ? 0 : 1)
496
+ };
497
+ }
498
+
499
+ function settle(draft) {
500
+ const {documents: documents, initialStageDocuments: initialStageDocuments, overdueDocuments: overdueDocuments, ...rest} = draft;
501
+ return {
502
+ ...rest,
503
+ documentCount: documents.size,
504
+ initialStageDocumentCount: initialStageDocuments.size,
505
+ overdueDocumentCount: overdueDocuments.size
506
+ };
507
+ }
508
+
509
+ function definitionSignals(args) {
510
+ const byId = new Map(args.instances.map(instance => [ instance._id, instance ])), drafts = /* @__PURE__ */ new Map;
511
+ for (const instance of args.instances) {
512
+ if (workflowEngine.terminalState(instance) !== "in-flight") continue;
513
+ const held = drafts.get(instance.definition) ?? NO_DRAFT;
514
+ drafts.set(instance.definition, fold({
515
+ byId: byId,
516
+ held: held,
517
+ instance: instance,
518
+ now: args.now,
519
+ who: args.identity
520
+ }));
521
+ }
522
+ return new Map([ ...drafts ].map(([name, draft]) => [ name, settle(draft) ]));
523
+ }
524
+
525
+ function catalogBands(rows) {
526
+ const assigned = rows.filter(row => row.signals.assignedToMe > 0);
527
+ if (assigned.length === 0) return [ {
528
+ rows: rows,
529
+ title: void 0
530
+ } ];
531
+ const rest = rows.filter(row => row.signals.assignedToMe === 0);
532
+ return [ {
533
+ rows: assigned,
534
+ title: "Assigned to you"
535
+ }, {
536
+ rows: rest,
537
+ title: "Other workflows"
538
+ } ].filter(band => band.rows.length > 0);
539
+ }
540
+
396
541
  function definitionCatalog(args) {
397
- const rejected = [ ...args.issues.values() ];
542
+ const rejected = [ ...args.issues.values() ], signals = definitionSignals({
543
+ identity: args.identity,
544
+ instances: args.instances,
545
+ now: args.now
546
+ });
398
547
  return args.definitions.map(definition => ({
399
548
  name: definition.name,
400
549
  title: definition.title ?? definition.name,
401
- activeCount: args.inFlight.filter(instance => instance.definition === definition.name).length,
550
+ signals: signals.get(definition.name) ?? noDefinitionSignals(),
402
551
  docTypes: [ ...new Set(args.mappings.filter(mapping => mapping.definition === definition.name).map(mapping => mapping.docType)) ],
403
552
  brokenBindings: rejected.filter(issue => issue.definition === definition.name && issue.version === definition.version).map(issue => ({
404
553
  docType: issue.docType,
@@ -436,7 +585,7 @@ function deployedPhrase(definition) {
436
585
  return ago === "" ? index.formatShortDateTime(deployedAt) : `${index.formatShortDateTime(deployedAt)} (${ago})`;
437
586
  }
438
587
 
439
- const TOOL_PANEL_PADDING = 3, TOOL_HEADING_PADDING = 2, logged = /* @__PURE__ */ new WeakSet;
588
+ const logged = /* @__PURE__ */ new WeakSet;
440
589
 
441
590
  function logReadFailure(what, error) {
442
591
  if (typeof error == "object" && error !== null) {
@@ -474,17 +623,17 @@ function useCappedInstances(filter) {
474
623
  }, [ instances, loading, unreadable, error ]);
475
624
  }
476
625
 
477
- const CATALOG_READ_FAILED = "Couldn’t load the deployed workflows. Try again later.", lastRead = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
626
+ const CATALOG_READ_FAILED = "Couldn’t load the deployed workflows. Try again later.", lastRead$1 = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
478
627
 
479
628
  function useDeployedDefinitions() {
480
- const {engine: engine} = index.useWorkflowContext(), [definitions, setDefinitions] = react.useState(() => lastRead.get(engine)?.rows), [error, setError] = react.useState(void 0);
629
+ const {engine: engine} = index.useWorkflowContext(), [definitions, setDefinitions] = react.useState(() => lastRead$1.get(engine)?.rows), [error, setError] = react.useState(void 0);
481
630
  return react.useEffect(() => {
482
631
  let cancelled = !1;
483
- setDefinitions(lastRead.get(engine)?.rows), setError(void 0);
632
+ setDefinitions(lastRead$1.get(engine)?.rows), setError(void 0);
484
633
  const seq = (issuedReads.get(engine) ?? 0) + 1;
485
634
  return issuedReads.set(engine, seq), (async () => {
486
- const rows = await index.readDeployedDefinitions(engine), latest = workflowEngine.latestDeployedDefinitions(rows), held = lastRead.get(engine);
487
- (held === void 0 || seq > held.seq) && lastRead.set(engine, {
635
+ const rows = await index.readDeployedDefinitions(engine), latest = workflowEngine.latestDeployedDefinitions(rows), held = lastRead$1.get(engine);
636
+ (held === void 0 || seq > held.seq) && lastRead$1.set(engine, {
488
637
  seq: seq,
489
638
  rows: latest
490
639
  }), cancelled || (setDefinitions(latest), setError(void 0));
@@ -499,13 +648,78 @@ function useDeployedDefinitions() {
499
648
  };
500
649
  }
501
650
 
651
+ const NO_TITLES = {
652
+ titles: /* @__PURE__ */ new Map,
653
+ resolved: /* @__PURE__ */ new Set
654
+ };
655
+
656
+ function useDocPreviewTitles(documents) {
657
+ const previewStore = sanity.useDocumentPreviewStore(), schema = sanity.useSchema(), {perspectiveStack: perspectiveStack} = sanity.usePerspective(), {actualTypes: actualTypes, binding: binding} = index.useWorkflowContext(), [state, setState] = react.useState(NO_TITLES), key = documents.map(doc => doc.id).join("|");
658
+ return react.useEffect(() => {
659
+ const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
660
+ titles: new Map(titles),
661
+ resolved: new Set(resolved)
662
+ }), localDocs = documents.flatMap(doc => {
663
+ const {parsed: parsed, local: local} = index.gdrLocality(doc.id, binding.contentResource);
664
+ return !parsed || !local ? (resolved.add(doc.id), []) : [ {
665
+ doc: doc,
666
+ bareId: parsed.documentId
667
+ } ];
668
+ }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
669
+ previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
670
+ }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
671
+ const schemaType = index.openableSchemaType(schema, typeName);
672
+ if (!schemaType) return !1;
673
+ if (previews.get(docId)?.typeName === typeName) return !0;
674
+ dropPreview(docId), resolved.delete(docId);
675
+ const subscription = previewStore.observeForPreview({
676
+ _id: bareId,
677
+ _type: schemaType.name
678
+ }, schemaType, {
679
+ perspective: perspectiveStack
680
+ }).subscribe({
681
+ next: event => {
682
+ resolved.add(docId);
683
+ const title = event.snapshot?.title;
684
+ typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
685
+ },
686
+ error: err => {
687
+ console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
688
+ publish();
689
+ }
690
+ });
691
+ return previews.set(docId, {
692
+ typeName: typeName,
693
+ unsubscribe: () => subscription.unsubscribe()
694
+ }), !0;
695
+ }, evaluate = () => {
696
+ for (const {doc: doc, bareId: bareId} of localDocs) {
697
+ const probe = actualTypes.read(bareId);
698
+ if (probe === null) {
699
+ dropPreview(doc.id), resolved.add(doc.id);
700
+ continue;
701
+ }
702
+ !observe({
703
+ docId: doc.id,
704
+ bareId: bareId,
705
+ typeName: probe ?? doc.type
706
+ }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
707
+ }
708
+ publish();
709
+ }, unsubscribeProbes = actualTypes.subscribe(evaluate);
710
+ return evaluate(), () => {
711
+ unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
712
+ };
713
+ }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
714
+ state;
715
+ }
716
+
502
717
  const INSTANCE_CAP_COUNT_NOTE = `Counts from the latest ${INSTANCE_CAP} workflows — older ones aren’t counted.`, NEWEST_INSTANCES = {
503
718
  includeCompleted: !0
504
- };
719
+ }, lastRead = /* @__PURE__ */ new WeakMap;
505
720
 
506
721
  function useToolInstances() {
507
- const {rows: rows, truncated: truncated, loading: loading, unreadable: unreadable, error: error} = useCappedInstances(NEWEST_INSTANCES);
508
- return react.useMemo(() => ({
722
+ const {engine: engine} = index.useWorkflowContext(), {rows: rows, truncated: truncated, loading: loading, unreadable: unreadable, error: error} = useCappedInstances(NEWEST_INSTANCES), fresh = react.useMemo(() => ({
509
723
  inFlight: rows.filter(instance => workflowEngine.terminalState(instance) === "in-flight"),
510
724
  settled: rows.filter(instance => workflowEngine.terminalState(instance) !== "in-flight"),
511
725
  truncated: truncated,
@@ -513,41 +727,118 @@ function useToolInstances() {
513
727
  unreadable: unreadable,
514
728
  error: error
515
729
  }), [ rows, truncated, loading, unreadable, error ]);
730
+ return react.useEffect(() => {
731
+ !fresh.loading && fresh.error === void 0 && lastRead.set(engine, fresh);
732
+ }, [ engine, fresh ]), loading ? lastRead.get(engine) ?? fresh : fresh;
733
+ }
734
+
735
+ function documentPhrase(count) {
736
+ return count === 0 ? "No documents" : pluralize__default.default("document", count, !0);
737
+ }
738
+
739
+ function documentlessTail(count) {
740
+ return count === 0 ? "" : ` (${pluralize__default.default("instance", count, !0)})`;
741
+ }
742
+
743
+ function documentlessNote(work) {
744
+ return work.documentCount > 0 ? "" : documentlessTail(work.documentlessCount);
745
+ }
746
+
747
+ function documentFace(work, atStart) {
748
+ const lead = `${documentPhrase(work.documentCount)} with an active workflow${documentlessNote(work)}`;
749
+ return atStart === 0 ? {
750
+ lead: lead,
751
+ startLine: void 0
752
+ } : atStart === work.documentCount ? {
753
+ lead: `${lead}, yet to leave the starting stage`,
754
+ startLine: void 0
755
+ } : {
756
+ lead: lead,
757
+ startLine: `${atStart} still at the starting stage`
758
+ };
759
+ }
760
+
761
+ function overdueHint(work) {
762
+ return `${documentPhrase(work.documentCount)} with overdue tasks${documentlessTail(work.documentlessCount)}`;
763
+ }
764
+
765
+ const UNTITLED_DOCUMENT = "Untitled", TRANSITION_HINT_PENDING = "Loading…";
766
+
767
+ function transitionHeading(at) {
768
+ return `Last stage change at ${index.formatShortDateTime(at)}`;
516
769
  }
517
770
 
518
- function activeSummary(active) {
519
- return active === 0 ? "No active instances" : pluralize__default.default("active instance", active, !0);
771
+ function transitionFace(args) {
772
+ return args.documentPending ? {
773
+ state: "pending"
774
+ } : {
775
+ state: "ready",
776
+ document: args.documentTitle,
777
+ heading: transitionHeading(args.at),
778
+ lead: args.documentTitle === void 0 ? "Changed to" : "changed to",
779
+ stage: args.stage
780
+ };
781
+ }
782
+
783
+ function catalogView(args) {
784
+ if (args.error !== void 0) return {
785
+ kind: "unreadable"
786
+ };
787
+ if (args.definitions === void 0 || args.instances.loading || args.identity.kind === "pending") return {
788
+ kind: "loading"
789
+ };
790
+ if (args.definitions.length === 0) return {
791
+ kind: "empty"
792
+ };
793
+ const rows = definitionCatalog({
794
+ definitions: args.definitions,
795
+ identity: index.identityOf(args.identity),
796
+ instances: [ ...args.instances.inFlight, ...args.instances.settled ],
797
+ issues: args.issues,
798
+ mappings: args.mappings,
799
+ now: args.now
800
+ });
801
+ return {
802
+ kind: "bands",
803
+ bands: catalogBands(rows)
804
+ };
520
805
  }
521
806
 
522
- function activeHeadline(args) {
523
- if (args.error === void 0) return args.loading ? "counting active instances…" : activeSummary(args.active);
807
+ function definitionAlert(row) {
808
+ if (row.signals.stuckCount !== 0) return {
809
+ hint: `${pluralize__default.default("run", row.signals.stuckCount, !0)} here can’t continue without help`
810
+ };
524
811
  }
525
812
 
526
- const CARD_MIN_WIDTH = 280, CARD_RADIUS$1 = 3, CARD_PADDING = 4, CARD_MIN_HEIGHT = 180;
813
+ const CARD_MIN_WIDTH = 280, CARD_RADIUS$1 = 3, CARD_PADDING = 4, HINT_DOCUMENT_MAX_WIDTH = 200, BAND_GAP = 6, CARD_MIN_HEIGHT = 120;
527
814
 
528
- function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
529
- const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappingIssues: mappingIssues, mappings: mappings} = index.useWorkflowContext(), schema = sanity.useSchema(), instances = useToolInstances(), undeployed = /* @__PURE__ */ jsxRuntime.jsx(UndeployedWorkflows, {
815
+ function OverviewTab({identity: identity, onOpenWorkflow: onOpenWorkflow}) {
816
+ const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappingIssues: mappingIssues, mappings: mappings} = index.useWorkflowContext(), schema = sanity.useSchema(), instances = useToolInstances(), now = index.useMinuteClock(), view = catalogView({
817
+ definitions: definitions,
818
+ error: error,
819
+ identity: identity,
820
+ instances: instances,
821
+ issues: mappingIssues,
822
+ mappings: mappings,
823
+ now: now
824
+ }), undeployed = /* @__PURE__ */ jsxRuntime.jsx(UndeployedWorkflows, {
530
825
  notices: undeployedWorkflowNotices({
531
826
  issues: mappingIssues,
532
827
  titleOf: name => schema.get(name)?.title
533
828
  })
534
829
  });
535
- if (error !== void 0) /* @__PURE__ */ return jsxRuntime.jsx(index.NoteBanner, {
830
+ return view.kind === "unreadable" ? /* @__PURE__ */ jsxRuntime.jsx(index.NoteBanner, {
536
831
  label: CATALOG_READ_FAILED,
537
832
  tone: "critical"
538
- });
539
- if (definitions === void 0) /* @__PURE__ */
540
- return jsxRuntime.jsx(TabStatusRow, {
833
+ }) : view.kind === "loading" ? /* @__PURE__ */ jsxRuntime.jsx(TabStatusRow, {
541
834
  children: /* @__PURE__ */ jsxRuntime.jsx(index.LoadingRow, {
542
835
  label: "Loading workflows…"
543
836
  })
544
- });
545
- if (definitions.length === 0) /* @__PURE__ */
546
- return jsxRuntime.jsxs(ui.Flex, {
837
+ }) : view.kind === "empty" ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
547
838
  direction: "column",
548
839
  flex: 1,
549
840
  gap: 3,
550
- paddingBottom: 4,
841
+ paddingBottom: 3,
551
842
  paddingTop: 3,
552
843
  children: [ undeployed,
553
844
  /* @__PURE__ */ jsxRuntime.jsx(index.EmptyState, {
@@ -555,19 +846,7 @@ function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
555
846
  icon: /* @__PURE__ */ jsxRuntime.jsx(Transfer.TransferIcon, {}),
556
847
  title: "No workflows deployed"
557
848
  }) ]
558
- });
559
- const rows = definitionCatalog({
560
- definitions: definitions,
561
- mappings: mappings,
562
- inFlight: instances.inFlight,
563
- issues: mappingIssues
564
- }), active = rows.reduce((sum, row) => sum + row.activeCount, 0), activity = activeHeadline({
565
- active: active,
566
- error: instances.error,
567
- loading: instances.loading
568
- });
569
- /* @__PURE__ */
570
- return jsxRuntime.jsxs(ui.Stack, {
849
+ }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
571
850
  gap: 3,
572
851
  paddingBottom: 4,
573
852
  paddingTop: 3,
@@ -575,39 +854,48 @@ function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
575
854
  label: INSTANCES_READ_FAILED,
576
855
  tone: "critical"
577
856
  }),
578
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
579
- align: "center",
580
- gap: 2,
581
- padding: TOOL_HEADING_PADDING,
582
- children: [
583
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
584
- size: 1,
585
- weight: "semibold",
586
- children: pluralize__default.default("workflow", rows.length, !0)
587
- }), activity === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
588
- muted: !0,
589
- size: 1,
590
- children: activity
591
- }) ]
592
- }),
593
- /* @__PURE__ */ jsxRuntime.jsx(ui.Grid, {
594
- gap: 3,
595
- style: {
596
- gridAutoRows: `minmax(${CARD_MIN_HEIGHT}px, auto)`,
597
- gridTemplateColumns: `repeat(auto-fill, minmax(${CARD_MIN_WIDTH}px, 1fr))`
598
- },
599
- children: rows.map(row => /* @__PURE__ */ jsxRuntime.jsx(WorkflowCard, {
600
- countKnown: instances.error === void 0 && !instances.loading,
601
- onOpen: () => onOpenWorkflow(row.name),
602
- row: row
603
- }, row.name))
857
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, {
858
+ gap: BAND_GAP,
859
+ children: view.bands.map(band => /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
860
+ gap: 3,
861
+ children: [ band.title === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
862
+ padding: TOOL_HEADING_PADDING,
863
+ children: /* @__PURE__ */ jsxRuntime.jsx(index.CountedHeading, {
864
+ count: band.rows.length,
865
+ title: band.title
866
+ })
867
+ }),
868
+ /* @__PURE__ */ jsxRuntime.jsx(CardGrid, {
869
+ countKnown: instances.error === void 0,
870
+ now: now,
871
+ onOpenWorkflow: onOpenWorkflow,
872
+ rows: band.rows
873
+ }) ]
874
+ }, band.title ?? UNBANDED_KEY))
604
875
  }), instances.truncated ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
605
876
  text: INSTANCE_CAP_COUNT_NOTE
606
877
  }) : null ]
607
878
  });
608
879
  }
609
880
 
610
- const CARD_ISSUES_HINT = "This workflow has issues";
881
+ const UNBANDED_KEY = "all";
882
+
883
+ function CardGrid({countKnown: countKnown, now: now, onOpenWorkflow: onOpenWorkflow, rows: rows}) {
884
+ /* @__PURE__ */
885
+ return jsxRuntime.jsx(ui.Grid, {
886
+ gap: 3,
887
+ style: {
888
+ gridAutoRows: `minmax(${CARD_MIN_HEIGHT}px, auto)`,
889
+ gridTemplateColumns: `repeat(auto-fill, minmax(${CARD_MIN_WIDTH}px, 1fr))`
890
+ },
891
+ children: rows.map(row => /* @__PURE__ */ jsxRuntime.jsx(WorkflowCard, {
892
+ countKnown: countKnown,
893
+ now: now,
894
+ onOpen: () => onOpenWorkflow(row.name),
895
+ row: row
896
+ }, row.name))
897
+ });
898
+ }
611
899
 
612
900
  function UndeployedWorkflows({notices: notices}) {
613
901
  return notices.length === 0 ? null :
@@ -620,8 +908,104 @@ function UndeployedWorkflows({notices: notices}) {
620
908
  });
621
909
  }
622
910
 
623
- function WorkflowCard({countKnown: countKnown, onOpen: onOpen, row: row}) {
624
- const flagged = row.brokenBindings.length > 0, card = /* @__PURE__ */ jsxRuntime.jsx(ui.Card, {
911
+ function LastTransitionHint({transition: transition}) {
912
+ const documents = react.useMemo(() => transition.document === void 0 ? [] : [ transition.document ], [ transition.document ]), {resolved: resolved, titles: titles} = useDocPreviewTitles(documents), id = transition.document?.id, face = transitionFace({
913
+ at: transition.at,
914
+ documentPending: id !== void 0 && !resolved.has(id),
915
+ documentTitle: id === void 0 ? void 0 : titles.get(id) ?? UNTITLED_DOCUMENT,
916
+ stage: transition.stage
917
+ });
918
+ return face.state === "pending" ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
919
+ muted: !0,
920
+ size: 1,
921
+ children: TRANSITION_HINT_PENDING
922
+ }) :
923
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
924
+ gap: 3,
925
+ children: [
926
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
927
+ muted: !0,
928
+ size: 1,
929
+ children: face.heading
930
+ }),
931
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, {
932
+ size: 1,
933
+ textOverflow: "ellipsis",
934
+ children: [ face.document === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(HintDocument, {
935
+ children: face.document
936
+ }), face.document === void 0 ? "" : " ", face.lead, " ",
937
+ /* @__PURE__ */ jsxRuntime.jsx(index.InlineEmphasis, {
938
+ children: face.stage
939
+ }) ]
940
+ }) ]
941
+ });
942
+ }
943
+
944
+ function HintDocument({children: children}) {
945
+ /* @__PURE__ */
946
+ return jsxRuntime.jsx("span", {
947
+ style: {
948
+ display: "inline-block",
949
+ maxWidth: HINT_DOCUMENT_MAX_WIDTH,
950
+ overflow: "hidden",
951
+ textOverflow: "ellipsis",
952
+ verticalAlign: "bottom",
953
+ whiteSpace: "nowrap"
954
+ },
955
+ children: /* @__PURE__ */ jsxRuntime.jsx(index.InlineEmphasis, {
956
+ children: children
957
+ })
958
+ });
959
+ }
960
+
961
+ function overdueWork(signals) {
962
+ return {
963
+ documentCount: signals.overdueDocumentCount,
964
+ documentlessCount: signals.overdueDocumentlessCount
965
+ };
966
+ }
967
+
968
+ function CardMetrics$1({now: now, signals: signals}) {
969
+ const overdue = overdueWork(signals), overdueTotal = overdue.documentCount + overdue.documentlessCount, documents = documentFace(signals, signals.initialStageDocumentCount);
970
+ /* @__PURE__ */
971
+ return jsxRuntime.jsxs(ui.Flex, {
972
+ align: "center",
973
+ gap: 3,
974
+ children: [
975
+ /* @__PURE__ */ jsxRuntime.jsx(index.MetricPair, {
976
+ Icon: Document.DocumentIcon,
977
+ empty: signals.documentCount === 0 && signals.documentlessCount === 0,
978
+ face: {
979
+ text: documents.lead,
980
+ ...documents.startLine === void 0 ? {} : {
981
+ description: documents.startLine
982
+ }
983
+ },
984
+ value: String(signals.documentCount)
985
+ }), signals.lastTransition === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(index.MetricPair, {
986
+ Icon: Clock.ClockIcon,
987
+ face: {
988
+ content: /* @__PURE__ */ jsxRuntime.jsx(LastTransitionHint, {
989
+ transition: signals.lastTransition
990
+ }),
991
+ wide: !0
992
+ },
993
+ value: index.formatShortAgo(signals.lastTransition.at, now)
994
+ }), overdueTotal === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(index.MetricPair, {
995
+ Icon: Calendar.CalendarIcon,
996
+ face: {
997
+ text: overdueHint(overdue)
998
+ },
999
+ tone: "caution",
1000
+ value: String(overdueTotal)
1001
+ }) ]
1002
+ });
1003
+ }
1004
+
1005
+ function WorkflowCard({countKnown: countKnown, now: now, onOpen: onOpen, row: row}) {
1006
+ const alert = definitionAlert(row);
1007
+ /* @__PURE__ */
1008
+ return jsxRuntime.jsxs(ui.Card, {
625
1009
  as: "button",
626
1010
  onClick: onOpen,
627
1011
  padding: CARD_PADDING,
@@ -634,50 +1018,44 @@ function WorkflowCard({countKnown: countKnown, onOpen: onOpen, row: row}) {
634
1018
  textAlign: "left",
635
1019
  width: "100%"
636
1020
  },
637
- ...flagged ? {
638
- tone: "caution"
639
- } : {},
640
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
1021
+ children: [
1022
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
641
1023
  align: "flex-start",
642
- gap: 3,
643
- children: [ flagged ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
644
- size: 1,
645
- children: /* @__PURE__ */ jsxRuntime.jsx(WarningOutline.WarningOutlineIcon, {})
646
- }) : null,
647
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
1024
+ gap: 2,
1025
+ style: {
648
1026
  flex: 1,
649
- gap: 3,
1027
+ minWidth: 0
1028
+ },
1029
+ children: [
1030
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
1031
+ size: 1,
650
1032
  style: {
1033
+ flex: 1,
651
1034
  minWidth: 0
652
1035
  },
653
- children: [
654
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
655
- size: 1,
656
- weight: "semibold",
657
- children: row.title
658
- }), countKnown ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
659
- muted: !0,
660
- size: 1,
661
- children: activeSummary(row.activeCount)
662
- }) : null ]
1036
+ weight: "medium",
1037
+ children: row.title
1038
+ }), alert === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(index.AlertGlyph, {
1039
+ Icon: ErrorOutline.ErrorOutlineIcon,
1040
+ hint: alert.hint,
1041
+ tone: "critical"
663
1042
  }) ]
664
- })
1043
+ }), countKnown ? /* @__PURE__ */ jsxRuntime.jsx(CardMetrics$1, {
1044
+ now: now,
1045
+ signals: row.signals
1046
+ }) : null ]
665
1047
  });
666
- return flagged ? /* @__PURE__ */ jsxRuntime.jsx(index.HoverHint, {
667
- fill: !0,
668
- text: CARD_ISSUES_HINT,
669
- children: card
670
- }) : card;
671
1048
  }
672
1049
 
673
1050
  function useStartableDefinitions() {
674
- const {definitions: definitions, error: error} = useDeployedDefinitions();
1051
+ const {definitions: definitions, error: error} = useDeployedDefinitions(), startable = react.useMemo(() => definitions?.filter(row => workflowEngine.isStartableDefinition(row)).map(row => ({
1052
+ name: row.name,
1053
+ title: row.title ?? row.name,
1054
+ description: row.description
1055
+ })), [ definitions ]);
675
1056
  return {
676
- definitions: react.useMemo(() => definitions?.filter(row => workflowEngine.isStartableDefinition(row)).map(row => ({
677
- name: row.name,
678
- title: row.title ?? row.name,
679
- description: row.description
680
- })), [ definitions ]),
1057
+ catalogEmpty: definitions?.length === 0,
1058
+ definitions: startable,
681
1059
  error: error
682
1060
  };
683
1061
  }
@@ -709,16 +1087,45 @@ function NewWorkflowButton() {
709
1087
  });
710
1088
  }
711
1089
 
1090
+ const PANEL_WIDTH = 340;
1091
+
1092
+ function NothingToStart({catalogEmpty: catalogEmpty}) {
1093
+ /* @__PURE__ */
1094
+ return jsxRuntime.jsx(ui.Box, {
1095
+ padding: 4,
1096
+ style: {
1097
+ width: PANEL_WIDTH
1098
+ },
1099
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
1100
+ gap: 3,
1101
+ children: [
1102
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
1103
+ align: "center",
1104
+ size: 1,
1105
+ weight: "medium",
1106
+ children: catalogEmpty ? "No workflows deployed" : "Nothing to start by hand"
1107
+ }),
1108
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
1109
+ align: "center",
1110
+ muted: !0,
1111
+ size: 1,
1112
+ children: catalogEmpty ? "Deploy a workflow to this project to start one from here" : "Every deployed workflow here is started by a parent workflow, never by hand"
1113
+ }) ]
1114
+ })
1115
+ });
1116
+ }
1117
+
712
1118
  function DefinitionPickerPanel({onPick: onPick}) {
713
- const {definitions: definitions, error: error} = useStartableDefinitions(), [query, setQuery] = react.useState(""), visible = react.useMemo(() => {
1119
+ const {catalogEmpty: catalogEmpty, definitions: definitions, error: error} = useStartableDefinitions(), [query, setQuery] = react.useState(""), visible = react.useMemo(() => {
714
1120
  const q = query.trim().toLowerCase();
715
1121
  return definitions ? q ? definitions.filter(d => `${d.title} ${d.name}`.toLowerCase().includes(q)) : definitions : [];
716
1122
  }, [ definitions, query ]), settled = error === void 0 ? definitions : void 0;
717
- /* @__PURE__ */
718
- return jsxRuntime.jsx(ui.Box, {
1123
+ return settled?.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(NothingToStart, {
1124
+ catalogEmpty: catalogEmpty
1125
+ }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
719
1126
  padding: 2,
720
1127
  style: {
721
- width: 340
1128
+ width: PANEL_WIDTH
722
1129
  },
723
1130
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
724
1131
  gap: 2,
@@ -735,8 +1142,6 @@ function DefinitionPickerPanel({onPick: onPick}) {
735
1142
  tone: "critical"
736
1143
  }) : null, !definitions && !error ? /* @__PURE__ */ jsxRuntime.jsx(index.LoadingRow, {
737
1144
  label: "Loading workflows…"
738
- }) : null, settled?.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
739
- text: "No startable workflows are deployed"
740
1145
  }) : null, settled !== void 0 && settled.length > 0 && visible.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
741
1146
  text: "No workflows match"
742
1147
  }) : null,
@@ -1015,13 +1420,20 @@ function useBandOpen(bands, key) {
1015
1420
  };
1016
1421
  }
1017
1422
 
1018
- const REVEAL_MAX_WAIT_MS = 2e3;
1423
+ const REVEAL_STALL_MS = 2e3, REVEAL_COLD_MS = 15e3;
1019
1424
 
1020
1425
  function useRevealGate(args) {
1021
- const {loading: loading, ready: ready} = args, settled = !loading && ready, expired = index.useDelayedFlag(!loading && !ready, REVEAL_MAX_WAIT_MS), [revealed, setRevealed] = react.useState(!1);
1426
+ const {loading: loading, progress: progress, ready: ready} = args, settled = !loading && ready, [stalled, setStalled] = react.useState(!1);
1427
+ react.useEffect(() => {
1428
+ if (loading || ready) return;
1429
+ setStalled(!1);
1430
+ const budget = progress > 0 ? REVEAL_STALL_MS : REVEAL_COLD_MS, timer = setTimeout(() => setStalled(!0), budget);
1431
+ return () => clearTimeout(timer);
1432
+ }, [ loading, progress, ready ]);
1433
+ const [revealed, setRevealed] = react.useState(!1);
1022
1434
  return react.useEffect(() => {
1023
- !revealed && (settled || expired) && setRevealed(!0);
1024
- }, [ expired, revealed, settled ]), revealed || settled || expired;
1435
+ !revealed && (settled || stalled) && setRevealed(!0);
1436
+ }, [ revealed, settled, stalled ]), revealed || settled || stalled;
1025
1437
  }
1026
1438
 
1027
1439
  function spawnOrigins(instances) {
@@ -1034,16 +1446,11 @@ function spawnOrigins(instances) {
1034
1446
  return origins;
1035
1447
  }
1036
1448
 
1037
- function rootOf(instance, byId) {
1038
- const rootRef = instance.ancestors[0];
1039
- return rootRef ? byId.get(workflowEngine.toBareId(rootRef.id)) ?? instance : instance;
1040
- }
1041
-
1042
1449
  function taskRowsOf(args) {
1043
1450
  const {instance: instance, identity: identity} = args, open = workflowEngine.findOpenStageEntry(instance);
1044
1451
  if (!open) return [];
1045
1452
  const definition = index.definitionSnapshotOf(instance), state = index.stateByActivity(instance), rows = (open.activities ?? []).map(activity => {
1046
- const fields = state.get(activity.name) ?? [], declared = index.findActivityNode({
1453
+ const fields = state.get(activity.name) ?? [], declared = workflowEngine.findActivityNode({
1047
1454
  definition: definition,
1048
1455
  stageName: open.name,
1049
1456
  activityName: activity.name
@@ -1098,7 +1505,7 @@ function deriveTaskGroups(args) {
1098
1505
  activityName: origin.activityName
1099
1506
  }).title : void 0,
1100
1507
  rows: rows
1101
- }, root = rootOf(instance, byId), documents = documentRefsOf(root), document2 = documents[0];
1508
+ }, root = rootInstanceOf(instance, byId), documents = documentRefsOf(root), document2 = documents[0];
1102
1509
  if (!document2) {
1103
1510
  withoutDocuments.push(entry);
1104
1511
  continue;
@@ -1404,72 +1811,6 @@ function SettleOrphansDialog({workflows: workflows, onClose: onClose}) {
1404
1811
  });
1405
1812
  }
1406
1813
 
1407
- const NO_TITLES = {
1408
- titles: /* @__PURE__ */ new Map,
1409
- resolved: /* @__PURE__ */ new Set
1410
- };
1411
-
1412
- function useDocPreviewTitles(documents) {
1413
- const previewStore = sanity.useDocumentPreviewStore(), schema = sanity.useSchema(), {perspectiveStack: perspectiveStack} = sanity.usePerspective(), {actualTypes: actualTypes, binding: binding} = index.useWorkflowContext(), [state, setState] = react.useState(NO_TITLES), key = documents.map(doc => doc.id).join("|");
1414
- return react.useEffect(() => {
1415
- const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
1416
- titles: new Map(titles),
1417
- resolved: new Set(resolved)
1418
- }), localDocs = documents.flatMap(doc => {
1419
- const {parsed: parsed, local: local} = index.gdrLocality(doc.id, binding.contentResource);
1420
- return !parsed || !local ? (resolved.add(doc.id), []) : [ {
1421
- doc: doc,
1422
- bareId: parsed.documentId
1423
- } ];
1424
- }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
1425
- previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
1426
- }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
1427
- const schemaType = index.openableSchemaType(schema, typeName);
1428
- if (!schemaType) return !1;
1429
- if (previews.get(docId)?.typeName === typeName) return !0;
1430
- dropPreview(docId), resolved.delete(docId);
1431
- const subscription = previewStore.observeForPreview({
1432
- _id: bareId,
1433
- _type: schemaType.name
1434
- }, schemaType, {
1435
- perspective: perspectiveStack
1436
- }).subscribe({
1437
- next: event => {
1438
- resolved.add(docId);
1439
- const title = event.snapshot?.title;
1440
- typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
1441
- },
1442
- error: err => {
1443
- console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
1444
- publish();
1445
- }
1446
- });
1447
- return previews.set(docId, {
1448
- typeName: typeName,
1449
- unsubscribe: () => subscription.unsubscribe()
1450
- }), !0;
1451
- }, evaluate = () => {
1452
- for (const {doc: doc, bareId: bareId} of localDocs) {
1453
- const probe = actualTypes.read(bareId);
1454
- if (probe === null) {
1455
- dropPreview(doc.id), resolved.add(doc.id);
1456
- continue;
1457
- }
1458
- !observe({
1459
- docId: doc.id,
1460
- bareId: bareId,
1461
- typeName: probe ?? doc.type
1462
- }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
1463
- }
1464
- publish();
1465
- }, unsubscribeProbes = actualTypes.subscribe(evaluate);
1466
- return evaluate(), () => {
1467
- unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
1468
- };
1469
- }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
1470
- state;
1471
- }
1472
-
1473
1814
  function taskFilterOptions(groups) {
1474
1815
  const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
1475
1816
  for (const group of groups.withDocuments) {
@@ -2647,6 +2988,7 @@ function TasksTab({error: error, filterSlot: filterSlot, instances: instances, l
2647
2988
  identity: identity
2648
2989
  }), [ instances, identity ]), orphans = useOrphanedWorkflows(groups), listed = react.useMemo(() => hideOrphanedGroups(groups, orphans.groupKeys), [ groups, orphans.groupKeys ]), options = react.useMemo(() => taskFilterOptions(listed), [ listed ]), titles = useGroupTitles(listed), revealed = useRevealGate({
2649
2990
  loading: loading,
2991
+ progress: titles.resolved.size,
2650
2992
  ready: titles.ready
2651
2993
  }), visible = react.useMemo(() => {
2652
2994
  const filtered = applyTaskFilters({
@@ -2706,6 +3048,30 @@ function TasksTab({error: error, filterSlot: filterSlot, instances: instances, l
2706
3048
  });
2707
3049
  }
2708
3050
 
3051
+ function cardFacts(args) {
3052
+ return new Map(args.instances.map(instance => [ instance._id, {
3053
+ pending: openActivitiesOf(instance).length,
3054
+ overdue: overdueTaskCount(instance, args.now),
3055
+ stuck: workflowEngine.documentStuckCause({
3056
+ instance: instance,
3057
+ definition: index.definitionSnapshotOf(instance)
3058
+ }) !== void 0,
3059
+ settled: workflowEngine.terminalState(instance) !== "in-flight"
3060
+ } ]));
3061
+ }
3062
+
3063
+ function laneTotals(cards, facts) {
3064
+ let overdue = 0, stuck = 0;
3065
+ for (const card of cards) {
3066
+ const held = facts.get(card.instanceId);
3067
+ held !== void 0 && (held.overdue > 0 && (overdue += 1), held.stuck && (stuck += 1));
3068
+ }
3069
+ return {
3070
+ overdue: overdue,
3071
+ stuck: stuck
3072
+ };
3073
+ }
3074
+
2709
3075
  const BOARD_EMPTY_STAGE_NOTE = "No items";
2710
3076
 
2711
3077
  function faceOf(instance) {
@@ -2764,6 +3130,24 @@ function boardDocuments(board) {
2764
3130
  return board.groups.flatMap(group => group.cards.flatMap(card => card.face.kind === "document" ? [ card.face.document ] : []));
2765
3131
  }
2766
3132
 
3133
+ function pendingHint(pending) {
3134
+ return pending === 0 ? "All tasks done" : `${pluralize__default.default("task", pending, !0)} still open`;
3135
+ }
3136
+
3137
+ function overdueTasksHint(overdue) {
3138
+ return `${pluralize__default.default("task", overdue, !0)} past ${overdue === 1 ? "its" : "their"} due date`;
3139
+ }
3140
+
3141
+ const STUCK_RUN_HINT = "This run can’t continue without help";
3142
+
3143
+ function laneOverdueHint(overdue) {
3144
+ return `${overdue} of these ${overdue === 1 ? "has" : "have"} overdue tasks`;
3145
+ }
3146
+
3147
+ function laneStuckHint(stuck) {
3148
+ return `${stuck} of these can’t continue without help`;
3149
+ }
3150
+
2767
3151
  function CardTitleFace({title: title}) {
2768
3152
  return title.kind === "title" ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, {
2769
3153
  children: title.title
@@ -2790,23 +3174,10 @@ function cardTitle(args) {
2790
3174
  };
2791
3175
  }
2792
3176
 
2793
- function ownOpenTasks(instance) {
2794
- const open = workflowEngine.findOpenStageEntry(instance);
2795
- return open ? (open.activities ?? []).filter(activity => index.isOpenActivityStatus(activity.status)).length : 0;
2796
- }
2797
-
2798
- function pendingTaskCounts(instances) {
2799
- return new Map(instances.map(instance => [ instance._id, ownOpenTasks(instance) ]));
2800
- }
2801
-
2802
- function pendingTasksPhrase(pending) {
2803
- if (pending !== void 0) return pending === 0 ? "All tasks done" : pluralize__default.default("pending task", pending, !0);
2804
- }
2805
-
2806
3177
  const CARD_RADIUS = 3;
2807
3178
 
2808
3179
  function BoardCard({card: card, wiring: wiring}) {
2809
- const {onSelect: onSelect, pending: pending, selectedInstanceId: selectedInstanceId, titles: titles} = wiring, {color: color} = ui.useTheme_v2(), selected = card.instanceId === selectedInstanceId;
3180
+ const {facts: facts, onSelect: onSelect, selectedInstanceId: selectedInstanceId, titles: titles} = wiring, {color: color} = ui.useTheme_v2(), selected = card.instanceId === selectedInstanceId, held = facts.get(card.instanceId);
2810
3181
  /* @__PURE__ */
2811
3182
  return jsxRuntime.jsx(ui.Card, {
2812
3183
  "aria-pressed": selected,
@@ -2824,54 +3195,93 @@ function BoardCard({card: card, wiring: wiring}) {
2824
3195
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
2825
3196
  gap: 3,
2826
3197
  children: [
2827
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
2828
- size: 1,
2829
- weight: "medium",
2830
- children: /* @__PURE__ */ jsxRuntime.jsx(CardTitleFace, {
2831
- title: cardTitle({
2832
- face: card.face,
2833
- titles: titles
3198
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
3199
+ align: "flex-start",
3200
+ gap: 2,
3201
+ children: [
3202
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
3203
+ size: 1,
3204
+ style: {
3205
+ flex: 1,
3206
+ minWidth: 0
3207
+ },
3208
+ weight: "medium",
3209
+ children: /* @__PURE__ */ jsxRuntime.jsx(CardTitleFace, {
3210
+ title: cardTitle({
3211
+ face: card.face,
3212
+ titles: titles
3213
+ })
2834
3214
  })
2835
- })
2836
- }),
2837
- /* @__PURE__ */ jsxRuntime.jsx(PendingTasks, {
2838
- pending: pending.get(card.instanceId)
3215
+ }), held?.stuck === !0 ? /* @__PURE__ */ jsxRuntime.jsx(index.AlertGlyph, {
3216
+ Icon: ErrorOutline.ErrorOutlineIcon,
3217
+ hint: STUCK_RUN_HINT,
3218
+ tone: "critical"
3219
+ }) : null ]
3220
+ }), held === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(CardMetrics, {
3221
+ facts: held
2839
3222
  }) ]
2840
3223
  })
2841
3224
  });
2842
3225
  }
2843
3226
 
2844
- function PendingTasks({pending: pending}) {
2845
- const badgeTrim = index.useBadgeCapTrim(), phrase = pendingTasksPhrase(pending);
2846
- return phrase === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, {
3227
+ function CardMetrics({facts: facts}) {
3228
+ return facts.settled ? null : /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
2847
3229
  align: "center",
2848
- style: badgeTrim,
2849
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, {
2850
- padding: 2,
2851
- radius: 2,
2852
- tone: "transparent",
2853
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
2854
- muted: pending === 0,
2855
- size: 1,
2856
- children: phrase
2857
- })
2858
- })
3230
+ gap: 3,
3231
+ children: [
3232
+ /* @__PURE__ */ jsxRuntime.jsx(index.MetricPair, {
3233
+ Icon: Empty.EmptyIcon,
3234
+ empty: facts.pending === 0,
3235
+ face: {
3236
+ text: pendingHint(facts.pending)
3237
+ },
3238
+ value: String(facts.pending)
3239
+ }), facts.overdue === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(index.MetricPair, {
3240
+ Icon: Calendar.CalendarIcon,
3241
+ face: {
3242
+ text: overdueTasksHint(facts.overdue)
3243
+ },
3244
+ tone: "caution",
3245
+ value: String(facts.overdue)
3246
+ }) ]
2859
3247
  });
2860
3248
  }
2861
3249
 
2862
- function StageGroupHeading({group: group}) {
3250
+ function StageGroupHeading({facts: facts, group: group}) {
3251
+ const totals = laneTotals(group.cards, facts);
2863
3252
  /* @__PURE__ */
2864
3253
  return jsxRuntime.jsxs(ui.Flex, {
2865
3254
  align: "center",
2866
3255
  gap: 2,
2867
3256
  children: [
2868
- /* @__PURE__ */ jsxRuntime.jsx(index.CountedLabel, {
3257
+ /* @__PURE__ */ jsxRuntime.jsx(index.CountedHeading, {
2869
3258
  count: group.cards.length,
2870
- label: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
2871
- size: 1,
2872
- weight: "semibold",
2873
- children: group.title
2874
- })
3259
+ title: group.title
3260
+ }), totals.overdue === 0 && totals.stuck === 0 ? null : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, {
3261
+ children: [
3262
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, {
3263
+ flex: 1
3264
+ }),
3265
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
3266
+ align: "center",
3267
+ flex: "none",
3268
+ gap: 3,
3269
+ children: [ totals.overdue === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(index.MetricPair, {
3270
+ Icon: Calendar.CalendarIcon,
3271
+ face: {
3272
+ text: laneOverdueHint(totals.overdue)
3273
+ },
3274
+ tone: "caution",
3275
+ value: String(totals.overdue)
3276
+ }), totals.stuck === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(index.MetricPair, {
3277
+ Icon: ErrorOutline.ErrorOutlineIcon,
3278
+ face: {
3279
+ text: laneStuckHint(totals.stuck)
3280
+ },
3281
+ tone: "critical",
3282
+ value: String(totals.stuck)
3283
+ }) ]
3284
+ }) ]
2875
3285
  }), group.retired ?
2876
3286
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
2877
3287
  muted: !0,
@@ -2920,6 +3330,7 @@ function StageColumn({group: group, leadingRule: leadingRule, trailingRule: trai
2920
3330
  paddingTop: 4,
2921
3331
  paddingX: LANE_PADDING,
2922
3332
  children: /* @__PURE__ */ jsxRuntime.jsx(StageGroupHeading, {
3333
+ facts: wiring.facts,
2923
3334
  group: group
2924
3335
  })
2925
3336
  }), group.cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, {
@@ -3060,7 +3471,7 @@ function useBoard(args) {
3060
3471
  }), {titles: titles, ready: ready} = seeded, board = react.useMemo(() => derived ? orderBoardByPreviewTitle(derived, titles) : void 0, [ derived, titles ]);
3061
3472
  return react.useMemo(() => ({
3062
3473
  board: board,
3063
- ready: ready,
3474
+ ready: board !== void 0 && ready,
3064
3475
  titles: seeded
3065
3476
  }), [ board, ready, seeded ]);
3066
3477
  }
@@ -3167,11 +3578,13 @@ function ActiveCount({face: face}) {
3167
3578
  }
3168
3579
 
3169
3580
  function DefinitionFacts({definition: definition}) {
3170
- const {mappingIssues: mappingIssues, mappings: mappings} = index.useWorkflowContext(), schema = sanity.useSchema(), instances = useToolInstances(), snapshotWidth = index.useContainerToken(2), [row] = definitionCatalog({
3581
+ const {mappingIssues: mappingIssues, mappings: mappings} = index.useWorkflowContext(), schema = sanity.useSchema(), instances = useToolInstances(), now = index.useMinuteClock(), snapshotWidth = index.useContainerToken(2), [row] = definitionCatalog({
3171
3582
  definitions: [ definition ],
3583
+ identity: void 0,
3172
3584
  mappings: mappings,
3173
- inFlight: instances.inFlight,
3174
- issues: mappingIssues
3585
+ instances: [ ...instances.inFlight, ...instances.settled ],
3586
+ issues: mappingIssues,
3587
+ now: now
3175
3588
  }), runsOn = docTypeLabels(row?.docTypes ?? [], name => schema.get(name)?.title).map(docType => docType.title);
3176
3589
  /* @__PURE__ */
3177
3590
  return jsxRuntime.jsxs(ui.Flex, {
@@ -3209,7 +3622,7 @@ function DefinitionFacts({definition: definition}) {
3209
3622
  label: "Active instances",
3210
3623
  children: /* @__PURE__ */ jsxRuntime.jsx(ActiveCount, {
3211
3624
  face: activeCountFace({
3212
- count: row?.activeCount ?? 0,
3625
+ count: row?.signals.activeCount ?? 0,
3213
3626
  error: instances.error,
3214
3627
  loading: instances.loading,
3215
3628
  truncated: instances.truncated
@@ -3277,7 +3690,7 @@ function pickerLabel(face) {
3277
3690
  }
3278
3691
 
3279
3692
  function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
3280
- const empty = options.length === 0;
3693
+ const empty = options.length === 0, reclaimed = -index.useSpaceToken(TOOL_HEADING_PADDING);
3281
3694
  /* @__PURE__ */
3282
3695
  return jsxRuntime.jsx(ui.MenuButton, {
3283
3696
  button: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, {
@@ -3287,6 +3700,9 @@ function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
3287
3700
  iconRight: ChevronDown.ChevronDownIcon,
3288
3701
  mode: "bleed",
3289
3702
  padding: TOOL_HEADING_PADDING,
3703
+ style: {
3704
+ margin: reclaimed
3705
+ },
3290
3706
  text: pickerLabel(face),
3291
3707
  textWeight: "semibold"
3292
3708
  }),
@@ -3317,6 +3733,7 @@ function WorkflowPageHeader({face: face, onSelectPage: onSelectPage, onSelectWor
3317
3733
  children: [
3318
3734
  /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, {
3319
3735
  align: "center",
3736
+ padding: TOOL_HEADING_PADDING,
3320
3737
  children: /* @__PURE__ */ jsxRuntime.jsx(WorkflowPicker, {
3321
3738
  face: face,
3322
3739
  onSelect: onSelectWorkflow,
@@ -3760,20 +4177,28 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3760
4177
  }), instances = useBoardInstances(view.streamName), {board: board, ready: ready, titles: titles} = useBoard({
3761
4178
  definition: selected,
3762
4179
  instances: instances.rows
3763
- }), {select: select, selectedInstanceId: selectedInstanceId} = usePageSelection(page), panel = usePanelPresence(panelSubject({
4180
+ }), {select: select, selectedInstanceId: selectedInstanceId} = usePageSelection(page), now = index.useMinuteClock(), panel = usePanelPresence(panelSubject({
3764
4181
  board: board,
3765
4182
  selectedInstanceId: selectedInstanceId
3766
4183
  })), revealed = useRevealGate({
3767
4184
  loading: instances.loading,
4185
+ progress: titles.resolved.size,
3768
4186
  ready: ready
3769
- }), waiting = index.useDelayedFlag(selection.kind === "loading" && catalogError === void 0, WAIT_CUE_DELAY_MS);
4187
+ });
3770
4188
  if (selection.kind === "loading") /* @__PURE__ */ return jsxRuntime.jsx(UnresolvedPage, {
3771
- error: catalogError,
3772
- waiting: waiting
4189
+ error: catalogError
4190
+ });
4191
+ const catalogEmpty = (definitions ?? []).length === 0;
4192
+ if (selection.kind === "unknown" && catalogEmpty) /* @__PURE__ */
4193
+ return jsxRuntime.jsx(EmptyCatalogPage, {
4194
+ name: selection.name
3773
4195
  });
3774
4196
  const wiring = {
3775
4197
  onSelect: select,
3776
- pending: pendingTaskCounts(instances.rows),
4198
+ facts: cardFacts({
4199
+ instances: instances.rows,
4200
+ now: now
4201
+ }),
3777
4202
  selectedInstanceId: selectedInstanceId,
3778
4203
  titles: titles
3779
4204
  };
@@ -3826,7 +4251,7 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3826
4251
  }),
3827
4252
  /* @__PURE__ */ jsxRuntime.jsx(PageBody, {
3828
4253
  board: board,
3829
- catalogEmpty: (definitions ?? []).length === 0,
4254
+ catalogEmpty: catalogEmpty,
3830
4255
  catalogError: catalogError,
3831
4256
  instancesError: instances.error,
3832
4257
  loading: !revealed,
@@ -3856,30 +4281,45 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3856
4281
  });
3857
4282
  }
3858
4283
 
3859
- const WAIT_CUE_DELAY_MS = 300;
3860
-
3861
- function UnresolvedPage({error: error, waiting: waiting}) {
4284
+ function UnresolvedPage({error: error}) {
3862
4285
  /* @__PURE__ */
3863
4286
  return jsxRuntime.jsx(ui.Flex, {
3864
4287
  direction: "column",
3865
4288
  height: "fill",
3866
4289
  overflow: "hidden",
3867
- children: error === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(WaitingFace, {
3868
- waiting: waiting
3869
- }) : /* @__PURE__ */ jsxRuntime.jsx(ReadErrorFace, {
4290
+ children: error === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(WaitingFace, {}) : /* @__PURE__ */ jsxRuntime.jsx(ReadErrorFace, {
3870
4291
  text: CATALOG_READ_FAILED
3871
4292
  })
3872
4293
  });
3873
4294
  }
3874
4295
 
3875
- function WaitingFace({waiting: waiting}) {
4296
+ function WaitingFace() {
3876
4297
  /* @__PURE__ */
3877
4298
  return jsxRuntime.jsx(StatusBox, {
3878
- children: waiting ? /* @__PURE__ */ jsxRuntime.jsx(TabStatusRow, {
4299
+ children: /* @__PURE__ */ jsxRuntime.jsx(TabStatusRow, {
3879
4300
  children: /* @__PURE__ */ jsxRuntime.jsx(index.LoadingRow, {
3880
4301
  label: "Loading workflow…"
3881
4302
  })
3882
- }) : null
4303
+ })
4304
+ });
4305
+ }
4306
+
4307
+ function EmptyCatalogPage({name: name}) {
4308
+ const face = unknownWorkflowFace({
4309
+ catalogEmpty: !0,
4310
+ name: name
4311
+ });
4312
+ /* @__PURE__ */
4313
+ return jsxRuntime.jsx(ui.Flex, {
4314
+ direction: "column",
4315
+ height: "fill",
4316
+ overflow: "hidden",
4317
+ padding: LANE_PADDING,
4318
+ children: /* @__PURE__ */ jsxRuntime.jsx(index.EmptyState, {
4319
+ description: face.description,
4320
+ icon: /* @__PURE__ */ jsxRuntime.jsx(Search.SearchIcon, {}),
4321
+ title: face.title
4322
+ })
3883
4323
  });
3884
4324
  }
3885
4325
 
@@ -4084,11 +4524,12 @@ function WorkflowsHome({filterSlot: filterSlot, onOpenInstance: onOpenInstance,
4084
4524
 
4085
4525
  function HomePanel({filterSlot: filterSlot, identity: identity, instances: instances, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, tab: tab}) {
4086
4526
  return tab === "overview" ? /* @__PURE__ */ jsxRuntime.jsx(OverviewTab, {
4527
+ identity: identity,
4087
4528
  onOpenWorkflow: onOpenWorkflow
4088
4529
  }) : /* @__PURE__ */ jsxRuntime.jsx(TasksTab, {
4089
4530
  error: instances.error,
4090
4531
  filterSlot: filterSlot,
4091
- identity: identity,
4532
+ identity: index.identityOf(identity),
4092
4533
  instances: [ ...instances.inFlight, ...instances.settled ],
4093
4534
  loading: instances.loading,
4094
4535
  onOpenInstance: onOpenInstance,