@sanity/workflow-studio-plugin 0.23.0 → 0.24.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,84 +1,1306 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
 
3
- import { Box, Text, Flex, Stack, Card, Badge, Menu, MenuItem, Button, useTheme_v2, TextInput, Dialog, TabPanel } from "@sanity/ui";
3
+ import { Box, Text, Card, Flex, Stack, MenuButton, Menu, MenuItem, Button, Badge, Dialog, TextInput, useTheme_v2, TabPanel } from "@sanity/ui";
4
4
 
5
- import { useWorkflowTelemetry, editFieldTarget } from "@sanity/workflow-react";
5
+ import { useWorkflowTelemetry } from "@sanity/workflow-react";
6
6
 
7
- import { useState, useEffect, useMemo, useRef } from "react";
7
+ import { useMemo, useSyncExternalStore, useCallback, useState, useEffect, useRef } from "react";
8
8
 
9
9
  import { useRouter } from "sanity/router";
10
10
 
11
- import { useSpaceToken, DocPreviewLink, SpinnerSlot, useWorkflowContext, readDeployedDefinitions, describeError, useContainerToken, LoadingRow, MetaRow, formatDate, HoverHint, useBadgeCapTrim, stageTitle, useClosableToast, UserAvatar, formatShortAgo, formatDateTime, useAvatarSize, HintedMenuButton, useUserDisplay, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, InvalidDocNotice, useDefinition, openActivityGone, instanceTitle, isEvaluationStale, InstanceSnapshotBody, ActivityDetailDialog, instanceBreadcrumb, DismissablePopover, definitionSnapshotOf, findActivity, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isActivityAssignedTo, assigneeUserIdsOf, isOpenActivityStatus, committedRowFace, activityRowProps, gdrLocality, CautionNote, WORKFLOW_API_VERSION, parseStoredDateValue, openableSchemaType, useUserDisplays, useProjectMembers, WorkflowTaskFiltersApplied, DocRefFace, useSaveField, dateControlValue, stopRowMouseDown, stopRowClick, parseDateFieldValue, serializeDateFieldValue, dateControlKind, CollapsibleBand, LinkChip, BreadcrumbTail, StageFace, TrailingHairline, StaleLock, ActivityRow, UnreadableDocsNote, ForMeEmptyState, pathSegmentTab, TabSwitch, WorkflowToolOpened, useAssignmentIdentity } from "./index.js";
11
+ import { createSubscribers, useWorkflowContext, readDeployedDefinitions, describeError, useDelayedFlag, instanceTitle, definitionSnapshotOf, stageTitle, DocPreviewLink, NOTICE_PADDING, chipPadding, CountedLabel, useSpaceToken, useRadiusToken, HoverHint, definitionFingerprint, CollapsibleBand, gdrLocality, openableSchemaType, WORKFLOW_API_VERSION, WorkflowTitleSeedDrifted, WorkflowBoardLayoutChanged, UnreadableDocsNote, LogEventOnMount, WorkflowBoardWorkflowSelected, LoadingRow, EmptyState, mappingIssueDetail, formatTimeAgo, formatShortDateTime, namesNoDeployedDefinition, SpinnerSlot, WorkflowDefinitionDetailViewed, useContainerToken, MetaRow, CautionNote, useBadgeCapTrim, TOAST_ID, useDefinition, useWorkflowToast, isLiveEntry, UserAvatar, formatShortAgo, formatDateTime, useAvatarSize, HintedMenuButton, useUserDisplay, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, InvalidDocNotice, openActivityGone, isEvaluationStale, InstanceSnapshotBody, ActivityDetailDialog, instanceBreadcrumb, DismissablePopover, findActivity, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isActivityAssignedTo, assigneeUserIdsOf, isOpenActivityStatus, isDateFieldKind, committedRowFace, activityRowProps, formatDate, parseStoredDateValue, useUserDisplays, useProjectMembers, WorkflowTaskFiltersApplied, DocRefFace, useEditField, useSaveField, dateControlValue, stopRowMouseDown, stopRowClick, dateControlKind, parseDateFieldValue, hasTimeOfDay, serializeDateFieldValue, LinkChip, BreadcrumbTail, StageFace, TrailingHairline, StaleLock, ActivityRow, ForMeEmptyState, pathSegmentTab, TabSwitch, WorkflowToolOpened, useAssignmentIdentity } from "./index.js";
12
+
13
+ import { SearchIcon } from "@sanity/icons/Search";
14
+
15
+ import { latestDeployedDefinitions, entryDocRefs, terminalState, startKindOf, isSubjectEntry, isInputSourced, isStartableDefinition, toBareId, findOpenStageEntry } from "@sanity/workflow-engine";
16
+
17
+ import { ChevronDownIcon } from "@sanity/icons/ChevronDown";
18
+
19
+ import { InlineIcon } from "@sanity/icons/Inline";
20
+
21
+ import { StackCompactIcon } from "@sanity/icons/StackCompact";
22
+
23
+ import { createPortal } from "react-dom";
24
+
25
+ import { isReferenceSchemaType } from "@sanity/types";
26
+
27
+ import { useDocumentPreviewStore, useSchema, usePerspective, useClient } from "sanity";
28
+
29
+ import { useWorkflowInstances } from "@sanity/workflow-studio";
30
+
31
+ import { WarningOutlineIcon } from "@sanity/icons/WarningOutline";
12
32
 
13
33
  import { WorkflowDiagram } from "@sanity/workflow-diagram";
14
34
 
15
- import { useSchema, useClient, useDocumentPreviewStore, usePerspective } from "sanity";
35
+ import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
36
+
37
+ import { InfoOutlineIcon } from "@sanity/icons/InfoOutline";
38
+
39
+ import { TransferIcon } from "@sanity/icons/Transfer";
40
+
41
+ import pluralize from "pluralize-esm";
42
+
43
+ import { ClockIcon } from "@sanity/icons/Clock";
44
+
45
+ import { CogIcon } from "@sanity/icons/Cog";
46
+
47
+ import { EllipsisHorizontalIcon } from "@sanity/icons/EllipsisHorizontal";
48
+
49
+ import { AddIcon } from "@sanity/icons/Add";
50
+
51
+ import { CheckmarkIcon } from "@sanity/icons/Checkmark";
52
+
53
+ import { CloseIcon } from "@sanity/icons/Close";
54
+
55
+ import { FilterIcon } from "@sanity/icons/Filter";
56
+
57
+ import { MemberAvatar, DatePicker, ClearableDatePicker } from "@sanity/workflow-components";
58
+
59
+ import { endOfDay } from "date-fns/endOfDay";
60
+
61
+ import { CalendarIcon } from "@sanity/icons/Calendar";
62
+
63
+ function MutedNote({text: text}) {
64
+ /* @__PURE__ */
65
+ return jsx(Box, {
66
+ paddingY: 3,
67
+ children: /* @__PURE__ */ jsx(Text, {
68
+ muted: !0,
69
+ size: 1,
70
+ children: text
71
+ })
72
+ });
73
+ }
74
+
75
+ function TabStatusRow({children: children}) {
76
+ /* @__PURE__ */
77
+ return jsx(Box, {
78
+ paddingLeft: 2,
79
+ paddingY: 3,
80
+ children: children
81
+ });
82
+ }
83
+
84
+ const TOOL_PANEL_PADDING = 3, UNREADABLE = {
85
+ readable: !1,
86
+ raw: void 0
87
+ };
88
+
89
+ function defaultStorage() {
90
+ try {
91
+ return globalThis.localStorage;
92
+ } catch {
93
+ return;
94
+ }
95
+ }
96
+
97
+ function readItem(storage, key) {
98
+ if (!storage) return UNREADABLE;
99
+ try {
100
+ const raw = storage.getItem(key);
101
+ return {
102
+ readable: !0,
103
+ raw: raw === null ? void 0 : raw
104
+ };
105
+ } catch {
106
+ return UNREADABLE;
107
+ }
108
+ }
109
+
110
+ function writeItem(args) {
111
+ if (!args.storage) return !1;
112
+ try {
113
+ return args.storage.setItem(args.key, args.value), !0;
114
+ } catch {
115
+ return !1;
116
+ }
117
+ }
118
+
119
+ const BAND_DEFAULT_OPEN = !0, UNGROUPED_BAND_KEY = "section:no-document", PAYLOAD_VERSION = 2, MAX_ENTRIES = 200;
120
+
121
+ function bandStateStorageKey(scope) {
122
+ return `sanity.workflows.tool.bands:${scope}`;
123
+ }
124
+
125
+ function isRecord(value) {
126
+ return typeof value == "object" && value !== null && !Array.isArray(value);
127
+ }
128
+
129
+ function bandEntryOf(value) {
130
+ if (!isRecord(value)) return;
131
+ const {open: open, touched: touched} = value;
132
+ if (!(typeof open != "boolean" || typeof touched != "number") && Number.isFinite(touched)) return {
133
+ open: open,
134
+ touched: touched
135
+ };
136
+ }
137
+
138
+ function parsePayload(raw) {
139
+ try {
140
+ const parsed = JSON.parse(raw);
141
+ if (!isRecord(parsed) || parsed.version !== PAYLOAD_VERSION || !isRecord(parsed.bands)) return;
142
+ const entries = [];
143
+ for (const [key, value] of Object.entries(parsed.bands)) {
144
+ const entry = bandEntryOf(value);
145
+ if (entry === void 0) return;
146
+ entries.push([ key, entry ]);
147
+ }
148
+ return Object.fromEntries(entries);
149
+ } catch {
150
+ return;
151
+ }
152
+ }
153
+
154
+ function readStored(storage, storageKey) {
155
+ const {readable: readable, raw: raw} = readItem(storage, storageKey);
156
+ if (!readable) return {
157
+ entries: void 0,
158
+ corrupt: !1
159
+ };
160
+ if (raw === void 0) return {
161
+ entries: {},
162
+ corrupt: !1
163
+ };
164
+ const entries = parsePayload(raw);
165
+ return {
166
+ entries: entries,
167
+ corrupt: entries === void 0
168
+ };
169
+ }
170
+
171
+ function writeStored(args) {
172
+ return writeItem({
173
+ storage: args.storage,
174
+ key: args.storageKey,
175
+ value: JSON.stringify({
176
+ version: PAYLOAD_VERSION,
177
+ bands: args.entries
178
+ })
179
+ });
180
+ }
181
+
182
+ function baseEntries(args) {
183
+ return args.stored === void 0 ? args.held : args.unpersisted ? mergeByTouched(args.stored, args.held) : args.stored;
184
+ }
185
+
186
+ function mergeByTouched(stored, held) {
187
+ const merged = {
188
+ ...stored
189
+ };
190
+ for (const [key, entry] of Object.entries(held)) {
191
+ const persisted = merged[key];
192
+ (persisted === void 0 || entry.touched >= persisted.touched) && (merged[key] = entry);
193
+ }
194
+ return merged;
195
+ }
196
+
197
+ function prune(entries) {
198
+ const held = Object.entries(entries);
199
+ return held.length <= MAX_ENTRIES ? entries : Object.fromEntries(held.sort(([, a], [, b]) => b.touched - a.touched).slice(0, MAX_ENTRIES));
200
+ }
201
+
202
+ function togglesOf(entries) {
203
+ return new Map(Object.entries(entries).map(([key, entry]) => [ key, entry.open ]));
204
+ }
205
+
206
+ function createBandStateStore(args) {
207
+ const storage = args.storage ?? defaultStorage(), initial = readStored(storage, args.storageKey);
208
+ initial.corrupt && writeStored({
209
+ storage: storage,
210
+ storageKey: args.storageKey,
211
+ entries: {}
212
+ });
213
+ let held = initial.entries ?? {}, toggles = togglesOf(held);
214
+ const subscribers = createSubscribers();
215
+ let unpersisted = !1;
216
+ return {
217
+ subscribe: subscribers.subscribe,
218
+ read: () => toggles,
219
+ setOpen(key, open) {
220
+ const stored = readStored(storage, args.storageKey).entries, base = baseEntries({
221
+ stored: stored,
222
+ held: held,
223
+ unpersisted: unpersisted
224
+ });
225
+ held = prune({
226
+ ...base,
227
+ [key]: {
228
+ open: open,
229
+ touched: Date.now()
230
+ }
231
+ }), toggles = togglesOf(held), unpersisted = !writeStored({
232
+ storage: storage,
233
+ storageKey: args.storageKey,
234
+ entries: held
235
+ }), subscribers.notify();
236
+ }
237
+ };
238
+ }
239
+
240
+ function useBandOpenState(args) {
241
+ const {segment: segment, scope: scope} = args, store = useMemo(() => createBandStateStore({
242
+ storageKey: bandStateStorageKey(scope)
243
+ }), [ scope ]);
244
+ return useMemo(() => ({
245
+ segment: segment,
246
+ store: store
247
+ }), [ segment, store ]);
248
+ }
249
+
250
+ function openOf(store, qualified) {
251
+ return store.read().get(qualified) ?? BAND_DEFAULT_OPEN;
252
+ }
253
+
254
+ function useBandOpen(bands, key) {
255
+ const {segment: segment, store: store} = bands, qualified = `${segment}/${key}`;
256
+ return {
257
+ open: useSyncExternalStore(store.subscribe, useCallback(() => openOf(store, qualified), [ store, qualified ])),
258
+ toggle: () => store.setOpen(qualified, !openOf(store, qualified))
259
+ };
260
+ }
261
+
262
+ const lastRead = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
263
+
264
+ function useDeployedDefinitions() {
265
+ const {engine: engine} = useWorkflowContext(), [definitions, setDefinitions] = useState(() => lastRead.get(engine)?.rows), [error, setError] = useState(void 0);
266
+ return useEffect(() => {
267
+ let cancelled = !1;
268
+ setDefinitions(lastRead.get(engine)?.rows), setError(void 0);
269
+ const seq = (issuedReads.get(engine) ?? 0) + 1;
270
+ return issuedReads.set(engine, seq), (async () => {
271
+ const rows = await readDeployedDefinitions(engine), latest = latestDeployedDefinitions(rows), held = lastRead.get(engine);
272
+ (held === void 0 || seq > held.seq) && lastRead.set(engine, {
273
+ seq: seq,
274
+ rows: latest
275
+ }), cancelled || (setDefinitions(latest), setError(void 0));
276
+ })().catch(err => {
277
+ cancelled || setError(describeError(err));
278
+ }), () => {
279
+ cancelled = !0;
280
+ };
281
+ }, [ engine ]), {
282
+ definitions: definitions,
283
+ error: error
284
+ };
285
+ }
286
+
287
+ const REVEAL_MAX_WAIT_MS = 2e3;
288
+
289
+ function useRevealGate(args) {
290
+ const {loading: loading, ready: ready} = args, settled = !loading && ready, expired = useDelayedFlag(!loading && !ready, REVEAL_MAX_WAIT_MS), [revealed, setRevealed] = useState(!1);
291
+ return useEffect(() => {
292
+ !revealed && (settled || expired) && setRevealed(!0);
293
+ }, [ expired, revealed, settled ]), revealed || settled || expired;
294
+ }
295
+
296
+ function documentRefsOf(instance) {
297
+ const seen = /* @__PURE__ */ new Map;
298
+ for (const ref of entryDocRefs(instance.fields)) seen.has(ref.id) || seen.set(ref.id, ref);
299
+ return [ ...seen.values() ];
300
+ }
301
+
302
+ function byTitleThenName(a, b) {
303
+ return a.title.localeCompare(b.title) || a.name.localeCompare(b.name);
304
+ }
305
+
306
+ function byResolvedTitle(titleOf, keyOf) {
307
+ return (a, b) => {
308
+ const aTitle = titleOf(a), bTitle = titleOf(b);
309
+ return aTitle !== void 0 && bTitle !== void 0 ? aTitle.localeCompare(bTitle) || keyOf(a).localeCompare(keyOf(b)) : aTitle !== void 0 ? -1 : bTitle !== void 0 ? 1 : keyOf(a).localeCompare(keyOf(b));
310
+ };
311
+ }
312
+
313
+ const BOARD_EMPTY_STAGE_NOTE = "No items";
314
+
315
+ function faceOf(instance) {
316
+ const document = documentRefsOf(instance)[0];
317
+ return document ? {
318
+ kind: "document",
319
+ document: document
320
+ } : {
321
+ kind: "instance",
322
+ title: instanceTitle(instance, definitionSnapshotOf(instance))
323
+ };
324
+ }
325
+
326
+ function boardRuns(instances) {
327
+ return instances.filter(instance => terminalState(instance) !== "aborted");
328
+ }
329
+
330
+ function groupsOf(args) {
331
+ const declared = args.definition.stages.map(stage => stage.name), retired = [ ...args.byStage.keys() ].filter(stage => !declared.includes(stage)).sort();
332
+ return [ ...declared, ...retired ].map(stage => ({
333
+ stage: stage,
334
+ title: stageTitle(args.definition, stage),
335
+ cards: args.byStage.get(stage) ?? [],
336
+ retired: !declared.includes(stage)
337
+ }));
338
+ }
339
+
340
+ function deriveBoard(args) {
341
+ const byStage = /* @__PURE__ */ new Map;
342
+ for (const instance of boardRuns(args.instances)) {
343
+ const card = {
344
+ instanceId: instance._id,
345
+ face: faceOf(instance)
346
+ }, held = byStage.get(instance.currentStage);
347
+ held ? held.push(card) : byStage.set(instance.currentStage, [ card ]);
348
+ }
349
+ return {
350
+ groups: groupsOf({
351
+ definition: args.definition,
352
+ byStage: byStage
353
+ })
354
+ };
355
+ }
356
+
357
+ function orderBoardByPreviewTitle(board, titles) {
358
+ const compare = byResolvedTitle(card => card.face.kind === "instance" ? card.face.title : titles.get(card.face.document.id), card => card.instanceId);
359
+ return {
360
+ groups: board.groups.map(group => ({
361
+ ...group,
362
+ cards: [ ...group.cards ].sort(compare)
363
+ }))
364
+ };
365
+ }
366
+
367
+ function boardDocuments(board) {
368
+ return board.groups.flatMap(group => group.cards.flatMap(card => card.face.kind === "document" ? [ card.face.document ] : []));
369
+ }
370
+
371
+ function BoardCard({card: card, size: size}) {
372
+ return size === "slim" ? card.face.kind === "instance" ? /* @__PURE__ */ jsx(InstanceFace, {
373
+ size: size,
374
+ title: card.face.title
375
+ }) : /* @__PURE__ */ jsx(DocPreviewLink, {
376
+ gdr: card.face.document,
377
+ layout: "row",
378
+ source: "board-card"
379
+ }) : /* @__PURE__ */ jsx(Card, {
380
+ border: !0,
381
+ radius: 3,
382
+ children: card.face.kind === "instance" ? /* @__PURE__ */ jsx(InstanceFace, {
383
+ size: size,
384
+ title: card.face.title
385
+ }) : /* @__PURE__ */ jsx(DocPreviewLink, {
386
+ gdr: card.face.document,
387
+ layout: "bare",
388
+ source: "board-card"
389
+ })
390
+ });
391
+ }
392
+
393
+ function InstanceFace({size: size, title: title}) {
394
+ const fill = size === "slim";
395
+ /* @__PURE__ */
396
+ return jsx(Box, {
397
+ padding: fill ? chipPadding(!0) : NOTICE_PADDING,
398
+ style: fill ? {
399
+ width: "100%"
400
+ } : {},
401
+ children: /* @__PURE__ */ jsx(Text, {
402
+ size: 1,
403
+ textOverflow: "ellipsis",
404
+ weight: "medium",
405
+ children: title
406
+ })
407
+ });
408
+ }
409
+
410
+ function StageGroupHeading({group: group}) {
411
+ /* @__PURE__ */
412
+ return jsxs(Flex, {
413
+ align: "center",
414
+ gap: 2,
415
+ children: [
416
+ /* @__PURE__ */ jsx(CountedLabel, {
417
+ count: group.cards.length,
418
+ label: /* @__PURE__ */ jsx(Text, {
419
+ size: 1,
420
+ weight: "medium",
421
+ children: group.title
422
+ })
423
+ }), group.retired ?
424
+ /* @__PURE__ */ jsx(Text, {
425
+ muted: !0,
426
+ size: 1,
427
+ children: "No longer in this workflow"
428
+ }) : null ]
429
+ });
430
+ }
431
+
432
+ const COLUMN_WIDTH = 320, PANEL_FADE = "linear-gradient(to bottom, rgb(0 0 0) 50%, rgb(0 0 0 / 0) 75%)";
433
+
434
+ function StageColumn({group: group}) {
435
+ /* @__PURE__ */
436
+ return jsxs(Box, {
437
+ flex: "none",
438
+ style: {
439
+ display: "flex",
440
+ flexDirection: "column",
441
+ position: "relative",
442
+ width: COLUMN_WIDTH
443
+ },
444
+ children: [
445
+ /* @__PURE__ */ jsx(Card, {
446
+ "aria-hidden": !0,
447
+ radius: 3,
448
+ style: {
449
+ inset: 0,
450
+ maskImage: PANEL_FADE,
451
+ position: "absolute",
452
+ WebkitMaskImage: PANEL_FADE
453
+ },
454
+ tone: "transparent"
455
+ }),
456
+ /* @__PURE__ */ jsx(Box, {
457
+ flex: "none",
458
+ paddingX: 3,
459
+ paddingY: 4,
460
+ style: {
461
+ position: "relative"
462
+ },
463
+ children: /* @__PURE__ */ jsx(StageGroupHeading, {
464
+ group: group
465
+ })
466
+ }), group.cards.length === 0 ?
467
+ /* @__PURE__ */ jsx(Flex, {
468
+ align: "center",
469
+ flex: 1,
470
+ justify: "center",
471
+ padding: 2,
472
+ style: {
473
+ position: "relative"
474
+ },
475
+ children: /* @__PURE__ */ jsx(Text, {
476
+ muted: !0,
477
+ size: 1,
478
+ children: BOARD_EMPTY_STAGE_NOTE
479
+ })
480
+ }) :
481
+ /* @__PURE__ */ jsx(Box, {
482
+ flex: 1,
483
+ overflow: "auto",
484
+ paddingBottom: 2,
485
+ paddingX: 2,
486
+ style: {
487
+ minHeight: 0,
488
+ position: "relative"
489
+ },
490
+ children: /* @__PURE__ */ jsx(Stack, {
491
+ gap: 1,
492
+ children: group.cards.map(card => /* @__PURE__ */ jsx(BoardCard, {
493
+ card: card,
494
+ size: "block"
495
+ }, card.instanceId))
496
+ })
497
+ }) ]
498
+ });
499
+ }
500
+
501
+ function StageGroups({board: board}) {
502
+ const topGap = useSpaceToken(TOOL_PANEL_PADDING);
503
+ /* @__PURE__ */
504
+ return jsx(Box, {
505
+ flex: 1,
506
+ style: {
507
+ minHeight: 0,
508
+ position: "relative"
509
+ },
510
+ children: /* @__PURE__ */ jsx(Box, {
511
+ style: {
512
+ bottom: 0,
513
+ left: 0,
514
+ overflowX: "auto",
515
+ overflowY: "hidden",
516
+ position: "absolute",
517
+ right: 0,
518
+ top: topGap
519
+ },
520
+ children: /* @__PURE__ */ jsx(Flex, {
521
+ align: "stretch",
522
+ gap: 2,
523
+ paddingX: TOOL_PANEL_PADDING,
524
+ style: {
525
+ boxSizing: "border-box",
526
+ height: "100%",
527
+ width: "max-content"
528
+ },
529
+ children: board.groups.map(group => /* @__PURE__ */ jsx(StageColumn, {
530
+ group: group
531
+ }, group.stage))
532
+ })
533
+ })
534
+ });
535
+ }
536
+
537
+ function isPlainClick(event) {
538
+ return !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey;
539
+ }
540
+
541
+ const BOARD_LAYOUTS = [ "columns", "stack" ], BOARD_DEFAULT_LAYOUT = "columns";
542
+
543
+ function boardLayoutStorageKey(scope) {
544
+ return `sanity.workflows.tool.board-layout:${scope}`;
545
+ }
546
+
547
+ function layoutOf(raw) {
548
+ return BOARD_LAYOUTS.find(layout => layout === raw) ?? BOARD_DEFAULT_LAYOUT;
549
+ }
550
+
551
+ function createBoardLayoutStore(args) {
552
+ const storage = args.storage ?? defaultStorage();
553
+ return {
554
+ read: () => layoutOf(readItem(storage, args.storageKey).raw),
555
+ set(layout) {
556
+ return writeItem({
557
+ storage: storage,
558
+ key: args.storageKey,
559
+ value: layout
560
+ }), layout;
561
+ }
562
+ };
563
+ }
564
+
565
+ const PICKER_RADIUS = 3;
566
+
567
+ function pickerLabel(face) {
568
+ return face.kind === "named" ? face.title : face.kind === "loading" ? "Loading" : "Select a workflow";
569
+ }
570
+
571
+ function WorkflowPicker({face: face, onSelect: onSelect, options: options}) {
572
+ const empty = options.length === 0, innerRadius = useRadiusToken(PICKER_RADIUS) - 1;
573
+ /* @__PURE__ */
574
+ return jsx(Card, {
575
+ border: !0,
576
+ radius: PICKER_RADIUS,
577
+ children: /* @__PURE__ */ jsxs(Flex, {
578
+ align: "center",
579
+ children: [
580
+ /* @__PURE__ */ jsx(Box, {
581
+ paddingLeft: 2,
582
+ paddingRight: 2,
583
+ children: /* @__PURE__ */ jsx(Text, {
584
+ muted: !0,
585
+ size: 1,
586
+ children: "Workflow"
587
+ })
588
+ }),
589
+ /* @__PURE__ */ jsx(MenuButton, {
590
+ button: /* @__PURE__ */ jsx(Button, {
591
+ fontSize: 1,
592
+ gap: 2,
593
+ iconRight: ChevronDownIcon,
594
+ mode: "bleed",
595
+ disabled: empty,
596
+ padding: 2,
597
+ radius: 0,
598
+ style: {
599
+ borderRadius: `0 ${innerRadius}px ${innerRadius}px 0`
600
+ },
601
+ text: pickerLabel(face)
602
+ }),
603
+ id: "workflows-board-picker",
604
+ menu: /* @__PURE__ */ jsx(Menu, {
605
+ children: options.map(option => /* @__PURE__ */ jsx(MenuItem, {
606
+ fontSize: 1,
607
+ onClick: () => onSelect(option.name),
608
+ pressed: face.kind === "named" && option.name === face.name,
609
+ text: option.title
610
+ }, option.name))
611
+ }),
612
+ popover: {
613
+ constrainSize: !0,
614
+ fallbackPlacements: [ "top-start" ],
615
+ placement: "bottom-start",
616
+ portal: !0
617
+ }
618
+ }) ]
619
+ })
620
+ });
621
+ }
622
+
623
+ const LAYOUT_LABELS = {
624
+ columns: {
625
+ icon: InlineIcon,
626
+ label: "Show as boards"
627
+ },
628
+ stack: {
629
+ icon: StackCompactIcon,
630
+ label: "Show as list"
631
+ }
632
+ };
633
+
634
+ function LayoutToggle({layout: layout, onChange: onChange}) {
635
+ /* @__PURE__ */
636
+ return jsx(Flex, {
637
+ align: "center",
638
+ gap: 1,
639
+ children: BOARD_LAYOUTS.map(value => {
640
+ const {icon: icon, label: label} = LAYOUT_LABELS[value];
641
+ /* @__PURE__ */
642
+ return jsx(HoverHint, {
643
+ text: label,
644
+ children: /* @__PURE__ */ jsx(Button, {
645
+ "aria-label": label,
646
+ "aria-pressed": value === layout,
647
+ fontSize: 1,
648
+ icon: icon,
649
+ mode: "bleed",
650
+ onClick: () => onChange(value),
651
+ padding: 2,
652
+ selected: value === layout
653
+ })
654
+ }, value);
655
+ })
656
+ });
657
+ }
658
+
659
+ function DefinitionLink({name: name, onOpen: onOpen}) {
660
+ const {resolvePathFromState: resolvePathFromState} = useRouter();
661
+ /* @__PURE__ */
662
+ return jsx(Button, {
663
+ as: "a",
664
+ fontSize: 1,
665
+ href: resolvePathFromState({
666
+ definitionName: name
667
+ }),
668
+ mode: "bleed",
669
+ onClick: event => {
670
+ isPlainClick(event) && (event.preventDefault(), onOpen(name));
671
+ },
672
+ padding: 2,
673
+ text: "View definition"
674
+ });
675
+ }
676
+
677
+ function BoardControlsPortal({definitionName: definitionName, layout: layout, onChangeLayout: onChangeLayout, face: face, onOpenDefinition: onOpenDefinition, onSelectWorkflow: onSelectWorkflow, options: options, slot: slot}) {
678
+ return slot ? createPortal(
679
+ /* @__PURE__ */ jsxs(Flex, {
680
+ align: "center",
681
+ gap: 2,
682
+ children: [
683
+ /* @__PURE__ */ jsx(WorkflowPicker, {
684
+ face: face,
685
+ onSelect: onSelectWorkflow,
686
+ options: options
687
+ }), definitionName === void 0 ? null : /* @__PURE__ */ jsx(DefinitionLink, {
688
+ name: definitionName,
689
+ onOpen: onOpenDefinition
690
+ }),
691
+ /* @__PURE__ */ jsx(Box, {
692
+ flex: 1
693
+ }),
694
+ /* @__PURE__ */ jsx(LayoutToggle, {
695
+ layout: layout,
696
+ onChange: onChangeLayout
697
+ }) ]
698
+ }), slot) : null;
699
+ }
700
+
701
+ function boardWorkflowOptions(definitions) {
702
+ return definitions.map(definition => ({
703
+ name: definition.name,
704
+ title: definition.title
705
+ })).sort(byTitleThenName);
706
+ }
707
+
708
+ function boardLandingName(selection) {
709
+ if (!(selection.kind !== "resolved" || selection.via !== "default")) return selection.definition.name;
710
+ }
711
+
712
+ function boardPickerFace(args) {
713
+ const {selection: selection, routeName: routeName} = args;
714
+ if (selection.kind === "resolved") {
715
+ const {name: name, title: title} = selection.definition;
716
+ return {
717
+ kind: "named",
718
+ name: name,
719
+ title: title
720
+ };
721
+ }
722
+ return selection.kind === "loading" && routeName !== void 0 ? {
723
+ kind: "loading"
724
+ } : {
725
+ kind: "none"
726
+ };
727
+ }
728
+
729
+ function boardSelectedPayload(selection) {
730
+ if (selection.kind === "resolved") return {
731
+ via: selection.via,
732
+ ...definitionFingerprint(selection.definition)
733
+ };
734
+ }
735
+
736
+ function resolveBoardSelection(args) {
737
+ const {definitions: definitions, routeName: routeName} = args;
738
+ if (definitions === void 0) return {
739
+ kind: "loading"
740
+ };
741
+ if (routeName !== void 0) {
742
+ const named = definitions.find(definition => definition.name === routeName);
743
+ return named ? {
744
+ kind: "resolved",
745
+ definition: named,
746
+ via: "address"
747
+ } : {
748
+ kind: "unknown",
749
+ name: routeName
750
+ };
751
+ }
752
+ const [landing] = [ ...definitions ].sort(byTitleThenName);
753
+ return landing ? {
754
+ kind: "resolved",
755
+ definition: landing,
756
+ via: "default"
757
+ } : {
758
+ kind: "empty"
759
+ };
760
+ }
761
+
762
+ function StageBand({bands: bands, group: group}) {
763
+ const {open: open, toggle: toggle} = useBandOpen(bands, group.stage);
764
+ /* @__PURE__ */
765
+ return jsx(CollapsibleBand, {
766
+ background: !0,
767
+ onToggle: toggle,
768
+ open: open,
769
+ sticky: !0,
770
+ title: group.title,
771
+ header: /* @__PURE__ */ jsx(StageGroupHeading, {
772
+ group: group
773
+ }),
774
+ children: /* @__PURE__ */ jsx(Stack, {
775
+ gap: 1,
776
+ paddingLeft: 5,
777
+ children: group.cards.length === 0 ?
778
+ /* @__PURE__ */ jsx(Box, {
779
+ padding: 3,
780
+ children: /* @__PURE__ */ jsx(Text, {
781
+ muted: !0,
782
+ size: 1,
783
+ children: BOARD_EMPTY_STAGE_NOTE
784
+ })
785
+ }) : group.cards.map(card => /* @__PURE__ */ jsx(BoardCard, {
786
+ card: card,
787
+ size: "slim"
788
+ }, card.instanceId))
789
+ })
790
+ });
791
+ }
792
+
793
+ function BoardStack({bands: bands, board: board}) {
794
+ /* @__PURE__ */
795
+ return jsx(Stack, {
796
+ gap: 1,
797
+ paddingBottom: 4,
798
+ paddingTop: 3,
799
+ paddingX: TOOL_PANEL_PADDING,
800
+ children: board.groups.map(group => /* @__PURE__ */ jsx(StageBand, {
801
+ bands: bands,
802
+ group: group
803
+ }, group.stage))
804
+ });
805
+ }
806
+
807
+ const ALIAS_PREFIX = "__seed_", MAX_PATH_DEPTH = 3, IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
808
+
809
+ function previewSelectOf(schemaType) {
810
+ const select = schemaType.preview?.select;
811
+ if (!(select === void 0 || Object.keys(select).length === 0)) return select;
812
+ }
813
+
814
+ function fieldTypeOf(parent, name) {
815
+ if (!(!("fields" in parent) || !Array.isArray(parent.fields))) return parent.fields.find(field => field.name === name)?.type;
816
+ }
817
+
818
+ function stepOf(args) {
819
+ const {parent: parent, segment: segment, terminal: terminal} = args, fieldType = fieldTypeOf(parent, segment);
820
+ if (fieldType !== void 0) {
821
+ if (terminal) return {
822
+ part: segment,
823
+ next: fieldType
824
+ };
825
+ if (isReferenceSchemaType(fieldType)) {
826
+ const target = fieldType.to.length === 1 ? fieldType.to[0] : void 0;
827
+ return target === void 0 ? void 0 : {
828
+ part: `${segment}->`,
829
+ next: target
830
+ };
831
+ }
832
+ return {
833
+ part: `${segment}.`,
834
+ next: fieldType
835
+ };
836
+ }
837
+ }
838
+
839
+ function compilePath(root, path) {
840
+ const segments = path.split(".");
841
+ if (segments.length > MAX_PATH_DEPTH || segments.some(s => !IDENTIFIER.test(s))) return;
842
+ let parent = root;
843
+ const parts = [];
844
+ for (const [index, segment] of segments.entries()) {
845
+ const step = stepOf({
846
+ parent: parent,
847
+ segment: segment,
848
+ terminal: index === segments.length - 1
849
+ });
850
+ if (step === void 0) return;
851
+ parts.push(step.part), parent = step.next;
852
+ }
853
+ return parts.join("");
854
+ }
855
+
856
+ function selectArmOf(schemaType) {
857
+ const select = previewSelectOf(schemaType);
858
+ if (select === void 0) return;
859
+ const entries = [];
860
+ for (const [key, path] of Object.entries(select)) {
861
+ const compiled = compilePath(schemaType, path);
862
+ if (compiled === void 0) return;
863
+ entries.push(`${JSON.stringify(ALIAS_PREFIX + key)}: ${compiled}`);
864
+ }
865
+ return `_type == ${JSON.stringify(schemaType.name)} => {${entries.join(", ")}}`;
866
+ }
867
+
868
+ function planTitleSeed(types) {
869
+ const seedable = /* @__PURE__ */ new Map, arms = [];
870
+ for (const type of types) {
871
+ const arm = selectArmOf(type);
872
+ arm !== void 0 && (seedable.set(type.name, type), arms.push(arm));
873
+ }
874
+ return {
875
+ query: `*[_id in $ids]{_id, _type${arms.length > 0 ? `, ...select(${arms.join(", ")})` : ""}}`,
876
+ seedable: seedable
877
+ };
878
+ }
16
879
 
17
- import { startKindOf, isSubjectEntry, isInputSourced, latestDeployedDefinitions, terminalState, entryDocRefs, isStartableDefinition, toBareId, findOpenStageEntry } from "@sanity/workflow-engine";
880
+ function isSeedRow(value) {
881
+ if (typeof value != "object" || value === null) return !1;
882
+ const row = value;
883
+ return typeof row._id == "string" && typeof row._type == "string";
884
+ }
18
885
 
19
- import { InfoOutlineIcon } from "@sanity/icons/InfoOutline";
886
+ function seededTitleOf(row, schemaType) {
887
+ const select = previewSelectOf(schemaType);
888
+ if (select === void 0) return;
889
+ const selected = {};
890
+ for (const key of Object.keys(select)) selected[key] = row[ALIAS_PREFIX + key];
891
+ const prepare = schemaType.preview?.prepare;
892
+ let title;
893
+ if (prepare === void 0) title = selected.title; else try {
894
+ title = prepare(selected).title;
895
+ } catch (err) {
896
+ console.error(`[workflow-studio-plugin] preview prepare for "${schemaType.name}" failed on seed:`, err);
897
+ return;
898
+ }
899
+ return typeof title == "string" ? title : void 0;
900
+ }
20
901
 
21
- import pluralize from "pluralize-esm";
902
+ function seedFromRows(args) {
903
+ const rows = args.rows.filter(isSeedRow), byBareId = new Map(rows.map(row => [ row._id, row ])), types = new Map(rows.map(row => [ row._id, row._type ])), titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set;
904
+ for (const {gdrId: gdrId, bareId: bareId} of args.docs) {
905
+ const row = byBareId.get(bareId);
906
+ if (row === void 0) continue;
907
+ const schemaType = args.seedable.get(row._type);
908
+ if (schemaType === void 0) continue;
909
+ resolved.add(gdrId);
910
+ const title = seededTitleOf(row, schemaType);
911
+ title !== void 0 && titles.set(gdrId, title);
912
+ }
913
+ return {
914
+ types: types,
915
+ titles: {
916
+ titles: titles,
917
+ resolved: resolved
918
+ }
919
+ };
920
+ }
22
921
 
23
- import { useWorkflowInstances } from "@sanity/workflow-studio";
922
+ function titleDrift(seed, live) {
923
+ const drifted = [];
924
+ for (const gdrId of seed.resolved) {
925
+ if (!live.resolved.has(gdrId)) continue;
926
+ const seeded = seed.titles.get(gdrId), observed = live.titles.get(gdrId);
927
+ seeded !== observed && drifted.push({
928
+ gdrId: gdrId,
929
+ seeded: seeded,
930
+ observed: observed
931
+ });
932
+ }
933
+ return drifted;
934
+ }
24
935
 
25
- import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
936
+ function mergeSeededTitles(live, seed) {
937
+ if (seed.resolved.size === 0) return live;
938
+ const titles = new Map(live.titles), resolved = new Set(live.resolved);
939
+ for (const gdrId of seed.resolved) {
940
+ if (live.resolved.has(gdrId)) continue;
941
+ resolved.add(gdrId);
942
+ const title = seed.titles.get(gdrId);
943
+ title !== void 0 && titles.set(gdrId, title);
944
+ }
945
+ return {
946
+ titles: titles,
947
+ resolved: resolved
948
+ };
949
+ }
26
950
 
27
- import { ClockIcon } from "@sanity/icons/Clock";
951
+ const NO_TITLES = {
952
+ titles: /* @__PURE__ */ new Map,
953
+ resolved: /* @__PURE__ */ new Set
954
+ };
28
955
 
29
- import { CogIcon } from "@sanity/icons/Cog";
956
+ function useDocPreviewTitles(documents) {
957
+ 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("|");
958
+ return useEffect(() => {
959
+ const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
960
+ titles: new Map(titles),
961
+ resolved: new Set(resolved)
962
+ }), localDocs = documents.flatMap(doc => {
963
+ const {parsed: parsed, local: local} = gdrLocality(doc.id, binding.contentResource);
964
+ return !parsed || !local ? (resolved.add(doc.id), []) : [ {
965
+ doc: doc,
966
+ bareId: parsed.documentId
967
+ } ];
968
+ }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
969
+ previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
970
+ }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
971
+ const schemaType = openableSchemaType(schema, typeName);
972
+ if (!schemaType) return !1;
973
+ if (previews.get(docId)?.typeName === typeName) return !0;
974
+ dropPreview(docId), resolved.delete(docId);
975
+ const subscription = previewStore.observeForPreview({
976
+ _id: bareId,
977
+ _type: schemaType.name
978
+ }, schemaType, {
979
+ perspective: perspectiveStack
980
+ }).subscribe({
981
+ next: event => {
982
+ resolved.add(docId);
983
+ const title = event.snapshot?.title;
984
+ typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
985
+ },
986
+ error: err => {
987
+ console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
988
+ publish();
989
+ }
990
+ });
991
+ return previews.set(docId, {
992
+ typeName: typeName,
993
+ unsubscribe: () => subscription.unsubscribe()
994
+ }), !0;
995
+ }, evaluate = () => {
996
+ for (const {doc: doc, bareId: bareId} of localDocs) {
997
+ const probe = actualTypes.read(bareId);
998
+ if (probe === null) {
999
+ dropPreview(doc.id), resolved.add(doc.id);
1000
+ continue;
1001
+ }
1002
+ !observe({
1003
+ docId: doc.id,
1004
+ bareId: bareId,
1005
+ typeName: probe ?? doc.type
1006
+ }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
1007
+ }
1008
+ publish();
1009
+ }, unsubscribeProbes = actualTypes.subscribe(evaluate);
1010
+ return evaluate(), () => {
1011
+ unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
1012
+ };
1013
+ }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
1014
+ state;
1015
+ }
30
1016
 
31
- import { EllipsisHorizontalIcon } from "@sanity/icons/EllipsisHorizontal";
1017
+ function usePreviewSeed(documents) {
1018
+ const client = useClient({
1019
+ apiVersion: WORKFLOW_API_VERSION
1020
+ }), schema = useSchema(), {perspectiveStack: perspectiveStack} = usePerspective(), {actualTypes: actualTypes, binding: binding} = useWorkflowContext(), [seed, setSeed] = useState(NO_TITLES), docs = useMemo(() => documents.flatMap(doc => {
1021
+ const {parsed: parsed, local: local} = gdrLocality(doc.id, binding.contentResource);
1022
+ return parsed === void 0 || !local ? [] : [ {
1023
+ gdrId: doc.id,
1024
+ bareId: parsed.documentId
1025
+ } ];
1026
+ }), [ documents, binding.contentResource ]), key = docs.map(doc => doc.gdrId).join("|");
1027
+ return useEffect(() => {
1028
+ if (setSeed(NO_TITLES), key === "") return;
1029
+ const plan = planTitleSeed(schema.getTypeNames().flatMap(name => openableSchemaType(schema, name) ?? []));
1030
+ let cancelled = !1;
1031
+ return client.fetch(plan.query, {
1032
+ ids: docs.map(doc => doc.bareId)
1033
+ }, {
1034
+ tag: "workflows.preview-seed",
1035
+ ...perspectiveStack.length > 0 ? {
1036
+ perspective: [ ...perspectiveStack ]
1037
+ } : {}
1038
+ }).then(rows => {
1039
+ if (cancelled) return;
1040
+ const result = seedFromRows({
1041
+ rows: rows,
1042
+ docs: docs,
1043
+ seedable: plan.seedable
1044
+ });
1045
+ actualTypes.seed(result.types), setSeed(result.titles);
1046
+ }).catch(err => {
1047
+ cancelled || console.error("[workflow-studio-plugin] preview seed failed:", err);
1048
+ }), () => {
1049
+ cancelled = !0;
1050
+ };
1051
+ }, [ key, client, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
1052
+ seed;
1053
+ }
32
1054
 
33
- import { AddIcon } from "@sanity/icons/Add";
1055
+ function useSeededTitles(args) {
1056
+ const {ordering: ordering, warm: warm} = args, seed = usePreviewSeed(warm), live = useDocPreviewTitles(ordering), telemetry = useWorkflowTelemetry(), warned = useRef(/* @__PURE__ */ new Set);
1057
+ return useEffect(() => {
1058
+ const fresh = titleDrift(seed, live).filter(drift => !warned.current.has(drift.gdrId));
1059
+ if (fresh.length !== 0) {
1060
+ warned.current.size === 0 && telemetry.log(WorkflowTitleSeedDrifted, {
1061
+ driftCount: fresh.length
1062
+ });
1063
+ for (const drift of fresh) warned.current.add(drift.gdrId), console.warn(`[workflow-studio-plugin] seeded preview title for "${drift.gdrId}" diverged from the live pipeline — the seed projection or prepare mirroring is drifting from Studio preview resolution`);
1064
+ }
1065
+ }, [ seed, live, telemetry ]), useMemo(() => {
1066
+ const merged = mergeSeededTitles(live, seed);
1067
+ return {
1068
+ ...merged,
1069
+ ready: ordering.every(doc => merged.resolved.has(doc.id))
1070
+ };
1071
+ }, [ ordering, live, seed ]);
1072
+ }
34
1073
 
35
- import { SearchIcon } from "@sanity/icons/Search";
1074
+ function useGroupTitles(groups) {
1075
+ const ordering = useMemo(() => groups.withDocuments.map(group => group.document), [ groups ]), warm = useMemo(() => groups.withDocuments.flatMap(group => group.documents), [ groups ]);
1076
+ return useSeededTitles({
1077
+ ordering: ordering,
1078
+ warm: warm
1079
+ });
1080
+ }
36
1081
 
37
- import { CheckmarkIcon } from "@sanity/icons/Checkmark";
1082
+ const NO_DOCUMENTS = [];
38
1083
 
39
- import { CloseIcon } from "@sanity/icons/Close";
1084
+ function useBoard(args) {
1085
+ const {definition: definition, instances: instances} = args, derived = useMemo(() => definition ? deriveBoard({
1086
+ instances: instances,
1087
+ definition: definition
1088
+ }) : void 0, [ definition, instances ]), documents = useMemo(() => derived ? boardDocuments(derived) : NO_DOCUMENTS, [ derived ]), {titles: titles, ready: ready} = useSeededTitles({
1089
+ ordering: documents,
1090
+ warm: documents
1091
+ }), board = useMemo(() => derived ? orderBoardByPreviewTitle(derived, titles) : void 0, [ derived, titles ]);
1092
+ return useMemo(() => ({
1093
+ board: board,
1094
+ ready: ready
1095
+ }), [ board, ready ]);
1096
+ }
40
1097
 
41
- import { FilterIcon } from "@sanity/icons/Filter";
1098
+ const INSTANCE_CAP = 200;
42
1099
 
43
- import { MemberAvatar, DatePicker, ClearableDatePicker } from "@sanity/workflow-components";
1100
+ function useCappedInstances(filter) {
1101
+ const {engine: engine} = useWorkflowContext(), capped = useMemo(() => ({
1102
+ ...filter,
1103
+ limit: INSTANCE_CAP
1104
+ }), [ filter ]), {instances: instances, loading: loading, unreadable: unreadable} = useWorkflowInstances({
1105
+ engine: engine,
1106
+ filter: capped
1107
+ });
1108
+ return useMemo(() => {
1109
+ const rows = instances ?? [];
1110
+ return {
1111
+ rows: rows,
1112
+ truncated: rows.length + unreadable.length === INSTANCE_CAP,
1113
+ loading: loading,
1114
+ unreadable: unreadable
1115
+ };
1116
+ }, [ instances, loading, unreadable ]);
1117
+ }
44
1118
 
45
- import { endOfDay } from "date-fns/endOfDay";
1119
+ const BOARD_CAP_NOTE = `Showing the latest ${INSTANCE_CAP} workflows — older ones aren’t on the board.`, NOTHING = {
1120
+ ids: []
1121
+ };
46
1122
 
47
- import { createPortal } from "react-dom";
1123
+ function useBoardInstances(definitionName) {
1124
+ const filter = useMemo(() => definitionName === void 0 ? NOTHING : {
1125
+ definition: definitionName,
1126
+ includeCompleted: !0
1127
+ }, [ definitionName ]);
1128
+ return useCappedInstances(filter);
1129
+ }
48
1130
 
49
- import { CalendarIcon } from "@sanity/icons/Calendar";
1131
+ function useBoardLayout(scope) {
1132
+ const store = useMemo(() => createBoardLayoutStore({
1133
+ storageKey: boardLayoutStorageKey(scope)
1134
+ }), [ scope ]), [held, setHeld] = useState(() => ({
1135
+ store: store,
1136
+ layout: store.read()
1137
+ }));
1138
+ return held.store !== store && setHeld({
1139
+ store: store,
1140
+ layout: store.read()
1141
+ }), {
1142
+ layout: held.layout,
1143
+ setLayout: layout => setHeld({
1144
+ store: store,
1145
+ layout: store.set(layout)
1146
+ })
1147
+ };
1148
+ }
50
1149
 
51
- function MutedNote({text: text}) {
1150
+ function DocumentsTab({filterSlot: filterSlot, onOpenDefinition: onOpenDefinition, onSelectWorkflow: onSelectWorkflow, routeDefinition: routeDefinition}) {
1151
+ const {binding: binding} = useWorkflowContext(), telemetry = useWorkflowTelemetry(), {definitions: definitions, error: error} = useDeployedDefinitions(), {layout: layout, setLayout: setLayout} = useBoardLayout(binding.engineResource.id), bands = useBandOpenState({
1152
+ segment: "documents",
1153
+ scope: binding.engineResource.id
1154
+ }), selection = resolveBoardSelection({
1155
+ definitions: definitions,
1156
+ routeName: routeDefinition
1157
+ }), selected = selection.kind === "resolved" ? selection.definition : void 0, instances = useBoardInstances(selected?.name), {board: board, ready: ready} = useBoard({
1158
+ definition: selected,
1159
+ instances: instances.rows
1160
+ }), revealed = useRevealGate({
1161
+ loading: instances.loading,
1162
+ ready: ready
1163
+ }), landing = boardLandingName(selection);
1164
+ useEffect(() => {
1165
+ landing !== void 0 && onSelectWorkflow(landing, {
1166
+ replace: !0
1167
+ });
1168
+ }, [ landing, onSelectWorkflow ]);
1169
+ const selectedPayload = boardSelectedPayload(selection);
52
1170
  /* @__PURE__ */
53
- return jsx(Box, {
54
- paddingY: 3,
55
- children: /* @__PURE__ */ jsx(Text, {
56
- muted: !0,
57
- size: 1,
58
- children: text
1171
+ return jsxs(Flex, {
1172
+ direction: "column",
1173
+ flex: 1,
1174
+ style: {
1175
+ minHeight: 0
1176
+ },
1177
+ children: [
1178
+ /* @__PURE__ */ jsx(BoardControlsPortal, {
1179
+ definitionName: selected?.name,
1180
+ layout: layout,
1181
+ onOpenDefinition: onOpenDefinition,
1182
+ onChangeLayout: next => {
1183
+ next !== layout && (telemetry.log(WorkflowBoardLayoutChanged, {
1184
+ layout: next
1185
+ }), setLayout(next));
1186
+ },
1187
+ onSelectWorkflow: onSelectWorkflow,
1188
+ options: boardWorkflowOptions(definitions ?? []),
1189
+ face: boardPickerFace({
1190
+ selection: selection,
1191
+ routeName: routeDefinition
1192
+ }),
1193
+ slot: filterSlot
1194
+ }),
1195
+ /* @__PURE__ */ jsxs(Flex, {
1196
+ direction: "column",
1197
+ flex: 1,
1198
+ gap: 2,
1199
+ style: {
1200
+ minHeight: 0
1201
+ },
1202
+ children: [ instances.unreadable.length === 0 ? null : /* @__PURE__ */ jsx(Box, {
1203
+ paddingX: TOOL_PANEL_PADDING,
1204
+ children: /* @__PURE__ */ jsx(UnreadableDocsNote, {
1205
+ unreadable: instances.unreadable
1206
+ })
1207
+ }),
1208
+ /* @__PURE__ */ jsx(BoardBody, {
1209
+ bands: bands,
1210
+ board: board,
1211
+ error: error,
1212
+ layout: layout,
1213
+ loading: !revealed,
1214
+ selection: selection
1215
+ }), instances.truncated ? /* @__PURE__ */ jsx(Box, {
1216
+ paddingX: TOOL_PANEL_PADDING,
1217
+ children: /* @__PURE__ */ jsx(MutedNote, {
1218
+ text: BOARD_CAP_NOTE
1219
+ })
1220
+ }) : null ]
1221
+ }), selectedPayload === void 0 ? null :
1222
+ /* @__PURE__ */ jsx(LogEventOnMount, {
1223
+ data: selectedPayload,
1224
+ event: WorkflowBoardWorkflowSelected
1225
+ }, selected?.name) ]
1226
+ });
1227
+ }
1228
+
1229
+ function BoardBody({bands: bands, board: board, error: error, layout: layout, loading: loading, selection: selection}) {
1230
+ return error !== void 0 ? /* @__PURE__ */ jsx(StatusBox, {
1231
+ children: /* @__PURE__ */ jsx(MutedNote, {
1232
+ text: `Could not load the deployed workflows: ${error}`
1233
+ })
1234
+ }) : selection.kind === "loading" ? /* @__PURE__ */ jsx(StatusBox, {
1235
+ children: /* @__PURE__ */ jsx(TabStatusRow, {
1236
+ children: /* @__PURE__ */ jsx(LoadingRow, {
1237
+ label: "Loading workflows…"
1238
+ })
1239
+ })
1240
+ }) : selection.kind === "empty" ? /* @__PURE__ */ jsx(StatusBox, {
1241
+ children: /* @__PURE__ */ jsx(EmptyState, {
1242
+ description: "Deploy a workflow to see its documents here",
1243
+ icon: /* @__PURE__ */ jsx(SearchIcon, {}),
1244
+ title: "No workflows deployed"
1245
+ })
1246
+ }) : selection.kind === "unknown" ? /* @__PURE__ */ jsx(StatusBox, {
1247
+ children: /* @__PURE__ */ jsx(EmptyState, {
1248
+ description: `No deployed workflow is named “${selection.name}” — pick one above`,
1249
+ icon: /* @__PURE__ */ jsx(SearchIcon, {}),
1250
+ title: "Workflow not found"
1251
+ })
1252
+ }) : loading || board === void 0 ? /* @__PURE__ */ jsx(StatusBox, {
1253
+ children: /* @__PURE__ */ jsx(TabStatusRow, {
1254
+ children: /* @__PURE__ */ jsx(LoadingRow, {
1255
+ label: "Loading documents…"
1256
+ })
59
1257
  })
1258
+ }) : layout === "stack" ? /* @__PURE__ */ jsx(BoardStack, {
1259
+ bands: bands,
1260
+ board: board
1261
+ }) : /* @__PURE__ */ jsx(StageGroups, {
1262
+ board: board
1263
+ });
1264
+ }
1265
+
1266
+ function StatusBox({children: children}) {
1267
+ /* @__PURE__ */
1268
+ return jsx(Flex, {
1269
+ direction: "column",
1270
+ flex: 1,
1271
+ paddingX: TOOL_PANEL_PADDING,
1272
+ children: children
60
1273
  });
61
1274
  }
62
1275
 
63
1276
  function definitionCatalog(args) {
1277
+ const rejected = [ ...args.issues.values() ];
64
1278
  return args.definitions.map(definition => ({
65
1279
  name: definition.name,
66
1280
  title: definition.title ?? definition.name,
67
1281
  description: definition.description,
68
1282
  activeCount: args.inFlight.filter(instance => instance.definition === definition.name).length,
69
- docTypes: [ ...new Set(args.mappings.filter(mapping => mapping.definition === definition.name).map(mapping => mapping.docType)) ]
70
- })).sort((a, b) => a.title.localeCompare(b.title) || a.name.localeCompare(b.name));
1283
+ docTypes: [ ...new Set(args.mappings.filter(mapping => mapping.definition === definition.name).map(mapping => mapping.docType)) ],
1284
+ brokenBindings: rejected.filter(issue => issue.definition === definition.name && issue.version === definition.version).map(issue => ({
1285
+ docType: issue.docType,
1286
+ detail: mappingIssueDetail(issue)
1287
+ })).sort((a, b) => a.docType.localeCompare(b.docType))
1288
+ })).sort(byTitleThenName);
1289
+ }
1290
+
1291
+ function undeployedWorkflowNotices(args) {
1292
+ return [ ...args.issues.values() ].filter(namesNoDeployedDefinition).sort((a, b) => a.definition.localeCompare(b.definition) || a.docType.localeCompare(b.docType)).map(issue => `“${issue.definition}” is set up to run on ${args.titleOf(issue.docType) ?? issue.docType} documents, but no deployed workflow has that name`);
71
1293
  }
72
1294
 
73
1295
  function docTypeLabels(docTypes, titleOf) {
74
1296
  return docTypes.map(name => ({
75
1297
  name: name,
76
1298
  title: titleOf(name) ?? name
77
- })).sort((a, b) => a.title.localeCompare(b.title) || a.name.localeCompare(b.name));
1299
+ })).sort(byTitleThenName);
78
1300
  }
79
1301
 
80
1302
  function startPhrase(definition) {
81
- if (definition.lifecycle === "child") return "Spawned by a parent workflow";
1303
+ if (definition.lifecycle === "child") return "Started by a parent workflow";
82
1304
  if (startKindOf(definition) === "autonomous") return "Starts automatically";
83
1305
  const subject = (definition.fields ?? []).find(isSubjectEntry);
84
1306
  return subject !== void 0 && isInputSourced(subject) ? "Started manually, from a document" : "Started manually";
@@ -88,6 +1310,37 @@ function deployedAtOf(definition) {
88
1310
  return typeof definition._updatedAt == "string" ? definition._updatedAt : void 0;
89
1311
  }
90
1312
 
1313
+ function deployedPhrase(definition) {
1314
+ const deployedAt = deployedAtOf(definition);
1315
+ if (deployedAt === void 0) return `v${definition.version}`;
1316
+ const ago = formatTimeAgo(deployedAt);
1317
+ return ago === "" ? formatShortDateTime(deployedAt) : `${formatShortDateTime(deployedAt)} (${ago})`;
1318
+ }
1319
+
1320
+ function DetailNotFound({back: back, message: message}) {
1321
+ /* @__PURE__ */
1322
+ return jsxs(Stack, {
1323
+ gap: 4,
1324
+ children: [
1325
+ /* @__PURE__ */ jsx(Text, {
1326
+ muted: !0,
1327
+ size: 1,
1328
+ children: message
1329
+ }),
1330
+ /* @__PURE__ */ jsx(Flex, {
1331
+ children: /* @__PURE__ */ jsx(Button, {
1332
+ fontSize: 1,
1333
+ gap: 2,
1334
+ icon: ArrowLeftIcon,
1335
+ mode: "ghost",
1336
+ onClick: back.go,
1337
+ padding: 2,
1338
+ text: back.label
1339
+ })
1340
+ }) ]
1341
+ });
1342
+ }
1343
+
91
1344
  function DetailTitle({busy: busy = !1, documentGdr: documentGdr, title: title}) {
92
1345
  const chipPullback = -useSpaceToken(1);
93
1346
  /* @__PURE__ */
@@ -122,7 +1375,8 @@ function DetailTitle({busy: busy = !1, documentGdr: documentGdr, title: title})
122
1375
  },
123
1376
  children: /* @__PURE__ */ jsx(DocPreviewLink, {
124
1377
  gdr: documentGdr,
125
- layout: "inline"
1378
+ layout: "inline",
1379
+ source: "detail-title"
126
1380
  })
127
1381
  }) ]
128
1382
  }),
@@ -132,46 +1386,59 @@ function DetailTitle({busy: busy = !1, documentGdr: documentGdr, title: title})
132
1386
  });
133
1387
  }
134
1388
 
135
- const lastRead = /* @__PURE__ */ new WeakMap, issuedReads = /* @__PURE__ */ new WeakMap;
1389
+ const INSTANCE_CAP_COUNT_NOTE = `Counts from the latest ${INSTANCE_CAP} workflows older ones aren’t counted.`, NEWEST_INSTANCES = {
1390
+ includeCompleted: !0
1391
+ };
136
1392
 
137
- function useDeployedDefinitions() {
138
- const {engine: engine} = useWorkflowContext(), [definitions, setDefinitions] = useState(() => lastRead.get(engine)?.rows), [error, setError] = useState(void 0);
139
- return useEffect(() => {
140
- let cancelled = !1;
141
- setDefinitions(lastRead.get(engine)?.rows), setError(void 0);
142
- const seq = (issuedReads.get(engine) ?? 0) + 1;
143
- return issuedReads.set(engine, seq), (async () => {
144
- const rows = await readDeployedDefinitions(engine), latest = latestDeployedDefinitions(rows), held = lastRead.get(engine);
145
- (held === void 0 || seq > held.seq) && lastRead.set(engine, {
146
- seq: seq,
147
- rows: latest
148
- }), cancelled || (setDefinitions(latest), setError(void 0));
149
- })().catch(err => {
150
- cancelled || setError(describeError(err));
151
- }), () => {
152
- cancelled = !0;
153
- };
154
- }, [ engine ]), {
155
- definitions: definitions,
156
- error: error
157
- };
1393
+ function useToolInstances() {
1394
+ const {rows: rows, truncated: truncated, loading: loading, unreadable: unreadable} = useCappedInstances(NEWEST_INSTANCES);
1395
+ return useMemo(() => ({
1396
+ inFlight: rows.filter(instance => terminalState(instance) === "in-flight"),
1397
+ settled: rows.filter(instance => terminalState(instance) !== "in-flight"),
1398
+ truncated: truncated,
1399
+ loading: loading,
1400
+ unreadable: unreadable
1401
+ }), [ rows, truncated, loading, unreadable ]);
158
1402
  }
159
1403
 
160
- function DefinitionDetail({definitionName: definitionName}) {
161
- const {definitions: definitions, error: error} = useDeployedDefinitions(), snapshotWidth = useContainerToken(2), definition = definitions?.find(row => row.name === definitionName);
162
- /* @__PURE__ */
163
- return jsxs(Flex, {
1404
+ function DefinitionDetail({definitionName: definitionName, backToList: backToList}) {
1405
+ const {definitions: definitions, error: error} = useDeployedDefinitions(), definition = definitions?.find(row => row.name === definitionName), loading = definitions === void 0 && error === void 0, viewed = loading || error !== void 0 ? null : /* @__PURE__ */ jsx(LogEventOnMount, {
1406
+ data: {
1407
+ found: definition !== void 0,
1408
+ ...definitionFingerprint(definition)
1409
+ },
1410
+ event: WorkflowDefinitionDetailViewed
1411
+ });
1412
+ return !loading && error === void 0 && definition === void 0 ? /* @__PURE__ */ jsxs(Box, {
1413
+ paddingX: 3,
1414
+ children: [ viewed,
1415
+ /* @__PURE__ */ jsx(TabStatusRow, {
1416
+ children: /* @__PURE__ */ jsx(DetailNotFound, {
1417
+ back: backToList,
1418
+ message: /* @__PURE__ */ jsxs(Fragment, {
1419
+ children: [ "Unable to find a definition with name “",
1420
+ /* @__PURE__ */ jsx("strong", {
1421
+ children: definitionName
1422
+ }), "”." ]
1423
+ })
1424
+ })
1425
+ }) ]
1426
+ }) : /* @__PURE__ */ jsxs(Flex, {
164
1427
  direction: "column",
165
1428
  height: "fill",
166
1429
  overflow: "hidden",
167
- children: [
1430
+ children: [ viewed,
168
1431
  /* @__PURE__ */ jsx(Box, {
169
1432
  flex: "none",
170
1433
  paddingBottom: 2,
171
1434
  paddingTop: 3,
172
1435
  paddingX: 3,
173
- children: /* @__PURE__ */ jsx(DetailTitle, {
174
- busy: definitions === void 0 && error === void 0,
1436
+ children: loading ? /* @__PURE__ */ jsx(Box, {
1437
+ paddingLeft: 2,
1438
+ children: /* @__PURE__ */ jsx(LoadingRow, {
1439
+ label: "Loading definition…"
1440
+ })
1441
+ }) : /* @__PURE__ */ jsx(DetailTitle, {
175
1442
  title: definition?.title ?? definitionName
176
1443
  })
177
1444
  }),
@@ -179,125 +1446,176 @@ function DefinitionDetail({definitionName: definitionName}) {
179
1446
  flex: 1,
180
1447
  overflow: "auto",
181
1448
  paddingX: 3,
182
- children: /* @__PURE__ */ jsx(Box, {
1449
+ children: /* @__PURE__ */ jsx(Flex, {
1450
+ direction: "column",
183
1451
  paddingBottom: 5,
184
1452
  paddingLeft: 2,
185
- paddingTop: 2,
1453
+ paddingTop: 4,
186
1454
  style: {
187
- maxWidth: snapshotWidth
1455
+ boxSizing: "border-box",
1456
+ minHeight: "100%"
188
1457
  },
189
1458
  children: /* @__PURE__ */ jsx(DefinitionBody, {
190
1459
  definition: definition,
191
- error: error,
192
- loaded: definitions !== void 0
1460
+ error: error
193
1461
  })
194
1462
  })
195
1463
  }) ]
196
1464
  });
197
1465
  }
198
1466
 
199
- function DefinitionBody({definition: definition, error: error, loaded: loaded}) {
1467
+ function DefinitionBody({definition: definition, error: error}) {
200
1468
  return error !== void 0 ? /* @__PURE__ */ jsx(MutedNote, {
201
1469
  text: `Could not load the deployed workflows: ${error}`
202
- }) : loaded ? definition === void 0 ? /* @__PURE__ */ jsx(MutedNote, {
203
- text: "No deployed workflow definition with this name — it may have been retracted."
204
- }) : /* @__PURE__ */ jsx(DefinitionFacts, {
1470
+ }) : definition === void 0 ? null : /* @__PURE__ */ jsx(DefinitionFacts, {
205
1471
  definition: definition
206
- }) : /* @__PURE__ */ jsx(LoadingRow, {
207
- label: "Loading definition…"
1472
+ });
1473
+ }
1474
+
1475
+ function SetupIssues({bindings: bindings}) {
1476
+ const schema = useSchema();
1477
+ return bindings.length === 0 ? null : /* @__PURE__ */ jsx(Card, {
1478
+ padding: 3,
1479
+ radius: 3,
1480
+ tone: "caution",
1481
+ children: /* @__PURE__ */ jsxs(Flex, {
1482
+ align: "flex-start",
1483
+ gap: 3,
1484
+ children: [
1485
+ /* @__PURE__ */ jsx(Text, {
1486
+ size: 1,
1487
+ children: /* @__PURE__ */ jsx(WarningOutlineIcon, {})
1488
+ }),
1489
+ /* @__PURE__ */ jsxs(Stack, {
1490
+ flex: 1,
1491
+ gap: 3,
1492
+ children: [
1493
+ /* @__PURE__ */ jsx(Text, {
1494
+ size: 1,
1495
+ weight: "semibold",
1496
+ children: "Setup issue"
1497
+ }), bindings.map(binding => /* @__PURE__ */ jsx(Text, {
1498
+ size: 1,
1499
+ children: `${schema.get(binding.docType)?.title ?? binding.docType}: ${binding.detail}.`
1500
+ }, binding.docType)),
1501
+ /* @__PURE__ */ jsx(Text, {
1502
+ muted: !0,
1503
+ size: 1,
1504
+ children: "Editors see this workflow disabled on these document types until a developer fixes it."
1505
+ }) ]
1506
+ }) ]
1507
+ })
208
1508
  });
209
1509
  }
210
1510
 
211
1511
  function DefinitionFacts({definition: definition}) {
212
- const {binding: binding, mappings: mappings} = useWorkflowContext(), schema = useSchema(), deployedAt = deployedAtOf(definition), [row] = definitionCatalog({
1512
+ const {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), snapshotWidth = useContainerToken(2), [row] = definitionCatalog({
213
1513
  definitions: [ definition ],
214
1514
  mappings: mappings,
215
- inFlight: []
1515
+ inFlight: instances.inFlight,
1516
+ issues: mappingIssues
216
1517
  }), runsOn = docTypeLabels(row?.docTypes ?? [], name => schema.get(name)?.title).map(docType => docType.title);
217
1518
  /* @__PURE__ */
218
- return jsxs(Stack, {
1519
+ return jsxs(Flex, {
1520
+ direction: "column",
1521
+ flex: 1,
219
1522
  gap: 4,
220
- children: [
221
- /* @__PURE__ */ jsx(MetaRow, {
222
- label: "Deployed",
223
- children: /* @__PURE__ */ jsxs(Text, {
224
- size: 1,
225
- children: [ deployedAt === void 0 ? `v${definition.version}` : formatDate(deployedAt), " ·", " ", binding.tag ]
226
- })
227
- }),
228
- /* @__PURE__ */ jsx(MetaRow, {
229
- label: "Trigger",
230
- children: /* @__PURE__ */ jsx(Text, {
231
- size: 1,
232
- children: startPhrase(definition)
1523
+ children: [ row === void 0 || row.brokenBindings.length === 0 ? null : /* @__PURE__ */ jsx(Box, {
1524
+ flex: "none",
1525
+ children: /* @__PURE__ */ jsx(SetupIssues, {
1526
+ bindings: row.brokenBindings
233
1527
  })
234
1528
  }),
235
- /* @__PURE__ */ jsx(MetaRow, {
236
- label: "Document types",
237
- children: runsOn.length === 0 ? /* @__PURE__ */ jsx(Text, {
238
- muted: !0,
239
- size: 1,
240
- children: "No document types in this Studio"
241
- }) : /* @__PURE__ */ jsx(Text, {
242
- size: 1,
243
- children: runsOn.join(", ")
1529
+ /* @__PURE__ */ jsx(Box, {
1530
+ flex: "none",
1531
+ style: {
1532
+ maxWidth: snapshotWidth
1533
+ },
1534
+ children: /* @__PURE__ */ jsxs(Stack, {
1535
+ gap: 4,
1536
+ children: [
1537
+ /* @__PURE__ */ jsx(MetaRow, {
1538
+ label: "Deployed",
1539
+ children: /* @__PURE__ */ jsx(Text, {
1540
+ size: 1,
1541
+ children: deployedPhrase(definition)
1542
+ })
1543
+ }),
1544
+ /* @__PURE__ */ jsx(MetaRow, {
1545
+ label: "Active instances",
1546
+ children: instances.loading ? /* @__PURE__ */ jsx(Text, {
1547
+ muted: !0,
1548
+ size: 1,
1549
+ children: "—"
1550
+ }) : /* @__PURE__ */ jsxs(Stack, {
1551
+ gap: 2,
1552
+ children: [
1553
+ /* @__PURE__ */ jsx(Text, {
1554
+ size: 1,
1555
+ children: row?.activeCount ?? 0
1556
+ }), instances.truncated ? /* @__PURE__ */ jsx(Text, {
1557
+ muted: !0,
1558
+ size: 1,
1559
+ children: INSTANCE_CAP_COUNT_NOTE
1560
+ }) : null ]
1561
+ })
1562
+ }),
1563
+ /* @__PURE__ */ jsx(MetaRow, {
1564
+ label: "Trigger",
1565
+ children: /* @__PURE__ */ jsx(Text, {
1566
+ size: 1,
1567
+ children: startPhrase(definition)
1568
+ })
1569
+ }),
1570
+ /* @__PURE__ */ jsx(MetaRow, {
1571
+ label: "Document types",
1572
+ children: runsOn.length === 0 ? /* @__PURE__ */ jsx(Text, {
1573
+ muted: !0,
1574
+ size: 1,
1575
+ children: "No document types in this Studio"
1576
+ }) : /* @__PURE__ */ jsx(Text, {
1577
+ size: 1,
1578
+ children: runsOn.join(", ")
1579
+ })
1580
+ }),
1581
+ /* @__PURE__ */ jsx(MetaRow, {
1582
+ label: "Description",
1583
+ children: definition.description === void 0 ? /* @__PURE__ */ jsx(Text, {
1584
+ muted: !0,
1585
+ size: 1,
1586
+ children: /* @__PURE__ */ jsx("em", {
1587
+ children: "No description"
1588
+ })
1589
+ }) : /* @__PURE__ */ jsx(Text, {
1590
+ size: 1,
1591
+ style: {
1592
+ whiteSpace: "pre-wrap"
1593
+ },
1594
+ children: definition.description
1595
+ })
1596
+ }) ]
244
1597
  })
245
1598
  }),
246
- /* @__PURE__ */ jsx(MetaRow, {
247
- label: "Description",
248
- children: definition.description === void 0 ? /* @__PURE__ */ jsx(Text, {
249
- muted: !0,
250
- size: 1,
251
- children: /* @__PURE__ */ jsx("em", {
252
- children: "No description"
1599
+ /* @__PURE__ */ jsx(Flex, {
1600
+ direction: "column",
1601
+ flex: 1,
1602
+ style: {
1603
+ minHeight: 340
1604
+ },
1605
+ children: /* @__PURE__ */ jsx(MetaRow, {
1606
+ fillHeight: !0,
1607
+ label: "Stages",
1608
+ children: /* @__PURE__ */ jsx(WorkflowDiagram, {
1609
+ definition: definition,
1610
+ explain: !0,
1611
+ fill: !0,
1612
+ height: "100%"
253
1613
  })
254
- }) : /* @__PURE__ */ jsx(Text, {
255
- size: 1,
256
- style: {
257
- whiteSpace: "pre-wrap"
258
- },
259
- children: definition.description
260
1614
  })
261
- }),
262
- /* @__PURE__ */ jsx(WorkflowDiagram, {
263
- definition: definition,
264
- explain: !0,
265
- height: 340
266
1615
  }) ]
267
1616
  });
268
1617
  }
269
1618
 
270
- function TabStatusRow({children: children}) {
271
- /* @__PURE__ */
272
- return jsx(Box, {
273
- paddingLeft: 2,
274
- paddingY: 3,
275
- children: children
276
- });
277
- }
278
-
279
- const INSTANCE_CAP = 200, NEWEST_INSTANCES = {
280
- includeCompleted: !0,
281
- limit: INSTANCE_CAP
282
- };
283
-
284
- function useToolInstances() {
285
- const {engine: engine} = useWorkflowContext(), {instances: instances, loading: loading, unreadable: unreadable} = useWorkflowInstances({
286
- engine: engine,
287
- filter: NEWEST_INSTANCES
288
- });
289
- return useMemo(() => {
290
- const rows = instances ?? [];
291
- return {
292
- inFlight: rows.filter(instance => terminalState(instance) === "in-flight"),
293
- settled: rows.filter(instance => terminalState(instance) !== "in-flight"),
294
- truncated: rows.length + unreadable.length === INSTANCE_CAP,
295
- loading: loading,
296
- unreadable: unreadable
297
- };
298
- }, [ instances, loading, unreadable ]);
299
- }
300
-
301
1619
  const MAX_VISIBLE_DOC_TYPES = 4, COLUMNS = {
302
1620
  definition: 6,
303
1621
  instances: 2,
@@ -314,7 +1632,12 @@ function Col({flex: flex, children: children}) {
314
1632
  }
315
1633
 
316
1634
  function DefinitionsTab({onOpenDefinition: onOpenDefinition}) {
317
- const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappings: mappings} = useWorkflowContext(), instances = useToolInstances();
1635
+ const {definitions: definitions, error: error} = useDeployedDefinitions(), {mappingIssues: mappingIssues, mappings: mappings} = useWorkflowContext(), schema = useSchema(), instances = useToolInstances(), undeployed = /* @__PURE__ */ jsx(UndeployedWorkflows, {
1636
+ notices: undeployedWorkflowNotices({
1637
+ issues: mappingIssues,
1638
+ titleOf: name => schema.get(name)?.title
1639
+ })
1640
+ });
318
1641
  if (error !== void 0) /* @__PURE__ */
319
1642
  return jsx(TabStatusRow, {
320
1643
  children: /* @__PURE__ */ jsx(Text, {
@@ -330,25 +1653,32 @@ function DefinitionsTab({onOpenDefinition: onOpenDefinition}) {
330
1653
  })
331
1654
  });
332
1655
  if (definitions.length === 0) /* @__PURE__ */
333
- return jsx(TabStatusRow, {
334
- children: /* @__PURE__ */ jsx(Text, {
335
- muted: !0,
336
- size: 1,
337
- children: "No workflow definitions are deployed"
338
- })
1656
+ return jsxs(Flex, {
1657
+ direction: "column",
1658
+ flex: 1,
1659
+ gap: 3,
1660
+ paddingBottom: 4,
1661
+ paddingTop: 3,
1662
+ children: [ undeployed,
1663
+ /* @__PURE__ */ jsx(EmptyState, {
1664
+ description: "Workflow definitions deployed to this project will appear here",
1665
+ icon: /* @__PURE__ */ jsx(TransferIcon, {}),
1666
+ title: "No definitions deployed"
1667
+ }) ]
339
1668
  });
340
1669
  const rows = definitionCatalog({
341
1670
  definitions: definitions,
342
1671
  mappings: mappings,
343
- inFlight: instances.inFlight
1672
+ inFlight: instances.inFlight,
1673
+ issues: mappingIssues
344
1674
  }), active = rows.reduce((sum, row) => sum + row.activeCount, 0);
345
1675
  /* @__PURE__ */
346
1676
  return jsxs(Stack, {
347
1677
  gap: 3,
348
1678
  paddingBottom: 4,
349
- children: [
1679
+ paddingTop: 3,
1680
+ children: [ undeployed,
350
1681
  /* @__PURE__ */ jsx(Box, {
351
- paddingTop: 3,
352
1682
  paddingX: 2,
353
1683
  children: /* @__PURE__ */ jsxs(Flex, {
354
1684
  align: "center",
@@ -435,7 +1765,7 @@ function DefinitionsTab({onOpenDefinition: onOpenDefinition}) {
435
1765
  }, row.name))
436
1766
  }) ]
437
1767
  }), instances.truncated ? /* @__PURE__ */ jsx(MutedNote, {
438
- text: `Counts from the latest ${INSTANCE_CAP} workflows — older ones aren't counted.`
1768
+ text: INSTANCE_CAP_COUNT_NOTE
439
1769
  }) : null ]
440
1770
  });
441
1771
  }
@@ -444,88 +1774,277 @@ function activeSummary(active) {
444
1774
  return `${pluralize("active instance", active, !0)}`;
445
1775
  }
446
1776
 
1777
+ const ROW_ISSUES_HINT = "This definition has issues";
1778
+
1779
+ function UndeployedWorkflows({notices: notices}) {
1780
+ return notices.length === 0 ? null :
1781
+ /* @__PURE__ */ jsx(Stack, {
1782
+ gap: 2,
1783
+ paddingBottom: 1,
1784
+ children: notices.map(notice => /* @__PURE__ */ jsx(CautionNote, {
1785
+ label: notice
1786
+ }, notice))
1787
+ });
1788
+ }
1789
+
447
1790
  function DefinitionRow({countPending: countPending, onOpen: onOpen, row: row}) {
448
- /* @__PURE__ */
449
- return jsx(Card, {
1791
+ const flagged = row.brokenBindings.length > 0, card = /* @__PURE__ */ jsx(Card, {
450
1792
  as: "button",
451
1793
  onClick: onOpen,
452
1794
  paddingX: 2,
453
1795
  paddingY: 3,
454
1796
  radius: 2,
455
1797
  style: {
456
- textAlign: "left"
1798
+ textAlign: "left",
1799
+ width: "100%"
457
1800
  },
1801
+ ...flagged ? {
1802
+ tone: "caution"
1803
+ } : {},
458
1804
  children: /* @__PURE__ */ jsxs(Flex, {
459
1805
  align: "flex-start",
460
1806
  gap: 4,
461
1807
  children: [
462
1808
  /* @__PURE__ */ jsx(Col, {
463
1809
  flex: COLUMNS.definition,
464
- children: /* @__PURE__ */ jsxs(Stack, {
1810
+ children: /* @__PURE__ */ jsxs(Flex, {
1811
+ align: "flex-start",
1812
+ gap: 3,
1813
+ paddingLeft: flagged ? 1 : 0,
1814
+ children: [ flagged ? /* @__PURE__ */ jsx(Text, {
1815
+ size: 1,
1816
+ children: /* @__PURE__ */ jsx(WarningOutlineIcon, {})
1817
+ }) : null,
1818
+ /* @__PURE__ */ jsxs(Stack, {
1819
+ flex: 1,
1820
+ gap: 3,
1821
+ style: {
1822
+ minWidth: 0
1823
+ },
1824
+ children: [
1825
+ /* @__PURE__ */ jsx(Text, {
1826
+ size: 1,
1827
+ weight: "semibold",
1828
+ children: row.title
1829
+ }), row.description === void 0 ? /* @__PURE__ */ jsx(Text, {
1830
+ muted: !0,
1831
+ size: 1,
1832
+ children: /* @__PURE__ */ jsx("em", {
1833
+ children: "No description"
1834
+ })
1835
+ }) : /* @__PURE__ */ jsx(Text, {
1836
+ muted: !0,
1837
+ size: 1,
1838
+ children: row.description
1839
+ }) ]
1840
+ }) ]
1841
+ })
1842
+ }),
1843
+ /* @__PURE__ */ jsx(Col, {
1844
+ flex: COLUMNS.instances,
1845
+ children: /* @__PURE__ */ jsx(Text, {
1846
+ muted: !0,
1847
+ size: 1,
1848
+ children: countPending ? "—" : `${row.activeCount} active`
1849
+ })
1850
+ }),
1851
+ /* @__PURE__ */ jsx(Col, {
1852
+ flex: COLUMNS.docTypes,
1853
+ children: /* @__PURE__ */ jsx(DocTypeChips, {
1854
+ docTypes: row.docTypes
1855
+ })
1856
+ }) ]
1857
+ })
1858
+ });
1859
+ return flagged ? /* @__PURE__ */ jsx(HoverHint, {
1860
+ fill: !0,
1861
+ text: ROW_ISSUES_HINT,
1862
+ children: card
1863
+ }) : card;
1864
+ }
1865
+
1866
+ function DocTypeChips({docTypes: docTypes}) {
1867
+ const schema = useSchema(), badgeTrim = useBadgeCapTrim();
1868
+ if (docTypes.length === 0) /* @__PURE__ */
1869
+ return jsx(Text, {
1870
+ muted: !0,
1871
+ size: 1,
1872
+ children: "—"
1873
+ });
1874
+ const labels = docTypeLabels(docTypes, name => schema.get(name)?.title), shown = labels.slice(0, MAX_VISIBLE_DOC_TYPES), overflow = labels.length - shown.length;
1875
+ /* @__PURE__ */
1876
+ return jsxs(Flex, {
1877
+ align: "center",
1878
+ gap: 2,
1879
+ style: badgeTrim,
1880
+ wrap: "wrap",
1881
+ children: [ shown.map(docType =>
1882
+ /* @__PURE__ */ jsx(Badge, {
1883
+ fontSize: 1,
1884
+ style: {
1885
+ minWidth: 0
1886
+ },
1887
+ children: /* @__PURE__ */ jsx("span", {
1888
+ style: {
1889
+ display: "block",
1890
+ overflow: "hidden",
1891
+ textOverflow: "ellipsis",
1892
+ whiteSpace: "nowrap"
1893
+ },
1894
+ children: docType.title
1895
+ })
1896
+ }, docType.name)), overflow > 0 ? /* @__PURE__ */ jsxs(Text, {
1897
+ muted: !0,
1898
+ size: 1,
1899
+ children: [ "+", overflow, " more" ]
1900
+ }) : null ]
1901
+ });
1902
+ }
1903
+
1904
+ function normalizeAbortReason(raw) {
1905
+ const trimmed = raw.trim();
1906
+ return trimmed === "" ? void 0 : trimmed;
1907
+ }
1908
+
1909
+ function abortedToast(title) {
1910
+ return {
1911
+ id: TOAST_ID.abort,
1912
+ status: "info",
1913
+ title: `Aborted “${title}”`
1914
+ };
1915
+ }
1916
+
1917
+ function abortAlreadySettledToast(title) {
1918
+ return {
1919
+ id: TOAST_ID.abort,
1920
+ status: "info",
1921
+ title: `“${title}” had already finished`,
1922
+ description: "Nothing was aborted — the workflow reached a terminal state first."
1923
+ };
1924
+ }
1925
+
1926
+ function abortUnconfirmedToast(args) {
1927
+ return {
1928
+ id: TOAST_ID.abort,
1929
+ status: "warning",
1930
+ title: `Couldn’t confirm “${args.title}” was aborted`,
1931
+ description: `${describeError(args.err)} — the abort may still have committed, so check the workflow’s state before trying again.`
1932
+ };
1933
+ }
1934
+
1935
+ function AbortWorkflowDialog({entry: entry, onClose: onClose}) {
1936
+ const {engine: engine} = useWorkflowContext(), definition = useDefinition(entry), toast = useWorkflowToast(), [typedReason, setTypedReason] = useState(""), [pending, setPending] = useState(!1), {instance: instance} = entry, title = instanceTitle(instance, definition), reason = normalizeAbortReason(typedReason), reasonInputId = `abort-workflow-reason-${instance._id}`, confirm = async () => {
1937
+ if (reason !== void 0) {
1938
+ setPending(!0);
1939
+ try {
1940
+ const {changed: changed} = await engine.abortInstance({
1941
+ instanceId: instance._id,
1942
+ reason: reason
1943
+ });
1944
+ toast.push(changed ? abortedToast(title) : abortAlreadySettledToast(title)), onClose();
1945
+ } catch (err) {
1946
+ toast.push(abortUnconfirmedToast({
1947
+ title: title,
1948
+ err: err
1949
+ })), setPending(!1);
1950
+ }
1951
+ }
1952
+ };
1953
+ /* @__PURE__ */
1954
+ return jsx(Dialog, {
1955
+ footer: /* @__PURE__ */ jsx(Box, {
1956
+ padding: 3,
1957
+ children: /* @__PURE__ */ jsxs(Flex, {
1958
+ gap: 2,
1959
+ justify: "flex-end",
1960
+ children: [
1961
+ /* @__PURE__ */ jsx(Button, {
1962
+ disabled: pending,
1963
+ fontSize: 1,
1964
+ mode: "bleed",
1965
+ onClick: onClose,
1966
+ padding: 2,
1967
+ text: "Cancel"
1968
+ }),
1969
+ /* @__PURE__ */ jsx(Button, {
1970
+ disabled: reason === void 0,
1971
+ fontSize: 1,
1972
+ loading: pending,
1973
+ onClick: () => {
1974
+ confirm();
1975
+ },
1976
+ padding: 2,
1977
+ text: "Abort workflow",
1978
+ tone: "critical"
1979
+ }) ]
1980
+ })
1981
+ }),
1982
+ header: "Abort workflow",
1983
+ id: `abort-workflow-${instance._id}`,
1984
+ onClose: () => {
1985
+ pending || onClose();
1986
+ },
1987
+ width: 0,
1988
+ children: /* @__PURE__ */ jsx(Box, {
1989
+ padding: 4,
1990
+ children: /* @__PURE__ */ jsxs(Stack, {
1991
+ gap: 5,
1992
+ children: [
1993
+ /* @__PURE__ */ jsxs(Text, {
1994
+ size: 1,
1995
+ children: [ "Abort the ",
1996
+ /* @__PURE__ */ jsx("strong", {
1997
+ children: title
1998
+ }), " workflow? This cannot be undone." ]
1999
+ }),
2000
+ /* @__PURE__ */ jsxs(Stack, {
465
2001
  gap: 3,
466
- style: {
467
- minWidth: 0
468
- },
469
2002
  children: [
470
2003
  /* @__PURE__ */ jsx(Text, {
2004
+ as: "label",
2005
+ htmlFor: reasonInputId,
471
2006
  size: 1,
472
- weight: "semibold",
473
- children: row.title
474
- }), row.description === void 0 ? /* @__PURE__ */ jsx(Text, {
475
- muted: !0,
476
- size: 1,
477
- children: /* @__PURE__ */ jsx("em", {
478
- children: "No description"
479
- })
480
- }) : /* @__PURE__ */ jsx(Text, {
481
- muted: !0,
482
- size: 1,
483
- children: row.description
2007
+ weight: "medium",
2008
+ children: "Reason"
2009
+ }),
2010
+ /* @__PURE__ */ jsx(TextInput, {
2011
+ autoFocus: !0,
2012
+ fontSize: 1,
2013
+ id: reasonInputId,
2014
+ onChange: event => setTypedReason(event.currentTarget.value),
2015
+ placeholder: "Why is this workflow being stopped?",
2016
+ value: typedReason
484
2017
  }) ]
485
- })
486
- }),
487
- /* @__PURE__ */ jsx(Col, {
488
- flex: COLUMNS.instances,
489
- children: /* @__PURE__ */ jsx(Text, {
490
- muted: !0,
491
- size: 1,
492
- children: countPending ? "—" : `${row.activeCount} active`
493
- })
494
- }),
495
- /* @__PURE__ */ jsx(Col, {
496
- flex: COLUMNS.docTypes,
497
- children: /* @__PURE__ */ jsx(DocTypeChips, {
498
- docTypes: row.docTypes
499
- })
500
- }) ]
2018
+ }) ]
2019
+ })
501
2020
  })
502
2021
  });
503
2022
  }
504
2023
 
505
- function DocTypeChips({docTypes: docTypes}) {
506
- const schema = useSchema(), badgeTrim = useBadgeCapTrim();
507
- if (docTypes.length === 0) /* @__PURE__ */
508
- return jsx(Text, {
509
- muted: !0,
510
- size: 1,
511
- children: "—"
512
- });
513
- const labels = docTypeLabels(docTypes, name => schema.get(name)?.title), shown = labels.slice(0, MAX_VISIBLE_DOC_TYPES), overflow = labels.length - shown.length;
514
- /* @__PURE__ */
515
- return jsxs(Flex, {
516
- align: "center",
517
- gap: 2,
518
- style: badgeTrim,
519
- wrap: "wrap",
520
- children: [ shown.map(docType => /* @__PURE__ */ jsx(Badge, {
521
- fontSize: 1,
522
- children: docType.title
523
- }, docType.name)), overflow > 0 ? /* @__PURE__ */ jsxs(Text, {
524
- muted: !0,
2024
+ function AbortWorkflowSection({entry: entry}) {
2025
+ const [aborting, setAborting] = useState(!1);
2026
+ return isLiveEntry(entry) ? /* @__PURE__ */ jsxs(Stack, {
2027
+ gap: 4,
2028
+ children: [
2029
+ /* @__PURE__ */ jsx(Text, {
525
2030
  size: 1,
526
- children: [ "+", overflow, " more" ]
2031
+ weight: "semibold",
2032
+ children: "Manage workflow"
2033
+ }),
2034
+ /* @__PURE__ */ jsx(Flex, {
2035
+ children: /* @__PURE__ */ jsx(Button, {
2036
+ fontSize: 1,
2037
+ mode: "ghost",
2038
+ onClick: () => setAborting(!0),
2039
+ padding: 2,
2040
+ text: "Abort workflow…",
2041
+ tone: "critical"
2042
+ })
2043
+ }), aborting ? /* @__PURE__ */ jsx(AbortWorkflowDialog, {
2044
+ entry: entry,
2045
+ onClose: () => setAborting(!1)
527
2046
  }) : null ]
528
- });
2047
+ }) : null;
529
2048
  }
530
2049
 
531
2050
  function makeTitleResolver(definition) {
@@ -600,19 +2119,21 @@ function historyLine(h, titles) {
600
2119
  }
601
2120
 
602
2121
  function PendingEffectRows({instance: instance, now: now}) {
603
- const {drainEffectsFor: drainEffectsFor, completeEffectFor: completeEffectFor, effectHandlers: effectHandlers} = useWorkflowContext(), toast = useClosableToast(), [busy, setBusy] = useState(!1), pending = instance.pendingEffects ?? [];
2122
+ const {drainEffectsFor: drainEffectsFor, completeEffectFor: completeEffectFor, effectHandlers: effectHandlers} = useWorkflowContext(), toast = useWorkflowToast(), [busy, setBusy] = useState(!1), pending = instance.pendingEffects ?? [];
604
2123
  if (pending.length === 0) return null;
605
2124
  const runRegistered = async () => {
606
2125
  setBusy(!0);
607
2126
  try {
608
2127
  await drainEffectsFor(instance._id), toast.push({
609
- status: "success",
610
- title: "Ran registered effect handlers"
2128
+ id: TOAST_ID.effectsDrain,
2129
+ status: "info",
2130
+ title: "Ran the registered handlers"
611
2131
  });
612
2132
  } catch (err) {
613
2133
  toast.push({
2134
+ id: TOAST_ID.effectsDrain,
614
2135
  status: "error",
615
- title: "Drain failed",
2136
+ title: "Failed to run pending effects",
616
2137
  description: describeError(err)
617
2138
  });
618
2139
  } finally {
@@ -626,13 +2147,15 @@ function PendingEffectRows({instance: instance, now: now}) {
626
2147
  status: status,
627
2148
  detail: "Resolved in Studio"
628
2149
  }), toast.push({
629
- status: "success",
630
- title: `Effect marked ${status}`
2150
+ id: TOAST_ID.effectResolve,
2151
+ status: "info",
2152
+ title: `Effect marked as ${status}`
631
2153
  });
632
2154
  } catch (err) {
633
2155
  toast.push({
2156
+ id: TOAST_ID.effectResolve,
634
2157
  status: "error",
635
- title: "Couldn’t resolve effect",
2158
+ title: "Failed to resolve the effect",
636
2159
  description: describeError(err)
637
2160
  });
638
2161
  } finally {
@@ -862,13 +2385,7 @@ function FeedGlyph({hint: hint, icon: icon}) {
862
2385
  });
863
2386
  }
864
2387
 
865
- function documentRefsOf(instance) {
866
- const seen = /* @__PURE__ */ new Map;
867
- for (const ref of entryDocRefs(instance.fields)) seen.has(ref.id) || seen.set(ref.id, ref);
868
- return [ ...seen.values() ];
869
- }
870
-
871
- function WorkflowInstanceDetail({instanceId: instanceId, onBack: onBack}) {
2388
+ function WorkflowInstanceDetail({instanceId: instanceId, backToList: backToList}) {
872
2389
  const entry = useWorkflowInstanceEntry(instanceId);
873
2390
  useLogEventOnMount(WorkflowInstanceDetailViewed, {
874
2391
  instanceId: instanceId
@@ -887,9 +2404,14 @@ function WorkflowInstanceDetail({instanceId: instanceId, onBack: onBack}) {
887
2404
  }) : /* @__PURE__ */ jsx(Box, {
888
2405
  paddingX: 3,
889
2406
  children: /* @__PURE__ */ jsx(TabStatusRow, {
890
- children: graceOver ? /* @__PURE__ */ jsx(NotFound, {
891
- id: instanceId,
892
- onBack: onBack
2407
+ children: graceOver ? /* @__PURE__ */ jsx(DetailNotFound, {
2408
+ back: backToList,
2409
+ message: /* @__PURE__ */ jsxs(Fragment, {
2410
+ children: [ "Unable to find workflow with id “",
2411
+ /* @__PURE__ */ jsx("strong", {
2412
+ children: instanceId
2413
+ }), "”." ]
2414
+ })
893
2415
  }) : /* @__PURE__ */ jsx(LoadingRow, {
894
2416
  label: "Loading workflow…"
895
2417
  })
@@ -897,29 +2419,6 @@ function WorkflowInstanceDetail({instanceId: instanceId, onBack: onBack}) {
897
2419
  });
898
2420
  }
899
2421
 
900
- function NotFound({id: id, onBack: onBack}) {
901
- /* @__PURE__ */
902
- return jsxs(Stack, {
903
- gap: 4,
904
- children: [
905
- /* @__PURE__ */ jsxs(Text, {
906
- muted: !0,
907
- size: 1,
908
- children: [ "No workflow with id “", id, "” — it may have been deleted." ]
909
- }),
910
- /* @__PURE__ */ jsx(Flex, {
911
- children: /* @__PURE__ */ jsx(Button, {
912
- fontSize: 1,
913
- icon: ArrowLeftIcon,
914
- mode: "ghost",
915
- onClick: onBack,
916
- padding: 2,
917
- text: "Back to workflows"
918
- })
919
- }) ]
920
- });
921
- }
922
-
923
2422
  function InstanceDetailPanel({entry: entry}) {
924
2423
  const definition = useDefinition(entry), snapshotWidth = useContainerToken(2), [openActivity, setOpenActivity] = useState(null), {instance: instance, ready: ready} = entry;
925
2424
  return useEffect(() => {
@@ -962,7 +2461,11 @@ function InstanceDetailPanel({entry: entry}) {
962
2461
  onOpenActivity: name => setOpenActivity({
963
2462
  stage: instance.currentStage,
964
2463
  activityName: name
965
- })
2464
+ }),
2465
+ surface: "tool-instance-detail"
2466
+ }),
2467
+ /* @__PURE__ */ jsx(AbortWorkflowSection, {
2468
+ entry: entry
966
2469
  }),
967
2470
  /* @__PURE__ */ jsxs(Stack, {
968
2471
  gap: 4,
@@ -1066,217 +2569,76 @@ function DefinitionPickerPanel({onPick: onPick}) {
1066
2569
  overflowY: "auto"
1067
2570
  },
1068
2571
  children: visible.map(definition => /* @__PURE__ */ jsx(Card, {
1069
- as: "button",
1070
- onClick: () => onPick(definition),
1071
- padding: 3,
1072
- radius: 2,
1073
- style: {
1074
- cursor: "pointer",
1075
- textAlign: "left"
1076
- },
1077
- children: /* @__PURE__ */ jsxs(Stack, {
1078
- gap: 2,
1079
- children: [
1080
- /* @__PURE__ */ jsx(Text, {
1081
- size: 1,
1082
- weight: "medium",
1083
- children: definition.title
1084
- }), definition.description ? /* @__PURE__ */ jsx(Text, {
1085
- muted: !0,
1086
- size: 1,
1087
- textOverflow: "ellipsis",
1088
- children: definition.description
1089
- }) : null ]
1090
- })
1091
- }, definition.name))
1092
- }) ]
1093
- })
1094
- });
1095
- }
1096
-
1097
- function ToolActivityDialog({target: target, onClose: onClose}) {
1098
- const entry = useWorkflowInstanceEntry(target.instanceId), definition = useDefinition(entry), resolvedOnce = useRef(!1);
1099
- return entry && (resolvedOnce.current = !0), useEffect(() => {
1100
- resolvedOnce.current && openActivityGone({
1101
- entry: entry,
1102
- target: target
1103
- }) && onClose();
1104
- }, [ entry, target, onClose ]), entry?.invalid ? /* @__PURE__ */ jsx(Dialog, {
1105
- header: "Workflow unavailable",
1106
- id: "workflow-activity-detail",
1107
- onClose: onClose,
1108
- width: 1,
1109
- children: /* @__PURE__ */ jsx(Box, {
1110
- padding: 4,
1111
- children: /* @__PURE__ */ jsx(InvalidDocNotice, {
1112
- invalid: entry.invalid
1113
- })
1114
- })
1115
- }) : entry?.evaluation ? /* @__PURE__ */ jsx(ActivityDetailDialog, {
1116
- activityName: target.activityName,
1117
- breadcrumb: instanceBreadcrumb(entry.instance, definition),
1118
- definition: definition,
1119
- document: documentRefsOf(entry.instance)[0],
1120
- entry: entry,
1121
- onClose: onClose,
1122
- source: "tool-task-list"
1123
- }) : /* @__PURE__ */ jsx(Dialog, {
1124
- header: "Loading activity…",
1125
- id: "workflow-activity-detail",
1126
- onClose: onClose,
1127
- width: 1,
1128
- children: /* @__PURE__ */ jsx(Box, {
1129
- padding: 4,
1130
- children: /* @__PURE__ */ jsx(LoadingRow, {
1131
- label: "Fetching the live workflow state…"
1132
- })
1133
- })
1134
- });
1135
- }
1136
-
1137
- const GROUP_DEFAULT_OPEN = !0;
1138
-
1139
- function instanceBandDefaultOpen(segment) {
1140
- return segment === "for-me";
1141
- }
1142
-
1143
- const UNGROUPED_BAND_KEY = "section:no-document", PAYLOAD_VERSION = 1, MAX_ENTRIES = 200;
1144
-
1145
- function bandStateStorageKey(scope) {
1146
- return `sanity.workflows.tool.bands:${scope}`;
1147
- }
1148
-
1149
- function isRecord(value) {
1150
- return typeof value == "object" && value !== null && !Array.isArray(value);
1151
- }
1152
-
1153
- function bandEntryOf(value) {
1154
- if (!isRecord(value)) return;
1155
- const {open: open, touched: touched} = value;
1156
- if (!(typeof open != "boolean" || typeof touched != "number") && Number.isFinite(touched)) return {
1157
- open: open,
1158
- touched: touched
1159
- };
1160
- }
1161
-
1162
- function parsePayload(raw) {
1163
- try {
1164
- const parsed = JSON.parse(raw);
1165
- if (!isRecord(parsed) || parsed.version !== PAYLOAD_VERSION || !isRecord(parsed.bands)) return;
1166
- const entries = [];
1167
- for (const [key, value] of Object.entries(parsed.bands)) {
1168
- const entry = bandEntryOf(value);
1169
- if (entry === void 0) return;
1170
- entries.push([ key, entry ]);
1171
- }
1172
- return Object.fromEntries(entries);
1173
- } catch {
1174
- return;
1175
- }
1176
- }
1177
-
1178
- function readStored(storage, storageKey) {
1179
- if (!storage) return {
1180
- entries: void 0,
1181
- corrupt: !1
1182
- };
1183
- let raw;
1184
- try {
1185
- raw = storage.getItem(storageKey);
1186
- } catch {
1187
- return {
1188
- entries: void 0,
1189
- corrupt: !1
1190
- };
1191
- }
1192
- if (raw === null) return {
1193
- entries: {},
1194
- corrupt: !1
1195
- };
1196
- const entries = parsePayload(raw);
1197
- return {
1198
- entries: entries,
1199
- corrupt: entries === void 0
1200
- };
1201
- }
1202
-
1203
- function writeStored(args) {
1204
- if (!args.storage) return !1;
1205
- try {
1206
- return args.storage.setItem(args.storageKey, JSON.stringify({
1207
- version: PAYLOAD_VERSION,
1208
- bands: args.entries
1209
- })), !0;
1210
- } catch {
1211
- return !1;
1212
- }
1213
- }
1214
-
1215
- function baseEntries(args) {
1216
- return args.stored === void 0 ? args.held : args.unpersisted ? mergeByTouched(args.stored, args.held) : args.stored;
1217
- }
1218
-
1219
- function mergeByTouched(stored, held) {
1220
- const merged = {
1221
- ...stored
1222
- };
1223
- for (const [key, entry] of Object.entries(held)) {
1224
- const persisted = merged[key];
1225
- (persisted === void 0 || entry.touched >= persisted.touched) && (merged[key] = entry);
1226
- }
1227
- return merged;
1228
- }
1229
-
1230
- function prune(entries) {
1231
- const held = Object.entries(entries);
1232
- return held.length <= MAX_ENTRIES ? entries : Object.fromEntries(held.sort(([, a], [, b]) => b.touched - a.touched).slice(0, MAX_ENTRIES));
1233
- }
1234
-
1235
- function togglesOf(entries) {
1236
- return new Map(Object.entries(entries).map(([key, entry]) => [ key, entry.open ]));
1237
- }
1238
-
1239
- function defaultStorage() {
1240
- try {
1241
- return globalThis.localStorage;
1242
- } catch {
1243
- return;
1244
- }
2572
+ as: "button",
2573
+ onClick: () => onPick(definition),
2574
+ padding: 3,
2575
+ radius: 2,
2576
+ style: {
2577
+ cursor: "pointer",
2578
+ textAlign: "left"
2579
+ },
2580
+ children: /* @__PURE__ */ jsxs(Stack, {
2581
+ gap: 2,
2582
+ children: [
2583
+ /* @__PURE__ */ jsx(Text, {
2584
+ size: 1,
2585
+ weight: "medium",
2586
+ children: definition.title
2587
+ }), definition.description ? /* @__PURE__ */ jsx(Text, {
2588
+ muted: !0,
2589
+ size: 1,
2590
+ textOverflow: "ellipsis",
2591
+ children: definition.description
2592
+ }) : null ]
2593
+ })
2594
+ }, definition.name))
2595
+ }) ]
2596
+ })
2597
+ });
1245
2598
  }
1246
2599
 
1247
- function createBandStateStore(args) {
1248
- const storage = args.storage ?? defaultStorage(), initial = readStored(storage, args.storageKey);
1249
- initial.corrupt && writeStored({
1250
- storage: storage,
1251
- storageKey: args.storageKey,
1252
- entries: {}
2600
+ function ToolActivityDialog({target: target, onClose: onClose}) {
2601
+ const entry = useWorkflowInstanceEntry(target.instanceId), definition = useDefinition(entry), resolvedOnce = useRef(!1);
2602
+ return entry && (resolvedOnce.current = !0), useEffect(() => {
2603
+ resolvedOnce.current && openActivityGone({
2604
+ entry: entry,
2605
+ target: target
2606
+ }) && onClose();
2607
+ }, [ entry, target, onClose ]), entry?.invalid ? /* @__PURE__ */ jsx(Dialog, {
2608
+ header: "Workflow unavailable",
2609
+ id: "workflow-activity-detail",
2610
+ onClose: onClose,
2611
+ width: 1,
2612
+ children: /* @__PURE__ */ jsx(Box, {
2613
+ padding: 4,
2614
+ children: /* @__PURE__ */ jsx(InvalidDocNotice, {
2615
+ invalid: entry.invalid
2616
+ })
2617
+ })
2618
+ }) : entry?.evaluation ? /* @__PURE__ */ jsx(ActivityDetailDialog, {
2619
+ activityName: target.activityName,
2620
+ breadcrumb: instanceBreadcrumb(entry.instance, definition),
2621
+ definition: definition,
2622
+ document: documentRefsOf(entry.instance)[0],
2623
+ entry: entry,
2624
+ onClose: onClose,
2625
+ source: "tool-task-list"
2626
+ }) : /* @__PURE__ */ jsx(Dialog, {
2627
+ header: "Loading activity…",
2628
+ id: "workflow-activity-detail",
2629
+ onClose: onClose,
2630
+ width: 1,
2631
+ children: /* @__PURE__ */ jsx(Box, {
2632
+ padding: 4,
2633
+ children: /* @__PURE__ */ jsx(LoadingRow, {
2634
+ label: "Fetching the live workflow state…"
2635
+ })
2636
+ })
1253
2637
  });
1254
- let held = initial.entries ?? {}, unpersisted = !1;
1255
- return {
1256
- read: () => togglesOf(held),
1257
- setOpen(key, open) {
1258
- const stored = readStored(storage, args.storageKey).entries, base = baseEntries({
1259
- stored: stored,
1260
- held: held,
1261
- unpersisted: unpersisted
1262
- });
1263
- return held = prune({
1264
- ...base,
1265
- [key]: {
1266
- open: open,
1267
- touched: Date.now()
1268
- }
1269
- }), unpersisted = !writeStored({
1270
- storage: storage,
1271
- storageKey: args.storageKey,
1272
- entries: held
1273
- }), togglesOf(held);
1274
- }
1275
- };
1276
2638
  }
1277
2639
 
1278
2640
  function datesOf(state) {
1279
- return state.filter(entry => entry._type === "date" || entry._type === "datetime").map(entry => entry.value).filter(value => typeof value == "string" && value !== "");
2641
+ return state.filter(entry => isDateFieldKind(entry._type)).map(entry => entry.value).filter(value => typeof value == "string" && value !== "");
1280
2642
  }
1281
2643
 
1282
2644
  function spawnOrigins(instances) {
@@ -1377,10 +2739,7 @@ function deriveTaskGroups(args) {
1377
2739
  }
1378
2740
 
1379
2741
  function orderGroupsByPreviewTitle(groups, titles) {
1380
- const withDocuments = [ ...groups.withDocuments ].sort((a, b) => {
1381
- const aTitle = titles.get(a.document.id), bTitle = titles.get(b.document.id);
1382
- return aTitle !== void 0 && bTitle !== void 0 ? aTitle.localeCompare(bTitle) || a.key.localeCompare(b.key) : aTitle !== void 0 ? -1 : bTitle !== void 0 ? 1 : a.key.localeCompare(b.key);
1383
- });
2742
+ const withDocuments = [ ...groups.withDocuments ].sort(byResolvedTitle(group => titles.get(group.document.id), group => group.key));
1384
2743
  return {
1385
2744
  ...groups,
1386
2745
  withDocuments: withDocuments
@@ -1532,11 +2891,13 @@ function workflowTally(workflows) {
1532
2891
  function settleToast(outcome) {
1533
2892
  const total = outcome.settled + outcome.skipped.length + outcome.failures.length;
1534
2893
  if (outcome.skipped.length === 0 && outcome.failures.length === 0) return {
1535
- status: "success",
2894
+ id: TOAST_ID.orphanSettle,
2895
+ status: "info",
1536
2896
  title: outcome.settled === 1 ? "Cleaned up 1 workflow" : `Cleaned up ${outcome.settled} workflows`
1537
2897
  };
1538
2898
  const lines = [ ...outcome.skipped.map(workflow => `${workflow.title}: its document exists again — left running`), ...outcome.failures.map(failure => `${failure.workflow.title}: ${failure.message}`) ];
1539
2899
  return {
2900
+ id: TOAST_ID.orphanSettle,
1540
2901
  status: "warning",
1541
2902
  title: `Cleaned up ${outcome.settled} of ${total} workflows`,
1542
2903
  description: lines.join(`\n`)
@@ -1545,9 +2906,10 @@ function settleToast(outcome) {
1545
2906
 
1546
2907
  function recheckFailedToast(err) {
1547
2908
  return {
2909
+ id: TOAST_ID.orphanSettle,
1548
2910
  status: "error",
1549
- title: "Nothing was cleaned up — the documents could not be re-checked",
1550
- description: describeError(err)
2911
+ title: "Nothing was cleaned up",
2912
+ description: `The documents could not be re-checked: ${describeError(err)}`
1551
2913
  };
1552
2914
  }
1553
2915
 
@@ -1577,7 +2939,7 @@ function OrphanedWorkflowsNote({orphans: orphans}) {
1577
2939
  function SettleOrphansDialog({workflows: workflows, onClose: onClose}) {
1578
2940
  const {engine: engine} = useWorkflowContext(), client = useClient({
1579
2941
  apiVersion: WORKFLOW_API_VERSION
1580
- }), toast = useClosableToast(), [pending, setPending] = useState(!1), confirm = async () => {
2942
+ }), toast = useWorkflowToast(), [pending, setPending] = useState(!1), confirm = async () => {
1581
2943
  setPending(!0);
1582
2944
  try {
1583
2945
  const outcome = await settleOrphans({
@@ -1822,72 +3184,6 @@ function applyTaskFilters(args) {
1822
3184
  });
1823
3185
  }
1824
3186
 
1825
- const NOTHING = {
1826
- titles: /* @__PURE__ */ new Map,
1827
- resolved: /* @__PURE__ */ new Set
1828
- };
1829
-
1830
- function useDocPreviewTitles(documents) {
1831
- const previewStore = useDocumentPreviewStore(), schema = useSchema(), {perspectiveStack: perspectiveStack} = usePerspective(), {actualTypes: actualTypes, binding: binding} = useWorkflowContext(), [state, setState] = useState(NOTHING), key = documents.map(doc => doc.id).join("|");
1832
- return useEffect(() => {
1833
- const titles = /* @__PURE__ */ new Map, resolved = /* @__PURE__ */ new Set, previews = /* @__PURE__ */ new Map, publish = () => setState({
1834
- titles: new Map(titles),
1835
- resolved: new Set(resolved)
1836
- }), localDocs = documents.flatMap(doc => {
1837
- const {parsed: parsed, local: local} = gdrLocality(doc.id, binding.contentResource);
1838
- return !parsed || !local ? (resolved.add(doc.id), []) : [ {
1839
- doc: doc,
1840
- bareId: parsed.documentId
1841
- } ];
1842
- }), untracks = localDocs.map(({bareId: bareId}) => actualTypes.track(bareId)), dropPreview = docId => {
1843
- previews.get(docId)?.unsubscribe(), previews.delete(docId), titles.delete(docId);
1844
- }, observe = ({docId: docId, bareId: bareId, typeName: typeName}) => {
1845
- const schemaType = openableSchemaType(schema, typeName);
1846
- if (!schemaType) return !1;
1847
- if (previews.get(docId)?.typeName === typeName) return !0;
1848
- dropPreview(docId), resolved.delete(docId);
1849
- const subscription = previewStore.observeForPreview({
1850
- _id: bareId,
1851
- _type: schemaType.name
1852
- }, schemaType, {
1853
- perspective: perspectiveStack
1854
- }).subscribe({
1855
- next: event => {
1856
- resolved.add(docId);
1857
- const title = event.snapshot?.title;
1858
- typeof title == "string" ? titles.set(docId, title) : titles.delete(docId), publish();
1859
- },
1860
- error: err => {
1861
- console.error(`[workflow-studio-plugin] preview for "${docId}" failed:`, err), resolved.add(docId),
1862
- publish();
1863
- }
1864
- });
1865
- return previews.set(docId, {
1866
- typeName: typeName,
1867
- unsubscribe: () => subscription.unsubscribe()
1868
- }), !0;
1869
- }, evaluate = () => {
1870
- for (const {doc: doc, bareId: bareId} of localDocs) {
1871
- const probe = actualTypes.read(bareId);
1872
- if (probe === null) {
1873
- dropPreview(doc.id), resolved.add(doc.id);
1874
- continue;
1875
- }
1876
- !observe({
1877
- docId: doc.id,
1878
- bareId: bareId,
1879
- typeName: probe ?? doc.type
1880
- }) && typeof probe == "string" && (dropPreview(doc.id), resolved.add(doc.id));
1881
- }
1882
- publish();
1883
- }, unsubscribeProbes = actualTypes.subscribe(evaluate);
1884
- return evaluate(), () => {
1885
- unsubscribeProbes(), untracks.forEach(untrack => untrack()), previews.forEach(preview => preview.unsubscribe());
1886
- };
1887
- }, [ key, previewStore, schema, actualTypes, binding.contentResource.id, perspectiveStack.join("|") ]),
1888
- state;
1889
- }
1890
-
1891
3187
  const HITS_CAP = 6;
1892
3188
 
1893
3189
  function TaskFilterMenu({filters: filters, options: options, onChange: onChange}) {
@@ -2386,8 +3682,10 @@ function FilterRowPortal({filters: filters, onChange: onChange, options: options
2386
3682
  }), slot) : null;
2387
3683
  }
2388
3684
 
2389
- function ActivityDateControl({instanceId: instanceId, state: state, dates: dates}) {
2390
- const {editFieldFor: editFieldFor} = useWorkflowContext(), {save: save} = useSaveField(), [open, setOpen] = useState(!1), raw = dateControlValue({
3685
+ function ActivityDateControl({instanceId: instanceId, state: state, surface: surface, dates: dates}) {
3686
+ const editField = useEditField(surface), {save: save} = useSaveField({
3687
+ instanceId: instanceId
3688
+ }), [open, setOpen] = useState(!1), raw = dateControlValue({
2391
3689
  state: state,
2392
3690
  dates: dates
2393
3691
  }), display = raw === void 0 ? null : /* @__PURE__ */ jsx(Text, {
@@ -2410,15 +3708,16 @@ function ActivityDateControl({instanceId: instanceId, state: state, dates: dates
2410
3708
  })
2411
3709
  });
2412
3710
  const kind = dateControlKind(state), commit = (mode, value) => {
2413
- state.kind === "editable" && save(() => editFieldFor(instanceId, {
2414
- target: editFieldTarget(state.field),
3711
+ state.kind === "editable" && save(() => editField({
3712
+ instanceId: instanceId,
3713
+ field: state.field,
2415
3714
  ...mode === "set" ? {
2416
3715
  mode: mode,
2417
3716
  value: value
2418
3717
  } : {
2419
3718
  mode: mode
2420
3719
  }
2421
- }).then(() => {}));
3720
+ }));
2422
3721
  }, handleClick = e => {
2423
3722
  e.stopPropagation(), setOpen(v => !v);
2424
3723
  };
@@ -2435,9 +3734,9 @@ function ActivityDateControl({instanceId: instanceId, state: state, dates: dates
2435
3734
  setOpen(!1), commit("unset");
2436
3735
  },
2437
3736
  onPick: next => {
2438
- kind === "date" && setOpen(!1), commit("set", serializeDateFieldValue(next, kind));
3737
+ hasTimeOfDay(kind) || setOpen(!1), commit("set", serializeDateFieldValue(next, kind));
2439
3738
  },
2440
- selectTime: kind === "datetime",
3739
+ selectTime: hasTimeOfDay(kind),
2441
3740
  value: parseDateFieldValue(state.field.value, kind)
2442
3741
  })
2443
3742
  }),
@@ -2501,7 +3800,8 @@ function RefChips({refs: refs, max: max = 2}) {
2501
3800
  wrap: "wrap",
2502
3801
  children: [ shown.map(ref => /* @__PURE__ */ jsx(DocPreviewLink, {
2503
3802
  gdr: ref,
2504
- layout: "inline"
3803
+ layout: "inline",
3804
+ source: "doc-ref-chip"
2505
3805
  }, ref.id)), overflow > 0 ? /* @__PURE__ */ jsxs(Text, {
2506
3806
  muted: !0,
2507
3807
  size: 1,
@@ -2512,54 +3812,16 @@ function RefChips({refs: refs, max: max = 2}) {
2512
3812
 
2513
3813
  const UNGROUPED_TITLE = "Workflows without Studio documents";
2514
3814
 
2515
- function TaskGroupList({bands: bands, groups: groups, instanceDefaultOpen: instanceDefaultOpen, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
3815
+ function TaskGroupList({groups: groups, ...wiring}) {
2516
3816
  /* @__PURE__ */
2517
3817
  return jsxs(Stack, {
2518
3818
  gap: 1,
2519
- children: [ groups.withDocuments.map(group => /* @__PURE__ */ jsx(CollapsibleBand, {
2520
- background: !0,
2521
- onToggle: () => bands.toggle(group.key, GROUP_DEFAULT_OPEN),
2522
- open: bands.isOpen(group.key, GROUP_DEFAULT_OPEN),
2523
- sticky: !0,
2524
- title: toBareId(group.document.id),
2525
- header: /* @__PURE__ */ jsx(Flex, {
2526
- align: "center",
2527
- flex: "none",
2528
- style: {
2529
- maxWidth: "60%"
2530
- },
2531
- children: /* @__PURE__ */ jsx(RefChips, {
2532
- max: 2,
2533
- refs: group.documents
2534
- })
2535
- }),
2536
- children: /* @__PURE__ */ jsx(InstanceBands, {
2537
- bands: bands,
2538
- instanceDefaultOpen: instanceDefaultOpen,
2539
- instances: group.instances,
2540
- onOpenTask: onOpenTask,
2541
- onOpenWorkflow: onOpenWorkflow,
2542
- showTerminalActions: showTerminalActions
2543
- })
2544
- }, group.key)), groups.withoutDocuments.length > 0 ? /* @__PURE__ */ jsx(CollapsibleBand, {
2545
- background: !0,
2546
- onToggle: () => bands.toggle(UNGROUPED_BAND_KEY, GROUP_DEFAULT_OPEN),
2547
- open: bands.isOpen(UNGROUPED_BAND_KEY, GROUP_DEFAULT_OPEN),
2548
- sticky: !0,
2549
- title: UNGROUPED_TITLE,
2550
- header: /* @__PURE__ */ jsx(Text, {
2551
- size: 1,
2552
- weight: "semibold",
2553
- children: UNGROUPED_TITLE
2554
- }),
2555
- children: /* @__PURE__ */ jsx(InstanceBands, {
2556
- bands: bands,
2557
- instanceDefaultOpen: instanceDefaultOpen,
2558
- instances: groups.withoutDocuments,
2559
- onOpenTask: onOpenTask,
2560
- onOpenWorkflow: onOpenWorkflow,
2561
- showTerminalActions: showTerminalActions
2562
- })
3819
+ children: [ groups.withDocuments.map(group => /* @__PURE__ */ jsx(DocumentBand, {
3820
+ group: group,
3821
+ ...wiring
3822
+ }, group.key)), groups.withoutDocuments.length > 0 ? /* @__PURE__ */ jsx(UngroupedBand, {
3823
+ instances: groups.withoutDocuments,
3824
+ ...wiring
2563
3825
  }) : null ]
2564
3826
  });
2565
3827
  }
@@ -2572,38 +3834,81 @@ function targetOf(row) {
2572
3834
  };
2573
3835
  }
2574
3836
 
2575
- function InstanceBands({bands: bands, instanceDefaultOpen: instanceDefaultOpen, instances: instances, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
3837
+ function DocumentBand({group: group, ...wiring}) {
3838
+ const {open: open, toggle: toggle} = useBandOpen(wiring.bands, group.key);
3839
+ /* @__PURE__ */
3840
+ return jsx(CollapsibleBand, {
3841
+ background: !0,
3842
+ onToggle: toggle,
3843
+ open: open,
3844
+ sticky: !0,
3845
+ title: toBareId(group.document.id),
3846
+ header: /* @__PURE__ */ jsx(Flex, {
3847
+ align: "center",
3848
+ flex: "none",
3849
+ style: {
3850
+ maxWidth: "60%"
3851
+ },
3852
+ children: /* @__PURE__ */ jsx(RefChips, {
3853
+ max: 2,
3854
+ refs: group.documents
3855
+ })
3856
+ }),
3857
+ children: /* @__PURE__ */ jsx(InstanceBands, {
3858
+ instances: group.instances,
3859
+ ...wiring
3860
+ })
3861
+ });
3862
+ }
3863
+
3864
+ function UngroupedBand({instances: instances, ...wiring}) {
3865
+ const {open: open, toggle: toggle} = useBandOpen(wiring.bands, UNGROUPED_BAND_KEY);
3866
+ /* @__PURE__ */
3867
+ return jsx(CollapsibleBand, {
3868
+ background: !0,
3869
+ onToggle: toggle,
3870
+ open: open,
3871
+ sticky: !0,
3872
+ title: UNGROUPED_TITLE,
3873
+ header: /* @__PURE__ */ jsx(Text, {
3874
+ size: 1,
3875
+ weight: "semibold",
3876
+ children: UNGROUPED_TITLE
3877
+ }),
3878
+ children: /* @__PURE__ */ jsx(InstanceBands, {
3879
+ instances: instances,
3880
+ ...wiring
3881
+ })
3882
+ });
3883
+ }
3884
+
3885
+ function InstanceBands({instances: instances, ...wiring}) {
2576
3886
  /* @__PURE__ */
2577
3887
  return jsx(Stack, {
2578
3888
  gap: 1,
2579
- children: instances.map(band => /* @__PURE__ */ jsx(InstanceBand, {
2580
- band: band,
2581
- bands: bands,
2582
- instanceDefaultOpen: instanceDefaultOpen,
2583
- onOpenTask: onOpenTask,
2584
- onOpenWorkflow: onOpenWorkflow,
2585
- showTerminalActions: showTerminalActions
2586
- }, band.instanceId))
3889
+ children: instances.map(instance => /* @__PURE__ */ jsx(InstanceBand, {
3890
+ instance: instance,
3891
+ ...wiring
3892
+ }, instance.instanceId))
2587
3893
  });
2588
3894
  }
2589
3895
 
2590
- function InstanceBand({band: band, bands: bands, instanceDefaultOpen: instanceDefaultOpen, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
2591
- const {resolvePathFromState: resolvePathFromState} = useRouter();
3896
+ function InstanceBand({bands: bands, instance: instance, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
3897
+ const {open: open, toggle: toggle} = useBandOpen(bands, instance.instanceId), {resolvePathFromState: resolvePathFromState} = useRouter();
2592
3898
  /* @__PURE__ */
2593
3899
  return jsx(CollapsibleBand, {
2594
- onToggle: () => bands.toggle(band.instanceId, instanceDefaultOpen),
2595
- open: bands.isOpen(band.instanceId, instanceDefaultOpen),
2596
- title: band.title,
3900
+ onToggle: toggle,
3901
+ open: open,
3902
+ title: instance.title,
2597
3903
  header: /* @__PURE__ */ jsxs(Fragment, {
2598
3904
  children: [
2599
3905
  /* @__PURE__ */ jsx(LinkChip, {
2600
3906
  hint: "View workflow",
2601
3907
  href: resolvePathFromState({
2602
- instanceId: band.instanceId
3908
+ instanceId: instance.instanceId
2603
3909
  }),
2604
3910
  onClick: event => {
2605
- event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || (event.preventDefault(),
2606
- onOpenWorkflow(band.instanceId));
3911
+ isPlainClick(event) && (event.preventDefault(), onOpenWorkflow(instance.instanceId));
2607
3912
  },
2608
3913
  children: /* @__PURE__ */ jsxs(Flex, {
2609
3914
  align: "center",
@@ -2619,12 +3924,12 @@ function InstanceBand({band: band, bands: bands, instanceDefaultOpen: instanceDe
2619
3924
  },
2620
3925
  textOverflow: "ellipsis",
2621
3926
  weight: "medium",
2622
- children: band.title
2623
- }), band.breadcrumb === void 0 ? null : /* @__PURE__ */ jsx(BreadcrumbTail, {
2624
- segments: [ band.breadcrumb ]
3927
+ children: instance.title
3928
+ }), instance.breadcrumb === void 0 ? null : /* @__PURE__ */ jsx(BreadcrumbTail, {
3929
+ segments: [ instance.breadcrumb ]
2625
3930
  }),
2626
3931
  /* @__PURE__ */ jsx(StageFace, {
2627
- title: band.stageTitle
3932
+ title: instance.stageTitle
2628
3933
  }) ]
2629
3934
  })
2630
3935
  }),
@@ -2634,7 +3939,7 @@ function InstanceBand({band: band, bands: bands, instanceDefaultOpen: instanceDe
2634
3939
  paddingLeft: 5,
2635
3940
  children: /* @__PURE__ */ jsx(TaskRows, {
2636
3941
  onOpenTask: onOpenTask,
2637
- rows: band.rows,
3942
+ rows: instance.rows,
2638
3943
  showTerminalActions: showTerminalActions
2639
3944
  })
2640
3945
  })
@@ -2668,40 +3973,19 @@ function TaskActivityRow({row: row, onOpen: onOpen, showTerminalActions: showTer
2668
3973
  dateControl: /* @__PURE__ */ jsx(ActivityDateControl, {
2669
3974
  dates: render.dates,
2670
3975
  instanceId: row.instanceId,
2671
- state: render.dateState
3976
+ state: render.dateState,
3977
+ surface: "tool-task-list"
2672
3978
  }),
2673
3979
  face: render.face,
2674
3980
  instanceId: row.instanceId,
2675
3981
  onOpen: onOpen,
3982
+ surface: "tool-task-list",
2676
3983
  terminalActions: showTerminalActions ? render.terminalActions : []
2677
3984
  })
2678
3985
  })
2679
3986
  });
2680
3987
  }
2681
3988
 
2682
- function useBandOpenState(args) {
2683
- const {segment: segment, scope: scope} = args, store = useMemo(() => createBandStateStore({
2684
- storageKey: bandStateStorageKey(scope)
2685
- }), [ scope ]), [held, setHeld] = useState(() => ({
2686
- store: store,
2687
- toggles: store.read()
2688
- }));
2689
- held.store !== store && setHeld({
2690
- store: store,
2691
- toggles: store.read()
2692
- });
2693
- const qualify = key => `${segment}/${key}`, isOpen = (key, defaultOpen) => held.toggles.get(qualify(key)) ?? defaultOpen;
2694
- return {
2695
- isOpen: isOpen,
2696
- toggle: (key, defaultOpen) => {
2697
- setHeld({
2698
- store: store,
2699
- toggles: store.setOpen(qualify(key), !isOpen(key, defaultOpen))
2700
- });
2701
- }
2702
- };
2703
- }
2704
-
2705
3989
  const NO_MISSING = /* @__PURE__ */ new Set;
2706
3990
 
2707
3991
  function sameIdSet(a, b) {
@@ -2741,15 +4025,18 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2741
4025
  }), [filters, setFilters] = useState({}), groups = useMemo(() => deriveTaskGroups({
2742
4026
  instances: instances,
2743
4027
  identity: identity
2744
- }), [ instances, identity ]), orphans = useOrphanedWorkflows(groups), listed = useMemo(() => hideOrphanedGroups(groups, orphans.groupKeys), [ groups, orphans.groupKeys ]), options = useMemo(() => taskFilterOptions(listed), [ listed ]), leadDocuments = useMemo(() => listed.withDocuments.map(group => group.document), [ listed ]), previewTitles = useDocPreviewTitles(leadDocuments), visible = useMemo(() => {
4028
+ }), [ instances, identity ]), orphans = useOrphanedWorkflows(groups), listed = useMemo(() => hideOrphanedGroups(groups, orphans.groupKeys), [ groups, orphans.groupKeys ]), options = useMemo(() => taskFilterOptions(listed), [ listed ]), titles = useGroupTitles(listed), revealed = useRevealGate({
4029
+ loading: loading,
4030
+ ready: titles.ready
4031
+ }), visible = useMemo(() => {
2745
4032
  const filtered = applyTaskFilters({
2746
4033
  groups: listed,
2747
4034
  filters: filters,
2748
4035
  now: /* @__PURE__ */ new Date
2749
4036
  }), cut = mineOnly ? onlyMyTasks(filtered) : filtered;
2750
- return orderGroupsByPreviewTitle(cut, previewTitles.titles);
2751
- }, [ listed, filters, mineOnly, previewTitles ]);
2752
- if (loading) /* @__PURE__ */
4037
+ return orderGroupsByPreviewTitle(cut, titles.titles);
4038
+ }, [ listed, filters, mineOnly, titles ]);
4039
+ if (loading || !revealed) /* @__PURE__ */
2753
4040
  return jsx(TabStatusRow, {
2754
4041
  children: /* @__PURE__ */ jsx(LoadingRow, {
2755
4042
  label: "Loading tasks…"
@@ -2757,16 +4044,13 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2757
4044
  });
2758
4045
  const empty = visible.withDocuments.length === 0 && visible.withoutDocuments.length === 0;
2759
4046
  let body;
2760
- return empty ? mineOnly ? body = /* @__PURE__ */ jsx(ForMeEmptyState, {}) : body = /* @__PURE__ */ jsx(TabStatusRow, {
2761
- children: /* @__PURE__ */ jsx(Text, {
2762
- muted: !0,
2763
- size: 1,
2764
- children: "No tasks match — adjust the filters, or start a workflow"
2765
- })
4047
+ return empty ? mineOnly ? body = /* @__PURE__ */ jsx(ForMeEmptyState, {}) : body = /* @__PURE__ */ jsx(EmptyState, {
4048
+ description: "Adjust the filters, or start a workflow",
4049
+ icon: /* @__PURE__ */ jsx(SearchIcon, {}),
4050
+ title: "No tasks match"
2766
4051
  }) : body = /* @__PURE__ */ jsx(TaskGroupList, {
2767
4052
  bands: bands,
2768
4053
  groups: visible,
2769
- instanceDefaultOpen: instanceBandDefaultOpen(segment),
2770
4054
  onOpenTask: onOpenTask,
2771
4055
  onOpenWorkflow: onOpenWorkflow,
2772
4056
  showTerminalActions: mineOnly
@@ -2792,52 +4076,106 @@ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loadin
2792
4076
  /* @__PURE__ */ jsx(OrphanedWorkflowsNote, {
2793
4077
  orphans: orphans
2794
4078
  }), body, truncated ? /* @__PURE__ */ jsx(MutedNote, {
2795
- text: `Tasks from the latest ${INSTANCE_CAP} workflows — older ones aren't listed.`
4079
+ text: `Tasks from the latest ${INSTANCE_CAP} workflows — older ones arent listed.`
2796
4080
  }) : null ]
2797
4081
  }) ]
2798
4082
  });
2799
4083
  }
2800
4084
 
2801
- const TOOL_TABS = [ {
2802
- value: "overview",
2803
- label: "Overview"
2804
- }, {
2805
- value: "definitions",
2806
- label: "Definitions"
2807
- }, {
2808
- value: "for-me",
2809
- label: "For me"
2810
- } ], TOOL_TAB_VALUES = TOOL_TABS.map(tab => tab.value);
4085
+ const TOOL_TAB_LABELS = {
4086
+ definitions: "Definitions",
4087
+ documents: "Documents",
4088
+ tasks: "Tasks",
4089
+ "for-me": "For me"
4090
+ }, TOOL_TAB_VALUES = Object.keys(TOOL_TAB_LABELS), TOOL_TABS = TOOL_TAB_VALUES.map(value => ({
4091
+ value: value,
4092
+ label: TOOL_TAB_LABELS[value]
4093
+ })), DEFAULT_TOOL_TAB = "definitions";
4094
+
4095
+ function toolTabState(tab) {
4096
+ return tab === DEFAULT_TOOL_TAB ? {} : {
4097
+ workflowsTab: tab
4098
+ };
4099
+ }
4100
+
4101
+ function boardState(definitionName) {
4102
+ return {
4103
+ boardDefinition: definitionName
4104
+ };
4105
+ }
4106
+
4107
+ function tabSelectionNavigates(route, tab) {
4108
+ return !(route.kind === "home" && tab === route.tab);
4109
+ }
4110
+
4111
+ function backToAllLabel(tab) {
4112
+ return `Back to all ${TOOL_TAB_LABELS[tab].toLowerCase()}`;
4113
+ }
4114
+
4115
+ function toolRoute(state) {
4116
+ const {instanceId: instanceId, definitionName: definitionName, boardDefinition: boardDefinition} = state;
4117
+ if (typeof instanceId == "string") return {
4118
+ kind: "instance",
4119
+ instanceId: instanceId,
4120
+ tab: "tasks"
4121
+ };
4122
+ if (typeof definitionName == "string") return {
4123
+ kind: "definition",
4124
+ definitionName: definitionName,
4125
+ tab: "definitions"
4126
+ };
4127
+ if (typeof boardDefinition == "string") return {
4128
+ kind: "home",
4129
+ tab: "documents",
4130
+ boardDefinition: boardDefinition
4131
+ };
4132
+ const tab = pathSegmentTab({
4133
+ segment: state.workflowsTab,
4134
+ tabs: TOOL_TAB_VALUES,
4135
+ fallback: DEFAULT_TOOL_TAB
4136
+ });
4137
+ return tab === void 0 ? {
4138
+ kind: "not-found",
4139
+ tab: void 0
4140
+ } : {
4141
+ kind: "home",
4142
+ tab: tab,
4143
+ boardDefinition: void 0
4144
+ };
4145
+ }
2811
4146
 
2812
4147
  function InstanceStreamKeepAlive() {
2813
4148
  return useToolInstances(), null;
2814
4149
  }
2815
4150
 
2816
4151
  function WorkflowsToolRoot() {
2817
- const router = useRouter(), {seedInstance: seedInstance} = useWorkflowContext(), telemetry = useWorkflowTelemetry(), [filterSlot, setFilterSlot] = useState(null), goHome = () => router.navigate({}), instanceId = typeof router.state.instanceId == "string" ? router.state.instanceId : void 0, definitionName = typeof router.state.definitionName == "string" ? router.state.definitionName : void 0, isHome = instanceId === void 0 && definitionName === void 0, [instanceStreamLatched, setInstanceStreamLatched] = useState(isHome);
4152
+ const router = useRouter(), {seedInstance: seedInstance} = useWorkflowContext(), telemetry = useWorkflowTelemetry(), [filterSlot, setFilterSlot] = useState(null), route = toolRoute(router.state), isHome = route.kind === "home" || route.kind === "not-found", [instanceStreamLatched, setInstanceStreamLatched] = useState(isHome);
2818
4153
  useEffect(() => {
2819
4154
  isHome && setInstanceStreamLatched(!0);
2820
4155
  }, [ isHome ]);
2821
- let tab;
2822
- instanceId !== void 0 ? tab = "overview" : definitionName !== void 0 ? tab = "definitions" : tab = pathSegmentTab({
2823
- segment: router.state.workflowsTab,
2824
- tabs: TOOL_TAB_VALUES,
2825
- fallback: "overview"
2826
- });
2827
- const setTab = next => router.navigate(next === "overview" ? {} : {
2828
- workflowsTab: next
2829
- });
2830
- useLogEventOnMount(WorkflowToolOpened, {
2831
- tab: tab,
4156
+ const setTab = next => router.navigate(toolTabState(next)), selectBoardWorkflow = useCallback((name, opts) => router.navigate(boardState(name), opts?.replace === !0 ? {
4157
+ replace: !0
4158
+ } : {}), [ router ]), backToList = tab => ({
4159
+ label: backToAllLabel(tab),
4160
+ go: () => setTab(tab)
4161
+ }), badSegment = route.kind === "not-found";
4162
+ useEffect(() => {
4163
+ badSegment && router.navigate({}, {
4164
+ replace: !0
4165
+ });
4166
+ }, [ badSegment, router ]), useLogEventOnMount(WorkflowToolOpened, {
4167
+ tab: route.tab ?? "not-found",
2832
4168
  via: "open"
2833
4169
  });
2834
4170
  let panel;
2835
- return instanceId !== void 0 ? panel = /* @__PURE__ */ jsx(WorkflowInstanceDetail, {
2836
- instanceId: instanceId,
2837
- onBack: goHome
2838
- }, instanceId) : definitionName !== void 0 ? panel = /* @__PURE__ */ jsx(DefinitionDetail, {
2839
- definitionName: definitionName
2840
- }, definitionName) : panel = /* @__PURE__ */ jsx(WorkflowsHome, {
4171
+ return route.kind === "instance" ? panel = /* @__PURE__ */ jsx(WorkflowInstanceDetail, {
4172
+ instanceId: route.instanceId,
4173
+ backToList: backToList(route.tab)
4174
+ }, route.instanceId) : route.kind === "definition" ? panel = /* @__PURE__ */ jsx(DefinitionDetail, {
4175
+ definitionName: route.definitionName,
4176
+ backToList: backToList(route.tab)
4177
+ }, route.definitionName) : route.kind === "not-found" ? panel = null : panel = /* @__PURE__ */ jsx(WorkflowsHome, {
4178
+ boardDefinition: route.boardDefinition,
2841
4179
  filterSlot: filterSlot,
2842
4180
  onOpenDefinition: name => router.navigate({
2843
4181
  definitionName: name
@@ -2847,7 +4185,8 @@ function WorkflowsToolRoot() {
2847
4185
  instanceId: typeof instance == "string" ? instance : instance._id
2848
4186
  });
2849
4187
  },
2850
- tab: tab
4188
+ onSelectWorkflow: selectBoardWorkflow,
4189
+ tab: route.tab
2851
4190
  }),
2852
4191
  /* @__PURE__ */ jsxs(Card, {
2853
4192
  height: "fill",
@@ -2882,13 +4221,13 @@ function WorkflowsToolRoot() {
2882
4221
  ariaControls: "workflows-tool-panel",
2883
4222
  idPrefix: "workflows-tool-tab",
2884
4223
  onSelect: next => {
2885
- next !== tab && telemetry.log(WorkflowToolOpened, {
4224
+ tabSelectionNavigates(route, next) && (next !== route.tab && telemetry.log(WorkflowToolOpened, {
2886
4225
  tab: next,
2887
4226
  via: "tab-switch"
2888
- }), setTab(next);
4227
+ }), setTab(next));
2889
4228
  },
2890
4229
  options: TOOL_TABS,
2891
- selected: tab
4230
+ selected: route.tab
2892
4231
  }),
2893
4232
  /* @__PURE__ */ jsx("div", {
2894
4233
  ref: setFilterSlot
@@ -2904,17 +4243,20 @@ function WorkflowsToolRoot() {
2904
4243
  });
2905
4244
  }
2906
4245
 
2907
- function WorkflowsHome({filterSlot: filterSlot, onOpenDefinition: onOpenDefinition, onOpenInstance: onOpenInstance, tab: tab}) {
4246
+ function WorkflowsHome({boardDefinition: boardDefinition, filterSlot: filterSlot, onOpenDefinition: onOpenDefinition, onOpenInstance: onOpenInstance, onSelectWorkflow: onSelectWorkflow, tab: tab}) {
2908
4247
  const instances = useToolInstances(), identity = useAssignmentIdentity(), [openTask, setOpenTask] = useState(null), openInstance = target => {
2909
4248
  if (typeof target != "string") return onOpenInstance(target);
2910
4249
  const held = [ ...instances.inFlight, ...instances.settled ].find(i => i._id === target);
2911
4250
  onOpenInstance(held ?? target);
2912
4251
  }, scrollerRef = useRef(null);
2913
- return useEffect(() => {
4252
+ useEffect(() => {
2914
4253
  scrollerRef.current?.scrollTo({
2915
4254
  top: 0
2916
4255
  });
2917
- }, [ tab ]), /* @__PURE__ */ jsxs(Flex, {
4256
+ }, [ tab ]);
4257
+ const bleeds = tab === "documents";
4258
+ /* @__PURE__ */
4259
+ return jsxs(Flex, {
2918
4260
  direction: "column",
2919
4261
  height: "fill",
2920
4262
  overflow: "hidden",
@@ -2923,9 +4265,9 @@ function WorkflowsHome({filterSlot: filterSlot, onOpenDefinition: onOpenDefiniti
2923
4265
  ref: scrollerRef,
2924
4266
  flex: 1,
2925
4267
  overflow: "auto",
2926
- paddingX: 3,
4268
+ paddingX: bleeds ? 0 : TOOL_PANEL_PADDING,
2927
4269
  style: {
2928
- scrollbarGutter: "stable"
4270
+ scrollbarGutter: bleeds ? "auto" : "stable"
2929
4271
  },
2930
4272
  children: /* @__PURE__ */ jsx(TabPanel, {
2931
4273
  "aria-labelledby": `workflows-tool-tab-${tab}`,
@@ -2938,18 +4280,19 @@ function WorkflowsHome({filterSlot: filterSlot, onOpenDefinition: onOpenDefiniti
2938
4280
  children: /* @__PURE__ */ jsx(Flex, {
2939
4281
  direction: "column",
2940
4282
  flex: 1,
2941
- children: tab === "definitions" ? /* @__PURE__ */ jsx(DefinitionsTab, {
2942
- onOpenDefinition: onOpenDefinition
2943
- }) : /* @__PURE__ */ jsx(TasksTab, {
4283
+ style: {
4284
+ minHeight: 0
4285
+ },
4286
+ children: /* @__PURE__ */ jsx(HomePanel, {
4287
+ boardDefinition: boardDefinition,
2944
4288
  filterSlot: filterSlot,
2945
4289
  identity: identity,
2946
- instances: [ ...instances.inFlight, ...instances.settled ],
2947
- loading: instances.loading,
4290
+ instances: instances,
4291
+ onOpenDefinition: onOpenDefinition,
2948
4292
  onOpenTask: setOpenTask,
2949
4293
  onOpenWorkflow: openInstance,
2950
- segment: tab,
2951
- truncated: instances.truncated,
2952
- unreadable: instances.unreadable
4294
+ onSelectWorkflow: onSelectWorkflow,
4295
+ tab: tab
2953
4296
  })
2954
4297
  })
2955
4298
  })
@@ -2960,4 +4303,25 @@ function WorkflowsHome({filterSlot: filterSlot, onOpenDefinition: onOpenDefiniti
2960
4303
  });
2961
4304
  }
2962
4305
 
4306
+ function HomePanel({boardDefinition: boardDefinition, filterSlot: filterSlot, identity: identity, instances: instances, onOpenDefinition: onOpenDefinition, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, onSelectWorkflow: onSelectWorkflow, tab: tab}) {
4307
+ return tab === "definitions" ? /* @__PURE__ */ jsx(DefinitionsTab, {
4308
+ onOpenDefinition: onOpenDefinition
4309
+ }) : tab === "documents" ? /* @__PURE__ */ jsx(DocumentsTab, {
4310
+ filterSlot: filterSlot,
4311
+ onOpenDefinition: onOpenDefinition,
4312
+ onSelectWorkflow: onSelectWorkflow,
4313
+ routeDefinition: boardDefinition
4314
+ }) : /* @__PURE__ */ jsx(TasksTab, {
4315
+ filterSlot: filterSlot,
4316
+ identity: identity,
4317
+ instances: [ ...instances.inFlight, ...instances.settled ],
4318
+ loading: instances.loading,
4319
+ onOpenTask: onOpenTask,
4320
+ onOpenWorkflow: onOpenWorkflow,
4321
+ segment: tab,
4322
+ truncated: instances.truncated,
4323
+ unreadable: instances.unreadable
4324
+ });
4325
+ }
4326
+
2963
4327
  export { WorkflowsToolRoot as default };