@sanity/workflow-studio-plugin 0.27.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,28 +585,60 @@ 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;
588
+ const logged = /* @__PURE__ */ new WeakSet;
589
+
590
+ function logReadFailure(what, error) {
591
+ if (typeof error == "object" && error !== null) {
592
+ if (logged.has(error)) return;
593
+ logged.add(error);
594
+ }
595
+ console.error(`[workflow-studio-plugin] could not load ${what}:`, error);
596
+ }
440
597
 
441
- function catalogReadError(error) {
442
- return `Could not load the deployed workflows: ${error}`;
598
+ function useReadFailureLog(what, error) {
599
+ react.useEffect(() => {
600
+ error !== void 0 && logReadFailure(what, error);
601
+ }, [ what, error ]);
443
602
  }
444
603
 
445
- const lastRead = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
604
+ const INSTANCE_CAP = 200, INSTANCES_READ_FAILED = "Couldn’t load workflow data. Try again later.";
605
+
606
+ function useCappedInstances(filter) {
607
+ const {engine: engine} = index.useWorkflowContext(), capped = react.useMemo(() => ({
608
+ ...filter,
609
+ limit: INSTANCE_CAP
610
+ }), [ filter ]), {instances: instances, loading: loading, unreadable: unreadable, error: error} = workflowStudio.useWorkflowInstances({
611
+ engine: engine,
612
+ filter: capped
613
+ });
614
+ return useReadFailureLog("workflow data", error), react.useMemo(() => {
615
+ const rows = instances ?? [];
616
+ return {
617
+ rows: rows,
618
+ truncated: rows.length + unreadable.length === INSTANCE_CAP,
619
+ loading: loading,
620
+ unreadable: unreadable,
621
+ error: error === void 0 ? void 0 : workflowEngine.errorMessage(error)
622
+ };
623
+ }, [ instances, loading, unreadable, error ]);
624
+ }
625
+
626
+ const CATALOG_READ_FAILED = "Couldn’t load the deployed workflows. Try again later.", lastRead$1 = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
446
627
 
447
628
  function useDeployedDefinitions() {
448
- 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);
449
630
  return react.useEffect(() => {
450
631
  let cancelled = !1;
451
- setDefinitions(lastRead.get(engine)?.rows), setError(void 0);
632
+ setDefinitions(lastRead$1.get(engine)?.rows), setError(void 0);
452
633
  const seq = (issuedReads.get(engine) ?? 0) + 1;
453
634
  return issuedReads.set(engine, seq), (async () => {
454
- const rows = await index.readDeployedDefinitions(engine), latest = workflowEngine.latestDeployedDefinitions(rows), held = lastRead.get(engine);
455
- (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, {
456
637
  seq: seq,
457
638
  rows: latest
458
639
  }), cancelled || (setDefinitions(latest), setError(void 0));
459
640
  })().catch(err => {
460
- cancelled || setError(index.describeError(err));
641
+ logReadFailure("the deployed workflows", err), cancelled || setError(index.describeError(err));
461
642
  }), () => {
462
643
  cancelled = !0;
463
644
  };
@@ -467,75 +648,197 @@ function useDeployedDefinitions() {
467
648
  };
468
649
  }
469
650
 
470
- const INSTANCE_CAP = 200;
651
+ const NO_TITLES = {
652
+ titles: /* @__PURE__ */ new Map,
653
+ resolved: /* @__PURE__ */ new Set
654
+ };
471
655
 
472
- function useCappedInstances(filter) {
473
- const {engine: engine} = index.useWorkflowContext(), capped = react.useMemo(() => ({
474
- ...filter,
475
- limit: INSTANCE_CAP
476
- }), [ filter ]), {instances: instances, loading: loading, unreadable: unreadable} = workflowStudio.useWorkflowInstances({
477
- engine: engine,
478
- filter: capped
479
- });
480
- return react.useMemo(() => {
481
- const rows = instances ?? [];
482
- return {
483
- rows: rows,
484
- truncated: rows.length + unreadable.length === INSTANCE_CAP,
485
- loading: loading,
486
- unreadable: unreadable
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());
487
712
  };
488
- }, [ instances, loading, unreadable ]);
713
+ }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
714
+ state;
489
715
  }
490
716
 
491
717
  const INSTANCE_CAP_COUNT_NOTE = `Counts from the latest ${INSTANCE_CAP} workflows — older ones aren’t counted.`, NEWEST_INSTANCES = {
492
718
  includeCompleted: !0
493
- };
719
+ }, lastRead = /* @__PURE__ */ new WeakMap;
494
720
 
495
721
  function useToolInstances() {
496
- const {rows: rows, truncated: truncated, loading: loading, unreadable: unreadable} = useCappedInstances(NEWEST_INSTANCES);
497
- 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(() => ({
498
723
  inFlight: rows.filter(instance => workflowEngine.terminalState(instance) === "in-flight"),
499
724
  settled: rows.filter(instance => workflowEngine.terminalState(instance) !== "in-flight"),
500
725
  truncated: truncated,
501
726
  loading: loading,
502
- unreadable: unreadable
503
- }), [ rows, truncated, loading, unreadable ]);
727
+ unreadable: unreadable,
728
+ error: error
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
+ };
504
759
  }
505
760
 
506
- function activeSummary(active) {
507
- return active === 0 ? "No active instances" : pluralize__default.default("active instance", active, !0);
761
+ function overdueHint(work) {
762
+ return `${documentPhrase(work.documentCount)} with overdue tasks${documentlessTail(work.documentlessCount)}`;
508
763
  }
509
764
 
510
- const CARD_MIN_WIDTH = 280, CARD_RADIUS$1 = 3, CARD_PADDING = 4, CARD_MIN_HEIGHT = 180;
765
+ const UNTITLED_DOCUMENT = "Untitled", TRANSITION_HINT_PENDING = "Loading…";
511
766
 
512
- function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
513
- const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappingIssues: mappingIssues, mappings: mappings} = index.useWorkflowContext(), schema = sanity.useSchema(), instances = useToolInstances(), undeployed = /* @__PURE__ */ jsxRuntime.jsx(UndeployedWorkflows, {
767
+ function transitionHeading(at) {
768
+ return `Last stage change at ${index.formatShortDateTime(at)}`;
769
+ }
770
+
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
+ };
805
+ }
806
+
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
+ };
811
+ }
812
+
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;
814
+
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, {
514
825
  notices: undeployedWorkflowNotices({
515
826
  issues: mappingIssues,
516
827
  titleOf: name => schema.get(name)?.title
517
828
  })
518
829
  });
519
- if (error !== void 0) /* @__PURE__ */
520
- return jsxRuntime.jsx(TabStatusRow, {
521
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
522
- muted: !0,
523
- size: 1,
524
- children: catalogReadError(error)
525
- })
526
- });
527
- if (definitions === void 0) /* @__PURE__ */
528
- return jsxRuntime.jsx(TabStatusRow, {
830
+ return view.kind === "unreadable" ? /* @__PURE__ */ jsxRuntime.jsx(index.NoteBanner, {
831
+ label: CATALOG_READ_FAILED,
832
+ tone: "critical"
833
+ }) : view.kind === "loading" ? /* @__PURE__ */ jsxRuntime.jsx(TabStatusRow, {
529
834
  children: /* @__PURE__ */ jsxRuntime.jsx(index.LoadingRow, {
530
835
  label: "Loading workflows…"
531
836
  })
532
- });
533
- if (definitions.length === 0) /* @__PURE__ */
534
- return jsxRuntime.jsxs(ui.Flex, {
837
+ }) : view.kind === "empty" ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
535
838
  direction: "column",
536
839
  flex: 1,
537
840
  gap: 3,
538
- paddingBottom: 4,
841
+ paddingBottom: 3,
539
842
  paddingTop: 3,
540
843
  children: [ undeployed,
541
844
  /* @__PURE__ */ jsxRuntime.jsx(index.EmptyState, {
@@ -543,67 +846,166 @@ function OverviewTab({onOpenWorkflow: onOpenWorkflow}) {
543
846
  icon: /* @__PURE__ */ jsxRuntime.jsx(Transfer.TransferIcon, {}),
544
847
  title: "No workflows deployed"
545
848
  }) ]
546
- });
547
- const rows = definitionCatalog({
548
- definitions: definitions,
549
- mappings: mappings,
550
- inFlight: instances.inFlight,
551
- issues: mappingIssues
552
- }), active = rows.reduce((sum, row) => sum + row.activeCount, 0);
553
- /* @__PURE__ */
554
- return jsxRuntime.jsxs(ui.Stack, {
849
+ }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
555
850
  gap: 3,
556
851
  paddingBottom: 4,
557
852
  paddingTop: 3,
558
- children: [ undeployed,
559
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
560
- align: "center",
561
- gap: 2,
562
- padding: TOOL_HEADING_PADDING,
563
- children: [
564
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
565
- size: 1,
566
- weight: "semibold",
567
- children: pluralize__default.default("workflow", rows.length, !0)
568
- }),
569
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
570
- muted: !0,
571
- size: 1,
572
- children: instances.loading ? "counting active instances…" : activeSummary(active)
573
- }) ]
853
+ children: [ undeployed, instances.error === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(index.NoteBanner, {
854
+ label: INSTANCES_READ_FAILED,
855
+ tone: "critical"
574
856
  }),
575
- /* @__PURE__ */ jsxRuntime.jsx(ui.Grid, {
576
- gap: 3,
577
- style: {
578
- gridAutoRows: `minmax(${CARD_MIN_HEIGHT}px, auto)`,
579
- gridTemplateColumns: `repeat(auto-fill, minmax(${CARD_MIN_WIDTH}px, 1fr))`
580
- },
581
- children: rows.map(row => /* @__PURE__ */ jsxRuntime.jsx(WorkflowCard, {
582
- countPending: instances.loading,
583
- onOpen: () => onOpenWorkflow(row.name),
584
- row: row
585
- }, 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))
586
875
  }), instances.truncated ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
587
876
  text: INSTANCE_CAP_COUNT_NOTE
588
877
  }) : null ]
589
878
  });
590
879
  }
591
880
 
592
- 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
+ }
593
899
 
594
900
  function UndeployedWorkflows({notices: notices}) {
595
901
  return notices.length === 0 ? null :
596
902
  /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, {
597
903
  gap: 2,
598
904
  paddingBottom: 1,
599
- children: notices.map(notice => /* @__PURE__ */ jsxRuntime.jsx(index.CautionNote, {
905
+ children: notices.map(notice => /* @__PURE__ */ jsxRuntime.jsx(index.NoteBanner, {
600
906
  label: notice
601
907
  }, notice))
602
908
  });
603
909
  }
604
910
 
605
- function WorkflowCard({countPending: countPending, onOpen: onOpen, row: row}) {
606
- 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, {
607
1009
  as: "button",
608
1010
  onClick: onOpen,
609
1011
  padding: CARD_PADDING,
@@ -616,50 +1018,44 @@ function WorkflowCard({countPending: countPending, onOpen: onOpen, row: row}) {
616
1018
  textAlign: "left",
617
1019
  width: "100%"
618
1020
  },
619
- ...flagged ? {
620
- tone: "caution"
621
- } : {},
622
- children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
1021
+ children: [
1022
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, {
623
1023
  align: "flex-start",
624
- gap: 3,
625
- children: [ flagged ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
626
- size: 1,
627
- children: /* @__PURE__ */ jsxRuntime.jsx(WarningOutline.WarningOutlineIcon, {})
628
- }) : null,
629
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
1024
+ gap: 2,
1025
+ style: {
630
1026
  flex: 1,
631
- gap: 3,
1027
+ minWidth: 0
1028
+ },
1029
+ children: [
1030
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
1031
+ size: 1,
632
1032
  style: {
1033
+ flex: 1,
633
1034
  minWidth: 0
634
1035
  },
635
- children: [
636
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
637
- size: 1,
638
- weight: "semibold",
639
- children: row.title
640
- }), countPending ? null : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
641
- muted: !0,
642
- size: 1,
643
- children: activeSummary(row.activeCount)
644
- }) ]
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"
645
1042
  }) ]
646
- })
1043
+ }), countKnown ? /* @__PURE__ */ jsxRuntime.jsx(CardMetrics$1, {
1044
+ now: now,
1045
+ signals: row.signals
1046
+ }) : null ]
647
1047
  });
648
- return flagged ? /* @__PURE__ */ jsxRuntime.jsx(index.HoverHint, {
649
- fill: !0,
650
- text: CARD_ISSUES_HINT,
651
- children: card
652
- }) : card;
653
1048
  }
654
1049
 
655
1050
  function useStartableDefinitions() {
656
- 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 ]);
657
1056
  return {
658
- definitions: react.useMemo(() => definitions?.filter(row => workflowEngine.isStartableDefinition(row)).map(row => ({
659
- name: row.name,
660
- title: row.title ?? row.name,
661
- description: row.description
662
- })), [ definitions ]),
1057
+ catalogEmpty: definitions?.length === 0,
1058
+ definitions: startable,
663
1059
  error: error
664
1060
  };
665
1061
  }
@@ -691,16 +1087,45 @@ function NewWorkflowButton() {
691
1087
  });
692
1088
  }
693
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
+
694
1118
  function DefinitionPickerPanel({onPick: onPick}) {
695
- 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(() => {
696
1120
  const q = query.trim().toLowerCase();
697
1121
  return definitions ? q ? definitions.filter(d => `${d.title} ${d.name}`.toLowerCase().includes(q)) : definitions : [];
698
- }, [ definitions, query ]);
699
- /* @__PURE__ */
700
- return jsxRuntime.jsx(ui.Box, {
1122
+ }, [ definitions, query ]), settled = error === void 0 ? definitions : void 0;
1123
+ return settled?.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(NothingToStart, {
1124
+ catalogEmpty: catalogEmpty
1125
+ }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
701
1126
  padding: 2,
702
1127
  style: {
703
- width: 340
1128
+ width: PANEL_WIDTH
704
1129
  },
705
1130
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
706
1131
  gap: 2,
@@ -712,13 +1137,12 @@ function DefinitionPickerPanel({onPick: onPick}) {
712
1137
  onChange: event => setQuery(event.currentTarget.value),
713
1138
  placeholder: "Filter workflows…",
714
1139
  value: query
715
- }), error ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
716
- text: catalogReadError(error)
1140
+ }), error ? /* @__PURE__ */ jsxRuntime.jsx(index.NoteBanner, {
1141
+ label: CATALOG_READ_FAILED,
1142
+ tone: "critical"
717
1143
  }) : null, !definitions && !error ? /* @__PURE__ */ jsxRuntime.jsx(index.LoadingRow, {
718
1144
  label: "Loading workflows…"
719
- }) : null, definitions && definitions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
720
- text: "No startable workflows are deployed"
721
- }) : null, definitions && definitions.length > 0 && visible.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
1145
+ }) : null, settled !== void 0 && settled.length > 0 && visible.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
722
1146
  text: "No workflows match"
723
1147
  }) : null,
724
1148
  /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, {
@@ -996,13 +1420,20 @@ function useBandOpen(bands, key) {
996
1420
  };
997
1421
  }
998
1422
 
999
- const REVEAL_MAX_WAIT_MS = 2e3;
1423
+ const REVEAL_STALL_MS = 2e3, REVEAL_COLD_MS = 15e3;
1000
1424
 
1001
1425
  function useRevealGate(args) {
1002
- 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);
1003
1434
  return react.useEffect(() => {
1004
- !revealed && (settled || expired) && setRevealed(!0);
1005
- }, [ expired, revealed, settled ]), revealed || settled || expired;
1435
+ !revealed && (settled || stalled) && setRevealed(!0);
1436
+ }, [ revealed, settled, stalled ]), revealed || settled || stalled;
1006
1437
  }
1007
1438
 
1008
1439
  function spawnOrigins(instances) {
@@ -1015,16 +1446,11 @@ function spawnOrigins(instances) {
1015
1446
  return origins;
1016
1447
  }
1017
1448
 
1018
- function rootOf(instance, byId) {
1019
- const rootRef = instance.ancestors[0];
1020
- return rootRef ? byId.get(workflowEngine.toBareId(rootRef.id)) ?? instance : instance;
1021
- }
1022
-
1023
1449
  function taskRowsOf(args) {
1024
1450
  const {instance: instance, identity: identity} = args, open = workflowEngine.findOpenStageEntry(instance);
1025
1451
  if (!open) return [];
1026
1452
  const definition = index.definitionSnapshotOf(instance), state = index.stateByActivity(instance), rows = (open.activities ?? []).map(activity => {
1027
- const fields = state.get(activity.name) ?? [], declared = index.findActivityNode({
1453
+ const fields = state.get(activity.name) ?? [], declared = workflowEngine.findActivityNode({
1028
1454
  definition: definition,
1029
1455
  stageName: open.name,
1030
1456
  activityName: activity.name
@@ -1049,6 +1475,10 @@ function taskRowsOf(args) {
1049
1475
  state: fields,
1050
1476
  who: identity
1051
1477
  }),
1478
+ roleMatched: identity !== void 0 && index.isRoleAssignedTo({
1479
+ state: fields,
1480
+ who: identity
1481
+ }),
1052
1482
  assignControl: index.rowAssignControlState(controlArgs),
1053
1483
  dateControl: index.rowDateControlState(controlArgs)
1054
1484
  };
@@ -1075,7 +1505,7 @@ function deriveTaskGroups(args) {
1075
1505
  activityName: origin.activityName
1076
1506
  }).title : void 0,
1077
1507
  rows: rows
1078
- }, root = rootOf(instance, byId), documents = documentRefsOf(root), document2 = documents[0];
1508
+ }, root = rootInstanceOf(instance, byId), documents = documentRefsOf(root), document2 = documents[0];
1079
1509
  if (!document2) {
1080
1510
  withoutDocuments.push(entry);
1081
1511
  continue;
@@ -1131,6 +1561,11 @@ function onlyMyTasks(groups) {
1131
1561
  });
1132
1562
  }
1133
1563
 
1564
+ function hasRoleMatchedTasks(groups) {
1565
+ const bandHasOne = band => band.rows.some(row => row.roleMatched);
1566
+ return groups.withDocuments.some(group => group.instances.some(bandHasOne)) || groups.withoutDocuments.some(bandHasOne);
1567
+ }
1568
+
1134
1569
  function rowActivityEval(entry, row) {
1135
1570
  const evaluation = entry?.evaluation;
1136
1571
  if (!(evaluation === void 0 || evaluation.currentStage.stage.name !== row.stage)) return evaluation.currentStage.activities.find(a => a.activity.name === row.activityName);
@@ -1274,7 +1709,7 @@ function OrphanedWorkflowsNote({orphans: orphans}) {
1274
1709
  /* @__PURE__ */
1275
1710
  return jsxRuntime.jsxs(jsxRuntime.Fragment, {
1276
1711
  children: [
1277
- /* @__PURE__ */ jsxRuntime.jsx(index.CautionNote, {
1712
+ /* @__PURE__ */ jsxRuntime.jsx(index.NoteBanner, {
1278
1713
  action: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, {
1279
1714
  fontSize: 1,
1280
1715
  mode: "ghost",
@@ -1376,72 +1811,6 @@ function SettleOrphansDialog({workflows: workflows, onClose: onClose}) {
1376
1811
  });
1377
1812
  }
1378
1813
 
1379
- const NO_TITLES = {
1380
- titles: /* @__PURE__ */ new Map,
1381
- resolved: /* @__PURE__ */ new Set
1382
- };
1383
-
1384
- function useDocPreviewTitles(documents) {
1385
- 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("|");
1386
- return react.useEffect(() => {
1387
- const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
1388
- titles: new Map(titles),
1389
- resolved: new Set(resolved)
1390
- }), localDocs = documents.flatMap(doc => {
1391
- const {parsed: parsed, local: local} = index.gdrLocality(doc.id, binding.contentResource);
1392
- return !parsed || !local ? (resolved.add(doc.id), []) : [ {
1393
- doc: doc,
1394
- bareId: parsed.documentId
1395
- } ];
1396
- }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
1397
- previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
1398
- }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
1399
- const schemaType = index.openableSchemaType(schema, typeName);
1400
- if (!schemaType) return !1;
1401
- if (previews.get(docId)?.typeName === typeName) return !0;
1402
- dropPreview(docId), resolved.delete(docId);
1403
- const subscription = previewStore.observeForPreview({
1404
- _id: bareId,
1405
- _type: schemaType.name
1406
- }, schemaType, {
1407
- perspective: perspectiveStack
1408
- }).subscribe({
1409
- next: event => {
1410
- resolved.add(docId);
1411
- const title = event.snapshot?.title;
1412
- typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
1413
- },
1414
- error: err => {
1415
- console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
1416
- publish();
1417
- }
1418
- });
1419
- return previews.set(docId, {
1420
- typeName: typeName,
1421
- unsubscribe: () => subscription.unsubscribe()
1422
- }), !0;
1423
- }, evaluate = () => {
1424
- for (const {doc: doc, bareId: bareId} of localDocs) {
1425
- const probe = actualTypes.read(bareId);
1426
- if (probe === null) {
1427
- dropPreview(doc.id), resolved.add(doc.id);
1428
- continue;
1429
- }
1430
- !observe({
1431
- docId: doc.id,
1432
- bareId: bareId,
1433
- typeName: probe ?? doc.type
1434
- }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
1435
- }
1436
- publish();
1437
- }, unsubscribeProbes = actualTypes.subscribe(evaluate);
1438
- return evaluate(), () => {
1439
- unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
1440
- };
1441
- }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
1442
- state;
1443
- }
1444
-
1445
1814
  function taskFilterOptions(groups) {
1446
1815
  const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
1447
1816
  for (const group of groups.withDocuments) {
@@ -2610,7 +2979,7 @@ function useOrphanedWorkflows(groups) {
2610
2979
  }), [ groups, contentResource, missing ]);
2611
2980
  }
2612
2981
 
2613
- function TasksTab({filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, tab: tab, truncated: truncated, unreadable: unreadable}) {
2982
+ function TasksTab({error: error, filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, tab: tab, truncated: truncated, unreadable: unreadable}) {
2614
2983
  const mineOnly = tab === "for-me", {binding: binding} = index.useWorkflowContext(), bands = useBandOpenState({
2615
2984
  segment: TASK_BAND_SEGMENTS[tab],
2616
2985
  scope: binding.engineResource.id
@@ -2619,6 +2988,7 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2619
2988
  identity: identity
2620
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({
2621
2990
  loading: loading,
2991
+ progress: titles.resolved.size,
2622
2992
  ready: titles.ready
2623
2993
  }), visible = react.useMemo(() => {
2624
2994
  const filtered = applyTaskFilters({
@@ -2628,6 +2998,10 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2628
2998
  }), cut = mineOnly ? onlyMyTasks(filtered) : filtered;
2629
2999
  return orderGroupsByPreviewTitle(cut, titles.titles);
2630
3000
  }, [ listed, filters, mineOnly, titles ]);
3001
+ if (error !== void 0) /* @__PURE__ */ return jsxRuntime.jsx(index.NoteBanner, {
3002
+ label: INSTANCES_READ_FAILED,
3003
+ tone: "critical"
3004
+ });
2631
3005
  if (loading || !revealed) /* @__PURE__ */
2632
3006
  return jsxRuntime.jsx(TabStatusRow, {
2633
3007
  children: /* @__PURE__ */ jsxRuntime.jsx(index.LoadingRow, {
@@ -2667,13 +3041,37 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2667
3041
  }),
2668
3042
  /* @__PURE__ */ jsxRuntime.jsx(OrphanedWorkflowsNote, {
2669
3043
  orphans: orphans
2670
- }), body, truncated ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
3044
+ }), mineOnly && hasRoleMatchedTasks(visible) ? /* @__PURE__ */ jsxRuntime.jsx(index.RoleAssignedNotice, {}) : null, body, truncated ? /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
2671
3045
  text: `Tasks from the latest ${INSTANCE_CAP} workflows — older ones aren’t listed.`
2672
3046
  }) : null ]
2673
3047
  }) ]
2674
3048
  });
2675
3049
  }
2676
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
+
2677
3075
  const BOARD_EMPTY_STAGE_NOTE = "No items";
2678
3076
 
2679
3077
  function faceOf(instance) {
@@ -2732,6 +3130,24 @@ function boardDocuments(board) {
2732
3130
  return board.groups.flatMap(group => group.cards.flatMap(card => card.face.kind === "document" ? [ card.face.document ] : []));
2733
3131
  }
2734
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
+
2735
3151
  function CardTitleFace({title: title}) {
2736
3152
  return title.kind === "title" ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, {
2737
3153
  children: title.title
@@ -2758,23 +3174,10 @@ function cardTitle(args) {
2758
3174
  };
2759
3175
  }
2760
3176
 
2761
- function ownOpenTasks(instance) {
2762
- const open = workflowEngine.findOpenStageEntry(instance);
2763
- return open ? (open.activities ?? []).filter(activity => index.isOpenActivityStatus(activity.status)).length : 0;
2764
- }
2765
-
2766
- function pendingTaskCounts(instances) {
2767
- return new Map(instances.map(instance => [ instance._id, ownOpenTasks(instance) ]));
2768
- }
2769
-
2770
- function pendingTasksPhrase(pending) {
2771
- if (pending !== void 0) return pending === 0 ? "All tasks done" : pluralize__default.default("pending task", pending, !0);
2772
- }
2773
-
2774
3177
  const CARD_RADIUS = 3;
2775
3178
 
2776
3179
  function BoardCard({card: card, wiring: wiring}) {
2777
- 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);
2778
3181
  /* @__PURE__ */
2779
3182
  return jsxRuntime.jsx(ui.Card, {
2780
3183
  "aria-pressed": selected,
@@ -2792,54 +3195,93 @@ function BoardCard({card: card, wiring: wiring}) {
2792
3195
  children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
2793
3196
  gap: 3,
2794
3197
  children: [
2795
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
2796
- size: 1,
2797
- weight: "medium",
2798
- children: /* @__PURE__ */ jsxRuntime.jsx(CardTitleFace, {
2799
- title: cardTitle({
2800
- face: card.face,
2801
- 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
+ })
2802
3214
  })
2803
- })
2804
- }),
2805
- /* @__PURE__ */ jsxRuntime.jsx(PendingTasks, {
2806
- 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
2807
3222
  }) ]
2808
3223
  })
2809
3224
  });
2810
3225
  }
2811
3226
 
2812
- function PendingTasks({pending: pending}) {
2813
- const badgeTrim = index.useBadgeCapTrim(), phrase = pendingTasksPhrase(pending);
2814
- 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, {
2815
3229
  align: "center",
2816
- style: badgeTrim,
2817
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, {
2818
- padding: 2,
2819
- radius: 2,
2820
- tone: "transparent",
2821
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
2822
- muted: pending === 0,
2823
- size: 1,
2824
- children: phrase
2825
- })
2826
- })
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
+ }) ]
2827
3247
  });
2828
3248
  }
2829
3249
 
2830
- function StageGroupHeading({group: group}) {
3250
+ function StageGroupHeading({facts: facts, group: group}) {
3251
+ const totals = laneTotals(group.cards, facts);
2831
3252
  /* @__PURE__ */
2832
3253
  return jsxRuntime.jsxs(ui.Flex, {
2833
3254
  align: "center",
2834
3255
  gap: 2,
2835
3256
  children: [
2836
- /* @__PURE__ */ jsxRuntime.jsx(index.CountedLabel, {
3257
+ /* @__PURE__ */ jsxRuntime.jsx(index.CountedHeading, {
2837
3258
  count: group.cards.length,
2838
- label: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
2839
- size: 1,
2840
- weight: "semibold",
2841
- children: group.title
2842
- })
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
+ }) ]
2843
3285
  }), group.retired ?
2844
3286
  /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
2845
3287
  muted: !0,
@@ -2888,6 +3330,7 @@ function StageColumn({group: group, leadingRule: leadingRule, trailingRule: trai
2888
3330
  paddingTop: 4,
2889
3331
  paddingX: LANE_PADDING,
2890
3332
  children: /* @__PURE__ */ jsxRuntime.jsx(StageGroupHeading, {
3333
+ facts: wiring.facts,
2891
3334
  group: group
2892
3335
  })
2893
3336
  }), group.cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, {
@@ -3028,7 +3471,7 @@ function useBoard(args) {
3028
3471
  }), {titles: titles, ready: ready} = seeded, board = react.useMemo(() => derived ? orderBoardByPreviewTitle(derived, titles) : void 0, [ derived, titles ]);
3029
3472
  return react.useMemo(() => ({
3030
3473
  board: board,
3031
- ready: ready,
3474
+ ready: board !== void 0 && ready,
3032
3475
  titles: seeded
3033
3476
  }), [ board, ready, seeded ]);
3034
3477
  }
@@ -3045,6 +3488,16 @@ function useBoardInstances(definitionName) {
3045
3488
  return useCappedInstances(filter);
3046
3489
  }
3047
3490
 
3491
+ function activeCountFace(args) {
3492
+ return args.error !== void 0 || args.loading ? {
3493
+ count: void 0,
3494
+ note: void 0
3495
+ } : {
3496
+ count: args.count,
3497
+ note: args.truncated ? INSTANCE_CAP_COUNT_NOTE : void 0
3498
+ };
3499
+ }
3500
+
3048
3501
  function DefinitionPanel({definition: definition}) {
3049
3502
  /* @__PURE__ */
3050
3503
  return jsxRuntime.jsx(ui.Box, {
@@ -3107,12 +3560,31 @@ function SetupIssues({bindings: bindings}) {
3107
3560
  });
3108
3561
  }
3109
3562
 
3563
+ function ActiveCount({face: face}) {
3564
+ /* @__PURE__ */
3565
+ return jsxRuntime.jsxs(ui.Stack, {
3566
+ gap: 2,
3567
+ children: [
3568
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
3569
+ muted: face.count === void 0,
3570
+ size: 1,
3571
+ children: face.count ?? "—"
3572
+ }), face.note === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
3573
+ muted: !0,
3574
+ size: 1,
3575
+ children: face.note
3576
+ }) ]
3577
+ });
3578
+ }
3579
+
3110
3580
  function DefinitionFacts({definition: definition}) {
3111
- 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({
3112
3582
  definitions: [ definition ],
3583
+ identity: void 0,
3113
3584
  mappings: mappings,
3114
- inFlight: instances.inFlight,
3115
- issues: mappingIssues
3585
+ instances: [ ...instances.inFlight, ...instances.settled ],
3586
+ issues: mappingIssues,
3587
+ now: now
3116
3588
  }), runsOn = docTypeLabels(row?.docTypes ?? [], name => schema.get(name)?.title).map(docType => docType.title);
3117
3589
  /* @__PURE__ */
3118
3590
  return jsxRuntime.jsxs(ui.Flex, {
@@ -3124,6 +3596,12 @@ function DefinitionFacts({definition: definition}) {
3124
3596
  children: /* @__PURE__ */ jsxRuntime.jsx(SetupIssues, {
3125
3597
  bindings: row.brokenBindings
3126
3598
  })
3599
+ }), instances.error === void 0 ? null : /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
3600
+ flex: "none",
3601
+ children: /* @__PURE__ */ jsxRuntime.jsx(index.NoteBanner, {
3602
+ label: INSTANCES_READ_FAILED,
3603
+ tone: "critical"
3604
+ })
3127
3605
  }),
3128
3606
  /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
3129
3607
  flex: "none",
@@ -3142,21 +3620,13 @@ function DefinitionFacts({definition: definition}) {
3142
3620
  }),
3143
3621
  /* @__PURE__ */ jsxRuntime.jsx(index.MetaRow, {
3144
3622
  label: "Active instances",
3145
- children: instances.loading ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
3146
- muted: !0,
3147
- size: 1,
3148
- children: "—"
3149
- }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, {
3150
- gap: 2,
3151
- children: [
3152
- /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
3153
- size: 1,
3154
- children: row?.activeCount ?? 0
3155
- }), instances.truncated ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, {
3156
- muted: !0,
3157
- size: 1,
3158
- children: INSTANCE_CAP_COUNT_NOTE
3159
- }) : null ]
3623
+ children: /* @__PURE__ */ jsxRuntime.jsx(ActiveCount, {
3624
+ face: activeCountFace({
3625
+ count: row?.signals.activeCount ?? 0,
3626
+ error: instances.error,
3627
+ loading: instances.loading,
3628
+ truncated: instances.truncated
3629
+ })
3160
3630
  })
3161
3631
  }),
3162
3632
  /* @__PURE__ */ jsxRuntime.jsx(index.MetaRow, {
@@ -3220,7 +3690,7 @@ function pickerLabel(face) {
3220
3690
  }
3221
3691
 
3222
3692
  function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
3223
- const empty = options.length === 0;
3693
+ const empty = options.length === 0, reclaimed = -index.useSpaceToken(TOOL_HEADING_PADDING);
3224
3694
  /* @__PURE__ */
3225
3695
  return jsxRuntime.jsx(ui.MenuButton, {
3226
3696
  button: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, {
@@ -3230,6 +3700,9 @@ function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
3230
3700
  iconRight: ChevronDown.ChevronDownIcon,
3231
3701
  mode: "bleed",
3232
3702
  padding: TOOL_HEADING_PADDING,
3703
+ style: {
3704
+ margin: reclaimed
3705
+ },
3233
3706
  text: pickerLabel(face),
3234
3707
  textWeight: "semibold"
3235
3708
  }),
@@ -3260,6 +3733,7 @@ function WorkflowPageHeader({face: face, onSelectPage: onSelectPage, onSelectWor
3260
3733
  children: [
3261
3734
  /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, {
3262
3735
  align: "center",
3736
+ padding: TOOL_HEADING_PADDING,
3263
3737
  children: /* @__PURE__ */ jsxRuntime.jsx(WorkflowPicker, {
3264
3738
  face: face,
3265
3739
  onSelect: onSelectWorkflow,
@@ -3693,30 +4167,38 @@ function usePageSelection(page) {
3693
4167
  }
3694
4168
 
3695
4169
  function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPage, page: page, workflowName: workflowName}) {
3696
- const {definitions: definitions, error: error} = useDeployedDefinitions(), selection = resolveWorkflowSelection({
4170
+ const {definitions: definitions, error: catalogError} = useDeployedDefinitions(), selection = resolveWorkflowSelection({
3697
4171
  definitions: definitions,
3698
4172
  routeName: workflowName
3699
4173
  }), selected = resolvedDefinition(selection), view = workflowPageView({
3700
- error: error,
4174
+ error: catalogError,
3701
4175
  page: page,
3702
4176
  selection: selection
3703
4177
  }), instances = useBoardInstances(view.streamName), {board: board, ready: ready, titles: titles} = useBoard({
3704
4178
  definition: selected,
3705
4179
  instances: instances.rows
3706
- }), {select: select, selectedInstanceId: selectedInstanceId} = usePageSelection(page), panel = usePanelPresence(panelSubject({
4180
+ }), {select: select, selectedInstanceId: selectedInstanceId} = usePageSelection(page), now = index.useMinuteClock(), panel = usePanelPresence(panelSubject({
3707
4181
  board: board,
3708
4182
  selectedInstanceId: selectedInstanceId
3709
4183
  })), revealed = useRevealGate({
3710
4184
  loading: instances.loading,
4185
+ progress: titles.resolved.size,
3711
4186
  ready: ready
3712
- }), waiting = index.useDelayedFlag(selection.kind === "loading" && error === void 0, WAIT_CUE_DELAY_MS);
4187
+ });
3713
4188
  if (selection.kind === "loading") /* @__PURE__ */ return jsxRuntime.jsx(UnresolvedPage, {
3714
- error: error,
3715
- 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
3716
4195
  });
3717
4196
  const wiring = {
3718
4197
  onSelect: select,
3719
- pending: pendingTaskCounts(instances.rows),
4198
+ facts: cardFacts({
4199
+ instances: instances.rows,
4200
+ now: now
4201
+ }),
3720
4202
  selectedInstanceId: selectedInstanceId,
3721
4203
  titles: titles
3722
4204
  };
@@ -3769,8 +4251,9 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3769
4251
  }),
3770
4252
  /* @__PURE__ */ jsxRuntime.jsx(PageBody, {
3771
4253
  board: board,
3772
- catalogEmpty: (definitions ?? []).length === 0,
3773
- error: error,
4254
+ catalogEmpty: catalogEmpty,
4255
+ catalogError: catalogError,
4256
+ instancesError: instances.error,
3774
4257
  loading: !revealed,
3775
4258
  page: page,
3776
4259
  selection: selection,
@@ -3798,45 +4281,66 @@ function WorkflowPage({onOpenWorkflow: onOpenWorkflow, onSelectPage: onSelectPag
3798
4281
  });
3799
4282
  }
3800
4283
 
3801
- const WAIT_CUE_DELAY_MS = 300;
3802
-
3803
- function UnresolvedPage({error: error, waiting: waiting}) {
4284
+ function UnresolvedPage({error: error}) {
3804
4285
  /* @__PURE__ */
3805
4286
  return jsxRuntime.jsx(ui.Flex, {
3806
4287
  direction: "column",
3807
4288
  height: "fill",
3808
4289
  overflow: "hidden",
3809
- children: error === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(WaitingFace, {
3810
- waiting: waiting
3811
- }) : /* @__PURE__ */ jsxRuntime.jsx(CatalogErrorFace, {
3812
- error: error
4290
+ children: error === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(WaitingFace, {}) : /* @__PURE__ */ jsxRuntime.jsx(ReadErrorFace, {
4291
+ text: CATALOG_READ_FAILED
3813
4292
  })
3814
4293
  });
3815
4294
  }
3816
4295
 
3817
- function WaitingFace({waiting: waiting}) {
4296
+ function WaitingFace() {
3818
4297
  /* @__PURE__ */
3819
4298
  return jsxRuntime.jsx(StatusBox, {
3820
- children: waiting ? /* @__PURE__ */ jsxRuntime.jsx(TabStatusRow, {
4299
+ children: /* @__PURE__ */ jsxRuntime.jsx(TabStatusRow, {
3821
4300
  children: /* @__PURE__ */ jsxRuntime.jsx(index.LoadingRow, {
3822
4301
  label: "Loading workflow…"
3823
4302
  })
3824
- }) : 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
+ })
3825
4323
  });
3826
4324
  }
3827
4325
 
3828
- function CatalogErrorFace({error: error}) {
4326
+ const BANNER_LANE_INSET = 2;
4327
+
4328
+ function ReadErrorFace({text: text}) {
3829
4329
  /* @__PURE__ */
3830
4330
  return jsxRuntime.jsx(StatusBox, {
3831
- children: /* @__PURE__ */ jsxRuntime.jsx(MutedNote, {
3832
- text: catalogReadError(error)
4331
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
4332
+ paddingTop: BANNER_LANE_INSET,
4333
+ children: /* @__PURE__ */ jsxRuntime.jsx(index.NoteBanner, {
4334
+ label: text,
4335
+ tone: "critical"
4336
+ })
3833
4337
  })
3834
4338
  });
3835
4339
  }
3836
4340
 
3837
- function PageBody({board: board, catalogEmpty: catalogEmpty, error: error, loading: loading, page: page, selection: selection, wiring: wiring}) {
3838
- if (error !== void 0) /* @__PURE__ */ return jsxRuntime.jsx(CatalogErrorFace, {
3839
- error: error
4341
+ function PageBody({board: board, catalogEmpty: catalogEmpty, catalogError: catalogError, instancesError: instancesError, loading: loading, page: page, selection: selection, wiring: wiring}) {
4342
+ if (catalogError !== void 0) /* @__PURE__ */ return jsxRuntime.jsx(ReadErrorFace, {
4343
+ text: CATALOG_READ_FAILED
3840
4344
  });
3841
4345
  if (selection.kind === "unknown") {
3842
4346
  const face = unknownWorkflowFace({
@@ -3854,6 +4358,8 @@ function PageBody({board: board, catalogEmpty: catalogEmpty, error: error, loadi
3854
4358
  }
3855
4359
  return page === "definition" ? /* @__PURE__ */ jsxRuntime.jsx(DefinitionPanel, {
3856
4360
  definition: selection.definition
4361
+ }) : instancesError !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(ReadErrorFace, {
4362
+ text: INSTANCES_READ_FAILED
3857
4363
  }) : loading || board === void 0 ? /* @__PURE__ */ jsxRuntime.jsx(StatusBox, {
3858
4364
  children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, {
3859
4365
  paddingTop: LANE_PADDING,
@@ -4018,10 +4524,12 @@ function WorkflowsHome({filterSlot: filterSlot, onOpenInstance: onOpenInstance,
4018
4524
 
4019
4525
  function HomePanel({filterSlot: filterSlot, identity: identity, instances: instances, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, tab: tab}) {
4020
4526
  return tab === "overview" ? /* @__PURE__ */ jsxRuntime.jsx(OverviewTab, {
4527
+ identity: identity,
4021
4528
  onOpenWorkflow: onOpenWorkflow
4022
4529
  }) : /* @__PURE__ */ jsxRuntime.jsx(TasksTab, {
4530
+ error: instances.error,
4023
4531
  filterSlot: filterSlot,
4024
- identity: identity,
4532
+ identity: index.identityOf(identity),
4025
4533
  instances: [ ...instances.inFlight, ...instances.settled ],
4026
4534
  loading: instances.loading,
4027
4535
  onOpenInstance: onOpenInstance,