@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.
@@ -8,22 +8,28 @@ import { useState, useEffect, useMemo, useRef, useSyncExternalStore, useCallback
8
8
 
9
9
  import { useRouter } from "sanity/router";
10
10
 
11
- import { isLiveEntry, AbortWorkflowDialog, isEvaluationStale, useSpaceToken, DocPreviewLink, SpinnerSlot, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, LoadingRow, InvalidDocNotice, useDefinition, useContainerToken, openActivityGone, InstanceSnapshotBody, ActivityLog, ActivityDetailDialog, instanceBreadcrumb, CodeChip, instanceTitle, namesNoDeployedDefinition, mappingIssueDetail, formatTimeAgo, formatShortDateTime, useWorkflowContext, readDeployedDefinitions, describeError, NoteBanner, EmptyState, HoverHint, DismissablePopover, createSubscribers, useDelayedFlag, definitionSnapshotOf, findActivity, stageTitle, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isRoleAssignedTo, isActivityAssignedTo, assigneesOf, dueDatesOf, datesOf, isOpenActivityStatus, committedRowFace, activityRowProps, gdrLocality, TOAST_ID, WORKFLOW_API_VERSION, useWorkflowToast, openableSchemaType, formatDate, parseStoredDateValue, useUserDisplays, useProjectMembers, WorkflowTaskFiltersApplied, DocRefFace, Hairline, LinkChip, BreadcrumbTail, StageFace, StaleLock, ActivityRow, WorkflowTitleSeedDrifted, UnreadableDocsNote, RoleAssignedNotice, ForMeEmptyState, useBadgeCapTrim, CountedLabel, MetaRow, TabSwitch, WORKFLOW_PAGE_TABS, definitionFingerprint, LogEventOnMount, WorkflowBoardWorkflowSelected, WorkflowDefinitionDetailViewed, toolRoute, landingState, TOOL_TABS, tabSelectionNavigates, WorkflowToolOpened, toolTabState, workflowPageState, instanceState, backToTabLabel, useAssignmentIdentity, workflowState } from "./index.js";
11
+ import { isLiveEntry, AbortWorkflowDialog, useSpaceToken, isEvaluationStale, DocPreviewLink, SpinnerSlot, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, LoadingRow, InvalidDocNotice, useDefinition, useContainerToken, openActivityGone, InstanceFaultNote, InstanceSnapshotBody, ActivityLog, ActivityDetailDialog, instanceBreadcrumb, CodeChip, instanceTitle, dueDatesOf, isDueDatePast, isOpenActivityStatus, definitionSnapshotOf, stageTitle, isActivityAssignedTo, namesNoDeployedDefinition, mappingIssueDetail, formatTimeAgo, formatShortDateTime, useWorkflowContext, readDeployedDefinitions, describeError, gdrLocality, openableSchemaType, identityOf, useMinuteClock, NoteBanner, EmptyState, CountedHeading, AlertGlyph, MetricPair, formatShortAgo, InlineEmphasis, DismissablePopover, createSubscribers, findActivity, stateByActivity, rowDateControlState, rowAssignControlState, isRoleAssignedTo, assigneesOf, datesOf, committedRowFace, activityRowProps, TOAST_ID, WORKFLOW_API_VERSION, useWorkflowToast, formatDate, parseStoredDateValue, useUserDisplays, useProjectMembers, HoverHint, WorkflowTaskFiltersApplied, DocRefFace, Hairline, LinkChip, BreadcrumbTail, StageFace, StaleLock, ActivityRow, WorkflowTitleSeedDrifted, UnreadableDocsNote, RoleAssignedNotice, ForMeEmptyState, MetaRow, TabSwitch, WORKFLOW_PAGE_TABS, definitionFingerprint, LogEventOnMount, WorkflowBoardWorkflowSelected, WorkflowDefinitionDetailViewed, toolRoute, landingState, TOOL_TABS, tabSelectionNavigates, WorkflowToolOpened, toolTabState, workflowPageState, instanceState, backToTabLabel, useAssignmentIdentity, workflowState } from "./index.js";
12
12
 
13
- import { entryDocRefs, startKindOf, isSubjectEntry, isInputSourced, errorMessage, latestDeployedDefinitions, terminalState, isStartableDefinition, toBareId, findOpenStageEntry } from "@sanity/workflow-engine";
13
+ import { entryDocRefs, toBareId, findOpenStageEntry, terminalState, documentStuckCause, startKindOf, isSubjectEntry, isInputSourced, errorMessage, latestDeployedDefinitions, isStartableDefinition, findActivityNode } from "@sanity/workflow-engine";
14
14
 
15
15
  import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
16
16
 
17
- import { TransferIcon } from "@sanity/icons/Transfer";
17
+ import { CalendarIcon } from "@sanity/icons/Calendar";
18
18
 
19
- import { WarningOutlineIcon } from "@sanity/icons/WarningOutline";
19
+ import { ClockIcon } from "@sanity/icons/Clock";
20
20
 
21
- import pluralize from "pluralize-esm";
21
+ import { DocumentIcon } from "@sanity/icons/Document";
22
+
23
+ import { ErrorOutlineIcon } from "@sanity/icons/ErrorOutline";
22
24
 
23
- import { useSchema, useClient, useDocumentPreviewStore, usePerspective } from "sanity";
25
+ import { TransferIcon } from "@sanity/icons/Transfer";
26
+
27
+ import { useDocumentPreviewStore, useSchema, usePerspective, useClient } from "sanity";
24
28
 
25
29
  import { useWorkflowInstances } from "@sanity/workflow-studio";
26
30
 
31
+ import pluralize from "pluralize-esm";
32
+
27
33
  import { AddIcon } from "@sanity/icons/Add";
28
34
 
29
35
  import { SearchIcon } from "@sanity/icons/Search";
@@ -46,6 +52,10 @@ import { ChevronRightIcon } from "@sanity/icons/ChevronRight";
46
52
 
47
53
  import { isReferenceSchemaType } from "@sanity/types";
48
54
 
55
+ import { EmptyIcon } from "@sanity/icons/Empty";
56
+
57
+ import { WarningOutlineIcon } from "@sanity/icons/WarningOutline";
58
+
49
59
  import { WorkflowDiagram } from "@sanity/workflow-diagram";
50
60
 
51
61
  import { useReducedMotion, motion, useSpring, useMotionValueEvent } from "motion/react";
@@ -89,11 +99,22 @@ function documentRefsOf(instance) {
89
99
  return [ ...seen.values() ];
90
100
  }
91
101
 
102
+ function rootInstanceOf(instance, byId) {
103
+ const rootRef = instance.ancestors[0];
104
+ return rootRef ? byId.get(toBareId(rootRef.id)) ?? instance : instance;
105
+ }
106
+
107
+ const TOOL_PANEL_PADDING = 3, TOOL_HEADING_PADDING = 2, PANEL_STEP = 3;
108
+
92
109
  function TabStatusRow({children: children}) {
110
+ const firstLineOffset = useSpaceToken(PANEL_STEP) + useSpaceToken(TOOL_HEADING_PADDING);
93
111
  /* @__PURE__ */
94
112
  return jsx(Box, {
95
- paddingLeft: 2,
96
- paddingY: 3,
113
+ paddingBottom: PANEL_STEP,
114
+ paddingLeft: TOOL_HEADING_PADDING,
115
+ style: {
116
+ paddingTop: firstLineOffset
117
+ },
97
118
  children: children
98
119
  });
99
120
  }
@@ -344,14 +365,24 @@ function InstanceDetailBody({entry: entry, framed: framed, subject: subject}) {
344
365
  },
345
366
  children: /* @__PURE__ */ jsxs(Stack, {
346
367
  gap: 5,
347
- children: [ subject === "preview" ? /* @__PURE__ */ jsx(SubjectPreview, {
368
+ children: [
369
+ /* @__PURE__ */ jsx(InstanceFaultNote, {
370
+ entry: entry,
371
+ scope: {
372
+ kind: "run",
373
+ onOpenActivity: activityName => setOpenActivity({
374
+ stage: instance.currentStage,
375
+ activityName: activityName
376
+ })
377
+ }
378
+ }), subject === "preview" ? /* @__PURE__ */ jsx(SubjectPreview, {
348
379
  instance: instance
349
380
  }) : null,
350
381
  /* @__PURE__ */ jsxs(Stack, {
351
382
  gap: 4,
352
383
  children: [
353
384
  /* @__PURE__ */ jsx(SectionHeading, {
354
- children: "Stages / Tasks"
385
+ children: "Stages"
355
386
  }),
356
387
  /* @__PURE__ */ jsx(InstanceSnapshotBody, {
357
388
  entry: entry,
@@ -433,12 +464,140 @@ function byResolvedTitle(titleOf, keyOf) {
433
464
  };
434
465
  }
435
466
 
467
+ function openActivitiesOf(instance) {
468
+ return (findOpenStageEntry(instance)?.activities ?? []).filter(activity => isOpenActivityStatus(activity.status));
469
+ }
470
+
471
+ function overdueTaskCount(instance, now) {
472
+ return openActivitiesOf(instance).filter(activity => dueDatesOf(activity.fields ?? []).some(due => isDueDatePast(due, now))).length;
473
+ }
474
+
475
+ const NO_SIGNALS = {
476
+ activeCount: 0,
477
+ assignedToMe: 0,
478
+ documentCount: 0,
479
+ documentlessCount: 0,
480
+ initialStageDocumentCount: 0,
481
+ lastTransition: void 0,
482
+ overdueDocumentCount: 0,
483
+ overdueDocumentlessCount: 0,
484
+ stuckCount: 0
485
+ };
486
+
487
+ function noDefinitionSignals() {
488
+ return NO_SIGNALS;
489
+ }
490
+
491
+ function holdsWorkFor(instance, who) {
492
+ return openActivitiesOf(instance).some(activity => isActivityAssignedTo({
493
+ state: activity.fields ?? [],
494
+ who: who
495
+ }));
496
+ }
497
+
498
+ function arrivalOf(instance, byId) {
499
+ const stage = findOpenStageEntry(instance);
500
+ if (stage !== void 0) return {
501
+ at: stage.enteredAt,
502
+ document: documentRefOf(instance, byId),
503
+ stage: stageTitle(definitionSnapshotOf(instance), stage.name)
504
+ };
505
+ }
506
+
507
+ function laterArrival(held, next) {
508
+ return next === void 0 ? held : held === void 0 || next.at > held.at ? next : held;
509
+ }
510
+
511
+ function documentRefOf(instance, byId) {
512
+ return documentRefsOf(rootInstanceOf(instance, byId))[0];
513
+ }
514
+
515
+ const NO_DRAFT = {
516
+ ...NO_SIGNALS,
517
+ documents: /* @__PURE__ */ new Set,
518
+ initialStageDocuments: /* @__PURE__ */ new Set,
519
+ overdueDocuments: /* @__PURE__ */ new Set
520
+ };
521
+
522
+ function withDocument(held, document2) {
523
+ return document2 === void 0 ? held : new Set(held).add(document2);
524
+ }
525
+
526
+ function atInitialStage(instance) {
527
+ const snapshot = definitionSnapshotOf(instance);
528
+ return snapshot !== void 0 && instance.currentStage === snapshot.initialStage;
529
+ }
530
+
531
+ function fold(args) {
532
+ const {byId: byId, held: held, instance: instance, now: now, who: who} = args, stuck = documentStuckCause({
533
+ instance: instance,
534
+ definition: definitionSnapshotOf(instance)
535
+ }), document2 = documentRefOf(instance, byId)?.id, overdue = overdueTaskCount(instance, now) > 0;
536
+ return {
537
+ activeCount: held.activeCount + 1,
538
+ assignedToMe: held.assignedToMe + (who !== void 0 && holdsWorkFor(instance, who) ? 1 : 0),
539
+ documentlessCount: held.documentlessCount + (document2 === void 0 ? 1 : 0),
540
+ documents: withDocument(held.documents, document2),
541
+ initialStageDocuments: atInitialStage(instance) ? withDocument(held.initialStageDocuments, document2) : held.initialStageDocuments,
542
+ lastTransition: laterArrival(held.lastTransition, arrivalOf(instance, byId)),
543
+ overdueDocumentlessCount: held.overdueDocumentlessCount + (overdue && document2 === void 0 ? 1 : 0),
544
+ overdueDocuments: overdue ? withDocument(held.overdueDocuments, document2) : held.overdueDocuments,
545
+ stuckCount: held.stuckCount + (stuck === void 0 ? 0 : 1)
546
+ };
547
+ }
548
+
549
+ function settle(draft) {
550
+ const {documents: documents, initialStageDocuments: initialStageDocuments, overdueDocuments: overdueDocuments, ...rest} = draft;
551
+ return {
552
+ ...rest,
553
+ documentCount: documents.size,
554
+ initialStageDocumentCount: initialStageDocuments.size,
555
+ overdueDocumentCount: overdueDocuments.size
556
+ };
557
+ }
558
+
559
+ function definitionSignals(args) {
560
+ const byId = new Map(args.instances.map(instance => [ instance._id, instance ])), drafts = /* @__PURE__ */ new Map;
561
+ for (const instance of args.instances) {
562
+ if (terminalState(instance) !== "in-flight") continue;
563
+ const held = drafts.get(instance.definition) ?? NO_DRAFT;
564
+ drafts.set(instance.definition, fold({
565
+ byId: byId,
566
+ held: held,
567
+ instance: instance,
568
+ now: args.now,
569
+ who: args.identity
570
+ }));
571
+ }
572
+ return new Map([ ...drafts ].map(([name, draft]) => [ name, settle(draft) ]));
573
+ }
574
+
575
+ function catalogBands(rows) {
576
+ const assigned = rows.filter(row => row.signals.assignedToMe > 0);
577
+ if (assigned.length === 0) return [ {
578
+ rows: rows,
579
+ title: void 0
580
+ } ];
581
+ const rest = rows.filter(row => row.signals.assignedToMe === 0);
582
+ return [ {
583
+ rows: assigned,
584
+ title: "Assigned to you"
585
+ }, {
586
+ rows: rest,
587
+ title: "Other workflows"
588
+ } ].filter(band => band.rows.length > 0);
589
+ }
590
+
436
591
  function definitionCatalog(args) {
437
- const rejected = [ ...args.issues.values() ];
592
+ const rejected = [ ...args.issues.values() ], signals = definitionSignals({
593
+ identity: args.identity,
594
+ instances: args.instances,
595
+ now: args.now
596
+ });
438
597
  return args.definitions.map(definition => ({
439
598
  name: definition.name,
440
599
  title: definition.title ?? definition.name,
441
- activeCount: args.inFlight.filter(instance => instance.definition === definition.name).length,
600
+ signals: signals.get(definition.name) ?? noDefinitionSignals(),
442
601
  docTypes: [ ...new Set(args.mappings.filter(mapping => mapping.definition === definition.name).map(mapping => mapping.docType)) ],
443
602
  brokenBindings: rejected.filter(issue => issue.definition === definition.name && issue.version === definition.version).map(issue => ({
444
603
  docType: issue.docType,
@@ -476,7 +635,7 @@ function deployedPhrase(definition) {
476
635
  return ago === "" ? formatShortDateTime(deployedAt) : `${formatShortDateTime(deployedAt)} (${ago})`;
477
636
  }
478
637
 
479
- const TOOL_PANEL_PADDING = 3, TOOL_HEADING_PADDING = 2, logged = /* @__PURE__ */ new WeakSet;
638
+ const logged = /* @__PURE__ */ new WeakSet;
480
639
 
481
640
  function logReadFailure(what, error) {
482
641
  if (typeof error == "object" && error !== null) {
@@ -514,17 +673,17 @@ function useCappedInstances(filter) {
514
673
  }, [ instances, loading, unreadable, error ]);
515
674
  }
516
675
 
517
- const CATALOG_READ_FAILED = "Couldn’t load the deployed workflows. Try again later.", lastRead = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
676
+ const CATALOG_READ_FAILED = "Couldn’t load the deployed workflows. Try again later.", lastRead$1 = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
518
677
 
519
678
  function useDeployedDefinitions() {
520
- const {engine: engine} = useWorkflowContext(), [definitions, setDefinitions] = useState(() => lastRead.get(engine)?.rows), [error, setError] = useState(void 0);
679
+ const {engine: engine} = useWorkflowContext(), [definitions, setDefinitions] = useState(() => lastRead$1.get(engine)?.rows), [error, setError] = useState(void 0);
521
680
  return useEffect(() => {
522
681
  let cancelled = !1;
523
- setDefinitions(lastRead.get(engine)?.rows), setError(void 0);
682
+ setDefinitions(lastRead$1.get(engine)?.rows), setError(void 0);
524
683
  const seq = (issuedReads.get(engine) ?? 0) + 1;
525
684
  return issuedReads.set(engine, seq), (async () => {
526
- const rows = await readDeployedDefinitions(engine), latest = latestDeployedDefinitions(rows), held = lastRead.get(engine);
527
- (held === void 0 || seq > held.seq) && lastRead.set(engine, {
685
+ const rows = await readDeployedDefinitions(engine), latest = latestDeployedDefinitions(rows), held = lastRead$1.get(engine);
686
+ (held === void 0 || seq > held.seq) && lastRead$1.set(engine, {
528
687
  seq: seq,
529
688
  rows: latest
530
689
  }), cancelled || (setDefinitions(latest), setError(void 0));
@@ -539,13 +698,78 @@ function useDeployedDefinitions() {
539
698
  };
540
699
  }
541
700
 
701
+ const NO_TITLES = {
702
+ titles: /* @__PURE__ */ new Map,
703
+ resolved: /* @__PURE__ */ new Set
704
+ };
705
+
706
+ function useDocPreviewTitles(documents) {
707
+ const previewStore = useDocumentPreviewStore(), schema = useSchema(), {perspectiveStack: perspectiveStack} = usePerspective(), {actualTypes: actualTypes, binding: binding} = useWorkflowContext(), [state, setState] = useState(NO_TITLES), key = documents.map(doc => doc.id).join("|");
708
+ return useEffect(() => {
709
+ const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
710
+ titles: new Map(titles),
711
+ resolved: new Set(resolved)
712
+ }), localDocs = documents.flatMap(doc => {
713
+ const {parsed: parsed, local: local} = gdrLocality(doc.id, binding.contentResource);
714
+ return !parsed || !local ? (resolved.add(doc.id), []) : [ {
715
+ doc: doc,
716
+ bareId: parsed.documentId
717
+ } ];
718
+ }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
719
+ previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
720
+ }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
721
+ const schemaType = openableSchemaType(schema, typeName);
722
+ if (!schemaType) return !1;
723
+ if (previews.get(docId)?.typeName === typeName) return !0;
724
+ dropPreview(docId), resolved.delete(docId);
725
+ const subscription = previewStore.observeForPreview({
726
+ _id: bareId,
727
+ _type: schemaType.name
728
+ }, schemaType, {
729
+ perspective: perspectiveStack
730
+ }).subscribe({
731
+ next: event => {
732
+ resolved.add(docId);
733
+ const title = event.snapshot?.title;
734
+ typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
735
+ },
736
+ error: err => {
737
+ console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
738
+ publish();
739
+ }
740
+ });
741
+ return previews.set(docId, {
742
+ typeName: typeName,
743
+ unsubscribe: () => subscription.unsubscribe()
744
+ }), !0;
745
+ }, evaluate = () => {
746
+ for (const {doc: doc, bareId: bareId} of localDocs) {
747
+ const probe = actualTypes.read(bareId);
748
+ if (probe === null) {
749
+ dropPreview(doc.id), resolved.add(doc.id);
750
+ continue;
751
+ }
752
+ !observe({
753
+ docId: doc.id,
754
+ bareId: bareId,
755
+ typeName: probe ?? doc.type
756
+ }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
757
+ }
758
+ publish();
759
+ }, unsubscribeProbes = actualTypes.subscribe(evaluate);
760
+ return evaluate(), () => {
761
+ unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
762
+ };
763
+ }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
764
+ state;
765
+ }
766
+
542
767
  const INSTANCE_CAP_COUNT_NOTE = `Counts from the latest ${INSTANCE_CAP} workflows — older ones aren’t counted.`, NEWEST_INSTANCES = {
543
768
  includeCompleted: !0
544
- };
769
+ }, lastRead = /* @__PURE__ */ new WeakMap;
545
770
 
546
771
  function useToolInstances() {
547
- const {rows: rows, truncated: truncated, loading: loading, unreadable: unreadable, error: error} = useCappedInstances(NEWEST_INSTANCES);
548
- return useMemo(() => ({
772
+ const {engine: engine} = useWorkflowContext(), {rows: rows, truncated: truncated, loading: loading, unreadable: unreadable, error: error} = useCappedInstances(NEWEST_INSTANCES), fresh = useMemo(() => ({
549
773
  inFlight: rows.filter(instance => terminalState(instance) === "in-flight"),
550
774
  settled: rows.filter(instance => terminalState(instance) !== "in-flight"),
551
775
  truncated: truncated,
@@ -553,41 +777,118 @@ function useToolInstances() {
553
777
  unreadable: unreadable,
554
778
  error: error
555
779
  }), [ rows, truncated, loading, unreadable, error ]);
780
+ return useEffect(() => {
781
+ !fresh.loading && fresh.error === void 0 && lastRead.set(engine, fresh);
782
+ }, [ engine, fresh ]), loading ? lastRead.get(engine) ?? fresh : fresh;
556
783
  }
557
784
 
558
- function activeSummary(active) {
559
- return active === 0 ? "No active instances" : pluralize("active instance", active, !0);
785
+ function documentPhrase(count) {
786
+ return count === 0 ? "No documents" : pluralize("document", count, !0);
560
787
  }
561
788
 
562
- function activeHeadline(args) {
563
- if (args.error === void 0) return args.loading ? "counting active instances…" : activeSummary(args.active);
789
+ function documentlessTail(count) {
790
+ return count === 0 ? "" : ` (${pluralize("instance", count, !0)})`;
564
791
  }
565
792
 
566
- const CARD_MIN_WIDTH = 280, CARD_RADIUS$1 = 3, CARD_PADDING = 4, CARD_MIN_HEIGHT = 180;
793
+ function documentlessNote(work) {
794
+ return work.documentCount > 0 ? "" : documentlessTail(work.documentlessCount);
795
+ }
567
796
 
568
- function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
569
- const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), undeployed = /* @__PURE__ */ jsx(UndeployedWorkflows, {
797
+ function documentFace(work, atStart) {
798
+ const lead = `${documentPhrase(work.documentCount)} with an active workflow${documentlessNote(work)}`;
799
+ return atStart === 0 ? {
800
+ lead: lead,
801
+ startLine: void 0
802
+ } : atStart === work.documentCount ? {
803
+ lead: `${lead}, yet to leave the starting stage`,
804
+ startLine: void 0
805
+ } : {
806
+ lead: lead,
807
+ startLine: `${atStart} still at the starting stage`
808
+ };
809
+ }
810
+
811
+ function overdueHint(work) {
812
+ return `${documentPhrase(work.documentCount)} with overdue tasks${documentlessTail(work.documentlessCount)}`;
813
+ }
814
+
815
+ const UNTITLED_DOCUMENT = "Untitled", TRANSITION_HINT_PENDING = "Loading…";
816
+
817
+ function transitionHeading(at) {
818
+ return `Last stage change at ${formatShortDateTime(at)}`;
819
+ }
820
+
821
+ function transitionFace(args) {
822
+ return args.documentPending ? {
823
+ state: "pending"
824
+ } : {
825
+ state: "ready",
826
+ document: args.documentTitle,
827
+ heading: transitionHeading(args.at),
828
+ lead: args.documentTitle === void 0 ? "Changed to" : "changed to",
829
+ stage: args.stage
830
+ };
831
+ }
832
+
833
+ function catalogView(args) {
834
+ if (args.error !== void 0) return {
835
+ kind: "unreadable"
836
+ };
837
+ if (args.definitions === void 0 || args.instances.loading || args.identity.kind === "pending") return {
838
+ kind: "loading"
839
+ };
840
+ if (args.definitions.length === 0) return {
841
+ kind: "empty"
842
+ };
843
+ const rows = definitionCatalog({
844
+ definitions: args.definitions,
845
+ identity: identityOf(args.identity),
846
+ instances: [ ...args.instances.inFlight, ...args.instances.settled ],
847
+ issues: args.issues,
848
+ mappings: args.mappings,
849
+ now: args.now
850
+ });
851
+ return {
852
+ kind: "bands",
853
+ bands: catalogBands(rows)
854
+ };
855
+ }
856
+
857
+ function definitionAlert(row) {
858
+ if (row.signals.stuckCount !== 0) return {
859
+ hint: `${pluralize("run", row.signals.stuckCount, !0)} here can’t continue without help`
860
+ };
861
+ }
862
+
863
+ const CARD_MIN_WIDTH = 280, CARD_RADIUS$1 = 3, CARD_PADDING = 4, HINT_DOCUMENT_MAX_WIDTH = 200, BAND_GAP = 6, CARD_MIN_HEIGHT = 120;
864
+
865
+ function OverviewTab({identity: identity, onOpenWorkflow: onOpenWorkflow}) {
866
+ const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), now = useMinuteClock(), view = catalogView({
867
+ definitions: definitions,
868
+ error: error,
869
+ identity: identity,
870
+ instances: instances,
871
+ issues: mappingIssues,
872
+ mappings: mappings,
873
+ now: now
874
+ }), undeployed = /* @__PURE__ */ jsx(UndeployedWorkflows, {
570
875
  notices: undeployedWorkflowNotices({
571
876
  issues: mappingIssues,
572
877
  titleOf: name => schema.get(name)?.title
573
878
  })
574
879
  });
575
- if (error !== void 0) /* @__PURE__ */ return jsx(NoteBanner, {
880
+ return view.kind === "unreadable" ? /* @__PURE__ */ jsx(NoteBanner, {
576
881
  label: CATALOG_READ_FAILED,
577
882
  tone: "critical"
578
- });
579
- if (definitions === void 0) /* @__PURE__ */
580
- return jsx(TabStatusRow, {
883
+ }) : view.kind === "loading" ? /* @__PURE__ */ jsx(TabStatusRow, {
581
884
  children: /* @__PURE__ */ jsx(LoadingRow, {
582
885
  label: "Loading workflows…"
583
886
  })
584
- });
585
- if (definitions.length === 0) /* @__PURE__ */
586
- return jsxs(Flex, {
887
+ }) : view.kind === "empty" ? /* @__PURE__ */ jsxs(Flex, {
587
888
  direction: "column",
588
889
  flex: 1,
589
890
  gap: 3,
590
- paddingBottom: 4,
891
+ paddingBottom: 3,
591
892
  paddingTop: 3,
592
893
  children: [ undeployed,
593
894
  /* @__PURE__ */ jsx(EmptyState, {
@@ -595,19 +896,7 @@ function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
595
896
  icon: /* @__PURE__ */ jsx(TransferIcon, {}),
596
897
  title: "No workflows deployed"
597
898
  }) ]
598
- });
599
- const rows = definitionCatalog({
600
- definitions: definitions,
601
- mappings: mappings,
602
- inFlight: instances.inFlight,
603
- issues: mappingIssues
604
- }), active = rows.reduce((sum, row) => sum + row.activeCount, 0), activity = activeHeadline({
605
- active: active,
606
- error: instances.error,
607
- loading: instances.loading
608
- });
609
- /* @__PURE__ */
610
- return jsxs(Stack, {
899
+ }) : /* @__PURE__ */ jsxs(Stack, {
611
900
  gap: 3,
612
901
  paddingBottom: 4,
613
902
  paddingTop: 3,
@@ -615,39 +904,48 @@ function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
615
904
  label: INSTANCES_READ_FAILED,
616
905
  tone: "critical"
617
906
  }),
618
- /* @__PURE__ */ jsxs(Flex, {
619
- align: "center",
620
- gap: 2,
621
- padding: TOOL_HEADING_PADDING,
622
- children: [
623
- /* @__PURE__ */ jsx(Text, {
624
- size: 1,
625
- weight: "semibold",
626
- children: pluralize("workflow", rows.length, !0)
627
- }), activity === void 0 ? null : /* @__PURE__ */ jsx(Text, {
628
- muted: !0,
629
- size: 1,
630
- children: activity
631
- }) ]
632
- }),
633
- /* @__PURE__ */ jsx(Grid, {
634
- gap: 3,
635
- style: {
636
- gridAutoRows: `minmax(${CARD_MIN_HEIGHT}px, auto)`,
637
- gridTemplateColumns: `repeat(auto-fill, minmax(${CARD_MIN_WIDTH}px, 1fr))`
638
- },
639
- children: rows.map(row => /* @__PURE__ */ jsx(WorkflowCard, {
640
- countKnown: instances.error === void 0 && !instances.loading,
641
- onOpen: () => onOpenWorkflow(row.name),
642
- row: row
643
- }, row.name))
907
+ /* @__PURE__ */ jsx(Stack, {
908
+ gap: BAND_GAP,
909
+ children: view.bands.map(band => /* @__PURE__ */ jsxs(Stack, {
910
+ gap: 3,
911
+ children: [ band.title === void 0 ? null : /* @__PURE__ */ jsx(Box, {
912
+ padding: TOOL_HEADING_PADDING,
913
+ children: /* @__PURE__ */ jsx(CountedHeading, {
914
+ count: band.rows.length,
915
+ title: band.title
916
+ })
917
+ }),
918
+ /* @__PURE__ */ jsx(CardGrid, {
919
+ countKnown: instances.error === void 0,
920
+ now: now,
921
+ onOpenWorkflow: onOpenWorkflow,
922
+ rows: band.rows
923
+ }) ]
924
+ }, band.title ?? UNBANDED_KEY))
644
925
  }), instances.truncated ? /* @__PURE__ */ jsx(MutedNote, {
645
926
  text: INSTANCE_CAP_COUNT_NOTE
646
927
  }) : null ]
647
928
  });
648
929
  }
649
930
 
650
- const CARD_ISSUES_HINT = "This workflow has issues";
931
+ const UNBANDED_KEY = "all";
932
+
933
+ function CardGrid({countKnown: countKnown, now: now, onOpenWorkflow: onOpenWorkflow, rows: rows}) {
934
+ /* @__PURE__ */
935
+ return jsx(Grid, {
936
+ gap: 3,
937
+ style: {
938
+ gridAutoRows: `minmax(${CARD_MIN_HEIGHT}px, auto)`,
939
+ gridTemplateColumns: `repeat(auto-fill, minmax(${CARD_MIN_WIDTH}px, 1fr))`
940
+ },
941
+ children: rows.map(row => /* @__PURE__ */ jsx(WorkflowCard, {
942
+ countKnown: countKnown,
943
+ now: now,
944
+ onOpen: () => onOpenWorkflow(row.name),
945
+ row: row
946
+ }, row.name))
947
+ });
948
+ }
651
949
 
652
950
  function UndeployedWorkflows({notices: notices}) {
653
951
  return notices.length === 0 ? null :
@@ -660,8 +958,104 @@ function UndeployedWorkflows({notices: notices}) {
660
958
  });
661
959
  }
662
960
 
663
- function WorkflowCard({countKnown: countKnown, onOpen: onOpen, row: row}) {
664
- const flagged = row.brokenBindings.length > 0, card = /* @__PURE__ */ jsx(Card, {
961
+ function LastTransitionHint({transition: transition}) {
962
+ const documents = useMemo(() => transition.document === void 0 ? [] : [ transition.document ], [ transition.document ]), {resolved: resolved, titles: titles} = useDocPreviewTitles(documents), id = transition.document?.id, face = transitionFace({
963
+ at: transition.at,
964
+ documentPending: id !== void 0 && !resolved.has(id),
965
+ documentTitle: id === void 0 ? void 0 : titles.get(id) ?? UNTITLED_DOCUMENT,
966
+ stage: transition.stage
967
+ });
968
+ return face.state === "pending" ? /* @__PURE__ */ jsx(Text, {
969
+ muted: !0,
970
+ size: 1,
971
+ children: TRANSITION_HINT_PENDING
972
+ }) :
973
+ /* @__PURE__ */ jsxs(Stack, {
974
+ gap: 3,
975
+ children: [
976
+ /* @__PURE__ */ jsx(Text, {
977
+ muted: !0,
978
+ size: 1,
979
+ children: face.heading
980
+ }),
981
+ /* @__PURE__ */ jsxs(Text, {
982
+ size: 1,
983
+ textOverflow: "ellipsis",
984
+ children: [ face.document === void 0 ? null : /* @__PURE__ */ jsx(HintDocument, {
985
+ children: face.document
986
+ }), face.document === void 0 ? "" : " ", face.lead, " ",
987
+ /* @__PURE__ */ jsx(InlineEmphasis, {
988
+ children: face.stage
989
+ }) ]
990
+ }) ]
991
+ });
992
+ }
993
+
994
+ function HintDocument({children: children}) {
995
+ /* @__PURE__ */
996
+ return jsx("span", {
997
+ style: {
998
+ display: "inline-block",
999
+ maxWidth: HINT_DOCUMENT_MAX_WIDTH,
1000
+ overflow: "hidden",
1001
+ textOverflow: "ellipsis",
1002
+ verticalAlign: "bottom",
1003
+ whiteSpace: "nowrap"
1004
+ },
1005
+ children: /* @__PURE__ */ jsx(InlineEmphasis, {
1006
+ children: children
1007
+ })
1008
+ });
1009
+ }
1010
+
1011
+ function overdueWork(signals) {
1012
+ return {
1013
+ documentCount: signals.overdueDocumentCount,
1014
+ documentlessCount: signals.overdueDocumentlessCount
1015
+ };
1016
+ }
1017
+
1018
+ function CardMetrics$1({now: now, signals: signals}) {
1019
+ const overdue = overdueWork(signals), overdueTotal = overdue.documentCount + overdue.documentlessCount, documents = documentFace(signals, signals.initialStageDocumentCount);
1020
+ /* @__PURE__ */
1021
+ return jsxs(Flex, {
1022
+ align: "center",
1023
+ gap: 3,
1024
+ children: [
1025
+ /* @__PURE__ */ jsx(MetricPair, {
1026
+ Icon: DocumentIcon,
1027
+ empty: signals.documentCount === 0 && signals.documentlessCount === 0,
1028
+ face: {
1029
+ text: documents.lead,
1030
+ ...documents.startLine === void 0 ? {} : {
1031
+ description: documents.startLine
1032
+ }
1033
+ },
1034
+ value: String(signals.documentCount)
1035
+ }), signals.lastTransition === void 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
1036
+ Icon: ClockIcon,
1037
+ face: {
1038
+ content: /* @__PURE__ */ jsx(LastTransitionHint, {
1039
+ transition: signals.lastTransition
1040
+ }),
1041
+ wide: !0
1042
+ },
1043
+ value: formatShortAgo(signals.lastTransition.at, now)
1044
+ }), overdueTotal === 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
1045
+ Icon: CalendarIcon,
1046
+ face: {
1047
+ text: overdueHint(overdue)
1048
+ },
1049
+ tone: "caution",
1050
+ value: String(overdueTotal)
1051
+ }) ]
1052
+ });
1053
+ }
1054
+
1055
+ function WorkflowCard({countKnown: countKnown, now: now, onOpen: onOpen, row: row}) {
1056
+ const alert = definitionAlert(row);
1057
+ /* @__PURE__ */
1058
+ return jsxs(Card, {
665
1059
  as: "button",
666
1060
  onClick: onOpen,
667
1061
  padding: CARD_PADDING,
@@ -674,50 +1068,44 @@ function WorkflowCard({countKnown: countKnown, onOpen: onOpen, row: row}) {
674
1068
  textAlign: "left",
675
1069
  width: "100%"
676
1070
  },
677
- ...flagged ? {
678
- tone: "caution"
679
- } : {},
680
- children: /* @__PURE__ */ jsxs(Flex, {
1071
+ children: [
1072
+ /* @__PURE__ */ jsxs(Flex, {
681
1073
  align: "flex-start",
682
- gap: 3,
683
- children: [ flagged ? /* @__PURE__ */ jsx(Text, {
684
- size: 1,
685
- children: /* @__PURE__ */ jsx(WarningOutlineIcon, {})
686
- }) : null,
687
- /* @__PURE__ */ jsxs(Stack, {
1074
+ gap: 2,
1075
+ style: {
688
1076
  flex: 1,
689
- gap: 3,
1077
+ minWidth: 0
1078
+ },
1079
+ children: [
1080
+ /* @__PURE__ */ jsx(Text, {
1081
+ size: 1,
690
1082
  style: {
1083
+ flex: 1,
691
1084
  minWidth: 0
692
1085
  },
693
- children: [
694
- /* @__PURE__ */ jsx(Text, {
695
- size: 1,
696
- weight: "semibold",
697
- children: row.title
698
- }), countKnown ? /* @__PURE__ */ jsx(Text, {
699
- muted: !0,
700
- size: 1,
701
- children: activeSummary(row.activeCount)
702
- }) : null ]
1086
+ weight: "medium",
1087
+ children: row.title
1088
+ }), alert === void 0 ? null : /* @__PURE__ */ jsx(AlertGlyph, {
1089
+ Icon: ErrorOutlineIcon,
1090
+ hint: alert.hint,
1091
+ tone: "critical"
703
1092
  }) ]
704
- })
1093
+ }), countKnown ? /* @__PURE__ */ jsx(CardMetrics$1, {
1094
+ now: now,
1095
+ signals: row.signals
1096
+ }) : null ]
705
1097
  });
706
- return flagged ? /* @__PURE__ */ jsx(HoverHint, {
707
- fill: !0,
708
- text: CARD_ISSUES_HINT,
709
- children: card
710
- }) : card;
711
1098
  }
712
1099
 
713
1100
  function useStartableDefinitions() {
714
- const {definitions: definitions, error: error} = useDeployedDefinitions();
1101
+ const {definitions: definitions, error: error} = useDeployedDefinitions(), startable = useMemo(() => definitions?.filter(row => isStartableDefinition(row)).map(row => ({
1102
+ name: row.name,
1103
+ title: row.title ?? row.name,
1104
+ description: row.description
1105
+ })), [ definitions ]);
715
1106
  return {
716
- definitions: useMemo(() => definitions?.filter(row => isStartableDefinition(row)).map(row => ({
717
- name: row.name,
718
- title: row.title ?? row.name,
719
- description: row.description
720
- })), [ definitions ]),
1107
+ catalogEmpty: definitions?.length === 0,
1108
+ definitions: startable,
721
1109
  error: error
722
1110
  };
723
1111
  }
@@ -749,16 +1137,45 @@ function NewWorkflowButton() {
749
1137
  });
750
1138
  }
751
1139
 
1140
+ const PANEL_WIDTH = 340;
1141
+
1142
+ function NothingToStart({catalogEmpty: catalogEmpty}) {
1143
+ /* @__PURE__ */
1144
+ return jsx(Box, {
1145
+ padding: 4,
1146
+ style: {
1147
+ width: PANEL_WIDTH
1148
+ },
1149
+ children: /* @__PURE__ */ jsxs(Stack, {
1150
+ gap: 3,
1151
+ children: [
1152
+ /* @__PURE__ */ jsx(Text, {
1153
+ align: "center",
1154
+ size: 1,
1155
+ weight: "medium",
1156
+ children: catalogEmpty ? "No workflows deployed" : "Nothing to start by hand"
1157
+ }),
1158
+ /* @__PURE__ */ jsx(Text, {
1159
+ align: "center",
1160
+ muted: !0,
1161
+ size: 1,
1162
+ 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"
1163
+ }) ]
1164
+ })
1165
+ });
1166
+ }
1167
+
752
1168
  function DefinitionPickerPanel({onPick: onPick}) {
753
- const {definitions: definitions, error: error} = useStartableDefinitions(), [query, setQuery] = useState(""), visible = useMemo(() => {
1169
+ const {catalogEmpty: catalogEmpty, definitions: definitions, error: error} = useStartableDefinitions(), [query, setQuery] = useState(""), visible = useMemo(() => {
754
1170
  const q = query.trim().toLowerCase();
755
1171
  return definitions ? q ? definitions.filter(d => `${d.title} ${d.name}`.toLowerCase().includes(q)) : definitions : [];
756
1172
  }, [ definitions, query ]), settled = error === void 0 ? definitions : void 0;
757
- /* @__PURE__ */
758
- return jsx(Box, {
1173
+ return settled?.length === 0 ? /* @__PURE__ */ jsx(NothingToStart, {
1174
+ catalogEmpty: catalogEmpty
1175
+ }) : /* @__PURE__ */ jsx(Box, {
759
1176
  padding: 2,
760
1177
  style: {
761
- width: 340
1178
+ width: PANEL_WIDTH
762
1179
  },
763
1180
  children: /* @__PURE__ */ jsxs(Stack, {
764
1181
  gap: 2,
@@ -775,8 +1192,6 @@ function DefinitionPickerPanel({onPick: onPick}) {
775
1192
  tone: "critical"
776
1193
  }) : null, !definitions && !error ? /* @__PURE__ */ jsx(LoadingRow, {
777
1194
  label: "Loading workflows…"
778
- }) : null, settled?.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
779
- text: "No startable workflows are deployed"
780
1195
  }) : null, settled !== void 0 && settled.length > 0 && visible.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
781
1196
  text: "No workflows match"
782
1197
  }) : null,
@@ -1055,13 +1470,20 @@ function useBandOpen(bands, key) {
1055
1470
  };
1056
1471
  }
1057
1472
 
1058
- const REVEAL_MAX_WAIT_MS = 2e3;
1473
+ const REVEAL_STALL_MS = 2e3, REVEAL_COLD_MS = 15e3;
1059
1474
 
1060
1475
  function useRevealGate(args) {
1061
- const {loading: loading, ready: ready} = args, settled = !loading && ready, expired = useDelayedFlag(!loading && !ready, REVEAL_MAX_WAIT_MS), [revealed, setRevealed] = useState(!1);
1476
+ const {loading: loading, progress: progress, ready: ready} = args, settled = !loading && ready, [stalled, setStalled] = useState(!1);
1477
+ useEffect(() => {
1478
+ if (loading || ready) return;
1479
+ setStalled(!1);
1480
+ const budget = progress > 0 ? REVEAL_STALL_MS : REVEAL_COLD_MS, timer = setTimeout(() => setStalled(!0), budget);
1481
+ return () => clearTimeout(timer);
1482
+ }, [ loading, progress, ready ]);
1483
+ const [revealed, setRevealed] = useState(!1);
1062
1484
  return useEffect(() => {
1063
- !revealed && (settled || expired) && setRevealed(!0);
1064
- }, [ expired, revealed, settled ]), revealed || settled || expired;
1485
+ !revealed && (settled || stalled) && setRevealed(!0);
1486
+ }, [ revealed, settled, stalled ]), revealed || settled || stalled;
1065
1487
  }
1066
1488
 
1067
1489
  function spawnOrigins(instances) {
@@ -1074,11 +1496,6 @@ function spawnOrigins(instances) {
1074
1496
  return origins;
1075
1497
  }
1076
1498
 
1077
- function rootOf(instance, byId) {
1078
- const rootRef = instance.ancestors[0];
1079
- return rootRef ? byId.get(toBareId(rootRef.id)) ?? instance : instance;
1080
- }
1081
-
1082
1499
  function taskRowsOf(args) {
1083
1500
  const {instance: instance, identity: identity} = args, open = findOpenStageEntry(instance);
1084
1501
  if (!open) return [];
@@ -1138,7 +1555,7 @@ function deriveTaskGroups(args) {
1138
1555
  activityName: origin.activityName
1139
1556
  }).title : void 0,
1140
1557
  rows: rows
1141
- }, root = rootOf(instance, byId), documents = documentRefsOf(root), document2 = documents[0];
1558
+ }, root = rootInstanceOf(instance, byId), documents = documentRefsOf(root), document2 = documents[0];
1142
1559
  if (!document2) {
1143
1560
  withoutDocuments.push(entry);
1144
1561
  continue;
@@ -1444,72 +1861,6 @@ function SettleOrphansDialog({workflows: workflows, onClose: onClose}) {
1444
1861
  });
1445
1862
  }
1446
1863
 
1447
- const NO_TITLES = {
1448
- titles: /* @__PURE__ */ new Map,
1449
- resolved: /* @__PURE__ */ new Set
1450
- };
1451
-
1452
- function useDocPreviewTitles(documents) {
1453
- const previewStore = useDocumentPreviewStore(), schema = useSchema(), {perspectiveStack: perspectiveStack} = usePerspective(), {actualTypes: actualTypes, binding: binding} = useWorkflowContext(), [state, setState] = useState(NO_TITLES), key = documents.map(doc => doc.id).join("|");
1454
- return useEffect(() => {
1455
- const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
1456
- titles: new Map(titles),
1457
- resolved: new Set(resolved)
1458
- }), localDocs = documents.flatMap(doc => {
1459
- const {parsed: parsed, local: local} = gdrLocality(doc.id, binding.contentResource);
1460
- return !parsed || !local ? (resolved.add(doc.id), []) : [ {
1461
- doc: doc,
1462
- bareId: parsed.documentId
1463
- } ];
1464
- }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
1465
- previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
1466
- }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
1467
- const schemaType = openableSchemaType(schema, typeName);
1468
- if (!schemaType) return !1;
1469
- if (previews.get(docId)?.typeName === typeName) return !0;
1470
- dropPreview(docId), resolved.delete(docId);
1471
- const subscription = previewStore.observeForPreview({
1472
- _id: bareId,
1473
- _type: schemaType.name
1474
- }, schemaType, {
1475
- perspective: perspectiveStack
1476
- }).subscribe({
1477
- next: event => {
1478
- resolved.add(docId);
1479
- const title = event.snapshot?.title;
1480
- typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
1481
- },
1482
- error: err => {
1483
- console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
1484
- publish();
1485
- }
1486
- });
1487
- return previews.set(docId, {
1488
- typeName: typeName,
1489
- unsubscribe: () => subscription.unsubscribe()
1490
- }), !0;
1491
- }, evaluate = () => {
1492
- for (const {doc: doc, bareId: bareId} of localDocs) {
1493
- const probe = actualTypes.read(bareId);
1494
- if (probe === null) {
1495
- dropPreview(doc.id), resolved.add(doc.id);
1496
- continue;
1497
- }
1498
- !observe({
1499
- docId: doc.id,
1500
- bareId: bareId,
1501
- typeName: probe ?? doc.type
1502
- }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
1503
- }
1504
- publish();
1505
- }, unsubscribeProbes = actualTypes.subscribe(evaluate);
1506
- return evaluate(), () => {
1507
- unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
1508
- };
1509
- }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
1510
- state;
1511
- }
1512
-
1513
1864
  function taskFilterOptions(groups) {
1514
1865
  const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
1515
1866
  for (const group of groups.withDocuments) {
@@ -2687,6 +3038,7 @@ function TasksTab({error: error, filterSlot: filterSlot, instances: instances, l
2687
3038
  identity: identity
2688
3039
  }), [ instances, identity ]), orphans = useOrphanedWorkflows(groups), listed = useMemo(() => hideOrphanedGroups(groups, orphans.groupKeys), [ groups, orphans.groupKeys ]), options = useMemo(() => taskFilterOptions(listed), [ listed ]), titles = useGroupTitles(listed), revealed = useRevealGate({
2689
3040
  loading: loading,
3041
+ progress: titles.resolved.size,
2690
3042
  ready: titles.ready
2691
3043
  }), visible = useMemo(() => {
2692
3044
  const filtered = applyTaskFilters({
@@ -2746,6 +3098,30 @@ function TasksTab({error: error, filterSlot: filterSlot, instances: instances, l
2746
3098
  });
2747
3099
  }
2748
3100
 
3101
+ function cardFacts(args) {
3102
+ return new Map(args.instances.map(instance => [ instance._id, {
3103
+ pending: openActivitiesOf(instance).length,
3104
+ overdue: overdueTaskCount(instance, args.now),
3105
+ stuck: documentStuckCause({
3106
+ instance: instance,
3107
+ definition: definitionSnapshotOf(instance)
3108
+ }) !== void 0,
3109
+ settled: terminalState(instance) !== "in-flight"
3110
+ } ]));
3111
+ }
3112
+
3113
+ function laneTotals(cards, facts) {
3114
+ let overdue = 0, stuck = 0;
3115
+ for (const card of cards) {
3116
+ const held = facts.get(card.instanceId);
3117
+ held !== void 0 && (held.overdue > 0 && (overdue += 1), held.stuck && (stuck += 1));
3118
+ }
3119
+ return {
3120
+ overdue: overdue,
3121
+ stuck: stuck
3122
+ };
3123
+ }
3124
+
2749
3125
  const BOARD_EMPTY_STAGE_NOTE = "No items";
2750
3126
 
2751
3127
  function faceOf(instance) {
@@ -2804,6 +3180,24 @@ function boardDocuments(board) {
2804
3180
  return board.groups.flatMap(group => group.cards.flatMap(card => card.face.kind === "document" ? [ card.face.document ] : []));
2805
3181
  }
2806
3182
 
3183
+ function pendingHint(pending) {
3184
+ return pending === 0 ? "All tasks done" : `${pluralize("task", pending, !0)} still open`;
3185
+ }
3186
+
3187
+ function overdueTasksHint(overdue) {
3188
+ return `${pluralize("task", overdue, !0)} past ${overdue === 1 ? "its" : "their"} due date`;
3189
+ }
3190
+
3191
+ const STUCK_RUN_HINT = "This run can’t continue without help";
3192
+
3193
+ function laneOverdueHint(overdue) {
3194
+ return `${overdue} of these ${overdue === 1 ? "has" : "have"} overdue tasks`;
3195
+ }
3196
+
3197
+ function laneStuckHint(stuck) {
3198
+ return `${stuck} of these can’t continue without help`;
3199
+ }
3200
+
2807
3201
  function CardTitleFace({title: title}) {
2808
3202
  return title.kind === "title" ? /* @__PURE__ */ jsx(Fragment, {
2809
3203
  children: title.title
@@ -2830,23 +3224,10 @@ function cardTitle(args) {
2830
3224
  };
2831
3225
  }
2832
3226
 
2833
- function ownOpenTasks(instance) {
2834
- const open = findOpenStageEntry(instance);
2835
- return open ? (open.activities ?? []).filter(activity => isOpenActivityStatus(activity.status)).length : 0;
2836
- }
2837
-
2838
- function pendingTaskCounts(instances) {
2839
- return new Map(instances.map(instance => [ instance._id, ownOpenTasks(instance) ]));
2840
- }
2841
-
2842
- function pendingTasksPhrase(pending) {
2843
- if (pending !== void 0) return pending === 0 ? "All tasks done" : pluralize("pending task", pending, !0);
2844
- }
2845
-
2846
3227
  const CARD_RADIUS = 3;
2847
3228
 
2848
3229
  function BoardCard({card: card, wiring: wiring}) {
2849
- const {onSelect: onSelect, pending: pending, selectedInstanceId: selectedInstanceId, titles: titles} = wiring, {color: color} = useTheme_v2(), selected = card.instanceId === selectedInstanceId;
3230
+ const {facts: facts, onSelect: onSelect, selectedInstanceId: selectedInstanceId, titles: titles} = wiring, {color: color} = useTheme_v2(), selected = card.instanceId === selectedInstanceId, held = facts.get(card.instanceId);
2850
3231
  /* @__PURE__ */
2851
3232
  return jsx(Card, {
2852
3233
  "aria-pressed": selected,
@@ -2864,54 +3245,93 @@ function BoardCard({card: card, wiring: wiring}) {
2864
3245
  children: /* @__PURE__ */ jsxs(Stack, {
2865
3246
  gap: 3,
2866
3247
  children: [
2867
- /* @__PURE__ */ jsx(Text, {
2868
- size: 1,
2869
- weight: "medium",
2870
- children: /* @__PURE__ */ jsx(CardTitleFace, {
2871
- title: cardTitle({
2872
- face: card.face,
2873
- titles: titles
3248
+ /* @__PURE__ */ jsxs(Flex, {
3249
+ align: "flex-start",
3250
+ gap: 2,
3251
+ children: [
3252
+ /* @__PURE__ */ jsx(Text, {
3253
+ size: 1,
3254
+ style: {
3255
+ flex: 1,
3256
+ minWidth: 0
3257
+ },
3258
+ weight: "medium",
3259
+ children: /* @__PURE__ */ jsx(CardTitleFace, {
3260
+ title: cardTitle({
3261
+ face: card.face,
3262
+ titles: titles
3263
+ })
2874
3264
  })
2875
- })
2876
- }),
2877
- /* @__PURE__ */ jsx(PendingTasks, {
2878
- pending: pending.get(card.instanceId)
3265
+ }), held?.stuck === !0 ? /* @__PURE__ */ jsx(AlertGlyph, {
3266
+ Icon: ErrorOutlineIcon,
3267
+ hint: STUCK_RUN_HINT,
3268
+ tone: "critical"
3269
+ }) : null ]
3270
+ }), held === void 0 ? null : /* @__PURE__ */ jsx(CardMetrics, {
3271
+ facts: held
2879
3272
  }) ]
2880
3273
  })
2881
3274
  });
2882
3275
  }
2883
3276
 
2884
- function PendingTasks({pending: pending}) {
2885
- const badgeTrim = useBadgeCapTrim(), phrase = pendingTasksPhrase(pending);
2886
- return phrase === void 0 ? null : /* @__PURE__ */ jsx(Flex, {
3277
+ function CardMetrics({facts: facts}) {
3278
+ return facts.settled ? null : /* @__PURE__ */ jsxs(Flex, {
2887
3279
  align: "center",
2888
- style: badgeTrim,
2889
- children: /* @__PURE__ */ jsx(Card, {
2890
- padding: 2,
2891
- radius: 2,
2892
- tone: "transparent",
2893
- children: /* @__PURE__ */ jsx(Text, {
2894
- muted: pending === 0,
2895
- size: 1,
2896
- children: phrase
2897
- })
2898
- })
3280
+ gap: 3,
3281
+ children: [
3282
+ /* @__PURE__ */ jsx(MetricPair, {
3283
+ Icon: EmptyIcon,
3284
+ empty: facts.pending === 0,
3285
+ face: {
3286
+ text: pendingHint(facts.pending)
3287
+ },
3288
+ value: String(facts.pending)
3289
+ }), facts.overdue === 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
3290
+ Icon: CalendarIcon,
3291
+ face: {
3292
+ text: overdueTasksHint(facts.overdue)
3293
+ },
3294
+ tone: "caution",
3295
+ value: String(facts.overdue)
3296
+ }) ]
2899
3297
  });
2900
3298
  }
2901
3299
 
2902
- function StageGroupHeading({group: group}) {
3300
+ function StageGroupHeading({facts: facts, group: group}) {
3301
+ const totals = laneTotals(group.cards, facts);
2903
3302
  /* @__PURE__ */
2904
3303
  return jsxs(Flex, {
2905
3304
  align: "center",
2906
3305
  gap: 2,
2907
3306
  children: [
2908
- /* @__PURE__ */ jsx(CountedLabel, {
3307
+ /* @__PURE__ */ jsx(CountedHeading, {
2909
3308
  count: group.cards.length,
2910
- label: /* @__PURE__ */ jsx(Text, {
2911
- size: 1,
2912
- weight: "semibold",
2913
- children: group.title
2914
- })
3309
+ title: group.title
3310
+ }), totals.overdue === 0 && totals.stuck === 0 ? null : /* @__PURE__ */ jsxs(Fragment, {
3311
+ children: [
3312
+ /* @__PURE__ */ jsx(Flex, {
3313
+ flex: 1
3314
+ }),
3315
+ /* @__PURE__ */ jsxs(Flex, {
3316
+ align: "center",
3317
+ flex: "none",
3318
+ gap: 3,
3319
+ children: [ totals.overdue === 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
3320
+ Icon: CalendarIcon,
3321
+ face: {
3322
+ text: laneOverdueHint(totals.overdue)
3323
+ },
3324
+ tone: "caution",
3325
+ value: String(totals.overdue)
3326
+ }), totals.stuck === 0 ? null : /* @__PURE__ */ jsx(MetricPair, {
3327
+ Icon: ErrorOutlineIcon,
3328
+ face: {
3329
+ text: laneStuckHint(totals.stuck)
3330
+ },
3331
+ tone: "critical",
3332
+ value: String(totals.stuck)
3333
+ }) ]
3334
+ }) ]
2915
3335
  }), group.retired ?
2916
3336
  /* @__PURE__ */ jsx(Text, {
2917
3337
  muted: !0,
@@ -2960,6 +3380,7 @@ function StageColumn({group: group, leadingRule: leadingRule, trailingRule: trai
2960
3380
  paddingTop: 4,
2961
3381
  paddingX: LANE_PADDING,
2962
3382
  children: /* @__PURE__ */ jsx(StageGroupHeading, {
3383
+ facts: wiring.facts,
2963
3384
  group: group
2964
3385
  })
2965
3386
  }), group.cards.length === 0 ? /* @__PURE__ */ jsx(Flex, {
@@ -3100,7 +3521,7 @@ function useBoard(args) {
3100
3521
  }), {titles: titles, ready: ready} = seeded, board = useMemo(() => derived ? orderBoardByPreviewTitle(derived, titles) : void 0, [ derived, titles ]);
3101
3522
  return useMemo(() => ({
3102
3523
  board: board,
3103
- ready: ready,
3524
+ ready: board !== void 0 && ready,
3104
3525
  titles: seeded
3105
3526
  }), [ board, ready, seeded ]);
3106
3527
  }
@@ -3207,11 +3628,13 @@ function ActiveCount({face: face}) {
3207
3628
  }
3208
3629
 
3209
3630
  function DefinitionFacts({definition: definition}) {
3210
- const {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), snapshotWidth = useContainerToken(2), [row] = definitionCatalog({
3631
+ const {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), now = useMinuteClock(), snapshotWidth = useContainerToken(2), [row] = definitionCatalog({
3211
3632
  definitions: [ definition ],
3633
+ identity: void 0,
3212
3634
  mappings: mappings,
3213
- inFlight: instances.inFlight,
3214
- issues: mappingIssues
3635
+ instances: [ ...instances.inFlight, ...instances.settled ],
3636
+ issues: mappingIssues,
3637
+ now: now
3215
3638
  }), runsOn = docTypeLabels(row?.docTypes ?? [], name => schema.get(name)?.title).map(docType => docType.title);
3216
3639
  /* @__PURE__ */
3217
3640
  return jsxs(Flex, {
@@ -3249,7 +3672,7 @@ function DefinitionFacts({definition: definition}) {
3249
3672
  label: "Active instances",
3250
3673
  children: /* @__PURE__ */ jsx(ActiveCount, {
3251
3674
  face: activeCountFace({
3252
- count: row?.activeCount ?? 0,
3675
+ count: row?.signals.activeCount ?? 0,
3253
3676
  error: instances.error,
3254
3677
  loading: instances.loading,
3255
3678
  truncated: instances.truncated
@@ -3317,7 +3740,7 @@ function pickerLabel(face) {
3317
3740
  }
3318
3741
 
3319
3742
  function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
3320
- const empty = options.length === 0;
3743
+ const empty = options.length === 0, reclaimed = -useSpaceToken(TOOL_HEADING_PADDING);
3321
3744
  /* @__PURE__ */
3322
3745
  return jsx(MenuButton, {
3323
3746
  button: /* @__PURE__ */ jsx(Button, {
@@ -3327,6 +3750,9 @@ function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
3327
3750
  iconRight: ChevronDownIcon,
3328
3751
  mode: "bleed",
3329
3752
  padding: TOOL_HEADING_PADDING,
3753
+ style: {
3754
+ margin: reclaimed
3755
+ },
3330
3756
  text: pickerLabel(face),
3331
3757
  textWeight: "semibold"
3332
3758
  }),
@@ -3357,6 +3783,7 @@ function WorkflowPageHeader({face: face, onSelectPage: onSelectPage, onSelectWor
3357
3783
  children: [
3358
3784
  /* @__PURE__ */ jsx(Flex, {
3359
3785
  align: "center",
3786
+ padding: TOOL_HEADING_PADDING,
3360
3787
  children: /* @__PURE__ */ jsx(WorkflowPicker, {
3361
3788
  face: face,
3362
3789
  onSelect: onSelectWorkflow,
@@ -3800,20 +4227,28 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3800
4227
  }), instances = useBoardInstances(view.streamName), {board: board, ready: ready, titles: titles} = useBoard({
3801
4228
  definition: selected,
3802
4229
  instances: instances.rows
3803
- }), {select: select, selectedInstanceId: selectedInstanceId} = usePageSelection(page), panel = usePanelPresence(panelSubject({
4230
+ }), {select: select, selectedInstanceId: selectedInstanceId} = usePageSelection(page), now = useMinuteClock(), panel = usePanelPresence(panelSubject({
3804
4231
  board: board,
3805
4232
  selectedInstanceId: selectedInstanceId
3806
4233
  })), revealed = useRevealGate({
3807
4234
  loading: instances.loading,
4235
+ progress: titles.resolved.size,
3808
4236
  ready: ready
3809
- }), waiting = useDelayedFlag(selection.kind === "loading" && catalogError === void 0, WAIT_CUE_DELAY_MS);
4237
+ });
3810
4238
  if (selection.kind === "loading") /* @__PURE__ */ return jsx(UnresolvedPage, {
3811
- error: catalogError,
3812
- waiting: waiting
4239
+ error: catalogError
4240
+ });
4241
+ const catalogEmpty = (definitions ?? []).length === 0;
4242
+ if (selection.kind === "unknown" && catalogEmpty) /* @__PURE__ */
4243
+ return jsx(EmptyCatalogPage, {
4244
+ name: selection.name
3813
4245
  });
3814
4246
  const wiring = {
3815
4247
  onSelect: select,
3816
- pending: pendingTaskCounts(instances.rows),
4248
+ facts: cardFacts({
4249
+ instances: instances.rows,
4250
+ now: now
4251
+ }),
3817
4252
  selectedInstanceId: selectedInstanceId,
3818
4253
  titles: titles
3819
4254
  };
@@ -3866,7 +4301,7 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3866
4301
  }),
3867
4302
  /* @__PURE__ */ jsx(PageBody, {
3868
4303
  board: board,
3869
- catalogEmpty: (definitions ?? []).length === 0,
4304
+ catalogEmpty: catalogEmpty,
3870
4305
  catalogError: catalogError,
3871
4306
  instancesError: instances.error,
3872
4307
  loading: !revealed,
@@ -3896,30 +4331,45 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3896
4331
  });
3897
4332
  }
3898
4333
 
3899
- const WAIT_CUE_DELAY_MS = 300;
3900
-
3901
- function UnresolvedPage({error: error, waiting: waiting}) {
4334
+ function UnresolvedPage({error: error}) {
3902
4335
  /* @__PURE__ */
3903
4336
  return jsx(Flex, {
3904
4337
  direction: "column",
3905
4338
  height: "fill",
3906
4339
  overflow: "hidden",
3907
- children: error === void 0 ? /* @__PURE__ */ jsx(WaitingFace, {
3908
- waiting: waiting
3909
- }) : /* @__PURE__ */ jsx(ReadErrorFace, {
4340
+ children: error === void 0 ? /* @__PURE__ */ jsx(WaitingFace, {}) : /* @__PURE__ */ jsx(ReadErrorFace, {
3910
4341
  text: CATALOG_READ_FAILED
3911
4342
  })
3912
4343
  });
3913
4344
  }
3914
4345
 
3915
- function WaitingFace({waiting: waiting}) {
4346
+ function WaitingFace() {
3916
4347
  /* @__PURE__ */
3917
4348
  return jsx(StatusBox, {
3918
- children: waiting ? /* @__PURE__ */ jsx(TabStatusRow, {
4349
+ children: /* @__PURE__ */ jsx(TabStatusRow, {
3919
4350
  children: /* @__PURE__ */ jsx(LoadingRow, {
3920
4351
  label: "Loading workflow…"
3921
4352
  })
3922
- }) : null
4353
+ })
4354
+ });
4355
+ }
4356
+
4357
+ function EmptyCatalogPage({name: name}) {
4358
+ const face = unknownWorkflowFace({
4359
+ catalogEmpty: !0,
4360
+ name: name
4361
+ });
4362
+ /* @__PURE__ */
4363
+ return jsx(Flex, {
4364
+ direction: "column",
4365
+ height: "fill",
4366
+ overflow: "hidden",
4367
+ padding: LANE_PADDING,
4368
+ children: /* @__PURE__ */ jsx(EmptyState, {
4369
+ description: face.description,
4370
+ icon: /* @__PURE__ */ jsx(SearchIcon, {}),
4371
+ title: face.title
4372
+ })
3923
4373
  });
3924
4374
  }
3925
4375
 
@@ -4124,11 +4574,12 @@ function WorkflowsHome({filterSlot: filterSlot, onOpenInstance: onOpenInstance,
4124
4574
 
4125
4575
  function HomePanel({filterSlot: filterSlot, identity: identity, instances: instances, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, tab: tab}) {
4126
4576
  return tab === "overview" ? /* @__PURE__ */ jsx(OverviewTab, {
4577
+ identity: identity,
4127
4578
  onOpenWorkflow: onOpenWorkflow
4128
4579
  }) : /* @__PURE__ */ jsx(TasksTab, {
4129
4580
  error: instances.error,
4130
4581
  filterSlot: filterSlot,
4131
- identity: identity,
4582
+ identity: identityOf(identity),
4132
4583
  instances: [ ...instances.inFlight, ...instances.settled ],
4133
4584
  loading: instances.loading,
4134
4585
  onOpenInstance: onOpenInstance,