@cat-factory/app 0.313.0 → 0.314.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.
- package/README.md +7 -0
- package/app/components/board/AddTaskModal.vue +29 -30
- package/app/components/board/ReviewFrictionDialog.vue +8 -5
- 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/composables/useNavContributions.ts +1 -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/ui/modals.ts +28 -0
- package/app/utils/describeWork.spec.ts +76 -0
- package/app/utils/describeWork.ts +71 -0
- package/app/utils/reviewFriction.ts +47 -0
- package/i18n/locales/de.json +26 -2
- package/i18n/locales/en.json +26 -2
- package/i18n/locales/es.json +26 -2
- package/i18n/locales/fr.json +26 -2
- package/i18n/locales/he.json +26 -2
- package/i18n/locales/it.json +26 -2
- package/i18n/locales/ja.json +26 -2
- package/i18n/locales/pl.json +26 -2
- package/i18n/locales/tr.json +26 -2
- package/i18n/locales/uk.json +26 -2
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ export function useTaskActions(taskId: () => string) {
|
|
|
43
43
|
|
|
44
44
|
const pipelineFor = useStartPipelineResolver()
|
|
45
45
|
const defaultPipeline = computed(() => pipelineFor(task.value))
|
|
46
|
+
const { startTask } = useStartWithFeedback()
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* Whether the outcome summary has something to show, asked of the SAME reduction the window
|
|
@@ -77,35 +78,12 @@ export function useTaskActions(taskId: () => string) {
|
|
|
77
78
|
})
|
|
78
79
|
return
|
|
79
80
|
}
|
|
80
|
-
const
|
|
81
|
-
if (!
|
|
82
|
-
toast.error('board.task.noPipelineTitle', { description: t('board.task.noPipelineBody') })
|
|
83
|
-
return
|
|
84
|
-
}
|
|
81
|
+
const block = task.value
|
|
82
|
+
if (!block) return
|
|
85
83
|
starting.value = true
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
const started = await execution.start(taskId(), pipeline)
|
|
90
|
-
if (started) {
|
|
91
|
-
// Confirm the (optimistic) start landed: the button unmounts once the stream pushes
|
|
92
|
-
// in_progress, so without this the successful action gives no feedback. A sandboxed start
|
|
93
|
-
// says so here rather than borrowing the live wording: this is the moment the reader learns
|
|
94
|
-
// what they just started, and the two runs differ in what they will end up doing.
|
|
95
|
-
// A sandboxed run is `warning`: it ran, and its outcome differs in a way the reader should
|
|
96
|
-
// know (it opens a pull request and merges nothing), so it stays until closed.
|
|
97
|
-
if (sandboxed.value) {
|
|
98
|
-
toast.warning('board.dryRunToast.title', {
|
|
99
|
-
description: t('board.dryRunToast.body', { name: pipeline.name }),
|
|
100
|
-
})
|
|
101
|
-
} else {
|
|
102
|
-
toast.success('board.task.startedToast.title', {
|
|
103
|
-
description: t('board.task.startedToast.body', { name: pipeline.name }),
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
starting.value = false
|
|
108
|
-
}
|
|
84
|
+
// On success the button unmounts once the stream pushes in_progress; otherwise revert the
|
|
85
|
+
// optimistic state (`startTask` has said why).
|
|
86
|
+
if (!(await startTask(block))) starting.value = false
|
|
109
87
|
}
|
|
110
88
|
|
|
111
89
|
async function merge() {
|
|
@@ -199,6 +177,51 @@ export function useTaskActions(taskId: () => string) {
|
|
|
199
177
|
}
|
|
200
178
|
}
|
|
201
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Start a task on the pipeline a plain "Start" uses, and tell the person what happened: the
|
|
182
|
+
* card's Start and the intake's "Add and start" both go through it, so the two say the same thing.
|
|
183
|
+
* Resolves false when nothing started (no pipeline to run, a cancelled credential prompt, or a
|
|
184
|
+
* refused start, which `execution.start` reports itself).
|
|
185
|
+
*/
|
|
186
|
+
export function useStartWithFeedback() {
|
|
187
|
+
const execution = useExecutionStore()
|
|
188
|
+
const toast = useActionToast()
|
|
189
|
+
const { t } = useI18n()
|
|
190
|
+
const { forcedFor } = useDryRunPolicy()
|
|
191
|
+
const pipelineFor = useStartPipelineResolver()
|
|
192
|
+
|
|
193
|
+
async function startTask(block: Pick<Block, 'id' | 'pipelineId'>): Promise<boolean> {
|
|
194
|
+
const pipeline = pipelineFor(block)
|
|
195
|
+
if (!pipeline) {
|
|
196
|
+
// A refusal decided in the browser, in translated copy: the `error` tone.
|
|
197
|
+
toast.error('board.task.noPipelineTitle', { description: t('board.task.noPipelineBody') })
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
// Read before the start: the sandbox policy is a fact about this task and role now.
|
|
201
|
+
const sandboxed = forcedFor(block.id)
|
|
202
|
+
const started = await execution.start(block.id, pipeline)
|
|
203
|
+
if (started) {
|
|
204
|
+
// Confirm the start landed: without this the action gives no feedback until the stream
|
|
205
|
+
// moves the card. A sandboxed start says so rather than borrowing the live wording: this is
|
|
206
|
+
// the moment the reader learns what they just started, and the two runs end differently.
|
|
207
|
+
// A sandboxed run is `warning`: it ran, and it ends differently (it opens a pull request and
|
|
208
|
+
// merges nothing), so the toast stays until closed.
|
|
209
|
+
if (sandboxed) {
|
|
210
|
+
toast.warning('board.dryRunToast.title', {
|
|
211
|
+
description: t('board.dryRunToast.body', { name: pipeline.name }),
|
|
212
|
+
})
|
|
213
|
+
} else {
|
|
214
|
+
toast.success('board.task.startedToast.title', {
|
|
215
|
+
description: t('board.task.startedToast.body', { name: pipeline.name }),
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return started
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { startTask }
|
|
223
|
+
}
|
|
224
|
+
|
|
202
225
|
/** The pipeline a plain "Start" runs, as the descriptor `execution.start` takes. */
|
|
203
226
|
export interface StartPipeline {
|
|
204
227
|
id: string
|
|
@@ -224,6 +224,7 @@ describe('navSlotFilter', () => {
|
|
|
224
224
|
tutorial: 'the walkthroughs - the surface with the fewest destinations needs them most',
|
|
225
225
|
'keyboard-shortcuts': 'the cheatsheet covers the board and the palette, which every role has',
|
|
226
226
|
'ui-role': 'the way BACK out of the narrowed role',
|
|
227
|
+
'describe-work': 'filing a task from one sentence is the job the intake role exists for',
|
|
227
228
|
'queue-view': 'which view of the board a person works in is their own choice, in every role',
|
|
228
229
|
'board-view': 'the way back to the canvas from the queue view, in every role',
|
|
229
230
|
}
|
|
@@ -196,6 +196,7 @@ export interface NavCommandSpec {
|
|
|
196
196
|
* dead button. Consumer modules don't use these — they carry their own `run`.
|
|
197
197
|
*/
|
|
198
198
|
export const NAV_ACTIONS = [
|
|
199
|
+
'describeWork',
|
|
199
200
|
'assistant',
|
|
200
201
|
'guidedReview',
|
|
201
202
|
'buildPipeline',
|
|
@@ -346,6 +347,26 @@ const S = (...s: NavSurface[]) => s as readonly NavSurface[]
|
|
|
346
347
|
* the whole sidebar costs it nothing it is there to do.
|
|
347
348
|
*/
|
|
348
349
|
export const NAV_CONTRIBUTIONS: readonly NavContribution[] = [
|
|
350
|
+
{
|
|
351
|
+
// The one-sentence intake (issue #2258): what needs doing, a service, feature or bug. First in
|
|
352
|
+
// Create, because filing a task is the most frequent thing anyone creates, and until now it had
|
|
353
|
+
// no entry here at all (it lived on each frame). Every tier and the intake role: describing
|
|
354
|
+
// work is what that role is for.
|
|
355
|
+
id: 'describe-work',
|
|
356
|
+
labelKey: 'queue.describeWork',
|
|
357
|
+
icon: 'i-lucide-plus',
|
|
358
|
+
surfaces: S('sidebar', 'command'),
|
|
359
|
+
gate: (g) => g.canWriteBoard && g.boardHasService,
|
|
360
|
+
action: 'describeWork',
|
|
361
|
+
intake: true,
|
|
362
|
+
testId: 'nav-describe-work',
|
|
363
|
+
sidebar: { group: 'create', order: 1 },
|
|
364
|
+
command: {
|
|
365
|
+
group: 'create',
|
|
366
|
+
order: 1,
|
|
367
|
+
keywordsKey: 'layout.commandBar.keywords.describeWork',
|
|
368
|
+
},
|
|
369
|
+
},
|
|
349
370
|
{
|
|
350
371
|
// The in-app assistant. `intake: true` and not `advanced`, because it is the shortest route
|
|
351
372
|
// to the two things a narrowed role is on the board to do (put a repository on it, file a
|
package/app/pages/index.vue
CHANGED
|
@@ -164,6 +164,7 @@ const TutorialNudge = defineAsyncView(() => import('~/components/tutorial/Tutori
|
|
|
164
164
|
const RolePrompt = defineAsyncView(() => import('~/components/layout/RolePrompt.vue'))
|
|
165
165
|
// The queue: the board's second view (issue #2258), loaded the first time someone opens it.
|
|
166
166
|
const QueueView = defineAsyncView(() => import('~/components/queue/QueueView.vue'))
|
|
167
|
+
const DescribeWorkModal = defineAsyncView(() => import('~/components/queue/DescribeWorkModal.vue'))
|
|
167
168
|
|
|
168
169
|
const workspace = useWorkspaceStore()
|
|
169
170
|
const github = useGitHubStore()
|
|
@@ -532,6 +533,7 @@ watch(
|
|
|
532
533
|
<AddTaskModal />
|
|
533
534
|
<ReviewFrictionDialog v-if="ui.reviewFrictionContext" />
|
|
534
535
|
<CreateInitiativeModal />
|
|
536
|
+
<DescribeWorkModal v-if="ui.describeWorkOpen" />
|
|
535
537
|
<CommandBar />
|
|
536
538
|
<PersonalCredentialModal />
|
|
537
539
|
<ConfirmDialog />
|
package/app/stores/ui/modals.ts
CHANGED
|
@@ -27,6 +27,14 @@ export interface AddTaskPrefill {
|
|
|
27
27
|
description?: string
|
|
28
28
|
/** Context items staged on the new task (e.g. the source issue), linked once created. */
|
|
29
29
|
context?: PendingContext[]
|
|
30
|
+
/**
|
|
31
|
+
* Choices a person already made on another surface (the one-sentence intake), carried over so
|
|
32
|
+
* the hand-off does not quietly reset them. Absent means the form's own defaults.
|
|
33
|
+
*/
|
|
34
|
+
taskType?: 'feature' | 'bug'
|
|
35
|
+
pipelineId?: string
|
|
36
|
+
riskPolicyId?: string
|
|
37
|
+
modelPresetId?: string
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
/**
|
|
@@ -90,6 +98,11 @@ export interface ReviewFrictionModalContext {
|
|
|
90
98
|
* started a second pipeline run against the same request.
|
|
91
99
|
*/
|
|
92
100
|
pending?: () => boolean
|
|
101
|
+
/**
|
|
102
|
+
* Close the form that raised the friction. "Go review" and each debt row leave for a waiting
|
|
103
|
+
* task, and the form must not stay open over it; only the opener knows which form it is.
|
|
104
|
+
*/
|
|
105
|
+
onLeave: () => void
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
/** Clears both hub came-from markers; injected into the slices whose `open*` handlers reset them. */
|
|
@@ -340,6 +353,17 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
|
|
|
340
353
|
function closeStartFromDesign() {
|
|
341
354
|
startFromDesign.value = null
|
|
342
355
|
}
|
|
356
|
+
// The one-sentence intake ("Describe new work", issue #2258): open, and the service it opened
|
|
357
|
+
// on, or null to let the reader pick one. The full form above stays the home of every other kind.
|
|
358
|
+
const describeWorkOpen = ref(false)
|
|
359
|
+
const describeWorkServiceId = ref<string | null>(null)
|
|
360
|
+
function openDescribeWork(serviceId: string | null = null) {
|
|
361
|
+
describeWorkServiceId.value = serviceId
|
|
362
|
+
describeWorkOpen.value = true
|
|
363
|
+
}
|
|
364
|
+
function closeDescribeWork() {
|
|
365
|
+
describeWorkOpen.value = false
|
|
366
|
+
}
|
|
343
367
|
function openAddTask(containerId: string, prefill: AddTaskPrefill | null = null) {
|
|
344
368
|
addTaskPrefill.value = prefill
|
|
345
369
|
addTaskContainerId.value = containerId
|
|
@@ -405,6 +429,10 @@ function createDocumentTaskModals(resetHubReturn: ResetHubReturn) {
|
|
|
405
429
|
closeStartFromDesign,
|
|
406
430
|
openAddTask,
|
|
407
431
|
closeAddTask,
|
|
432
|
+
describeWorkOpen,
|
|
433
|
+
describeWorkServiceId,
|
|
434
|
+
openDescribeWork,
|
|
435
|
+
closeDescribeWork,
|
|
408
436
|
openReviewFriction,
|
|
409
437
|
closeReviewFriction,
|
|
410
438
|
openAddRecurring,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { DERIVED_TITLE_MAX, splitDescribedWork } from '~/utils/describeWork'
|
|
3
|
+
|
|
4
|
+
describe('splitDescribedWork', () => {
|
|
5
|
+
it('uses one short sentence as the title and adds no description', () => {
|
|
6
|
+
expect(splitDescribedWork(' Fix the login redirect ')).toEqual({
|
|
7
|
+
title: 'Fix the login redirect',
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('titles with the first sentence and keeps the WHOLE text as the description', () => {
|
|
12
|
+
const text = 'Add an audit log. Record each sign-in with the time and the user.'
|
|
13
|
+
expect(splitDescribedWork(text)).toEqual({ title: 'Add an audit log', description: text })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('reads only the first line for the title', () => {
|
|
17
|
+
const text = 'Rate limit per API key\nUse a token bucket. 100 requests a minute.'
|
|
18
|
+
expect(splitDescribedWork(text)).toEqual({ title: 'Rate limit per API key', description: text })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('shortens a long first sentence at a word boundary, with an ellipsis', () => {
|
|
22
|
+
const words = 'word '.repeat(40).trim()
|
|
23
|
+
const { title, description } = splitDescribedWork(words)
|
|
24
|
+
expect(title.length).toBeLessThanOrEqual(DERIVED_TITLE_MAX)
|
|
25
|
+
expect(title.endsWith('word…')).toBe(true)
|
|
26
|
+
expect(description).toBe(words)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('does not cut a dotted name such as a file or a version', () => {
|
|
30
|
+
expect(splitDescribedWork('Bump vite to 7.1 in package.json').title).toBe(
|
|
31
|
+
'Bump vite to 7.1 in package.json',
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('does not end a sentence at a common abbreviation', () => {
|
|
36
|
+
const text = 'Fix e.g. the login redirect loop. It happens on Safari.'
|
|
37
|
+
expect(splitDescribedWork(text)).toEqual({
|
|
38
|
+
title: 'Fix e.g. the login redirect loop',
|
|
39
|
+
description: text,
|
|
40
|
+
})
|
|
41
|
+
expect(splitDescribedWork('Ask Dr. Smith about the export. She owns it.').title).toBe(
|
|
42
|
+
'Ask Dr. Smith about the export',
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('treats a run of dots as one mark', () => {
|
|
47
|
+
expect(splitDescribedWork('Fix this... later, after the release.').title).toBe('Fix this')
|
|
48
|
+
expect(splitDescribedWork('Use a cache, e.g. Redis, for sessions.').title).toBe(
|
|
49
|
+
'Use a cache, e.g. Redis, for sessions',
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('ends a sentence at a full-width mark, with or without a space after it', () => {
|
|
54
|
+
const text = '修复登录重定向。记录每次登录。'
|
|
55
|
+
expect(splitDescribedWork(text)).toEqual({ title: '修复登录重定向', description: text })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('keeps a question or an exclamation mark, and drops a period', () => {
|
|
59
|
+
expect(splitDescribedWork('Why does login fail?')).toEqual({ title: 'Why does login fail?' })
|
|
60
|
+
// A final period says nothing the title does not, so it adds no description.
|
|
61
|
+
expect(splitDescribedWork('Fix the login redirect.')).toEqual({
|
|
62
|
+
title: 'Fix the login redirect',
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('never cuts an emoji in half when it shortens', () => {
|
|
67
|
+
const text = `${'a'.repeat(DERIVED_TITLE_MAX - 2)}😀😀😀 more words after it`
|
|
68
|
+
const { title } = splitDescribedWork(text)
|
|
69
|
+
expect(title).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/)
|
|
70
|
+
expect(Array.from(title).length).toBeLessThanOrEqual(DERIVED_TITLE_MAX)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('returns an empty title for blank input, so the dialog cannot submit it', () => {
|
|
74
|
+
expect(splitDescribedWork(' \n ')).toEqual({ title: '' })
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** The longest title the one-box intake derives. Long enough for a sentence, short for a card. */
|
|
2
|
+
export const DERIVED_TITLE_MAX = 80
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Abbreviations whose period does not end a sentence. Only the common ones a task description
|
|
6
|
+
* starts with: the title is derived, so a miss costs a short title, never lost text (the whole text
|
|
7
|
+
* is the description).
|
|
8
|
+
*/
|
|
9
|
+
const ABBREVIATIONS = new Set(['e.g', 'i.e', 'etc', 'vs', 'cf', 'dr', 'mr', 'mrs', 'ms', 'st'])
|
|
10
|
+
|
|
11
|
+
/** Ends a sentence. Full-width marks end one with or without a following space. */
|
|
12
|
+
const FULL_WIDTH_END = new Set(['。', '!', '?'])
|
|
13
|
+
const KEPT_END = new Set(['?', '!', '?', '!'])
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Split what a person typed into the one-box intake into a task title and a description.
|
|
17
|
+
*
|
|
18
|
+
* The title is the first line, cut at its first sentence end, and shortened at a word boundary
|
|
19
|
+
* to {@link DERIVED_TITLE_MAX} with an ellipsis. A question or an exclamation keeps its mark, which
|
|
20
|
+
* is part of what it says; a period does not. The description is the WHOLE text, unedited,
|
|
21
|
+
* whenever it says more than the title does: nothing the person wrote is dropped, and the agents
|
|
22
|
+
* read the description, never the title alone. Returns an empty title for blank input.
|
|
23
|
+
*/
|
|
24
|
+
export function splitDescribedWork(text: string): { title: string; description?: string } {
|
|
25
|
+
const body = text.trim()
|
|
26
|
+
if (!body) return { title: '' }
|
|
27
|
+
const firstLine = body.split(/\r?\n/, 1)[0]!.trim()
|
|
28
|
+
const title = shorten(firstSentence(firstLine) || firstLine)
|
|
29
|
+
return stripEnd(title) === stripEnd(body) ? { title } : { title, description: body }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The first sentence of a line, ending mark included only when it is `?` or `!`. */
|
|
33
|
+
function firstSentence(line: string): string {
|
|
34
|
+
const chars = Array.from(line)
|
|
35
|
+
for (let i = 0; i < chars.length; i++) {
|
|
36
|
+
const c = chars[i]!
|
|
37
|
+
const next = chars[i + 1]
|
|
38
|
+
// A run of dots ("this... later") is ONE mark: only its last dot can end the sentence, and
|
|
39
|
+
// the whole run is cut with it.
|
|
40
|
+
if (c === '.' && next === '.') continue
|
|
41
|
+
const ends =
|
|
42
|
+
FULL_WIDTH_END.has(c) || ('.!?'.includes(c) && (next === undefined || /\s/.test(next)))
|
|
43
|
+
if (!ends) continue
|
|
44
|
+
if (c === '.' && isAbbreviation(chars.slice(0, i).join(''))) continue
|
|
45
|
+
let end = KEPT_END.has(c) ? i + 1 : i
|
|
46
|
+
while (c === '.' && end > 0 && chars[end - 1] === '.') end--
|
|
47
|
+
return chars.slice(0, end).join('').trim()
|
|
48
|
+
}
|
|
49
|
+
return line
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether the word before a period is an abbreviation ("e.g", "Dr"), not a sentence's last word. */
|
|
53
|
+
function isAbbreviation(before: string): boolean {
|
|
54
|
+
const word = before.split(/\s/).pop() ?? ''
|
|
55
|
+
return ABBREVIATIONS.has(word.toLowerCase())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The text without its final sentence mark, so "Fix the login." says no more than "Fix the login". */
|
|
59
|
+
function stripEnd(s: string): string {
|
|
60
|
+
return s.replace(/[.!?。!?…]+$/u, '')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Shorten by CODE POINTS, so a cut never splits an emoji or another astral character in half. */
|
|
64
|
+
function shorten(line: string): string {
|
|
65
|
+
const chars = Array.from(line)
|
|
66
|
+
if (chars.length <= DERIVED_TITLE_MAX) return line
|
|
67
|
+
const cut = chars.slice(0, DERIVED_TITLE_MAX - 1).join('')
|
|
68
|
+
const lastSpace = cut.lastIndexOf(' ')
|
|
69
|
+
const keep = lastSpace > cut.length / 2 ? cut.slice(0, lastSpace) : cut
|
|
70
|
+
return `${keep.trimEnd()}…`
|
|
71
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReviewFrictionModalContext } from '~/stores/ui/modals'
|
|
2
|
+
import type { parseConflict } from '~/composables/usePipelineErrorToast'
|
|
3
|
+
|
|
4
|
+
/** A conflict `parseConflict` recognised. */
|
|
5
|
+
type ParsedConflict = NonNullable<ReturnType<typeof parseConflict>>
|
|
6
|
+
|
|
7
|
+
/** Whether a create was refused by the opt-in review-debt friction gate. */
|
|
8
|
+
export function isReviewDebtConflict(conflict: ParsedConflict | null): conflict is ParsedConflict {
|
|
9
|
+
return conflict?.reason === 'review_debt_warn' || conflict?.reason === 'review_debt_blocked'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Turn a parsed review-debt friction 409 into the dialog's context (see ReviewFrictionDialog.vue).
|
|
14
|
+
*
|
|
15
|
+
* Shared by every surface that creates a task, so the friction reads the same whichever form
|
|
16
|
+
* raised it. `retry` re-submits with `acknowledgeReviewDebt` and is offered only on the soft
|
|
17
|
+
* `warn` tier; `pending` is a GETTER over the opener's own in-flight flag, for the reason the
|
|
18
|
+
* context type documents; `leave` closes the opener when the person leaves to review.
|
|
19
|
+
*/
|
|
20
|
+
export function reviewFrictionContext(
|
|
21
|
+
conflict: ParsedConflict,
|
|
22
|
+
retry: () => void,
|
|
23
|
+
pending: () => boolean,
|
|
24
|
+
leave: () => void,
|
|
25
|
+
): ReviewFrictionModalContext {
|
|
26
|
+
const details = conflict.details
|
|
27
|
+
const rawDebt = Array.isArray(details.debt) ? details.debt : []
|
|
28
|
+
const debt = rawDebt.map((d) => {
|
|
29
|
+
const row = (d ?? {}) as { blockId?: unknown; title?: unknown; waitingMinutes?: unknown }
|
|
30
|
+
return {
|
|
31
|
+
blockId: typeof row.blockId === 'string' ? row.blockId : '',
|
|
32
|
+
title: typeof row.title === 'string' ? row.title : null,
|
|
33
|
+
waitingMinutes: typeof row.waitingMinutes === 'number' ? row.waitingMinutes : 0,
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
const isWarn = conflict.reason === 'review_debt_warn'
|
|
37
|
+
return {
|
|
38
|
+
kind: isWarn ? 'warn' : 'blocked',
|
|
39
|
+
reason:
|
|
40
|
+
details.friction === 'count' || details.friction === 'stuck' ? details.friction : undefined,
|
|
41
|
+
threshold: typeof details.threshold === 'number' ? details.threshold : null,
|
|
42
|
+
debt,
|
|
43
|
+
onConfirm: isWarn ? retry : null,
|
|
44
|
+
pending,
|
|
45
|
+
onLeave: leave,
|
|
46
|
+
}
|
|
47
|
+
}
|
package/i18n/locales/de.json
CHANGED
|
@@ -2807,7 +2807,8 @@
|
|
|
2807
2807
|
"notificationSettings": "benachrichtigungen e-mail routing kanäle posteingang",
|
|
2808
2808
|
"setupChecklist": "einrichtung checkliste setup runner modell github speicher umgebung konfigurieren",
|
|
2809
2809
|
"queueView": "warteschlange ansicht wartet auf dich liste",
|
|
2810
|
-
"boardView": "board canvas karte ansicht planen"
|
|
2810
|
+
"boardView": "board canvas karte ansicht planen",
|
|
2811
|
+
"describeWork": "neue arbeit beschreiben aufgabe hinzufügen erstellen feature bug"
|
|
2811
2812
|
}
|
|
2812
2813
|
},
|
|
2813
2814
|
"shortcuts": {
|
|
@@ -8856,12 +8857,35 @@
|
|
|
8856
8857
|
"parked": "Der Durchlauf wartet auf einen Menschen. Öffne die Aufgabe, um zu sehen, was fehlt.",
|
|
8857
8858
|
"waitingOn": "Wartet auf {deps}",
|
|
8858
8859
|
"pr": "Pull Request #{number}"
|
|
8859
|
-
}
|
|
8860
|
+
},
|
|
8861
|
+
"describeWork": "Neue Arbeit beschreiben"
|
|
8860
8862
|
},
|
|
8861
8863
|
"views": {
|
|
8862
8864
|
"title": "Ansichten",
|
|
8863
8865
|
"queue": "Warteschlange",
|
|
8864
8866
|
"board": "Board",
|
|
8865
8867
|
"openQueue": "Warteschlangen-Ansicht öffnen"
|
|
8868
|
+
},
|
|
8869
|
+
"describeWork": {
|
|
8870
|
+
"title": "Neue Arbeit beschreiben",
|
|
8871
|
+
"textLabel": "Was zu tun ist",
|
|
8872
|
+
"textHint": "Ein Satz reicht. Füge mehr Details hinzu, wenn du welche hast.",
|
|
8873
|
+
"textPlaceholder": "Füge ein Audit-Log für Anmeldeereignisse hinzu. Erfasse jede Anmeldung mit Zeit, Benutzer und Ergebnis.",
|
|
8874
|
+
"savedAs": "Aufgabentitel: {title}",
|
|
8875
|
+
"serviceLabel": "Service",
|
|
8876
|
+
"servicePlaceholder": "Service auswählen",
|
|
8877
|
+
"kindLabel": "Art",
|
|
8878
|
+
"kind": {
|
|
8879
|
+
"feature": "Feature",
|
|
8880
|
+
"bug": "Bug"
|
|
8881
|
+
},
|
|
8882
|
+
"moreKinds": "Weitere Arten…",
|
|
8883
|
+
"changeHowItRuns": "Ablauf ändern",
|
|
8884
|
+
"workspaceDefault": "Workspace-Standard",
|
|
8885
|
+
"defaultPipelineHint": "Der Workspace-Standard: {name}. Eine hier gewählte Pipeline ersetzt ihn für diese Aufgabe.",
|
|
8886
|
+
"attachContext": "Kontext anhängen (öffnet das vollständige Formular)",
|
|
8887
|
+
"add": "Zur Warteschlange hinzufügen",
|
|
8888
|
+
"addAndStart": "Hinzufügen und starten",
|
|
8889
|
+
"addedToast": "„{title}“ zur Warteschlange hinzugefügt"
|
|
8866
8890
|
}
|
|
8867
8891
|
}
|
package/i18n/locales/en.json
CHANGED
|
@@ -2835,7 +2835,8 @@
|
|
|
2835
2835
|
"notificationSettings": "notifications email routing channels inbox",
|
|
2836
2836
|
"setupChecklist": "setup checklist runner model github storage environment configure",
|
|
2837
2837
|
"queueView": "queue view needs you inbox list waiting",
|
|
2838
|
-
"boardView": "board canvas map view plan"
|
|
2838
|
+
"boardView": "board canvas map view plan",
|
|
2839
|
+
"describeWork": "describe new work task add create feature bug sentence"
|
|
2839
2840
|
}
|
|
2840
2841
|
},
|
|
2841
2842
|
"shortcuts": {
|
|
@@ -9187,12 +9188,35 @@
|
|
|
9187
9188
|
"parked": "The run waits for a person. Open the task to see what it needs.",
|
|
9188
9189
|
"waitingOn": "Waits for {deps}",
|
|
9189
9190
|
"pr": "Pull request #{number}"
|
|
9190
|
-
}
|
|
9191
|
+
},
|
|
9192
|
+
"describeWork": "Describe new work"
|
|
9191
9193
|
},
|
|
9192
9194
|
"views": {
|
|
9193
9195
|
"title": "Views",
|
|
9194
9196
|
"queue": "Queue",
|
|
9195
9197
|
"board": "Board",
|
|
9196
9198
|
"openQueue": "Open the queue view"
|
|
9199
|
+
},
|
|
9200
|
+
"describeWork": {
|
|
9201
|
+
"title": "Describe new work",
|
|
9202
|
+
"textLabel": "What must be done",
|
|
9203
|
+
"textHint": "One sentence is enough. Add more detail if you have it.",
|
|
9204
|
+
"textPlaceholder": "Add an audit log for sign-in events. Record each sign-in with the time, the user and the result.",
|
|
9205
|
+
"savedAs": "Task title: {title}",
|
|
9206
|
+
"serviceLabel": "Service",
|
|
9207
|
+
"servicePlaceholder": "Select a service",
|
|
9208
|
+
"kindLabel": "Kind",
|
|
9209
|
+
"kind": {
|
|
9210
|
+
"feature": "Feature",
|
|
9211
|
+
"bug": "Bug"
|
|
9212
|
+
},
|
|
9213
|
+
"moreKinds": "More kinds…",
|
|
9214
|
+
"changeHowItRuns": "Change how this runs",
|
|
9215
|
+
"workspaceDefault": "Workspace default",
|
|
9216
|
+
"defaultPipelineHint": "The workspace default: {name}. A pipeline pinned here replaces it for this task.",
|
|
9217
|
+
"attachContext": "Attach context (opens the full form)",
|
|
9218
|
+
"add": "Add to queue",
|
|
9219
|
+
"addAndStart": "Add and start",
|
|
9220
|
+
"addedToast": "Added “{title}” to the queue"
|
|
9197
9221
|
}
|
|
9198
9222
|
}
|
package/i18n/locales/es.json
CHANGED
|
@@ -2680,7 +2680,8 @@
|
|
|
2680
2680
|
"notificationSettings": "notificaciones correo enrutamiento canales bandeja",
|
|
2681
2681
|
"setupChecklist": "configuración lista setup runner modelo github almacenamiento entorno",
|
|
2682
2682
|
"queueView": "cola vista te necesita lista pendiente",
|
|
2683
|
-
"boardView": "tablero lienzo mapa vista planificar"
|
|
2683
|
+
"boardView": "tablero lienzo mapa vista planificar",
|
|
2684
|
+
"describeWork": "describir nuevo trabajo tarea añadir crear funcionalidad error"
|
|
2684
2685
|
}
|
|
2685
2686
|
},
|
|
2686
2687
|
"integrationsHub": {
|
|
@@ -8856,12 +8857,35 @@
|
|
|
8856
8857
|
"parked": "La ejecución espera a una persona. Abre la tarea para ver qué necesita.",
|
|
8857
8858
|
"waitingOn": "Espera a {deps}",
|
|
8858
8859
|
"pr": "Pull request #{number}"
|
|
8859
|
-
}
|
|
8860
|
+
},
|
|
8861
|
+
"describeWork": "Describir trabajo nuevo"
|
|
8860
8862
|
},
|
|
8861
8863
|
"views": {
|
|
8862
8864
|
"title": "Vistas",
|
|
8863
8865
|
"queue": "Cola",
|
|
8864
8866
|
"board": "Tablero",
|
|
8865
8867
|
"openQueue": "Abrir la vista de cola"
|
|
8868
|
+
},
|
|
8869
|
+
"describeWork": {
|
|
8870
|
+
"title": "Describir trabajo nuevo",
|
|
8871
|
+
"textLabel": "Qué hay que hacer",
|
|
8872
|
+
"textHint": "Una frase basta. Añade más detalle si lo tienes.",
|
|
8873
|
+
"textPlaceholder": "Añade un registro de auditoría para los inicios de sesión. Registra cada inicio con la hora, el usuario y el resultado.",
|
|
8874
|
+
"savedAs": "Título de la tarea: {title}",
|
|
8875
|
+
"serviceLabel": "Servicio",
|
|
8876
|
+
"servicePlaceholder": "Selecciona un servicio",
|
|
8877
|
+
"kindLabel": "Tipo",
|
|
8878
|
+
"kind": {
|
|
8879
|
+
"feature": "Funcionalidad",
|
|
8880
|
+
"bug": "Error"
|
|
8881
|
+
},
|
|
8882
|
+
"moreKinds": "Más tipos…",
|
|
8883
|
+
"changeHowItRuns": "Cambiar cómo se ejecuta",
|
|
8884
|
+
"workspaceDefault": "Predeterminado del workspace",
|
|
8885
|
+
"defaultPipelineHint": "El predeterminado del workspace: {name}. Un pipeline elegido aquí lo sustituye para esta tarea.",
|
|
8886
|
+
"attachContext": "Adjuntar contexto (abre el formulario completo)",
|
|
8887
|
+
"add": "Añadir a la cola",
|
|
8888
|
+
"addAndStart": "Añadir e iniciar",
|
|
8889
|
+
"addedToast": "Se añadió «{title}» a la cola"
|
|
8866
8890
|
}
|
|
8867
8891
|
}
|
package/i18n/locales/fr.json
CHANGED
|
@@ -2680,7 +2680,8 @@
|
|
|
2680
2680
|
"notificationSettings": "notifications e-mail routage canaux boîte",
|
|
2681
2681
|
"setupChecklist": "configuration liste setup runner modèle github stockage environnement",
|
|
2682
2682
|
"queueView": "file attente vue liste en attente",
|
|
2683
|
-
"boardView": "tableau canevas carte vue planifier"
|
|
2683
|
+
"boardView": "tableau canevas carte vue planifier",
|
|
2684
|
+
"describeWork": "décrire nouveau travail tâche ajouter créer fonctionnalité bug"
|
|
2684
2685
|
}
|
|
2685
2686
|
},
|
|
2686
2687
|
"integrationsHub": {
|
|
@@ -8856,12 +8857,35 @@
|
|
|
8856
8857
|
"parked": "L’exécution attend une personne. Ouvrez la tâche pour voir ce qu’il faut.",
|
|
8857
8858
|
"waitingOn": "Attend {deps}",
|
|
8858
8859
|
"pr": "Pull request #{number}"
|
|
8859
|
-
}
|
|
8860
|
+
},
|
|
8861
|
+
"describeWork": "Décrire un nouveau travail"
|
|
8860
8862
|
},
|
|
8861
8863
|
"views": {
|
|
8862
8864
|
"title": "Vues",
|
|
8863
8865
|
"queue": "File d’attente",
|
|
8864
8866
|
"board": "Tableau",
|
|
8865
8867
|
"openQueue": "Ouvrir la vue file d’attente"
|
|
8868
|
+
},
|
|
8869
|
+
"describeWork": {
|
|
8870
|
+
"title": "Décrire un nouveau travail",
|
|
8871
|
+
"textLabel": "Ce qu’il faut faire",
|
|
8872
|
+
"textHint": "Une phrase suffit. Ajoutez plus de détails si vous en avez.",
|
|
8873
|
+
"textPlaceholder": "Ajouter un journal d’audit pour les connexions. Enregistrer chaque connexion avec l’heure, l’utilisateur et le résultat.",
|
|
8874
|
+
"savedAs": "Titre de la tâche : {title}",
|
|
8875
|
+
"serviceLabel": "Service",
|
|
8876
|
+
"servicePlaceholder": "Sélectionnez un service",
|
|
8877
|
+
"kindLabel": "Type",
|
|
8878
|
+
"kind": {
|
|
8879
|
+
"feature": "Fonctionnalité",
|
|
8880
|
+
"bug": "Bogue"
|
|
8881
|
+
},
|
|
8882
|
+
"moreKinds": "Plus de types…",
|
|
8883
|
+
"changeHowItRuns": "Modifier le déroulement",
|
|
8884
|
+
"workspaceDefault": "Défaut du workspace",
|
|
8885
|
+
"defaultPipelineHint": "Le défaut du workspace : {name}. Un pipeline choisi ici le remplace pour cette tâche.",
|
|
8886
|
+
"attachContext": "Joindre du contexte (ouvre le formulaire complet)",
|
|
8887
|
+
"add": "Ajouter à la file",
|
|
8888
|
+
"addAndStart": "Ajouter et démarrer",
|
|
8889
|
+
"addedToast": "« {title} » a été ajoutée à la file"
|
|
8866
8890
|
}
|
|
8867
8891
|
}
|
package/i18n/locales/he.json
CHANGED
|
@@ -2680,7 +2680,8 @@
|
|
|
2680
2680
|
"notificationSettings": "התראות אימייל ניתוב ערוצים תיבה",
|
|
2681
2681
|
"setupChecklist": "הגדרה רשימה setup runner מודל github אחסון סביבה",
|
|
2682
2682
|
"queueView": "תור תצוגה ממתין רשימה",
|
|
2683
|
-
"boardView": "לוח קנבס מפה תצוגה תכנון"
|
|
2683
|
+
"boardView": "לוח קנבס מפה תצוגה תכנון",
|
|
2684
|
+
"describeWork": "תאר עבודה חדשה משימה הוסף צור תכונה באג"
|
|
2684
2685
|
}
|
|
2685
2686
|
},
|
|
2686
2687
|
"integrationsHub": {
|
|
@@ -8856,12 +8857,35 @@
|
|
|
8856
8857
|
"parked": "ההרצה ממתינה לאדם. פתחו את המשימה כדי לראות מה נדרש.",
|
|
8857
8858
|
"waitingOn": "ממתין ל-{deps}",
|
|
8858
8859
|
"pr": "בקשת משיכה #{number}"
|
|
8859
|
-
}
|
|
8860
|
+
},
|
|
8861
|
+
"describeWork": "תיאור עבודה חדשה"
|
|
8860
8862
|
},
|
|
8861
8863
|
"views": {
|
|
8862
8864
|
"title": "תצוגות",
|
|
8863
8865
|
"queue": "תור",
|
|
8864
8866
|
"board": "לוח",
|
|
8865
8867
|
"openQueue": "פתח את תצוגת התור"
|
|
8868
|
+
},
|
|
8869
|
+
"describeWork": {
|
|
8870
|
+
"title": "תיאור עבודה חדשה",
|
|
8871
|
+
"textLabel": "מה צריך לעשות",
|
|
8872
|
+
"textHint": "משפט אחד מספיק. הוסיפו פרטים נוספים אם יש לכם.",
|
|
8873
|
+
"textPlaceholder": "הוסיפו יומן ביקורת לאירועי כניסה. תעדו כל כניסה עם הזמן, המשתמש והתוצאה.",
|
|
8874
|
+
"savedAs": "כותרת המשימה: {title}",
|
|
8875
|
+
"serviceLabel": "שירות",
|
|
8876
|
+
"servicePlaceholder": "בחירת שירות",
|
|
8877
|
+
"kindLabel": "סוג",
|
|
8878
|
+
"kind": {
|
|
8879
|
+
"feature": "פיצ׳ר",
|
|
8880
|
+
"bug": "באג"
|
|
8881
|
+
},
|
|
8882
|
+
"moreKinds": "סוגים נוספים…",
|
|
8883
|
+
"changeHowItRuns": "שינוי אופן ההרצה",
|
|
8884
|
+
"workspaceDefault": "ברירת המחדל של סביבת העבודה",
|
|
8885
|
+
"defaultPipelineHint": "ברירת המחדל של סביבת העבודה: {name}. צינור שנבחר כאן מחליף אותה במשימה הזו.",
|
|
8886
|
+
"attachContext": "צירוף הקשר (פותח את הטופס המלא)",
|
|
8887
|
+
"add": "הוספה לתור",
|
|
8888
|
+
"addAndStart": "הוספה והתחלה",
|
|
8889
|
+
"addedToast": "“{title}” נוספה לתור"
|
|
8866
8890
|
}
|
|
8867
8891
|
}
|