@cat-factory/app 0.150.0 → 0.150.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -77,6 +77,44 @@ const schemeItems = computed(() => [
|
|
|
77
77
|
{ label: 'http', value: 'http' },
|
|
78
78
|
])
|
|
79
79
|
|
|
80
|
+
// A stored non-secret value is `unknown`; coerce it to the string the form field holds,
|
|
81
|
+
// defaulting a missing/wrong-typed value to '' (the same `typeof … === 'string' ? … : ''`
|
|
82
|
+
// each field used inline before).
|
|
83
|
+
function readString(v: unknown): string {
|
|
84
|
+
return typeof v === 'string' ? v : ''
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Prefill the manifest-source fields from the stored discriminated config.
|
|
88
|
+
function applyManifestSource(k: Record<string, unknown>): void {
|
|
89
|
+
const src = k.manifestSource as Record<string, unknown> | undefined
|
|
90
|
+
if (src?.type === 'separate') {
|
|
91
|
+
form.manifestSourceType = 'separate'
|
|
92
|
+
form.manifestRepo = readString(src.repo)
|
|
93
|
+
form.manifestRef = readString(src.ref)
|
|
94
|
+
form.manifestPath = readString(src.path)
|
|
95
|
+
} else if (src?.type === 'colocated') {
|
|
96
|
+
form.manifestSourceType = 'colocated'
|
|
97
|
+
form.manifestPath = readString(src.path)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Prefill the URL-derivation fields (+ scheme) from the stored discriminated config.
|
|
102
|
+
function applyUrl(k: Record<string, unknown>): void {
|
|
103
|
+
const url = k.url as Record<string, unknown> | undefined
|
|
104
|
+
if (url?.source === 'ingressTemplate') {
|
|
105
|
+
form.urlSource = 'ingressTemplate'
|
|
106
|
+
form.hostTemplate = readString(url.hostTemplate)
|
|
107
|
+
} else if (url?.source === 'ingressStatus') {
|
|
108
|
+
form.urlSource = 'ingressStatus'
|
|
109
|
+
form.ingressName = readString(url.ingressName)
|
|
110
|
+
} else if (url?.source === 'serviceStatus') {
|
|
111
|
+
form.urlSource = 'serviceStatus'
|
|
112
|
+
form.serviceName = readString(url.serviceName)
|
|
113
|
+
form.servicePort = typeof url.port === 'number' ? String(url.port) : ''
|
|
114
|
+
}
|
|
115
|
+
form.urlScheme = url && (url.scheme === 'http' || url.scheme === 'https') ? url.scheme : 'default'
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
// A registered k8s env connection exposes its non-secret config, so prefill every
|
|
81
119
|
// non-secret field from it (never the token — secrets are write-only and re-entered on
|
|
82
120
|
// update). This lets an edit change one field without re-typing the whole form.
|
|
@@ -89,36 +127,14 @@ watch(
|
|
|
89
127
|
? (c.config as { kubernetes: Record<string, unknown> }).kubernetes
|
|
90
128
|
: undefined
|
|
91
129
|
if (!k) return
|
|
92
|
-
form.label =
|
|
93
|
-
form.apiServerUrl =
|
|
94
|
-
form.caCertPem =
|
|
130
|
+
form.label = readString(k.label)
|
|
131
|
+
form.apiServerUrl = readString(k.apiServerUrl)
|
|
132
|
+
form.caCertPem = readString(k.caCertPem)
|
|
95
133
|
form.insecureSkipTlsVerify = k.insecureSkipTlsVerify === true
|
|
96
|
-
form.namespaceTemplate =
|
|
97
|
-
form.imageTemplate =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
form.manifestSourceType = 'separate'
|
|
101
|
-
form.manifestRepo = typeof src.repo === 'string' ? src.repo : ''
|
|
102
|
-
form.manifestRef = typeof src.ref === 'string' ? src.ref : ''
|
|
103
|
-
form.manifestPath = typeof src.path === 'string' ? src.path : ''
|
|
104
|
-
} else if (src?.type === 'colocated') {
|
|
105
|
-
form.manifestSourceType = 'colocated'
|
|
106
|
-
form.manifestPath = typeof src.path === 'string' ? src.path : ''
|
|
107
|
-
}
|
|
108
|
-
const url = k.url as Record<string, unknown> | undefined
|
|
109
|
-
if (url?.source === 'ingressTemplate') {
|
|
110
|
-
form.urlSource = 'ingressTemplate'
|
|
111
|
-
form.hostTemplate = typeof url.hostTemplate === 'string' ? url.hostTemplate : ''
|
|
112
|
-
} else if (url?.source === 'ingressStatus') {
|
|
113
|
-
form.urlSource = 'ingressStatus'
|
|
114
|
-
form.ingressName = typeof url.ingressName === 'string' ? url.ingressName : ''
|
|
115
|
-
} else if (url?.source === 'serviceStatus') {
|
|
116
|
-
form.urlSource = 'serviceStatus'
|
|
117
|
-
form.serviceName = typeof url.serviceName === 'string' ? url.serviceName : ''
|
|
118
|
-
form.servicePort = typeof url.port === 'number' ? String(url.port) : ''
|
|
119
|
-
}
|
|
120
|
-
form.urlScheme =
|
|
121
|
-
url && (url.scheme === 'http' || url.scheme === 'https') ? url.scheme : 'default'
|
|
134
|
+
form.namespaceTemplate = readString(k.namespaceTemplate)
|
|
135
|
+
form.imageTemplate = readString(k.imageTemplate)
|
|
136
|
+
applyManifestSource(k)
|
|
137
|
+
applyUrl(k)
|
|
122
138
|
},
|
|
123
139
|
{ immediate: true },
|
|
124
140
|
)
|
|
@@ -186,200 +186,231 @@ export function parseConflict(
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/** The non-null parsed shape of a backend conflict, as returned by {@link parseConflict}. */
|
|
190
|
+
type ParsedConflict = NonNullable<ReturnType<typeof parseConflict>>
|
|
191
|
+
|
|
189
192
|
export function usePipelineErrorToast() {
|
|
190
193
|
const toast = useToast()
|
|
191
194
|
const ui = useUiStore()
|
|
192
195
|
const { t, te } = useI18n()
|
|
193
196
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
197
|
+
// The headline case: a pipeline step's model has no usable provider. Name the
|
|
198
|
+
// offending model(s), explain no provider is available, and offer the one-click jump
|
|
199
|
+
// to the AI setup — the same remedy the startup "No AI model configured" banner gives.
|
|
200
|
+
function presentProvidersUnconfigured(conflict: ParsedConflict): void {
|
|
201
|
+
const models = Array.isArray(conflict.details.models) ? conflict.details.models : []
|
|
202
|
+
const list = models.join(', ')
|
|
203
|
+
toast.add({
|
|
204
|
+
title: t('errors.conflict.providersUnconfigured.title'),
|
|
205
|
+
description: list
|
|
206
|
+
? t('errors.conflict.providersUnconfigured.body', { models: list })
|
|
207
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
208
|
+
color: 'error',
|
|
209
|
+
icon: 'i-lucide-cpu',
|
|
210
|
+
// Stay until dismissed: an actionable toast whose remedy button vanishes on the ~5s
|
|
211
|
+
// auto-dismiss takes the one-click fix with it before the user can reach it.
|
|
212
|
+
duration: 0,
|
|
213
|
+
actions: [
|
|
214
|
+
{
|
|
215
|
+
label: t('errors.conflict.providersUnconfigured.action'),
|
|
216
|
+
icon: 'i-lucide-settings',
|
|
217
|
+
onClick: () => ui.openAiProviderSetup(),
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
})
|
|
221
|
+
}
|
|
200
222
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
223
|
+
// A pipeline step relies on binary-artifact storage (the UI Tester uploads screenshots)
|
|
224
|
+
// but the account has none configured. Explain it and offer the jump to the content-storage
|
|
225
|
+
// settings — the same shape as the providers-unconfigured case above. Prefer the localized
|
|
226
|
+
// body (it carries no runtime interpolation) so non-English users see translated copy; the
|
|
227
|
+
// raw backend prose is only the last-resort fallback when the locale lacks the key.
|
|
228
|
+
function presentBinaryStorageUnconfigured(conflict: ParsedConflict): void {
|
|
229
|
+
toast.add({
|
|
230
|
+
title: t('errors.conflict.binaryStorageUnconfigured.title'),
|
|
231
|
+
description: te('errors.conflict.binaryStorageUnconfigured.body')
|
|
232
|
+
? t('errors.conflict.binaryStorageUnconfigured.body')
|
|
233
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
234
|
+
color: 'error',
|
|
235
|
+
icon: 'i-lucide-image',
|
|
236
|
+
// Sticky, like the providers-unconfigured toast above: keep the "Configure storage"
|
|
237
|
+
// remedy reachable instead of letting it auto-dismiss.
|
|
238
|
+
duration: 0,
|
|
239
|
+
actions: [
|
|
240
|
+
{
|
|
241
|
+
label: t('errors.conflict.binaryStorageUnconfigured.action'),
|
|
242
|
+
icon: 'i-lucide-settings',
|
|
243
|
+
onClick: () => ui.openContentStorageSettings(),
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
})
|
|
247
|
+
}
|
|
227
248
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
// A pipeline includes a Deployer, but the SERVICE's ephemeral-environment config (the in-repo
|
|
250
|
+
// "what/where") is incomplete for its declared type. Steer the user straight to THAT service's
|
|
251
|
+
// environment config — the compose wizard for docker-compose, the service inspector otherwise —
|
|
252
|
+
// falling back to the workspace infrastructure window if the frame id wasn't carried.
|
|
253
|
+
function presentDeployerServiceConfig(conflict: ParsedConflict): void {
|
|
254
|
+
const frameId =
|
|
255
|
+
typeof conflict.details.frameId === 'string' ? conflict.details.frameId : undefined
|
|
256
|
+
const provisionType =
|
|
257
|
+
typeof conflict.details.provisionType === 'string'
|
|
258
|
+
? conflict.details.provisionType
|
|
259
|
+
: undefined
|
|
260
|
+
const missing = Array.isArray(conflict.details.missing)
|
|
261
|
+
? conflict.details.missing.join(', ')
|
|
262
|
+
: ''
|
|
263
|
+
toast.add({
|
|
264
|
+
title: t('errors.conflict.deployerServiceConfig.title'),
|
|
265
|
+
description: missing
|
|
266
|
+
? t('errors.conflict.deployerServiceConfig.body', { missing })
|
|
267
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
268
|
+
color: 'error',
|
|
269
|
+
icon: 'i-lucide-server',
|
|
270
|
+
// Sticky, like the other actionable conflicts: keep the "Fix configuration" jump reachable.
|
|
271
|
+
duration: 0,
|
|
272
|
+
actions: [
|
|
273
|
+
{
|
|
274
|
+
label: t('errors.conflict.deployerServiceConfig.action'),
|
|
275
|
+
icon: 'i-lucide-settings',
|
|
276
|
+
onClick: () => {
|
|
277
|
+
if (frameId && provisionType === 'docker-compose') ui.openEnvironmentSetup(frameId)
|
|
278
|
+
else if (frameId) ui.select(frameId)
|
|
279
|
+
else ui.openProviderConnection('environment')
|
|
249
280
|
},
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
})
|
|
284
|
+
}
|
|
254
285
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
: (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
actions: [
|
|
279
|
-
{
|
|
280
|
-
label: t('errors.conflict.deployerServiceConfig.action'),
|
|
281
|
-
icon: 'i-lucide-settings',
|
|
282
|
-
onClick: () => {
|
|
283
|
-
if (frameId && provisionType === 'docker-compose') ui.openEnvironmentSetup(frameId)
|
|
284
|
-
else if (frameId) ui.select(frameId)
|
|
285
|
-
else ui.openProviderConnection('environment')
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
})
|
|
290
|
-
return
|
|
291
|
-
}
|
|
286
|
+
// A pipeline includes a Deployer and the service config is sound, but no WORKSPACE handler
|
|
287
|
+
// resolves for the service's provision type (missing or ambiguous). Steer to the Infrastructure
|
|
288
|
+
// window's Test-environments tab. (Also raised by the Tester start gate — same fix applies.)
|
|
289
|
+
function presentProvisionTypeUnhandled(conflict: ParsedConflict): void {
|
|
290
|
+
const type =
|
|
291
|
+
typeof conflict.details.provisionType === 'string' ? conflict.details.provisionType : ''
|
|
292
|
+
toast.add({
|
|
293
|
+
title: t('errors.conflict.provisionTypeUnhandled.title'),
|
|
294
|
+
description: type
|
|
295
|
+
? t('errors.conflict.provisionTypeUnhandled.body', { type })
|
|
296
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
297
|
+
color: 'error',
|
|
298
|
+
icon: 'i-lucide-server-cog',
|
|
299
|
+
duration: 0,
|
|
300
|
+
actions: [
|
|
301
|
+
{
|
|
302
|
+
label: t('errors.conflict.provisionTypeUnhandled.action'),
|
|
303
|
+
icon: 'i-lucide-settings',
|
|
304
|
+
onClick: () => ui.openProviderConnection('environment'),
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
})
|
|
308
|
+
}
|
|
292
309
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
// A pipeline includes a Deployer, the config is structurally complete, but the live connection
|
|
311
|
+
// probe of the resolved deployment integration failed (unreachable endpoint / apiserver, bad
|
|
312
|
+
// token). Surface the provider's failure detail and steer to the handler to fix + re-test it.
|
|
313
|
+
function presentDeployerConnectionFailed(conflict: ParsedConflict): void {
|
|
314
|
+
const detail = typeof conflict.details.detail === 'string' ? conflict.details.detail : undefined
|
|
315
|
+
toast.add({
|
|
316
|
+
title: t('errors.conflict.deployerConnectionFailed.title'),
|
|
317
|
+
description: detail
|
|
318
|
+
? t('errors.conflict.deployerConnectionFailed.body', { detail })
|
|
319
|
+
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
320
|
+
color: 'error',
|
|
321
|
+
icon: 'i-lucide-plug',
|
|
322
|
+
duration: 0,
|
|
323
|
+
actions: [
|
|
324
|
+
{
|
|
325
|
+
label: t('errors.conflict.deployerConnectionFailed.action'),
|
|
326
|
+
icon: 'i-lucide-settings',
|
|
327
|
+
onClick: () => ui.openProviderConnection('environment'),
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Dispatch the bespoke conflict reasons (a runtime-interpolated body + a "configure X" action,
|
|
335
|
+
* each with its own key namespace — the ones excluded from `CONFLICT_INFO`). Returns `true` when
|
|
336
|
+
* the reason was one of them (and the toast was raised), `false` to fall through to the generic
|
|
337
|
+
* map. The reason values are mutually exclusive, so dispatch order is irrelevant.
|
|
338
|
+
*/
|
|
339
|
+
function presentBespokeConflict(conflict: ParsedConflict): boolean {
|
|
340
|
+
switch (conflict.reason) {
|
|
341
|
+
case 'providers_unconfigured':
|
|
342
|
+
presentProvidersUnconfigured(conflict)
|
|
343
|
+
return true
|
|
344
|
+
case 'binary_storage_unconfigured':
|
|
345
|
+
presentBinaryStorageUnconfigured(conflict)
|
|
346
|
+
return true
|
|
347
|
+
case 'deployer_service_provisioning_incomplete':
|
|
348
|
+
presentDeployerServiceConfig(conflict)
|
|
349
|
+
return true
|
|
350
|
+
case 'provision_type_unhandled':
|
|
351
|
+
presentProvisionTypeUnhandled(conflict)
|
|
352
|
+
return true
|
|
353
|
+
case 'deployer_connection_test_failed':
|
|
354
|
+
presentDeployerConnectionFailed(conflict)
|
|
355
|
+
return true
|
|
356
|
+
default:
|
|
357
|
+
return false
|
|
316
358
|
}
|
|
359
|
+
}
|
|
317
360
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Per-reason copy from the exhaustive map: a translated title + description, and a jump
|
|
363
|
+
* action for the reasons a UI panel can fix. `te` (translation-exists) guards every lookup,
|
|
364
|
+
* so a key missing from the active locale falls back rather than leaking a raw key: the
|
|
365
|
+
* title falls to the caller's key, the description to the raw backend `message`. An unknown
|
|
366
|
+
* reason (not in the map) gets the same generic title + raw-message fallback.
|
|
367
|
+
*/
|
|
368
|
+
function presentMappedConflict(conflict: ParsedConflict, fallbackTitleKey: string): void {
|
|
369
|
+
const info = conflict.reason
|
|
370
|
+
? CONFLICT_INFO[conflict.reason as Exclude<ConflictReason, BespokeConflictReason>]
|
|
371
|
+
: undefined
|
|
372
|
+
if (info) {
|
|
324
373
|
toast.add({
|
|
325
|
-
title: t(
|
|
326
|
-
description:
|
|
327
|
-
? t(
|
|
374
|
+
title: te(info.titleKey) ? t(info.titleKey) : t(fallbackTitleKey),
|
|
375
|
+
description: te(info.descriptionKey)
|
|
376
|
+
? t(info.descriptionKey)
|
|
328
377
|
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
329
|
-
color: '
|
|
330
|
-
icon: 'i-lucide-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
378
|
+
color: 'warning',
|
|
379
|
+
icon: 'i-lucide-triangle-alert',
|
|
380
|
+
// A reason with a jump action becomes an actionable, sticky toast (like the bespoke
|
|
381
|
+
// conflicts above) so the one-click remedy doesn't auto-dismiss before it's reached.
|
|
382
|
+
...(info.action
|
|
383
|
+
? {
|
|
384
|
+
duration: 0,
|
|
385
|
+
actions: [
|
|
386
|
+
{
|
|
387
|
+
label: t(info.action.labelKey),
|
|
388
|
+
icon: info.action.icon,
|
|
389
|
+
onClick: () => info.action?.run(ui),
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
}
|
|
393
|
+
: {}),
|
|
339
394
|
})
|
|
340
395
|
return
|
|
341
396
|
}
|
|
397
|
+
toast.add({
|
|
398
|
+
title: t(fallbackTitleKey),
|
|
399
|
+
description: conflict.message ?? t('errors.conflict.fallbackMessage'),
|
|
400
|
+
color: 'warning',
|
|
401
|
+
icon: 'i-lucide-triangle-alert',
|
|
402
|
+
})
|
|
403
|
+
}
|
|
342
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Present `error` as a toast. `fallbackTitleKey` is an i18n message key used for
|
|
407
|
+
* non-conflict failures and any conflict reason without a dedicated title.
|
|
408
|
+
*/
|
|
409
|
+
function present(error: unknown, fallbackTitleKey = 'common.actionFailed'): void {
|
|
410
|
+
const conflict = parseConflict(error)
|
|
343
411
|
if (conflict) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// so a key missing from the active locale falls back rather than leaking a raw key: the
|
|
347
|
-
// title falls to the caller's key, the description to the raw backend `message`. An unknown
|
|
348
|
-
// reason (not in the map) gets the same generic title + raw-message fallback.
|
|
349
|
-
const info = conflict.reason
|
|
350
|
-
? CONFLICT_INFO[conflict.reason as Exclude<ConflictReason, BespokeConflictReason>]
|
|
351
|
-
: undefined
|
|
352
|
-
if (info) {
|
|
353
|
-
toast.add({
|
|
354
|
-
title: te(info.titleKey) ? t(info.titleKey) : t(fallbackTitleKey),
|
|
355
|
-
description: te(info.descriptionKey)
|
|
356
|
-
? t(info.descriptionKey)
|
|
357
|
-
: (conflict.message ?? t('errors.conflict.fallbackMessage')),
|
|
358
|
-
color: 'warning',
|
|
359
|
-
icon: 'i-lucide-triangle-alert',
|
|
360
|
-
// A reason with a jump action becomes an actionable, sticky toast (like the bespoke
|
|
361
|
-
// conflicts above) so the one-click remedy doesn't auto-dismiss before it's reached.
|
|
362
|
-
...(info.action
|
|
363
|
-
? {
|
|
364
|
-
duration: 0,
|
|
365
|
-
actions: [
|
|
366
|
-
{
|
|
367
|
-
label: t(info.action.labelKey),
|
|
368
|
-
icon: info.action.icon,
|
|
369
|
-
onClick: () => info.action?.run(ui),
|
|
370
|
-
},
|
|
371
|
-
],
|
|
372
|
-
}
|
|
373
|
-
: {}),
|
|
374
|
-
})
|
|
375
|
-
return
|
|
376
|
-
}
|
|
377
|
-
toast.add({
|
|
378
|
-
title: t(fallbackTitleKey),
|
|
379
|
-
description: conflict.message ?? t('errors.conflict.fallbackMessage'),
|
|
380
|
-
color: 'warning',
|
|
381
|
-
icon: 'i-lucide-triangle-alert',
|
|
382
|
-
})
|
|
412
|
+
if (presentBespokeConflict(conflict)) return
|
|
413
|
+
presentMappedConflict(conflict, fallbackTitleKey)
|
|
383
414
|
return
|
|
384
415
|
}
|
|
385
416
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.150.
|
|
3
|
+
"version": "0.150.1",
|
|
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",
|