@cat-factory/app 0.47.4 → 0.47.6
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 +40 -29
- 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 +2 -2
|
@@ -21,6 +21,7 @@ const props = withDefaults(
|
|
|
21
21
|
)
|
|
22
22
|
const emit = defineEmits<{ 'update:modelValue': [string | undefined] }>()
|
|
23
23
|
|
|
24
|
+
const { t } = useI18n()
|
|
24
25
|
const github = useGitHubStore()
|
|
25
26
|
const toast = useToast()
|
|
26
27
|
|
|
@@ -51,7 +52,7 @@ async function browseTo(path: string) {
|
|
|
51
52
|
} catch (e) {
|
|
52
53
|
treeEntries.value = []
|
|
53
54
|
toast.add({
|
|
54
|
-
title: '
|
|
55
|
+
title: t('github.repoTree.errors.listDirectory'),
|
|
55
56
|
description: e instanceof Error ? e.message : String(e),
|
|
56
57
|
icon: 'i-lucide-triangle-alert',
|
|
57
58
|
color: 'error',
|
|
@@ -85,7 +86,7 @@ watch(
|
|
|
85
86
|
:disabled="loading"
|
|
86
87
|
@click="browseTo('')"
|
|
87
88
|
>
|
|
88
|
-
root
|
|
89
|
+
{{ t('github.repoTree.root') }}
|
|
89
90
|
</UButton>
|
|
90
91
|
<template v-for="crumb in breadcrumbs" :key="crumb.path">
|
|
91
92
|
<span class="text-slate-600">/</span>
|
|
@@ -103,9 +104,11 @@ watch(
|
|
|
103
104
|
|
|
104
105
|
<!-- listing -->
|
|
105
106
|
<div class="max-h-56 overflow-auto rounded border border-slate-800">
|
|
106
|
-
<div v-if="loading" class="p-3 text-sm text-slate-400">
|
|
107
|
+
<div v-if="loading" class="p-3 text-sm text-slate-400">
|
|
108
|
+
{{ t('github.repoTree.loading') }}
|
|
109
|
+
</div>
|
|
107
110
|
<div v-else-if="isEmpty" class="p-3 text-sm text-slate-400">
|
|
108
|
-
{{ mode === 'dir' ? '
|
|
111
|
+
{{ mode === 'dir' ? t('github.repoTree.noSubdirectories') : t('github.repoTree.empty') }}
|
|
109
112
|
</div>
|
|
110
113
|
<ul v-else class="divide-y divide-slate-800">
|
|
111
114
|
<li
|
|
@@ -128,7 +131,11 @@ watch(
|
|
|
128
131
|
:color="modelValue === entry.path ? 'primary' : 'neutral'"
|
|
129
132
|
@click="pick(entry.path)"
|
|
130
133
|
>
|
|
131
|
-
{{
|
|
134
|
+
{{
|
|
135
|
+
modelValue === entry.path
|
|
136
|
+
? t('github.repoTree.selected')
|
|
137
|
+
: t('github.repoTree.select')
|
|
138
|
+
}}
|
|
132
139
|
</UButton>
|
|
133
140
|
</li>
|
|
134
141
|
<template v-if="mode === 'file'">
|
|
@@ -164,7 +171,7 @@ watch(
|
|
|
164
171
|
:color="modelValue === currentPath ? 'primary' : 'neutral'"
|
|
165
172
|
@click="pick(currentPath)"
|
|
166
173
|
>
|
|
167
|
-
|
|
174
|
+
{{ t('github.repoTree.useThisFolder') }}
|
|
168
175
|
</UButton>
|
|
169
176
|
</div>
|
|
170
177
|
</div>
|
|
@@ -12,6 +12,7 @@ import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
|
12
12
|
const ui = useUiStore()
|
|
13
13
|
const slack = useSlackStore()
|
|
14
14
|
const toast = useToast()
|
|
15
|
+
const { t } = useI18n()
|
|
15
16
|
|
|
16
17
|
const open = computed({
|
|
17
18
|
get: () => ui.slackOpen,
|
|
@@ -19,20 +20,23 @@ const open = computed({
|
|
|
19
20
|
})
|
|
20
21
|
const back = useIntegrationBack(open)
|
|
21
22
|
|
|
22
|
-
const ROUTABLE
|
|
23
|
-
{ type: 'merge_review', label: '
|
|
24
|
-
{ type: 'pipeline_complete', label: '
|
|
25
|
-
{ type: 'ci_failed', label: '
|
|
26
|
-
{ type: 'test_failed', label: '
|
|
27
|
-
{ type: 'requirement_review', label: '
|
|
28
|
-
{ type: 'clarity_review', label: '
|
|
29
|
-
{ type: 'release_regression', label: '
|
|
30
|
-
{ type: 'human_test_ready', label: '
|
|
31
|
-
{ type: 'visual_confirmation_ready', label: '
|
|
32
|
-
]
|
|
23
|
+
const ROUTABLE = computed<{ type: NotificationType; label: string }[]>(() => [
|
|
24
|
+
{ type: 'merge_review', label: t('slack.routable.merge_review') },
|
|
25
|
+
{ type: 'pipeline_complete', label: t('slack.routable.pipeline_complete') },
|
|
26
|
+
{ type: 'ci_failed', label: t('slack.routable.ci_failed') },
|
|
27
|
+
{ type: 'test_failed', label: t('slack.routable.test_failed') },
|
|
28
|
+
{ type: 'requirement_review', label: t('slack.routable.requirement_review') },
|
|
29
|
+
{ type: 'clarity_review', label: t('slack.routable.clarity_review') },
|
|
30
|
+
{ type: 'release_regression', label: t('slack.routable.release_regression') },
|
|
31
|
+
{ type: 'human_test_ready', label: t('slack.routable.human_test_ready') },
|
|
32
|
+
{ type: 'visual_confirmation_ready', label: t('slack.routable.visual_confirmation_ready') },
|
|
33
|
+
])
|
|
33
34
|
|
|
34
35
|
/** Notification-role options for a mapped member (drives who gets @-mentioned). */
|
|
35
|
-
const ROLE_OPTIONS: SlackMemberRole[]
|
|
36
|
+
const ROLE_OPTIONS = computed<{ label: string; value: SlackMemberRole }[]>(() => [
|
|
37
|
+
{ label: t('slack.role.engineering'), value: 'engineering' },
|
|
38
|
+
{ label: t('slack.role.product'), value: 'product' },
|
|
39
|
+
])
|
|
36
40
|
|
|
37
41
|
// Local editable copies, synced from the store on load.
|
|
38
42
|
const routes = reactive<Record<NotificationType, SlackRoute>>({
|
|
@@ -71,13 +75,13 @@ watch(
|
|
|
71
75
|
if (!isOpen || !slack.connected) return
|
|
72
76
|
try {
|
|
73
77
|
await Promise.all([slack.loadSettings(), slack.loadMemberMapping(), slack.loadChannels()])
|
|
74
|
-
for (const { type } of ROUTABLE) {
|
|
78
|
+
for (const { type } of ROUTABLE.value) {
|
|
75
79
|
routes[type] = slack.settings?.routes[type] ?? { enabled: false, channel: '' }
|
|
76
80
|
}
|
|
77
81
|
mentionsEnabled.value = slack.settings?.mentionsEnabled ?? false
|
|
78
82
|
mapping.value = slack.memberMapping.map((e) => ({ role: 'engineering', ...e }))
|
|
79
83
|
} catch (e) {
|
|
80
|
-
notifyError('
|
|
84
|
+
notifyError(t('slack.error.loadSettings'), e)
|
|
81
85
|
}
|
|
82
86
|
},
|
|
83
87
|
// Lazy v-if mount: the panel mounts with `open` already true, so load immediately.
|
|
@@ -88,7 +92,7 @@ async function connectViaOAuth() {
|
|
|
88
92
|
try {
|
|
89
93
|
window.location.href = await slack.installUrl()
|
|
90
94
|
} catch (e) {
|
|
91
|
-
notifyError('
|
|
95
|
+
notifyError(t('slack.error.startOAuth'), e)
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
|
|
@@ -97,9 +101,9 @@ async function connectWithToken() {
|
|
|
97
101
|
try {
|
|
98
102
|
await slack.connectWithToken(tokenInput.value.trim())
|
|
99
103
|
tokenInput.value = ''
|
|
100
|
-
toast.add({ title: '
|
|
104
|
+
toast.add({ title: t('slack.toast.connected'), icon: 'i-lucide-check', color: 'success' })
|
|
101
105
|
} catch (e) {
|
|
102
|
-
notifyError('
|
|
106
|
+
notifyError(t('slack.error.connect'), e)
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -107,7 +111,7 @@ async function disconnect() {
|
|
|
107
111
|
try {
|
|
108
112
|
await slack.disconnect()
|
|
109
113
|
} catch (e) {
|
|
110
|
-
notifyError('
|
|
114
|
+
notifyError(t('slack.error.disconnect'), e)
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -118,9 +122,9 @@ async function saveRouting() {
|
|
|
118
122
|
routes: { ...routes },
|
|
119
123
|
mentionsEnabled: mentionsEnabled.value,
|
|
120
124
|
})
|
|
121
|
-
toast.add({ title: '
|
|
125
|
+
toast.add({ title: t('slack.toast.routingSaved'), icon: 'i-lucide-check', color: 'success' })
|
|
122
126
|
} catch (e) {
|
|
123
|
-
notifyError('
|
|
127
|
+
notifyError(t('slack.error.saveRouting'), e)
|
|
124
128
|
} finally {
|
|
125
129
|
busy.value = false
|
|
126
130
|
}
|
|
@@ -138,9 +142,9 @@ async function saveMapping() {
|
|
|
138
142
|
const entries = mapping.value.filter((e) => e.userId.trim() && e.slackUserId.trim())
|
|
139
143
|
await slack.updateMemberMapping(entries)
|
|
140
144
|
mapping.value = slack.memberMapping.map((e) => ({ ...e }))
|
|
141
|
-
toast.add({ title: '
|
|
145
|
+
toast.add({ title: t('slack.toast.mapSaved'), icon: 'i-lucide-check', color: 'success' })
|
|
142
146
|
} catch (e) {
|
|
143
|
-
notifyError('
|
|
147
|
+
notifyError(t('slack.error.saveMap'), e)
|
|
144
148
|
} finally {
|
|
145
149
|
busy.value = false
|
|
146
150
|
}
|
|
@@ -148,15 +152,14 @@ async function saveMapping() {
|
|
|
148
152
|
</script>
|
|
149
153
|
|
|
150
154
|
<template>
|
|
151
|
-
<UModal v-model:open="open" title="
|
|
155
|
+
<UModal v-model:open="open" :title="t('slack.panel.title')" :ui="{ content: 'max-w-2xl' }">
|
|
152
156
|
<template #title>
|
|
153
|
-
<IntegrationBackTitle title="
|
|
157
|
+
<IntegrationBackTitle :title="t('slack.panel.title')" @back="back" />
|
|
154
158
|
</template>
|
|
155
159
|
<template #body>
|
|
156
160
|
<div class="space-y-5">
|
|
157
161
|
<p class="text-xs text-slate-400">
|
|
158
|
-
|
|
159
|
-
connection is shared across the account; routing is per board.
|
|
162
|
+
{{ t('slack.panel.intro') }}
|
|
160
163
|
</p>
|
|
161
164
|
|
|
162
165
|
<!-- not connected: connect UI -->
|
|
@@ -167,11 +170,11 @@ async function saveMapping() {
|
|
|
167
170
|
icon="i-lucide-slack"
|
|
168
171
|
@click="connectViaOAuth"
|
|
169
172
|
>
|
|
170
|
-
|
|
173
|
+
{{ t('slack.connect.addToSlack') }}
|
|
171
174
|
</UButton>
|
|
172
175
|
<div class="space-y-1">
|
|
173
176
|
<span class="block text-[10px] uppercase tracking-wide text-slate-500">
|
|
174
|
-
|
|
177
|
+
{{ t('slack.connect.orPasteToken') }}
|
|
175
178
|
</span>
|
|
176
179
|
<div class="flex gap-2">
|
|
177
180
|
<UInput
|
|
@@ -189,7 +192,7 @@ async function saveMapping() {
|
|
|
189
192
|
:disabled="!tokenInput.trim()"
|
|
190
193
|
@click="connectWithToken"
|
|
191
194
|
>
|
|
192
|
-
|
|
195
|
+
{{ t('slack.connect.connect') }}
|
|
193
196
|
</UButton>
|
|
194
197
|
</div>
|
|
195
198
|
</div>
|
|
@@ -202,7 +205,11 @@ async function saveMapping() {
|
|
|
202
205
|
>
|
|
203
206
|
<UIcon name="i-lucide-slack" class="text-emerald-400" />
|
|
204
207
|
<span class="flex-1 text-sm text-slate-200">
|
|
205
|
-
|
|
208
|
+
<i18n-t keypath="slack.connected.label" tag="span">
|
|
209
|
+
<template #team>
|
|
210
|
+
<span class="font-semibold">{{ slack.connection?.teamName }}</span>
|
|
211
|
+
</template>
|
|
212
|
+
</i18n-t>
|
|
206
213
|
</span>
|
|
207
214
|
<UButton
|
|
208
215
|
color="error"
|
|
@@ -211,13 +218,15 @@ async function saveMapping() {
|
|
|
211
218
|
icon="i-lucide-unplug"
|
|
212
219
|
@click="disconnect"
|
|
213
220
|
>
|
|
214
|
-
|
|
221
|
+
{{ t('slack.connected.disconnect') }}
|
|
215
222
|
</UButton>
|
|
216
223
|
</div>
|
|
217
224
|
|
|
218
225
|
<!-- routing -->
|
|
219
226
|
<div class="space-y-3">
|
|
220
|
-
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
227
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
228
|
+
{{ t('slack.routing.heading') }}
|
|
229
|
+
</p>
|
|
221
230
|
<div
|
|
222
231
|
v-for="row in ROUTABLE"
|
|
223
232
|
:key="row.type"
|
|
@@ -229,7 +238,7 @@ async function saveMapping() {
|
|
|
229
238
|
v-model="routes[row.type]!.channel"
|
|
230
239
|
size="sm"
|
|
231
240
|
class="flex-1"
|
|
232
|
-
placeholder="
|
|
241
|
+
:placeholder="t('slack.routing.channelPlaceholder')"
|
|
233
242
|
:disabled="!routes[row.type]!.enabled"
|
|
234
243
|
list="slack-channels"
|
|
235
244
|
/>
|
|
@@ -240,7 +249,7 @@ async function saveMapping() {
|
|
|
240
249
|
|
|
241
250
|
<label class="flex items-center gap-2">
|
|
242
251
|
<USwitch v-model="mentionsEnabled" size="sm" />
|
|
243
|
-
<span class="text-sm text-slate-300"
|
|
252
|
+
<span class="text-sm text-slate-300">{{ t('slack.routing.mentionMembers') }}</span>
|
|
244
253
|
</label>
|
|
245
254
|
|
|
246
255
|
<div class="flex justify-end">
|
|
@@ -252,7 +261,7 @@ async function saveMapping() {
|
|
|
252
261
|
:loading="busy"
|
|
253
262
|
@click="saveRouting"
|
|
254
263
|
>
|
|
255
|
-
|
|
264
|
+
{{ t('slack.routing.save') }}
|
|
256
265
|
</UButton>
|
|
257
266
|
</div>
|
|
258
267
|
</div>
|
|
@@ -260,21 +269,37 @@ async function saveMapping() {
|
|
|
260
269
|
<!-- member mapping -->
|
|
261
270
|
<div v-if="mentionsEnabled" class="space-y-2">
|
|
262
271
|
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
263
|
-
|
|
272
|
+
{{ t('slack.members.heading') }}
|
|
264
273
|
</p>
|
|
265
274
|
<p class="text-[11px] leading-snug text-slate-500">
|
|
266
|
-
<
|
|
267
|
-
|
|
275
|
+
<i18n-t keypath="slack.members.hint" tag="span">
|
|
276
|
+
<template #product>
|
|
277
|
+
<span class="font-medium text-slate-400">{{
|
|
278
|
+
t('slack.members.productLabel')
|
|
279
|
+
}}</span>
|
|
280
|
+
</template>
|
|
281
|
+
</i18n-t>
|
|
268
282
|
</p>
|
|
269
283
|
<div v-for="(entry, i) in mapping" :key="i" class="flex items-center gap-2">
|
|
270
|
-
<UInput
|
|
284
|
+
<UInput
|
|
285
|
+
v-model="entry.userId"
|
|
286
|
+
size="sm"
|
|
287
|
+
class="w-40"
|
|
288
|
+
:placeholder="t('slack.members.userIdPlaceholder')"
|
|
289
|
+
/>
|
|
271
290
|
<UInput
|
|
272
291
|
v-model="entry.slackUserId"
|
|
273
292
|
size="sm"
|
|
274
293
|
class="flex-1"
|
|
275
|
-
placeholder="
|
|
294
|
+
:placeholder="t('slack.members.slackIdPlaceholder')"
|
|
295
|
+
/>
|
|
296
|
+
<USelect
|
|
297
|
+
v-model="entry.role"
|
|
298
|
+
:items="ROLE_OPTIONS"
|
|
299
|
+
value-key="value"
|
|
300
|
+
size="sm"
|
|
301
|
+
class="w-32"
|
|
276
302
|
/>
|
|
277
|
-
<USelect v-model="entry.role" :items="ROLE_OPTIONS" size="sm" class="w-32" />
|
|
278
303
|
<UButton
|
|
279
304
|
color="error"
|
|
280
305
|
variant="ghost"
|
|
@@ -291,7 +316,7 @@ async function saveMapping() {
|
|
|
291
316
|
icon="i-lucide-plus"
|
|
292
317
|
@click="addMapping"
|
|
293
318
|
>
|
|
294
|
-
|
|
319
|
+
{{ t('slack.members.add') }}
|
|
295
320
|
</UButton>
|
|
296
321
|
<UButton
|
|
297
322
|
color="primary"
|
|
@@ -301,7 +326,7 @@ async function saveMapping() {
|
|
|
301
326
|
:loading="busy"
|
|
302
327
|
@click="saveMapping"
|
|
303
328
|
>
|
|
304
|
-
|
|
329
|
+
{{ t('slack.members.save') }}
|
|
305
330
|
</UButton>
|
|
306
331
|
</div>
|
|
307
332
|
</div>
|
|
@@ -20,6 +20,7 @@ const props = defineProps<{
|
|
|
20
20
|
}>()
|
|
21
21
|
const emit = defineEmits<{ pick: [item: PendingContext] }>()
|
|
22
22
|
|
|
23
|
+
const { t } = useI18n()
|
|
23
24
|
const tasks = useTasksStore()
|
|
24
25
|
|
|
25
26
|
const chosen = computed(() => new Set(props.chosenKeys ?? []))
|
|
@@ -186,28 +187,30 @@ onMounted(() => {
|
|
|
186
187
|
class="w-full"
|
|
187
188
|
:placeholder="
|
|
188
189
|
searchable
|
|
189
|
-
? '
|
|
190
|
-
: (descriptor?.refPlaceholder ?? '
|
|
190
|
+
? t('tasks.picker.searchPlaceholder')
|
|
191
|
+
: (descriptor?.refPlaceholder ?? t('tasks.picker.refPlaceholder'))
|
|
191
192
|
"
|
|
192
193
|
@keydown.enter="refRow && pickRef(refRow)"
|
|
193
194
|
/>
|
|
194
195
|
|
|
195
196
|
<p v-if="searchError" class="px-1 text-[11px] text-amber-400">
|
|
196
|
-
|
|
197
|
+
{{ t('tasks.picker.searchFailed', { error: searchError }) }}
|
|
197
198
|
</p>
|
|
198
199
|
|
|
199
200
|
<div class="max-h-56 space-y-0.5 overflow-y-auto">
|
|
200
201
|
<!-- Already-imported issues (linked directly, no re-fetch). -->
|
|
201
202
|
<button
|
|
202
|
-
v-for="
|
|
203
|
-
:key="`imp:${
|
|
203
|
+
v-for="row in importedRows"
|
|
204
|
+
:key="`imp:${row.externalId}`"
|
|
204
205
|
type="button"
|
|
205
206
|
class="flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-xs text-slate-300 hover:bg-slate-800/70"
|
|
206
|
-
@click="pickImported(
|
|
207
|
+
@click="pickImported(row)"
|
|
207
208
|
>
|
|
208
209
|
<UIcon :name="icon" class="h-3.5 w-3.5 shrink-0 text-indigo-400" />
|
|
209
|
-
<span class="truncate">{{
|
|
210
|
-
<UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">
|
|
210
|
+
<span class="truncate">{{ row.externalId }} · {{ row.title }}</span>
|
|
211
|
+
<UBadge color="neutral" variant="soft" size="xs" class="ml-auto shrink-0">{{
|
|
212
|
+
t('tasks.picker.imported')
|
|
213
|
+
}}</UBadge>
|
|
211
214
|
</button>
|
|
212
215
|
|
|
213
216
|
<!-- Tracker search hits (imported on add). -->
|
|
@@ -233,18 +236,22 @@ onMounted(() => {
|
|
|
233
236
|
@click="pickRef(refRow)"
|
|
234
237
|
>
|
|
235
238
|
<UIcon name="i-lucide-link" class="h-3.5 w-3.5 shrink-0 text-slate-400" />
|
|
236
|
-
<span class="truncate"
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
<span class="truncate">
|
|
240
|
+
<i18n-t keypath="tasks.picker.attachByReference" tag="span" scope="global">
|
|
241
|
+
<template #ref>
|
|
242
|
+
<span class="text-slate-200">{{ refRow }}</span>
|
|
243
|
+
</template>
|
|
244
|
+
</i18n-t>
|
|
245
|
+
</span>
|
|
239
246
|
</button>
|
|
240
247
|
|
|
241
248
|
<p v-if="empty" class="px-2 py-1.5 text-[11px] text-slate-500">
|
|
242
249
|
{{
|
|
243
250
|
query.trim()
|
|
244
|
-
? '
|
|
251
|
+
? t('tasks.picker.noMatches')
|
|
245
252
|
: searchable
|
|
246
|
-
? '
|
|
247
|
-
: '
|
|
253
|
+
? t('tasks.picker.emptySearchable')
|
|
254
|
+
: t('tasks.picker.emptyRefOnly')
|
|
248
255
|
}}
|
|
249
256
|
</p>
|
|
250
257
|
</div>
|
|
@@ -9,6 +9,7 @@ import type { Block, TaskSourceKind } from '~/types/domain'
|
|
|
9
9
|
|
|
10
10
|
const props = defineProps<{ block: Block }>()
|
|
11
11
|
|
|
12
|
+
const { t } = useI18n()
|
|
12
13
|
const tasks = useTasksStore()
|
|
13
14
|
const ui = useUiStore()
|
|
14
15
|
const toast = useToast()
|
|
@@ -22,10 +23,10 @@ const linked = computed(() => tasks.tasksForBlock(props.block.id))
|
|
|
22
23
|
async function attach(source: TaskSourceKind, externalId: string) {
|
|
23
24
|
try {
|
|
24
25
|
await tasks.linkToBlock(props.block.id, source, externalId)
|
|
25
|
-
toast.add({ title: '
|
|
26
|
+
toast.add({ title: t('tasks.contextIssues.attached'), icon: 'i-lucide-link' })
|
|
26
27
|
} catch (e) {
|
|
27
28
|
toast.add({
|
|
28
|
-
title: '
|
|
29
|
+
title: t('tasks.contextIssues.attachFailed'),
|
|
29
30
|
description: e instanceof Error ? e.message : String(e),
|
|
30
31
|
icon: 'i-lucide-triangle-alert',
|
|
31
32
|
color: 'error',
|
|
@@ -43,7 +44,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
|
43
44
|
onSelect: () => attach(t.source, t.externalId),
|
|
44
45
|
}))
|
|
45
46
|
items.push({
|
|
46
|
-
label: '
|
|
47
|
+
label: t('tasks.contextIssues.importIssue'),
|
|
47
48
|
icon: 'i-lucide-file-down',
|
|
48
49
|
onSelect: () => ui.openTaskImport(),
|
|
49
50
|
})
|
|
@@ -55,10 +56,12 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
|
55
56
|
<div v-if="tasks.available" class="space-y-2">
|
|
56
57
|
<div class="flex items-center justify-between">
|
|
57
58
|
<span class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
58
|
-
|
|
59
|
+
{{ t('tasks.contextIssues.title') }}
|
|
59
60
|
</span>
|
|
60
61
|
<UDropdownMenu :items="attachMenu" :content="{ side: 'bottom', align: 'end' }">
|
|
61
|
-
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">
|
|
62
|
+
<UButton color="neutral" variant="soft" size="xs" icon="i-lucide-plus">{{
|
|
63
|
+
t('tasks.contextIssues.attach')
|
|
64
|
+
}}</UButton>
|
|
62
65
|
</UDropdownMenu>
|
|
63
66
|
</div>
|
|
64
67
|
|
|
@@ -82,7 +85,7 @@ const attachMenu = computed<DropdownMenuItem[][]>(() => {
|
|
|
82
85
|
</a>
|
|
83
86
|
</div>
|
|
84
87
|
<p v-else class="text-[11px] text-slate-500">
|
|
85
|
-
|
|
88
|
+
{{ t('tasks.contextIssues.emptyHint') }}
|
|
86
89
|
</p>
|
|
87
90
|
</div>
|
|
88
91
|
</template>
|