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