@sanity/workflow-studio-plugin 0.26.0 → 0.27.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,20 +1,14 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
 
3
- import { Text, Dialog, Box, Stack, TextInput, Flex, Button, Menu, MenuItem, useTheme_v2, Card, Grid, Badge, MenuButton, TabPanel } from "@sanity/ui";
3
+ import { Text, Stack, Flex, Button, Box, Card, Grid, TextInput, Dialog, Badge, useTheme_v2, MenuButton, Menu, MenuItem, TabPanel } from "@sanity/ui";
4
4
 
5
5
  import { useWorkflowTelemetry } from "@sanity/workflow-react";
6
6
 
7
- import { useState, useMemo, useEffect, useRef, useSyncExternalStore, useCallback } from "react";
7
+ import { useState, useEffect, useMemo, useRef, useSyncExternalStore, useCallback } from "react";
8
8
 
9
9
  import { useRouter } from "sanity/router";
10
10
 
11
- import { TOAST_ID, describeError, useWorkflowContext, useDefinition, useWorkflowToast, useLogEventOnMount, WorkflowAbortDialogOpened, instanceTitle, WorkflowAbortDialogSubmitted, isLiveEntry, stageTitle, UserAvatar, useAvatarSize, HintedMenuButton, HoverHint, useUserDisplay, formatShortAgo, formatDateTime, isEvaluationStale, useSpaceToken, DocPreviewLink, SpinnerSlot, useWorkflowInstanceEntry, WorkflowInstanceDetailViewed, LoadingRow, InvalidDocNotice, useContainerToken, openActivityGone, InstanceSnapshotBody, ActivityDetailDialog, instanceBreadcrumb, namesNoDeployedDefinition, mappingIssueDetail, formatTimeAgo, formatShortDateTime, readDeployedDefinitions, EmptyState, CautionNote, DismissablePopover, createSubscribers, useDelayedFlag, definitionSnapshotOf, findActivity, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isActivityAssignedTo, assigneeUserIdsOf, dueDatesOf, datesOf, isOpenActivityStatus, committedRowFace, activityRowProps, gdrLocality, WORKFLOW_API_VERSION, openableSchemaType, formatDate, parseStoredDateValue, useUserDisplays, useProjectMembers, WorkflowTaskFiltersApplied, DocRefFace, CollapsibleBand, LinkChip, BreadcrumbTail, StageFace, TrailingHairline, StaleLock, ActivityRow, WorkflowTitleSeedDrifted, UnreadableDocsNote, ForMeEmptyState, useBadgeCapTrim, CountedLabel, MetaRow, TabSwitch, WORKFLOW_PAGE_TABS, definitionFingerprint, Hairline, LogEventOnMount, WorkflowBoardWorkflowSelected, WorkflowDefinitionDetailViewed, toolRoute, landingState, TOOL_TABS, tabSelectionNavigates, WorkflowToolOpened, toolTabState, workflowPageState, instanceState, backToTabLabel, useAssignmentIdentity, workflowState } from "./index.js";
12
-
13
- import { ClockIcon } from "@sanity/icons/Clock";
14
-
15
- import { CogIcon } from "@sanity/icons/Cog";
16
-
17
- import { EllipsisHorizontalIcon } from "@sanity/icons/EllipsisHorizontal";
11
+ import { isLiveEntry, AbortWorkflowDialog, isEvaluationStale, useSpaceToken, DocPreviewLink, SpinnerSlot, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, LoadingRow, InvalidDocNotice, useDefinition, useContainerToken, openActivityGone, InstanceSnapshotBody, ActivityLog, ActivityDetailDialog, instanceBreadcrumb, CodeChip, instanceTitle, namesNoDeployedDefinition, mappingIssueDetail, formatTimeAgo, formatShortDateTime, useWorkflowContext, readDeployedDefinitions, describeError, EmptyState, CautionNote, HoverHint, DismissablePopover, createSubscribers, useDelayedFlag, definitionSnapshotOf, findActivity, stageTitle, stateByActivity, findActivityNode, rowDateControlState, rowAssignControlState, isActivityAssignedTo, assigneesOf, dueDatesOf, datesOf, isOpenActivityStatus, committedRowFace, activityRowProps, gdrLocality, TOAST_ID, WORKFLOW_API_VERSION, useWorkflowToast, openableSchemaType, formatDate, parseStoredDateValue, useUserDisplays, useProjectMembers, WorkflowTaskFiltersApplied, DocRefFace, Hairline, LinkChip, BreadcrumbTail, StageFace, StaleLock, ActivityRow, WorkflowTitleSeedDrifted, UnreadableDocsNote, ForMeEmptyState, useBadgeCapTrim, CountedLabel, MetaRow, TabSwitch, WORKFLOW_PAGE_TABS, definitionFingerprint, LogEventOnMount, WorkflowBoardWorkflowSelected, WorkflowDefinitionDetailViewed, toolRoute, landingState, TOOL_TABS, tabSelectionNavigates, WorkflowToolOpened, toolTabState, workflowPageState, instanceState, backToTabLabel, useAssignmentIdentity, workflowState } from "./index.js";
18
12
 
19
13
  import { entryDocRefs, startKindOf, isSubjectEntry, isInputSourced, latestDeployedDefinitions, terminalState, isStartableDefinition, toBareId, findOpenStageEntry } from "@sanity/workflow-engine";
20
14
 
@@ -46,24 +40,16 @@ import { endOfDay } from "date-fns/endOfDay";
46
40
 
47
41
  import { createPortal } from "react-dom";
48
42
 
43
+ import { ChevronDownIcon } from "@sanity/icons/ChevronDown";
44
+
45
+ import { ChevronRightIcon } from "@sanity/icons/ChevronRight";
46
+
49
47
  import { isReferenceSchemaType } from "@sanity/types";
50
48
 
51
49
  import { WorkflowDiagram } from "@sanity/workflow-diagram";
52
50
 
53
- import { ChevronDownIcon } from "@sanity/icons/ChevronDown";
54
-
55
51
  import { useReducedMotion, motion, useSpring, useMotionValueEvent } from "motion/react";
56
52
 
57
- function CodeChip({value: value}) {
58
- /* @__PURE__ */
59
- return jsx("code", {
60
- style: {
61
- overflowWrap: "anywhere"
62
- },
63
- children: value
64
- });
65
- }
66
-
67
53
  function SectionHeading({children: children}) {
68
54
  /* @__PURE__ */
69
55
  return jsx(Text, {
@@ -73,146 +59,6 @@ function SectionHeading({children: children}) {
73
59
  });
74
60
  }
75
61
 
76
- function normalizeAbortReason(raw) {
77
- const trimmed = raw.trim();
78
- return trimmed === "" ? void 0 : trimmed;
79
- }
80
-
81
- function abortCommitReport(args) {
82
- return args.changed ? {
83
- outcome: "aborted",
84
- toast: abortedToast(args.title)
85
- } : {
86
- outcome: "already-settled",
87
- toast: abortAlreadySettledToast(args.title)
88
- };
89
- }
90
-
91
- function abortedToast(title) {
92
- return {
93
- id: TOAST_ID.abort,
94
- status: "info",
95
- title: `Aborted “${title}”`
96
- };
97
- }
98
-
99
- function abortAlreadySettledToast(title) {
100
- return {
101
- id: TOAST_ID.abort,
102
- status: "info",
103
- title: `“${title}” had already finished`,
104
- description: "Nothing was aborted — the workflow reached a terminal state first."
105
- };
106
- }
107
-
108
- function abortUnconfirmedToast(args) {
109
- return {
110
- id: TOAST_ID.abort,
111
- status: "warning",
112
- title: `Couldn’t confirm “${args.title}” was aborted`,
113
- description: `${describeError(args.err)} — the abort may still have committed, so check the workflow’s state before trying again.`
114
- };
115
- }
116
-
117
- function AbortWorkflowDialog({entry: entry, onClose: onClose}) {
118
- const {engine: engine} = useWorkflowContext(), definition = useDefinition(entry), toast = useWorkflowToast(), telemetry = useWorkflowTelemetry(), [typedReason, setTypedReason] = useState(""), [pending, setPending] = useState(!1), {instance: instance} = entry, title = instanceTitle(instance, definition), reason = normalizeAbortReason(typedReason), reasonInputId = `abort-workflow-reason-${instance._id}`;
119
- useLogEventOnMount(WorkflowAbortDialogOpened, {
120
- instanceId: instance._id
121
- });
122
- const confirm = async () => {
123
- if (reason === void 0) return;
124
- const log = outcome => telemetry.log(WorkflowAbortDialogSubmitted, {
125
- instanceId: instance._id,
126
- outcome: outcome
127
- });
128
- setPending(!0);
129
- try {
130
- const {changed: changed} = await engine.abortInstance({
131
- instanceId: instance._id,
132
- reason: reason
133
- }), report = abortCommitReport({
134
- title: title,
135
- changed: changed
136
- });
137
- toast.push(report.toast), log(report.outcome), onClose();
138
- } catch (err) {
139
- toast.push(abortUnconfirmedToast({
140
- title: title,
141
- err: err
142
- })), log("unconfirmed"), setPending(!1);
143
- }
144
- };
145
- /* @__PURE__ */
146
- return jsx(Dialog, {
147
- footer: /* @__PURE__ */ jsx(Box, {
148
- padding: 3,
149
- children: /* @__PURE__ */ jsxs(Flex, {
150
- gap: 2,
151
- justify: "flex-end",
152
- children: [
153
- /* @__PURE__ */ jsx(Button, {
154
- disabled: pending,
155
- fontSize: 1,
156
- mode: "bleed",
157
- onClick: onClose,
158
- padding: 2,
159
- text: "Cancel"
160
- }),
161
- /* @__PURE__ */ jsx(Button, {
162
- disabled: reason === void 0,
163
- fontSize: 1,
164
- loading: pending,
165
- onClick: () => {
166
- confirm();
167
- },
168
- padding: 2,
169
- text: "Abort workflow",
170
- tone: "critical"
171
- }) ]
172
- })
173
- }),
174
- header: "Abort workflow",
175
- id: `abort-workflow-${instance._id}`,
176
- onClose: () => {
177
- pending || onClose();
178
- },
179
- width: 0,
180
- children: /* @__PURE__ */ jsx(Box, {
181
- padding: 4,
182
- children: /* @__PURE__ */ jsxs(Stack, {
183
- gap: 5,
184
- children: [
185
- /* @__PURE__ */ jsxs(Text, {
186
- size: 1,
187
- children: [ "Abort the ",
188
- /* @__PURE__ */ jsx("strong", {
189
- children: title
190
- }), " workflow? This cannot be undone." ]
191
- }),
192
- /* @__PURE__ */ jsxs(Stack, {
193
- gap: 3,
194
- children: [
195
- /* @__PURE__ */ jsx(Text, {
196
- as: "label",
197
- htmlFor: reasonInputId,
198
- size: 1,
199
- weight: "medium",
200
- children: "Reason"
201
- }),
202
- /* @__PURE__ */ jsx(TextInput, {
203
- autoFocus: !0,
204
- fontSize: 1,
205
- id: reasonInputId,
206
- onChange: event => setTypedReason(event.currentTarget.value),
207
- placeholder: "Why is this workflow being stopped?",
208
- value: typedReason
209
- }) ]
210
- }) ]
211
- })
212
- })
213
- });
214
- }
215
-
216
62
  function AbortWorkflowSection({entry: entry}) {
217
63
  const [aborting, setAborting] = useState(!1);
218
64
  return isLiveEntry(entry) ? /* @__PURE__ */ jsxs(Stack, {
@@ -237,361 +83,6 @@ function AbortWorkflowSection({entry: entry}) {
237
83
  }) : null;
238
84
  }
239
85
 
240
- function makeTitleResolver(definition) {
241
- const activities = (definition?.stages ?? []).flatMap(s => s.activities ?? []);
242
- return {
243
- action: (activityName, actionName) => activities.find(t => t.name === activityName)?.actions?.find(a => a.name === actionName)?.title ?? actionName,
244
- activity: name => activities.find(t => t.name === name)?.title ?? name,
245
- stage: name => stageTitle(definition, name)
246
- };
247
- }
248
-
249
- function attributed(actor, ...parts) {
250
- if (actor) return {
251
- actorId: actor.id,
252
- parts: parts
253
- };
254
- const [first, ...rest] = parts;
255
- return typeof first != "string" ? {
256
- actorId: void 0,
257
- parts: parts
258
- } : {
259
- actorId: void 0,
260
- parts: [ first.charAt(0).toUpperCase() + first.slice(1), ...rest ]
261
- };
262
- }
263
-
264
- function standalone(...parts) {
265
- return {
266
- actorId: void 0,
267
- parts: parts
268
- };
269
- }
270
-
271
- const historyLiner = {
272
- stageEntered: (h, titles) => attributed(h.actor, `entered ${titles.stage(h.stage)}${h.fromStage ? ` from ${titles.stage(h.fromStage)}` : ""}`),
273
- stageExited: (h, titles) => attributed(h.actor, `left ${titles.stage(h.stage)} for ${titles.stage(h.toStage)}`),
274
- activityStatusChanged: (h, titles) => attributed(h.actor, `changed ${titles.activity(h.activity)}: ${h.from} → ${h.to}`),
275
- actionFired: (h, titles) => {
276
- const phrase = `fired ${titles.action(h.activity, h.action)} on ${titles.activity(h.activity)}`;
277
- return h.triggered ? standalone(`The workflow ${phrase}`) : attributed(h.actor, phrase);
278
- },
279
- transitionFired: (h, titles) => attributed(h.actor, `moved ${titles.stage(h.fromStage)} → ${titles.stage(h.toStage)}`),
280
- effectQueued: h => standalone("Effect ", {
281
- code: h.effect
282
- }, " queued"),
283
- effectCompleted: h => {
284
- const detail = h.detail ? ` — ${h.detail}` : "";
285
- return h.actor ? attributed(h.actor, "resolved effect ", {
286
- code: h.effect
287
- }, ` — ${h.status}${detail}`) : standalone("Effect ", {
288
- code: h.effect
289
- }, ` ${h.status}${detail}`);
290
- },
291
- effectClaimReleased: h => attributed(h.actor, "released a stale claim on effect ", {
292
- code: h.effect
293
- }, ` ${h.via === "sweep" ? "via the sweeper" : "via a drain takeover"}`),
294
- spawned: (h, titles) => attributed(h.actor, `spawned a child workflow from ${titles.activity(h.activity)}`),
295
- subworkflowAdopted: (h, titles) => standalone(`Re-adopted child workflow into ${titles.activity(h.activity)} on re-entering ${titles.stage(h.stage)}`),
296
- subworkflowResolved: (h, titles) => standalone(`Child workflow of ${titles.activity(h.activity)} ${h.status === "done" ? "completed" : "was aborted"}`),
297
- subworkflowOrphaned: h => standalone(`Orphaned child workflow reached terminal — ${h.detail}`),
298
- aborted: (h, titles) => attributed(h.actor, `aborted the workflow at ${titles.stage(h.stage)}${h.reason ? ` — ${h.reason}` : ""}`),
299
- opApplied: (h, titles) => h.action ? attributed(h.actor, `changed state via action ${titles.action(h.activity ?? "", h.action)}`) : h.activity ? attributed(h.actor, `changed state via activity ${titles.activity(h.activity)}`) : attributed(h.actor, "changed state via ", {
300
- code: h.opType
301
- }),
302
- fieldQueryDiscarded: h => standalone({
303
- code: JSON.stringify(h)
304
- })
305
- };
306
-
307
- function historyLine(h, titles) {
308
- return historyLiner[h._type](h, titles);
309
- }
310
-
311
- function historyWindow(args) {
312
- const {entries: entries, expanded: expanded} = args;
313
- return {
314
- overflows: entries.length > 6,
315
- shown: expanded ? entries : entries.slice(0, 6)
316
- };
317
- }
318
-
319
- function PendingEffectRows({instance: instance, now: now}) {
320
- const {drainEffectsFor: drainEffectsFor, completeEffectFor: completeEffectFor, effectHandlers: effectHandlers} = useWorkflowContext(), toast = useWorkflowToast(), [busy, setBusy] = useState(!1), pending = instance.pendingEffects ?? [];
321
- if (pending.length === 0) return null;
322
- const runRegistered = async () => {
323
- setBusy(!0);
324
- try {
325
- await drainEffectsFor(instance._id), toast.push({
326
- id: TOAST_ID.effectsDrain,
327
- status: "info",
328
- title: "Ran the registered handlers"
329
- });
330
- } catch (err) {
331
- toast.push({
332
- id: TOAST_ID.effectsDrain,
333
- status: "error",
334
- title: "Failed to run pending effects",
335
- description: describeError(err)
336
- });
337
- } finally {
338
- setBusy(!1);
339
- }
340
- }, resolve = async (effectKey, status) => {
341
- setBusy(!0);
342
- try {
343
- await completeEffectFor(instance._id, {
344
- effectKey: effectKey,
345
- status: status,
346
- detail: "Resolved in Studio"
347
- }), toast.push({
348
- id: TOAST_ID.effectResolve,
349
- status: "info",
350
- title: `Effect marked as ${status}`
351
- });
352
- } catch (err) {
353
- toast.push({
354
- id: TOAST_ID.effectResolve,
355
- status: "error",
356
- title: "Failed to resolve the effect",
357
- description: describeError(err)
358
- });
359
- } finally {
360
- setBusy(!1);
361
- }
362
- };
363
- /* @__PURE__ */
364
- return jsx(Fragment, {
365
- children: pending.map(fx => /* @__PURE__ */ jsx(PendingEffectRow, {
366
- busy: busy,
367
- effect: fx,
368
- instanceId: instance._id,
369
- now: now,
370
- onResolve: status => resolve(fx._key, status),
371
- onRunHandlers: runRegistered,
372
- registered: fx.name in effectHandlers
373
- }, fx._key))
374
- });
375
- }
376
-
377
- function PendingEffectRow({busy: busy, effect: effect, instanceId: instanceId, now: now, onResolve: onResolve, onRunHandlers: onRunHandlers, registered: registered}) {
378
- const label = effect.title ?? effect.name, avatarSize = useAvatarSize();
379
- /* @__PURE__ */
380
- return jsxs(Flex, {
381
- align: "center",
382
- gap: 2,
383
- children: [
384
- /* @__PURE__ */ jsx(FeedGlyph, {
385
- hint: "Waiting to run",
386
- icon: /* @__PURE__ */ jsx(ClockIcon, {})
387
- }),
388
- /* @__PURE__ */ jsxs(Flex, {
389
- align: "center",
390
- flex: 1,
391
- gap: 1,
392
- style: {
393
- minWidth: 0
394
- },
395
- children: [
396
- /* @__PURE__ */ jsx(Box, {
397
- style: {
398
- minWidth: 0
399
- },
400
- children: /* @__PURE__ */ jsxs(Text, {
401
- size: 1,
402
- textOverflow: "ellipsis",
403
- children: [ effect.title ?? /* @__PURE__ */ jsx(CodeChip, {
404
- value: effect.name
405
- }), registered ? " — ready to run in Studio" : " — waiting for the runtime" ]
406
- })
407
- }),
408
- /* @__PURE__ */ jsx(FeedStamp, {
409
- at: effect.queuedAt,
410
- now: now
411
- }) ]
412
- }),
413
- /* @__PURE__ */ jsx(Flex, {
414
- align: "center",
415
- flex: "none",
416
- style: {
417
- height: avatarSize
418
- },
419
- children: /* @__PURE__ */ jsx(HintedMenuButton, {
420
- button: /* @__PURE__ */ jsx(Button, {
421
- "aria-label": `Resolve ${label}`,
422
- disabled: busy,
423
- fontSize: 1,
424
- icon: EllipsisHorizontalIcon,
425
- mode: "bleed",
426
- padding: 2
427
- }),
428
- hint: "Resolve",
429
- id: `pending-effect-menu-${instanceId}-${effect._key}`,
430
- menu: /* @__PURE__ */ jsxs(Menu, {
431
- children: [ registered ? /* @__PURE__ */ jsx(MenuItem, {
432
- onClick: onRunHandlers,
433
- text: "Run registered handlers"
434
- }) : null,
435
- /* @__PURE__ */ jsx(MenuItem, {
436
- onClick: () => onResolve("done"),
437
- text: "Mark done"
438
- }),
439
- /* @__PURE__ */ jsx(MenuItem, {
440
- onClick: () => onResolve("failed"),
441
- text: "Mark failed",
442
- tone: "critical"
443
- }) ]
444
- }),
445
- popover: {
446
- placement: "bottom-end",
447
- portal: !0
448
- }
449
- })
450
- }) ]
451
- });
452
- }
453
-
454
- function ActivityLog({instance: instance, definition: definition}) {
455
- const titles = useMemo(() => makeTitleResolver(definition), [ definition ]), entries = useMemo(() => instance.history.toReversed(), [ instance.history ]), [now, setNow] = useState(() => /* @__PURE__ */ new Date), [expanded, setExpanded] = useState(!1);
456
- useEffect(() => {
457
- const timer = setInterval(() => setNow(/* @__PURE__ */ new Date), 6e4);
458
- return () => clearInterval(timer);
459
- }, []);
460
- const history = historyWindow({
461
- entries: entries,
462
- expanded: expanded
463
- });
464
- return entries.length === 0 && (instance.pendingEffects ?? []).length === 0 ? /* @__PURE__ */ jsx(Text, {
465
- muted: !0,
466
- size: 1,
467
- children: "No activity yet"
468
- }) : /* @__PURE__ */ jsxs(Stack, {
469
- gap: 3,
470
- children: [
471
- /* @__PURE__ */ jsx(PendingEffectRows, {
472
- instance: instance,
473
- now: now
474
- }), history.shown.map(h => /* @__PURE__ */ jsx(HistoryRow, {
475
- entry: h,
476
- now: now,
477
- titles: titles
478
- }, h._key)), history.overflows ? /* @__PURE__ */ jsx(Flex, {
479
- children: /* @__PURE__ */ jsx(Button, {
480
- fontSize: 1,
481
- mode: "bleed",
482
- onClick: () => setExpanded(!expanded),
483
- padding: 2,
484
- text: expanded ? "Show less history" : "Show all history"
485
- })
486
- }) : null ]
487
- });
488
- }
489
-
490
- function FeedStamp({at: at, now: now}) {
491
- /* @__PURE__ */
492
- return jsxs(Flex, {
493
- align: "center",
494
- flex: "none",
495
- gap: 1,
496
- children: [
497
- /* @__PURE__ */ jsx(Text, {
498
- muted: !0,
499
- size: 1,
500
- children: "·"
501
- }),
502
- /* @__PURE__ */ jsx(HoverHint, {
503
- text: formatDateTime(at),
504
- children: /* @__PURE__ */ jsx(Text, {
505
- muted: !0,
506
- size: 1,
507
- style: {
508
- whiteSpace: "nowrap"
509
- },
510
- children: formatShortAgo(at, now)
511
- })
512
- }) ]
513
- });
514
- }
515
-
516
- function HistoryRow({entry: entry, now: now, titles: titles}) {
517
- const line = historyLine(entry, titles);
518
- /* @__PURE__ */
519
- return jsxs(Flex, {
520
- align: "center",
521
- gap: 2,
522
- children: [ line.actorId === void 0 ? /* @__PURE__ */ jsx(FeedGlyph, {
523
- hint: "The workflow",
524
- icon: /* @__PURE__ */ jsx(CogIcon, {})
525
- }) : /* @__PURE__ */ jsx(UserAvatar, {
526
- id: line.actorId
527
- }),
528
- /* @__PURE__ */ jsxs(Flex, {
529
- align: "center",
530
- flex: 1,
531
- gap: 1,
532
- style: {
533
- minWidth: 0
534
- },
535
- children: [
536
- /* @__PURE__ */ jsx(Box, {
537
- style: {
538
- minWidth: 0
539
- },
540
- children: /* @__PURE__ */ jsxs(Text, {
541
- muted: !0,
542
- size: 1,
543
- textOverflow: "ellipsis",
544
- children: [ line.actorId === void 0 ? null : /* @__PURE__ */ jsxs(Fragment, {
545
- children: [
546
- /* @__PURE__ */ jsx(ActorNameSpan, {
547
- id: line.actorId
548
- }), " " ]
549
- }), line.parts.map((part, index) => typeof part == "string" ? part : /* @__PURE__ */ jsx(CodeChip, {
550
- value: part.code
551
- }, index)) ]
552
- })
553
- }),
554
- /* @__PURE__ */ jsx(FeedStamp, {
555
- at: entry.at,
556
- now: now
557
- }) ]
558
- }) ]
559
- });
560
- }
561
-
562
- function ActorNameSpan({id: id}) {
563
- const {name: name} = useUserDisplay(id), {font: font} = useTheme_v2();
564
- /* @__PURE__ */
565
- return jsx("span", {
566
- style: {
567
- fontWeight: font.text.weights.medium
568
- },
569
- children: name
570
- });
571
- }
572
-
573
- function FeedGlyph({hint: hint, icon: icon}) {
574
- const size = useAvatarSize();
575
- /* @__PURE__ */
576
- return jsx(HoverHint, {
577
- text: hint,
578
- children: /* @__PURE__ */ jsx(Flex, {
579
- align: "center",
580
- flex: "none",
581
- justify: "center",
582
- style: {
583
- height: size,
584
- width: size
585
- },
586
- children: /* @__PURE__ */ jsx(Text, {
587
- muted: !0,
588
- size: 1,
589
- children: icon
590
- })
591
- })
592
- });
593
- }
594
-
595
86
  function documentRefsOf(instance) {
596
87
  const seen = /* @__PURE__ */ new Map;
597
88
  for (const ref of entryDocRefs(instance.fields)) seen.has(ref.id) || seen.set(ref.id, ref);
@@ -1593,7 +1084,7 @@ function taskRowsOf(args) {
1593
1084
  open: isOpenActivityStatus(activity.status),
1594
1085
  dates: datesOf(fields),
1595
1086
  dueDates: dueDatesOf(fields),
1596
- assigneeIds: assigneeUserIdsOf(fields),
1087
+ assignees: assigneesOf(fields),
1597
1088
  assignedToMe: identity !== void 0 && isActivityAssignedTo({
1598
1089
  state: fields,
1599
1090
  who: identity
@@ -1690,7 +1181,7 @@ function taskRowRender(row, entry) {
1690
1181
  return entry?.evaluation === void 0 || activityEval === void 0 ? {
1691
1182
  face: committedRowFace(row),
1692
1183
  assignState: row.assignControl,
1693
- assigneeIds: row.assigneeIds,
1184
+ assignees: row.assignees,
1694
1185
  dateState: row.dateControl,
1695
1186
  dueDates: row.dueDates,
1696
1187
  terminalActions: [],
@@ -2127,7 +1618,7 @@ function statusArm(row, statuses) {
2127
1618
 
2128
1619
  function assigneeArm(row, assigneeIds) {
2129
1620
  const arm = activeArm(assigneeIds);
2130
- return arm ? arm.some(id => row.assigneeIds.includes(id)) : !0;
1621
+ return arm ? row.assignees.some(item => item.type === "user" && arm.includes(item.id)) : !0;
2131
1622
  }
2132
1623
 
2133
1624
  function rowMatches(args) {
@@ -2651,6 +2142,55 @@ function FilterRowPortal({filters: filters, onChange: onChange, options: options
2651
2142
  }), slot) : null;
2652
2143
  }
2653
2144
 
2145
+ function CollapsibleBand({header: header, children: children, background: background = !1, defaultOpen: defaultOpen = !0, onToggle: onToggle, open: controlledOpen, sticky: sticky = !1, title: title}) {
2146
+ const [heldOpen, setHeldOpen] = useState(defaultOpen), open = controlledOpen ?? heldOpen, toggle = onToggle ?? (() => setHeldOpen(value => !value)), verb = open ? "Collapse" : "Expand";
2147
+ /* @__PURE__ */
2148
+ return jsxs(Stack, {
2149
+ gap: 1,
2150
+ children: [
2151
+ /* @__PURE__ */ jsx(Card, {
2152
+ onDoubleClick: event => {
2153
+ event.target instanceof Element && event.target.closest("a, button") || toggle();
2154
+ },
2155
+ padding: 1,
2156
+ radius: 3,
2157
+ style: {
2158
+ userSelect: "none",
2159
+ ...sticky ? {
2160
+ position: "sticky",
2161
+ top: 0,
2162
+ zIndex: 1
2163
+ } : {}
2164
+ },
2165
+ tone: background ? "transparent" : "inherit",
2166
+ children: /* @__PURE__ */ jsxs(Flex, {
2167
+ align: "center",
2168
+ children: [
2169
+ /* @__PURE__ */ jsx(Button, {
2170
+ "aria-label": `${verb} ${title}`,
2171
+ fontSize: 1,
2172
+ icon: open ? ChevronDownIcon : ChevronRightIcon,
2173
+ mode: "bleed",
2174
+ onClick: toggle,
2175
+ padding: 2
2176
+ }), header ]
2177
+ })
2178
+ }), open ? children : null ]
2179
+ });
2180
+ }
2181
+
2182
+ function TrailingHairline() {
2183
+ /* @__PURE__ */
2184
+ return jsx(Box, {
2185
+ flex: 1,
2186
+ paddingLeft: 1,
2187
+ paddingRight: 1,
2188
+ children: /* @__PURE__ */ jsx(Hairline, {
2189
+ weight: .5
2190
+ })
2191
+ });
2192
+ }
2193
+
2654
2194
  function isPlainClick(event) {
2655
2195
  return !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey;
2656
2196
  }
@@ -2856,7 +2396,7 @@ function TaskActivityRow({row: row, onOpen: onOpen, showTerminalActions: showTer
2856
2396
  stale: render.stale,
2857
2397
  children: /* @__PURE__ */ jsx(ActivityRow, {
2858
2398
  assignState: render.assignState,
2859
- assigneeIds: render.assigneeIds,
2399
+ assignees: render.assignees,
2860
2400
  dateState: render.dateState,
2861
2401
  dueDates: render.dueDates,
2862
2402
  face: render.face,