@sanity/workflow-studio-plugin 0.30.0 → 0.31.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.
@@ -16,7 +16,7 @@ import { useMemo, useState, useEffect, useRef, Fragment as Fragment$1, useLayout
16
16
 
17
17
  import { StateLink, useRouter } from "sanity/router";
18
18
 
19
- import { isOpenActivityStatus, dueDatesOf, isDueDatePast, parseStoredDateValue, countBy, stageTitle, assigneesOf, findActivity, RUN_SEARCH_PARAM, toolTabState, namesNoDeployedDefinition, mappingIssueDetail, formatTimeAgo, formatShortDateTime, useSpaceToken, useWorkflowContext, readDeployedDefinitions, describeError, useMinuteClock, NoteBanner, LoadingRow, EmptyState, AgoStamp, AlertGlyph, gdrLocality, openableSchemaType, instanceTitle, DocRefFace, useCapTrimFor, useTextIconSize, RowAssignees, CountedHeading, MetricPair, useRadiusToken, WORKFLOW_API_VERSION, WorkflowTitleSeedDrifted, useCurrentUserId, LogEventOnMount, WorkflowBoardWorkflowSelected, definitionFingerprint, HoverHint, useReleaseTitles, useProjectMembers, useUserDisplays, useAvatarSize, RoleAssignedNotice, isLiveEntry, AbortWorkflowDialog, isEvaluationStale, SpinnerSlot, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, InvalidDocNotice, useDefinition, useContainerToken, openActivityGone, InstanceFaultNote, InstanceSnapshotBody, ActivityLog, ActivityDetailDialog, instanceBreadcrumb, CodeChip, DocPreviewLink, UnreadableDocsNote, identityOf, DismissablePopover, MetaRow, WorkflowDefinitionDetailViewed, toolRoute, landingState, TabSwitch, tabSelectionNavigates, WorkflowToolOpened, workflowState, definitionsRouteOf, TOOL_TABS, useAssignmentIdentity } from "./index.js";
19
+ import { isOpenActivityStatus, dueDatesOf, isDueDatePast, parseStoredDateValue, findActivity, isHandsOff, assigneesOf, activityLabel, countBy, stageTitle, userIdsOf, RUN_SEARCH_PARAM, toolTabState, namesNoDeployedDefinition, mappingIssueDetail, formatTimeAgo, formatShortDateTime, useSpaceToken, useWorkflowContext, readDeployedDefinitions, describeError, useMinuteClock, NoteBanner, LoadingRow, EmptyState, AgoStamp, AlertGlyph, gdrLocality, openableSchemaType, instanceTitle, MetricLockup, shortSpanOf, useAvatarSize, HoverHint, useUserDisplays, uniqueIds, disambiguatedNames, useProjectMembers, MetricPair, AlertChip, DocRefFace, useCapTrimFor, useTextIconSize, CountedHeading, useRadiusToken, WORKFLOW_API_VERSION, WorkflowTitleSeedDrifted, useCurrentUserId, LogEventOnMount, WorkflowBoardWorkflowSelected, definitionFingerprint, useReleaseTitles, RoleAssignedNotice, isLiveEntry, AbortWorkflowDialog, isEvaluationStale, SpinnerSlot, useWorkflowInstanceEntry, useLogEventOnMount, WorkflowInstanceDetailViewed, InvalidDocNotice, useDefinition, useContainerToken, openActivityGone, InstanceFaultNote, InstanceSnapshotBody, ActivityLog, ActivityDetailDialog, instanceBreadcrumb, CodeChip, DocPreviewLink, UnreadableDocsNote, identityOf, DismissablePopover, MetaRow, WorkflowDefinitionDetailViewed, toolRoute, landingState, TabSwitch, tabSelectionNavigates, WorkflowToolOpened, workflowState, definitionsRouteOf, TOOL_TABS, useAssignmentIdentity } from "./index.js";
20
20
 
21
21
  import { TransferIcon } from "@sanity/icons/Transfer";
22
22
 
@@ -24,7 +24,7 @@ import { WarningOutlineIcon } from "@sanity/icons/WarningOutline";
24
24
 
25
25
  import { useSchema, useDocumentPreviewStore, usePerspective, useClient } from "sanity";
26
26
 
27
- import { entryDocRefs, toBareId, findOpenStageEntry, terminalState, findStageNode, startKindOf, isSubjectEntry, isInputSourced, latestDeployedDefinitions, isStartableDefinition } from "@sanity/workflow-engine";
27
+ import { entryDocRefs, toBareId, findOpenStageEntry, findStageNode, deriveWorkflowAutonomy, deriveExecutorClassification, terminalState, startKindOf, isSubjectEntry, isInputSourced, latestDeployedDefinitions, isStartableDefinition } from "@sanity/workflow-engine";
28
28
 
29
29
  import { useWorkflowInstancePreviews } from "@sanity/workflow-studio";
30
30
 
@@ -32,24 +32,26 @@ import pluralize from "pluralize-esm";
32
32
 
33
33
  import { useVirtualizer } from "@tanstack/react-virtual";
34
34
 
35
+ import { UserIcon } from "@sanity/icons/User";
36
+
37
+ import { RoleAvatar, memberRoleFor, MemberAvatar, roleLabel, roleLabelFor, AssigneeStack } from "@sanity/workflow-components";
38
+
39
+ import { CalendarIcon } from "@sanity/icons/Calendar";
40
+
35
41
  import { ErrorOutlineIcon } from "@sanity/icons/ErrorOutline";
36
42
 
37
43
  import { isReferenceSchemaType } from "@sanity/types";
38
44
 
39
45
  import { createPortal } from "react-dom";
40
46
 
41
- import { CloseIcon } from "@sanity/icons/Close";
42
-
43
- import { UserIcon } from "@sanity/icons/User";
47
+ import { styled } from "styled-components";
44
48
 
45
- import { RoleAvatar, MemberAvatar, roleLabelFor, AssigneeStack } from "@sanity/workflow-components";
49
+ import { CloseIcon } from "@sanity/icons/Close";
46
50
 
47
51
  import { AddIcon } from "@sanity/icons/Add";
48
52
 
49
53
  import { CheckmarkIcon } from "@sanity/icons/Checkmark";
50
54
 
51
- import { styled } from "styled-components";
52
-
53
55
  import { SearchIcon } from "@sanity/icons/Search";
54
56
 
55
57
  import { useReducedMotion, motion, useSpring, useMotionValueEvent } from "motion/react";
@@ -91,14 +93,17 @@ function overdueTasks(instance, now) {
91
93
  return openActivitiesOf(instance).filter(activity => dueDatesOf(activity.fields ?? []).some(due => isDueDatePast(due, now)));
92
94
  }
93
95
 
94
- function overdueTaskCount(instance, now) {
95
- return overdueTasks(instance, now).length;
96
+ function earliestPastDue(activity, now) {
97
+ return earliestPastDueAcross([ activity ], now);
96
98
  }
97
99
 
98
- function earliestPastDue(activity, now) {
99
- const past = dueDatesOf(activity.fields ?? []).filter(due => isDueDatePast(due, now));
100
+ function earliestPastDueAcross(activities, now) {
101
+ return earliestOf(activities.flatMap(activity => dueDatesOf(activity.fields ?? []).filter(due => isDueDatePast(due, now))));
102
+ }
103
+
104
+ function earliestOf(values) {
100
105
  let earliest;
101
- for (const value of past) {
106
+ for (const value of values) {
102
107
  const time = parseStoredDateValue(value)?.getTime();
103
108
  time !== void 0 && (earliest === void 0 || time < earliest.time) && (earliest = {
104
109
  time: time,
@@ -108,6 +113,135 @@ function earliestPastDue(activity, now) {
108
113
  return earliest?.value;
109
114
  }
110
115
 
116
+ const AUTONOMY_BY_DEFINITION = /* @__PURE__ */ new WeakMap;
117
+
118
+ function cachedAutonomy(definition) {
119
+ const hit = AUTONOMY_BY_DEFINITION.get(definition);
120
+ if (hit !== void 0) return hit;
121
+ const derived = deriveWorkflowAutonomy(definition);
122
+ return AUTONOMY_BY_DEFINITION.set(definition, derived), derived;
123
+ }
124
+
125
+ function isAutomatedActivity(args) {
126
+ const stage = args.autonomy?.stages.find(entry => entry.stage === args.stageName);
127
+ return isHandsOff({
128
+ classification: deriveExecutorClassification(args.activity),
129
+ completesWithoutCaller: stage?.activities[args.activity.name]?.completesWithoutCaller
130
+ });
131
+ }
132
+
133
+ function taskOf(args) {
134
+ const {entry: entry, now: now, title: title} = args;
135
+ return {
136
+ name: entry.name,
137
+ title: title,
138
+ assignees: assigneesOf(entry.fields ?? []),
139
+ overdueSince: earliestPastDue(entry, now)
140
+ };
141
+ }
142
+
143
+ function humanTask(args) {
144
+ const {activity: activity, autonomy: autonomy, entry: entry, now: now, stageName: stageName} = args;
145
+ return entry === void 0 ? [] : isAutomatedActivity({
146
+ activity: activity,
147
+ autonomy: autonomy,
148
+ stageName: stageName
149
+ }) ? [] : [ taskOf({
150
+ entry: entry,
151
+ now: now,
152
+ title: activityLabel(activity)
153
+ }) ];
154
+ }
155
+
156
+ function undeclaredTasks(args) {
157
+ const {definition: definition, now: now, open: open, seen: seen, stageName: stageName} = args;
158
+ return open.filter(entry => !seen.has(entry.name)).map(entry => taskOf({
159
+ entry: entry,
160
+ now: now,
161
+ title: findActivity({
162
+ activityName: entry.name,
163
+ definition: definition,
164
+ stageName: stageName
165
+ }).title
166
+ }));
167
+ }
168
+
169
+ function autonomyForPinned(definition, pinnedVersion) {
170
+ if (!(definition === void 0 || definition.version !== pinnedVersion)) return cachedAutonomy(definition);
171
+ }
172
+
173
+ function stageTasksOf(args) {
174
+ const {definition: definition, instance: instance, now: now} = args, open = openActivitiesOf(instance);
175
+ if (open.length === 0) return [];
176
+ const autonomy = autonomyForPinned(definition, instance.pinnedVersion), stageName = instance.currentStage, openByName = new Map(open.map(entry => [ entry.name, entry ])), declared = findStageNode({
177
+ definition: definition,
178
+ stageName: stageName
179
+ })?.activities ?? [];
180
+ return [ ...declared.flatMap(activity => humanTask({
181
+ activity: activity,
182
+ autonomy: autonomy,
183
+ entry: openByName.get(activity.name),
184
+ now: now,
185
+ stageName: stageName
186
+ })), ...undeclaredTasks({
187
+ definition: definition,
188
+ now: now,
189
+ open: open,
190
+ seen: new Set(declared.map(activity => activity.name)),
191
+ stageName: stageName
192
+ }) ];
193
+ }
194
+
195
+ function groupWidthOf(task, metrics) {
196
+ const stacked = Math.max(1, task.assignees.length);
197
+ return metrics.face + (stacked - 1) * (metrics.face - metrics.overlap);
198
+ }
199
+
200
+ function rowWidthOf(tasks, metrics) {
201
+ return tasks.reduce((total, task) => total + groupWidthOf(task, metrics), 0) + Math.max(0, tasks.length - 1) * metrics.gap;
202
+ }
203
+
204
+ function countWithin(args) {
205
+ const {budget: budget, metrics: metrics, tasks: tasks} = args;
206
+ let used = 0, count = 0;
207
+ for (const task of tasks) {
208
+ const next = used + (count === 0 ? 0 : metrics.gap) + groupWidthOf(task, metrics);
209
+ if (next > budget) break;
210
+ used = next, count += 1;
211
+ }
212
+ return Math.max(1, count);
213
+ }
214
+
215
+ function visibleStageTasks(args) {
216
+ const {metrics: metrics, room: room, tasks: tasks} = args;
217
+ if (rowWidthOf(tasks, metrics) <= room) return {
218
+ overflow: 0,
219
+ visible: tasks
220
+ };
221
+ const shown = countWithin({
222
+ budget: room - metrics.gap - metrics.counter,
223
+ metrics: metrics,
224
+ tasks: tasks
225
+ });
226
+ return {
227
+ overflow: tasks.length - shown,
228
+ visible: tasks.slice(0, shown)
229
+ };
230
+ }
231
+
232
+ function stackedAssigneesOf(assignees) {
233
+ return [ ...assignees.filter(assignee => assignee.type === "role"), ...assignees.filter(assignee => assignee.type === "user") ];
234
+ }
235
+
236
+ function taskHoldersOf(assignees) {
237
+ return assignees.length === 0 ? [ {
238
+ kind: "unassigned"
239
+ } ] : stackedAssigneesOf(assignees).map(assignee => ({
240
+ kind: "held",
241
+ assignee: assignee
242
+ }));
243
+ }
244
+
111
245
  function runsScopeOf(row) {
112
246
  return row.endedAt === void 0 ? "open" : "closed";
113
247
  }
@@ -125,7 +259,7 @@ function runCountsByWorkflow(rows) {
125
259
  }
126
260
 
127
261
  function runCountsByAssignee(rows) {
128
- return countBy(rows, row => new Set(row.assignees.flatMap(assignee => assignee.type === "user" ? [ assignee.id ] : [])));
262
+ return countBy(rows, row => new Set(userIdsOf(row.assignees)));
129
263
  }
130
264
 
131
265
  function runCountsByRole(rows) {
@@ -194,12 +328,17 @@ function runRowOf(args) {
194
328
  attention: attentionKindOf(attentionDetail),
195
329
  attentionDetail: attentionDetail,
196
330
  assignees: held.assignees,
197
- unassignedOpenCount: held.unassignedCount
331
+ unassignedOpenCount: held.unassignedCount,
332
+ tasks: settled ? [] : stageTasksOf({
333
+ definition: definition,
334
+ instance: preview,
335
+ now: now
336
+ })
198
337
  };
199
338
  }
200
339
 
201
340
  function attentionKindOf(detail) {
202
- if (detail !== void 0) return detail.kind === "overdue" || detail.kind === "overdue-many" ? "overdue" : "blocked";
341
+ if (detail !== void 0) return detail.kind === "overdue" ? "overdue" : "blocked";
203
342
  }
204
343
 
205
344
  function outcomeOf(preview, definition) {
@@ -210,6 +349,7 @@ function outcomeOf(preview, definition) {
210
349
  function blockedRunDetail(args) {
211
350
  const {definition: definition, preview: preview} = args, {stage: stage, titleOf: titleOf} = activityTitleResolver(preview, definition), failed = (stage?.activities ?? []).filter(activity => activity.status === "failed");
212
351
  if (failed.length > 1) return {
352
+ count: failed.length,
213
353
  kind: "blocked-many"
214
354
  };
215
355
  const only = failed[0];
@@ -217,19 +357,29 @@ function blockedRunDetail(args) {
217
357
  kind: "blocked",
218
358
  task: titleOf(only.name)
219
359
  };
220
- const effect = stuckEffectOf({
360
+ const stuck = stuckEffectOf({
221
361
  definition: definition,
222
362
  preview: preview,
223
363
  stage: stage
224
364
  });
225
- return effect === void 0 ? void 0 : {
226
- effect: effect.title ?? effect.name,
365
+ return stuck === void 0 ? void 0 : {
366
+ effect: stuck.effect.title ?? stuck.effect.name,
367
+ failed: stuck.failed,
227
368
  kind: "blocked-effect"
228
369
  };
229
370
  }
230
371
 
231
372
  function stuckEffectOf(args) {
232
- return failedDeclaredEffect(args) ?? args.preview.claimedEffects[0];
373
+ const reported = failedDeclaredEffect(args);
374
+ if (reported !== void 0) return {
375
+ effect: reported,
376
+ failed: !0
377
+ };
378
+ const claimed = args.preview.claimedEffects[0];
379
+ return claimed === void 0 ? void 0 : {
380
+ effect: claimed,
381
+ failed: !1
382
+ };
233
383
  }
234
384
 
235
385
  function failedDeclaredEffect(args) {
@@ -245,13 +395,12 @@ function failedDeclaredEffect(args) {
245
395
  function overdueRunDetail(args) {
246
396
  const {definition: definition, now: now, preview: preview} = args, late = overdueTasks(preview, now);
247
397
  if (late.length === 0) return;
248
- const {titleOf: titleOf} = activityTitleResolver(preview, definition), only = late.length === 1 ? late[0] : void 0, due = only === void 0 ? void 0 : earliestPastDue(only, now);
249
- return only === void 0 || due === void 0 ? {
250
- kind: "overdue-many"
251
- } : {
252
- due: due,
398
+ const {titleOf: titleOf} = activityTitleResolver(preview, definition), only = late.length === 1 ? late[0] : void 0;
399
+ return {
400
+ count: late.length,
401
+ due: earliestPastDueAcross(late, now),
253
402
  kind: "overdue",
254
- task: titleOf(only.name)
403
+ task: only === void 0 ? void 0 : titleOf(only.name)
255
404
  };
256
405
  }
257
406
 
@@ -272,10 +421,6 @@ const NO_ASSIGNEES = {
272
421
  unassignedCount: 0
273
422
  };
274
423
 
275
- function openRunAssignees(preview) {
276
- return openAssignees(preview).assignees;
277
- }
278
-
279
424
  function openAssignees(instance) {
280
425
  const users = /* @__PURE__ */ new Map, roles = /* @__PURE__ */ new Map;
281
426
  let unassignedCount = 0;
@@ -447,7 +592,7 @@ function isRunAttention(value) {
447
592
  }
448
593
 
449
594
  function assigneeIdsOf(rows) {
450
- const ids = rows.flatMap(row => row.assignees.flatMap(assignee => assignee.type === "user" ? [ assignee.id ] : []));
595
+ const ids = rows.flatMap(row => userIdsOf(row.assignees));
451
596
  return [ ...new Set(ids) ];
452
597
  }
453
598
 
@@ -1272,20 +1417,26 @@ function tiebreak(a, b) {
1272
1417
  }
1273
1418
 
1274
1419
  function cardFacts(args) {
1275
- return new Map(args.previews.map(preview => [ preview._id, {
1276
- assignees: openRunAssignees(preview),
1277
- overdue: overdueTaskCount(preview, args.now),
1278
- overdueDetail: overdueRunDetail({
1279
- definition: args.definition,
1280
- now: args.now,
1281
- preview: preview
1282
- }),
1283
- blocked: blockedRunDetail({
1284
- definition: args.definition,
1285
- preview: preview
1286
- }),
1287
- settled: terminalState(preview) !== "in-flight"
1288
- } ]));
1420
+ return new Map(args.previews.map(preview => {
1421
+ const settled = terminalState(preview) !== "in-flight";
1422
+ return [ preview._id, {
1423
+ tasks: settled ? [] : stageTasksOf({
1424
+ definition: args.definition,
1425
+ instance: preview,
1426
+ now: args.now
1427
+ }),
1428
+ overdueDetail: overdueRunDetail({
1429
+ definition: args.definition,
1430
+ now: args.now,
1431
+ preview: preview
1432
+ }),
1433
+ blocked: blockedRunDetail({
1434
+ definition: args.definition,
1435
+ preview: preview
1436
+ }),
1437
+ settled: settled
1438
+ } ];
1439
+ }));
1289
1440
  }
1290
1441
 
1291
1442
  function laneTotals(cards, facts) {
@@ -1294,7 +1445,7 @@ function laneTotals(cards, facts) {
1294
1445
  const held = facts.get(card.instanceId);
1295
1446
  if (held === void 0) continue;
1296
1447
  const key = card.face.kind === "document" ? card.face.document.id : `run:${card.instanceId}`;
1297
- held.overdue > 0 && overdue.add(key), held.blocked !== void 0 && stuck.add(key);
1448
+ held.overdueDetail !== void 0 && overdue.add(key), held.blocked !== void 0 && stuck.add(key);
1298
1449
  }
1299
1450
  return {
1300
1451
  overdue: overdue.size,
@@ -1360,6 +1511,237 @@ function boardDocuments(board) {
1360
1511
  return board.groups.flatMap(group => group.cards.flatMap(card => card.face.kind === "document" ? [ card.face.document ] : []));
1361
1512
  }
1362
1513
 
1514
+ const LATE_FACE = {
1515
+ Icon: CalendarIcon,
1516
+ tone: "caution"
1517
+ };
1518
+
1519
+ function LateSpan({now: now, since: since}) {
1520
+ /* @__PURE__ */
1521
+ return jsx(MetricLockup, {
1522
+ Icon: LATE_FACE.Icon,
1523
+ tone: LATE_FACE.tone,
1524
+ value: shortSpanOf(parseStoredDateValue(since), now)
1525
+ });
1526
+ }
1527
+
1528
+ const HOLDER_OVERLAP = 7, HOLDER_RING = 1.5, GROUP_GAP_TOKEN = 3;
1529
+
1530
+ function useDisplayById(ids) {
1531
+ const displays = useUserDisplays(uniqueIds(ids)), labels = disambiguatedNames(displays);
1532
+ return new Map(displays.map((display, index) => [ display.id, {
1533
+ ...display,
1534
+ label: labels[index] ?? display.name
1535
+ } ]));
1536
+ }
1537
+
1538
+ function holderKey(assignee, index) {
1539
+ return assignee.type === "user" ? assignee.id : `${assignee.role}-${index}`;
1540
+ }
1541
+
1542
+ function stageTasksFace(args) {
1543
+ return {
1544
+ content: /* @__PURE__ */ jsx(StageTasksCard, {
1545
+ now: args.now,
1546
+ tasks: args.tasks
1547
+ }),
1548
+ flush: !0,
1549
+ wide: !0
1550
+ };
1551
+ }
1552
+
1553
+ function StageTaskGroups({now: now, room: room, tasks: tasks}) {
1554
+ const gap = useSpaceToken(GROUP_GAP_TOKEN), face = useAvatarSize(), {overflow: overflow, visible: visible} = visibleStageTasks({
1555
+ metrics: {
1556
+ counter: face,
1557
+ face: face,
1558
+ gap: gap,
1559
+ overlap: HOLDER_OVERLAP
1560
+ },
1561
+ room: room ?? face * 3 + gap * 2,
1562
+ tasks: tasks
1563
+ });
1564
+ return tasks.length === 0 ? null : /* @__PURE__ */ jsx(HoverHint, {
1565
+ ...stageTasksFace({
1566
+ now: now,
1567
+ tasks: tasks
1568
+ }),
1569
+ children: /* @__PURE__ */ jsxs(Flex, {
1570
+ align: "center",
1571
+ "aria-label": "Tasks",
1572
+ style: {
1573
+ columnGap: gap
1574
+ },
1575
+ children: [ visible.map(task => /* @__PURE__ */ jsx(TaskGroupFace, {
1576
+ assignees: task.assignees
1577
+ }, task.name)), overflow === 0 ? null : /* @__PURE__ */ jsxs(Text, {
1578
+ muted: !0,
1579
+ size: 1,
1580
+ children: [ "+", overflow ]
1581
+ }) ]
1582
+ })
1583
+ });
1584
+ }
1585
+
1586
+ function TaskGroupFace({assignees: assignees}) {
1587
+ return assignees.length === 0 ? /* @__PURE__ */ jsx(UnassignedMark, {}) : /* @__PURE__ */ jsx(OverlappedHolders, {
1588
+ assignees: assignees
1589
+ });
1590
+ }
1591
+
1592
+ function HolderFace({assignee: assignee, displayById: displayById}) {
1593
+ const vocabulary = useProjectMembers();
1594
+ if (assignee.type === "role") /* @__PURE__ */
1595
+ return jsx(RoleAvatar, {
1596
+ role: memberRoleFor(vocabulary, assignee.role),
1597
+ size: 0
1598
+ });
1599
+ const display = displayById.get(assignee.id);
1600
+ /* @__PURE__ */
1601
+ return jsx(MemberAvatar, {
1602
+ imageUrl: display?.imageUrl,
1603
+ name: display?.name ?? assignee.id,
1604
+ size: 0
1605
+ });
1606
+ }
1607
+
1608
+ function OverlappedHolders({assignees: assignees}) {
1609
+ const {color: color} = useTheme_v2(), stacked = stackedAssigneesOf(assignees), displayById = useDisplayById(userIdsOf(stacked)), ring = `0 0 0 ${HOLDER_RING}px ${color.bg}`;
1610
+ /* @__PURE__ */
1611
+ return jsx(Flex, {
1612
+ align: "center",
1613
+ style: {
1614
+ flexShrink: 0,
1615
+ isolation: "isolate"
1616
+ },
1617
+ children: stacked.map((assignee, index) => /* @__PURE__ */ jsx(Box, {
1618
+ style: {
1619
+ borderRadius: assignee.type === "role" ? void 0 : "50%",
1620
+ boxShadow: ring,
1621
+ flexShrink: 0,
1622
+ marginLeft: index === 0 ? 0 : -HOLDER_OVERLAP,
1623
+ position: "relative",
1624
+ zIndex: index
1625
+ },
1626
+ children: /* @__PURE__ */ jsx(HolderFace, {
1627
+ assignee: assignee,
1628
+ displayById: displayById
1629
+ })
1630
+ }, holderKey(assignee, index)))
1631
+ });
1632
+ }
1633
+
1634
+ function UnassignedMark() {
1635
+ const size = useAvatarSize(), {color: color} = useTheme_v2();
1636
+ /* @__PURE__ */
1637
+ return jsx(Flex, {
1638
+ align: "center",
1639
+ justify: "center",
1640
+ style: {
1641
+ border: `1px dashed ${color.border}`,
1642
+ borderRadius: "50%",
1643
+ boxSizing: "border-box",
1644
+ flexShrink: 0,
1645
+ height: size,
1646
+ width: size
1647
+ },
1648
+ children: /* @__PURE__ */ jsx(Text, {
1649
+ size: 0,
1650
+ children: /* @__PURE__ */ jsx(UserIcon, {
1651
+ style: {
1652
+ color: color.input.default.enabled.placeholder
1653
+ }
1654
+ })
1655
+ })
1656
+ });
1657
+ }
1658
+
1659
+ function StageTasksCard({now: now, tasks: tasks}) {
1660
+ const displayById = useDisplayById(tasks.flatMap(task => userIdsOf(task.assignees)));
1661
+ /* @__PURE__ */
1662
+ return jsxs(Stack, {
1663
+ gap: 4,
1664
+ padding: 3,
1665
+ children: [
1666
+ /* @__PURE__ */ jsx(Text, {
1667
+ muted: !0,
1668
+ size: 0,
1669
+ style: {
1670
+ letterSpacing: "0.06em",
1671
+ textTransform: "uppercase"
1672
+ },
1673
+ weight: "semibold",
1674
+ children: "Tasks in this stage"
1675
+ }),
1676
+ /* @__PURE__ */ jsx(Stack, {
1677
+ gap: 4,
1678
+ children: tasks.map(task => /* @__PURE__ */ jsx(TaskCardRow, {
1679
+ displayById: displayById,
1680
+ now: now,
1681
+ task: task
1682
+ }, task.name))
1683
+ }) ]
1684
+ });
1685
+ }
1686
+
1687
+ function TaskCardRow({displayById: displayById, now: now, task: task}) {
1688
+ /* @__PURE__ */
1689
+ return jsxs(Stack, {
1690
+ gap: 3,
1691
+ children: [
1692
+ /* @__PURE__ */ jsxs(Flex, {
1693
+ align: "center",
1694
+ gap: 3,
1695
+ children: [
1696
+ /* @__PURE__ */ jsx(Text, {
1697
+ size: 1,
1698
+ children: task.title
1699
+ }), task.overdueSince === void 0 ? null : /* @__PURE__ */ jsx(LateSpan, {
1700
+ now: now,
1701
+ since: task.overdueSince
1702
+ }) ]
1703
+ }),
1704
+ /* @__PURE__ */ jsx(Stack, {
1705
+ gap: 2,
1706
+ children: taskHoldersOf(task.assignees).map((holder, index) => /* @__PURE__ */ jsx(HolderLine, {
1707
+ displayById: displayById,
1708
+ holder: holder
1709
+ }, holder.kind === "held" ? holderKey(holder.assignee, index) : "unassigned"))
1710
+ }) ]
1711
+ });
1712
+ }
1713
+
1714
+ function HolderLine({displayById: displayById, holder: holder}) {
1715
+ const vocabulary = useProjectMembers();
1716
+ if (holder.kind === "unassigned") /* @__PURE__ */
1717
+ return jsxs(Flex, {
1718
+ align: "center",
1719
+ gap: 2,
1720
+ children: [
1721
+ /* @__PURE__ */ jsx(UnassignedMark, {}),
1722
+ /* @__PURE__ */ jsx(Text, {
1723
+ muted: !0,
1724
+ size: 1,
1725
+ children: "Unassigned"
1726
+ }) ]
1727
+ });
1728
+ const {assignee: assignee} = holder, label = assignee.type === "user" ? displayById.get(assignee.id)?.label ?? assignee.id : roleLabel(memberRoleFor(vocabulary, assignee.role));
1729
+ /* @__PURE__ */
1730
+ return jsxs(Flex, {
1731
+ align: "center",
1732
+ gap: 2,
1733
+ children: [
1734
+ /* @__PURE__ */ jsx(HolderFace, {
1735
+ assignee: assignee,
1736
+ displayById: displayById
1737
+ }),
1738
+ /* @__PURE__ */ jsx(Text, {
1739
+ size: 1,
1740
+ children: label
1741
+ }) ]
1742
+ });
1743
+ }
1744
+
1363
1745
  function overdueAttentionHint(documents) {
1364
1746
  const task = documents === 1 ? "an overdue task" : "overdue tasks";
1365
1747
  return `${pluralize("document", documents, !0)} ${documents === 1 ? "has" : "have"} ${task}`;
@@ -1369,32 +1751,69 @@ function blockedAttentionHint(documents) {
1369
1751
  return `${pluralize("document", documents, !0)} ${documents === 1 ? "is" : "are"} blocked`;
1370
1752
  }
1371
1753
 
1754
+ function overdueMark(detail, now) {
1755
+ const span = overdueSpan(detail.due, now), tasks = pluralize("task", detail.count, !0);
1756
+ return detail.count === 1 && detail.task !== void 0 ? {
1757
+ hint: `“${detail.task}” — ${span === void 0 ? "overdue" : `${span} overdue`}`,
1758
+ value: span ?? tasks
1759
+ } : {
1760
+ hint: span === void 0 ? `${tasks} overdue` : `${tasks} overdue — the longest by ${span}`,
1761
+ value: tasks
1762
+ };
1763
+ }
1764
+
1765
+ function overdueSpan(due, now) {
1766
+ const from = parseStoredDateValue(due);
1767
+ if (from !== void 0) return shortSpanOf(from, now);
1768
+ }
1769
+
1770
+ function overdueMarkOpensPanel(tasks) {
1771
+ return tasks.some(task => task.overdueSince !== void 0);
1772
+ }
1773
+
1774
+ function blockedMarkHint(detail) {
1775
+ return detail.kind === "blocked-many" ? `${pluralize("task", detail.count, !0)} were sent back` : detail.kind === "blocked-effect" ? `Automation “${detail.effect}” ${detail.failed ? "failed" : "didn’t finish"}` : detail.task === void 0 ? "A task was sent back" : `“${detail.task}” was sent back`;
1776
+ }
1777
+
1372
1778
  const ATTENTION_FACES = {
1373
1779
  blocked: {
1374
1780
  Icon: ErrorOutlineIcon,
1375
1781
  tone: "critical"
1376
1782
  },
1377
- overdue: {
1378
- Icon: WarningOutlineIcon,
1379
- tone: "caution"
1380
- }
1783
+ overdue: LATE_FACE
1381
1784
  };
1382
1785
 
1383
- function AttentionGlyph({detail: detail}) {
1384
- const overdue = detail.kind === "overdue" || detail.kind === "overdue-many", face = ATTENTION_FACES[overdue ? "overdue" : "blocked"], words = overdue ? overdueAttentionHint(1) : blockedAttentionHint(1);
1786
+ function AttentionMark({detail: detail, now: now, tasks: tasks}) {
1787
+ if (detail.kind === "overdue") {
1788
+ const {hint: hint2, value: value} = overdueMark(detail, now);
1789
+ /* @__PURE__ */
1790
+ return jsx(MetricPair, {
1791
+ Icon: ATTENTION_FACES.overdue.Icon,
1792
+ face: overdueMarkOpensPanel(tasks) ? stageTasksFace({
1793
+ now: now,
1794
+ tasks: tasks
1795
+ }) : {
1796
+ text: hint2
1797
+ },
1798
+ label: hint2,
1799
+ tone: ATTENTION_FACES.overdue.tone,
1800
+ value: value
1801
+ });
1802
+ }
1803
+ const hint = blockedMarkHint(detail);
1385
1804
  /* @__PURE__ */
1386
- return jsx(AlertGlyph, {
1387
- Icon: face.Icon,
1388
- hint: words,
1389
- label: words,
1390
- tone: face.tone
1805
+ return jsx(AlertChip, {
1806
+ Icon: ATTENTION_FACES.blocked.Icon,
1807
+ hint: hint,
1808
+ label: hint,
1809
+ tone: ATTENTION_FACES.blocked.tone
1391
1810
  });
1392
1811
  }
1393
1812
 
1394
1813
  const CARD_RADIUS = 3;
1395
1814
 
1396
1815
  function BoardCard({card: card, wiring: wiring}) {
1397
- const {facts: facts, onSelect: onSelect, selectedInstanceId: selectedInstanceId} = wiring, {color: color} = useTheme_v2(), faceInset = useSpaceToken(2), selected = card.instanceId === selectedInstanceId, held = facts.get(card.instanceId);
1816
+ const {facts: facts, now: now, onSelect: onSelect, selectedInstanceId: selectedInstanceId} = wiring, {color: color} = useTheme_v2(), faceInset = useSpaceToken(2), selected = card.instanceId === selectedInstanceId, held = facts.get(card.instanceId);
1398
1817
  /* @__PURE__ */
1399
1818
  return jsx(Card, {
1400
1819
  "aria-pressed": selected,
@@ -1430,22 +1849,24 @@ function BoardCard({card: card, wiring: wiring}) {
1430
1849
  })
1431
1850
  })
1432
1851
  }), held === void 0 ? null : /* @__PURE__ */ jsx(CardMetrics, {
1433
- facts: held
1852
+ facts: held,
1853
+ now: now
1434
1854
  }) ]
1435
1855
  })
1436
1856
  });
1437
1857
  }
1438
1858
 
1439
- function CardMetrics({facts: facts}) {
1440
- const markOverhang = -useCapTrimFor(useTextIconSize()), bare = facts.assignees.length === 0 && facts.overdueDetail === void 0 && facts.blocked === void 0;
1859
+ function CardMetrics({facts: facts, now: now}) {
1860
+ const markOverhang = -useCapTrimFor(useTextIconSize()), bare = facts.tasks.length === 0 && facts.overdueDetail === void 0 && facts.blocked === void 0;
1441
1861
  return facts.settled || bare ? null : /* @__PURE__ */ jsx(Box, {
1442
1862
  padding: 1,
1443
1863
  children: /* @__PURE__ */ jsxs(Flex, {
1444
1864
  align: "center",
1445
1865
  gap: 3,
1446
1866
  children: [
1447
- /* @__PURE__ */ jsx(RowAssignees, {
1448
- assignees: facts.assignees
1867
+ /* @__PURE__ */ jsx(StageTaskGroups, {
1868
+ now: now,
1869
+ tasks: facts.tasks
1449
1870
  }),
1450
1871
  /* @__PURE__ */ jsxs(Flex, {
1451
1872
  align: "center",
@@ -1455,10 +1876,14 @@ function CardMetrics({facts: facts}) {
1455
1876
  style: {
1456
1877
  paddingRight: markOverhang
1457
1878
  },
1458
- children: [ facts.overdueDetail === void 0 ? null : /* @__PURE__ */ jsx(AttentionGlyph, {
1459
- detail: facts.overdueDetail
1460
- }), facts.blocked === void 0 ? null : /* @__PURE__ */ jsx(AttentionGlyph, {
1461
- detail: facts.blocked
1879
+ children: [ facts.overdueDetail === void 0 ? null : /* @__PURE__ */ jsx(AttentionMark, {
1880
+ detail: facts.overdueDetail,
1881
+ now: now,
1882
+ tasks: facts.tasks
1883
+ }), facts.blocked === void 0 ? null : /* @__PURE__ */ jsx(AttentionMark, {
1884
+ detail: facts.blocked,
1885
+ now: now,
1886
+ tasks: facts.tasks
1462
1887
  }) ]
1463
1888
  }) ]
1464
1889
  })
@@ -1959,6 +2384,7 @@ function RunsBoard({onSelect: onSelect, panelOpen: panelOpen, selectedInstanceId
1959
2384
  now: now,
1960
2385
  previews: shown
1961
2386
  }),
2387
+ now: now,
1962
2388
  onSelect: onSelect,
1963
2389
  selectedInstanceId: selectedInstanceId
1964
2390
  }
@@ -2891,6 +3317,12 @@ function SegmentDivider() {
2891
3317
  });
2892
3318
  }
2893
3319
 
3320
+ const SurfaceButton = styled(Button)`
3321
+ &:not(:hover):not(:active):not([data-selected]) {
3322
+ --card-bg-color: ${({$surface: $surface}) => $surface};
3323
+ }
3324
+ `;
3325
+
2894
3326
  function RunsFilterRow({boardMode: boardMode, definition: definition, definitions: definitions, displayToggle: displayToggle, onView: onView, ready: ready, scoped: scoped, slot: slot, view: view, workflowCounts: workflowCounts, workflowScoped: workflowScoped}) {
2895
3327
  return ready ? slot === null ? null : createPortal(
2896
3328
  /* @__PURE__ */ jsxs(Flex, {
@@ -2900,9 +3332,7 @@ function RunsFilterRow({boardMode: boardMode, definition: definition, definition
2900
3332
  children: [
2901
3333
  /* @__PURE__ */ jsxs(Flex, {
2902
3334
  align: "center",
2903
- flex: 1,
2904
3335
  gap: 2,
2905
- wrap: "wrap",
2906
3336
  children: [
2907
3337
  /* @__PURE__ */ jsx(WorkflowSelect, {
2908
3338
  counts: workflowCounts,
@@ -2913,7 +3343,15 @@ function RunsFilterRow({boardMode: boardMode, definition: definition, definition
2913
3343
  }), boardMode ? null : /* @__PURE__ */ jsx(ScopeSelect, {
2914
3344
  onView: onView,
2915
3345
  view: view
2916
- }),
3346
+ }) ]
3347
+ }),
3348
+ /* @__PURE__ */ jsxs(Flex, {
3349
+ align: "center",
3350
+ flex: 1,
3351
+ gap: 2,
3352
+ justify: "flex-end",
3353
+ wrap: "wrap",
3354
+ children: [
2917
3355
  /* @__PURE__ */ jsx(RunsFilterChips, {
2918
3356
  definition: definition,
2919
3357
  onView: onView,
@@ -3028,13 +3466,14 @@ function PickerLineGhost() {
3028
3466
  }
3029
3467
 
3030
3468
  function ScopeSelect({onView: onView, view: view}) {
3031
- const pick = scope => onView({
3469
+ const {color: color} = useTheme_v2(), pick = scope => onView({
3032
3470
  scope: scope,
3033
3471
  filters: view.filters
3034
3472
  });
3035
3473
  /* @__PURE__ */
3036
3474
  return jsx(MenuButton, {
3037
- button: /* @__PURE__ */ jsx(Button, {
3475
+ button: /* @__PURE__ */ jsx(SurfaceButton, {
3476
+ $surface: color.bg,
3038
3477
  fontSize: 1,
3039
3478
  iconRight: ChevronDownIcon,
3040
3479
  mode: "ghost",
@@ -4076,7 +4515,17 @@ const TIME_WIDTH = 112, WORKFLOW_WIDTH = {
4076
4515
  key: "ended",
4077
4516
  label: "Closed",
4078
4517
  width: TIME_WIDTH
4079
- } ], ASSIGNEES_WIDTH = 90, ATTENTION_WIDTH = 48;
4518
+ } ], TASKS_WIDTH = 120, TASKS_WIDTH_ROOMY = 168, ROOMY_TABLE_WIDTH = 1040;
4519
+
4520
+ function tasksWidthOf(tableWidth) {
4521
+ return tableWidth === void 0 ? TASKS_WIDTH : tableWidth >= ROOMY_TABLE_WIDTH ? TASKS_WIDTH_ROOMY : TASKS_WIDTH;
4522
+ }
4523
+
4524
+ function taskRoomOf(args) {
4525
+ return tasksWidthOf(args.tableWidth) - args.inset * 2;
4526
+ }
4527
+
4528
+ const ATTENTION_WIDTH = 96;
4080
4529
 
4081
4530
  function useRowWindow(args) {
4082
4531
  const {element: element, keyAt: keyAt, rowCount: rowCount} = args, [scroller, setScroller] = useState(null);
@@ -4122,7 +4571,7 @@ function SpacerRow({height: height}) {
4122
4571
  function documentWidthOf(args) {
4123
4572
  const {columns: columns, scope: scope, tableWidth: tableWidth} = args;
4124
4573
  if (tableWidth === void 0) return;
4125
- const trailing = scope === "open" ? ASSIGNEES_WIDTH + ATTENTION_WIDTH : 0, fixed = columns.reduce((sum, column) => {
4574
+ const trailing = scope === "open" ? tasksWidthOf(tableWidth) + ATTENTION_WIDTH : 0, fixed = columns.reduce((sum, column) => {
4126
4575
  const width = resolveWidth(column.width, tableWidth);
4127
4576
  return typeof width == "number" ? sum + width : sum;
4128
4577
  }, trailing);
@@ -4130,7 +4579,7 @@ function documentWidthOf(args) {
4130
4579
  }
4131
4580
 
4132
4581
  function RunsTable({now: now, onSelectRun: onSelectRun, onSort: onSort, rows: rows, scope: scope, scopedToWorkflow: scopedToWorkflow, selectedId: selectedId, sort: sort}) {
4133
- const columns = (scope === "closed" ? CLOSED_COLUMNS : OPEN_COLUMNS).filter(column => !(scopedToWorkflow && column.key === "workflow")), styles = useTableStyles(), [element, setElement] = useState(null), rect = useElementRect(element), window2 = useRowWindow({
4582
+ const columns = (scope === "closed" ? CLOSED_COLUMNS : OPEN_COLUMNS).filter(column => !(scopedToWorkflow && column.key === "workflow")), styles = useTableStyles(), cellInset = useSpaceToken(2), [element, setElement] = useState(null), rect = useElementRect(element), window2 = useRowWindow({
4134
4583
  element: element,
4135
4584
  keyAt: index => rows[index]?.id ?? index,
4136
4585
  rowCount: rows.length
@@ -4179,14 +4628,14 @@ function RunsTable({now: now, onSelectRun: onSelectRun, onSort: onSort, rows: ro
4179
4628
  /* @__PURE__ */ jsx("th", {
4180
4629
  style: {
4181
4630
  ...styles.head,
4182
- width: ASSIGNEES_WIDTH
4631
+ width: tasksWidthOf(rect?.width)
4183
4632
  },
4184
4633
  children: /* @__PURE__ */ jsx(Box, {
4185
4634
  padding: 2,
4186
4635
  children: /* @__PURE__ */ jsx(Text, {
4187
4636
  muted: !0,
4188
4637
  size: 1,
4189
- children: "Assignees"
4638
+ children: "Tasks"
4190
4639
  })
4191
4640
  })
4192
4641
  }),
@@ -4214,7 +4663,11 @@ function RunsTable({now: now, onSelectRun: onSelectRun, onSort: onSort, rows: ro
4214
4663
  row: row,
4215
4664
  scope: scope,
4216
4665
  scopedToWorkflow: scopedToWorkflow,
4217
- selected: row.id === selectedId
4666
+ selected: row.id === selectedId,
4667
+ taskRoom: taskRoomOf({
4668
+ inset: cellInset,
4669
+ tableWidth: rect?.width
4670
+ })
4218
4671
  }, item.key);
4219
4672
  }),
4220
4673
  /* @__PURE__ */ jsx(SpacerRow, {
@@ -4225,7 +4678,7 @@ function RunsTable({now: now, onSelectRun: onSelectRun, onSort: onSort, rows: ro
4225
4678
  });
4226
4679
  }
4227
4680
 
4228
- const RunTableRow = memo(function({dataIndex: dataIndex, measureRow: measureRow, now: now, onSelectRun: onSelectRun, row: row, scope: scope, scopedToWorkflow: scopedToWorkflow, selected: selected}) {
4681
+ const RunTableRow = memo(function({dataIndex: dataIndex, measureRow: measureRow, now: now, onSelectRun: onSelectRun, row: row, scope: scope, scopedToWorkflow: scopedToWorkflow, selected: selected, taskRoom: taskRoom}) {
4229
4682
  const styles = useTableStyles(), documentInset = useSpaceToken(1);
4230
4683
  /* @__PURE__ */
4231
4684
  return jsxs(TableRowCard, {
@@ -4255,7 +4708,8 @@ const RunTableRow = memo(function({dataIndex: dataIndex, measureRow: measureRow,
4255
4708
  row: row
4256
4709
  }) : /* @__PURE__ */ jsx(OpenCells, {
4257
4710
  now: now,
4258
- row: row
4711
+ row: row,
4712
+ taskRoom: taskRoom
4259
4713
  }) ]
4260
4714
  });
4261
4715
  });
@@ -4281,7 +4735,7 @@ function DocumentCell({row: row}) {
4281
4735
  });
4282
4736
  }
4283
4737
 
4284
- function OpenCells({now: now, row: row}) {
4738
+ function OpenCells({now: now, row: row, taskRoom: taskRoom}) {
4285
4739
  const styles = useTableStyles();
4286
4740
  /* @__PURE__ */
4287
4741
  return jsxs(Fragment, {
@@ -4302,17 +4756,23 @@ function OpenCells({now: now, row: row}) {
4302
4756
  })
4303
4757
  }),
4304
4758
  /* @__PURE__ */ jsx("td", {
4305
- style: styles.cell,
4759
+ style: {
4760
+ ...styles.cell,
4761
+ overflow: "hidden"
4762
+ },
4306
4763
  children: /* @__PURE__ */ jsx(Flex, {
4307
4764
  align: "center",
4308
- children: /* @__PURE__ */ jsx(RowAssignees, {
4309
- assignees: row.assignees
4765
+ children: /* @__PURE__ */ jsx(StageTaskGroups, {
4766
+ now: now,
4767
+ room: taskRoom,
4768
+ tasks: row.tasks
4310
4769
  })
4311
4770
  })
4312
4771
  }),
4313
4772
  /* @__PURE__ */ jsx("td", {
4314
4773
  style: styles.cell,
4315
- children: /* @__PURE__ */ jsx(AttentionMark, {
4774
+ children: /* @__PURE__ */ jsx(AttentionCell, {
4775
+ now: now,
4316
4776
  row: row
4317
4777
  })
4318
4778
  }) ]
@@ -4349,12 +4809,14 @@ function ClosedCells({now: now, row: row}) {
4349
4809
  });
4350
4810
  }
4351
4811
 
4352
- function AttentionMark({row: row}) {
4812
+ function AttentionCell({now: now, row: row}) {
4353
4813
  return row.attentionDetail === void 0 ? null :
4354
4814
  /* @__PURE__ */ jsx(Flex, {
4355
4815
  align: "center",
4356
- children: /* @__PURE__ */ jsx(AttentionGlyph, {
4357
- detail: row.attentionDetail
4816
+ children: /* @__PURE__ */ jsx(AttentionMark, {
4817
+ detail: row.attentionDetail,
4818
+ now: now,
4819
+ tasks: row.tasks
4358
4820
  })
4359
4821
  });
4360
4822
  }
@@ -5145,7 +5607,7 @@ function StatusBox({children: children}) {
5145
5607
  });
5146
5608
  }
5147
5609
 
5148
- const EDITORIAL_WORKFLOWS_DOCS_URL = "https://www.sanity.io/docs/editorial-workflows", WORK_TABS = TOOL_TABS.filter(tab => tab.value !== "definitions");
5610
+ const WORKFLOWS_DOCS_URL = "https://www.sanity.io/docs/workflows", WORK_TABS = TOOL_TABS.filter(tab => tab.value !== "definitions");
5149
5611
 
5150
5612
  function WorkflowsToolRoot() {
5151
5613
  const router = useRouter(), telemetry = useWorkflowTelemetry(), {color: color} = useTheme_v2(), [filterSlot, setFilterSlot] = useState(null), route = toolRoute(router.state), previews = useToolPreviews(), setTab = next => router.navigate(toolTabState(next)), selectTab = next => {
@@ -5260,7 +5722,7 @@ function ToolOverflowMenu({onOpenDefinitions: onOpenDefinitions}) {
5260
5722
  /* @__PURE__ */ jsx(MenuItem, {
5261
5723
  as: "a",
5262
5724
  fontSize: 1,
5263
- href: EDITORIAL_WORKFLOWS_DOCS_URL,
5725
+ href: WORKFLOWS_DOCS_URL,
5264
5726
  iconRight: LaunchIcon,
5265
5727
  rel: "noopener noreferrer",
5266
5728
  target: "_blank",