@cat-factory/app 0.233.0 → 0.234.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/app/components/settings/TaskTypeSuppressionsPanel.vue +119 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +22 -0
- package/app/composables/api/taskTypeSuppressions.ts +25 -0
- package/app/composables/useApi.ts +2 -0
- package/app/composables/usePipelineHealth.spec.ts +15 -1
- package/app/composables/usePipelineHealth.ts +13 -7
- package/app/docs/consumer-extensions.md +7 -1
- package/app/stores/pipelines.ts +22 -5
- package/app/stores/taskTypes.spec.ts +29 -0
- package/app/stores/taskTypes.ts +40 -1
- package/app/stores/workspace/hydrate.ts +5 -0
- package/app/types/domain.ts +3 -0
- package/app/utils/descriptorFields.ts +13 -27
- package/i18n/locales/de.json +8 -0
- package/i18n/locales/en.json +8 -0
- package/i18n/locales/es.json +8 -0
- package/i18n/locales/fr.json +8 -0
- package/i18n/locales/he.json +8 -0
- package/i18n/locales/it.json +8 -0
- package/i18n/locales/ja.json +8 -0
- package/i18n/locales/pl.json +8 -0
- package/i18n/locales/tr.json +8 -0
- package/i18n/locales/uk.json +8 -0
- package/package.json +2 -2
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Workspace settings: which of the deployment's REUSABLE OPERATIONS this board offers
|
|
3
|
+
// (`backend/docs/reusable-operations.md`). An org registers its operations process-wide, so a
|
|
4
|
+
// team that runs three of twenty needs a way to clear the rest out of its create picker.
|
|
5
|
+
//
|
|
6
|
+
// The list comes from its OWN read, not from the board snapshot's `customTaskTypes`: a suppressed
|
|
7
|
+
// operation is by construction absent from that catalog, so this screen is the only surface that
|
|
8
|
+
// can offer the way back. Every write answers with the whole list AND invalidates the catalog the
|
|
9
|
+
// picker renders, so a change is followed by a board refresh rather than a local patch.
|
|
10
|
+
//
|
|
11
|
+
// Labels and descriptions are DEPLOYMENT-authored English rendered verbatim (the descriptor
|
|
12
|
+
// convention); only the chrome around them is i18n.
|
|
13
|
+
import { onMounted, ref } from 'vue'
|
|
14
|
+
import type { TaskTypeSuppression } from '~/types/domain'
|
|
15
|
+
|
|
16
|
+
const { t } = useI18n()
|
|
17
|
+
const api = useApi()
|
|
18
|
+
const workspace = useWorkspaceStore()
|
|
19
|
+
const toast = useToast()
|
|
20
|
+
|
|
21
|
+
const rows = ref<TaskTypeSuppression[]>([])
|
|
22
|
+
const loading = ref(false)
|
|
23
|
+
/** The single id being written, so only its own switch shows the pending state. */
|
|
24
|
+
const busyId = ref<string | null>(null)
|
|
25
|
+
|
|
26
|
+
onMounted(() => void load())
|
|
27
|
+
|
|
28
|
+
async function load() {
|
|
29
|
+
if (!workspace.workspaceId) return
|
|
30
|
+
loading.value = true
|
|
31
|
+
try {
|
|
32
|
+
rows.value = (await api.listTaskTypeSuppressions(workspace.requireId())).taskTypes
|
|
33
|
+
} catch (e) {
|
|
34
|
+
fail(e)
|
|
35
|
+
} finally {
|
|
36
|
+
loading.value = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Flip one operation. The board snapshot's task-type catalog is derived from this state, so a
|
|
42
|
+
* successful write refreshes the workspace: leaving it stale would keep offering a hidden
|
|
43
|
+
* operation in the create picker until the next unrelated reload.
|
|
44
|
+
*/
|
|
45
|
+
async function toggle(row: TaskTypeSuppression, offered: boolean) {
|
|
46
|
+
const id = row.taskType.taskType
|
|
47
|
+
busyId.value = id
|
|
48
|
+
try {
|
|
49
|
+
const result = offered
|
|
50
|
+
? await api.restoreTaskType(workspace.requireId(), id)
|
|
51
|
+
: await api.suppressTaskType(workspace.requireId(), id)
|
|
52
|
+
rows.value = result.taskTypes
|
|
53
|
+
await workspace.refresh()
|
|
54
|
+
} catch (e) {
|
|
55
|
+
fail(e)
|
|
56
|
+
} finally {
|
|
57
|
+
busyId.value = null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fail(e: unknown) {
|
|
62
|
+
toast.add({
|
|
63
|
+
title: t('settings.taskTypeSuppressions.saveFailed'),
|
|
64
|
+
description: e instanceof Error ? e.message : String(e),
|
|
65
|
+
icon: 'i-lucide-triangle-alert',
|
|
66
|
+
color: 'error',
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<div class="space-y-4">
|
|
73
|
+
<p class="text-xs text-slate-400">
|
|
74
|
+
{{ t('settings.taskTypeSuppressions.intro') }}
|
|
75
|
+
</p>
|
|
76
|
+
|
|
77
|
+
<p v-if="loading" class="text-[11px] text-slate-500">
|
|
78
|
+
{{ t('settings.taskTypeSuppressions.loading') }}
|
|
79
|
+
</p>
|
|
80
|
+
<p v-else-if="!rows.length" class="text-[11px] text-slate-500">
|
|
81
|
+
{{ t('settings.taskTypeSuppressions.empty') }}
|
|
82
|
+
</p>
|
|
83
|
+
<ul v-else class="space-y-2" data-testid="task-type-suppressions">
|
|
84
|
+
<li
|
|
85
|
+
v-for="row in rows"
|
|
86
|
+
:key="row.taskType.taskType"
|
|
87
|
+
class="flex items-start justify-between gap-3 rounded border border-slate-800 px-3 py-2"
|
|
88
|
+
data-testid="task-type-suppression"
|
|
89
|
+
:data-task-type="row.taskType.taskType"
|
|
90
|
+
>
|
|
91
|
+
<div class="min-w-0">
|
|
92
|
+
<div class="flex items-center gap-1.5">
|
|
93
|
+
<UIcon :name="row.taskType.presentation.icon" class="h-3.5 w-3.5 shrink-0" />
|
|
94
|
+
<span class="truncate text-xs font-medium text-slate-200">
|
|
95
|
+
{{ row.taskType.presentation.label }}
|
|
96
|
+
</span>
|
|
97
|
+
<UBadge
|
|
98
|
+
v-if="row.taskType.presentation.category"
|
|
99
|
+
color="neutral"
|
|
100
|
+
variant="subtle"
|
|
101
|
+
size="sm"
|
|
102
|
+
>
|
|
103
|
+
{{ row.taskType.presentation.category }}
|
|
104
|
+
</UBadge>
|
|
105
|
+
</div>
|
|
106
|
+
<p class="mt-0.5 text-[11px] text-slate-500">
|
|
107
|
+
{{ row.taskType.presentation.description }}
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
<USwitch
|
|
111
|
+
:model-value="!row.suppressed"
|
|
112
|
+
:loading="busyId === row.taskType.taskType"
|
|
113
|
+
:aria-label="t('settings.taskTypeSuppressions.offer')"
|
|
114
|
+
@update:model-value="(offered: boolean) => toggle(row, offered)"
|
|
115
|
+
/>
|
|
116
|
+
</li>
|
|
117
|
+
</ul>
|
|
118
|
+
</div>
|
|
119
|
+
</template>
|
|
@@ -15,6 +15,7 @@ import type { InputGateMode, ReviewFrictionMode, TaskLimitMode } from '~/types/d
|
|
|
15
15
|
import RiskPolicyPanel from '~/components/settings/RiskPolicyPanel.vue'
|
|
16
16
|
import IssueTrackerPanel from '~/components/settings/IssueTrackerPanel.vue'
|
|
17
17
|
import ServiceFragmentDefaultsPanel from '~/components/settings/ServiceFragmentDefaultsPanel.vue'
|
|
18
|
+
import TaskTypeSuppressionsPanel from '~/components/settings/TaskTypeSuppressionsPanel.vue'
|
|
18
19
|
import BudgetSettings from '~/components/settings/BudgetSettings.vue'
|
|
19
20
|
import UsageSettings from '~/components/settings/UsageSettings.vue'
|
|
20
21
|
import WorkspaceMembersSettings from '~/components/layout/WorkspaceMembersSettings.vue'
|
|
@@ -29,6 +30,10 @@ const workspace = useWorkspaceStore()
|
|
|
29
30
|
const access = useWorkspaceAccess()
|
|
30
31
|
const toast = useToast()
|
|
31
32
|
const slots = useReactiveSlots<AppSlots>()
|
|
33
|
+
// Whether the deployment registers any reusable operation at all, hidden or not, so the
|
|
34
|
+
// Operations tab exists only where there is something for it to manage. Not the OFFERED catalog:
|
|
35
|
+
// hiding the last operation would then take away the only screen that un-hides one.
|
|
36
|
+
const taskTypes = useTaskTypesStore()
|
|
32
37
|
|
|
33
38
|
// The Metadata tab exists only where the deployment DECLARES custom fields — an unwired
|
|
34
39
|
// capability is invisible, not an empty tab in every deployment. Declared-but-malformed fields
|
|
@@ -86,6 +91,18 @@ const tabs = computed(() => [
|
|
|
86
91
|
icon: 'i-lucide-book-open-check',
|
|
87
92
|
slot: 'fragments',
|
|
88
93
|
},
|
|
94
|
+
// Which of the deployment's reusable operations this board offers. Only where the deployment
|
|
95
|
+
// registers any: on the stock product the tab would name a catalog that does not exist.
|
|
96
|
+
...(taskTypes.hasRegisteredOperations
|
|
97
|
+
? [
|
|
98
|
+
{
|
|
99
|
+
value: 'operations',
|
|
100
|
+
label: t('settings.workspaceSettings.tabs.operations'),
|
|
101
|
+
icon: 'i-lucide-plug',
|
|
102
|
+
slot: 'operations',
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
: []),
|
|
89
106
|
...(hasMetadataFields.value
|
|
90
107
|
? [
|
|
91
108
|
{
|
|
@@ -605,6 +622,11 @@ async function save() {
|
|
|
605
622
|
<ServiceFragmentDefaultsPanel />
|
|
606
623
|
</template>
|
|
607
624
|
|
|
625
|
+
<!-- Reusable operations this board offers (only where the deployment registers any) -->
|
|
626
|
+
<template v-if="taskTypes.hasRegisteredOperations" #operations>
|
|
627
|
+
<TaskTypeSuppressionsPanel />
|
|
628
|
+
</template>
|
|
629
|
+
|
|
608
630
|
<!-- Custom workspace metadata (only where the deployment declares fields) -->
|
|
609
631
|
<template v-if="hasMetadataFields" #metadata>
|
|
610
632
|
<WorkspaceMetadataSettings />
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listTaskTypeSuppressionsContract,
|
|
3
|
+
restoreTaskTypeContract,
|
|
4
|
+
suppressTaskTypeContract,
|
|
5
|
+
} from '@cat-factory/contracts'
|
|
6
|
+
import type { ApiContext } from './context'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Which of the deployment's REUSABLE OPERATIONS this board offers
|
|
10
|
+
* (`backend/docs/reusable-operations.md`). Every call answers with the WHOLE list, because the
|
|
11
|
+
* board snapshot's `customTaskTypes` changes with it: hiding one removes it from the picker's own
|
|
12
|
+
* catalog, so a point response would leave the caller reconciling against data it just invalidated.
|
|
13
|
+
*/
|
|
14
|
+
export function taskTypeSuppressionsApi({ send, ws }: ApiContext) {
|
|
15
|
+
return {
|
|
16
|
+
listTaskTypeSuppressions: (workspaceId: string) =>
|
|
17
|
+
send(listTaskTypeSuppressionsContract, { pathPrefix: ws(workspaceId) }),
|
|
18
|
+
|
|
19
|
+
suppressTaskType: (workspaceId: string, taskType: string) =>
|
|
20
|
+
send(suppressTaskTypeContract, { pathPrefix: ws(workspaceId), pathParams: { taskType } }),
|
|
21
|
+
|
|
22
|
+
restoreTaskType: (workspaceId: string, taskType: string) =>
|
|
23
|
+
send(restoreTaskTypeContract, { pathPrefix: ws(workspaceId), pathParams: { taskType } }),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { ApiContext } from './api/context'
|
|
|
4
4
|
import { accountsApi } from './api/accounts'
|
|
5
5
|
import { agentPromptsApi } from './api/agentPrompts'
|
|
6
6
|
import { agentSettingsApi } from './api/agentSettings'
|
|
7
|
+
import { taskTypeSuppressionsApi } from './api/taskTypeSuppressions'
|
|
7
8
|
import { platformObservabilityApi } from './api/platformObservability'
|
|
8
9
|
import { reportsApi } from './api/reports'
|
|
9
10
|
import { authApi } from './api/auth'
|
|
@@ -144,6 +145,7 @@ export function useApi() {
|
|
|
144
145
|
...presetsApi(ctx),
|
|
145
146
|
...agentPromptsApi(ctx),
|
|
146
147
|
...agentSettingsApi(ctx),
|
|
148
|
+
...taskTypeSuppressionsApi(ctx),
|
|
147
149
|
...preflightsApi(ctx),
|
|
148
150
|
...publicApiKeysApi(ctx),
|
|
149
151
|
...sharedStacksApi(ctx),
|
|
@@ -37,6 +37,7 @@ function scan(
|
|
|
37
37
|
pipelines: Pipeline[],
|
|
38
38
|
versions: Record<string, number> = {},
|
|
39
39
|
retired: { id: string; replacedBy?: string }[] = [],
|
|
40
|
+
names: Record<string, string> = {},
|
|
40
41
|
) {
|
|
41
42
|
const store = usePipelinesStore()
|
|
42
43
|
const retiredIds = new Set(retired.map((r) => r.id))
|
|
@@ -46,7 +47,7 @@ function scan(
|
|
|
46
47
|
...versions,
|
|
47
48
|
}).filter(([id]) => !retiredIds.has(id)),
|
|
48
49
|
)
|
|
49
|
-
store.hydrate(pipelines, catalogVersions, retired)
|
|
50
|
+
store.hydrate(pipelines, catalogVersions, retired, names)
|
|
50
51
|
return usePipelineHealth()
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -202,6 +203,19 @@ describe('usePipelineHealth', () => {
|
|
|
202
203
|
expect(outdated.value).toHaveLength(0)
|
|
203
204
|
})
|
|
204
205
|
|
|
206
|
+
it("names an un-adopted catalog entry from the catalog's own name map, not its id", () => {
|
|
207
|
+
// The case that made the humanised fallback wrong: a deployment's registered pipeline behind a
|
|
208
|
+
// reusable operation. `pl_org_introduce_api` humanises to "org introduce api", a name that
|
|
209
|
+
// appears nowhere else in the product, and this advisory is shown on exactly the boards that
|
|
210
|
+
// predate the operation. With the map, the offer reads as the pipeline actually is.
|
|
211
|
+
const stored = builtin(['coder', 'reviewer'], { id: 'pl_full', version: 1 })
|
|
212
|
+
const { newPipelines } = scan([stored], { pl_full: 1, pl_org_introduce_api: 1 }, [], {
|
|
213
|
+
pl_full: 'Full build',
|
|
214
|
+
pl_org_introduce_api: 'Introduce API',
|
|
215
|
+
})
|
|
216
|
+
expect(newPipelines.value).toEqual([{ id: 'pl_org_introduce_api', name: 'Introduce API' }])
|
|
217
|
+
})
|
|
218
|
+
|
|
205
219
|
it('reports no new pipelines when every catalog id is already stored', () => {
|
|
206
220
|
const stored = builtin(['coder', 'reviewer'], { id: 'pl_full', version: 1 })
|
|
207
221
|
const { newPipelines, hasIssues } = scan([stored], { pl_full: 1 })
|
|
@@ -57,12 +57,17 @@ export interface NewPipeline {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* A
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* A catalog entry's display name for the "new pipeline" advisory, used only while the entry has no
|
|
61
|
+
* stored row to take a name off. The snapshot's companion name map answers it; the humanised id
|
|
62
|
+
* (`pl_review` -> "review", rendered capitalised) is the FALLBACK for a facade that ships no map.
|
|
63
|
+
*
|
|
64
|
+
* The map is not a nicety. Humanising was fine for the shipped built-ins, whose ids read as their
|
|
65
|
+
* names, and wrong the moment a deployment registers its own: a reusable operation's
|
|
66
|
+
* `pl_org_introduce_api` was offered as "org introduce api", a name appearing nowhere else in the
|
|
67
|
+
* product, on exactly the boards that predate the operation and therefore see this advisory.
|
|
63
68
|
*/
|
|
64
|
-
function builtinPipelineName(id: string): string {
|
|
65
|
-
return id.replace(/^pl_/, '').replace(/_/g, ' ')
|
|
69
|
+
function builtinPipelineName(id: string, names: Record<string, string>): string {
|
|
70
|
+
return names[id] ?? id.replace(/^pl_/, '').replace(/_/g, ' ')
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
/** Producers a companion kind is allowed to review (inverse of {@link COMPANION_FOR_PRODUCER}). */
|
|
@@ -207,7 +212,7 @@ export function usePipelineHealth() {
|
|
|
207
212
|
const storedIds = new Set(store.pipelines.map((p) => p.id))
|
|
208
213
|
return Object.keys(store.catalogVersions)
|
|
209
214
|
.filter((id) => !storedIds.has(id))
|
|
210
|
-
.map((id) => ({ id, name: builtinPipelineName(id) }))
|
|
215
|
+
.map((id) => ({ id, name: builtinPipelineName(id, store.catalogNames) }))
|
|
211
216
|
})
|
|
212
217
|
|
|
213
218
|
// Retired built-ins this workspace still stores: the ones seeded before the withdrawal. A
|
|
@@ -238,7 +243,8 @@ export function usePipelineHealth() {
|
|
|
238
243
|
function resolveReplacement(id: string): { id: string; name: string } | undefined {
|
|
239
244
|
const stored = store.getPipeline(id)
|
|
240
245
|
if (stored) return { id, name: stored.name }
|
|
241
|
-
if (id in store.catalogVersions)
|
|
246
|
+
if (id in store.catalogVersions)
|
|
247
|
+
return { id, name: builtinPipelineName(id, store.catalogNames) }
|
|
242
248
|
return undefined
|
|
243
249
|
}
|
|
244
250
|
|
|
@@ -206,7 +206,13 @@ stray `API delivery` / `API Delivery` pair does not split a category in half.
|
|
|
206
206
|
|
|
207
207
|
Your own strings (labels, category captions, descriptions) are rendered verbatim and never enter a
|
|
208
208
|
locale catalog; only the platform's own chrome around them is i18n, which is why the "Other" heading
|
|
209
|
-
is the one caption you do not supply.
|
|
209
|
+
is the one caption you do not supply.
|
|
210
|
+
|
|
211
|
+
**A workspace admin can HIDE any registered type from that board** (Workspace settings → Operations).
|
|
212
|
+
Only backend-REGISTERED types are hideable: a type your frontend module ships as a code
|
|
213
|
+
contribution has no backend row to suppress, so it is offered on every board. If your catalog is
|
|
214
|
+
large enough that teams will want to trim it, register the types on the backend rather than
|
|
215
|
+
contributing them here. Each row carries `data-testid="task-type-row"` plus
|
|
210
216
|
`data-task-type-row="<id>"`, and each choice `data-testid="task-type-<taskType>"`, so your own e2e
|
|
211
217
|
suite can address a row and the caption inside it.
|
|
212
218
|
|
package/app/stores/pipelines.ts
CHANGED
|
@@ -32,6 +32,13 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
32
32
|
* a newer definition available (see `usePipelineHealth`).
|
|
33
33
|
*/
|
|
34
34
|
const catalogVersions = ref<Record<string, number>>({})
|
|
35
|
+
/**
|
|
36
|
+
* The catalog's own NAME per id in {@link catalogVersions}, from the same snapshot field pair.
|
|
37
|
+
* Read only where a catalog entry has no stored row to take a name off (the "new built-ins"
|
|
38
|
+
* advisory and the replacement a retirement points at); everywhere else the stored row's authored
|
|
39
|
+
* name is the answer. Empty for a facade that ships no name map.
|
|
40
|
+
*/
|
|
41
|
+
const catalogNames = ref<Record<string, string>>({})
|
|
35
42
|
/**
|
|
36
43
|
* Built-in pipelines WITHDRAWN from the catalog (`retiredPipelines()`), from the workspace
|
|
37
44
|
* snapshot. A stored pipeline whose id appears here is no longer relevant and can be REMOVED —
|
|
@@ -75,18 +82,27 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
75
82
|
const editingId = ref<string | null>(null)
|
|
76
83
|
|
|
77
84
|
/**
|
|
78
|
-
* Replace the cached pipelines (and the current built-in catalog versions +
|
|
79
|
-
* snapshot. `retired` is applied even when EMPTY, unlike `versions`: an
|
|
80
|
-
* facade shipped no retirements, and carrying the previous board's forward
|
|
81
|
-
* for a pipeline this deployment still ships.
|
|
85
|
+
* Replace the cached pipelines (and the current built-in catalog versions + names +
|
|
86
|
+
* retirements) from a snapshot. `retired` is applied even when EMPTY, unlike `versions`: an
|
|
87
|
+
* absent list means the facade shipped no retirements, and carrying the previous board's forward
|
|
88
|
+
* would offer a delete for a pipeline this deployment still ships. `names` rides `versions`,
|
|
89
|
+
* being the other half of one read.
|
|
82
90
|
*/
|
|
83
91
|
function hydrate(
|
|
84
92
|
next: Pipeline[],
|
|
85
93
|
versions?: Record<string, number>,
|
|
86
94
|
retired?: RetiredPipelineWire[],
|
|
95
|
+
names?: Record<string, string>,
|
|
87
96
|
) {
|
|
88
97
|
pipelines.value = next
|
|
89
|
-
|
|
98
|
+
// The two catalog maps move TOGETHER, because they are one snapshot read split in two and are
|
|
99
|
+
// keyed identically by construction. Assigning names on their own truthiness would let a
|
|
100
|
+
// facade that ships versions and no names leave the previous board's names indexed against
|
|
101
|
+
// this board's ids, which is the one way the pair can disagree.
|
|
102
|
+
if (versions) {
|
|
103
|
+
catalogVersions.value = versions
|
|
104
|
+
catalogNames.value = names ?? {}
|
|
105
|
+
}
|
|
90
106
|
retiredPipelines.value = retired ?? []
|
|
91
107
|
}
|
|
92
108
|
|
|
@@ -133,6 +149,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
133
149
|
return {
|
|
134
150
|
pipelines,
|
|
135
151
|
catalogVersions,
|
|
152
|
+
catalogNames,
|
|
136
153
|
retiredPipelines,
|
|
137
154
|
gateConfigForms,
|
|
138
155
|
draft,
|
|
@@ -86,6 +86,35 @@ describe('taskTypes store — custom task-type catalog (extension slice B)', ()
|
|
|
86
86
|
expect(isKnownTaskType('acme:ws1')).toBe(false)
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
+
it('still reports registered operations when the board has hidden every one of them', () => {
|
|
90
|
+
// The regression this exists for: the workspace-settings Operations tab was gated on the
|
|
91
|
+
// OFFERED catalog, which suppression empties. Hiding the last operation therefore removed the
|
|
92
|
+
// only screen that un-hides one, and nothing failed: the tab simply was not there.
|
|
93
|
+
const store = useTaskTypesStore()
|
|
94
|
+
expect(store.hasRegisteredOperations).toBe(false)
|
|
95
|
+
|
|
96
|
+
hydrate(store, [backendType('acme:kept'), backendType('acme:hidden')])
|
|
97
|
+
store.hydrateSuppressed([])
|
|
98
|
+
expect(store.hasRegisteredOperations).toBe(true)
|
|
99
|
+
|
|
100
|
+
// Every registered operation hidden: the offered catalog is empty and the screen must remain.
|
|
101
|
+
hydrate(store, [])
|
|
102
|
+
store.hydrateSuppressed(['acme:kept', 'acme:hidden'])
|
|
103
|
+
expect(store.customTaskTypes).toEqual([])
|
|
104
|
+
expect(store.hasRegisteredOperations).toBe(true)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('drops the previous board’s suppressions on re-hydrate (per-workspace state)', () => {
|
|
108
|
+
// Assigned unconditionally rather than on truthiness: carrying a hidden list forward would
|
|
109
|
+
// leave the Operations tab standing on a board that registers nothing.
|
|
110
|
+
const store = useTaskTypesStore()
|
|
111
|
+
hydrate(store, [])
|
|
112
|
+
store.hydrateSuppressed(['acme:ws1'])
|
|
113
|
+
expect(store.hasRegisteredOperations).toBe(true)
|
|
114
|
+
store.hydrateSuppressed([])
|
|
115
|
+
expect(store.hasRegisteredOperations).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
|
|
89
118
|
it('get() returns the full registration (for the create-form field descriptors)', () => {
|
|
90
119
|
const store = useTaskTypesStore()
|
|
91
120
|
hydrate(store, [
|
package/app/stores/taskTypes.ts
CHANGED
|
@@ -28,6 +28,11 @@ export const useTaskTypesStore = defineStore('taskTypes', () => {
|
|
|
28
28
|
// The active per-workspace capability manifest (shared with the agents store), or null before
|
|
29
29
|
// the first hydrate. This store reads only its own `taskTypes` slot off it.
|
|
30
30
|
const capabilitiesManifest = ref<RemoteModuleManifest<AppSlots> | null>(null)
|
|
31
|
+
// The BACKEND-registered ids this board HIDES (`snapshot.suppressedTaskTypes`). Not part of the
|
|
32
|
+
// catalog above by construction (a suppressed type must not be creatable), but the board did
|
|
33
|
+
// decide about it, and that decision is the difference between a deployment with no operations
|
|
34
|
+
// and one whose operations are all hidden. See {@link hasRegisteredOperations}.
|
|
35
|
+
const suppressedTaskTypes = ref<string[]>([])
|
|
31
36
|
|
|
32
37
|
/**
|
|
33
38
|
* The merged CUSTOM task types (consumer-slot → backend-manifest), de-duplicated and never
|
|
@@ -47,6 +52,23 @@ export const useTaskTypesStore = defineStore('taskTypes', () => {
|
|
|
47
52
|
return out
|
|
48
53
|
})
|
|
49
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Whether this deployment registers any REUSABLE OPERATION on the backend, hidden or not: what
|
|
57
|
+
* decides whether the workspace-settings Operations tab exists.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately NOT `customTaskTypes.length`. That list is what the board OFFERS, so hiding the
|
|
60
|
+
* last operation empties it and the tab that un-hides one would disappear with it, leaving no
|
|
61
|
+
* way back short of an API call. The suppressed ids are the other half of the same catalog.
|
|
62
|
+
*
|
|
63
|
+
* Consumer CODE-shipped types are excluded on purpose: they have no backend row to suppress, so
|
|
64
|
+
* a deployment that ships only those has nothing for that screen to manage.
|
|
65
|
+
*/
|
|
66
|
+
const hasRegisteredOperations = computed<boolean>(
|
|
67
|
+
() =>
|
|
68
|
+
(capabilitiesManifest.value?.slots?.taskTypes ?? []).length > 0 ||
|
|
69
|
+
suppressedTaskTypes.value.length > 0,
|
|
70
|
+
)
|
|
71
|
+
|
|
50
72
|
/** The custom types indexed by id, for a per-type lookup (e.g. the create-form field descriptors). */
|
|
51
73
|
const byTaskType = computed<Record<string, CustomTaskType>>(() =>
|
|
52
74
|
Object.fromEntries(customTaskTypes.value.map((t) => [t.taskType, t])),
|
|
@@ -84,5 +106,22 @@ export const useTaskTypesStore = defineStore('taskTypes', () => {
|
|
|
84
106
|
capabilitiesManifest.value = manifest
|
|
85
107
|
}
|
|
86
108
|
|
|
87
|
-
|
|
109
|
+
/**
|
|
110
|
+
* The suppressed-id half of the same snapshot read. Assigned unconditionally (an absent field is
|
|
111
|
+
* an empty list): this is per-WORKSPACE state, so carrying the previous board's answer forward
|
|
112
|
+
* would leave the Operations tab standing on a board that hid nothing.
|
|
113
|
+
*/
|
|
114
|
+
function hydrateSuppressed(ids: readonly string[]) {
|
|
115
|
+
suppressedTaskTypes.value = [...ids]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
customTaskTypes,
|
|
120
|
+
suppressedTaskTypes,
|
|
121
|
+
hasRegisteredOperations,
|
|
122
|
+
get,
|
|
123
|
+
registerConsumerTaskTypes,
|
|
124
|
+
hydrateCapabilities,
|
|
125
|
+
hydrateSuppressed,
|
|
126
|
+
}
|
|
88
127
|
})
|
|
@@ -71,6 +71,7 @@ export function applySnapshotToStores(snapshot: WorkspaceSnapshot, boardSince?:
|
|
|
71
71
|
snapshot.pipelines,
|
|
72
72
|
snapshot.pipelineCatalogVersions,
|
|
73
73
|
snapshot.retiredPipelines,
|
|
74
|
+
snapshot.pipelineCatalogNames,
|
|
74
75
|
)
|
|
75
76
|
useExecutionStore().hydrate(snapshot.executions, snapshot.workspace.id)
|
|
76
77
|
useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [], snapshot.workspace.id)
|
|
@@ -114,6 +115,10 @@ export function applySnapshotToStores(snapshot: WorkspaceSnapshot, boardSince?:
|
|
|
114
115
|
snapshot.binaryGeneratorsUnavailable === true,
|
|
115
116
|
)
|
|
116
117
|
useTaskTypesStore().hydrateCapabilities(capabilities)
|
|
118
|
+
// The complement of the offered catalog above: the registered operations this board HIDES. Both
|
|
119
|
+
// halves come from the one snapshot, so the settings screen can exist for a board that hid
|
|
120
|
+
// every one of them (the state whose only way back is that screen).
|
|
121
|
+
useTaskTypesStore().hydrateSuppressed(snapshot.suppressedTaskTypes ?? [])
|
|
117
122
|
// The per-step parameters each registered gate declares, so a gated step's config form in the
|
|
118
123
|
// builder comes from the gate's own registration rather than a form hard-coded per gate.
|
|
119
124
|
usePipelinesStore().hydrateGateConfigForms(snapshot.gateConfigForms ?? [])
|
package/app/types/domain.ts
CHANGED
|
@@ -67,6 +67,9 @@ export type {
|
|
|
67
67
|
TaskTypePresentation,
|
|
68
68
|
TaskTypeFieldDescriptor,
|
|
69
69
|
TaskTypeFieldOption,
|
|
70
|
+
// One row of the workspace's operation-suppression screen: a registered custom task type plus
|
|
71
|
+
// whether THIS board hides it (`backend/docs/reusable-operations.md`).
|
|
72
|
+
TaskTypeSuppression,
|
|
70
73
|
// The shared descriptor-driven form vocabulary (`contracts/src/form-fields.ts`): one field
|
|
71
74
|
// shape and one filled-value bag behind both the initiative-preset form and a custom task
|
|
72
75
|
// type's per-case form, so `DescriptorFields.vue` renders either.
|
|
@@ -1,40 +1,26 @@
|
|
|
1
|
+
import { descriptorFieldDefaults } from '@cat-factory/contracts'
|
|
1
2
|
import type { DescriptorField, DescriptorFieldValue, DescriptorFieldValues } from '~/types/domain'
|
|
2
3
|
|
|
3
4
|
// Form-side helpers over the shared descriptor-field vocabulary (`contracts/src/form-fields.ts`),
|
|
4
5
|
// used by every surface that renders one through `DescriptorFields.vue`: an initiative preset's
|
|
5
6
|
// create form and a reusable operation's per-case form on a custom task type.
|
|
6
7
|
//
|
|
7
|
-
// The RULES (visibility, validation, sanitization, prose rendering
|
|
8
|
-
// server has to agree about them. What lives here is what only a FORM
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// The RULES (visibility, validation, sanitization, prose rendering, and now default seeding) live
|
|
9
|
+
// in contracts, because the server has to agree about them. What lives here is what only a FORM
|
|
10
|
+
// decides: how one edit changes the bag. Pure functions over the value bag rather than methods
|
|
11
|
+
// inside the SFC, so the mutation rules a wrong answer would freeze on an entity are unit-testable
|
|
12
|
+
// without mounting a component.
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* The initial
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* The initial values a field list implies, for seeding a freshly opened form. A repo-detection
|
|
16
|
+
* probe's prefill and the user's own edits layer on top.
|
|
17
|
+
*
|
|
18
|
+
* The SHARED helper, not a form-side copy: the server folds the same defaults in at the creation
|
|
19
|
+
* door (`withDescriptorFieldDefaults`), so a duplicate here would be the drift that made a headless
|
|
20
|
+
* caller and this form disagree about what a descriptor's default means.
|
|
20
21
|
*/
|
|
21
22
|
export function defaultDescriptorValues(fields: readonly DescriptorField[]): DescriptorFieldValues {
|
|
22
|
-
|
|
23
|
-
for (const field of fields) {
|
|
24
|
-
if (field.type === 'checkbox-group') {
|
|
25
|
-
if (field.defaultValues?.length) values[field.key] = [...field.defaultValues]
|
|
26
|
-
} else if (field.type === 'checkbox') {
|
|
27
|
-
if (field.default === 'true') values[field.key] = true
|
|
28
|
-
} else if (field.type === 'number') {
|
|
29
|
-
const parsed = Number(field.default)
|
|
30
|
-
if (field.default !== undefined && field.default !== '' && Number.isFinite(parsed)) {
|
|
31
|
-
values[field.key] = parsed
|
|
32
|
-
}
|
|
33
|
-
} else if (field.default) {
|
|
34
|
-
values[field.key] = field.default
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return values
|
|
23
|
+
return descriptorFieldDefaults(fields)
|
|
38
24
|
}
|
|
39
25
|
|
|
40
26
|
/**
|
package/i18n/locales/de.json
CHANGED
|
@@ -353,6 +353,13 @@
|
|
|
353
353
|
"manageAccount": "Kontofragmente verwalten →"
|
|
354
354
|
}
|
|
355
355
|
},
|
|
356
|
+
"taskTypeSuppressions": {
|
|
357
|
+
"intro": "Wählen Sie aus, welche wiederverwendbaren Operationen dieses Deployments auf diesem Board angeboten werden. Wird eine ausgeblendet, verschwindet sie hier aus der Aufgabenauswahl und das Anlegen von Arbeit darunter wird abgelehnt; andere Boards bleiben unberührt.",
|
|
358
|
+
"loading": "Operationen werden geladen…",
|
|
359
|
+
"empty": "Dieses Deployment registriert keine wiederverwendbaren Operationen.",
|
|
360
|
+
"offer": "Auf diesem Board anbieten",
|
|
361
|
+
"saveFailed": "Die angebotenen Operationen konnten nicht geändert werden"
|
|
362
|
+
},
|
|
356
363
|
"issueTracker": {
|
|
357
364
|
"filing": {
|
|
358
365
|
"heading": "Wo Tickets abgelegt werden",
|
|
@@ -932,6 +939,7 @@
|
|
|
932
939
|
"merge": "Risikorichtlinien",
|
|
933
940
|
"tracker": "Issue-Tracker",
|
|
934
941
|
"fragments": "Dienst-Best-Practices",
|
|
942
|
+
"operations": "Operationen",
|
|
935
943
|
"metadata": "Metadaten",
|
|
936
944
|
"members": "Mitglieder"
|
|
937
945
|
},
|
package/i18n/locales/en.json
CHANGED
|
@@ -2914,6 +2914,13 @@
|
|
|
2914
2914
|
"manageAccount": "Manage account fragments →"
|
|
2915
2915
|
}
|
|
2916
2916
|
},
|
|
2917
|
+
"taskTypeSuppressions": {
|
|
2918
|
+
"intro": "Choose which of this deployment's reusable operations this board offers. Hiding one removes it from the create-task picker here and refuses creating work under it; other boards are unaffected.",
|
|
2919
|
+
"loading": "Loading operations…",
|
|
2920
|
+
"empty": "This deployment registers no reusable operations.",
|
|
2921
|
+
"offer": "Offer on this board",
|
|
2922
|
+
"saveFailed": "Could not change which operations this board offers"
|
|
2923
|
+
},
|
|
2917
2924
|
"issueTracker": {
|
|
2918
2925
|
"filing": {
|
|
2919
2926
|
"heading": "Where tickets are filed",
|
|
@@ -3502,6 +3509,7 @@
|
|
|
3502
3509
|
"merge": "Risk policies",
|
|
3503
3510
|
"tracker": "Issue tracker",
|
|
3504
3511
|
"fragments": "Service best practices",
|
|
3512
|
+
"operations": "Operations",
|
|
3505
3513
|
"metadata": "Metadata",
|
|
3506
3514
|
"members": "Members"
|
|
3507
3515
|
},
|
package/i18n/locales/es.json
CHANGED
|
@@ -2666,6 +2666,13 @@
|
|
|
2666
2666
|
"manageAccount": "Gestionar los fragmentos de la cuenta →"
|
|
2667
2667
|
}
|
|
2668
2668
|
},
|
|
2669
|
+
"taskTypeSuppressions": {
|
|
2670
|
+
"intro": "Elige qué operaciones reutilizables de este despliegue ofrece este tablero. Ocultar una la quita del selector de creación de tareas aquí y rechaza crear trabajo con ella; otros tableros no se ven afectados.",
|
|
2671
|
+
"loading": "Cargando operaciones…",
|
|
2672
|
+
"empty": "Este despliegue no registra ninguna operación reutilizable.",
|
|
2673
|
+
"offer": "Ofrecer en este tablero",
|
|
2674
|
+
"saveFailed": "No se pudieron cambiar las operaciones que ofrece este tablero"
|
|
2675
|
+
},
|
|
2669
2676
|
"issueTracker": {
|
|
2670
2677
|
"filing": {
|
|
2671
2678
|
"heading": "Dónde se registran los tickets",
|
|
@@ -3245,6 +3252,7 @@
|
|
|
3245
3252
|
"merge": "Políticas de riesgo",
|
|
3246
3253
|
"tracker": "Gestor de incidencias",
|
|
3247
3254
|
"fragments": "Buenas prácticas del servicio",
|
|
3255
|
+
"operations": "Operaciones",
|
|
3248
3256
|
"metadata": "Metadatos",
|
|
3249
3257
|
"members": "Miembros"
|
|
3250
3258
|
},
|
package/i18n/locales/fr.json
CHANGED
|
@@ -2666,6 +2666,13 @@
|
|
|
2666
2666
|
"manageAccount": "Gérer les fragments du compte →"
|
|
2667
2667
|
}
|
|
2668
2668
|
},
|
|
2669
|
+
"taskTypeSuppressions": {
|
|
2670
|
+
"intro": "Choisissez les opérations réutilisables de ce déploiement proposées sur ce tableau. En masquer une la retire du sélecteur de création de tâche ici et refuse la création de travail associée ; les autres tableaux ne sont pas affectés.",
|
|
2671
|
+
"loading": "Chargement des opérations…",
|
|
2672
|
+
"empty": "Ce déploiement n'enregistre aucune opération réutilisable.",
|
|
2673
|
+
"offer": "Proposer sur ce tableau",
|
|
2674
|
+
"saveFailed": "Impossible de modifier les opérations proposées sur ce tableau"
|
|
2675
|
+
},
|
|
2669
2676
|
"issueTracker": {
|
|
2670
2677
|
"filing": {
|
|
2671
2678
|
"heading": "Où les tickets sont créés",
|
|
@@ -3245,6 +3252,7 @@
|
|
|
3245
3252
|
"merge": "Politiques de risque",
|
|
3246
3253
|
"tracker": "Suivi des tickets",
|
|
3247
3254
|
"fragments": "Bonnes pratiques du service",
|
|
3255
|
+
"operations": "Opérations",
|
|
3248
3256
|
"metadata": "Métadonnées",
|
|
3249
3257
|
"members": "Membres"
|
|
3250
3258
|
},
|
package/i18n/locales/he.json
CHANGED
|
@@ -2807,6 +2807,13 @@
|
|
|
2807
2807
|
"manageAccount": "נהל מקטעי חשבון ←"
|
|
2808
2808
|
}
|
|
2809
2809
|
},
|
|
2810
|
+
"taskTypeSuppressions": {
|
|
2811
|
+
"intro": "בחרו אילו פעולות רב-פעמיות של פריסה זו יוצעו בלוח הזה. הסתרה של פעולה מסירה אותה מבורר יצירת המשימות כאן ומונעת יצירת עבודה תחתיה; לוחות אחרים אינם מושפעים.",
|
|
2812
|
+
"loading": "טוען פעולות…",
|
|
2813
|
+
"empty": "פריסה זו אינה רושמת פעולות רב-פעמיות.",
|
|
2814
|
+
"offer": "הצעה בלוח הזה",
|
|
2815
|
+
"saveFailed": "לא ניתן היה לשנות אילו פעולות מוצעות בלוח הזה"
|
|
2816
|
+
},
|
|
2810
2817
|
"issueTracker": {
|
|
2811
2818
|
"filing": {
|
|
2812
2819
|
"heading": "היכן מוגשים כרטיסים",
|
|
@@ -3386,6 +3393,7 @@
|
|
|
3386
3393
|
"merge": "מדיניות סיכון",
|
|
3387
3394
|
"tracker": "מעקב כרטיסים",
|
|
3388
3395
|
"fragments": "שיטות עבודה מומלצות לשירות",
|
|
3396
|
+
"operations": "פעולות",
|
|
3389
3397
|
"metadata": "מטא-נתונים",
|
|
3390
3398
|
"members": "חברים"
|
|
3391
3399
|
},
|
package/i18n/locales/it.json
CHANGED
|
@@ -353,6 +353,13 @@
|
|
|
353
353
|
"manageAccount": "Gestisci i frammenti dell'account →"
|
|
354
354
|
}
|
|
355
355
|
},
|
|
356
|
+
"taskTypeSuppressions": {
|
|
357
|
+
"intro": "Scegli quali operazioni riutilizzabili di questo deployment vengono offerte su questa board. Nasconderne una la rimuove dal selettore di creazione attività qui e rifiuta la creazione di lavoro con essa; le altre board non sono interessate.",
|
|
358
|
+
"loading": "Caricamento delle operazioni…",
|
|
359
|
+
"empty": "Questo deployment non registra operazioni riutilizzabili.",
|
|
360
|
+
"offer": "Offri su questa board",
|
|
361
|
+
"saveFailed": "Impossibile modificare le operazioni offerte su questa board"
|
|
362
|
+
},
|
|
356
363
|
"issueTracker": {
|
|
357
364
|
"filing": {
|
|
358
365
|
"heading": "Dove vengono registrati i ticket",
|
|
@@ -932,6 +939,7 @@
|
|
|
932
939
|
"merge": "Criteri di rischio",
|
|
933
940
|
"tracker": "Tracker degli issue",
|
|
934
941
|
"fragments": "Best practice dei servizi",
|
|
942
|
+
"operations": "Operazioni",
|
|
935
943
|
"metadata": "Metadati",
|
|
936
944
|
"members": "Membri"
|
|
937
945
|
},
|
package/i18n/locales/ja.json
CHANGED
|
@@ -2807,6 +2807,13 @@
|
|
|
2807
2807
|
"manageAccount": "アカウントのフラグメントを管理 →"
|
|
2808
2808
|
}
|
|
2809
2809
|
},
|
|
2810
|
+
"taskTypeSuppressions": {
|
|
2811
|
+
"intro": "このデプロイメントの再利用可能オペレーションのうち、このボードで提供するものを選びます。非表示にするとここのタスク作成ピッカーから消え、そのタイプでの作成は拒否されます。他のボードには影響しません。",
|
|
2812
|
+
"loading": "オペレーションを読み込んでいます…",
|
|
2813
|
+
"empty": "このデプロイメントには再利用可能オペレーションが登録されていません。",
|
|
2814
|
+
"offer": "このボードで提供する",
|
|
2815
|
+
"saveFailed": "このボードで提供するオペレーションを変更できませんでした"
|
|
2816
|
+
},
|
|
2810
2817
|
"issueTracker": {
|
|
2811
2818
|
"filing": {
|
|
2812
2819
|
"heading": "チケットの起票先",
|
|
@@ -3386,6 +3393,7 @@
|
|
|
3386
3393
|
"merge": "リスクポリシー",
|
|
3387
3394
|
"tracker": "課題トラッカー",
|
|
3388
3395
|
"fragments": "サービスのベストプラクティス",
|
|
3396
|
+
"operations": "オペレーション",
|
|
3389
3397
|
"metadata": "メタデータ",
|
|
3390
3398
|
"members": "メンバー"
|
|
3391
3399
|
},
|
package/i18n/locales/pl.json
CHANGED
|
@@ -2666,6 +2666,13 @@
|
|
|
2666
2666
|
"manageAccount": "Zarządzaj fragmentami konta →"
|
|
2667
2667
|
}
|
|
2668
2668
|
},
|
|
2669
|
+
"taskTypeSuppressions": {
|
|
2670
|
+
"intro": "Wybierz, które operacje wielokrotnego użytku tego wdrożenia są dostępne na tej tablicy. Ukrycie usuwa operację z listy wyboru przy tworzeniu zadania i blokuje tworzenie pracy tego typu; inne tablice pozostają bez zmian.",
|
|
2671
|
+
"loading": "Wczytywanie operacji…",
|
|
2672
|
+
"empty": "To wdrożenie nie rejestruje żadnych operacji wielokrotnego użytku.",
|
|
2673
|
+
"offer": "Udostępnij na tej tablicy",
|
|
2674
|
+
"saveFailed": "Nie udało się zmienić operacji dostępnych na tej tablicy"
|
|
2675
|
+
},
|
|
2669
2676
|
"issueTracker": {
|
|
2670
2677
|
"filing": {
|
|
2671
2678
|
"heading": "Gdzie są zgłaszane zgłoszenia",
|
|
@@ -3245,6 +3252,7 @@
|
|
|
3245
3252
|
"merge": "Zasady ryzyka",
|
|
3246
3253
|
"tracker": "System zgłoszeń",
|
|
3247
3254
|
"fragments": "Dobre praktyki usługi",
|
|
3255
|
+
"operations": "Operacje",
|
|
3248
3256
|
"metadata": "Metadane",
|
|
3249
3257
|
"members": "Członkowie"
|
|
3250
3258
|
},
|
package/i18n/locales/tr.json
CHANGED
|
@@ -2807,6 +2807,13 @@
|
|
|
2807
2807
|
"manageAccount": "Hesap parçalarını yönet →"
|
|
2808
2808
|
}
|
|
2809
2809
|
},
|
|
2810
|
+
"taskTypeSuppressions": {
|
|
2811
|
+
"intro": "Bu dağıtımın yeniden kullanılabilir operasyonlarından hangilerinin bu panoda sunulacağını seçin. Birini gizlemek onu buradaki görev oluşturma seçicisinden kaldırır ve o türde iş oluşturmayı reddeder; diğer panolar etkilenmez.",
|
|
2812
|
+
"loading": "Operasyonlar yükleniyor…",
|
|
2813
|
+
"empty": "Bu dağıtım hiçbir yeniden kullanılabilir operasyon kaydetmiyor.",
|
|
2814
|
+
"offer": "Bu panoda sun",
|
|
2815
|
+
"saveFailed": "Bu panoda sunulan operasyonlar değiştirilemedi"
|
|
2816
|
+
},
|
|
2810
2817
|
"issueTracker": {
|
|
2811
2818
|
"filing": {
|
|
2812
2819
|
"heading": "Biletlerin nereye açıldığı",
|
|
@@ -3386,6 +3393,7 @@
|
|
|
3386
3393
|
"merge": "Risk ilkeleri",
|
|
3387
3394
|
"tracker": "Sorun takipçisi",
|
|
3388
3395
|
"fragments": "Servis en iyi uygulamaları",
|
|
3396
|
+
"operations": "Operasyonlar",
|
|
3389
3397
|
"metadata": "Meta veriler",
|
|
3390
3398
|
"members": "Üyeler"
|
|
3391
3399
|
},
|
package/i18n/locales/uk.json
CHANGED
|
@@ -2666,6 +2666,13 @@
|
|
|
2666
2666
|
"manageAccount": "Керувати фрагментами облікового запису →"
|
|
2667
2667
|
}
|
|
2668
2668
|
},
|
|
2669
|
+
"taskTypeSuppressions": {
|
|
2670
|
+
"intro": "Оберіть, які багаторазові операції цього розгортання пропонує ця дошка. Приховування прибирає операцію з вибору при створенні завдання тут і забороняє створювати роботу під нею; інші дошки не змінюються.",
|
|
2671
|
+
"loading": "Завантаження операцій…",
|
|
2672
|
+
"empty": "Це розгортання не реєструє багаторазових операцій.",
|
|
2673
|
+
"offer": "Пропонувати на цій дошці",
|
|
2674
|
+
"saveFailed": "Не вдалося змінити операції, які пропонує ця дошка"
|
|
2675
|
+
},
|
|
2669
2676
|
"issueTracker": {
|
|
2670
2677
|
"filing": {
|
|
2671
2678
|
"heading": "Де реєструються тикети",
|
|
@@ -3245,6 +3252,7 @@
|
|
|
3245
3252
|
"merge": "Політики ризику",
|
|
3246
3253
|
"tracker": "Трекер задач",
|
|
3247
3254
|
"fragments": "Найкращі практики сервісу",
|
|
3255
|
+
"operations": "Операції",
|
|
3248
3256
|
"metadata": "Метадані",
|
|
3249
3257
|
"members": "Учасники"
|
|
3250
3258
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.234.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"valibot": "^1.4.2",
|
|
41
41
|
"vue": "3.5.40",
|
|
42
42
|
"wretch": "^3.0.9",
|
|
43
|
-
"@cat-factory/contracts": "0.
|
|
43
|
+
"@cat-factory/contracts": "0.252.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@toad-contracts/testing": "0.3.2",
|