@sanity/workflow-studio-plugin 0.4.0 → 0.20.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,18 +1,22 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
 
3
- import { Box, Text, Flex, Button, Badge, Stack, Card, useTheme_v2, Spinner, TextInput, Dialog, Menu, MenuItem, TabList, Tab, Container } from "@sanity/ui";
3
+ import { Stack, Box, Flex, Text, Button, Card, TextInput, Dialog, Badge, useTheme_v2, Container, TabPanel } from "@sanity/ui";
4
4
 
5
- import { useMemo, useState, useEffect, useRef } from "react";
5
+ import { useState, useEffect, useMemo, useRef } from "react";
6
6
 
7
7
  import { useRouter } from "sanity/router";
8
8
 
9
- import { DocPreviewLink, findActivity, definitionSnapshotOf, instanceTitle, stateByActivity, isActivityAssignedTo, assigneeUserIdsOf, isOpenActivityStatus, stageTitle, formatDate, parseStoredDateValue, useWorkflowContext, gdrLocality, useUserDisplays, useProjectMembers, DismissablePopover, HoverHint, DocRefFace, useAssignmentIdentity, LoadingRow, AbortedBadge, instanceNotice, StaleLock, ActivitiesList, TodoItemsList, isEvaluationStale, useWorkflowInstanceEntry, InvalidDocNotice, useDefinition, openActivityGone, GroupHeading, ActivityDetailDialog, instanceBreadcrumb, StageChip, readDeployedDefinitions, describeError, StageGlyph, useDocLink, HintedMenuButton, ActivityStatusIcon, UserAvatarGroup } from "./index.js";
9
+ import { instanceNotice, StaleLock, ActivitiesList, TodoItemsList, isEvaluationStale, useWorkflowInstanceEntry, InvalidDocNotice, LoadingRow, useDefinition, openActivityGone, GroupHeading, ActivityDetailDialog, instanceBreadcrumb, HoverHint, instanceTitle, formatDate, StageChip, stageTitle, AbortedBadge, SpinnerSlot, DocPreviewLink, useWorkflowContext, readDeployedDefinitions, describeError, DismissablePopover, definitionSnapshotOf, findActivity, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isActivityAssignedTo, assigneeUserIdsOf, isOpenActivityStatus, committedRowFace, activityRowProps, parseStoredDateValue, gdrLocality, useUserDisplays, useProjectMembers, DocRefFace, useSaveField, dateControlValue, stopRowMouseDown, stopRowClick, parseDateFieldValue, serializeDateFieldValue, dateControlKind, editTargetOf, CollapsibleBand, LinkChip, BreadcrumbTail, StageFace, TrailingHairline, ActivityRow, UnreadableDocsNote, useAssignmentIdentity, TabSwitch, workflowsTabCodec } from "./index.js";
10
10
 
11
- import { ChevronDownIcon } from "@sanity/icons/ChevronDown";
11
+ import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
12
+
13
+ import { WorkflowDiagram } from "@sanity/workflow-diagram";
14
+
15
+ import { entryDocRefs, terminalState, latestDeployedDefinitions, isStartableDefinition, toBareId, findOpenStageEntry } from "@sanity/workflow-engine";
12
16
 
13
- import { ChevronRightIcon } from "@sanity/icons/ChevronRight";
17
+ import { AddIcon } from "@sanity/icons/Add";
14
18
 
15
- import { entryDocRefs, extractDocumentId, findOpenStageEntry, terminalState, latestDeployedDefinitions, isStartableDefinition } from "@sanity/workflow-engine";
19
+ import { SearchIcon } from "@sanity/icons/Search";
16
20
 
17
21
  import { CheckmarkIcon } from "@sanity/icons/Checkmark";
18
22
 
@@ -20,106 +24,437 @@ import { CloseIcon } from "@sanity/icons/Close";
20
24
 
21
25
  import { FilterIcon } from "@sanity/icons/Filter";
22
26
 
23
- import { MemberAvatar, DatePicker } from "@sanity/workflow-components";
27
+ import { MemberAvatar, DatePicker, ClearableDatePicker } from "@sanity/workflow-components";
24
28
 
25
29
  import { endOfDay } from "date-fns/endOfDay";
26
30
 
27
- import { useDocumentPreviewStore, useSchema } from "sanity";
28
-
29
- import { useWorkflowInstances } from "@sanity/workflow-studio";
30
-
31
- import { ArrowLeftIcon } from "@sanity/icons/ArrowLeft";
31
+ import { createPortal } from "react-dom";
32
32
 
33
- import { WorkflowDiagram } from "@sanity/workflow-diagram";
33
+ import { useDocumentPreviewStore, useSchema } from "sanity";
34
34
 
35
- import { AddIcon } from "@sanity/icons/Add";
35
+ import { CalendarIcon } from "@sanity/icons/Calendar";
36
36
 
37
- import { SearchIcon } from "@sanity/icons/Search";
37
+ import { useWorkflowInstances } from "@sanity/workflow-studio";
38
38
 
39
- import { EllipsisHorizontalIcon } from "@sanity/icons/EllipsisHorizontal";
39
+ function documentRefsOf(instance) {
40
+ const seen = /* @__PURE__ */ new Map;
41
+ for (const ref of entryDocRefs(instance.fields)) seen.has(ref.id) || seen.set(ref.id, ref);
42
+ return [ ...seen.values() ];
43
+ }
40
44
 
41
- function MutedNote({text: text}) {
42
- /* @__PURE__ */
43
- return jsx(Box, {
44
- paddingY: 3,
45
- children: /* @__PURE__ */ jsx(Text, {
46
- muted: !0,
47
- size: 1,
48
- children: text
45
+ function InstanceTaskLists({entry: entry, onOpenActivity: onOpenActivity}) {
46
+ return instanceNotice(entry) || /* @__PURE__ */ jsx(StaleLock, {
47
+ stale: isEvaluationStale(entry),
48
+ children: /* @__PURE__ */ jsxs(Stack, {
49
+ children: [
50
+ /* @__PURE__ */ jsx(ActivitiesList, {
51
+ entry: entry,
52
+ onOpenActivity: onOpenActivity
53
+ }),
54
+ /* @__PURE__ */ jsx(TodoItemsList, {
55
+ entry: entry
56
+ }) ]
49
57
  })
50
58
  });
51
59
  }
52
60
 
53
- function documentRefsOf(instance) {
54
- const seen = /* @__PURE__ */ new Map;
55
- for (const ref of entryDocRefs(instance.fields)) seen.has(ref.id) || seen.set(ref.id, ref);
56
- return [ ...seen.values() ];
61
+ function WorkflowInstanceDetail({instanceId: instanceId, onBack: onBack}) {
62
+ const entry = useWorkflowInstanceEntry(instanceId), [graceOver, setGraceOver] = useState(!1);
63
+ return useEffect(() => {
64
+ const timer = setTimeout(() => setGraceOver(!0), 2500);
65
+ return () => clearTimeout(timer);
66
+ }, [ instanceId ]), entry?.invalid ? /* @__PURE__ */ jsx(Box, {
67
+ padding: 4,
68
+ children: /* @__PURE__ */ jsx(InvalidDocNotice, {
69
+ invalid: entry.invalid
70
+ })
71
+ }) : entry ? /* @__PURE__ */ jsx(InstanceDetailPanel, {
72
+ entry: entry,
73
+ onBack: onBack
74
+ }) : /* @__PURE__ */ jsx(Box, {
75
+ padding: 4,
76
+ children: graceOver ? /* @__PURE__ */ jsx(NotFound, {
77
+ id: instanceId,
78
+ onBack: onBack
79
+ }) : /* @__PURE__ */ jsx(LoadingRow, {
80
+ label: "Loading workflow…"
81
+ })
82
+ });
57
83
  }
58
84
 
59
- function RefChips({refs: refs, max: max = 2}) {
60
- if (refs.length === 0) return null;
61
- const shown = refs.slice(0, max), overflow = refs.length - shown.length;
85
+ function NotFound({id: id, onBack: onBack}) {
62
86
  /* @__PURE__ */
63
- return jsxs(Flex, {
64
- align: "center",
65
- gap: 3,
66
- wrap: "wrap",
67
- children: [ shown.map(ref => /* @__PURE__ */ jsx(DocPreviewLink, {
68
- gdr: ref,
69
- layout: "inline"
70
- }, ref.id)), overflow > 0 ? /* @__PURE__ */ jsxs(Text, {
87
+ return jsxs(Stack, {
88
+ gap: 4,
89
+ children: [
90
+ /* @__PURE__ */ jsxs(Text, {
71
91
  muted: !0,
72
92
  size: 1,
73
- children: [ "+", overflow, " more" ]
93
+ children: [ "No workflow with id “", id, "” — it may have been deleted." ]
94
+ }),
95
+ /* @__PURE__ */ jsx(Flex, {
96
+ children: /* @__PURE__ */ jsx(Button, {
97
+ fontSize: 1,
98
+ icon: ArrowLeftIcon,
99
+ mode: "ghost",
100
+ onClick: onBack,
101
+ padding: 2,
102
+ text: "Back to workflows"
103
+ })
104
+ }) ]
105
+ });
106
+ }
107
+
108
+ function InstanceDetailPanel({entry: entry, onBack: onBack}) {
109
+ const definition = useDefinition(entry), [openActivity, setOpenActivity] = useState(null), {instance: instance} = entry;
110
+ return useEffect(() => {
111
+ openActivity && openActivityGone({
112
+ entry: entry,
113
+ target: openActivity
114
+ }) && setOpenActivity(null);
115
+ }, [ entry, openActivity ]), /* @__PURE__ */ jsxs(Flex, {
116
+ direction: "column",
117
+ height: "fill",
118
+ overflow: "hidden",
119
+ children: [
120
+ /* @__PURE__ */ jsx(DetailHeader, {
121
+ definition: definition,
122
+ entry: entry,
123
+ onBack: onBack
124
+ }),
125
+ /* @__PURE__ */ jsx(Box, {
126
+ flex: 1,
127
+ overflow: "auto",
128
+ paddingX: 4,
129
+ children: /* @__PURE__ */ jsxs(Stack, {
130
+ gap: 4,
131
+ paddingBottom: 5,
132
+ children: [
133
+ /* @__PURE__ */ jsx(StudioDocumentsSection, {
134
+ instance: instance
135
+ }),
136
+ /* @__PURE__ */ jsx(StageDiagramSection, {
137
+ definition: definition,
138
+ entry: entry
139
+ }),
140
+ /* @__PURE__ */ jsx(GroupHeading, {
141
+ title: "All tasks"
142
+ }),
143
+ /* @__PURE__ */ jsx(InstanceTaskLists, {
144
+ entry: entry,
145
+ onOpenActivity: name => setOpenActivity({
146
+ stage: instance.currentStage,
147
+ activityName: name
148
+ })
149
+ }) ]
150
+ })
151
+ }), openActivity ? /* @__PURE__ */ jsx(ActivityDetailDialog, {
152
+ activityName: openActivity.activityName,
153
+ breadcrumb: instanceBreadcrumb(instance, definition),
154
+ definition: definition,
155
+ entry: entry,
156
+ onClose: () => setOpenActivity(null)
74
157
  }) : null ]
75
158
  });
76
159
  }
77
160
 
78
- function DocRefChips({instance: instance, max: max = 2}) {
79
- const refs = useMemo(() => documentRefsOf(instance), [ instance ]);
161
+ function DetailHeader({entry: entry, definition: definition, onBack: onBack}) {
162
+ const {instance: instance, ready: ready} = entry;
80
163
  /* @__PURE__ */
81
- return jsx(RefChips, {
82
- max: max,
83
- refs: refs
164
+ return jsx(Card, {
165
+ flex: "none",
166
+ paddingX: 4,
167
+ paddingY: 3,
168
+ children: /* @__PURE__ */ jsxs(Flex, {
169
+ align: "center",
170
+ gap: 3,
171
+ wrap: "wrap",
172
+ children: [
173
+ /* @__PURE__ */ jsx(HoverHint, {
174
+ text: "Back to workflows",
175
+ children: /* @__PURE__ */ jsx(Button, {
176
+ "aria-label": "Back to workflows",
177
+ fontSize: 1,
178
+ icon: ArrowLeftIcon,
179
+ mode: "bleed",
180
+ onClick: onBack,
181
+ padding: 2
182
+ })
183
+ }),
184
+ /* @__PURE__ */ jsxs(Stack, {
185
+ gap: 2,
186
+ children: [
187
+ /* @__PURE__ */ jsx(Text, {
188
+ size: 1,
189
+ weight: "semibold",
190
+ children: instanceTitle(instance, definition)
191
+ }),
192
+ /* @__PURE__ */ jsxs(Text, {
193
+ muted: !0,
194
+ size: 1,
195
+ children: [ "Started ", formatDate(instance.startedAt) ]
196
+ }) ]
197
+ }), terminalState(instance) === "in-flight" ? /* @__PURE__ */ jsx(StageChip, {
198
+ title: stageTitle(definition, instance.currentStage)
199
+ }) : /* @__PURE__ */ jsx(AbortedBadge, {
200
+ instance: instance
201
+ }),
202
+ /* @__PURE__ */ jsx(SpinnerSlot, {
203
+ busy: !ready || isEvaluationStale(entry)
204
+ }) ]
205
+ })
84
206
  });
85
207
  }
86
208
 
87
- function datesOf(state) {
88
- return state.filter(entry => entry._type === "date" || entry._type === "datetime").map(entry => entry.value).filter(value => typeof value == "string" && value !== "");
209
+ function StudioDocumentsSection({instance: instance}) {
210
+ const refs = documentRefsOf(instance);
211
+ return refs.length === 0 ? null : /* @__PURE__ */ jsxs(Stack, {
212
+ gap: 3,
213
+ marginTop: 3,
214
+ children: [
215
+ /* @__PURE__ */ jsx(Text, {
216
+ muted: !0,
217
+ size: 1,
218
+ weight: "medium",
219
+ children: refs.length === 1 ? "Studio document" : "Studio documents"
220
+ }),
221
+ /* @__PURE__ */ jsx(Stack, {
222
+ gap: 2,
223
+ children: refs.map(ref => /* @__PURE__ */ jsx(DocPreviewLink, {
224
+ gdr: ref
225
+ }, ref.id))
226
+ }) ]
227
+ });
89
228
  }
90
229
 
91
- function spawnOrigins(instances) {
92
- const origins = /* @__PURE__ */ new Map;
93
- for (const parent of instances) for (const row of parent.subworkflows ?? []) origins.set(extractDocumentId(row.ref.id), {
94
- parent: parent,
95
- activityName: row.activity,
96
- cohortStage: row.cohortStage
230
+ function StageDiagramSection({entry: entry, definition: definition}) {
231
+ const {instance: instance} = entry;
232
+ return definition ?
233
+ /* @__PURE__ */ jsx(WorkflowDiagram, {
234
+ currentStage: terminalState(instance) === "in-flight" ? instance.currentStage : void 0,
235
+ definition: definition,
236
+ evaluation: entry.evaluation,
237
+ explain: !0,
238
+ guardCount: entry.guards?.length,
239
+ height: 340,
240
+ history: instance.history
241
+ }, instance._id) : /* @__PURE__ */ jsx(Card, {
242
+ border: !0,
243
+ marginTop: 3,
244
+ padding: 3,
245
+ radius: 2,
246
+ tone: "caution",
247
+ children: /* @__PURE__ */ jsx(Text, {
248
+ muted: !0,
249
+ size: 1,
250
+ children: "Couldn’t load this workflow’s full details — the stage diagram is unavailable, and technical names show instead of titles."
251
+ })
97
252
  });
98
- return origins;
99
253
  }
100
254
 
101
- function rootOf(instance, byId) {
102
- const rootRef = instance.ancestors[0];
103
- return rootRef ? byId.get(extractDocumentId(rootRef.id)) ?? instance : instance;
255
+ function MutedNote({text: text}) {
256
+ /* @__PURE__ */
257
+ return jsx(Box, {
258
+ paddingY: 3,
259
+ children: /* @__PURE__ */ jsx(Text, {
260
+ muted: !0,
261
+ size: 1,
262
+ children: text
263
+ })
264
+ });
104
265
  }
105
266
 
106
- function taskRowsOf(args) {
107
- const {instance: instance, breadcrumb: breadcrumb, identity: identity} = args, open = findOpenStageEntry(instance);
108
- if (!open) return [];
109
- const definition = definitionSnapshotOf(instance), state = stateByActivity(instance), rows = (open.activities ?? []).map(activity => {
110
- const fields = state.get(activity.name) ?? [], declared = findActivity({
267
+ function useStartableDefinitions() {
268
+ const {engine: engine} = useWorkflowContext(), [definitions, setDefinitions] = useState(void 0), [error, setError] = useState(void 0);
269
+ return useEffect(() => {
270
+ let cancelled = !1;
271
+ return (async () => {
272
+ const rows = await readDeployedDefinitions(engine), startable = latestDeployedDefinitions(rows).filter(row => isStartableDefinition(row)).map(row => ({
273
+ name: row.name,
274
+ title: row.title ?? row.name,
275
+ description: row.description
276
+ }));
277
+ cancelled || (setDefinitions(startable), setError(void 0));
278
+ })().catch(err => {
279
+ cancelled || setError(describeError(err));
280
+ }), () => {
281
+ cancelled = !0;
282
+ };
283
+ }, [ engine ]), {
284
+ definitions: definitions,
285
+ error: error
286
+ };
287
+ }
288
+
289
+ function NewWorkflowButton() {
290
+ const {openStartDialog: openStartDialog} = useWorkflowContext(), [open, setOpen] = useState(!1);
291
+ /* @__PURE__ */
292
+ return jsx(DismissablePopover, {
293
+ content: open ? /* @__PURE__ */ jsx(DefinitionPickerPanel, {
294
+ onPick: definition => {
295
+ setOpen(!1), openStartDialog({
296
+ definition: definition.name,
297
+ label: definition.title
298
+ });
299
+ }
300
+ }) : null,
301
+ onDismiss: () => setOpen(!1),
302
+ open: open,
303
+ children: /* @__PURE__ */ jsx(Button, {
304
+ fontSize: 1,
305
+ gap: 2,
306
+ icon: AddIcon,
307
+ onClick: () => setOpen(v => !v),
308
+ padding: 2,
309
+ selected: open,
310
+ text: "New workflow"
311
+ })
312
+ });
313
+ }
314
+
315
+ function DefinitionPickerPanel({onPick: onPick}) {
316
+ const {definitions: definitions, error: error} = useStartableDefinitions(), [query, setQuery] = useState(""), visible = useMemo(() => {
317
+ const q = query.trim().toLowerCase();
318
+ return definitions ? q ? definitions.filter(d => `${d.title} ${d.name}`.toLowerCase().includes(q)) : definitions : [];
319
+ }, [ definitions, query ]);
320
+ /* @__PURE__ */
321
+ return jsx(Box, {
322
+ padding: 2,
323
+ style: {
324
+ width: 340
325
+ },
326
+ children: /* @__PURE__ */ jsxs(Stack, {
327
+ gap: 2,
328
+ children: [
329
+ /* @__PURE__ */ jsx(TextInput, {
330
+ autoFocus: !0,
331
+ fontSize: 1,
332
+ icon: SearchIcon,
333
+ onChange: event => setQuery(event.currentTarget.value),
334
+ placeholder: "Filter workflows…",
335
+ value: query
336
+ }), error ? /* @__PURE__ */ jsx(MutedNote, {
337
+ text: `Could not load the deployed workflows: ${error}`
338
+ }) : null, !definitions && !error ? /* @__PURE__ */ jsx(LoadingRow, {
339
+ label: "Loading workflows…"
340
+ }) : null, definitions && definitions.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
341
+ text: "No startable workflows are deployed"
342
+ }) : null, definitions && definitions.length > 0 && visible.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
343
+ text: "No workflows match"
344
+ }) : null,
345
+ /* @__PURE__ */ jsx(Stack, {
346
+ gap: 1,
347
+ style: {
348
+ maxHeight: 320,
349
+ overflowY: "auto"
350
+ },
351
+ children: visible.map(definition => /* @__PURE__ */ jsx(Card, {
352
+ as: "button",
353
+ onClick: () => onPick(definition),
354
+ padding: 3,
355
+ radius: 2,
356
+ style: {
357
+ cursor: "pointer",
358
+ textAlign: "left"
359
+ },
360
+ children: /* @__PURE__ */ jsxs(Stack, {
361
+ gap: 2,
362
+ children: [
363
+ /* @__PURE__ */ jsx(Text, {
364
+ size: 1,
365
+ weight: "medium",
366
+ children: definition.title
367
+ }), definition.description ? /* @__PURE__ */ jsx(Text, {
368
+ muted: !0,
369
+ size: 1,
370
+ textOverflow: "ellipsis",
371
+ children: definition.description
372
+ }) : null ]
373
+ })
374
+ }, definition.name))
375
+ }) ]
376
+ })
377
+ });
378
+ }
379
+
380
+ function ToolActivityDialog({target: target, onClose: onClose}) {
381
+ const entry = useWorkflowInstanceEntry(target.instanceId), definition = useDefinition(entry), resolvedOnce = useRef(!1);
382
+ return entry && (resolvedOnce.current = !0), useEffect(() => {
383
+ resolvedOnce.current && openActivityGone({
384
+ entry: entry,
385
+ target: target
386
+ }) && onClose();
387
+ }, [ entry, target, onClose ]), entry?.invalid ? /* @__PURE__ */ jsx(Dialog, {
388
+ header: "Workflow unavailable",
389
+ id: "workflow-activity-detail",
390
+ onClose: onClose,
391
+ width: 1,
392
+ children: /* @__PURE__ */ jsx(Box, {
393
+ padding: 4,
394
+ children: /* @__PURE__ */ jsx(InvalidDocNotice, {
395
+ invalid: entry.invalid
396
+ })
397
+ })
398
+ }) : entry?.evaluation ? /* @__PURE__ */ jsx(ActivityDetailDialog, {
399
+ activityName: target.activityName,
400
+ breadcrumb: instanceBreadcrumb(entry.instance, definition),
401
+ definition: definition,
402
+ document: documentRefsOf(entry.instance)[0],
403
+ entry: entry,
404
+ onClose: onClose
405
+ }) : /* @__PURE__ */ jsx(Dialog, {
406
+ header: "Loading activity…",
407
+ id: "workflow-activity-detail",
408
+ onClose: onClose,
409
+ width: 1,
410
+ children: /* @__PURE__ */ jsx(Box, {
411
+ padding: 4,
412
+ children: /* @__PURE__ */ jsx(LoadingRow, {
413
+ label: "Fetching the live workflow state…"
414
+ })
415
+ })
416
+ });
417
+ }
418
+
419
+ function datesOf(state) {
420
+ return state.filter(entry => entry._type === "date" || entry._type === "datetime").map(entry => entry.value).filter(value => typeof value == "string" && value !== "");
421
+ }
422
+
423
+ function spawnOrigins(instances) {
424
+ const origins = /* @__PURE__ */ new Map;
425
+ for (const parent of instances) for (const row of parent.subworkflows ?? []) origins.set(toBareId(row.ref.id), {
426
+ parent: parent,
427
+ activityName: row.activity,
428
+ cohortStage: row.cohortStage
429
+ });
430
+ return origins;
431
+ }
432
+
433
+ function rootOf(instance, byId) {
434
+ const rootRef = instance.ancestors[0];
435
+ return rootRef ? byId.get(toBareId(rootRef.id)) ?? instance : instance;
436
+ }
437
+
438
+ function taskRowsOf(args) {
439
+ const {instance: instance, identity: identity} = args, open = findOpenStageEntry(instance);
440
+ if (!open) return [];
441
+ const definition = definitionSnapshotOf(instance), state = stateByActivity(instance), rows = (open.activities ?? []).map(activity => {
442
+ const fields = state.get(activity.name) ?? [], declared = findActivityNode({
111
443
  definition: definition,
112
444
  stageName: open.name,
113
445
  activityName: activity.name
114
- });
446
+ }) ?? {
447
+ name: activity.name
448
+ }, controlArgs = {
449
+ activity: declared,
450
+ status: activity.status,
451
+ editableFields: void 0
452
+ };
115
453
  return {
116
454
  instanceId: instance._id,
117
455
  activityName: activity.name,
118
- title: declared.title,
119
- description: declared.description,
120
- breadcrumb: breadcrumb,
456
+ title: declared.title ?? activity.name,
121
457
  stage: open.name,
122
- stageTitle: stageTitle(definition, open.name),
123
458
  status: activity.status,
124
459
  open: isOpenActivityStatus(activity.status),
125
460
  dates: datesOf(fields),
@@ -127,69 +462,124 @@ function taskRowsOf(args) {
127
462
  assignedToMe: identity !== void 0 && isActivityAssignedTo({
128
463
  state: fields,
129
464
  who: identity
130
- })
465
+ }),
466
+ assignControl: rowAssignControlState(controlArgs),
467
+ dateControl: rowDateControlState(controlArgs)
131
468
  };
132
469
  });
133
470
  return [ ...rows.filter(r => r.open), ...rows.filter(r => !r.open) ];
134
471
  }
135
472
 
136
473
  function deriveTaskGroups(args) {
137
- const {instances: instances, identity: identity} = args, byId = new Map(instances.map(instance => [ instance._id, instance ])), origins = spawnOrigins(instances), groups = /* @__PURE__ */ new Map, ordered = [ ...instances ].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
474
+ const {instances: instances, identity: identity} = args, byId = new Map(instances.map(instance => [ instance._id, instance ])), origins = spawnOrigins(instances), docGroups = /* @__PURE__ */ new Map, withoutDocuments = [], ordered = [ ...instances ].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
138
475
  for (const instance of ordered) {
139
- const origin = origins.get(instance._id), breadcrumb = origin ? findActivity({
140
- definition: definitionSnapshotOf(origin.parent),
141
- stageName: origin.cohortStage,
142
- activityName: origin.activityName
143
- }).title : void 0, rows = taskRowsOf({
476
+ const rows = taskRowsOf({
144
477
  instance: instance,
145
- breadcrumb: breadcrumb,
146
478
  identity: identity
147
- });
148
- if (rows.length === 0) continue;
149
- const root = rootOf(instance, byId), documents = documentRefsOf(root), document = documents[0], key = document ? `doc:${document.id}` : `def:${root.definition}`, held = groups.get(key);
479
+ }), first = rows[0];
480
+ if (!first) continue;
481
+ const origin = origins.get(instance._id), definition = definitionSnapshotOf(instance), entry = {
482
+ instanceId: instance._id,
483
+ workflowName: instance.definition,
484
+ title: instanceTitle(instance, definition),
485
+ stageTitle: stageTitle(definition, first.stage),
486
+ breadcrumb: origin ? findActivity({
487
+ definition: definitionSnapshotOf(origin.parent),
488
+ stageName: origin.cohortStage,
489
+ activityName: origin.activityName
490
+ }).title : void 0,
491
+ rows: rows
492
+ }, root = rootOf(instance, byId), documents = documentRefsOf(root), document = documents[0];
493
+ if (!document) {
494
+ withoutDocuments.push(entry);
495
+ continue;
496
+ }
497
+ const key = `doc:${document.id}`, held = docGroups.get(key);
150
498
  if (held) {
151
- held.rows.push(...rows);
499
+ held.instances.push(entry);
152
500
  continue;
153
501
  }
154
- const definition = definitionSnapshotOf(root);
155
- groups.set(key, {
502
+ docGroups.set(key, {
156
503
  group: {
157
504
  key: key,
158
505
  document: document,
159
- documents: documents,
160
- instanceId: root._id,
161
- workflowName: root.definition,
162
- workflowTitle: instanceTitle(root, definition),
163
- workflowDescription: definition?.description
506
+ documents: documents
164
507
  },
165
- rows: rows
508
+ instances: [ entry ]
166
509
  });
167
510
  }
168
- const assembled = [ ...groups.values() ].map(({group: group, rows: rows}) => ({
169
- ...group,
170
- rows: rows
171
- }));
172
511
  return {
173
- withDocuments: assembled.filter(g => g.document !== void 0),
174
- withoutDocuments: assembled.filter(g => g.document === void 0)
512
+ withDocuments: [ ...docGroups.values() ].map(({group: group, instances: bands}) => ({
513
+ ...group,
514
+ instances: bands
515
+ })),
516
+ withoutDocuments: withoutDocuments
517
+ };
518
+ }
519
+
520
+ function cutTaskGroups(groups, cut) {
521
+ const cutBands = bands => bands.filter(band => cut.keepBand?.(band) ?? !0).map(band => ({
522
+ ...band,
523
+ rows: band.rows.filter(cut.keepRow)
524
+ })).filter(band => band.rows.length > 0);
525
+ return {
526
+ withDocuments: groups.withDocuments.filter(group => cut.keepGroup?.(group) ?? !0).map(group => ({
527
+ ...group,
528
+ instances: cutBands(group.instances)
529
+ })).filter(group => group.instances.length > 0),
530
+ withoutDocuments: cut.dropUngrouped ? [] : cutBands(groups.withoutDocuments)
175
531
  };
176
532
  }
177
533
 
178
534
  function onlyMyTasks(groups) {
179
- const cut = list => list.map(group => ({
180
- ...group,
181
- rows: group.rows.filter(row => row.assignedToMe)
182
- })).filter(group => group.rows.length > 0);
535
+ return cutTaskGroups(groups, {
536
+ keepRow: row => row.assignedToMe
537
+ });
538
+ }
539
+
540
+ function rowActivityEval(entry, row) {
541
+ const evaluation = entry?.evaluation;
542
+ if (!(evaluation === void 0 || evaluation.currentStage.stage.name !== row.stage)) return evaluation.currentStage.activities.find(a => a.activity.name === row.activityName);
543
+ }
544
+
545
+ function taskRowRender(row, entry) {
546
+ const activityEval = rowActivityEval(entry, row);
547
+ if (entry?.evaluation === void 0 || activityEval === void 0) return {
548
+ face: committedRowFace(row),
549
+ assignState: row.assignControl,
550
+ assigneeIds: row.assigneeIds,
551
+ dates: row.dates,
552
+ dateState: row.dateControl,
553
+ terminalActions: [],
554
+ stale: !1
555
+ };
556
+ const activityState = stateByActivity(entry.instance), {face: face, assignState: assignState, assigneeIds: assigneeIds, terminalActions: terminalActions} = activityRowProps({
557
+ activityEval: activityEval,
558
+ activityState: activityState,
559
+ editableFields: entry.evaluation.editableFields
560
+ });
183
561
  return {
184
- withDocuments: cut(groups.withDocuments),
185
- withoutDocuments: cut(groups.withoutDocuments)
562
+ face: face,
563
+ assignState: assignState,
564
+ assigneeIds: assigneeIds,
565
+ dates: datesOf(activityState.get(row.activityName) ?? []),
566
+ dateState: rowDateControlState({
567
+ activity: activityEval.activity,
568
+ status: activityEval.status,
569
+ editableFields: entry.evaluation.editableFields
570
+ }),
571
+ terminalActions: terminalActions,
572
+ stale: isEvaluationStale(entry)
186
573
  };
187
574
  }
188
575
 
189
576
  function taskFilterOptions(groups) {
190
- const all = [ ...groups.withDocuments, ...groups.withoutDocuments ], workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
191
- for (const group of all) workflows.set(group.workflowName, group.workflowTitle),
192
- group.document && documents.set(group.document.id, group.document);
577
+ const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
578
+ for (const group of groups.withDocuments) {
579
+ documents.set(group.document.id, group.document);
580
+ for (const band of group.instances) workflows.set(band.workflowName, band.title);
581
+ }
582
+ for (const band of groups.withoutDocuments) workflows.set(band.workflowName, band.title);
193
583
  return {
194
584
  workflows: [ ...workflows ].map(([name, title]) => ({
195
585
  name: name,
@@ -306,72 +696,18 @@ function rowMatches(args) {
306
696
  }));
307
697
  }
308
698
 
309
- function groupMatches(group, filters) {
310
- const workflows = activeArm(filters.workflows);
311
- if (workflows && !workflows.includes(group.workflowName)) return !1;
312
- const documentIds = activeArm(filters.documentIds);
313
- return !(documentIds && (!group.document || !documentIds.includes(group.document.id)));
314
- }
315
-
316
699
  function applyTaskFilters(args) {
317
- const {groups: groups, filters: filters, now: now} = args, cut = list => list.filter(group => groupMatches(group, filters)).map(group => ({
318
- ...group,
319
- rows: group.rows.filter(row => rowMatches({
700
+ const {groups: groups, filters: filters, now: now} = args, documentIds = activeArm(filters.documentIds), workflows = activeArm(filters.workflows);
701
+ return cutTaskGroups(groups, {
702
+ keepRow: row => rowMatches({
320
703
  row: row,
321
704
  filters: filters,
322
705
  now: now
323
- }))
324
- })).filter(group => group.rows.length > 0);
325
- return {
326
- withDocuments: cut(groups.withDocuments),
327
- withoutDocuments: cut(groups.withoutDocuments)
328
- };
329
- }
330
-
331
- function instanceFilterOptions(rows) {
332
- const workflows = /* @__PURE__ */ new Map, documents = /* @__PURE__ */ new Map;
333
- for (const row of rows) {
334
- workflows.set(row.instance.definition, row.workflowTitle);
335
- for (const doc of row.documents) documents.set(doc.id, doc);
336
- }
337
- return {
338
- workflows: [ ...workflows ].map(([name, title]) => ({
339
- name: name,
340
- title: title
341
- })),
342
- documents: [ ...documents.values() ]
343
- };
344
- }
345
-
346
- function applyInstanceFilters(args) {
347
- const {rows: rows, filters: filters} = args, workflows = activeArm(filters.workflows), documentIds = activeArm(filters.documentIds);
348
- return rows.filter(row => !workflows || workflows.includes(row.instance.definition)).filter(row => !documentIds || row.documents.some(doc => documentIds.includes(doc.id))).map(row => row.instance);
349
- }
350
-
351
- function hasTaskArms(filters) {
352
- return (filters.assigneeIds?.length ?? 0) > 0 || filters.date !== void 0;
353
- }
354
-
355
- function instanceStatusMatches(instance, statuses) {
356
- const arm = activeArm(statuses);
357
- if (!arm) return !0;
358
- const state = terminalState(instance);
359
- return state === "in-flight" ? arm.includes("open") : state === "completed" && arm.includes("completed");
360
- }
361
-
362
- function instanceIdsMatchingTaskArms(args) {
363
- const {instances: instances, identity: identity, filters: filters, now: now} = args;
364
- if (!hasTaskArms(filters)) return null;
365
- const groups = deriveTaskGroups({
366
- instances: instances,
367
- identity: identity
368
- }), {statuses: _workflowLevel, ...taskArms} = filters, cut = applyTaskFilters({
369
- groups: groups,
370
- filters: taskArms,
371
- now: now
372
- }), ids = /* @__PURE__ */ new Set;
373
- for (const group of [ ...cut.withDocuments, ...cut.withoutDocuments ]) for (const row of group.rows) ids.add(row.instanceId);
374
- return ids;
706
+ }),
707
+ keepBand: band => !workflows || workflows.includes(band.workflowName),
708
+ keepGroup: group => !documentIds || documentIds.includes(group.document.id),
709
+ dropUngrouped: documentIds !== void 0
710
+ });
375
711
  }
376
712
 
377
713
  const NOTHING = {
@@ -770,7 +1106,7 @@ function DocumentField({documents: documents, activeIds: activeIds, onToggle: on
770
1106
  if (documents.length === 0) return null;
771
1107
  const rows = documents.map(doc => ({
772
1108
  key: doc.id,
773
- text: `${titles.get(doc.id) ?? ""} ${doc.type} ${extractDocumentId(doc.id)}`,
1109
+ text: `${titles.get(doc.id) ?? ""} ${doc.type} ${toBareId(doc.id)}`,
774
1110
  node: /* @__PURE__ */ jsx(DocRefFace, {
775
1111
  gdr: doc
776
1112
  })
@@ -855,7 +1191,7 @@ function useDocumentChipLabels(documentIds, documents) {
855
1191
  const schema = useSchema(), activeDocs = documents.filter(doc => documentIds.includes(doc.id)), {titles: titles, resolved: resolved} = useDocPreviewTitles(activeDocs), labels = /* @__PURE__ */ new Map;
856
1192
  for (const doc of activeDocs) {
857
1193
  if (!resolved.has(doc.id)) continue;
858
- const label = titles.get(doc.id) ?? schemaTypeTitle(schema, doc.type) ?? extractDocumentId(doc.id);
1194
+ const label = titles.get(doc.id) ?? schemaTypeTitle(schema, doc.type) ?? toBareId(doc.id);
859
1195
  labels.set(doc.id, label);
860
1196
  }
861
1197
  return labels;
@@ -879,653 +1215,163 @@ function ActiveFilterChips({filters: filters, options: options, onChange: onChan
879
1215
  });
880
1216
  }
881
1217
 
882
- const INSTANCE_CAP = 200, NEWEST_INSTANCES = {
883
- includeCompleted: !0,
884
- limit: INSTANCE_CAP
885
- };
886
-
887
- function useToolInstances() {
888
- const {engine: engine} = useWorkflowContext(), {instances: instances, loading: loading, invalid: invalid} = useWorkflowInstances({
889
- engine: engine,
890
- filter: NEWEST_INSTANCES
891
- });
892
- return useMemo(() => {
893
- const rows = instances ?? [];
894
- return {
895
- inFlight: rows.filter(instance => terminalState(instance) === "in-flight"),
896
- settled: rows.filter(instance => terminalState(instance) !== "in-flight"),
897
- truncated: rows.length === INSTANCE_CAP,
898
- loading: loading,
899
- invalid: invalid
900
- };
901
- }, [ instances, loading, invalid ]);
1218
+ function FilterRowPortal({filters: filters, onChange: onChange, options: options, slot: slot}) {
1219
+ return slot ? createPortal(
1220
+ /* @__PURE__ */ jsxs(Flex, {
1221
+ align: "center",
1222
+ gap: 2,
1223
+ justify: "flex-end",
1224
+ children: [
1225
+ /* @__PURE__ */ jsx(ActiveFilterChips, {
1226
+ filters: filters,
1227
+ onChange: onChange,
1228
+ options: options
1229
+ }),
1230
+ /* @__PURE__ */ jsx(TaskFilterMenu, {
1231
+ filters: filters,
1232
+ onChange: onChange,
1233
+ options: options
1234
+ }) ]
1235
+ }), slot) : null;
902
1236
  }
903
1237
 
904
- function toFilterRows(instances) {
905
- return instances.map(instance => ({
906
- instance: instance,
907
- workflowTitle: instanceTitle(instance, definitionSnapshotOf(instance)),
908
- documents: documentRefsOf(instance)
909
- }));
910
- }
911
-
912
- function cutInstances(args) {
913
- const {rows: rows, filters: filters, taskArmIds: taskArmIds} = args, kept = applyInstanceFilters({
914
- rows: rows,
915
- filters: filters
916
- }).filter(instance => instanceStatusMatches(instance, filters.statuses));
917
- return taskArmIds === null ? kept : kept.filter(instance => taskArmIds.has(instance._id));
918
- }
919
-
920
- function WorkflowsDashboard({instances: instances, onOpenInstance: onOpenInstance}) {
921
- const {inFlight: inFlight, settled: settled, truncated: truncated, loading: loading} = instances, [filters, setFilters] = useState({}), identity = useAssignmentIdentity(), inFlightRows = useMemo(() => toFilterRows(inFlight), [ inFlight ]), settledRows = useMemo(() => toFilterRows(settled), [ settled ]), options = useMemo(() => instanceFilterOptions([ ...inFlightRows, ...settledRows ]), [ inFlightRows, settledRows ]), taskArmIds = useMemo(() => instanceIdsMatchingTaskArms({
922
- instances: [ ...inFlight, ...settled ],
923
- identity: identity,
924
- filters: filters,
925
- now: /* @__PURE__ */ new Date
926
- }), [ inFlight, settled, identity, filters ]), visibleInFlight = useMemo(() => cutInstances({
927
- rows: inFlightRows,
928
- filters: filters,
929
- taskArmIds: taskArmIds
930
- }), [ inFlightRows, filters, taskArmIds ]), visibleSettled = useMemo(() => cutInstances({
931
- rows: settledRows,
932
- filters: filters,
933
- taskArmIds: taskArmIds
934
- }), [ settledRows, filters, taskArmIds ]);
935
- return loading ? /* @__PURE__ */ jsx(LoadingRow, {
936
- label: "Loading workflows…"
937
- }) : inFlight.length === 0 && settled.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
938
- text: "No workflows yet — start one with “New workflow”"
939
- }) : /* @__PURE__ */ jsxs(Stack, {
940
- gap: 2,
941
- children: [
942
- /* @__PURE__ */ jsxs(Flex, {
943
- align: "center",
944
- gap: 2,
945
- justify: "flex-end",
946
- children: [
947
- /* @__PURE__ */ jsx(ActiveFilterChips, {
948
- filters: filters,
949
- onChange: setFilters,
950
- options: options
951
- }),
952
- /* @__PURE__ */ jsx(TaskFilterMenu, {
953
- filters: filters,
954
- onChange: setFilters,
955
- options: options
956
- }) ]
957
- }), visibleInFlight.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
958
- text: "No running workflows match"
959
- }) : /* @__PURE__ */ jsx(InstanceRows, {
960
- instances: visibleInFlight,
961
- onOpenInstance: onOpenInstance
962
- }),
963
- /* @__PURE__ */ jsx(SettledRows, {
964
- defaultShown: filters.statuses?.includes("completed") ?? !1,
965
- instances: visibleSettled,
966
- onOpenInstance: onOpenInstance
967
- }), truncated ? /* @__PURE__ */ jsx(MutedNote, {
968
- text: `Showing the latest ${INSTANCE_CAP} workflows.`
969
- }) : null ]
970
- });
971
- }
972
-
973
- function InstanceRows({instances: instances, onOpenInstance: onOpenInstance}) {
974
- /* @__PURE__ */
975
- return jsx(Fragment, {
976
- children: instances.map(instance => /* @__PURE__ */ jsx(InstanceRow, {
977
- instance: instance,
978
- onOpen: () => onOpenInstance(instance)
979
- }, instance._id))
980
- });
981
- }
982
-
983
- function SettledRows({instances: instances, onOpenInstance: onOpenInstance, defaultShown: defaultShown}) {
984
- const [toggled, setToggled] = useState(void 0), shown = toggled ?? defaultShown;
985
- return instances.length === 0 ? null : /* @__PURE__ */ jsxs(Stack, {
986
- gap: 2,
987
- marginTop: 3,
988
- children: [
989
- /* @__PURE__ */ jsx(Flex, {
990
- children: /* @__PURE__ */ jsx(Button, {
991
- fontSize: 1,
992
- icon: shown ? ChevronDownIcon : ChevronRightIcon,
993
- mode: "bleed",
994
- onClick: () => setToggled(!shown),
995
- padding: 2,
996
- text: `${shown ? "Hide" : "Show"} ${instances.length} finished`
997
- })
998
- }), shown ? /* @__PURE__ */ jsx(InstanceRows, {
999
- instances: instances,
1000
- onOpenInstance: onOpenInstance
1001
- }) : null ]
1002
- });
1003
- }
1004
-
1005
- function InstanceRow({instance: instance, onOpen: onOpen}) {
1006
- const definition = useMemo(() => definitionSnapshotOf(instance), [ instance ]);
1007
- /* @__PURE__ */
1008
- return jsxs(Flex, {
1009
- align: "center",
1010
- gap: 2,
1011
- children: [
1012
- /* @__PURE__ */ jsx(Card, {
1013
- as: "button",
1014
- flex: 1,
1015
- onClick: onOpen,
1016
- padding: 3,
1017
- paddingLeft: 4,
1018
- radius: 2,
1019
- style: {
1020
- cursor: "pointer",
1021
- textAlign: "left"
1022
- },
1023
- children: /* @__PURE__ */ jsxs(Flex, {
1024
- align: "center",
1025
- gap: 3,
1026
- children: [
1027
- /* @__PURE__ */ jsx(Text, {
1028
- size: 1,
1029
- weight: "medium",
1030
- children: instanceTitle(instance, definition)
1031
- }),
1032
- /* @__PURE__ */ jsxs(Text, {
1033
- muted: !0,
1034
- size: 1,
1035
- children: [ "Started ", formatDate(instance.startedAt) ]
1036
- }),
1037
- /* @__PURE__ */ jsx(Flex, {
1038
- flex: 1
1039
- }),
1040
- /* @__PURE__ */ jsx(AbortedBadge, {
1041
- instance: instance
1042
- }) ]
1043
- })
1044
- }),
1045
- /* @__PURE__ */ jsx(DocRefChips, {
1046
- instance: instance
1047
- }) ]
1048
- });
1049
- }
1050
-
1051
- function SpinnerSlot({busy: busy}) {
1052
- /* @__PURE__ */
1053
- return jsx(Flex, {
1054
- align: "center",
1055
- flex: "none",
1056
- justify: "center",
1057
- style: {
1058
- width: 19,
1059
- height: 19
1060
- },
1061
- children: busy ? /* @__PURE__ */ jsx(Spinner, {
1062
- muted: !0
1063
- }) : null
1064
- });
1065
- }
1066
-
1067
- function InstanceTaskLists({entry: entry, onOpenActivity: onOpenActivity}) {
1068
- return instanceNotice(entry) || /* @__PURE__ */ jsx(StaleLock, {
1069
- stale: isEvaluationStale(entry),
1070
- children: /* @__PURE__ */ jsxs(Stack, {
1071
- children: [
1072
- /* @__PURE__ */ jsx(ActivitiesList, {
1073
- entry: entry,
1074
- onOpenActivity: onOpenActivity
1075
- }),
1076
- /* @__PURE__ */ jsx(TodoItemsList, {
1077
- entry: entry
1078
- }) ]
1079
- })
1080
- });
1081
- }
1082
-
1083
- function WorkflowInstanceDetail({instanceId: instanceId, onBack: onBack}) {
1084
- const entry = useWorkflowInstanceEntry(instanceId), [graceOver, setGraceOver] = useState(!1);
1085
- return useEffect(() => {
1086
- const timer = setTimeout(() => setGraceOver(!0), 2500);
1087
- return () => clearTimeout(timer);
1088
- }, [ instanceId ]), entry?.invalid ? /* @__PURE__ */ jsx(Box, {
1089
- padding: 4,
1090
- children: /* @__PURE__ */ jsx(InvalidDocNotice, {
1091
- invalid: entry.invalid
1092
- })
1093
- }) : entry ? /* @__PURE__ */ jsx(InstanceDetailPanel, {
1094
- entry: entry,
1095
- onBack: onBack
1096
- }) : /* @__PURE__ */ jsx(Box, {
1097
- padding: 4,
1098
- children: graceOver ? /* @__PURE__ */ jsx(NotFound, {
1099
- id: instanceId,
1100
- onBack: onBack
1101
- }) : /* @__PURE__ */ jsx(LoadingRow, {
1102
- label: "Loading workflow…"
1103
- })
1104
- });
1105
- }
1106
-
1107
- function NotFound({id: id, onBack: onBack}) {
1108
- /* @__PURE__ */
1109
- return jsxs(Stack, {
1110
- gap: 4,
1111
- children: [
1112
- /* @__PURE__ */ jsxs(Text, {
1113
- muted: !0,
1114
- size: 1,
1115
- children: [ "No workflow with id “", id, "” — it may have been deleted." ]
1116
- }),
1117
- /* @__PURE__ */ jsx(Flex, {
1118
- children: /* @__PURE__ */ jsx(Button, {
1119
- fontSize: 1,
1120
- icon: ArrowLeftIcon,
1121
- mode: "ghost",
1122
- onClick: onBack,
1123
- padding: 2,
1124
- text: "Back to workflows"
1125
- })
1126
- }) ]
1127
- });
1128
- }
1129
-
1130
- function InstanceDetailPanel({entry: entry, onBack: onBack}) {
1131
- const definition = useDefinition(entry), [openActivity, setOpenActivity] = useState(null), {instance: instance} = entry;
1132
- return useEffect(() => {
1133
- openActivity && openActivityGone({
1134
- entry: entry,
1135
- target: openActivity
1136
- }) && setOpenActivity(null);
1137
- }, [ entry, openActivity ]), /* @__PURE__ */ jsxs(Flex, {
1138
- direction: "column",
1139
- height: "fill",
1140
- overflow: "hidden",
1141
- children: [
1142
- /* @__PURE__ */ jsx(DetailHeader, {
1143
- definition: definition,
1144
- entry: entry,
1145
- onBack: onBack
1146
- }),
1147
- /* @__PURE__ */ jsx(Box, {
1148
- flex: 1,
1149
- overflow: "auto",
1150
- paddingX: 4,
1151
- children: /* @__PURE__ */ jsxs(Stack, {
1152
- gap: 4,
1153
- paddingBottom: 5,
1154
- children: [
1155
- /* @__PURE__ */ jsx(StudioDocumentsSection, {
1156
- instance: instance
1157
- }),
1158
- /* @__PURE__ */ jsx(StageDiagramSection, {
1159
- definition: definition,
1160
- entry: entry
1161
- }),
1162
- /* @__PURE__ */ jsx(GroupHeading, {
1163
- title: "All tasks"
1164
- }),
1165
- /* @__PURE__ */ jsx(InstanceTaskLists, {
1166
- entry: entry,
1167
- onOpenActivity: name => setOpenActivity({
1168
- stage: instance.currentStage,
1169
- activityName: name
1170
- })
1171
- }) ]
1172
- })
1173
- }), openActivity ? /* @__PURE__ */ jsx(ActivityDetailDialog, {
1174
- activityName: openActivity.activityName,
1175
- breadcrumb: instanceBreadcrumb(instance, definition),
1176
- definition: definition,
1177
- entry: entry,
1178
- onClose: () => setOpenActivity(null)
1179
- }) : null ]
1180
- });
1181
- }
1182
-
1183
- function DetailHeader({entry: entry, definition: definition, onBack: onBack}) {
1184
- const {instance: instance, ready: ready} = entry;
1185
- /* @__PURE__ */
1186
- return jsx(Card, {
1187
- flex: "none",
1188
- paddingX: 4,
1189
- paddingY: 3,
1190
- children: /* @__PURE__ */ jsxs(Flex, {
1191
- align: "center",
1192
- gap: 3,
1193
- wrap: "wrap",
1194
- children: [
1195
- /* @__PURE__ */ jsx(HoverHint, {
1196
- text: "Back to workflows",
1197
- children: /* @__PURE__ */ jsx(Button, {
1198
- "aria-label": "Back to workflows",
1199
- fontSize: 1,
1200
- icon: ArrowLeftIcon,
1201
- mode: "bleed",
1202
- onClick: onBack,
1203
- padding: 2
1204
- })
1205
- }),
1206
- /* @__PURE__ */ jsxs(Stack, {
1207
- gap: 2,
1208
- children: [
1209
- /* @__PURE__ */ jsx(Text, {
1210
- size: 1,
1211
- weight: "semibold",
1212
- children: instanceTitle(instance, definition)
1213
- }),
1214
- /* @__PURE__ */ jsxs(Text, {
1215
- muted: !0,
1216
- size: 1,
1217
- children: [ "Started ", formatDate(instance.startedAt) ]
1218
- }) ]
1219
- }), terminalState(instance) === "in-flight" ? /* @__PURE__ */ jsx(StageChip, {
1220
- title: stageTitle(definition, instance.currentStage)
1221
- }) : /* @__PURE__ */ jsx(AbortedBadge, {
1222
- instance: instance
1223
- }),
1224
- /* @__PURE__ */ jsx(SpinnerSlot, {
1225
- busy: !ready || isEvaluationStale(entry)
1226
- }) ]
1227
- })
1228
- });
1229
- }
1230
-
1231
- function StudioDocumentsSection({instance: instance}) {
1232
- const refs = documentRefsOf(instance);
1233
- return refs.length === 0 ? null : /* @__PURE__ */ jsxs(Stack, {
1234
- gap: 3,
1235
- marginTop: 3,
1236
- children: [
1237
- /* @__PURE__ */ jsx(Text, {
1238
- muted: !0,
1239
- size: 1,
1240
- weight: "medium",
1241
- children: refs.length === 1 ? "Studio document" : "Studio documents"
1242
- }),
1243
- /* @__PURE__ */ jsx(Stack, {
1244
- gap: 2,
1245
- children: refs.map(ref => /* @__PURE__ */ jsx(DocPreviewLink, {
1246
- gdr: ref
1247
- }, ref.id))
1248
- }) ]
1249
- });
1250
- }
1251
-
1252
- function StageDiagramSection({entry: entry, definition: definition}) {
1253
- const {instance: instance} = entry;
1254
- return definition ?
1255
- /* @__PURE__ */ jsx(WorkflowDiagram, {
1256
- currentStage: terminalState(instance) === "in-flight" ? instance.currentStage : void 0,
1257
- definition: definition,
1258
- evaluation: entry.evaluation,
1259
- explain: !0,
1260
- guardCount: entry.guards?.length,
1261
- height: 340,
1262
- history: instance.history
1263
- }, instance._id) : /* @__PURE__ */ jsx(Card, {
1264
- border: !0,
1265
- marginTop: 3,
1266
- padding: 3,
1267
- radius: 2,
1268
- tone: "caution",
1269
- children: /* @__PURE__ */ jsx(Text, {
1270
- muted: !0,
1271
- size: 1,
1272
- children: "Couldn’t load this workflow’s full details — the stage diagram is unavailable, and technical names show instead of titles."
1273
- })
1238
+ function ActivityDateControl({instanceId: instanceId, state: state, dates: dates}) {
1239
+ const {editFieldFor: editFieldFor} = useWorkflowContext(), {save: save} = useSaveField(), [open, setOpen] = useState(!1), raw = dateControlValue({
1240
+ state: state,
1241
+ dates: dates
1242
+ }), display = raw === void 0 ? null : /* @__PURE__ */ jsx(Text, {
1243
+ muted: !0,
1244
+ size: 1,
1245
+ children: formatDate(raw)
1274
1246
  });
1275
- }
1276
-
1277
- function useStartableDefinitions() {
1278
- const {engine: engine} = useWorkflowContext(), [definitions, setDefinitions] = useState(void 0), [error, setError] = useState(void 0);
1279
- return useEffect(() => {
1280
- let cancelled = !1;
1281
- return (async () => {
1282
- const rows = await readDeployedDefinitions(engine), startable = latestDeployedDefinitions(rows).filter(row => isStartableDefinition(row)).map(row => ({
1283
- name: row.name,
1284
- title: row.title ?? row.name,
1285
- description: row.description
1286
- }));
1287
- cancelled || (setDefinitions(startable), setError(void 0));
1288
- })().catch(err => {
1289
- cancelled || setError(describeError(err));
1290
- }), () => {
1291
- cancelled = !0;
1292
- };
1293
- }, [ engine ]), {
1294
- definitions: definitions,
1295
- error: error
1296
- };
1297
- }
1298
-
1299
- function NewWorkflowButton() {
1300
- const {openStartDialog: openStartDialog} = useWorkflowContext(), [open, setOpen] = useState(!1);
1301
- /* @__PURE__ */
1302
- return jsx(DismissablePopover, {
1303
- content: open ? /* @__PURE__ */ jsx(DefinitionPickerPanel, {
1304
- onPick: definition => {
1305
- setOpen(!1), openStartDialog({
1306
- definition: definition.name,
1307
- label: definition.title
1308
- });
1309
- }
1310
- }) : null,
1311
- onDismiss: () => setOpen(!1),
1312
- open: open,
1247
+ if (state.kind === "none") return display;
1248
+ if (state.kind === "closed") return display === null ? null : /* @__PURE__ */ jsx(HoverHint, {
1249
+ text: state.reason,
1313
1250
  children: /* @__PURE__ */ jsx(Button, {
1251
+ "aria-label": "Date",
1252
+ as: "span",
1314
1253
  fontSize: 1,
1315
- icon: AddIcon,
1316
- onClick: () => setOpen(v => !v),
1254
+ mode: "bleed",
1255
+ onClick: stopRowClick,
1256
+ onMouseDown: stopRowMouseDown,
1317
1257
  padding: 2,
1318
- selected: open,
1319
- text: "New workflow",
1320
- tone: "primary"
1321
- })
1322
- });
1323
- }
1324
-
1325
- function DefinitionPickerPanel({onPick: onPick}) {
1326
- const {definitions: definitions, error: error} = useStartableDefinitions(), [query, setQuery] = useState(""), visible = useMemo(() => {
1327
- const q = query.trim().toLowerCase();
1328
- return definitions ? q ? definitions.filter(d => `${d.title} ${d.name}`.toLowerCase().includes(q)) : definitions : [];
1329
- }, [ definitions, query ]);
1330
- /* @__PURE__ */
1331
- return jsx(Box, {
1332
- padding: 2,
1333
- style: {
1334
- width: 340
1335
- },
1336
- children: /* @__PURE__ */ jsxs(Stack, {
1337
- gap: 2,
1338
- children: [
1339
- /* @__PURE__ */ jsx(TextInput, {
1340
- autoFocus: !0,
1341
- fontSize: 1,
1342
- icon: SearchIcon,
1343
- onChange: event => setQuery(event.currentTarget.value),
1344
- placeholder: "Filter workflows…",
1345
- value: query
1346
- }), error ? /* @__PURE__ */ jsx(MutedNote, {
1347
- text: `Could not load the deployed workflows: ${error}`
1348
- }) : null, !definitions && !error ? /* @__PURE__ */ jsx(LoadingRow, {
1349
- label: "Loading workflows…"
1350
- }) : null, definitions && definitions.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
1351
- text: "No startable workflows are deployed"
1352
- }) : null, definitions && definitions.length > 0 && visible.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
1353
- text: "No workflows match"
1354
- }) : null,
1355
- /* @__PURE__ */ jsx(Stack, {
1356
- gap: 1,
1357
- style: {
1358
- maxHeight: 320,
1359
- overflowY: "auto"
1360
- },
1361
- children: visible.map(definition => /* @__PURE__ */ jsx(Card, {
1362
- as: "button",
1363
- onClick: () => onPick(definition),
1364
- padding: 3,
1365
- radius: 2,
1366
- style: {
1367
- cursor: "pointer",
1368
- textAlign: "left"
1369
- },
1370
- children: /* @__PURE__ */ jsxs(Stack, {
1371
- gap: 2,
1372
- children: [
1373
- /* @__PURE__ */ jsx(Text, {
1374
- size: 1,
1375
- weight: "medium",
1376
- children: definition.title
1377
- }), definition.description ? /* @__PURE__ */ jsx(Text, {
1378
- muted: !0,
1379
- size: 1,
1380
- textOverflow: "ellipsis",
1381
- children: definition.description
1382
- }) : null ]
1383
- })
1384
- }, definition.name))
1385
- }) ]
1386
- })
1387
- });
1388
- }
1389
-
1390
- function ToolActivityDialog({target: target, onClose: onClose}) {
1391
- const entry = useWorkflowInstanceEntry(target.instanceId), definition = useDefinition(entry), resolvedOnce = useRef(!1);
1392
- return entry && (resolvedOnce.current = !0), useEffect(() => {
1393
- resolvedOnce.current && openActivityGone({
1394
- entry: entry,
1395
- target: target
1396
- }) && onClose();
1397
- }, [ entry, target, onClose ]), entry?.invalid ? /* @__PURE__ */ jsx(Dialog, {
1398
- header: "Workflow unavailable",
1399
- id: "workflow-activity-detail",
1400
- onClose: onClose,
1401
- width: 1,
1402
- children: /* @__PURE__ */ jsx(Box, {
1403
- padding: 4,
1404
- children: /* @__PURE__ */ jsx(InvalidDocNotice, {
1405
- invalid: entry.invalid
1406
- })
1407
- })
1408
- }) : entry?.evaluation ? /* @__PURE__ */ jsx(ActivityDetailDialog, {
1409
- activityName: target.activityName,
1410
- breadcrumb: instanceBreadcrumb(entry.instance, definition),
1411
- definition: definition,
1412
- document: documentRefsOf(entry.instance)[0],
1413
- entry: entry,
1414
- onClose: onClose
1415
- }) : /* @__PURE__ */ jsx(Dialog, {
1416
- header: "Loading activity…",
1417
- id: "workflow-activity-detail",
1418
- onClose: onClose,
1419
- width: 1,
1420
- children: /* @__PURE__ */ jsx(Box, {
1421
- padding: 4,
1422
- children: /* @__PURE__ */ jsx(LoadingRow, {
1423
- label: "Fetching the live workflow state…"
1258
+ children: display
1259
+ })
1260
+ });
1261
+ const kind = dateControlKind(state), commit = (mode, value) => {
1262
+ state.kind === "editable" && save(() => editFieldFor(instanceId, {
1263
+ target: editTargetOf(state.field),
1264
+ ...mode === "set" ? {
1265
+ mode: mode,
1266
+ value: value
1267
+ } : {
1268
+ mode: mode
1269
+ }
1270
+ }).then(() => {}));
1271
+ }, handleClick = e => {
1272
+ e.stopPropagation(), setOpen(v => !v);
1273
+ };
1274
+ /* @__PURE__ */
1275
+ return jsx(DismissablePopover, {
1276
+ content: /* @__PURE__ */ jsx(Box, {
1277
+ onClick: stopRowClick,
1278
+ children: state.kind === "loading" || kind === void 0 ? /* @__PURE__ */ jsx(LoadingRow, {
1279
+ label: "Loading…",
1280
+ padding: 3
1281
+ }) : /* @__PURE__ */ jsx(ClearableDatePicker, {
1282
+ clearLabel: "Clear date",
1283
+ onClear: () => {
1284
+ setOpen(!1), commit("unset");
1285
+ },
1286
+ onPick: next => {
1287
+ kind === "date" && setOpen(!1), commit("set", serializeDateFieldValue(next, kind));
1288
+ },
1289
+ selectTime: kind === "datetime",
1290
+ value: parseDateFieldValue(state.field.value, kind)
1291
+ })
1292
+ }),
1293
+ onDismiss: () => setOpen(!1),
1294
+ open: open,
1295
+ children: display ? /* @__PURE__ */ jsx(Button, {
1296
+ "aria-label": "Change date",
1297
+ as: "span",
1298
+ fontSize: 1,
1299
+ mode: "bleed",
1300
+ onClick: handleClick,
1301
+ onMouseDown: stopRowMouseDown,
1302
+ padding: 2,
1303
+ children: display
1304
+ }) : /* @__PURE__ */ jsx(HoverHint, {
1305
+ disabled: open,
1306
+ text: "Set date",
1307
+ children: /* @__PURE__ */ jsx(Button, {
1308
+ "aria-label": "Set date",
1309
+ as: "span",
1310
+ fontSize: 1,
1311
+ icon: CalendarIcon,
1312
+ mode: "bleed",
1313
+ onClick: handleClick,
1314
+ onMouseDown: stopRowMouseDown,
1315
+ padding: 2
1424
1316
  })
1425
1317
  })
1426
1318
  });
1427
1319
  }
1428
1320
 
1429
- function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow}) {
1430
- return groups.withDocuments.length === 0 && groups.withoutDocuments.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
1431
- text: "No tasks match — adjust the filters, or start a workflow"
1432
- }) : /* @__PURE__ */ jsxs(Stack, {
1433
- gap: 2,
1434
- children: [ groups.withDocuments.map(group => /* @__PURE__ */ jsx(DocumentTaskGroup, {
1435
- group: group,
1436
- onOpenTask: onOpenTask,
1437
- onOpenWorkflow: onOpenWorkflow
1438
- }, group.key)), groups.withoutDocuments.length > 0 ? /* @__PURE__ */ jsx(CollapsibleGroup, {
1439
- header: /* @__PURE__ */ jsx(GroupTitle, {
1440
- text: "Workflows without Studio documents"
1441
- }),
1442
- children: /* @__PURE__ */ jsx(Stack, {
1443
- gap: 2,
1444
- paddingLeft: 2,
1445
- children: groups.withoutDocuments.map(group => /* @__PURE__ */ jsx(CollapsibleGroup, {
1446
- header: /* @__PURE__ */ jsxs(Fragment, {
1447
- children: [
1448
- /* @__PURE__ */ jsx(Text, {
1449
- muted: !0,
1450
- size: 1,
1451
- children: /* @__PURE__ */ jsx(StageGlyph, {})
1452
- }),
1453
- /* @__PURE__ */ jsx(WorkflowBadgeTitle, {
1454
- group: group
1455
- }),
1456
- /* @__PURE__ */ jsx(Box, {
1457
- flex: 1
1458
- }),
1459
- /* @__PURE__ */ jsx(GroupMenu, {
1460
- group: group,
1461
- onOpenWorkflow: onOpenWorkflow
1462
- }) ]
1463
- }),
1464
- children: /* @__PURE__ */ jsx(TaskRows, {
1465
- onOpenTask: onOpenTask,
1466
- rows: group.rows
1467
- })
1468
- }, group.key))
1469
- })
1470
- }) : null ]
1471
- });
1321
+ const PREWARM_MARGIN = "600px 0px";
1322
+
1323
+ function useNearViewport() {
1324
+ const [element, setElement] = useState(null), [near, setNear] = useState(!1);
1325
+ return useEffect(() => {
1326
+ if (element === null) return;
1327
+ if (typeof IntersectionObserver > "u") {
1328
+ setNear(!0);
1329
+ return;
1330
+ }
1331
+ const observer = new IntersectionObserver(entries => {
1332
+ setNear(entries.at(-1)?.isIntersecting ?? !1);
1333
+ }, {
1334
+ rootMargin: PREWARM_MARGIN
1335
+ });
1336
+ return observer.observe(element), () => observer.disconnect();
1337
+ }, [ element ]), {
1338
+ ref: setElement,
1339
+ near: near
1340
+ };
1472
1341
  }
1473
1342
 
1474
- function GroupTitle({text: text}) {
1343
+ function RefChips({refs: refs, max: max = 2}) {
1344
+ if (refs.length === 0) return null;
1345
+ const shown = refs.slice(0, max), overflow = refs.length - shown.length;
1475
1346
  /* @__PURE__ */
1476
- return jsx(Text, {
1477
- size: 1,
1478
- weight: "semibold",
1479
- children: text
1347
+ return jsxs(Flex, {
1348
+ align: "center",
1349
+ gap: 3,
1350
+ wrap: "wrap",
1351
+ children: [ shown.map(ref => /* @__PURE__ */ jsx(DocPreviewLink, {
1352
+ gdr: ref,
1353
+ layout: "inline"
1354
+ }, ref.id)), overflow > 0 ? /* @__PURE__ */ jsxs(Text, {
1355
+ muted: !0,
1356
+ size: 1,
1357
+ children: [ "+", overflow, " more" ]
1358
+ }) : null ]
1480
1359
  });
1481
1360
  }
1482
1361
 
1483
- function WorkflowBadgeTitle({group: group}) {
1484
- const badge = /* @__PURE__ */ jsx(Badge, {
1485
- fontSize: 1,
1486
- mode: "outline",
1487
- children: group.workflowTitle
1488
- });
1489
- return group.workflowDescription ? /* @__PURE__ */ jsx(HoverHint, {
1490
- text: group.workflowDescription,
1491
- children: badge
1492
- }) : badge;
1493
- }
1362
+ const EMPTY_NOTE = "No tasks match — adjust the filters, or start a workflow", UNGROUPED_TITLE = "Workflows without Studio documents";
1494
1363
 
1495
- function CollapsibleGroup({header: header, children: children}) {
1496
- const [open, setOpen] = useState(!0);
1497
- /* @__PURE__ */
1498
- return jsxs(Stack, {
1364
+ function TaskGroupList({groups: groups, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1365
+ return groups.withDocuments.length === 0 && groups.withoutDocuments.length === 0 ? /* @__PURE__ */ jsx(MutedNote, {
1366
+ text: EMPTY_NOTE
1367
+ }) : /* @__PURE__ */ jsxs(Stack, {
1499
1368
  gap: 2,
1500
- children: [
1501
- /* @__PURE__ */ jsx(Card, {
1502
- paddingX: 2,
1503
- paddingY: 2,
1504
- radius: 2,
1505
- tone: "transparent",
1506
- children: /* @__PURE__ */ jsxs(Flex, {
1369
+ children: [ groups.withDocuments.map(group => /* @__PURE__ */ jsx(CollapsibleBand, {
1370
+ background: !0,
1371
+ sticky: !0,
1372
+ title: toBareId(group.document.id),
1373
+ header: /* @__PURE__ */ jsx(Flex, {
1507
1374
  align: "center",
1508
- gap: 2,
1509
- children: [
1510
- /* @__PURE__ */ jsx(Button, {
1511
- "aria-label": open ? "Collapse" : "Expand",
1512
- fontSize: 1,
1513
- icon: open ? ChevronDownIcon : ChevronRightIcon,
1514
- mode: "bleed",
1515
- onClick: () => setOpen(v => !v),
1516
- padding: 1
1517
- }), header ]
1518
- })
1519
- }), open ? children : null ]
1520
- });
1521
- }
1522
-
1523
- function DocumentTaskGroup({group: group, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow}) {
1524
- const document = group.document;
1525
- return document ? /* @__PURE__ */ jsx(CollapsibleGroup, {
1526
- header: /* @__PURE__ */ jsxs(Fragment, {
1527
- children: [
1528
- /* @__PURE__ */ jsx(Box, {
1529
1375
  flex: "none",
1530
1376
  style: {
1531
1377
  maxWidth: "60%"
@@ -1534,323 +1380,338 @@ function DocumentTaskGroup({group: group, onOpenTask: onOpenTask, onOpenWorkflow
1534
1380
  max: 2,
1535
1381
  refs: group.documents
1536
1382
  })
1537
- }),
1538
- /* @__PURE__ */ jsx(WorkflowBadgeTitle, {
1539
- group: group
1540
- }),
1541
- /* @__PURE__ */ jsx(Box, {
1542
- flex: 1
1543
- }),
1544
- /* @__PURE__ */ jsx(GroupMenu, {
1545
- document: document,
1546
- group: group,
1547
- onOpenWorkflow: onOpenWorkflow
1548
- }) ]
1549
- }),
1550
- children: /* @__PURE__ */ jsx(TaskRows, {
1383
+ }),
1384
+ children: /* @__PURE__ */ jsx(InstanceBands, {
1385
+ instances: group.instances,
1386
+ onOpenTask: onOpenTask,
1387
+ onOpenWorkflow: onOpenWorkflow,
1388
+ showTerminalActions: showTerminalActions
1389
+ })
1390
+ }, group.key)), groups.withoutDocuments.length > 0 ? /* @__PURE__ */ jsx(CollapsibleBand, {
1391
+ background: !0,
1392
+ sticky: !0,
1393
+ title: UNGROUPED_TITLE,
1394
+ header: /* @__PURE__ */ jsx(Text, {
1395
+ size: 1,
1396
+ weight: "semibold",
1397
+ children: UNGROUPED_TITLE
1398
+ }),
1399
+ children: /* @__PURE__ */ jsx(InstanceBands, {
1400
+ instances: groups.withoutDocuments,
1401
+ onOpenTask: onOpenTask,
1402
+ onOpenWorkflow: onOpenWorkflow,
1403
+ showTerminalActions: showTerminalActions
1404
+ })
1405
+ }) : null ]
1406
+ });
1407
+ }
1408
+
1409
+ function targetOf(row) {
1410
+ return {
1411
+ instanceId: row.instanceId,
1412
+ stage: row.stage,
1413
+ activityName: row.activityName
1414
+ };
1415
+ }
1416
+
1417
+ function InstanceBands({instances: instances, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1418
+ /* @__PURE__ */
1419
+ return jsx(Stack, {
1420
+ gap: 1,
1421
+ children: instances.map(band => /* @__PURE__ */ jsx(InstanceBand, {
1422
+ band: band,
1551
1423
  onOpenTask: onOpenTask,
1552
- rows: group.rows
1553
- })
1554
- }) : null;
1424
+ onOpenWorkflow: onOpenWorkflow,
1425
+ showTerminalActions: showTerminalActions
1426
+ }, band.instanceId))
1427
+ });
1555
1428
  }
1556
1429
 
1557
- function GroupMenu({group: group, document: document, onOpenWorkflow: onOpenWorkflow}) {
1558
- const router = useRouter(), state = useDocLink(document);
1430
+ function InstanceBand({band: band, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, showTerminalActions: showTerminalActions}) {
1431
+ const {resolvePathFromState: resolvePathFromState} = useRouter();
1559
1432
  /* @__PURE__ */
1560
- return jsx(HintedMenuButton, {
1561
- button: /* @__PURE__ */ jsx(Button, {
1562
- "aria-label": "Group actions",
1563
- fontSize: 1,
1564
- icon: EllipsisHorizontalIcon,
1565
- mode: "bleed",
1566
- padding: 2
1567
- }),
1568
- hint: "Group actions",
1569
- id: `task-group-menu-${group.key}`,
1570
- menu: /* @__PURE__ */ jsxs(Menu, {
1433
+ return jsx(CollapsibleBand, {
1434
+ title: band.title,
1435
+ header: /* @__PURE__ */ jsxs(Fragment, {
1571
1436
  children: [
1572
- /* @__PURE__ */ jsx(MenuItem, {
1573
- onClick: () => onOpenWorkflow(group.instanceId),
1574
- text: "View workflow"
1575
- }), state.kind === "linked" ? /* @__PURE__ */ jsx(MenuItem, {
1576
- onClick: () => router.navigateIntent("edit", {
1577
- id: state.bareId
1437
+ /* @__PURE__ */ jsx(LinkChip, {
1438
+ hint: "View workflow",
1439
+ href: resolvePathFromState({
1440
+ instanceId: band.instanceId
1578
1441
  }),
1579
- text: "Open document"
1580
- }) : null ]
1442
+ onClick: event => {
1443
+ event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || (event.preventDefault(),
1444
+ onOpenWorkflow(band.instanceId));
1445
+ },
1446
+ children: /* @__PURE__ */ jsxs(Flex, {
1447
+ align: "center",
1448
+ gap: 2,
1449
+ style: {
1450
+ minWidth: 0
1451
+ },
1452
+ children: [
1453
+ /* @__PURE__ */ jsx(Text, {
1454
+ size: 1,
1455
+ style: {
1456
+ minWidth: 0
1457
+ },
1458
+ textOverflow: "ellipsis",
1459
+ weight: "medium",
1460
+ children: band.title
1461
+ }), band.breadcrumb === void 0 ? null : /* @__PURE__ */ jsx(BreadcrumbTail, {
1462
+ segments: [ band.breadcrumb ]
1463
+ }),
1464
+ /* @__PURE__ */ jsx(StageFace, {
1465
+ title: band.stageTitle
1466
+ }) ]
1467
+ })
1468
+ }),
1469
+ /* @__PURE__ */ jsx(TrailingHairline, {}) ]
1581
1470
  }),
1582
- popover: {
1583
- portal: !0
1584
- }
1471
+ children: /* @__PURE__ */ jsx(Box, {
1472
+ paddingLeft: 5,
1473
+ children: /* @__PURE__ */ jsx(TaskRows, {
1474
+ onOpenTask: onOpenTask,
1475
+ rows: band.rows,
1476
+ showTerminalActions: showTerminalActions
1477
+ })
1478
+ })
1585
1479
  });
1586
1480
  }
1587
1481
 
1588
- function TaskRows({rows: rows, onOpenTask: onOpenTask}) {
1482
+ function TaskRows({rows: rows, onOpenTask: onOpenTask, showTerminalActions: showTerminalActions}) {
1589
1483
  /* @__PURE__ */
1590
1484
  return jsx(Stack, {
1591
- children: rows.map(row => /* @__PURE__ */ jsx(TaskRowButton, {
1592
- onOpen: () => onOpenTask({
1593
- instanceId: row.instanceId,
1594
- stage: row.stage,
1595
- activityName: row.activityName
1596
- }),
1597
- row: row
1485
+ gap: 1,
1486
+ children: rows.map(row => /* @__PURE__ */ jsx(TaskActivityRow, {
1487
+ onOpen: () => onOpenTask(targetOf(row)),
1488
+ row: row,
1489
+ showTerminalActions: showTerminalActions
1598
1490
  }, `${row.instanceId}:${row.activityName}`))
1599
1491
  });
1600
1492
  }
1601
1493
 
1602
- function RowDate({dates: dates}) {
1603
- const first = dates.find(value => parseStoredDateValue(value) !== void 0);
1604
- return first === void 0 ? null : /* @__PURE__ */ jsx(Text, {
1605
- muted: !0,
1606
- size: 1,
1607
- children: formatDate(first)
1608
- });
1609
- }
1610
-
1611
- function RowTitle({row: row}) {
1612
- const title = /* @__PURE__ */ jsx(Text, {
1613
- size: 1,
1614
- style: row.open ? void 0 : {
1615
- opacity: .66
1616
- },
1617
- weight: "medium",
1618
- children: row.title
1494
+ function TaskActivityRow({row: row, onOpen: onOpen, showTerminalActions: showTerminalActions}) {
1495
+ const {ref: ref, near: near} = useNearViewport(), entry = useWorkflowInstanceEntry(row.instanceId, {
1496
+ evaluate: near
1497
+ }), render = taskRowRender(row, entry);
1498
+ /* @__PURE__ */
1499
+ return jsx(Box, {
1500
+ ref: ref,
1501
+ children: /* @__PURE__ */ jsx(StaleLock, {
1502
+ stale: render.stale,
1503
+ children: /* @__PURE__ */ jsx(ActivityRow, {
1504
+ assignState: render.assignState,
1505
+ assigneeIds: render.assigneeIds,
1506
+ dateControl: /* @__PURE__ */ jsx(ActivityDateControl, {
1507
+ dates: render.dates,
1508
+ instanceId: row.instanceId,
1509
+ state: render.dateState
1510
+ }),
1511
+ face: render.face,
1512
+ instanceId: row.instanceId,
1513
+ onOpen: onOpen,
1514
+ terminalActions: showTerminalActions ? render.terminalActions : []
1515
+ })
1516
+ })
1619
1517
  });
1620
- return row.description ? /* @__PURE__ */ jsx(HoverHint, {
1621
- text: row.description,
1622
- children: title
1623
- }) : title;
1624
1518
  }
1625
1519
 
1626
- function RowBreadcrumb({breadcrumb: breadcrumb}) {
1627
- return breadcrumb ? /* @__PURE__ */ jsxs(Flex, {
1628
- align: "center",
1629
- gap: 2,
1630
- children: [
1631
- /* @__PURE__ */ jsx(Text, {
1632
- muted: !0,
1633
- size: 1,
1634
- children: breadcrumb
1635
- }),
1636
- /* @__PURE__ */ jsx(Text, {
1637
- muted: !0,
1638
- size: 1,
1639
- children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
1640
- }) ]
1641
- }) : null;
1642
- }
1520
+ const INSTANCE_CAP = 200, NEWEST_INSTANCES = {
1521
+ includeCompleted: !0,
1522
+ limit: INSTANCE_CAP
1523
+ };
1643
1524
 
1644
- function TaskRowButton({row: row, onOpen: onOpen}) {
1645
- /* @__PURE__ */
1646
- return jsx(Card, {
1647
- as: "button",
1648
- onClick: onOpen,
1649
- padding: 3,
1650
- paddingLeft: 4,
1651
- radius: 2,
1652
- style: {
1653
- cursor: "pointer",
1654
- textAlign: "left"
1655
- },
1656
- children: /* @__PURE__ */ jsxs(Flex, {
1657
- align: "center",
1658
- gap: 3,
1659
- children: [
1660
- /* @__PURE__ */ jsx(ActivityStatusIcon, {
1661
- status: row.status
1662
- }),
1663
- /* @__PURE__ */ jsx(RowBreadcrumb, {
1664
- breadcrumb: row.breadcrumb
1665
- }),
1666
- /* @__PURE__ */ jsx(RowTitle, {
1667
- row: row
1668
- }),
1669
- /* @__PURE__ */ jsx(Badge, {
1670
- fontSize: 1,
1671
- mode: "outline",
1672
- tone: "default",
1673
- children: row.stageTitle
1674
- }),
1675
- /* @__PURE__ */ jsx(Flex, {
1676
- flex: 1
1677
- }),
1678
- /* @__PURE__ */ jsx(RowDate, {
1679
- dates: row.dates
1680
- }), row.assigneeIds.length > 0 ? /* @__PURE__ */ jsx(UserAvatarGroup, {
1681
- ids: row.assigneeIds
1682
- }) : null ]
1683
- })
1525
+ function useToolInstances() {
1526
+ const {engine: engine} = useWorkflowContext(), {instances: instances, loading: loading, unreadable: unreadable} = useWorkflowInstances({
1527
+ engine: engine,
1528
+ filter: NEWEST_INSTANCES
1684
1529
  });
1530
+ return useMemo(() => {
1531
+ const rows = instances ?? [];
1532
+ return {
1533
+ inFlight: rows.filter(instance => terminalState(instance) === "in-flight"),
1534
+ settled: rows.filter(instance => terminalState(instance) !== "in-flight"),
1535
+ truncated: rows.length + unreadable.length === INSTANCE_CAP,
1536
+ loading: loading,
1537
+ unreadable: unreadable
1538
+ };
1539
+ }, [ instances, loading, unreadable ]);
1685
1540
  }
1686
1541
 
1687
- function TasksTab({instances: instances, loading: loading, identity: identity, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, truncated: truncated}) {
1688
- const [segment, setSegment] = useState("mine"), [filters, setFilters] = useState({}), groups = useMemo(() => deriveTaskGroups({
1542
+ function TasksTab({filterSlot: filterSlot, instances: instances, loading: loading, identity: identity, mineOnly: mineOnly, onOpenTask: onOpenTask, onOpenWorkflow: onOpenWorkflow, truncated: truncated, unreadable: unreadable}) {
1543
+ const [filters, setFilters] = useState({}), groups = useMemo(() => deriveTaskGroups({
1689
1544
  instances: instances,
1690
1545
  identity: identity
1691
1546
  }), [ instances, identity ]), options = useMemo(() => taskFilterOptions(groups), [ groups ]), visible = useMemo(() => {
1692
- const segmented = segment === "mine" ? onlyMyTasks(groups) : groups;
1693
- return applyTaskFilters({
1694
- groups: segmented,
1547
+ const filtered = applyTaskFilters({
1548
+ groups: groups,
1695
1549
  filters: filters,
1696
1550
  now: /* @__PURE__ */ new Date
1697
1551
  });
1698
- }, [ groups, segment, filters ]);
1699
- return loading ? /* @__PURE__ */ jsx(LoadingRow, {
1700
- label: "Loading tasks…"
1552
+ return mineOnly ? onlyMyTasks(filtered) : filtered;
1553
+ }, [ groups, filters, mineOnly ]);
1554
+ return loading ?
1555
+ /* @__PURE__ */ jsx(Box, {
1556
+ paddingLeft: 2,
1557
+ children: /* @__PURE__ */ jsx(LoadingRow, {
1558
+ label: "Loading tasks…"
1559
+ })
1701
1560
  }) : /* @__PURE__ */ jsxs(Box, {
1702
1561
  children: [
1703
- /* @__PURE__ */ jsxs(Flex, {
1704
- align: "center",
1562
+ /* @__PURE__ */ jsx(FilterRowPortal, {
1563
+ filters: filters,
1564
+ onChange: setFilters,
1565
+ options: options,
1566
+ slot: filterSlot
1567
+ }),
1568
+ /* @__PURE__ */ jsxs(Stack, {
1705
1569
  gap: 2,
1706
- paddingBottom: 3,
1707
1570
  children: [
1708
- /* @__PURE__ */ jsxs(TabList, {
1709
- gap: 1,
1710
- children: [
1711
- /* @__PURE__ */ jsx(Tab, {
1712
- "aria-controls": "workflow-tasks-panel",
1713
- fontSize: 1,
1714
- id: "workflow-tasks-mine",
1715
- label: "My tasks",
1716
- onClick: () => setSegment("mine"),
1717
- padding: 2,
1718
- selected: segment === "mine"
1719
- }),
1720
- /* @__PURE__ */ jsx(Tab, {
1721
- "aria-controls": "workflow-tasks-panel",
1722
- fontSize: 1,
1723
- id: "workflow-tasks-all",
1724
- label: "All tasks",
1725
- onClick: () => setSegment("all"),
1726
- padding: 2,
1727
- selected: segment === "all"
1728
- }) ]
1729
- }),
1730
- /* @__PURE__ */ jsx(Box, {
1731
- flex: 1
1732
- }),
1733
- /* @__PURE__ */ jsx(ActiveFilterChips, {
1734
- filters: filters,
1735
- onChange: setFilters,
1736
- options: options
1571
+ /* @__PURE__ */ jsx(UnreadableDocsNote, {
1572
+ unreadable: unreadable
1737
1573
  }),
1738
- /* @__PURE__ */ jsx(TaskFilterMenu, {
1739
- filters: filters,
1740
- onChange: setFilters,
1741
- options: options
1742
- }) ]
1743
- }),
1744
- /* @__PURE__ */ jsxs(Box, {
1745
- id: "workflow-tasks-panel",
1746
- children: [
1747
1574
  /* @__PURE__ */ jsx(TaskGroupList, {
1748
1575
  groups: visible,
1749
1576
  onOpenTask: onOpenTask,
1750
- onOpenWorkflow: onOpenWorkflow
1751
- }), truncated ? /* @__PURE__ */ jsx(MutedNote, {
1752
- text: `Tasks from the latest ${INSTANCE_CAP} workflows — older ones aren't listed.`
1753
- }) : null ]
1754
- }) ]
1577
+ onOpenWorkflow: onOpenWorkflow,
1578
+ showTerminalActions: mineOnly
1579
+ }) ]
1580
+ }), truncated ? /* @__PURE__ */ jsx(MutedNote, {
1581
+ text: `Tasks from the latest ${INSTANCE_CAP} workflows — older ones aren't listed.`
1582
+ }) : null ]
1755
1583
  });
1756
1584
  }
1757
1585
 
1586
+ const TOOL_TABS = [ {
1587
+ value: "overview",
1588
+ label: "Overview"
1589
+ }, {
1590
+ value: "for-me",
1591
+ label: "For me"
1592
+ } ], [firstToolTab, ...restToolTabs] = TOOL_TABS, TOOL_TAB_CODEC = workflowsTabCodec([ firstToolTab.value, ...restToolTabs.map(tab => tab.value) ]);
1593
+
1594
+ function useToolTab() {
1595
+ const router = useRouter(), params = Object.fromEntries(router.state._searchParams ?? []);
1596
+ return {
1597
+ tab: TOOL_TAB_CODEC.read(params),
1598
+ setTab: next => router.navigate({
1599
+ _searchParams: definedEntries(TOOL_TAB_CODEC.write(params, next))
1600
+ })
1601
+ };
1602
+ }
1603
+
1604
+ function definedEntries(params) {
1605
+ return Object.entries(params).filter(entry => entry[1] !== void 0);
1606
+ }
1607
+
1608
+ function carryingSearchParams(state, searchParams) {
1609
+ return searchParams === void 0 ? state : {
1610
+ ...state,
1611
+ _searchParams: searchParams
1612
+ };
1613
+ }
1614
+
1758
1615
  function WorkflowsToolRoot() {
1759
1616
  const router = useRouter(), {seedInstance: seedInstance} = useWorkflowContext(), instanceId = typeof router.state.instanceId == "string" ? router.state.instanceId : void 0;
1760
1617
  return instanceId ? /* @__PURE__ */ jsx(WorkflowInstanceDetail, {
1761
1618
  instanceId: instanceId,
1762
- onBack: () => router.navigate({})
1619
+ onBack: () => router.navigate(carryingSearchParams({}, router.state._searchParams))
1763
1620
  }, instanceId) : /* @__PURE__ */ jsx(WorkflowsHome, {
1764
1621
  onOpenInstance: instance => {
1765
- typeof instance != "string" && seedInstance(instance), router.navigate({
1622
+ typeof instance != "string" && seedInstance(instance), router.navigate(carryingSearchParams({
1766
1623
  instanceId: typeof instance == "string" ? instance : instance._id
1767
- });
1624
+ }, router.state._searchParams));
1768
1625
  }
1769
1626
  });
1770
1627
  }
1771
1628
 
1772
- function ToolPanel({identity: identity, instances: instances, onOpenInstance: onOpenInstance, onOpenTask: onOpenTask, tab: tab}) {
1773
- return instances.invalid ? /* @__PURE__ */ jsx(InvalidDocNotice, {
1774
- invalid: instances.invalid
1775
- }) : tab === "tasks" ? /* @__PURE__ */ jsx(TasksTab, {
1776
- identity: identity,
1777
- instances: [ ...instances.inFlight, ...instances.settled ],
1778
- loading: instances.loading,
1779
- onOpenTask: onOpenTask,
1780
- onOpenWorkflow: onOpenInstance,
1781
- truncated: instances.truncated
1782
- }) : /* @__PURE__ */ jsx(WorkflowsDashboard, {
1783
- instances: instances,
1784
- onOpenInstance: onOpenInstance
1785
- });
1786
- }
1787
-
1788
1629
  function WorkflowsHome({onOpenInstance: onOpenInstance}) {
1789
- const [tab, setTab] = useState("tasks"), instances = useToolInstances(), identity = useAssignmentIdentity(), [openTask, setOpenTask] = useState(null);
1790
- /* @__PURE__ */
1791
- return jsxs(Card, {
1630
+ const {tab: tab, setTab: setTab} = useToolTab(), instances = useToolInstances(), identity = useAssignmentIdentity(), [openTask, setOpenTask] = useState(null), openInstance = target => {
1631
+ if (typeof target != "string") return onOpenInstance(target);
1632
+ const held = [ ...instances.inFlight, ...instances.settled ].find(i => i._id === target);
1633
+ onOpenInstance(held ?? target);
1634
+ }, [filterSlot, setFilterSlot] = useState(null), scrollerRef = useRef(null);
1635
+ return useEffect(() => {
1636
+ scrollerRef.current?.scrollTo({
1637
+ top: 0
1638
+ });
1639
+ }, [ tab ]),
1640
+ /* @__PURE__ */ jsxs(Card, {
1792
1641
  height: "fill",
1793
- overflow: "auto",
1794
1642
  children: [
1795
- /* @__PURE__ */ jsx(Box, {
1796
- padding: 4,
1797
- children: /* @__PURE__ */ jsx(Container, {
1798
- width: 2,
1799
- children: /* @__PURE__ */ jsxs(Stack, {
1800
- gap: 4,
1801
- children: [
1802
- /* @__PURE__ */ jsxs(Flex, {
1803
- align: "center",
1643
+ /* @__PURE__ */ jsxs(Flex, {
1644
+ direction: "column",
1645
+ height: "fill",
1646
+ overflow: "hidden",
1647
+ children: [
1648
+ /* @__PURE__ */ jsx(Box, {
1649
+ flex: "none",
1650
+ padding: 3,
1651
+ children: /* @__PURE__ */ jsx(Container, {
1652
+ width: 2,
1653
+ children: /* @__PURE__ */ jsxs(Stack, {
1804
1654
  gap: 3,
1805
1655
  children: [
1806
- /* @__PURE__ */ jsx(Text, {
1807
- size: 2,
1808
- weight: "semibold",
1809
- children: "Workflows"
1810
- }),
1811
- /* @__PURE__ */ jsx(Box, {
1812
- flex: 1
1656
+ /* @__PURE__ */ jsxs(Flex, {
1657
+ align: "center",
1658
+ gap: 3,
1659
+ paddingLeft: 2,
1660
+ children: [
1661
+ /* @__PURE__ */ jsx(Text, {
1662
+ size: 2,
1663
+ weight: "semibold",
1664
+ children: "Workflows"
1665
+ }),
1666
+ /* @__PURE__ */ jsx(Box, {
1667
+ flex: 1
1668
+ }),
1669
+ /* @__PURE__ */ jsx(NewWorkflowButton, {}) ]
1813
1670
  }),
1814
- /* @__PURE__ */ jsx(NewWorkflowButton, {}) ]
1815
- }),
1816
- /* @__PURE__ */ jsxs(TabList, {
1817
- gap: 1,
1818
- children: [
1819
- /* @__PURE__ */ jsx(Tab, {
1820
- "aria-controls": "workflows-tool-panel",
1821
- fontSize: 1,
1822
- id: "workflows-tool-tab-tasks",
1823
- label: "Tasks",
1824
- onClick: () => setTab("tasks"),
1825
- padding: 2,
1826
- selected: tab === "tasks"
1671
+ /* @__PURE__ */ jsx(TabSwitch, {
1672
+ ariaControls: "workflows-tool-panel",
1673
+ idPrefix: "workflows-tool-tab",
1674
+ onSelect: setTab,
1675
+ options: TOOL_TABS,
1676
+ selected: tab
1827
1677
  }),
1828
- /* @__PURE__ */ jsx(Tab, {
1829
- "aria-controls": "workflows-tool-panel",
1830
- fontSize: 1,
1831
- id: "workflows-tool-tab-workflows",
1832
- label: "All workflows",
1833
- onClick: () => setTab("workflows"),
1834
- padding: 2,
1835
- selected: tab === "workflows"
1678
+ /* @__PURE__ */ jsx("div", {
1679
+ ref: setFilterSlot
1836
1680
  }) ]
1837
- }),
1838
- /* @__PURE__ */ jsx(Box, {
1681
+ })
1682
+ })
1683
+ }),
1684
+ /* @__PURE__ */ jsx(Box, {
1685
+ ref: scrollerRef,
1686
+ flex: 1,
1687
+ overflow: "auto",
1688
+ paddingX: 3,
1689
+ style: {
1690
+ scrollbarGutter: "stable"
1691
+ },
1692
+ children: /* @__PURE__ */ jsx(Container, {
1693
+ width: 2,
1694
+ children: /* @__PURE__ */ jsx(TabPanel, {
1695
+ "aria-labelledby": `workflows-tool-tab-${tab}`,
1839
1696
  id: "workflows-tool-panel",
1840
- children: /* @__PURE__ */ jsx(ToolPanel, {
1841
- identity: identity,
1842
- instances: instances,
1843
- onOpenInstance: target => {
1844
- if (typeof target != "string") return onOpenInstance(target);
1845
- const held = [ ...instances.inFlight, ...instances.settled ].find(i => i._id === target);
1846
- onOpenInstance(held ?? target);
1847
- },
1848
- onOpenTask: setOpenTask,
1849
- tab: tab
1697
+ children: /* @__PURE__ */ jsx(Box, {
1698
+ paddingBottom: 4,
1699
+ paddingTop: 1,
1700
+ children: /* @__PURE__ */ jsx(TasksTab, {
1701
+ filterSlot: filterSlot,
1702
+ identity: identity,
1703
+ instances: [ ...instances.inFlight, ...instances.settled ],
1704
+ loading: instances.loading,
1705
+ mineOnly: tab === "for-me",
1706
+ onOpenTask: setOpenTask,
1707
+ onOpenWorkflow: openInstance,
1708
+ truncated: instances.truncated,
1709
+ unreadable: instances.unreadable
1710
+ })
1850
1711
  })
1851
- }) ]
1712
+ })
1852
1713
  })
1853
- })
1714
+ }) ]
1854
1715
  }), openTask ? /* @__PURE__ */ jsx(ToolActivityDialog, {
1855
1716
  onClose: () => setOpenTask(null),
1856
1717
  target: openTask