@cat-factory/app 0.115.2 → 0.115.3
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/fragments/FragmentLibraryManager.vue +102 -46
- package/app/components/slack/SlackPanel.vue +40 -10
- package/app/utils/slackMemberMapping.spec.ts +94 -0
- package/app/utils/slackMemberMapping.ts +46 -0
- package/i18n/locales/de.json +8 -1
- package/i18n/locales/en.json +8 -1
- package/i18n/locales/es.json +8 -1
- package/i18n/locales/fr.json +8 -1
- package/i18n/locales/he.json +8 -1
- package/i18n/locales/it.json +8 -1
- package/i18n/locales/ja.json +8 -1
- package/i18n/locales/pl.json +8 -1
- package/i18n/locales/tr.json +8 -1
- package/i18n/locales/uk.json +8 -1
- package/package.json +6 -6
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// the merged catalog (built-in ∪ account ∪ workspace) an agent is selected from per
|
|
7
7
|
// run. The account scope has no resolved/merged catalog and fetches document
|
|
8
8
|
// fragments through `viaWorkspaceId` (document-source credentials are per-workspace).
|
|
9
|
-
import { computed, ref, watch } from 'vue'
|
|
9
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
10
10
|
import type {
|
|
11
11
|
DocumentSourceKind,
|
|
12
12
|
FragmentOwnerKind,
|
|
@@ -111,6 +111,24 @@ function notifyError(title: string, e: unknown) {
|
|
|
111
111
|
})
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// Per-row / per-form in-flight tracking. The store's single `library.loading` flag
|
|
115
|
+
// drove every row's button at once (UX-29) and cross-spun the add/link forms; key
|
|
116
|
+
// each async action so only the control that triggered it shows a spinner.
|
|
117
|
+
const busyRows = reactive(new Set<string>())
|
|
118
|
+
const rowBusy = (key: string) => busyRows.has(key)
|
|
119
|
+
async function withRow(key: string, fn: () => Promise<void>) {
|
|
120
|
+
if (busyRows.has(key)) return
|
|
121
|
+
busyRows.add(key)
|
|
122
|
+
try {
|
|
123
|
+
await fn()
|
|
124
|
+
} finally {
|
|
125
|
+
busyRows.delete(key)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const creating = ref(false)
|
|
129
|
+
const linkingDoc = ref(false)
|
|
130
|
+
const linkingSource = ref(false)
|
|
131
|
+
|
|
114
132
|
// ---- create a hand-authored fragment --------------------------------------
|
|
115
133
|
const draft = ref({ title: '', summary: '', body: '', tags: '' })
|
|
116
134
|
const draftValid = computed(
|
|
@@ -119,6 +137,7 @@ const draftValid = computed(
|
|
|
119
137
|
|
|
120
138
|
async function createFragment() {
|
|
121
139
|
if (!draftValid.value) return
|
|
140
|
+
creating.value = true
|
|
122
141
|
try {
|
|
123
142
|
await library.create({
|
|
124
143
|
title: draft.value.title.trim(),
|
|
@@ -133,6 +152,8 @@ async function createFragment() {
|
|
|
133
152
|
toast.add({ title: t('fragments.toast.added'), icon: 'i-lucide-check' })
|
|
134
153
|
} catch (e) {
|
|
135
154
|
notifyError(t('fragments.toast.addFailed'), e)
|
|
155
|
+
} finally {
|
|
156
|
+
creating.value = false
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
|
|
@@ -146,12 +167,14 @@ async function removeFragment(id: string) {
|
|
|
146
167
|
icon: 'i-lucide-trash-2',
|
|
147
168
|
})
|
|
148
169
|
if (!ok) return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
await withRow(`remove:${id}`, async () => {
|
|
171
|
+
try {
|
|
172
|
+
await library.remove(id)
|
|
173
|
+
toast.add({ title: t('fragments.toast.removed'), icon: 'i-lucide-trash-2' })
|
|
174
|
+
} catch (e) {
|
|
175
|
+
notifyError(t('fragments.toast.removeFailed'), e)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
155
178
|
}
|
|
156
179
|
|
|
157
180
|
// ---- document-backed (living) fragments -----------------------------------
|
|
@@ -201,6 +224,7 @@ const documentFragments = computed(() => library.fragments.filter((f) => f.docum
|
|
|
201
224
|
|
|
202
225
|
async function linkDocumentFragment() {
|
|
203
226
|
if (!docDraftValid.value) return
|
|
227
|
+
linkingDoc.value = true
|
|
204
228
|
try {
|
|
205
229
|
await library.createDocumentFragment({
|
|
206
230
|
source: docDraft.value.source as DocumentSourceKind,
|
|
@@ -214,16 +238,20 @@ async function linkDocumentFragment() {
|
|
|
214
238
|
toast.add({ title: t('fragments.toast.documentLinked'), icon: 'i-lucide-link' })
|
|
215
239
|
} catch (e) {
|
|
216
240
|
notifyError(t('fragments.toast.linkDocumentFailed'), e)
|
|
241
|
+
} finally {
|
|
242
|
+
linkingDoc.value = false
|
|
217
243
|
}
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
async function refreshFragment(id: string) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
await withRow(`refresh:${id}`, async () => {
|
|
248
|
+
try {
|
|
249
|
+
await library.refreshDocumentFragment(id)
|
|
250
|
+
toast.add({ title: t('fragments.toast.refreshed'), icon: 'i-lucide-refresh-cw' })
|
|
251
|
+
} catch (e) {
|
|
252
|
+
notifyError(t('fragments.toast.refreshFailed'), e)
|
|
253
|
+
}
|
|
254
|
+
})
|
|
227
255
|
}
|
|
228
256
|
|
|
229
257
|
// ---- repo sources ----------------------------------------------------------
|
|
@@ -263,6 +291,7 @@ async function linkSource() {
|
|
|
263
291
|
if (!ownerName) return
|
|
264
292
|
const dirPath =
|
|
265
293
|
(githubReady.value ? sourceDir.value : manualSource.value.dirPath.trim()) || undefined
|
|
294
|
+
linkingSource.value = true
|
|
266
295
|
try {
|
|
267
296
|
const source = await library.linkSource({
|
|
268
297
|
repoOwner: ownerName.owner,
|
|
@@ -271,48 +300,71 @@ async function linkSource() {
|
|
|
271
300
|
gitRef: sourceRef.value.trim() || undefined,
|
|
272
301
|
})
|
|
273
302
|
resetSourceDraft()
|
|
303
|
+
// Auto-sync the freshly-linked source via the store method directly (not the
|
|
304
|
+
// `syncSource` row wrapper): a failure here should surface as a link failure, and
|
|
305
|
+
// the form-level `linkingSource` spinner already covers the whole operation.
|
|
274
306
|
await library.syncSource(source.id)
|
|
275
307
|
toast.add({ title: t('fragments.toast.sourceLinked'), icon: 'i-lucide-git-branch' })
|
|
276
308
|
} catch (e) {
|
|
277
309
|
notifyError(t('fragments.toast.linkSourceFailed'), e)
|
|
310
|
+
} finally {
|
|
311
|
+
linkingSource.value = false
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
314
|
|
|
281
315
|
async function syncSource(id: string) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
316
|
+
await withRow(`sync:${id}`, async () => {
|
|
317
|
+
try {
|
|
318
|
+
const result = await library.syncSource(id)
|
|
319
|
+
toast.add({
|
|
320
|
+
title: t('fragments.toast.synced', {
|
|
321
|
+
updated: result.upserted,
|
|
322
|
+
removed: result.tombstoned,
|
|
323
|
+
}),
|
|
324
|
+
icon: 'i-lucide-refresh-cw',
|
|
325
|
+
color: 'info',
|
|
326
|
+
})
|
|
327
|
+
} catch (e) {
|
|
328
|
+
notifyError(t('fragments.toast.syncFailed'), e)
|
|
329
|
+
}
|
|
330
|
+
})
|
|
295
331
|
}
|
|
296
332
|
|
|
297
333
|
async function checkSource(id: string) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
334
|
+
await withRow(`check:${id}`, async () => {
|
|
335
|
+
try {
|
|
336
|
+
const status = await library.checkSource(id)
|
|
337
|
+
toast.add({
|
|
338
|
+
title: status.changed
|
|
339
|
+
? t('fragments.toast.changesAvailable')
|
|
340
|
+
: t('fragments.toast.upToDate'),
|
|
341
|
+
icon: status.changed ? 'i-lucide-bell-dot' : 'i-lucide-check',
|
|
342
|
+
})
|
|
343
|
+
} catch (e) {
|
|
344
|
+
notifyError(t('fragments.toast.checkSourceFailed'), e)
|
|
345
|
+
}
|
|
346
|
+
})
|
|
307
347
|
}
|
|
308
348
|
|
|
309
349
|
async function unlinkSource(id: string) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
350
|
+
const source = library.sources.find((s) => s.id === id)
|
|
351
|
+
const repo = source ? `${source.repoOwner}/${source.repoName}` : ''
|
|
352
|
+
const ok = await confirm({
|
|
353
|
+
title: t('fragments.confirmUnlinkSource.title'),
|
|
354
|
+
description: t('fragments.confirmUnlinkSource.body', { repo }),
|
|
355
|
+
variant: 'destructive',
|
|
356
|
+
confirmLabel: t('fragments.confirmUnlinkSource.confirm'),
|
|
357
|
+
icon: 'i-lucide-unplug',
|
|
358
|
+
})
|
|
359
|
+
if (!ok) return
|
|
360
|
+
await withRow(`unlink:${id}`, async () => {
|
|
361
|
+
try {
|
|
362
|
+
await library.unlinkSource(id)
|
|
363
|
+
toast.add({ title: t('fragments.toast.sourceUnlinked'), icon: 'i-lucide-unplug' })
|
|
364
|
+
} catch (e) {
|
|
365
|
+
notifyError(t('fragments.toast.unlinkSourceFailed'), e)
|
|
366
|
+
}
|
|
367
|
+
})
|
|
316
368
|
}
|
|
317
369
|
</script>
|
|
318
370
|
|
|
@@ -417,6 +469,7 @@ async function unlinkSource(id: string) {
|
|
|
417
469
|
color="error"
|
|
418
470
|
variant="ghost"
|
|
419
471
|
class="ms-auto"
|
|
472
|
+
:loading="rowBusy(`remove:${f.id}`)"
|
|
420
473
|
@click="removeFragment(f.id)"
|
|
421
474
|
/>
|
|
422
475
|
</div>
|
|
@@ -446,7 +499,7 @@ async function unlinkSource(id: string) {
|
|
|
446
499
|
icon="i-lucide-plus"
|
|
447
500
|
size="sm"
|
|
448
501
|
:disabled="!draftValid"
|
|
449
|
-
:loading="
|
|
502
|
+
:loading="creating"
|
|
450
503
|
class="self-start"
|
|
451
504
|
@click="createFragment"
|
|
452
505
|
>
|
|
@@ -487,7 +540,7 @@ async function unlinkSource(id: string) {
|
|
|
487
540
|
icon="i-lucide-refresh-cw"
|
|
488
541
|
size="xs"
|
|
489
542
|
variant="ghost"
|
|
490
|
-
:loading="
|
|
543
|
+
:loading="rowBusy(`refresh:${f.id}`)"
|
|
491
544
|
:title="t('fragments.documents.refreshTitle')"
|
|
492
545
|
@click="refreshFragment(f.id)"
|
|
493
546
|
/>
|
|
@@ -496,6 +549,7 @@ async function unlinkSource(id: string) {
|
|
|
496
549
|
size="xs"
|
|
497
550
|
color="error"
|
|
498
551
|
variant="ghost"
|
|
552
|
+
:loading="rowBusy(`remove:${f.id}`)"
|
|
499
553
|
@click="removeFragment(f.id)"
|
|
500
554
|
/>
|
|
501
555
|
</div>
|
|
@@ -553,7 +607,7 @@ async function unlinkSource(id: string) {
|
|
|
553
607
|
icon="i-lucide-link"
|
|
554
608
|
size="sm"
|
|
555
609
|
:disabled="!docDraftValid"
|
|
556
|
-
:loading="
|
|
610
|
+
:loading="linkingDoc"
|
|
557
611
|
class="self-start"
|
|
558
612
|
@click="linkDocumentFragment"
|
|
559
613
|
>
|
|
@@ -598,13 +652,14 @@ async function unlinkSource(id: string) {
|
|
|
598
652
|
icon="i-lucide-search-check"
|
|
599
653
|
size="xs"
|
|
600
654
|
variant="ghost"
|
|
655
|
+
:loading="rowBusy(`check:${s.id}`)"
|
|
601
656
|
@click="checkSource(s.id)"
|
|
602
657
|
/>
|
|
603
658
|
<UButton
|
|
604
659
|
icon="i-lucide-refresh-cw"
|
|
605
660
|
size="xs"
|
|
606
661
|
variant="ghost"
|
|
607
|
-
:loading="
|
|
662
|
+
:loading="rowBusy(`sync:${s.id}`)"
|
|
608
663
|
@click="syncSource(s.id)"
|
|
609
664
|
/>
|
|
610
665
|
<UButton
|
|
@@ -612,6 +667,7 @@ async function unlinkSource(id: string) {
|
|
|
612
667
|
size="xs"
|
|
613
668
|
color="error"
|
|
614
669
|
variant="ghost"
|
|
670
|
+
:loading="rowBusy(`unlink:${s.id}`)"
|
|
615
671
|
@click="unlinkSource(s.id)"
|
|
616
672
|
/>
|
|
617
673
|
</div>
|
|
@@ -669,7 +725,7 @@ async function unlinkSource(id: string) {
|
|
|
669
725
|
icon="i-lucide-link"
|
|
670
726
|
size="sm"
|
|
671
727
|
:disabled="!sourceValid"
|
|
672
|
-
:loading="
|
|
728
|
+
:loading="linkingSource"
|
|
673
729
|
class="self-start"
|
|
674
730
|
@click="linkSource"
|
|
675
731
|
>
|
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
// - Mentions (per-account): toggle + GitHub-user-id → Slack-member-id map.
|
|
7
7
|
import { computed, reactive, ref, watch } from 'vue'
|
|
8
8
|
import type { NotificationType } from '~/types/notifications'
|
|
9
|
-
import type {
|
|
9
|
+
import type { SlackMemberRole, SlackRoute } from '~/types/slack'
|
|
10
|
+
import {
|
|
11
|
+
type MemberRow,
|
|
12
|
+
emptyMemberRow,
|
|
13
|
+
hasHalfFilledRow,
|
|
14
|
+
toMemberEntries,
|
|
15
|
+
toMemberRow,
|
|
16
|
+
} from '~/utils/slackMemberMapping'
|
|
10
17
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
11
18
|
import SecretInput from '~/components/common/SecretInput.vue'
|
|
12
19
|
|
|
@@ -60,9 +67,15 @@ const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
|
60
67
|
initiative: { enabled: false, channel: '' },
|
|
61
68
|
})
|
|
62
69
|
const mentionsEnabled = ref(false)
|
|
63
|
-
|
|
70
|
+
// Editable member rows carry a client-only stable `uid` (see `slackMemberMapping`) so
|
|
71
|
+
// a mid-list delete keys the v-model by identity, not the array index (index keys
|
|
72
|
+
// silently rebound a neighbour's inputs — UX-23).
|
|
73
|
+
let uidSeq = 0
|
|
74
|
+
const nextUid = () => `m${++uidSeq}`
|
|
75
|
+
const mapping = ref<MemberRow[]>([])
|
|
64
76
|
const tokenInput = ref('')
|
|
65
77
|
const busy = ref(false)
|
|
78
|
+
const connectingOAuth = ref(false)
|
|
66
79
|
|
|
67
80
|
function notifyError(title: string, e: unknown) {
|
|
68
81
|
toast.add({
|
|
@@ -84,7 +97,7 @@ watch(
|
|
|
84
97
|
routes[type] = slack.settings?.routes[type] ?? { enabled: false, channel: '' }
|
|
85
98
|
}
|
|
86
99
|
mentionsEnabled.value = slack.settings?.mentionsEnabled ?? false
|
|
87
|
-
mapping.value = slack.memberMapping.map((e) => (
|
|
100
|
+
mapping.value = slack.memberMapping.map((e) => toMemberRow(e, nextUid()))
|
|
88
101
|
} catch (e) {
|
|
89
102
|
notifyError(t('slack.error.loadSettings'), e)
|
|
90
103
|
}
|
|
@@ -94,9 +107,13 @@ watch(
|
|
|
94
107
|
)
|
|
95
108
|
|
|
96
109
|
async function connectViaOAuth() {
|
|
110
|
+
connectingOAuth.value = true
|
|
97
111
|
try {
|
|
112
|
+
// On success the browser navigates away, so `connectingOAuth` never resets here —
|
|
113
|
+
// it only clears on the error path below.
|
|
98
114
|
window.location.href = await slack.installUrl()
|
|
99
115
|
} catch (e) {
|
|
116
|
+
connectingOAuth.value = false
|
|
100
117
|
notifyError(t('slack.error.startOAuth'), e)
|
|
101
118
|
}
|
|
102
119
|
}
|
|
@@ -144,17 +161,29 @@ async function saveRouting() {
|
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
function addMapping() {
|
|
147
|
-
mapping.value.push(
|
|
164
|
+
mapping.value.push(emptyMemberRow(nextUid()))
|
|
148
165
|
}
|
|
149
|
-
function removeMapping(
|
|
150
|
-
mapping.value.
|
|
166
|
+
function removeMapping(uid: string) {
|
|
167
|
+
mapping.value = mapping.value.filter((e) => e.uid !== uid)
|
|
151
168
|
}
|
|
152
169
|
async function saveMapping() {
|
|
170
|
+
// A partially-filled row (one id present, the other blank) used to be silently
|
|
171
|
+
// dropped on save (UX-23) — block instead so the user doesn't lose the entry. A
|
|
172
|
+
// fully-empty row is just an unused slot and is ignored.
|
|
173
|
+
if (hasHalfFilledRow(mapping.value)) {
|
|
174
|
+
toast.add({
|
|
175
|
+
title: t('slack.members.incompleteTitle'),
|
|
176
|
+
description: t('slack.members.incompleteBody'),
|
|
177
|
+
icon: 'i-lucide-triangle-alert',
|
|
178
|
+
color: 'warning',
|
|
179
|
+
})
|
|
180
|
+
return
|
|
181
|
+
}
|
|
153
182
|
busy.value = true
|
|
154
183
|
try {
|
|
155
|
-
const entries = mapping.value
|
|
184
|
+
const entries = toMemberEntries(mapping.value)
|
|
156
185
|
await slack.updateMemberMapping(entries)
|
|
157
|
-
mapping.value = slack.memberMapping.map((e) => (
|
|
186
|
+
mapping.value = slack.memberMapping.map((e) => toMemberRow(e, nextUid()))
|
|
158
187
|
toast.add({ title: t('slack.toast.mapSaved'), icon: 'i-lucide-check', color: 'success' })
|
|
159
188
|
} catch (e) {
|
|
160
189
|
notifyError(t('slack.error.saveMap'), e)
|
|
@@ -181,6 +210,7 @@ async function saveMapping() {
|
|
|
181
210
|
v-if="slack.oauthEnabled"
|
|
182
211
|
color="primary"
|
|
183
212
|
icon="i-lucide-slack"
|
|
213
|
+
:loading="connectingOAuth"
|
|
184
214
|
@click="connectViaOAuth"
|
|
185
215
|
>
|
|
186
216
|
{{ t('slack.connect.addToSlack') }}
|
|
@@ -287,7 +317,7 @@ async function saveMapping() {
|
|
|
287
317
|
</template>
|
|
288
318
|
</i18n-t>
|
|
289
319
|
</p>
|
|
290
|
-
<div v-for="
|
|
320
|
+
<div v-for="entry in mapping" :key="entry.uid" class="flex items-center gap-2">
|
|
291
321
|
<UInput
|
|
292
322
|
v-model="entry.userId"
|
|
293
323
|
size="sm"
|
|
@@ -312,7 +342,7 @@ async function saveMapping() {
|
|
|
312
342
|
variant="ghost"
|
|
313
343
|
size="xs"
|
|
314
344
|
icon="i-lucide-trash-2"
|
|
315
|
-
@click="removeMapping(
|
|
345
|
+
@click="removeMapping(entry.uid)"
|
|
316
346
|
/>
|
|
317
347
|
</div>
|
|
318
348
|
<div class="flex justify-between">
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import type { SlackMemberMappingEntry } from '~/types/slack'
|
|
3
|
+
import {
|
|
4
|
+
type MemberRow,
|
|
5
|
+
emptyMemberRow,
|
|
6
|
+
hasHalfFilledRow,
|
|
7
|
+
toMemberEntries,
|
|
8
|
+
toMemberRow,
|
|
9
|
+
} from './slackMemberMapping'
|
|
10
|
+
|
|
11
|
+
const row = (partial: Partial<MemberRow> & { uid: string }): MemberRow => ({
|
|
12
|
+
userId: '',
|
|
13
|
+
slackUserId: '',
|
|
14
|
+
role: 'engineering',
|
|
15
|
+
...partial,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('hasHalfFilledRow', () => {
|
|
19
|
+
it('is false for fully-filled rows', () => {
|
|
20
|
+
expect(hasHalfFilledRow([row({ uid: 'a', userId: 'usr_1', slackUserId: 'U1' })])).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('is false for fully-empty rows (unused slots)', () => {
|
|
24
|
+
expect(hasHalfFilledRow([row({ uid: 'a' }), row({ uid: 'b' })])).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('is true when only the user id is filled', () => {
|
|
28
|
+
expect(hasHalfFilledRow([row({ uid: 'a', userId: 'usr_1' })])).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('is true when only the Slack id is filled', () => {
|
|
32
|
+
expect(hasHalfFilledRow([row({ uid: 'a', slackUserId: 'U1' })])).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('treats whitespace-only ids as blank', () => {
|
|
36
|
+
expect(hasHalfFilledRow([row({ uid: 'a', userId: ' ', slackUserId: 'U1' })])).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('flags a half-filled row among valid ones', () => {
|
|
40
|
+
expect(
|
|
41
|
+
hasHalfFilledRow([
|
|
42
|
+
row({ uid: 'a', userId: 'usr_1', slackUserId: 'U1' }),
|
|
43
|
+
row({ uid: 'b', userId: 'usr_2' }),
|
|
44
|
+
]),
|
|
45
|
+
).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('toMemberEntries', () => {
|
|
50
|
+
it('keeps fully-filled rows, drops empty slots, and strips uid', () => {
|
|
51
|
+
const rows: MemberRow[] = [
|
|
52
|
+
row({ uid: 'a', userId: 'usr_1', slackUserId: 'U1', role: 'product' }),
|
|
53
|
+
row({ uid: 'b' }), // empty slot — dropped
|
|
54
|
+
row({ uid: 'c', userId: 'usr_2', slackUserId: 'U2' }),
|
|
55
|
+
]
|
|
56
|
+
expect(toMemberEntries(rows)).toEqual([
|
|
57
|
+
{ userId: 'usr_1', slackUserId: 'U1', role: 'product' },
|
|
58
|
+
{ userId: 'usr_2', slackUserId: 'U2', role: 'engineering' },
|
|
59
|
+
])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does not leak the client-only uid onto the wire payload', () => {
|
|
63
|
+
const [entry] = toMemberEntries([row({ uid: 'a', userId: 'usr_1', slackUserId: 'U1' })])
|
|
64
|
+
expect(entry).not.toHaveProperty('uid')
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('toMemberRow', () => {
|
|
69
|
+
it('stamps the uid and defaults a missing role', () => {
|
|
70
|
+
const entry: SlackMemberMappingEntry = { userId: 'usr_1', slackUserId: 'U1' }
|
|
71
|
+
expect(toMemberRow(entry, 'm1')).toEqual({
|
|
72
|
+
userId: 'usr_1',
|
|
73
|
+
slackUserId: 'U1',
|
|
74
|
+
role: 'engineering',
|
|
75
|
+
uid: 'm1',
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('preserves an explicit role', () => {
|
|
80
|
+
const entry: SlackMemberMappingEntry = { userId: 'usr_1', slackUserId: 'U1', role: 'product' }
|
|
81
|
+
expect(toMemberRow(entry, 'm2').role).toBe('product')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('emptyMemberRow', () => {
|
|
86
|
+
it('builds a blank engineering row with the given uid', () => {
|
|
87
|
+
expect(emptyMemberRow('m9')).toEqual({
|
|
88
|
+
uid: 'm9',
|
|
89
|
+
userId: '',
|
|
90
|
+
slackUserId: '',
|
|
91
|
+
role: 'engineering',
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SlackMemberMappingEntry } from '~/types/slack'
|
|
2
|
+
|
|
3
|
+
// Pure helpers for the Slack member-mapping editor (SlackPanel.vue). Extracted so
|
|
4
|
+
// the save-time integrity rules (UX-23) can be unit-tested without mounting the panel.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An editable member-map row: the wire entry plus a client-only stable `uid` so a
|
|
8
|
+
* mid-list delete keys the `v-model` by identity, not the array index (index keys
|
|
9
|
+
* silently rebound a neighbour's inputs — UX-23).
|
|
10
|
+
*/
|
|
11
|
+
export type MemberRow = SlackMemberMappingEntry & { uid: string }
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when any row has exactly one of the two ids filled. A half-entered mapping
|
|
15
|
+
* used to be silently dropped on save (UX-23); the panel blocks the save instead so
|
|
16
|
+
* the user doesn't lose it. A fully-empty row is an unused slot, not half-filled.
|
|
17
|
+
*/
|
|
18
|
+
export function hasHalfFilledRow(
|
|
19
|
+
rows: readonly Pick<MemberRow, 'userId' | 'slackUserId'>[],
|
|
20
|
+
): boolean {
|
|
21
|
+
return rows.some((e) => Boolean(e.userId.trim()) !== Boolean(e.slackUserId.trim()))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The rows to persist: fully-filled only (empty slots dropped), with the client-only
|
|
26
|
+
* `uid` stripped from the wire payload.
|
|
27
|
+
*/
|
|
28
|
+
export function toMemberEntries(rows: readonly MemberRow[]): SlackMemberMappingEntry[] {
|
|
29
|
+
return rows
|
|
30
|
+
.filter((e) => e.userId.trim() && e.slackUserId.trim())
|
|
31
|
+
.map(({ uid: _uid, ...entry }) => entry)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Wrap a stored wire entry as an editable row: stamp a stable `uid` and default the
|
|
36
|
+
* `role` (absent on older maps) so the initial load and the post-save reload produce
|
|
37
|
+
* identical rows rather than drifting on the default.
|
|
38
|
+
*/
|
|
39
|
+
export function toMemberRow(entry: SlackMemberMappingEntry, uid: string): MemberRow {
|
|
40
|
+
return { role: 'engineering', ...entry, uid }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A fresh, empty editable row stamped with the given stable `uid`. */
|
|
44
|
+
export function emptyMemberRow(uid: string): MemberRow {
|
|
45
|
+
return { uid, userId: '', slackUserId: '', role: 'engineering' }
|
|
46
|
+
}
|
package/i18n/locales/de.json
CHANGED
|
@@ -3368,6 +3368,11 @@
|
|
|
3368
3368
|
"title": "Dieses Fragment löschen?",
|
|
3369
3369
|
"body": "\"{name}\" wird entfernt. Dies kann nicht rückgängig gemacht werden."
|
|
3370
3370
|
},
|
|
3371
|
+
"confirmUnlinkSource": {
|
|
3372
|
+
"title": "Diese Quelle trennen?",
|
|
3373
|
+
"body": "„{repo}“ und die davon synchronisierten Richtlinien-Fragmente werden entfernt. Du kannst sie später erneut verknüpfen.",
|
|
3374
|
+
"confirm": "Trennen"
|
|
3375
|
+
},
|
|
3371
3376
|
"unavailable": "Die Prompt-Fragment-Bibliothek ist für dieses Deployment nicht aktiviert."
|
|
3372
3377
|
},
|
|
3373
3378
|
"brainstorm": {
|
|
@@ -3838,7 +3843,9 @@
|
|
|
3838
3843
|
"userIdPlaceholder": "Benutzer-ID (usr_...)",
|
|
3839
3844
|
"slackIdPlaceholder": "Slack-Mitglieds-ID (U...)",
|
|
3840
3845
|
"add": "Mitglied hinzufügen",
|
|
3841
|
-
"save": "Zuordnung speichern"
|
|
3846
|
+
"save": "Zuordnung speichern",
|
|
3847
|
+
"incompleteTitle": "Unvollständige Mitgliederzeile",
|
|
3848
|
+
"incompleteBody": "Gib vor dem Speichern sowohl die Benutzer-ID als auch die Slack-Mitglieds-ID ein oder entferne die Zeile."
|
|
3842
3849
|
},
|
|
3843
3850
|
"routable": {
|
|
3844
3851
|
"merge_review": "Merge-Review",
|
package/i18n/locales/en.json
CHANGED
|
@@ -2986,7 +2986,9 @@
|
|
|
2986
2986
|
"userIdPlaceholder": "User id (usr_...)",
|
|
2987
2987
|
"slackIdPlaceholder": "Slack member id (U...)",
|
|
2988
2988
|
"add": "Add member",
|
|
2989
|
-
"save": "Save map"
|
|
2989
|
+
"save": "Save map",
|
|
2990
|
+
"incompleteTitle": "Incomplete member row",
|
|
2991
|
+
"incompleteBody": "Fill in both the user id and the Slack member id, or remove the row, before saving."
|
|
2990
2992
|
},
|
|
2991
2993
|
"routable": {
|
|
2992
2994
|
"merge_review": "Merge review",
|
|
@@ -4319,6 +4321,11 @@
|
|
|
4319
4321
|
"title": "Delete this fragment?",
|
|
4320
4322
|
"body": "\"{name}\" will be removed. This can't be undone."
|
|
4321
4323
|
},
|
|
4324
|
+
"confirmUnlinkSource": {
|
|
4325
|
+
"title": "Unlink this source?",
|
|
4326
|
+
"body": "\"{repo}\" and the guideline fragments it synced will be removed. You can re-link it later.",
|
|
4327
|
+
"confirm": "Unlink"
|
|
4328
|
+
},
|
|
4322
4329
|
"unavailable": "The prompt-fragment library isn't enabled for this deployment."
|
|
4323
4330
|
},
|
|
4324
4331
|
"sandbox": {
|
package/i18n/locales/es.json
CHANGED
|
@@ -2899,7 +2899,9 @@
|
|
|
2899
2899
|
"userIdPlaceholder": "Id de usuario (usr_...)",
|
|
2900
2900
|
"slackIdPlaceholder": "Id de miembro de Slack (U...)",
|
|
2901
2901
|
"add": "Anadir miembro",
|
|
2902
|
-
"save": "Guardar mapa"
|
|
2902
|
+
"save": "Guardar mapa",
|
|
2903
|
+
"incompleteTitle": "Fila de miembro incompleta",
|
|
2904
|
+
"incompleteBody": "Antes de guardar, completa tanto el id de usuario como el id de miembro de Slack, o elimina la fila."
|
|
2903
2905
|
},
|
|
2904
2906
|
"routable": {
|
|
2905
2907
|
"merge_review": "Revision de fusion",
|
|
@@ -4152,6 +4154,11 @@
|
|
|
4152
4154
|
"title": "¿Eliminar este fragmento?",
|
|
4153
4155
|
"body": "Se eliminará \"{name}\". Esta acción no se puede deshacer."
|
|
4154
4156
|
},
|
|
4157
|
+
"confirmUnlinkSource": {
|
|
4158
|
+
"title": "¿Desvincular esta fuente?",
|
|
4159
|
+
"body": "«{repo}» y los fragmentos de directrices que sincronizó se eliminarán. Puedes volver a vincularla más tarde.",
|
|
4160
|
+
"confirm": "Desvincular"
|
|
4161
|
+
},
|
|
4155
4162
|
"unavailable": "La biblioteca de fragmentos de prompt no está habilitada en esta implementación."
|
|
4156
4163
|
},
|
|
4157
4164
|
"sandbox": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -2899,7 +2899,9 @@
|
|
|
2899
2899
|
"userIdPlaceholder": "Id utilisateur (usr_...)",
|
|
2900
2900
|
"slackIdPlaceholder": "Id de membre Slack (U...)",
|
|
2901
2901
|
"add": "Ajouter un membre",
|
|
2902
|
-
"save": "Enregistrer la carte"
|
|
2902
|
+
"save": "Enregistrer la carte",
|
|
2903
|
+
"incompleteTitle": "Ligne de membre incomplète",
|
|
2904
|
+
"incompleteBody": "Avant d’enregistrer, renseignez l’id utilisateur et l’id de membre Slack, ou supprimez la ligne."
|
|
2903
2905
|
},
|
|
2904
2906
|
"routable": {
|
|
2905
2907
|
"merge_review": "Revue de fusion",
|
|
@@ -4152,6 +4154,11 @@
|
|
|
4152
4154
|
"title": "Supprimer ce fragment ?",
|
|
4153
4155
|
"body": "\"{name}\" sera supprimé. Cette action est irréversible."
|
|
4154
4156
|
},
|
|
4157
|
+
"confirmUnlinkSource": {
|
|
4158
|
+
"title": "Dissocier cette source ?",
|
|
4159
|
+
"body": "« {repo} » et les fragments de directives qu’elle a synchronisés seront supprimés. Vous pourrez la relier plus tard.",
|
|
4160
|
+
"confirm": "Dissocier"
|
|
4161
|
+
},
|
|
4155
4162
|
"unavailable": "La bibliothèque de fragments de prompt n'est pas activée pour ce déploiement."
|
|
4156
4163
|
},
|
|
4157
4164
|
"sandbox": {
|
package/i18n/locales/he.json
CHANGED
|
@@ -2910,7 +2910,9 @@
|
|
|
2910
2910
|
"userIdPlaceholder": "מזהה משתמש (usr_...)",
|
|
2911
2911
|
"slackIdPlaceholder": "מזהה חבר Slack (U...)",
|
|
2912
2912
|
"add": "הוסף חבר",
|
|
2913
|
-
"save": "שמור מפה"
|
|
2913
|
+
"save": "שמור מפה",
|
|
2914
|
+
"incompleteTitle": "שורת חבר חסרה",
|
|
2915
|
+
"incompleteBody": "לפני השמירה מלא גם את מזהה המשתמש וגם את מזהה החבר ב-Slack, או הסר את השורה."
|
|
2914
2916
|
},
|
|
2915
2917
|
"routable": {
|
|
2916
2918
|
"merge_review": "סקירת מיזוג",
|
|
@@ -4163,6 +4165,11 @@
|
|
|
4163
4165
|
"title": "למחוק את המקטע הזה?",
|
|
4164
4166
|
"body": "\"{name}\" יימחק. לא ניתן לבטל פעולה זו."
|
|
4165
4167
|
},
|
|
4168
|
+
"confirmUnlinkSource": {
|
|
4169
|
+
"title": "לנתק את המקור הזה?",
|
|
4170
|
+
"body": "המקור \"{repo}\" והקטעים המנחים שסונכרנו ממנו יוסרו. ניתן לקשר אותו מחדש בהמשך.",
|
|
4171
|
+
"confirm": "ניתוק"
|
|
4172
|
+
},
|
|
4166
4173
|
"unavailable": "ספריית מקטעי הפרומפט אינה מופעלת בפריסה זו."
|
|
4167
4174
|
},
|
|
4168
4175
|
"sandbox": {
|
package/i18n/locales/it.json
CHANGED
|
@@ -3368,6 +3368,11 @@
|
|
|
3368
3368
|
"title": "Eliminare questo frammento?",
|
|
3369
3369
|
"body": "\"{name}\" verra rimosso. Questa operazione non puo essere annullata."
|
|
3370
3370
|
},
|
|
3371
|
+
"confirmUnlinkSource": {
|
|
3372
|
+
"title": "Scollegare questa fonte?",
|
|
3373
|
+
"body": "«{repo}» e i frammenti di linee guida che ha sincronizzato verranno rimossi. Potrai ricollegarla in seguito.",
|
|
3374
|
+
"confirm": "Scollega"
|
|
3375
|
+
},
|
|
3371
3376
|
"unavailable": "La libreria di frammenti di prompt non e abilitata per questo deployment."
|
|
3372
3377
|
},
|
|
3373
3378
|
"brainstorm": {
|
|
@@ -3838,7 +3843,9 @@
|
|
|
3838
3843
|
"userIdPlaceholder": "Id utente (usr_...)",
|
|
3839
3844
|
"slackIdPlaceholder": "Id membro Slack (U...)",
|
|
3840
3845
|
"add": "Aggiungi membro",
|
|
3841
|
-
"save": "Salva mappa"
|
|
3846
|
+
"save": "Salva mappa",
|
|
3847
|
+
"incompleteTitle": "Riga membro incompleta",
|
|
3848
|
+
"incompleteBody": "Prima di salvare, compila sia l’id utente sia l’id membro Slack, oppure rimuovi la riga."
|
|
3842
3849
|
},
|
|
3843
3850
|
"routable": {
|
|
3844
3851
|
"merge_review": "Revisione del merge",
|
package/i18n/locales/ja.json
CHANGED
|
@@ -2911,7 +2911,9 @@
|
|
|
2911
2911
|
"userIdPlaceholder": "ユーザーID (usr_...)",
|
|
2912
2912
|
"slackIdPlaceholder": "SlackメンバーID (U...)",
|
|
2913
2913
|
"add": "メンバーを追加",
|
|
2914
|
-
"save": "マップを保存"
|
|
2914
|
+
"save": "マップを保存",
|
|
2915
|
+
"incompleteTitle": "メンバー行が未入力です",
|
|
2916
|
+
"incompleteBody": "保存する前に、ユーザー ID と Slack メンバー ID の両方を入力するか、行を削除してください。"
|
|
2915
2917
|
},
|
|
2916
2918
|
"routable": {
|
|
2917
2919
|
"merge_review": "マージレビュー",
|
|
@@ -4164,6 +4166,11 @@
|
|
|
4164
4166
|
"title": "このフラグメントを削除しますか?",
|
|
4165
4167
|
"body": "「{name}」が削除されます。 この操作は取り消せません。"
|
|
4166
4168
|
},
|
|
4169
|
+
"confirmUnlinkSource": {
|
|
4170
|
+
"title": "このソースのリンクを解除しますか?",
|
|
4171
|
+
"body": "「{repo}」と、そこから同期されたガイドライン断片が削除されます。後で再リンクできます。",
|
|
4172
|
+
"confirm": "リンク解除"
|
|
4173
|
+
},
|
|
4167
4174
|
"unavailable": "このデプロイではプロンプトフラグメントライブラリが有効になっていません。"
|
|
4168
4175
|
},
|
|
4169
4176
|
"sandbox": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -2899,7 +2899,9 @@
|
|
|
2899
2899
|
"userIdPlaceholder": "Id uzytkownika (usr_...)",
|
|
2900
2900
|
"slackIdPlaceholder": "Id czlonka Slacka (U...)",
|
|
2901
2901
|
"add": "Dodaj czlonka",
|
|
2902
|
-
"save": "Zapisz mape"
|
|
2902
|
+
"save": "Zapisz mape",
|
|
2903
|
+
"incompleteTitle": "Niekompletny wiersz członka",
|
|
2904
|
+
"incompleteBody": "Przed zapisaniem uzupełnij zarówno identyfikator użytkownika, jak i identyfikator członka Slack, albo usuń wiersz."
|
|
2903
2905
|
},
|
|
2904
2906
|
"routable": {
|
|
2905
2907
|
"merge_review": "Przeglad scalenia",
|
|
@@ -4152,6 +4154,11 @@
|
|
|
4152
4154
|
"title": "Usunąć ten fragment?",
|
|
4153
4155
|
"body": "\"{name}\" zostanie usunięty. Tej operacji nie można cofnąć."
|
|
4154
4156
|
},
|
|
4157
|
+
"confirmUnlinkSource": {
|
|
4158
|
+
"title": "Odłączyć to źródło?",
|
|
4159
|
+
"body": "„{repo}” oraz zsynchronizowane z niego fragmenty wytycznych zostaną usunięte. Możesz połączyć je ponownie później.",
|
|
4160
|
+
"confirm": "Odłącz"
|
|
4161
|
+
},
|
|
4155
4162
|
"unavailable": "Biblioteka fragmentów promptów nie jest włączona w tym wdrożeniu."
|
|
4156
4163
|
},
|
|
4157
4164
|
"sandbox": {
|
package/i18n/locales/tr.json
CHANGED
|
@@ -2911,7 +2911,9 @@
|
|
|
2911
2911
|
"userIdPlaceholder": "Kullanıcı kimliği (usr_...)",
|
|
2912
2912
|
"slackIdPlaceholder": "Slack üye kimliği (U...)",
|
|
2913
2913
|
"add": "Üye ekle",
|
|
2914
|
-
"save": "Eşlemeyi kaydet"
|
|
2914
|
+
"save": "Eşlemeyi kaydet",
|
|
2915
|
+
"incompleteTitle": "Eksik üye satırı",
|
|
2916
|
+
"incompleteBody": "Kaydetmeden önce hem kullanıcı kimliğini hem de Slack üye kimliğini girin ya da satırı kaldırın."
|
|
2915
2917
|
},
|
|
2916
2918
|
"routable": {
|
|
2917
2919
|
"merge_review": "Birleştirme incelemesi",
|
|
@@ -4164,6 +4166,11 @@
|
|
|
4164
4166
|
"title": "Bu parça silinsin mi?",
|
|
4165
4167
|
"body": "\"{name}\" kaldırılacak. Bu işlem geri alınamaz."
|
|
4166
4168
|
},
|
|
4169
|
+
"confirmUnlinkSource": {
|
|
4170
|
+
"title": "Bu kaynağın bağlantısı kaldırılsın mı?",
|
|
4171
|
+
"body": "\"{repo}\" ve onun eşitlediği kılavuz parçaları kaldırılacak. Daha sonra yeniden bağlayabilirsiniz.",
|
|
4172
|
+
"confirm": "Bağlantıyı kaldır"
|
|
4173
|
+
},
|
|
4167
4174
|
"unavailable": "Bu dağıtımda istem parçası kütüphanesi etkin değil."
|
|
4168
4175
|
},
|
|
4169
4176
|
"sandbox": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -2899,7 +2899,9 @@
|
|
|
2899
2899
|
"userIdPlaceholder": "Id користувача (usr_...)",
|
|
2900
2900
|
"slackIdPlaceholder": "Id учасника Slack (U...)",
|
|
2901
2901
|
"add": "Додати учасника",
|
|
2902
|
-
"save": "Зберегти карту"
|
|
2902
|
+
"save": "Зберегти карту",
|
|
2903
|
+
"incompleteTitle": "Неповний рядок учасника",
|
|
2904
|
+
"incompleteBody": "Перед збереженням заповніть і ідентифікатор користувача, і ідентифікатор учасника Slack, або видаліть рядок."
|
|
2903
2905
|
},
|
|
2904
2906
|
"routable": {
|
|
2905
2907
|
"merge_review": "Перевірка злиття",
|
|
@@ -4152,6 +4154,11 @@
|
|
|
4152
4154
|
"title": "Видалити цей фрагмент?",
|
|
4153
4155
|
"body": "\"{name}\" буде видалено. Цю дію не можна скасувати."
|
|
4154
4156
|
},
|
|
4157
|
+
"confirmUnlinkSource": {
|
|
4158
|
+
"title": "Від’єднати це джерело?",
|
|
4159
|
+
"body": "«{repo}» та синхронізовані з нього фрагменти настанов буде видалено. Ви зможете під’єднати його знову пізніше.",
|
|
4160
|
+
"confirm": "Від’єднати"
|
|
4161
|
+
},
|
|
4155
4162
|
"unavailable": "Бібліотека фрагментів промптів не ввімкнена для цього розгортання."
|
|
4156
4163
|
},
|
|
4157
4164
|
"sandbox": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.115.
|
|
3
|
+
"version": "0.115.3",
|
|
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",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@nuxt/ui": "^4.9.0",
|
|
22
|
-
"@nuxtjs/i18n": "^10.4.
|
|
22
|
+
"@nuxtjs/i18n": "^10.4.1",
|
|
23
23
|
"@pinia/nuxt": "^0.11.3",
|
|
24
24
|
"@toad-contracts/core": "0.4.0",
|
|
25
25
|
"@toad-contracts/frontend-http-client": "0.3.2",
|
|
@@ -34,18 +34,18 @@
|
|
|
34
34
|
"valibot": "^1.4.2",
|
|
35
35
|
"vue": "3.5.39",
|
|
36
36
|
"wretch": "^3.0.9",
|
|
37
|
-
"@cat-factory/contracts": "0.127.
|
|
37
|
+
"@cat-factory/contracts": "0.127.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|
|
41
41
|
"@types/markdown-it": "^14.1.2",
|
|
42
42
|
"happy-dom": "^20.10.6",
|
|
43
|
-
"msw": "^2.
|
|
43
|
+
"msw": "^2.15.0",
|
|
44
44
|
"nuxt": "^4.4.8",
|
|
45
45
|
"typescript": "^6.0.3",
|
|
46
|
-
"vitest": "^4.1.
|
|
46
|
+
"vitest": "^4.1.10",
|
|
47
47
|
"vue-i18n-extract": "^2.0.7",
|
|
48
|
-
"vue-tsc": "^3.3.
|
|
48
|
+
"vue-tsc": "^3.3.7"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"nuxt": "^4.4.8"
|