@cat-factory/app 0.163.0 → 0.165.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 +4 -1
- package/app/components/board/RecurringPipelineModal.vue +9 -22
- package/app/components/focus/BlockFocusView.vue +25 -19
- package/app/components/pipeline/PipelinePicker.vue +38 -16
- package/app/components/pipeline/PipelinePreview.vue +65 -23
- package/app/components/requirements/RequirementsReviewWindow.logic.spec.ts +145 -0
- package/app/components/requirements/RequirementsReviewWindow.logic.ts +105 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +303 -215
- package/app/components/tasks/ContextIssuePicker.logic.spec.ts +89 -0
- package/app/components/tasks/ContextIssuePicker.logic.ts +74 -0
- package/app/components/tasks/ContextIssuePicker.vue +71 -21
- package/app/components/tasks/TaskImportModal.vue +2 -4
- package/app/utils/pipeline.spec.ts +41 -1
- package/app/utils/pipeline.ts +9 -0
- package/i18n/locales/de.json +13 -2
- package/i18n/locales/en.json +13 -2
- package/i18n/locales/es.json +13 -2
- package/i18n/locales/fr.json +13 -2
- package/i18n/locales/he.json +13 -2
- package/i18n/locales/it.json +13 -2
- package/i18n/locales/ja.json +13 -2
- package/i18n/locales/pl.json +13 -2
- package/i18n/locales/tr.json +13 -2
- package/i18n/locales/uk.json +13 -2
- package/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { buildSourceChoices, reconcileSource } from './ContextIssuePicker.logic'
|
|
3
|
+
import type { TaskSourceState } from '~/types/domain'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The pure source-selection behind `<ContextIssuePicker>`. Pins what the always-visible
|
|
7
|
+
* selector promises: the tracker in use is named even when it is the only one, a tracker
|
|
8
|
+
* the workspace hasn't got yet is offered as something to ADD (worded for its actual
|
|
9
|
+
* state), and the selection stays valid as the offered set changes underneath it.
|
|
10
|
+
*/
|
|
11
|
+
const state = (source: string, { available = true, enabled = true } = {}): TaskSourceState =>
|
|
12
|
+
({
|
|
13
|
+
source,
|
|
14
|
+
label: source.toUpperCase(),
|
|
15
|
+
icon: `i-lucide-${source}`,
|
|
16
|
+
available,
|
|
17
|
+
enabled,
|
|
18
|
+
credentialFields: [],
|
|
19
|
+
refLabel: '',
|
|
20
|
+
refPlaceholder: '',
|
|
21
|
+
}) as unknown as TaskSourceState
|
|
22
|
+
|
|
23
|
+
describe('buildSourceChoices', () => {
|
|
24
|
+
it('lists a single offered tracker, marked active (the selector is never hidden)', () => {
|
|
25
|
+
const groups = buildSourceChoices([state('github')], 'github')
|
|
26
|
+
expect(groups).toEqual([
|
|
27
|
+
[
|
|
28
|
+
{
|
|
29
|
+
action: 'select',
|
|
30
|
+
source: 'github',
|
|
31
|
+
label: 'GITHUB',
|
|
32
|
+
icon: 'i-lucide-github',
|
|
33
|
+
active: true,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('marks only the selected tracker active', () => {
|
|
40
|
+
const [offered] = buildSourceChoices([state('github'), state('jira')], 'jira')
|
|
41
|
+
expect(offered!.map((c) => [c.source, 'active' in c && c.active])).toEqual([
|
|
42
|
+
['github', false],
|
|
43
|
+
['jira', true],
|
|
44
|
+
])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('offers an unavailable tracker as `connect` and an available-but-off one as `enable`', () => {
|
|
48
|
+
const [, addable] = buildSourceChoices(
|
|
49
|
+
[state('github'), state('jira', { available: false }), state('linear', { enabled: false })],
|
|
50
|
+
'github',
|
|
51
|
+
)
|
|
52
|
+
expect(addable).toEqual([
|
|
53
|
+
{ action: 'connect', source: 'jira', label: 'JIRA', icon: 'i-lucide-jira' },
|
|
54
|
+
{ action: 'enable', source: 'linear', label: 'LINEAR', icon: 'i-lucide-linear' },
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('drops empty groups so the menu renders no stray separator', () => {
|
|
59
|
+
expect(buildSourceChoices([state('github')], 'github')).toHaveLength(1)
|
|
60
|
+
expect(buildSourceChoices([state('jira', { available: false })], undefined)).toHaveLength(1)
|
|
61
|
+
expect(buildSourceChoices([], undefined)).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('reconcileSource', () => {
|
|
66
|
+
it('selects the tracker the user just went off to connect, once it is offered', () => {
|
|
67
|
+
expect(reconcileSource(['github', 'jira'], 'github', 'jira')).toBe('jira')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('keeps the current selection while the connect is still pending', () => {
|
|
71
|
+
expect(reconcileSource(['github'], 'github', 'jira')).toBe('github')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('keeps a still-offered selection', () => {
|
|
75
|
+
expect(reconcileSource(['github', 'jira'], 'jira', null)).toBe('jira')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('falls back to the first offered tracker when the selection stopped being offered', () => {
|
|
79
|
+
expect(reconcileSource(['github'], 'jira', null)).toBe('github')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('selects the first offered tracker when nothing is selected yet', () => {
|
|
83
|
+
expect(reconcileSource(['github'], undefined, null)).toBe('github')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('resolves to nothing when the workspace offers no tracker at all', () => {
|
|
87
|
+
expect(reconcileSource([], 'jira', 'jira')).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { TaskSourceKind, TaskSourceState } from '~/types/domain'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure source-selection logic for `<ContextIssuePicker>`: which tracker the picker is
|
|
5
|
+
* searching, and what the source menu offers. Kept out of the component so both the
|
|
6
|
+
* template's `computed`s and the unit spec call the same code.
|
|
7
|
+
*
|
|
8
|
+
* The menu is deliberately two-tier — pick an already-offered tracker, or go and add
|
|
9
|
+
* one — because "attach a context issue" is where a user first discovers a tracker is
|
|
10
|
+
* missing, and sending them to the Integrations hub loses their in-progress task.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** One row of the picker's source menu. */
|
|
14
|
+
export type SourceChoice =
|
|
15
|
+
/** An offered tracker the picker can search right now. */
|
|
16
|
+
| { action: 'select'; source: TaskSourceKind; label: string; icon: string; active: boolean }
|
|
17
|
+
/**
|
|
18
|
+
* A configured tracker that is not offered yet, so it can be added from here. `connect`
|
|
19
|
+
* has no credential/App behind it; `enable` is connected but toggled off for the
|
|
20
|
+
* workspace. Both open the same connect modal, which serves either case — only the
|
|
21
|
+
* wording differs, so the user isn't told to "connect" something already connected.
|
|
22
|
+
*/
|
|
23
|
+
| { action: 'connect' | 'enable'; source: TaskSourceKind; label: string; icon: string }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The picker's source menu, as non-empty groups (the offered trackers, then the ones the
|
|
27
|
+
* user could add). Empty groups are dropped so the menu never renders a stray separator.
|
|
28
|
+
*/
|
|
29
|
+
export function buildSourceChoices(
|
|
30
|
+
sources: TaskSourceState[],
|
|
31
|
+
selected: TaskSourceKind | undefined,
|
|
32
|
+
): SourceChoice[][] {
|
|
33
|
+
const offered: SourceChoice[] = []
|
|
34
|
+
const addable: SourceChoice[] = []
|
|
35
|
+
for (const s of sources) {
|
|
36
|
+
if (s.available && s.enabled) {
|
|
37
|
+
offered.push({
|
|
38
|
+
action: 'select',
|
|
39
|
+
source: s.source,
|
|
40
|
+
label: s.label,
|
|
41
|
+
icon: s.icon,
|
|
42
|
+
active: s.source === selected,
|
|
43
|
+
})
|
|
44
|
+
} else {
|
|
45
|
+
addable.push({
|
|
46
|
+
action: s.available ? 'enable' : 'connect',
|
|
47
|
+
source: s.source,
|
|
48
|
+
label: s.label,
|
|
49
|
+
icon: s.icon,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [offered, addable].filter((group) => group.length > 0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The source the picker should hold once the offered set changes — after a connect, a
|
|
58
|
+
* disconnect, or the per-workspace toggle flipping elsewhere.
|
|
59
|
+
*
|
|
60
|
+
* `awaiting` is the tracker the user just left to connect: the moment it becomes offered
|
|
61
|
+
* it wins, so they land back on the source they went to add rather than on whatever was
|
|
62
|
+
* selected before. Otherwise a still-offered selection is kept, and a selection that
|
|
63
|
+
* stopped being offered falls back to the first one (searching a tracker the workspace no
|
|
64
|
+
* longer offers only yields errors).
|
|
65
|
+
*/
|
|
66
|
+
export function reconcileSource(
|
|
67
|
+
offered: TaskSourceKind[],
|
|
68
|
+
selected: TaskSourceKind | undefined,
|
|
69
|
+
awaiting: TaskSourceKind | null,
|
|
70
|
+
): TaskSourceKind | undefined {
|
|
71
|
+
if (awaiting && offered.includes(awaiting)) return awaiting
|
|
72
|
+
if (selected && offered.includes(selected)) return selected
|
|
73
|
+
return offered[0]
|
|
74
|
+
}
|
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
// It only *stages* a choice: the caller collects PendingContext items and links
|
|
7
7
|
// them once the block exists (see useContextLinking). A search hit / pasted ref
|
|
8
8
|
// carries `needsImport: true` so it's fetched + persisted before linking.
|
|
9
|
+
//
|
|
10
|
+
// The tracker being searched is ALWAYS on screen (even when the workspace offers
|
|
11
|
+
// exactly one), and its menu doubles as the "add a tracker" affordance: attaching a
|
|
12
|
+
// context issue is where a missing integration is discovered, and the connect modal
|
|
13
|
+
// opens over the caller's form rather than navigating away from it.
|
|
14
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
9
15
|
import type { SourceTask, TaskSearchResult, TaskSourceKind } from '~/types/domain'
|
|
10
16
|
import EmptyState from '~/components/common/EmptyState.vue'
|
|
17
|
+
import { buildSourceChoices, reconcileSource } from '~/components/tasks/ContextIssuePicker.logic'
|
|
11
18
|
|
|
12
19
|
const props = defineProps<{
|
|
13
20
|
/** contextKeys already staged by the caller, so they're filtered out / not re-offered. */
|
|
@@ -23,13 +30,6 @@ const props = defineProps<{
|
|
|
23
30
|
* `v-model:source`); omitted, the picker manages it internally (the add-task case).
|
|
24
31
|
*/
|
|
25
32
|
source?: TaskSourceKind
|
|
26
|
-
/**
|
|
27
|
-
* Always render the source selector, even with a single offered tracker — so the
|
|
28
|
-
* user can see *which* tracker is being searched (the "create task from issue"
|
|
29
|
-
* surface, where the source is otherwise invisible). Off by default: the inline
|
|
30
|
-
* add-task picker stays compact and only shows a selector when there's a choice.
|
|
31
|
-
*/
|
|
32
|
-
alwaysShowSource?: boolean
|
|
33
33
|
}>()
|
|
34
34
|
const emit = defineEmits<{
|
|
35
35
|
pick: [item: PendingContext]
|
|
@@ -38,12 +38,14 @@ const emit = defineEmits<{
|
|
|
38
38
|
|
|
39
39
|
const { t } = useI18n()
|
|
40
40
|
const tasks = useTasksStore()
|
|
41
|
+
const ui = useUiStore()
|
|
41
42
|
|
|
42
43
|
const chosen = computed(() => new Set(props.chosenKeys ?? []))
|
|
43
44
|
|
|
44
45
|
// Source: default to the first offered tracker. Controlled when the parent passes
|
|
45
|
-
// `source` (write-through to `update:source`), else internal.
|
|
46
|
-
//
|
|
46
|
+
// `source` (write-through to `update:source`), else internal. The selector is always
|
|
47
|
+
// rendered, single tracker or not — which tracker is being searched decides what a
|
|
48
|
+
// pasted key resolves to, so it must never be invisible.
|
|
47
49
|
const internalSource = ref<TaskSourceKind | undefined>(tasks.offeredSources[0]?.source)
|
|
48
50
|
const source = computed<TaskSourceKind | undefined>({
|
|
49
51
|
get: () => props.source ?? internalSource.value,
|
|
@@ -52,13 +54,49 @@ const source = computed<TaskSourceKind | undefined>({
|
|
|
52
54
|
if (v) emit('update:source', v)
|
|
53
55
|
},
|
|
54
56
|
})
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
+
const descriptor = computed(() => (source.value ? tasks.descriptorFor(source.value) : undefined))
|
|
58
|
+
|
|
59
|
+
// The tracker the user left to connect, so it becomes the selection the moment it turns
|
|
60
|
+
// up offered (the connect modal re-probes on success). Also the reconcile trigger for a
|
|
61
|
+
// source that stops being offered — disconnected, or toggled off in settings.
|
|
62
|
+
const awaitingConnect = ref<TaskSourceKind | null>(null)
|
|
63
|
+
function addSource(s: TaskSourceKind) {
|
|
64
|
+
awaitingConnect.value = s
|
|
65
|
+
ui.openTaskConnect(s)
|
|
66
|
+
}
|
|
67
|
+
watch(
|
|
68
|
+
() => tasks.offeredSources.map((s) => s.source),
|
|
69
|
+
(offered) => {
|
|
70
|
+
const next = reconcileSource(offered, source.value, awaitingConnect.value)
|
|
71
|
+
if (next && next === awaitingConnect.value) awaitingConnect.value = null
|
|
72
|
+
if (next !== source.value && next) source.value = next
|
|
73
|
+
},
|
|
57
74
|
)
|
|
58
|
-
|
|
59
|
-
|
|
75
|
+
|
|
76
|
+
// Two-tier menu: pick an offered tracker, or add one that isn't offered yet.
|
|
77
|
+
const sourceMenu = computed<DropdownMenuItem[][]>(() =>
|
|
78
|
+
buildSourceChoices(tasks.sources, source.value).map((group) =>
|
|
79
|
+
group.map((choice) =>
|
|
80
|
+
choice.action === 'select'
|
|
81
|
+
? {
|
|
82
|
+
label: choice.label,
|
|
83
|
+
icon: choice.icon,
|
|
84
|
+
trailingIcon: choice.active ? 'i-lucide-check' : undefined,
|
|
85
|
+
onSelect: () => {
|
|
86
|
+
source.value = choice.source
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
: {
|
|
90
|
+
label:
|
|
91
|
+
choice.action === 'enable'
|
|
92
|
+
? t('tasks.picker.enableSource', { label: choice.label })
|
|
93
|
+
: t('tasks.picker.connectSource', { label: choice.label }),
|
|
94
|
+
icon: 'i-lucide-plug',
|
|
95
|
+
onSelect: () => addSource(choice.source),
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
),
|
|
60
99
|
)
|
|
61
|
-
const descriptor = computed(() => (source.value ? tasks.descriptorFor(source.value) : undefined))
|
|
62
100
|
const searchable = computed(() => descriptor.value?.searchable ?? false)
|
|
63
101
|
|
|
64
102
|
const query = ref('')
|
|
@@ -222,13 +260,25 @@ onMounted(() => {
|
|
|
222
260
|
|
|
223
261
|
<template>
|
|
224
262
|
<div class="space-y-2 rounded-lg border border-slate-800 bg-slate-900/40 p-2">
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
263
|
+
<!-- Which tracker is being searched, always visible, plus the trackers the user
|
|
264
|
+
could add from here (each opens the connect modal over the caller's form). -->
|
|
265
|
+
<div class="flex items-center gap-1.5">
|
|
266
|
+
<span class="shrink-0 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
|
267
|
+
{{ t('tasks.picker.sourceLabel') }}
|
|
268
|
+
</span>
|
|
269
|
+
<UDropdownMenu :items="sourceMenu" :content="{ side: 'bottom', align: 'start' }">
|
|
270
|
+
<UButton
|
|
271
|
+
color="neutral"
|
|
272
|
+
variant="soft"
|
|
273
|
+
size="xs"
|
|
274
|
+
:icon="icon"
|
|
275
|
+
trailing-icon="i-lucide-chevron-down"
|
|
276
|
+
class="max-w-full"
|
|
277
|
+
>
|
|
278
|
+
<span class="truncate">{{ descriptor?.label ?? t('tasks.picker.noSource') }}</span>
|
|
279
|
+
</UButton>
|
|
280
|
+
</UDropdownMenu>
|
|
281
|
+
</div>
|
|
232
282
|
|
|
233
283
|
<UInput
|
|
234
284
|
v-model="query"
|
|
@@ -27,7 +27,7 @@ const back = useIntegrationBack(open)
|
|
|
27
27
|
|
|
28
28
|
// The tracker being browsed. Owned here (not the picker) so the epic action and the
|
|
29
29
|
// ref-input placeholder share the same selected source; passed to the picker via
|
|
30
|
-
// `v-model:source
|
|
30
|
+
// `v-model:source`, whose selector always shows (and can add) the tracker in use.
|
|
31
31
|
const source = ref<TaskSourceKind | undefined>(undefined)
|
|
32
32
|
const ref_ = ref('')
|
|
33
33
|
const importing = ref(false)
|
|
@@ -162,13 +162,11 @@ async function doSpawnEpic() {
|
|
|
162
162
|
uses for context issues: search by title, pick an already-imported one,
|
|
163
163
|
or paste a URL/key — choosing one opens the prefilled add-task form. The
|
|
164
164
|
search is scoped to the chosen container's repo (so a GitHub search stays
|
|
165
|
-
in that service's repo and a pasted URL / bare number resolves there)
|
|
166
|
-
the source selector is always shown so it's clear which tracker is in use. -->
|
|
165
|
+
in that service's repo and a pasted URL / bare number resolves there). -->
|
|
167
166
|
<UFormField :label="t('tasks.import.searchIssues')">
|
|
168
167
|
<ContextIssuePicker
|
|
169
168
|
v-model:source="source"
|
|
170
169
|
:scope-block-id="containerId"
|
|
171
|
-
always-show-source
|
|
172
170
|
@pick="createFromPick"
|
|
173
171
|
/>
|
|
174
172
|
</UFormField>
|
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { pipelineAllowedForTaskType, purposeAllowsAgentCategory } from '@cat-factory/contracts'
|
|
3
3
|
import type { Block, Pipeline } from '~/types/domain'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
pipelineAllowedForManualStart,
|
|
6
|
+
pipelineDisplaySteps,
|
|
7
|
+
pipelineGateCount,
|
|
8
|
+
} from '~/utils/pipeline'
|
|
5
9
|
|
|
6
10
|
// A minimal pipeline: only the fields the launch/task-type filters read matter here.
|
|
7
11
|
function pipeline(over: Partial<Pipeline> = {}): Pipeline {
|
|
8
12
|
return { id: 'pl_x', name: 'X', agentKinds: ['coder'], ...over } as Pipeline
|
|
9
13
|
}
|
|
10
14
|
|
|
15
|
+
// The data behind every picker's preview pane: the ordered steps a run will actually execute.
|
|
16
|
+
describe('pipelineDisplaySteps', () => {
|
|
17
|
+
it('keeps the authored order and flags the gated steps', () => {
|
|
18
|
+
const p = pipeline({
|
|
19
|
+
agentKinds: ['task-estimator', 'coder', 'reviewer'],
|
|
20
|
+
gates: [false, true, false],
|
|
21
|
+
})
|
|
22
|
+
expect(pipelineDisplaySteps(p)).toEqual([
|
|
23
|
+
{ kind: 'task-estimator', gated: false },
|
|
24
|
+
{ kind: 'coder', gated: true },
|
|
25
|
+
{ kind: 'reviewer', gated: false },
|
|
26
|
+
])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('drops steps disabled by default — they never run, so listing them would misdescribe it', () => {
|
|
30
|
+
// The short `enabled` array also pins "no entry ⇒ enabled": `tester` has none and stays.
|
|
31
|
+
const p = pipeline({ agentKinds: ['architect', 'coder', 'tester'], enabled: [false, true] })
|
|
32
|
+
expect(pipelineDisplaySteps(p).map((s) => s.kind)).toEqual(['coder', 'tester'])
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('pipelineGateCount', () => {
|
|
37
|
+
it('counts only the gates that a run can actually stop at', () => {
|
|
38
|
+
expect(pipelineGateCount(pipeline({ agentKinds: ['coder'] }))).toBe(0)
|
|
39
|
+
expect(
|
|
40
|
+
pipelineGateCount(pipeline({ agentKinds: ['coder', 'merger'], gates: [true, true] })),
|
|
41
|
+
).toBe(2)
|
|
42
|
+
// A gate declared on a disabled step gates nothing: the step is skipped at run.
|
|
43
|
+
expect(
|
|
44
|
+
pipelineGateCount(
|
|
45
|
+
pipeline({ agentKinds: ['coder', 'merger'], gates: [true, true], enabled: [true, false] }),
|
|
46
|
+
),
|
|
47
|
+
).toBe(1)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
11
51
|
describe('pipelineAllowedForTaskType', () => {
|
|
12
52
|
it('a document task offers ONLY document-purpose pipelines', () => {
|
|
13
53
|
expect(pipelineAllowedForTaskType(pipeline({ purpose: 'document' }), 'document')).toBe(true)
|
package/app/utils/pipeline.ts
CHANGED
|
@@ -29,6 +29,15 @@ export function pipelineDisplaySteps(pipeline: Pipeline): PipelineDisplayStep[]
|
|
|
29
29
|
.map(({ kind, gated }) => ({ kind, gated }))
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* How many times a run of `pipeline` will stop for a human. Counted off the DISPLAYED steps, not
|
|
34
|
+
* `pipeline.gates`, because a gate declared on a step that is disabled by default gates nothing —
|
|
35
|
+
* that step never runs, so promising a stop there would misstate what the pipeline does.
|
|
36
|
+
*/
|
|
37
|
+
export function pipelineGateCount(pipeline: Pipeline): number {
|
|
38
|
+
return pipelineDisplaySteps(pipeline).filter((s) => s.gated).length
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
// Re-exported so a picker can import the task-type gate from the same module as the
|
|
33
42
|
// launch/frame gates it composes with (the classifier itself lives in `@cat-factory/contracts`).
|
|
34
43
|
export { pipelineAllowedForTaskType }
|
package/i18n/locales/de.json
CHANGED
|
@@ -3245,7 +3245,11 @@
|
|
|
3245
3245
|
"attachByReference": "{ref} per Referenz anhängen",
|
|
3246
3246
|
"noMatches": "Keine passenden Issues.",
|
|
3247
3247
|
"emptySearchable": "Nach Titel suchen oder ein importiertes Issue auswählen.",
|
|
3248
|
-
"emptyRefOnly": "Fügen Sie eine Issue-URL oder einen -Schlüssel ein, um es anzuhängen."
|
|
3248
|
+
"emptyRefOnly": "Fügen Sie eine Issue-URL oder einen -Schlüssel ein, um es anzuhängen.",
|
|
3249
|
+
"sourceLabel": "Quelle",
|
|
3250
|
+
"noSource": "Quelle wählen",
|
|
3251
|
+
"connectSource": "{label} verbinden",
|
|
3252
|
+
"enableSource": "{label} aktivieren"
|
|
3249
3253
|
},
|
|
3250
3254
|
"contextIssues": {
|
|
3251
3255
|
"title": "Kontext-Issues",
|
|
@@ -3358,10 +3362,12 @@
|
|
|
3358
3362
|
},
|
|
3359
3363
|
"preview": {
|
|
3360
3364
|
"stepCount": "{count} Schritt | {count} Schritte",
|
|
3365
|
+
"gateCount": "{count} Freigabe | {count} Freigaben",
|
|
3361
3366
|
"gated": "Manuelle Freigabe nach diesem Schritt"
|
|
3362
3367
|
},
|
|
3363
3368
|
"picker": {
|
|
3364
|
-
"noneHint": "Keine Standard-Pipeline. Du wählst beim Ausführen der Aufgabe eine aus."
|
|
3369
|
+
"noneHint": "Keine Standard-Pipeline. Du wählst beim Ausführen der Aufgabe eine aus.",
|
|
3370
|
+
"hoverHint": "Fahre über eine Pipeline, um die Schritte zu sehen, die sie ausführt."
|
|
3365
3371
|
},
|
|
3366
3372
|
"builder": {
|
|
3367
3373
|
"title": "Pipeline-Builder",
|
|
@@ -3586,6 +3592,11 @@
|
|
|
3586
3592
|
"recommendationRequested": "Empfehlung angefordert — prüfen Sie den Vorschlag unten.",
|
|
3587
3593
|
"recommendedDefault": "Empfohlene Vorgabe: behalten, bearbeiten oder die Feststellung verwerfen.",
|
|
3588
3594
|
"needsYourInput": "Benötigt Ihre Eingabe",
|
|
3595
|
+
"group": {
|
|
3596
|
+
"action": "Benötigt Ihre Reaktion",
|
|
3597
|
+
"waiting": "Warten auf Vorschläge",
|
|
3598
|
+
"settled": "Erledigt"
|
|
3599
|
+
},
|
|
3589
3600
|
"mode": {
|
|
3590
3601
|
"answer": "Beantworten",
|
|
3591
3602
|
"dismiss": "Verwerfen",
|
package/i18n/locales/en.json
CHANGED
|
@@ -3644,7 +3644,11 @@
|
|
|
3644
3644
|
"attachByReference": "Attach {ref} by reference",
|
|
3645
3645
|
"noMatches": "No matching issues.",
|
|
3646
3646
|
"emptySearchable": "Search by title, or pick an imported issue.",
|
|
3647
|
-
"emptyRefOnly": "Paste an issue URL or key to attach it."
|
|
3647
|
+
"emptyRefOnly": "Paste an issue URL or key to attach it.",
|
|
3648
|
+
"sourceLabel": "Source",
|
|
3649
|
+
"noSource": "Choose a source",
|
|
3650
|
+
"connectSource": "Connect {label}",
|
|
3651
|
+
"enableSource": "Enable {label}"
|
|
3648
3652
|
},
|
|
3649
3653
|
"contextIssues": {
|
|
3650
3654
|
"title": "Context issues",
|
|
@@ -3763,10 +3767,12 @@
|
|
|
3763
3767
|
},
|
|
3764
3768
|
"preview": {
|
|
3765
3769
|
"stepCount": "{count} step | {count} steps",
|
|
3770
|
+
"gateCount": "{count} approval gate | {count} approval gates",
|
|
3766
3771
|
"gated": "Human approval gate after this step"
|
|
3767
3772
|
},
|
|
3768
3773
|
"picker": {
|
|
3769
|
-
"noneHint": "No default pipeline. You choose one when you run the task."
|
|
3774
|
+
"noneHint": "No default pipeline. You choose one when you run the task.",
|
|
3775
|
+
"hoverHint": "Hover a pipeline to see the steps it runs."
|
|
3770
3776
|
},
|
|
3771
3777
|
"builder": {
|
|
3772
3778
|
"title": "Pipeline builder",
|
|
@@ -4139,6 +4145,11 @@
|
|
|
4139
4145
|
"recommendationRequested": "Recommendation requested — review the suggestion below.",
|
|
4140
4146
|
"recommendedDefault": "Recommended default — keep it, edit it, or dismiss the finding.",
|
|
4141
4147
|
"needsYourInput": "Needs your input",
|
|
4148
|
+
"group": {
|
|
4149
|
+
"action": "Needs your reaction",
|
|
4150
|
+
"waiting": "Waiting on suggestions",
|
|
4151
|
+
"settled": "Handled"
|
|
4152
|
+
},
|
|
4142
4153
|
"mode": {
|
|
4143
4154
|
"answer": "Answer",
|
|
4144
4155
|
"dismiss": "Dismiss",
|
package/i18n/locales/es.json
CHANGED
|
@@ -3539,7 +3539,11 @@
|
|
|
3539
3539
|
"attachByReference": "Adjuntar {ref} por referencia",
|
|
3540
3540
|
"noMatches": "No hay incidencias coincidentes.",
|
|
3541
3541
|
"emptySearchable": "Busca por título o elige una incidencia importada.",
|
|
3542
|
-
"emptyRefOnly": "Pega la URL o clave de una incidencia para adjuntarla."
|
|
3542
|
+
"emptyRefOnly": "Pega la URL o clave de una incidencia para adjuntarla.",
|
|
3543
|
+
"sourceLabel": "Fuente",
|
|
3544
|
+
"noSource": "Elige una fuente",
|
|
3545
|
+
"connectSource": "Conectar {label}",
|
|
3546
|
+
"enableSource": "Habilitar {label}"
|
|
3543
3547
|
},
|
|
3544
3548
|
"contextIssues": {
|
|
3545
3549
|
"title": "Incidencias de contexto",
|
|
@@ -3652,10 +3656,12 @@
|
|
|
3652
3656
|
},
|
|
3653
3657
|
"preview": {
|
|
3654
3658
|
"stepCount": "{count} paso | {count} pasos",
|
|
3659
|
+
"gateCount": "{count} aprobación | {count} aprobaciones",
|
|
3655
3660
|
"gated": "Aprobación humana después de este paso"
|
|
3656
3661
|
},
|
|
3657
3662
|
"picker": {
|
|
3658
|
-
"noneHint": "Sin pipeline predeterminado. Eliges uno al ejecutar la tarea."
|
|
3663
|
+
"noneHint": "Sin pipeline predeterminado. Eliges uno al ejecutar la tarea.",
|
|
3664
|
+
"hoverHint": "Pasa el cursor por una pipeline para ver los pasos que ejecuta."
|
|
3659
3665
|
},
|
|
3660
3666
|
"builder": {
|
|
3661
3667
|
"title": "Constructor de pipelines",
|
|
@@ -3990,6 +3996,11 @@
|
|
|
3990
3996
|
"recommendationRequested": "Recomendación solicitada — revisa la sugerencia más abajo.",
|
|
3991
3997
|
"recommendedDefault": "Valor recomendado por defecto: consérvalo, edítalo o descarta el hallazgo.",
|
|
3992
3998
|
"needsYourInput": "Necesita tu intervención",
|
|
3999
|
+
"group": {
|
|
4000
|
+
"action": "Necesita tu reacción",
|
|
4001
|
+
"waiting": "Esperando sugerencias",
|
|
4002
|
+
"settled": "Gestionados"
|
|
4003
|
+
},
|
|
3993
4004
|
"mode": {
|
|
3994
4005
|
"answer": "Responder",
|
|
3995
4006
|
"dismiss": "Descartar",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -3539,7 +3539,11 @@
|
|
|
3539
3539
|
"attachByReference": "Joindre {ref} par référence",
|
|
3540
3540
|
"noMatches": "Aucun ticket correspondant.",
|
|
3541
3541
|
"emptySearchable": "Recherchez par titre ou choisissez un ticket importé.",
|
|
3542
|
-
"emptyRefOnly": "Collez l'URL ou la clé d'un ticket pour le joindre."
|
|
3542
|
+
"emptyRefOnly": "Collez l'URL ou la clé d'un ticket pour le joindre.",
|
|
3543
|
+
"sourceLabel": "Source",
|
|
3544
|
+
"noSource": "Choisir une source",
|
|
3545
|
+
"connectSource": "Connecter {label}",
|
|
3546
|
+
"enableSource": "Activer {label}"
|
|
3543
3547
|
},
|
|
3544
3548
|
"contextIssues": {
|
|
3545
3549
|
"title": "Tickets de contexte",
|
|
@@ -3652,10 +3656,12 @@
|
|
|
3652
3656
|
},
|
|
3653
3657
|
"preview": {
|
|
3654
3658
|
"stepCount": "{count} étape | {count} étapes",
|
|
3659
|
+
"gateCount": "{count} validation | {count} validations",
|
|
3655
3660
|
"gated": "Validation humaine après cette étape"
|
|
3656
3661
|
},
|
|
3657
3662
|
"picker": {
|
|
3658
|
-
"noneHint": "Aucun pipeline par défaut. Vous en choisissez un au lancement de la tâche."
|
|
3663
|
+
"noneHint": "Aucun pipeline par défaut. Vous en choisissez un au lancement de la tâche.",
|
|
3664
|
+
"hoverHint": "Survolez un pipeline pour voir les étapes qu'il exécute."
|
|
3659
3665
|
},
|
|
3660
3666
|
"builder": {
|
|
3661
3667
|
"title": "Constructeur de pipelines",
|
|
@@ -3990,6 +3996,11 @@
|
|
|
3990
3996
|
"recommendationRequested": "Recommandation demandée — consultez la suggestion ci-dessous.",
|
|
3991
3997
|
"recommendedDefault": "Valeur recommandée par défaut : conservez-la, modifiez-la ou rejetez la constatation.",
|
|
3992
3998
|
"needsYourInput": "Nécessite votre intervention",
|
|
3999
|
+
"group": {
|
|
4000
|
+
"action": "Nécessite votre réaction",
|
|
4001
|
+
"waiting": "En attente de suggestions",
|
|
4002
|
+
"settled": "Traitées"
|
|
4003
|
+
},
|
|
3993
4004
|
"mode": {
|
|
3994
4005
|
"answer": "Répondre",
|
|
3995
4006
|
"dismiss": "Ignorer",
|
package/i18n/locales/he.json
CHANGED
|
@@ -3550,7 +3550,11 @@
|
|
|
3550
3550
|
"attachByReference": "צרף {ref} לפי הפניה",
|
|
3551
3551
|
"noMatches": "אין ניושנים תואמים.",
|
|
3552
3552
|
"emptySearchable": "חפש לפי כותרת, או בחר ניושן מיובא.",
|
|
3553
|
-
"emptyRefOnly": "הדבק URL או מפתח של ניושן כדי לצרף אותו."
|
|
3553
|
+
"emptyRefOnly": "הדבק URL או מפתח של ניושן כדי לצרף אותו.",
|
|
3554
|
+
"sourceLabel": "מקור",
|
|
3555
|
+
"noSource": "בחר מקור",
|
|
3556
|
+
"connectSource": "חבר {label}",
|
|
3557
|
+
"enableSource": "הפעל {label}"
|
|
3554
3558
|
},
|
|
3555
3559
|
"contextIssues": {
|
|
3556
3560
|
"title": "ניושני הקשר",
|
|
@@ -3663,10 +3667,12 @@
|
|
|
3663
3667
|
},
|
|
3664
3668
|
"preview": {
|
|
3665
3669
|
"stepCount": "שלב {count} | {count} שלבים",
|
|
3670
|
+
"gateCount": "אישור {count} | {count} אישורים",
|
|
3666
3671
|
"gated": "אישור אנושי אחרי שלב זה"
|
|
3667
3672
|
},
|
|
3668
3673
|
"picker": {
|
|
3669
|
-
"noneHint": "אין צינור ברירת מחדל. בוחרים אחד כשמריצים את המשימה."
|
|
3674
|
+
"noneHint": "אין צינור ברירת מחדל. בוחרים אחד כשמריצים את המשימה.",
|
|
3675
|
+
"hoverHint": "העבירו את העכבר מעל צינור כדי לראות את השלבים שהוא מריץ."
|
|
3670
3676
|
},
|
|
3671
3677
|
"builder": {
|
|
3672
3678
|
"title": "בונה הצינור",
|
|
@@ -4001,6 +4007,11 @@
|
|
|
4001
4007
|
"recommendationRequested": "התבקשה המלצה — סקור את ההצעה שלהלן.",
|
|
4002
4008
|
"recommendedDefault": "ברירת מחדל מומלצת: השאר אותה, ערוך אותה או דחה את הממצא.",
|
|
4003
4009
|
"needsYourInput": "דורש את הקלט שלך",
|
|
4010
|
+
"group": {
|
|
4011
|
+
"action": "דורש את תגובתך",
|
|
4012
|
+
"waiting": "ממתין להצעות",
|
|
4013
|
+
"settled": "טופלו"
|
|
4014
|
+
},
|
|
4004
4015
|
"mode": {
|
|
4005
4016
|
"answer": "ענה",
|
|
4006
4017
|
"dismiss": "דחה",
|
package/i18n/locales/it.json
CHANGED
|
@@ -3245,7 +3245,11 @@
|
|
|
3245
3245
|
"attachByReference": "Allega {ref} per riferimento",
|
|
3246
3246
|
"noMatches": "Nessuna issue corrispondente.",
|
|
3247
3247
|
"emptySearchable": "Cerca per titolo, oppure scegli una issue importata.",
|
|
3248
|
-
"emptyRefOnly": "Incolla l'URL o la chiave di una issue per allegarla."
|
|
3248
|
+
"emptyRefOnly": "Incolla l'URL o la chiave di una issue per allegarla.",
|
|
3249
|
+
"sourceLabel": "Fonte",
|
|
3250
|
+
"noSource": "Scegli una fonte",
|
|
3251
|
+
"connectSource": "Collega {label}",
|
|
3252
|
+
"enableSource": "Abilita {label}"
|
|
3249
3253
|
},
|
|
3250
3254
|
"contextIssues": {
|
|
3251
3255
|
"title": "Issue di contesto",
|
|
@@ -3358,10 +3362,12 @@
|
|
|
3358
3362
|
},
|
|
3359
3363
|
"preview": {
|
|
3360
3364
|
"stepCount": "{count} step | {count} step",
|
|
3365
|
+
"gateCount": "{count} approvazione | {count} approvazioni",
|
|
3361
3366
|
"gated": "Approvazione umana dopo questo step"
|
|
3362
3367
|
},
|
|
3363
3368
|
"picker": {
|
|
3364
|
-
"noneHint": "Nessuna pipeline predefinita. Ne scegli una quando esegui l'attivita."
|
|
3369
|
+
"noneHint": "Nessuna pipeline predefinita. Ne scegli una quando esegui l'attivita.",
|
|
3370
|
+
"hoverHint": "Passa il mouse su una pipeline per vedere gli step che esegue."
|
|
3365
3371
|
},
|
|
3366
3372
|
"builder": {
|
|
3367
3373
|
"title": "Costruttore di pipeline",
|
|
@@ -3586,6 +3592,11 @@
|
|
|
3586
3592
|
"recommendationRequested": "Raccomandazione richiesta — rivedi il suggerimento qui sotto.",
|
|
3587
3593
|
"recommendedDefault": "Valore predefinito consigliato: mantienilo, modificalo o ignora il rilievo.",
|
|
3588
3594
|
"needsYourInput": "Richiede il tuo intervento",
|
|
3595
|
+
"group": {
|
|
3596
|
+
"action": "Richiede la tua reazione",
|
|
3597
|
+
"waiting": "In attesa di suggerimenti",
|
|
3598
|
+
"settled": "Gestiti"
|
|
3599
|
+
},
|
|
3589
3600
|
"mode": {
|
|
3590
3601
|
"answer": "Rispondi",
|
|
3591
3602
|
"dismiss": "Ignora",
|