@cat-factory/app 0.47.3 → 0.47.5
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/documents/ContextDocumentPicker.vue +17 -10
- package/app/components/documents/DocumentImportModal.vue +26 -13
- package/app/components/documents/DocumentSourceConnectModal.vue +15 -11
- package/app/components/documents/SpawnPreviewModal.vue +27 -11
- package/app/components/documents/TaskContextDocs.vue +9 -6
- package/app/components/github/AddServiceFromRepoModal.vue +54 -41
- package/app/components/github/GitHubConnect.vue +30 -19
- package/app/components/github/GitHubOnboarding.vue +5 -6
- package/app/components/github/GitHubPanel.vue +101 -63
- package/app/components/github/RepoTreeBrowser.vue +13 -6
- package/app/components/slack/SlackPanel.vue +68 -43
- package/app/components/tasks/ContextIssuePicker.vue +21 -14
- package/app/components/tasks/TaskContextIssues.vue +9 -6
- package/app/components/tasks/TaskImportModal.vue +38 -26
- package/app/components/tasks/TaskSourceConnectModal.vue +21 -16
- package/i18n/locales/en.json +333 -0
- package/i18n/locales/es.json +330 -0
- package/i18n/locales/fr.json +330 -0
- package/i18n/locales/pl.json +330 -0
- package/i18n/locales/uk.json +330 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ const props = defineProps<{
|
|
|
16
16
|
}>()
|
|
17
17
|
const emit = defineEmits<{ pick: [item: PendingContext] }>()
|
|
18
18
|
|
|
19
|
+
const { t } = useI18n()
|
|
19
20
|
const documents = useDocumentsStore()
|
|
20
21
|
|
|
21
22
|
const chosen = computed(() => new Set(props.chosenKeys ?? []))
|
|
@@ -182,14 +183,14 @@ onMounted(() => {
|
|
|
182
183
|
class="w-full"
|
|
183
184
|
:placeholder="
|
|
184
185
|
searchable
|
|
185
|
-
? '
|
|
186
|
-
: (descriptor?.refPlaceholder ?? '
|
|
186
|
+
? t('documents.picker.searchPlaceholder')
|
|
187
|
+
: (descriptor?.refPlaceholder ?? t('documents.picker.refPlaceholder'))
|
|
187
188
|
"
|
|
188
189
|
@keydown.enter="refRow && pickRef(refRow)"
|
|
189
190
|
/>
|
|
190
191
|
|
|
191
192
|
<p v-if="searchError" class="px-1 text-[11px] text-amber-400">
|
|
192
|
-
|
|
193
|
+
{{ t('documents.picker.searchFailed', { error: searchError }) }}
|
|
193
194
|
</p>
|
|
194
195
|
|
|
195
196
|
<div class="max-h-56 space-y-0.5 overflow-y-auto">
|
|
@@ -203,7 +204,9 @@ onMounted(() => {
|
|
|
203
204
|
>
|
|
204
205
|
<UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-indigo-400" />
|
|
205
206
|
<span class="truncate">{{ d.title }}</span>
|
|
206
|
-
<UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">
|
|
207
|
+
<UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">{{
|
|
208
|
+
t('documents.picker.importedBadge')
|
|
209
|
+
}}</UBadge>
|
|
207
210
|
</button>
|
|
208
211
|
|
|
209
212
|
<!-- Source search hits (imported on add). -->
|
|
@@ -226,18 +229,22 @@ onMounted(() => {
|
|
|
226
229
|
@click="pickRef(refRow)"
|
|
227
230
|
>
|
|
228
231
|
<UIcon name="i-lucide-link" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
|
|
229
|
-
<span class="truncate"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
+
<span class="truncate">
|
|
233
|
+
<i18n-t keypath="documents.picker.attachByReference" scope="global">
|
|
234
|
+
<template #ref>
|
|
235
|
+
<span class="text-slate-200">{{ refRow }}</span>
|
|
236
|
+
</template>
|
|
237
|
+
</i18n-t>
|
|
238
|
+
</span>
|
|
232
239
|
</button>
|
|
233
240
|
|
|
234
241
|
<p v-if="empty" class="px-2 py-1.5 text-[11px] text-slate-500">
|
|
235
242
|
{{
|
|
236
243
|
query.trim()
|
|
237
|
-
? '
|
|
244
|
+
? t('documents.picker.noMatches')
|
|
238
245
|
: searchable
|
|
239
|
-
? '
|
|
240
|
-
: '
|
|
246
|
+
? t('documents.picker.emptySearchable')
|
|
247
|
+
: t('documents.picker.emptyRefOnly')
|
|
241
248
|
}}
|
|
242
249
|
</p>
|
|
243
250
|
</div>
|
|
@@ -6,6 +6,7 @@ import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
|
6
6
|
// board structure. A source selector lets the user choose which connected source
|
|
7
7
|
// to import from (Confluence, Notion, …). Carries an optional target frame from
|
|
8
8
|
// the inspector so "Preview & spawn" lands the structure inside that frame.
|
|
9
|
+
const { t } = useI18n()
|
|
9
10
|
const ui = useUiStore()
|
|
10
11
|
const documents = useDocumentsStore()
|
|
11
12
|
const board = useBoardStore()
|
|
@@ -60,10 +61,13 @@ async function doImport() {
|
|
|
60
61
|
try {
|
|
61
62
|
const doc = await documents.importDocument(source.value, value)
|
|
62
63
|
ref_.value = ''
|
|
63
|
-
toast.add({
|
|
64
|
+
toast.add({
|
|
65
|
+
title: t('documents.import.imported', { title: doc.title }),
|
|
66
|
+
icon: 'i-lucide-file-down',
|
|
67
|
+
})
|
|
64
68
|
} catch (e) {
|
|
65
69
|
toast.add({
|
|
66
|
-
title: '
|
|
70
|
+
title: t('documents.import.importFailed'),
|
|
67
71
|
description: e instanceof Error ? e.message : String(e),
|
|
68
72
|
icon: 'i-lucide-triangle-alert',
|
|
69
73
|
color: 'error',
|
|
@@ -79,14 +83,14 @@ function preview(externalId: string) {
|
|
|
79
83
|
</script>
|
|
80
84
|
|
|
81
85
|
<template>
|
|
82
|
-
<UModal v-model:open="open" title="
|
|
86
|
+
<UModal v-model:open="open" :title="t('documents.import.title')">
|
|
83
87
|
<template #title>
|
|
84
|
-
<IntegrationBackTitle title="
|
|
88
|
+
<IntegrationBackTitle :title="t('documents.import.title')" @back="back" />
|
|
85
89
|
</template>
|
|
86
90
|
<template #body>
|
|
87
91
|
<div v-if="!documents.anyConnected" class="space-y-3 text-center">
|
|
88
92
|
<UIcon name="i-lucide-plug" class="mx-auto h-8 w-8 text-slate-500" />
|
|
89
|
-
<p class="text-sm text-slate-400">
|
|
93
|
+
<p class="text-sm text-slate-400">{{ t('documents.import.connectFirst') }}</p>
|
|
90
94
|
<div class="flex justify-center gap-2">
|
|
91
95
|
<UButton
|
|
92
96
|
v-for="s in documents.sources"
|
|
@@ -96,22 +100,29 @@ function preview(externalId: string) {
|
|
|
96
100
|
:icon="s.icon"
|
|
97
101
|
@click="ui.openDocumentConnect(s.source)"
|
|
98
102
|
>
|
|
99
|
-
|
|
103
|
+
{{ t('documents.import.connectSource', { source: s.label }) }}
|
|
100
104
|
</UButton>
|
|
101
105
|
</div>
|
|
102
106
|
</div>
|
|
103
107
|
|
|
104
108
|
<div v-else class="space-y-4">
|
|
105
109
|
<p v-if="targetFrameTitle" class="text-xs text-slate-400">
|
|
106
|
-
|
|
110
|
+
<i18n-t keypath="documents.import.spawningInto" scope="global">
|
|
111
|
+
<template #frame>
|
|
112
|
+
<span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
|
|
113
|
+
</template>
|
|
114
|
+
</i18n-t>
|
|
107
115
|
</p>
|
|
108
116
|
|
|
109
|
-
<UFormField v-if="sourceItems.length > 1" label="
|
|
117
|
+
<UFormField v-if="sourceItems.length > 1" :label="t('documents.import.sourceLabel')">
|
|
110
118
|
<USelect v-model="source" :items="sourceItems" class="w-full" />
|
|
111
119
|
</UFormField>
|
|
112
120
|
|
|
113
121
|
<div class="flex items-end gap-2">
|
|
114
|
-
<UFormField
|
|
122
|
+
<UFormField
|
|
123
|
+
:label="descriptor?.refLabel ?? t('documents.import.refLabel')"
|
|
124
|
+
class="flex-1"
|
|
125
|
+
>
|
|
115
126
|
<UInput
|
|
116
127
|
v-model="ref_"
|
|
117
128
|
:placeholder="descriptor?.refPlaceholder"
|
|
@@ -126,13 +137,13 @@ function preview(externalId: string) {
|
|
|
126
137
|
:disabled="!ref_.trim()"
|
|
127
138
|
@click="doImport"
|
|
128
139
|
>
|
|
129
|
-
|
|
140
|
+
{{ t('documents.import.importButton') }}
|
|
130
141
|
</UButton>
|
|
131
142
|
</div>
|
|
132
143
|
|
|
133
144
|
<div v-if="sourceDocs.length" class="space-y-2">
|
|
134
145
|
<h3 class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
135
|
-
|
|
146
|
+
{{ t('documents.import.importedHeading') }}
|
|
136
147
|
</h3>
|
|
137
148
|
<div
|
|
138
149
|
v-for="doc in sourceDocs"
|
|
@@ -158,12 +169,14 @@ function preview(externalId: string) {
|
|
|
158
169
|
icon="i-lucide-wand-sparkles"
|
|
159
170
|
@click="preview(doc.externalId)"
|
|
160
171
|
>
|
|
161
|
-
|
|
172
|
+
{{ t('documents.import.previewSpawn') }}
|
|
162
173
|
</UButton>
|
|
163
174
|
</div>
|
|
164
175
|
</div>
|
|
165
176
|
</div>
|
|
166
|
-
<p v-else class="text-center text-xs text-slate-500">
|
|
177
|
+
<p v-else class="text-center text-xs text-slate-500">
|
|
178
|
+
{{ t('documents.import.noneImported') }}
|
|
179
|
+
</p>
|
|
167
180
|
</div>
|
|
168
181
|
</template>
|
|
169
182
|
</UModal>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// "Connected" with empty fields.
|
|
7
7
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
8
8
|
|
|
9
|
+
const { t } = useI18n()
|
|
9
10
|
const ui = useUiStore()
|
|
10
11
|
const documents = useDocumentsStore()
|
|
11
12
|
const toast = useToast()
|
|
@@ -54,14 +55,14 @@ async function submit() {
|
|
|
54
55
|
try {
|
|
55
56
|
await documents.connect(source.value, credentials)
|
|
56
57
|
toast.add({
|
|
57
|
-
title:
|
|
58
|
+
title: t('documents.connect.connected', { source: descriptor.value!.label }),
|
|
58
59
|
icon: 'i-lucide-check',
|
|
59
60
|
color: 'success',
|
|
60
61
|
})
|
|
61
62
|
ui.closeDocumentConnect()
|
|
62
63
|
} catch (e) {
|
|
63
64
|
toast.add({
|
|
64
|
-
title: '
|
|
65
|
+
title: t('documents.connect.connectFailed'),
|
|
65
66
|
description: e instanceof Error ? e.message : String(e),
|
|
66
67
|
icon: 'i-lucide-triangle-alert',
|
|
67
68
|
color: 'error',
|
|
@@ -75,7 +76,9 @@ async function disconnect() {
|
|
|
75
76
|
if (!source.value) return
|
|
76
77
|
await documents.disconnect(source.value)
|
|
77
78
|
toast.add({
|
|
78
|
-
title:
|
|
79
|
+
title: t('documents.connect.disconnected', {
|
|
80
|
+
source: descriptor.value?.label ?? t('documents.connect.sourceFallback'),
|
|
81
|
+
}),
|
|
79
82
|
icon: 'i-lucide-unplug',
|
|
80
83
|
})
|
|
81
84
|
ui.closeDocumentConnect()
|
|
@@ -83,15 +86,17 @@ async function disconnect() {
|
|
|
83
86
|
</script>
|
|
84
87
|
|
|
85
88
|
<template>
|
|
86
|
-
<UModal v-model:open="open" :title="descriptor?.label ?? '
|
|
89
|
+
<UModal v-model:open="open" :title="descriptor?.label ?? t('documents.connect.title')">
|
|
87
90
|
<template #title>
|
|
88
|
-
<IntegrationBackTitle
|
|
91
|
+
<IntegrationBackTitle
|
|
92
|
+
:title="descriptor?.label ?? t('documents.connect.title')"
|
|
93
|
+
@back="back"
|
|
94
|
+
/>
|
|
89
95
|
</template>
|
|
90
96
|
<template #body>
|
|
91
97
|
<div v-if="descriptor" class="space-y-4">
|
|
92
98
|
<p class="text-sm text-slate-400">
|
|
93
|
-
|
|
94
|
-
structure or attach them to tasks as agent context.
|
|
99
|
+
{{ t('documents.connect.intro', { source: descriptor.label }) }}
|
|
95
100
|
</p>
|
|
96
101
|
|
|
97
102
|
<p
|
|
@@ -101,8 +106,7 @@ async function disconnect() {
|
|
|
101
106
|
>
|
|
102
107
|
<UIcon name="i-lucide-user" class="mt-0.5 size-3.5 shrink-0" />
|
|
103
108
|
<span>
|
|
104
|
-
|
|
105
|
-
account, never shared with the rest of the workspace.
|
|
109
|
+
{{ t('documents.connect.personalNote') }}
|
|
106
110
|
</span>
|
|
107
111
|
</p>
|
|
108
112
|
|
|
@@ -130,7 +134,7 @@ async function disconnect() {
|
|
|
130
134
|
icon="i-lucide-unplug"
|
|
131
135
|
@click="disconnect"
|
|
132
136
|
>
|
|
133
|
-
|
|
137
|
+
{{ t('documents.connect.disconnect') }}
|
|
134
138
|
</UButton>
|
|
135
139
|
<div v-else />
|
|
136
140
|
<UButton
|
|
@@ -140,7 +144,7 @@ async function disconnect() {
|
|
|
140
144
|
:disabled="!canSubmit"
|
|
141
145
|
@click="submit"
|
|
142
146
|
>
|
|
143
|
-
{{ connected ? '
|
|
147
|
+
{{ connected ? t('documents.connect.update') : t('documents.connect.connect') }}
|
|
144
148
|
</UButton>
|
|
145
149
|
</div>
|
|
146
150
|
</div>
|
|
@@ -4,6 +4,7 @@ import type { DocumentBoardPlan } from '~/types/domain'
|
|
|
4
4
|
// Preview the structure an imported document expands into, then spawn it. The
|
|
5
5
|
// plan is fetched fresh on open; a badge makes clear whether an LLM or the
|
|
6
6
|
// deterministic heading parser produced it.
|
|
7
|
+
const { t } = useI18n()
|
|
7
8
|
const ui = useUiStore()
|
|
8
9
|
const documents = useDocumentsStore()
|
|
9
10
|
const board = useBoardStore()
|
|
@@ -36,7 +37,7 @@ watch(
|
|
|
36
37
|
plan.value = await documents.plan(preview.source, externalId)
|
|
37
38
|
} catch (e) {
|
|
38
39
|
toast.add({
|
|
39
|
-
title: '
|
|
40
|
+
title: t('documents.spawn.planFailed'),
|
|
40
41
|
description: e instanceof Error ? e.message : String(e),
|
|
41
42
|
icon: 'i-lucide-triangle-alert',
|
|
42
43
|
color: 'error',
|
|
@@ -59,8 +60,12 @@ async function spawn() {
|
|
|
59
60
|
targetFrameId.value ?? undefined,
|
|
60
61
|
)
|
|
61
62
|
toast.add({
|
|
62
|
-
title: '
|
|
63
|
-
description:
|
|
63
|
+
title: t('documents.spawn.spawned'),
|
|
64
|
+
description: t('documents.spawn.summary', {
|
|
65
|
+
frames: t('documents.spawn.frameCount', { count: result.frames }, result.frames),
|
|
66
|
+
modules: t('documents.spawn.moduleCount', { count: result.modules }, result.modules),
|
|
67
|
+
tasks: t('documents.spawn.taskCount', { count: result.tasks }, result.tasks),
|
|
68
|
+
}),
|
|
64
69
|
icon: 'i-lucide-check',
|
|
65
70
|
color: 'success',
|
|
66
71
|
})
|
|
@@ -68,7 +73,7 @@ async function spawn() {
|
|
|
68
73
|
ui.closeDocumentImport()
|
|
69
74
|
} catch (e) {
|
|
70
75
|
toast.add({
|
|
71
|
-
title: '
|
|
76
|
+
title: t('documents.spawn.spawnFailed'),
|
|
72
77
|
description: e instanceof Error ? e.message : String(e),
|
|
73
78
|
icon: 'i-lucide-triangle-alert',
|
|
74
79
|
color: 'error',
|
|
@@ -80,7 +85,7 @@ async function spawn() {
|
|
|
80
85
|
</script>
|
|
81
86
|
|
|
82
87
|
<template>
|
|
83
|
-
<UModal v-model:open="open" title="
|
|
88
|
+
<UModal v-model:open="open" :title="t('documents.spawn.title')">
|
|
84
89
|
<template #body>
|
|
85
90
|
<div class="space-y-4">
|
|
86
91
|
<div v-if="plan" class="flex items-center justify-between gap-2">
|
|
@@ -89,16 +94,25 @@ async function spawn() {
|
|
|
89
94
|
variant="subtle"
|
|
90
95
|
size="sm"
|
|
91
96
|
>
|
|
92
|
-
{{
|
|
97
|
+
{{
|
|
98
|
+
plan.planner === 'llm'
|
|
99
|
+
? t('documents.spawn.plannerLlm')
|
|
100
|
+
: t('documents.spawn.plannerHeadings')
|
|
101
|
+
}}
|
|
93
102
|
</UBadge>
|
|
94
103
|
<span v-if="targetFrameTitle" class="text-xs text-slate-400">
|
|
95
|
-
|
|
104
|
+
<i18n-t keypath="documents.spawn.intoFrame" scope="global">
|
|
105
|
+
<template #frame>
|
|
106
|
+
<span class="font-medium text-slate-200">{{ targetFrameTitle }}</span>
|
|
107
|
+
</template>
|
|
108
|
+
</i18n-t>
|
|
96
109
|
</span>
|
|
97
|
-
<span v-else class="text-xs text-slate-400">
|
|
110
|
+
<span v-else class="text-xs text-slate-400">{{ t('documents.spawn.asTopLevel') }}</span>
|
|
98
111
|
</div>
|
|
99
112
|
|
|
100
113
|
<div v-if="loadingPlan" class="flex items-center gap-2 text-sm text-slate-400">
|
|
101
|
-
<UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
|
|
114
|
+
<UIcon name="i-lucide-loader" class="h-4 w-4 animate-spin" />
|
|
115
|
+
{{ t('documents.spawn.buildingPlan') }}
|
|
102
116
|
</div>
|
|
103
117
|
|
|
104
118
|
<div v-else-if="plan" class="max-h-80 space-y-3 overflow-y-auto pr-1">
|
|
@@ -144,7 +158,9 @@ async function spawn() {
|
|
|
144
158
|
</div>
|
|
145
159
|
|
|
146
160
|
<div class="flex justify-end gap-2 pt-1">
|
|
147
|
-
<UButton color="neutral" variant="ghost" @click="ui.closeSpawnPreview()">
|
|
161
|
+
<UButton color="neutral" variant="ghost" @click="ui.closeSpawnPreview()">{{
|
|
162
|
+
t('common.cancel')
|
|
163
|
+
}}</UButton>
|
|
148
164
|
<UButton
|
|
149
165
|
color="primary"
|
|
150
166
|
icon="i-lucide-wand-sparkles"
|
|
@@ -152,7 +168,7 @@ async function spawn() {
|
|
|
152
168
|
:disabled="!plan || loadingPlan"
|
|
153
169
|
@click="spawn"
|
|
154
170
|
>
|
|
155
|
-
|
|
171
|
+
{{ t('documents.spawn.spawnOntoBoard') }}
|
|
156
172
|
</UButton>
|
|
157
173
|
</div>
|
|
158
174
|
</div>
|
|
@@ -7,6 +7,7 @@ import type { Block, DocumentSourceKind } from '~/types/domain'
|
|
|
7
7
|
// backend's userPromptFor). Rendered only when the integration is available.
|
|
8
8
|
const props = defineProps<{ block: Block }>()
|
|
9
9
|
|
|
10
|
+
const { t } = useI18n()
|
|
10
11
|
const documents = useDocumentsStore()
|
|
11
12
|
const ui = useUiStore()
|
|
12
13
|
const toast = useToast()
|
|
@@ -20,10 +21,10 @@ const linked = computed(() => documents.docsForBlock(props.block.id))
|
|
|
20
21
|
async function attach(source: DocumentSourceKind, externalId: string) {
|
|
21
22
|
try {
|
|
22
23
|
await documents.linkToBlock(props.block.id, source, externalId)
|
|
23
|
-
toast.add({ title: '
|
|
24
|
+
toast.add({ title: t('documents.taskDocs.attached'), icon: 'i-lucide-link' })
|
|
24
25
|
} catch (e) {
|
|
25
26
|
toast.add({
|
|
26
|
-
title: '
|
|
27
|
+
title: t('documents.taskDocs.attachFailed'),
|
|
27
28
|
description: e instanceof Error ? e.message : String(e),
|
|
28
29
|
icon: 'i-lucide-triangle-alert',
|
|
29
30
|
color: 'error',
|
|
@@ -41,7 +42,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
|
41
42
|
onSelect: () => attach(d.source, d.externalId),
|
|
42
43
|
}))
|
|
43
44
|
items.push({
|
|
44
|
-
label: '
|
|
45
|
+
label: t('documents.taskDocs.importPage'),
|
|
45
46
|
icon: 'i-lucide-file-down',
|
|
46
47
|
onSelect: () => ui.openDocumentImport(null),
|
|
47
48
|
})
|
|
@@ -53,10 +54,12 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
|
53
54
|
<div v-if="documents.available" class="space-y-2">
|
|
54
55
|
<div class="flex items-center justify-between">
|
|
55
56
|
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
56
|
-
|
|
57
|
+
{{ t('documents.taskDocs.heading') }}
|
|
57
58
|
</span>
|
|
58
59
|
<UDropdownMenu :items="attachMenu" :content="{ side: 'bottom', align: 'end' }">
|
|
59
|
-
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">
|
|
60
|
+
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">{{
|
|
61
|
+
t('documents.taskDocs.attach')
|
|
62
|
+
}}</UButton>
|
|
60
63
|
</UDropdownMenu>
|
|
61
64
|
</div>
|
|
62
65
|
|
|
@@ -77,7 +80,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
|
77
80
|
</a>
|
|
78
81
|
</div>
|
|
79
82
|
<p v-else class="text-[11px] text-slate-500">
|
|
80
|
-
|
|
83
|
+
{{ t('documents.taskDocs.empty') }}
|
|
81
84
|
</p>
|
|
82
85
|
</div>
|
|
83
86
|
</template>
|
|
@@ -15,6 +15,7 @@ import RepoTreeBrowser from '~/components/github/RepoTreeBrowser.vue'
|
|
|
15
15
|
import ServiceTestConfig from '~/components/panels/inspector/ServiceTestConfig.vue'
|
|
16
16
|
import ServiceFragments from '~/components/panels/inspector/ServiceFragments.vue'
|
|
17
17
|
|
|
18
|
+
const { t } = useI18n()
|
|
18
19
|
const ui = useUiStore()
|
|
19
20
|
const github = useGitHubStore()
|
|
20
21
|
const board = useBoardStore()
|
|
@@ -39,18 +40,6 @@ async function loadRepos() {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
// On open: ensure we know the connection + which repos the App can access, and
|
|
43
|
-
// the workspace's already-tracked repos (to flag ones already on the board).
|
|
44
|
-
watch(
|
|
45
|
-
open,
|
|
46
|
-
(isOpen) => {
|
|
47
|
-
if (!isOpen) return
|
|
48
|
-
resetSelection()
|
|
49
|
-
void loadRepos()
|
|
50
|
-
},
|
|
51
|
-
{ immediate: true },
|
|
52
|
-
)
|
|
53
|
-
|
|
54
43
|
// If the user connects from inside the modal (the not-connected prompt), pull the
|
|
55
44
|
// repo list as soon as the connection is bound.
|
|
56
45
|
watch(
|
|
@@ -72,9 +61,13 @@ const onBoardIds = computed(
|
|
|
72
61
|
const repoItems = computed(() =>
|
|
73
62
|
github.availableRepos.map((r) => {
|
|
74
63
|
const onBoard = onBoardIds.value.has(r.githubId) && !r.isMonorepo
|
|
75
|
-
const
|
|
64
|
+
const suffix = [
|
|
65
|
+
r.private ? t('github.addService.repoLabel.private') : '',
|
|
66
|
+
r.isMonorepo ? t('github.addService.repoLabel.monorepo') : '',
|
|
67
|
+
onBoard ? t('github.addService.repoLabel.onBoard') : '',
|
|
68
|
+
].join('')
|
|
76
69
|
return {
|
|
77
|
-
label: `${r.owner}/${r.name}${
|
|
70
|
+
label: `${r.owner}/${r.name}${suffix}`,
|
|
78
71
|
// Searched on (lowercased once) — the owner/name, so the filter matches either.
|
|
79
72
|
search: `${r.owner}/${r.name}`.toLowerCase(),
|
|
80
73
|
value: r.githubId,
|
|
@@ -152,6 +145,20 @@ const configuredBlock = computed(() =>
|
|
|
152
145
|
configuredBlockId.value ? board.getBlock(configuredBlockId.value) : undefined,
|
|
153
146
|
)
|
|
154
147
|
|
|
148
|
+
// On open: ensure we know the connection + which repos the App can access, and
|
|
149
|
+
// the workspace's already-tracked repos (to flag ones already on the board).
|
|
150
|
+
// Declared after every ref resetSelection() touches so the `immediate` run
|
|
151
|
+
// doesn't access them inside their temporal dead zone.
|
|
152
|
+
watch(
|
|
153
|
+
open,
|
|
154
|
+
(isOpen) => {
|
|
155
|
+
if (!isOpen) return
|
|
156
|
+
resetSelection()
|
|
157
|
+
void loadRepos()
|
|
158
|
+
},
|
|
159
|
+
{ immediate: true },
|
|
160
|
+
)
|
|
161
|
+
|
|
155
162
|
// A monorepo service needs a chosen directory; a whole-repo service can be added once.
|
|
156
163
|
const canAdd = computed(
|
|
157
164
|
() =>
|
|
@@ -173,8 +180,8 @@ async function add() {
|
|
|
173
180
|
configuredBlockId.value = block.id
|
|
174
181
|
configuredDirectory.value = isMonorepo.value ? selectedDirectory.value : undefined
|
|
175
182
|
toast.add({
|
|
176
|
-
title: '
|
|
177
|
-
description:
|
|
183
|
+
title: t('github.addService.toast.addedTitle'),
|
|
184
|
+
description: t('github.addService.toast.addedDescription', { title: block.title }),
|
|
178
185
|
icon: 'i-lucide-check',
|
|
179
186
|
color: 'success',
|
|
180
187
|
})
|
|
@@ -182,7 +189,7 @@ async function add() {
|
|
|
182
189
|
selectedDirectory.value = undefined
|
|
183
190
|
} catch (e) {
|
|
184
191
|
toast.add({
|
|
185
|
-
title: '
|
|
192
|
+
title: t('github.addService.toast.addFailedTitle'),
|
|
186
193
|
description: e instanceof Error ? e.message : String(e),
|
|
187
194
|
icon: 'i-lucide-triangle-alert',
|
|
188
195
|
color: 'error',
|
|
@@ -198,12 +205,11 @@ function done() {
|
|
|
198
205
|
</script>
|
|
199
206
|
|
|
200
207
|
<template>
|
|
201
|
-
<UModal v-model:open="open" title="
|
|
208
|
+
<UModal v-model:open="open" :title="t('github.addService.title')" :ui="{ content: 'max-w-xl' }">
|
|
202
209
|
<template #body>
|
|
203
210
|
<div class="space-y-6">
|
|
204
211
|
<p class="text-sm text-slate-400">
|
|
205
|
-
|
|
206
|
-
is linked to a new service frame as-is, and tasks you run on it target that repo.
|
|
212
|
+
{{ t('github.addService.intro') }}
|
|
207
213
|
</p>
|
|
208
214
|
|
|
209
215
|
<!-- not connected: linking a repo needs the App bound to this workspace -->
|
|
@@ -214,8 +220,7 @@ function done() {
|
|
|
214
220
|
<div class="flex items-start gap-2">
|
|
215
221
|
<UIcon name="i-lucide-plug-zap" class="mt-0.5 h-4 w-4 shrink-0 text-amber-400" />
|
|
216
222
|
<p class="text-sm text-amber-200/90">
|
|
217
|
-
|
|
218
|
-
install it.
|
|
223
|
+
{{ t('github.addService.connectFirst') }}
|
|
219
224
|
</p>
|
|
220
225
|
</div>
|
|
221
226
|
<GitHubConnect />
|
|
@@ -223,18 +228,18 @@ function done() {
|
|
|
223
228
|
|
|
224
229
|
<template v-else>
|
|
225
230
|
<UFormField
|
|
226
|
-
label="
|
|
227
|
-
description="
|
|
231
|
+
:label="t('github.addService.repository')"
|
|
232
|
+
:description="t('github.addService.repositoryHint')"
|
|
228
233
|
required
|
|
229
234
|
>
|
|
230
235
|
<div v-if="!hasRepos" class="text-sm text-slate-400">
|
|
231
|
-
|
|
236
|
+
{{ t('github.addService.noReposAvailable') }}
|
|
232
237
|
</div>
|
|
233
238
|
<div v-else class="space-y-1.5">
|
|
234
239
|
<UInput
|
|
235
240
|
v-model="repoSearch"
|
|
236
241
|
icon="i-lucide-search"
|
|
237
|
-
placeholder="
|
|
242
|
+
:placeholder="t('github.addService.filterPlaceholder')"
|
|
238
243
|
class="w-full"
|
|
239
244
|
:ui="{ trailing: 'pe-1' }"
|
|
240
245
|
>
|
|
@@ -244,7 +249,7 @@ function done() {
|
|
|
244
249
|
variant="link"
|
|
245
250
|
size="sm"
|
|
246
251
|
icon="i-lucide-x"
|
|
247
|
-
aria-label="
|
|
252
|
+
:aria-label="t('github.addService.clearFilter')"
|
|
248
253
|
@click="repoSearch = ''"
|
|
249
254
|
/>
|
|
250
255
|
</template>
|
|
@@ -252,11 +257,16 @@ function done() {
|
|
|
252
257
|
<USelect
|
|
253
258
|
v-model="selectedRepoId"
|
|
254
259
|
:items="filteredRepoItems"
|
|
255
|
-
placeholder="
|
|
260
|
+
:placeholder="t('github.addService.chooseRepository')"
|
|
256
261
|
class="w-full"
|
|
257
262
|
/>
|
|
258
263
|
<p class="text-xs text-slate-500">
|
|
259
|
-
|
|
264
|
+
{{
|
|
265
|
+
t('github.addService.showingCount', {
|
|
266
|
+
shown: filteredRepoItems.length,
|
|
267
|
+
total: repoItems.length,
|
|
268
|
+
})
|
|
269
|
+
}}
|
|
260
270
|
</p>
|
|
261
271
|
</div>
|
|
262
272
|
</UFormField>
|
|
@@ -265,8 +275,8 @@ function done() {
|
|
|
265
275
|
<div v-if="selectedRepoId !== undefined" class="space-y-3">
|
|
266
276
|
<USwitch
|
|
267
277
|
:model-value="isMonorepo"
|
|
268
|
-
label="
|
|
269
|
-
description="
|
|
278
|
+
:label="t('github.addService.monorepoLabel')"
|
|
279
|
+
:description="t('github.addService.monorepoDescription')"
|
|
270
280
|
@update:model-value="toggleMonorepo"
|
|
271
281
|
/>
|
|
272
282
|
|
|
@@ -275,8 +285,7 @@ function done() {
|
|
|
275
285
|
class="rounded-md border border-slate-700/60 bg-slate-900/40 p-3"
|
|
276
286
|
>
|
|
277
287
|
<p class="mb-2 text-xs text-slate-400">
|
|
278
|
-
|
|
279
|
-
working on this service will run within that subdirectory.
|
|
288
|
+
{{ t('github.addService.monorepoBrowseHint') }}
|
|
280
289
|
</p>
|
|
281
290
|
<RepoTreeBrowser
|
|
282
291
|
v-model="selectedDirectory"
|
|
@@ -285,10 +294,10 @@ function done() {
|
|
|
285
294
|
/>
|
|
286
295
|
<p class="mt-2 truncate text-xs text-slate-400">
|
|
287
296
|
<template v-if="selectedDirectory">
|
|
288
|
-
|
|
297
|
+
{{ t('github.addService.serviceDirectory') }}
|
|
289
298
|
<code class="text-slate-200">{{ selectedDirectory }}</code>
|
|
290
299
|
</template>
|
|
291
|
-
<template v-else>
|
|
300
|
+
<template v-else>{{ t('github.addService.noDirectorySelected') }}</template>
|
|
292
301
|
</p>
|
|
293
302
|
</div>
|
|
294
303
|
</div>
|
|
@@ -302,7 +311,7 @@ function done() {
|
|
|
302
311
|
class="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-400"
|
|
303
312
|
>
|
|
304
313
|
<UIcon name="i-lucide-check" class="h-3.5 w-3.5" />
|
|
305
|
-
{{ configuredBlock.title }}
|
|
314
|
+
{{ t('github.addService.addedConfigure', { title: configuredBlock.title }) }}
|
|
306
315
|
</div>
|
|
307
316
|
<ServiceTestConfig
|
|
308
317
|
:block="configuredBlock"
|
|
@@ -319,10 +328,10 @@ function done() {
|
|
|
319
328
|
size="sm"
|
|
320
329
|
icon="i-lucide-shield-check"
|
|
321
330
|
trailing-icon="i-lucide-external-link"
|
|
322
|
-
title="
|
|
331
|
+
:title="t('github.addService.grantAccessTitle')"
|
|
323
332
|
@click="openManageInstall"
|
|
324
333
|
>
|
|
325
|
-
|
|
334
|
+
{{ t('github.addService.grantAccess') }}
|
|
326
335
|
</UButton>
|
|
327
336
|
<UButton
|
|
328
337
|
color="neutral"
|
|
@@ -332,13 +341,13 @@ function done() {
|
|
|
332
341
|
:loading="github.loadingAvailable"
|
|
333
342
|
@click="github.loadAvailableRepos()"
|
|
334
343
|
>
|
|
335
|
-
|
|
344
|
+
{{ t('github.addService.refreshList') }}
|
|
336
345
|
</UButton>
|
|
337
346
|
</div>
|
|
338
347
|
|
|
339
348
|
<div class="flex justify-end gap-2">
|
|
340
349
|
<UButton v-if="configuredBlock" color="neutral" variant="soft" size="sm" @click="done">
|
|
341
|
-
|
|
350
|
+
{{ t('github.addService.done') }}
|
|
342
351
|
</UButton>
|
|
343
352
|
<UButton
|
|
344
353
|
v-if="!configuredBlock || isMonorepo"
|
|
@@ -348,7 +357,11 @@ function done() {
|
|
|
348
357
|
:disabled="!canAdd"
|
|
349
358
|
@click="add"
|
|
350
359
|
>
|
|
351
|
-
{{
|
|
360
|
+
{{
|
|
361
|
+
configuredBlock && isMonorepo
|
|
362
|
+
? t('github.addService.addAnother')
|
|
363
|
+
: t('github.addService.add')
|
|
364
|
+
}}
|
|
352
365
|
</UButton>
|
|
353
366
|
</div>
|
|
354
367
|
</template>
|