@cat-factory/app 0.313.0 → 0.315.1
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.
- package/README.md +7 -0
- package/app/components/board/AddTaskModal.vue +29 -30
- package/app/components/board/ReviewFrictionDialog.vue +8 -5
- package/app/components/layout/NotificationsInbox.vue +62 -129
- package/app/components/pipeline/PipelinePicker.vue +8 -2
- package/app/components/queue/DescribeWorkModal.vue +367 -0
- package/app/components/queue/QueueView.vue +18 -0
- package/app/components/settings/ApiTokensPanel.vue +86 -7
- package/app/composables/useNavContributions.ts +1 -0
- package/app/composables/usePipelineErrorToast.ts +4 -0
- package/app/composables/useTaskActions.ts +51 -28
- package/app/modular/nav-contributions.spec.ts +1 -0
- package/app/modular/nav-contributions.ts +21 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/publicApiKeys.spec.ts +28 -0
- package/app/stores/publicApiKeys.ts +17 -3
- package/app/stores/ui/modals.ts +28 -0
- package/app/utils/describeWork.spec.ts +76 -0
- package/app/utils/describeWork.ts +71 -0
- package/app/utils/notificationRoutes.spec.ts +81 -0
- package/app/utils/notificationRoutes.ts +179 -0
- package/app/utils/reviewFriction.ts +47 -0
- package/i18n/locales/de.json +50 -7
- package/i18n/locales/en.json +50 -7
- package/i18n/locales/es.json +50 -7
- package/i18n/locales/fr.json +50 -7
- package/i18n/locales/he.json +50 -7
- package/i18n/locales/it.json +50 -7
- package/i18n/locales/ja.json +50 -7
- package/i18n/locales/pl.json +50 -7
- package/i18n/locales/tr.json +50 -7
- package/i18n/locales/uk.json +50 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1492,6 +1492,13 @@ example ships in [`deploy/frontend`](https://github.com/kibertoad/cat-factory/tr
|
|
|
1492
1492
|
switchers, the `SpendWarningBanner`, and the toolbar (zoom, LOD, decision queue).
|
|
1493
1493
|
- **Palettes** (`components/palettes`): drag blocks, pipelines and agents onto
|
|
1494
1494
|
the board.
|
|
1495
|
+
- **Describe new work** (`components/queue/DescribeWorkModal.vue`, `utils/describeWork.ts`): the
|
|
1496
|
+
one-sentence intake, from Create, the palette and the queue header. It files a feature or a bug
|
|
1497
|
+
from a sentence and a service, titled by the first sentence; the overrides sit behind one
|
|
1498
|
+
disclosure, and the merge policy and model preset follow the full form's tier rule
|
|
1499
|
+
(`showOverrideField`), so basic mode and the designer role see the pipeline only.
|
|
1500
|
+
It rebuilds no other kind: "More kinds" and "Attach context" hand the text to `AddTaskModal`,
|
|
1501
|
+
which stays the one home of those rules. Both share `utils/reviewFriction.ts` for the review-debt 409.
|
|
1495
1502
|
- **Setup checklist** (`components/setup/SetupChecklistPanel.vue`, `utils/setupChecklist.ts`):
|
|
1496
1503
|
every prerequisite for "a task can run and merge" in one list, for people who can change the
|
|
1497
1504
|
setup. It adds no probe of its own: each row reads what its banner or startup dialog already
|
|
@@ -36,6 +36,7 @@ import FragmentSelector from '~/components/fragments/FragmentSelector.vue'
|
|
|
36
36
|
import ReviewSkillQueue from '~/components/skills/ReviewSkillQueue.vue'
|
|
37
37
|
import RiskPolicyPicker from '~/components/riskPolicy/RiskPolicyPicker.vue'
|
|
38
38
|
import { parseConflict } from '~/composables/usePipelineErrorToast'
|
|
39
|
+
import { isReviewDebtConflict, reviewFrictionContext } from '~/utils/reviewFriction'
|
|
39
40
|
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
40
41
|
import type { ReviewTargetReason } from '@cat-factory/contracts'
|
|
41
42
|
import {
|
|
@@ -526,6 +527,8 @@ function defaultPipelineIdFor(type: TaskTypeChoice): string {
|
|
|
526
527
|
return offered ? preset : ''
|
|
527
528
|
}
|
|
528
529
|
|
|
530
|
+
// Set while a prefill's type is being seeded with a carried pipeline (see `onModalOpen` below).
|
|
531
|
+
let seedingPrefill = false
|
|
529
532
|
watch(taskType, (next) => {
|
|
530
533
|
const custom = customTaskTypes.value.find((tt) => tt.taskType === next)
|
|
531
534
|
// A custom type owns a fresh field bag on every switch (its descriptors differ per type), seeded
|
|
@@ -533,6 +536,10 @@ watch(taskType, (next) => {
|
|
|
533
536
|
customFieldValues.value = descriptorFieldDefaults(custom?.fields ?? [])
|
|
534
537
|
// An unresolvable preset leaves the current selection alone rather than blanking it: a type
|
|
535
538
|
// switch is an edit to a form the user is already filling in, not a reset.
|
|
539
|
+
if (seedingPrefill) {
|
|
540
|
+
seedingPrefill = false
|
|
541
|
+
return
|
|
542
|
+
}
|
|
536
543
|
const preset = defaultPipelineIdFor(next)
|
|
537
544
|
if (preset) pipelineId.value = preset
|
|
538
545
|
})
|
|
@@ -667,13 +674,23 @@ onModalOpen(open, () => {
|
|
|
667
674
|
agentConfigValues.value = {}
|
|
668
675
|
pendingContext.value = []
|
|
669
676
|
// Seed from a prefill when opened from another surface (e.g. "create task from
|
|
670
|
-
// issue" sets the title + stages the issue as linked context). Pipeline / preset
|
|
671
|
-
//
|
|
677
|
+
// issue" sets the title + stages the issue as linked context). Pipeline / preset stay at their
|
|
678
|
+
// defaults unless the opener carries a choice the person already made (the one-sentence
|
|
679
|
+
// intake's kind and overrides), which the hand-off must not reset.
|
|
672
680
|
const prefill = ui.addTaskPrefill
|
|
673
681
|
if (prefill) {
|
|
674
682
|
if (prefill.title) title.value = prefill.title
|
|
675
683
|
if (prefill.description) description.value = prefill.description
|
|
676
684
|
if (prefill.context?.length) pendingContext.value = [...prefill.context]
|
|
685
|
+
if (prefill.taskType && prefill.taskType !== taskType.value) {
|
|
686
|
+
taskType.value = prefill.taskType
|
|
687
|
+
// The type watcher runs after this block and would replace a carried pipeline with the
|
|
688
|
+
// new type's default; a seeded type is not the edit that watcher is for.
|
|
689
|
+
seedingPrefill = !!prefill.pipelineId
|
|
690
|
+
}
|
|
691
|
+
if (prefill.pipelineId) pipelineId.value = prefill.pipelineId
|
|
692
|
+
if (prefill.riskPolicyId) riskPolicyId.value = prefill.riskPolicyId
|
|
693
|
+
if (prefill.modelPresetId) modelPresetId.value = prefill.modelPresetId
|
|
677
694
|
}
|
|
678
695
|
documents.loadDocuments().catch(() => {})
|
|
679
696
|
tasks.loadTasks().catch(() => {})
|
|
@@ -831,8 +848,15 @@ async function submitCreate(acknowledgeReviewDebt: boolean) {
|
|
|
831
848
|
ui.closeAddTask()
|
|
832
849
|
} catch (e) {
|
|
833
850
|
const conflict = parseConflict(e)
|
|
834
|
-
if (conflict
|
|
835
|
-
|
|
851
|
+
if (isReviewDebtConflict(conflict)) {
|
|
852
|
+
ui.openReviewFriction(
|
|
853
|
+
reviewFrictionContext(
|
|
854
|
+
conflict,
|
|
855
|
+
() => void submitCreate(true),
|
|
856
|
+
() => saving.value,
|
|
857
|
+
() => ui.closeAddTask(),
|
|
858
|
+
),
|
|
859
|
+
)
|
|
836
860
|
return
|
|
837
861
|
}
|
|
838
862
|
const refusal = createRefusalMessage(e)
|
|
@@ -883,32 +907,6 @@ function createRefusalMessage(error: unknown): string | null {
|
|
|
883
907
|
if (reason === 'task_type_fields_invalid') return t('board.addTask.customFieldsInvalid')
|
|
884
908
|
return REVIEW_TARGET_MESSAGES[reason as ReviewTargetReason]?.(details) ?? null
|
|
885
909
|
}
|
|
886
|
-
|
|
887
|
-
/** Turn a parsed review-debt friction 409 into the dialog context (see ReviewFrictionDialog.vue). */
|
|
888
|
-
function openReviewFrictionDialog(conflict: NonNullable<ReturnType<typeof parseConflict>>) {
|
|
889
|
-
const details = conflict.details
|
|
890
|
-
const rawDebt = Array.isArray(details.debt) ? details.debt : []
|
|
891
|
-
const debt = rawDebt.map((d) => {
|
|
892
|
-
const row = (d ?? {}) as { blockId?: unknown; title?: unknown; waitingMinutes?: unknown }
|
|
893
|
-
return {
|
|
894
|
-
blockId: typeof row.blockId === 'string' ? row.blockId : '',
|
|
895
|
-
title: typeof row.title === 'string' ? row.title : null,
|
|
896
|
-
waitingMinutes: typeof row.waitingMinutes === 'number' ? row.waitingMinutes : 0,
|
|
897
|
-
}
|
|
898
|
-
})
|
|
899
|
-
const isWarn = conflict.reason === 'review_debt_warn'
|
|
900
|
-
ui.openReviewFriction({
|
|
901
|
-
kind: isWarn ? 'warn' : 'blocked',
|
|
902
|
-
reason:
|
|
903
|
-
details.friction === 'count' || details.friction === 'stuck' ? details.friction : undefined,
|
|
904
|
-
threshold: typeof details.threshold === 'number' ? details.threshold : null,
|
|
905
|
-
debt,
|
|
906
|
-
onConfirm: isWarn ? () => void submitCreate(true) : null,
|
|
907
|
-
// A getter, not a snapshot: the dialog reads it inside a computed so its button spins and
|
|
908
|
-
// locks for as long as the retry actually runs (UX-78).
|
|
909
|
-
pending: () => saving.value,
|
|
910
|
-
})
|
|
911
|
-
}
|
|
912
910
|
</script>
|
|
913
911
|
|
|
914
912
|
<template>
|
|
@@ -954,6 +952,7 @@ function openReviewFrictionDialog(conflict: NonNullable<ReturnType<typeof parseC
|
|
|
954
952
|
:icon="ty.icon"
|
|
955
953
|
size="xs"
|
|
956
954
|
:title="ty.description"
|
|
955
|
+
:aria-pressed="taskType === ty.value"
|
|
957
956
|
:data-testid="`task-type-${ty.value}`"
|
|
958
957
|
@click="
|
|
959
958
|
() => {
|
|
@@ -53,17 +53,20 @@ const body = computed(() => {
|
|
|
53
53
|
/** Deep-link to a waiting task's block and dismiss the whole friction flow. */
|
|
54
54
|
function goToBlock(blockId: string) {
|
|
55
55
|
if (blockId) ui.select(blockId)
|
|
56
|
+
leave()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Close the friction and the form that raised it. */
|
|
60
|
+
function leave() {
|
|
61
|
+
const onLeave = ctx.value?.onLeave
|
|
56
62
|
ui.closeReviewFriction()
|
|
57
|
-
|
|
63
|
+
onLeave?.()
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
function goReview() {
|
|
61
67
|
const first = ctx.value?.debt[0]
|
|
62
68
|
if (first) goToBlock(first.blockId)
|
|
63
|
-
else
|
|
64
|
-
ui.closeReviewFriction()
|
|
65
|
-
ui.closeAddTask()
|
|
66
|
-
}
|
|
69
|
+
else leave()
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
function createAnyway() {
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import type { Notification } from '~/types/domain'
|
|
3
3
|
import type { ReviewEffort } from '~/types/merge'
|
|
4
4
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
5
|
+
import {
|
|
6
|
+
notificationTarget,
|
|
7
|
+
notificationVerbKey,
|
|
8
|
+
type NotificationRouteDeps,
|
|
9
|
+
} from '~/utils/notificationRoutes'
|
|
5
10
|
|
|
6
11
|
// The board's notification inbox: a bell with an open-count badge that opens a
|
|
7
12
|
// panel of human-actionable items (a PR awaiting a merge decision, a completed
|
|
@@ -133,6 +138,45 @@ function actionLabel(n: Notification): string {
|
|
|
133
138
|
return te(key) ? t(key) : t('layout.notifications.action.markRead')
|
|
134
139
|
}
|
|
135
140
|
|
|
141
|
+
// The store reads and openers every route uses (`utils/notificationRoutes.ts`).
|
|
142
|
+
const routeDeps: NotificationRouteDeps = {
|
|
143
|
+
getInstance: (id) => execution.getInstance(id),
|
|
144
|
+
select: (id) => ui.select(id),
|
|
145
|
+
openStepDetail: (id, index) => ui.openStepDetail(id, index),
|
|
146
|
+
openDecision: (id, decisionId) => ui.openDecision(id, decisionId),
|
|
147
|
+
openFollowUps: (id) => ui.openFollowUps(id),
|
|
148
|
+
openForkDecision: (id) => ui.openForkDecision(id),
|
|
149
|
+
openPrReview: (id) => ui.openPrReview(id),
|
|
150
|
+
openBugFishing: (id) => ui.openBugFishing(id),
|
|
151
|
+
openRequirementReview: (id) => ui.openRequirementReview(id),
|
|
152
|
+
openClarityReview: (id) => ui.openClarityReview(id),
|
|
153
|
+
openInitiativeTracker: (id) => ui.openInitiativeTracker(id),
|
|
154
|
+
openOperatorDashboard: () => ui.openOperatorDashboard(),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The primary verb for a card whose act button only marks it read, from the same resolution its
|
|
159
|
+
* click takes. `te()`-guarded like the act label: a locale missing the key gets no button rather
|
|
160
|
+
* than a raw key path.
|
|
161
|
+
*/
|
|
162
|
+
function goToLabel(n: Notification): string | null {
|
|
163
|
+
const key = notificationVerbKey(n, routeDeps)
|
|
164
|
+
return key && te(key) ? t(key) : null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Each open card's go-to label, computed once per change rather than once per binding. */
|
|
168
|
+
const goToLabels = computed(
|
|
169
|
+
() => new Map(notifications.open.map((n) => [n.id, goToLabel(n)] as const)),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
// The popover is closed when a card takes the person somewhere, so it does not stay open under
|
|
173
|
+
// the window it opened and reappear when that window closes.
|
|
174
|
+
const open = ref(false)
|
|
175
|
+
function goTo(n: Notification) {
|
|
176
|
+
open.value = false
|
|
177
|
+
reveal(n)
|
|
178
|
+
}
|
|
179
|
+
|
|
136
180
|
/** A notification the escalation sweep has flagged as overdue (waited past the threshold). */
|
|
137
181
|
function isUrgent(n: Notification): boolean {
|
|
138
182
|
return n.severity === 'urgent'
|
|
@@ -217,120 +261,9 @@ async function dismiss(n: Notification) {
|
|
|
217
261
|
}
|
|
218
262
|
}
|
|
219
263
|
|
|
220
|
-
/**
|
|
221
|
-
* Clicking a notification's title takes the user where they can act on it. A
|
|
222
|
-
* `requirement_review` / `clarity_review` summons them straight back into the matching review
|
|
223
|
-
* window (the async incorporate/re-review raised new findings or hit the cap); every other
|
|
224
|
-
* type just focuses the related block on the board.
|
|
225
|
-
*/
|
|
264
|
+
/** Clicking a notification's title, or its verb, takes the user where they can act on it. */
|
|
226
265
|
function reveal(n: Notification) {
|
|
227
|
-
|
|
228
|
-
// dashboard where the live aggregate numbers behind the alert live.
|
|
229
|
-
if (n.type === 'platform_health') return ui.openOperatorDashboard()
|
|
230
|
-
if (!n.blockId) return
|
|
231
|
-
if (n.type === 'requirement_review') ui.openRequirementReview(n.blockId)
|
|
232
|
-
else if (n.type === 'clarity_review') ui.openClarityReview(n.blockId)
|
|
233
|
-
else if (n.type === 'decision_required') revealDecision(n)
|
|
234
|
-
else if (n.type === 'human_test_ready') revealHumanTest(n)
|
|
235
|
-
else if (n.type === 'visual_confirmation_ready') revealVisualConfirm(n)
|
|
236
|
-
else if (n.type === 'human_review') revealHumanReview(n)
|
|
237
|
-
else if (n.type === 'followup_pending') revealFollowUps(n)
|
|
238
|
-
else if (n.type === 'fork_decision_pending') revealForkDecision(n)
|
|
239
|
-
else if (n.type === 'judge_review') revealJudge(n)
|
|
240
|
-
else if (n.type === 'pr_review_ready') revealPrReview(n)
|
|
241
|
-
else if (n.type === 'bug_fishing_triage') revealBugFishing(n)
|
|
242
|
-
else if (n.type === 'initiative') ui.openInitiativeTracker(n.blockId)
|
|
243
|
-
else ui.select(n.blockId)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Open the gate window for a parked `human-review` gate: find the run's human-review step and
|
|
248
|
-
* open it through the universal step dispatch (its archetype declares the `gate` result view,
|
|
249
|
-
* where the human can request a freeform fix). Falls back to focusing the block.
|
|
250
|
-
*/
|
|
251
|
-
function revealHumanReview(n: Notification) {
|
|
252
|
-
const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
|
|
253
|
-
const idx = instance?.steps.findIndex((s) => s.agentKind === 'human-review') ?? -1
|
|
254
|
-
if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
|
|
255
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Open the Follow-up companion window for a run whose Coder parked on undecided items.
|
|
260
|
-
* Falls back to focusing the block when the run isn't loaded.
|
|
261
|
-
*/
|
|
262
|
-
function revealFollowUps(n: Notification) {
|
|
263
|
-
if (n.executionId && execution.getInstance(n.executionId)) ui.openFollowUps(n.executionId)
|
|
264
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Open the implementation-fork decision window for a run parked awaiting a fork choice.
|
|
269
|
-
* Falls back to focusing the block when the run isn't loaded.
|
|
270
|
-
*/
|
|
271
|
-
function revealForkDecision(n: Notification) {
|
|
272
|
-
if (n.executionId && execution.getInstance(n.executionId)) ui.openForkDecision(n.executionId)
|
|
273
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Open the judge window for a run parked on a rubric verdict: find the step carrying the parked
|
|
278
|
-
* verdict and open it through the universal step dispatch (a registered judge declares the
|
|
279
|
-
* `judge` result view). Falls back to focusing the block when the run isn't loaded.
|
|
280
|
-
*/
|
|
281
|
-
function revealJudge(n: Notification) {
|
|
282
|
-
const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
|
|
283
|
-
const idx = instance?.steps.findIndex((s) => s.judge?.status === 'awaiting_decision') ?? -1
|
|
284
|
-
if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
|
|
285
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Open the PR deep-review window for a run parked awaiting a finding selection.
|
|
290
|
-
* Falls back to focusing the block when the run isn't loaded.
|
|
291
|
-
*/
|
|
292
|
-
function revealPrReview(n: Notification) {
|
|
293
|
-
if (n.executionId && execution.getInstance(n.executionId)) ui.openPrReview(n.executionId)
|
|
294
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Open the bug-fishing expedition window for a run whose angles have all settled. Falls back to
|
|
299
|
-
* focusing the block when the run is not loaded, exactly like its PR-review sibling.
|
|
300
|
-
*/
|
|
301
|
-
function revealBugFishing(n: Notification) {
|
|
302
|
-
if (n.executionId && execution.getInstance(n.executionId)) ui.openBugFishing(n.executionId)
|
|
303
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Open the human-testing window for a parked `human-test` gate: find the run's parked
|
|
308
|
-
* human-test step and open it through the universal step dispatch (its archetype declares
|
|
309
|
-
* the `human-test` result view). Falls back to focusing the block.
|
|
310
|
-
*/
|
|
311
|
-
function revealHumanTest(n: Notification) {
|
|
312
|
-
const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
|
|
313
|
-
const idx =
|
|
314
|
-
instance?.steps.findIndex(
|
|
315
|
-
(s) => s.agentKind === 'human-test' && s.state === 'waiting_decision',
|
|
316
|
-
) ?? -1
|
|
317
|
-
if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
|
|
318
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Open the visual-confirmation window for a parked `visual-confirmation` gate: find the run's
|
|
323
|
-
* parked step and open it through the universal step dispatch (its archetype declares the
|
|
324
|
-
* `visual-confirm` result view). Falls back to focusing the block.
|
|
325
|
-
*/
|
|
326
|
-
function revealVisualConfirm(n: Notification) {
|
|
327
|
-
const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
|
|
328
|
-
const idx =
|
|
329
|
-
instance?.steps.findIndex(
|
|
330
|
-
(s) => s.agentKind === 'visual-confirmation' && s.state === 'waiting_decision',
|
|
331
|
-
) ?? -1
|
|
332
|
-
if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
|
|
333
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
266
|
+
notificationTarget(n, routeDeps)?.open()
|
|
334
267
|
}
|
|
335
268
|
|
|
336
269
|
/**
|
|
@@ -370,23 +303,10 @@ function revealFailingRun(run: { executionId: string; blockId: string | null })
|
|
|
370
303
|
if (execution.getInstance(run.executionId)) ui.openObservability(run.executionId)
|
|
371
304
|
else if (run.blockId) ui.select(run.blockId)
|
|
372
305
|
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Open the decision surface for a parked iteration-cap run: find the run's step that is
|
|
376
|
-
* waiting on a human and open it through the universal step dispatch — which routes a
|
|
377
|
-
* `requirements-review` step to the review window and a companion step to its detail
|
|
378
|
-
* panel (where the iteration-cap prompt lives). Falls back to focusing the block.
|
|
379
|
-
*/
|
|
380
|
-
function revealDecision(n: Notification) {
|
|
381
|
-
const instance = n.executionId ? execution.getInstance(n.executionId) : undefined
|
|
382
|
-
const idx = instance?.steps.findIndex((s) => s.state === 'waiting_decision') ?? -1
|
|
383
|
-
if (instance && idx >= 0) ui.openStepDetail(instance.id, idx)
|
|
384
|
-
else if (n.blockId) ui.select(n.blockId)
|
|
385
|
-
}
|
|
386
306
|
</script>
|
|
387
307
|
|
|
388
308
|
<template>
|
|
389
|
-
<UPopover v-if="notifications.count" :content="{ align: 'end' }">
|
|
309
|
+
<UPopover v-if="notifications.count" v-model:open="open" :content="{ align: 'end' }">
|
|
390
310
|
<UButton
|
|
391
311
|
data-testid="notifications-bell"
|
|
392
312
|
:color="hasUrgent ? 'error' : 'warning'"
|
|
@@ -426,7 +346,7 @@ function revealDecision(n: Notification) {
|
|
|
426
346
|
variant="link"
|
|
427
347
|
class="block min-w-0 flex-1 truncate p-0 text-start text-sm font-medium text-default hover:underline"
|
|
428
348
|
:title="n.title"
|
|
429
|
-
@click="
|
|
349
|
+
@click="goTo(n)"
|
|
430
350
|
>
|
|
431
351
|
{{ n.title }}
|
|
432
352
|
</UButton>
|
|
@@ -502,11 +422,24 @@ function revealDecision(n: Notification) {
|
|
|
502
422
|
@update:model-value="effortByCard[n.id] = $event"
|
|
503
423
|
/>
|
|
504
424
|
<div class="mt-2 flex items-center gap-1.5">
|
|
425
|
+
<!-- The action, where marking read is not one: opens what the title opens. -->
|
|
426
|
+
<!-- Soft, not solid: the inbox lists many cards, and a solid button on each would put
|
|
427
|
+
many primaries in one view. -->
|
|
505
428
|
<UButton
|
|
506
|
-
|
|
429
|
+
v-if="goToLabels.get(n.id)"
|
|
430
|
+
data-testid="notification-go-to"
|
|
507
431
|
:color="accent(n)"
|
|
508
432
|
variant="soft"
|
|
509
433
|
size="xs"
|
|
434
|
+
@click="goTo(n)"
|
|
435
|
+
>
|
|
436
|
+
{{ goToLabels.get(n.id) }}
|
|
437
|
+
</UButton>
|
|
438
|
+
<UButton
|
|
439
|
+
data-testid="notification-act"
|
|
440
|
+
:color="goToLabels.get(n.id) ? 'neutral' : accent(n)"
|
|
441
|
+
:variant="goToLabels.get(n.id) ? 'outline' : 'soft'"
|
|
442
|
+
size="xs"
|
|
510
443
|
:loading="busy === n.id"
|
|
511
444
|
:disabled="!access.canExecuteRuns.value"
|
|
512
445
|
:title="access.canExecuteRuns.value ? undefined : t('access.noRunExecute')"
|
|
@@ -21,6 +21,12 @@ const props = withDefaults(
|
|
|
21
21
|
* chosen row — and the row is left out entirely.
|
|
22
22
|
*/
|
|
23
23
|
noneLabel?: string
|
|
24
|
+
/**
|
|
25
|
+
* What the preview pane says about the "none" row. Defaults to the full form's meaning ("no
|
|
26
|
+
* default; you choose when you run it"); a surface whose "none" row means the workspace default
|
|
27
|
+
* names what that default resolves to instead.
|
|
28
|
+
*/
|
|
29
|
+
noneHint?: string
|
|
24
30
|
/**
|
|
25
31
|
* Default-trigger text while nothing is selected. Defaults to {@link noneLabel}, which reads
|
|
26
32
|
* correctly when the "none" row IS the empty state; a surface without that row supplies its
|
|
@@ -30,7 +36,7 @@ const props = withDefaults(
|
|
|
30
36
|
/** Extra classes for the default trigger button (e.g. full-width in the modal). */
|
|
31
37
|
triggerClass?: string
|
|
32
38
|
}>(),
|
|
33
|
-
{ noneLabel: undefined, placeholder: undefined, triggerClass: '' },
|
|
39
|
+
{ noneLabel: undefined, noneHint: undefined, placeholder: undefined, triggerClass: '' },
|
|
34
40
|
)
|
|
35
41
|
|
|
36
42
|
const emit = defineEmits<{ 'update:modelValue': [string] }>()
|
|
@@ -59,7 +65,7 @@ const previewPipeline = computed<Pipeline | null>(() => {
|
|
|
59
65
|
*/
|
|
60
66
|
const emptyHint = computed(() =>
|
|
61
67
|
props.noneLabel !== undefined && (hoverId.value ?? props.modelValue) === ''
|
|
62
|
-
? t('pipeline.picker.noneHint')
|
|
68
|
+
? (props.noneHint ?? t('pipeline.picker.noneHint'))
|
|
63
69
|
: t('pipeline.picker.hoverHint'),
|
|
64
70
|
)
|
|
65
71
|
|