i18n-dashboard 0.17.0 → 0.18.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.
- package/package.json +1 -1
- package/src/assets/locales/en.json +16 -1
- package/src/components/common/FormatImportModal.vue +272 -0
- package/src/components/common/FormatSnippet.vue +52 -0
- package/src/components/{ScanModal.vue → common/ScanModal.vue} +26 -34
- package/src/components/{dashboard → common}/WidgetConfigModal.vue +64 -81
- package/src/components/{dashboard → common}/WidgetPicker.vue +44 -50
- package/src/components/{dashboard → common}/widgets/ActivityWidget.vue +57 -75
- package/src/components/common/widgets/CustomIframeWidget.vue +59 -0
- package/src/components/common/widgets/LanguagesCoverageWidget.vue +162 -0
- package/src/components/{dashboard → common}/widgets/ProjectsWidget.vue +42 -43
- package/src/components/common/widgets/ReviewWidget.vue +133 -0
- package/src/components/common/widgets/StatWidget.vue +138 -0
- package/src/components/dashboard/WidgetGrid.vue +100 -94
- package/src/components/{GitRepoManager.vue → project/GitRepoManager.vue} +13 -17
- package/src/components/{LanguagePicker.vue → project/LanguagePicker.vue} +19 -23
- package/src/components/{LinkedKeyPicker.vue → project/LinkedKeyPicker.vue} +18 -26
- package/src/components/{PathPicker.vue → project/PathPicker.vue} +23 -49
- package/src/components/{PluralEditor.vue → project/PluralEditor.vue} +20 -22
- package/src/components/{TranslationHistoryModal.vue → project/TranslationHistoryModal.vue} +25 -19
- package/src/components/{TranslationRow.vue → project/TranslationRow.vue} +76 -102
- package/src/components/{dashboard/ProjectWidgetGrid.vue → project/WidgetGrid.vue} +99 -94
- package/src/composables/useAdmin.ts +127 -0
- package/src/composables/useAuth.ts +9 -1
- package/src/composables/useDashboard.ts +13 -12
- package/src/composables/useFormats.ts +79 -58
- package/src/composables/useFs.ts +24 -0
- package/src/composables/useKeys.ts +51 -12
- package/src/composables/useLanguages.ts +19 -17
- package/src/composables/useOnboarding.ts +45 -0
- package/src/composables/useProfile.ts +3 -3
- package/src/composables/useProject.ts +31 -5
- package/src/composables/useProjectDashboard.ts +15 -16
- package/src/composables/useReview.ts +13 -9
- package/src/composables/useSettings.ts +7 -3
- package/src/composables/useT.ts +2 -1
- package/src/composables/useUsers.ts +10 -5
- package/src/composables/useWidgetData.ts +10 -8
- package/src/composables/useWidgetRegistry.ts +4 -3
- package/src/consts/dashboard.const.ts +50 -49
- package/src/enums/dashboard.enum.ts +23 -0
- package/src/interfaces/dashboard.interface.ts +60 -2
- package/src/interfaces/key.interface.ts +28 -0
- package/src/interfaces/languages.interface.ts +14 -0
- package/src/interfaces/project.interface.ts +24 -0
- package/src/interfaces/scan.interface.ts +15 -0
- package/src/interfaces/scanner.interface.ts +7 -0
- package/src/interfaces/translation.interface.ts +16 -0
- package/src/layouts/auth.vue +8 -5
- package/src/layouts/default.vue +321 -242
- package/src/middleware/auth.global.ts +4 -3
- package/src/pages/admin/customization.vue +255 -264
- package/src/pages/admin/logs.vue +46 -77
- package/src/pages/admin/security.vue +60 -65
- package/src/pages/admin/smtp.vue +141 -158
- package/src/pages/forgot-password.vue +18 -20
- package/src/pages/index.vue +1 -1
- package/src/pages/login.vue +55 -53
- package/src/pages/onboarding.vue +421 -408
- package/src/pages/projects/[id]/formats/datetime.vue +189 -172
- package/src/pages/projects/[id]/formats/modifiers.vue +144 -119
- package/src/pages/projects/[id]/formats/number.vue +195 -168
- package/src/pages/projects/[id]/index.vue +1 -1
- package/src/pages/projects/[id]/languages.vue +378 -335
- package/src/pages/projects/[id]/review.vue +76 -68
- package/src/pages/projects/[id]/settings.vue +204 -136
- package/src/pages/projects/[id]/translations/[keyId].vue +448 -405
- package/src/pages/projects/[id]/translations/index.vue +202 -164
- package/src/pages/projects/[id]/users.vue +301 -266
- package/src/pages/projects/index.vue +122 -126
- package/src/pages/reset-password.vue +68 -72
- package/src/pages/users/[id]/profile.vue +292 -264
- package/src/pages/users/index.vue +63 -63
- package/src/server/api/formats/detect.post.ts +74 -0
- package/src/server/api/formats/import-from-config.post.ts +68 -0
- package/src/server/api/languages/[code].put.ts +25 -0
- package/src/server/api/projects/[id].put.ts +2 -1
- package/src/server/api/stats/global.get.ts +34 -23
- package/src/server/utils/project-config.util.ts +1 -1
- package/src/server/utils/scanner.util.ts +216 -1
- package/src/services/admin.service.ts +87 -0
- package/src/services/auth.service.ts +12 -0
- package/src/services/dashboard.service.ts +22 -0
- package/src/services/formats.service.ts +76 -0
- package/src/services/fs.service.ts +17 -0
- package/src/services/language.service.ts +4 -0
- package/src/services/locale.service.ts +9 -0
- package/src/services/onboarding.service.ts +60 -0
- package/src/services/project.service.ts +15 -0
- package/src/services/scan.service.ts +11 -0
- package/src/services/settings.service.ts +4 -0
- package/src/services/stats.service.ts +4 -0
- package/src/types/dashboard.type.ts +5 -11
- package/src/components/dashboard/widgets/CustomIframeWidget.vue +0 -56
- package/src/components/dashboard/widgets/LanguagesCoverageWidget.vue +0 -144
- package/src/components/dashboard/widgets/ReviewWidget.vue +0 -199
- package/src/components/dashboard/widgets/StatWidget.vue +0 -159
- package/src/server/api/languages/[id].put.ts +0 -24
package/package.json
CHANGED
|
@@ -598,5 +598,20 @@
|
|
|
598
598
|
"customization.widget_icon_hint": "Heroicons name, e.g. i-heroicons-chart-bar",
|
|
599
599
|
"customization.widget_sizes": "Available sizes",
|
|
600
600
|
"customization.widget_default_size": "Default size",
|
|
601
|
-
"customization.nav_label": "Customization"
|
|
601
|
+
"customization.nav_label": "Customization",
|
|
602
|
+
|
|
603
|
+
"history.modal_title": "Translation History",
|
|
604
|
+
"history.no_history": "No history available",
|
|
605
|
+
"history.before": "Before",
|
|
606
|
+
"history.after": "After",
|
|
607
|
+
"history.source_google": "Google Translate",
|
|
608
|
+
"history.source_sync": "File Sync",
|
|
609
|
+
"history.source_manual": "Manual",
|
|
610
|
+
|
|
611
|
+
"dashboard.widget_no_url": "No URL configured",
|
|
612
|
+
|
|
613
|
+
"projects.locales_path_placeholder": "src/locales",
|
|
614
|
+
"projects.key_separator_placeholder": ".",
|
|
615
|
+
"projects.detection_failed": "Detection failed",
|
|
616
|
+
"projects.create_failed": "Could not create project"
|
|
602
617
|
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<u-modal
|
|
3
|
+
v-model:open="open"
|
|
4
|
+
:title="t('formats.import_title', 'Import formats from project config')"
|
|
5
|
+
:ui="{ width: '40rem' }"
|
|
6
|
+
>
|
|
7
|
+
<template #body>
|
|
8
|
+
<div class="space-y-4">
|
|
9
|
+
<!-- Loading -->
|
|
10
|
+
<div
|
|
11
|
+
v-if="loading"
|
|
12
|
+
class="flex items-center justify-center py-10 gap-3 text-gray-400"
|
|
13
|
+
>
|
|
14
|
+
<u-icon
|
|
15
|
+
class="animate-spin text-xl"
|
|
16
|
+
name="i-heroicons-arrow-path"
|
|
17
|
+
/>
|
|
18
|
+
<span class="text-sm">{{ t('formats.import_scanning', 'Scanning config files…') }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- No formats found -->
|
|
22
|
+
<div
|
|
23
|
+
v-else-if="result && !result.sourceFile"
|
|
24
|
+
class="text-center py-10 text-gray-400"
|
|
25
|
+
>
|
|
26
|
+
<u-icon
|
|
27
|
+
class="text-3xl mb-2"
|
|
28
|
+
name="i-heroicons-magnifying-glass"
|
|
29
|
+
/>
|
|
30
|
+
<p class="font-medium text-sm">
|
|
31
|
+
{{ t('formats.import_none_found', 'No formats detected') }}
|
|
32
|
+
</p>
|
|
33
|
+
<p class="text-xs mt-1">
|
|
34
|
+
{{ t('formats.import_none_found_hint', 'No numberFormats, datetimeFormats or modifiers found in config files.') }}
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<template v-else-if="result">
|
|
39
|
+
<!-- Source file -->
|
|
40
|
+
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 rounded-lg px-3 py-2">
|
|
41
|
+
<u-icon name="i-heroicons-document-text" />
|
|
42
|
+
{{ t('formats.import_source', 'Detected in') }}
|
|
43
|
+
<code class="font-mono font-medium text-gray-700 dark:text-gray-300">{{ result.sourceFile }}</code>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<!-- Nothing new to import -->
|
|
47
|
+
<div
|
|
48
|
+
v-if="totalToImport === 0"
|
|
49
|
+
class="bg-green-50 dark:bg-green-900/20 rounded-lg px-3 py-2 text-sm text-green-700 dark:text-green-400 flex items-center gap-2"
|
|
50
|
+
>
|
|
51
|
+
<u-icon name="i-heroicons-check-circle" />
|
|
52
|
+
{{ t('formats.import_all_synced', 'All detected formats are already configured in this project.') }}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Formats to import -->
|
|
56
|
+
<template v-if="totalToImport > 0">
|
|
57
|
+
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
58
|
+
{{ t('formats.import_new', 'New formats to import') }}
|
|
59
|
+
<u-badge
|
|
60
|
+
:label="String(totalToImport)"
|
|
61
|
+
color="primary"
|
|
62
|
+
size="xs"
|
|
63
|
+
class="ml-1"
|
|
64
|
+
variant="soft"
|
|
65
|
+
/>
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
<!-- Number formats -->
|
|
69
|
+
<div v-if="selected.numberFormats.length">
|
|
70
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 mb-2">
|
|
71
|
+
{{ t('formats.number_title', 'Number formats') }}
|
|
72
|
+
</p>
|
|
73
|
+
<div class="space-y-1">
|
|
74
|
+
<div
|
|
75
|
+
v-for="f in selected.numberFormats"
|
|
76
|
+
:key="`n-${f.locale}-${f.name}`"
|
|
77
|
+
class="flex items-center gap-2.5 px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
78
|
+
@click="f.checked = !f.checked"
|
|
79
|
+
>
|
|
80
|
+
<u-checkbox
|
|
81
|
+
:model-value="f.checked"
|
|
82
|
+
@click.stop
|
|
83
|
+
@update:model-value="f.checked = Boolean($event)"
|
|
84
|
+
/>
|
|
85
|
+
<span class="text-xs font-mono bg-gray-200 dark:bg-gray-700 px-1.5 py-0.5 rounded text-gray-600 dark:text-gray-300">{{ f.locale }}</span>
|
|
86
|
+
<span class="text-sm font-medium text-gray-800 dark:text-gray-200 flex-1">{{ f.name }}</span>
|
|
87
|
+
<code class="text-xs text-gray-400 truncate max-w-40">{{ formatOptionsPreview(f.options) }}</code>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Datetime formats -->
|
|
93
|
+
<div v-if="selected.datetimeFormats.length">
|
|
94
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 mb-2">
|
|
95
|
+
{{ t('formats.datetime_title', 'Date formats') }}
|
|
96
|
+
</p>
|
|
97
|
+
<div class="space-y-1">
|
|
98
|
+
<div
|
|
99
|
+
v-for="f in selected.datetimeFormats"
|
|
100
|
+
:key="`d-${f.locale}-${f.name}`"
|
|
101
|
+
class="flex items-center gap-2.5 px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
102
|
+
@click="f.checked = !f.checked"
|
|
103
|
+
>
|
|
104
|
+
<u-checkbox
|
|
105
|
+
:model-value="f.checked"
|
|
106
|
+
@click.stop
|
|
107
|
+
@update:model-value="f.checked = Boolean($event)"
|
|
108
|
+
/>
|
|
109
|
+
<span class="text-xs font-mono bg-gray-200 dark:bg-gray-700 px-1.5 py-0.5 rounded text-gray-600 dark:text-gray-300">{{ f.locale }}</span>
|
|
110
|
+
<span class="text-sm font-medium text-gray-800 dark:text-gray-200 flex-1">{{ f.name }}</span>
|
|
111
|
+
<code class="text-xs text-gray-400 truncate max-w-40">{{ formatOptionsPreview(f.options) }}</code>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Modifiers -->
|
|
117
|
+
<div v-if="selected.modifiers.length">
|
|
118
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-gray-400 mb-2">
|
|
119
|
+
{{ t('settings.custom_modifiers', 'Custom modifiers') }}
|
|
120
|
+
</p>
|
|
121
|
+
<div class="space-y-1">
|
|
122
|
+
<div
|
|
123
|
+
v-for="m in selected.modifiers"
|
|
124
|
+
:key="`m-${m.name}`"
|
|
125
|
+
class="flex items-center gap-2.5 px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
126
|
+
@click="m.checked = !m.checked"
|
|
127
|
+
>
|
|
128
|
+
<u-checkbox
|
|
129
|
+
:model-value="m.checked"
|
|
130
|
+
@click.stop
|
|
131
|
+
@update:model-value="m.checked = Boolean($event)"
|
|
132
|
+
/>
|
|
133
|
+
<span class="text-sm font-medium text-gray-800 dark:text-gray-200 flex-1">{{ m.name }}</span>
|
|
134
|
+
<code class="text-xs text-gray-400 truncate max-w-48 font-mono">{{ m.body }}</code>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<!-- Already existing -->
|
|
141
|
+
<div
|
|
142
|
+
v-if="totalAlreadyExisting > 0"
|
|
143
|
+
class="text-xs text-gray-400 flex items-center gap-1.5 pt-1"
|
|
144
|
+
>
|
|
145
|
+
<u-icon name="i-heroicons-check" />
|
|
146
|
+
{{ totalAlreadyExisting }}
|
|
147
|
+
{{ t('formats.import_already_exist', 'format(s) already configured — skipped') }}
|
|
148
|
+
</div>
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<p
|
|
152
|
+
v-if="error"
|
|
153
|
+
class="text-sm text-red-500"
|
|
154
|
+
>
|
|
155
|
+
{{ error }}
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
159
|
+
|
|
160
|
+
<template #footer>
|
|
161
|
+
<div class="flex justify-end gap-2">
|
|
162
|
+
<u-button
|
|
163
|
+
color="neutral"
|
|
164
|
+
variant="ghost"
|
|
165
|
+
@click="open = false"
|
|
166
|
+
>
|
|
167
|
+
{{ t('common.cancel', 'Cancel') }}
|
|
168
|
+
</u-button>
|
|
169
|
+
<u-button
|
|
170
|
+
v-if="totalToImport > 0"
|
|
171
|
+
:disabled="checkedCount === 0"
|
|
172
|
+
:loading="importing"
|
|
173
|
+
icon="i-heroicons-arrow-down-tray"
|
|
174
|
+
@click="doImport"
|
|
175
|
+
>
|
|
176
|
+
{{ t('formats.import_btn', 'Import') }} ({{ checkedCount }})
|
|
177
|
+
</u-button>
|
|
178
|
+
</div>
|
|
179
|
+
</template>
|
|
180
|
+
</u-modal>
|
|
181
|
+
</template>
|
|
182
|
+
|
|
183
|
+
<script setup lang="ts">
|
|
184
|
+
const { t } = useT()
|
|
185
|
+
const { detectFromConfig, importFromConfig } = useFormats()
|
|
186
|
+
const toast = useToast()
|
|
187
|
+
|
|
188
|
+
const props = defineProps<{
|
|
189
|
+
rootPath?: string
|
|
190
|
+
}>()
|
|
191
|
+
|
|
192
|
+
const emit = defineEmits<{ done: [] }>()
|
|
193
|
+
|
|
194
|
+
const open = defineModel<boolean>('open', { default: false })
|
|
195
|
+
|
|
196
|
+
const loading = ref(false)
|
|
197
|
+
const importing = ref(false)
|
|
198
|
+
const error = ref('')
|
|
199
|
+
const result = ref<any>(null)
|
|
200
|
+
|
|
201
|
+
const selected = ref<{
|
|
202
|
+
numberFormats: Array<{ locale: string; name: string; options: Record<string, any>; checked: boolean }>
|
|
203
|
+
datetimeFormats: Array<{ locale: string; name: string; options: Record<string, any>; checked: boolean }>
|
|
204
|
+
modifiers: Array<{ name: string; body: string; checked: boolean }>
|
|
205
|
+
}>({ numberFormats: [], datetimeFormats: [], modifiers: [] })
|
|
206
|
+
|
|
207
|
+
const totalToImport = computed(
|
|
208
|
+
() => selected.value.numberFormats.length + selected.value.datetimeFormats.length + selected.value.modifiers.length,
|
|
209
|
+
)
|
|
210
|
+
const totalAlreadyExisting = computed(() => {
|
|
211
|
+
if (!result.value) return 0
|
|
212
|
+
return (
|
|
213
|
+
result.value.alreadyExisting.numberFormats.length +
|
|
214
|
+
result.value.alreadyExisting.datetimeFormats.length +
|
|
215
|
+
result.value.alreadyExisting.modifiers.length
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
const checkedCount = computed(
|
|
219
|
+
() =>
|
|
220
|
+
selected.value.numberFormats.filter((f) => f.checked).length +
|
|
221
|
+
selected.value.datetimeFormats.filter((f) => f.checked).length +
|
|
222
|
+
selected.value.modifiers.filter((m) => m.checked).length,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
const formatOptionsPreview = (opts: Record<string, any>) => {
|
|
226
|
+
const entries = Object.entries(opts).slice(0, 3)
|
|
227
|
+
return entries.map(([k, v]) => `${k}: ${v}`).join(', ')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
watch(open, async (val) => {
|
|
231
|
+
if (!val) return
|
|
232
|
+
error.value = ''
|
|
233
|
+
result.value = null
|
|
234
|
+
selected.value = { numberFormats: [], datetimeFormats: [], modifiers: [] }
|
|
235
|
+
loading.value = true
|
|
236
|
+
try {
|
|
237
|
+
result.value = await detectFromConfig(props.rootPath)
|
|
238
|
+
if (result.value) {
|
|
239
|
+
selected.value.numberFormats = result.value.toImport.numberFormats.map((f: any) => ({ ...f, checked: true }))
|
|
240
|
+
selected.value.datetimeFormats = result.value.toImport.datetimeFormats.map((f: any) => ({ ...f, checked: true }))
|
|
241
|
+
selected.value.modifiers = result.value.toImport.modifiers.map((m: any) => ({ ...m, checked: true }))
|
|
242
|
+
}
|
|
243
|
+
} catch (e: any) {
|
|
244
|
+
error.value = e?.data?.message ?? e?.message ?? t('common.error', 'Error')
|
|
245
|
+
} finally {
|
|
246
|
+
loading.value = false
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const doImport = async () => {
|
|
251
|
+
importing.value = true
|
|
252
|
+
try {
|
|
253
|
+
const res = await importFromConfig(
|
|
254
|
+
selected.value.numberFormats.filter((f) => f.checked).map(({ locale, name, options }) => ({ locale, name, options })),
|
|
255
|
+
selected.value.datetimeFormats.filter((f) => f.checked).map(({ locale, name, options }) => ({ locale, name, options })),
|
|
256
|
+
selected.value.modifiers.filter((m) => m.checked).map(({ name, body }) => ({ name, body })),
|
|
257
|
+
) as any
|
|
258
|
+
const total = res.added.numberFormats + res.added.datetimeFormats + res.added.modifiers
|
|
259
|
+
toast.add({
|
|
260
|
+
title: t('formats.import_success', 'Formats imported'),
|
|
261
|
+
description: `${total} ${t('formats.import_added', 'format(s) added')}`,
|
|
262
|
+
color: 'success',
|
|
263
|
+
})
|
|
264
|
+
open.value = false
|
|
265
|
+
emit('done')
|
|
266
|
+
} catch (e: any) {
|
|
267
|
+
error.value = e?.data?.message ?? e?.message ?? t('common.error', 'Error')
|
|
268
|
+
} finally {
|
|
269
|
+
importing.value = false
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<u-card v-if="snippet">
|
|
3
|
+
<template #header>
|
|
4
|
+
<div class="flex items-center justify-between gap-2">
|
|
5
|
+
<div class="flex items-center gap-2">
|
|
6
|
+
<u-icon
|
|
7
|
+
class="text-green-500"
|
|
8
|
+
name="i-heroicons-code-bracket"
|
|
9
|
+
/>
|
|
10
|
+
<h2 class="font-semibold text-gray-900 dark:text-white">
|
|
11
|
+
{{ t('formats.snippet_title', 'Integration snippet') }}
|
|
12
|
+
</h2>
|
|
13
|
+
</div>
|
|
14
|
+
<u-button
|
|
15
|
+
:label="copied ? t('common.copied', 'Copied!') : t('common.copy', 'Copy')"
|
|
16
|
+
:color="copied ? 'success' : 'neutral'"
|
|
17
|
+
:icon="copied ? 'i-heroicons-check' : 'i-heroicons-clipboard'"
|
|
18
|
+
size="xs"
|
|
19
|
+
variant="ghost"
|
|
20
|
+
@click="copy"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
|
26
|
+
{{ t('formats.snippet_hint', 'Paste this into your') }}
|
|
27
|
+
<code class="font-mono bg-gray-100 dark:bg-gray-800 px-1 rounded">i18n.ts</code>
|
|
28
|
+
{{ t('formats.snippet_hint2', 'to use all configured formats and modifiers.') }}
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="relative">
|
|
32
|
+
<pre class="text-xs font-mono bg-gray-950 dark:bg-gray-900 text-gray-100 rounded-lg p-4 overflow-x-auto leading-relaxed"><code>{{ snippet }}</code></pre>
|
|
33
|
+
</div>
|
|
34
|
+
</u-card>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script lang="ts" setup>
|
|
38
|
+
export interface IFormatSnippetProps {
|
|
39
|
+
snippet: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const props = defineProps<IFormatSnippetProps>()
|
|
43
|
+
|
|
44
|
+
const { t } = useT()
|
|
45
|
+
const copied = ref(false)
|
|
46
|
+
|
|
47
|
+
const copy = async () => {
|
|
48
|
+
await navigator.clipboard.writeText(props.snippet)
|
|
49
|
+
copied.value = true
|
|
50
|
+
setTimeout(() => { copied.value = false }, 2000)
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<u-modal
|
|
3
3
|
v-model:open="open"
|
|
4
4
|
:title="t('scan.modal_title', 'Scan project')"
|
|
5
5
|
:ui="{ width: '48rem' }"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
|
18
18
|
@click="mode = m.value"
|
|
19
19
|
>
|
|
20
|
-
<
|
|
20
|
+
<u-icon
|
|
21
21
|
:name="m.icon"
|
|
22
22
|
class="text-sm"
|
|
23
23
|
/>
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
34
34
|
{{ t('scan.local_hint', 'Select the root folder of your Vue.js project. The scanner will detect all $t(), t(), <i18n-t> and v-t usages.') }}
|
|
35
35
|
</p>
|
|
36
|
-
<
|
|
37
|
-
<
|
|
36
|
+
<u-form-field :label="t('scan.local_path_label', 'Project root folder')">
|
|
37
|
+
<project-path-picker
|
|
38
38
|
v-model="localPath"
|
|
39
39
|
class="w-full"
|
|
40
40
|
/>
|
|
41
|
-
</
|
|
41
|
+
</u-form-field>
|
|
42
42
|
</div>
|
|
43
43
|
|
|
44
44
|
<!-- Git mode -->
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
50
50
|
{{ t('scan.git_hint', 'Clone a Git repository and scan source files for translation keys.') }}
|
|
51
51
|
</p>
|
|
52
|
-
<
|
|
52
|
+
<project-git-repo-manager v-model="gitRepo" />
|
|
53
53
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
54
|
-
<
|
|
54
|
+
<u-switch
|
|
55
55
|
v-model="saveRepo"
|
|
56
56
|
size="sm"
|
|
57
57
|
/>
|
|
@@ -148,37 +148,35 @@
|
|
|
148
148
|
|
|
149
149
|
<template #footer>
|
|
150
150
|
<div class="flex justify-end gap-2">
|
|
151
|
-
<
|
|
151
|
+
<u-button
|
|
152
152
|
color="neutral"
|
|
153
153
|
variant="ghost"
|
|
154
154
|
@click="open = false"
|
|
155
155
|
>
|
|
156
156
|
{{ t('common.cancel', 'Cancel') }}
|
|
157
|
-
</
|
|
158
|
-
<
|
|
157
|
+
</u-button>
|
|
158
|
+
<u-button
|
|
159
159
|
:loading="loading"
|
|
160
160
|
:disabled="mode === 'local' ? !localPath : !gitRepo?.url"
|
|
161
161
|
icon="i-heroicons-magnifying-glass"
|
|
162
162
|
@click="runScan"
|
|
163
163
|
>
|
|
164
164
|
{{ t('scan.run', 'Scan') }}
|
|
165
|
-
</
|
|
165
|
+
</u-button>
|
|
166
166
|
</div>
|
|
167
167
|
</template>
|
|
168
|
-
</
|
|
168
|
+
</u-modal>
|
|
169
169
|
</template>
|
|
170
170
|
|
|
171
171
|
<script setup lang="ts">
|
|
172
|
-
import type {
|
|
172
|
+
import type { IScanModalProps, IScanModalEmits } from '../../interfaces/scan.interface'
|
|
173
173
|
|
|
174
174
|
const { t } = useT()
|
|
175
|
+
const { updateProject, scanWithOptions } = useProject()
|
|
175
176
|
|
|
176
|
-
const props = defineProps<
|
|
177
|
-
projectId: number
|
|
178
|
-
project?: { languages?: { code: string; name: string }[]; root_path?: string; git_repo?: IGitRepo | null }
|
|
179
|
-
}>()
|
|
177
|
+
const props = defineProps<IScanModalProps>()
|
|
180
178
|
|
|
181
|
-
const emit = defineEmits<
|
|
179
|
+
const emit = defineEmits<IScanModalEmits>()
|
|
182
180
|
|
|
183
181
|
const open = defineModel<boolean>('open', { default: false })
|
|
184
182
|
|
|
@@ -206,32 +204,26 @@ watch(open, (val) => {
|
|
|
206
204
|
}
|
|
207
205
|
})
|
|
208
206
|
|
|
209
|
-
async
|
|
207
|
+
const runScan = async () => {
|
|
210
208
|
loading.value = true
|
|
211
209
|
error.value = ''
|
|
212
210
|
result.value = null
|
|
213
211
|
try {
|
|
214
212
|
if (mode.value === 'git' && saveRepo.value && gitRepo.value?.url) {
|
|
215
|
-
await
|
|
216
|
-
method: 'PUT',
|
|
217
|
-
body: { git_repo: gitRepo.value },
|
|
218
|
-
})
|
|
213
|
+
await updateProject(props.projectId, { git_repo: gitRepo.value })
|
|
219
214
|
}
|
|
220
215
|
|
|
221
|
-
result.value = await
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
git_branch: mode.value === 'git' ? gitRepo.value?.branch : undefined,
|
|
229
|
-
git_token: mode.value === 'git' ? gitRepo.value?.token : undefined,
|
|
230
|
-
},
|
|
216
|
+
result.value = await scanWithOptions({
|
|
217
|
+
project_id: props.projectId,
|
|
218
|
+
mode: mode.value,
|
|
219
|
+
root_path: mode.value === 'local' ? localPath.value : undefined,
|
|
220
|
+
git_url: mode.value === 'git' ? gitRepo.value?.url : undefined,
|
|
221
|
+
git_branch: mode.value === 'git' ? gitRepo.value?.branch : undefined,
|
|
222
|
+
git_token: mode.value === 'git' ? gitRepo.value?.token : undefined,
|
|
231
223
|
})
|
|
232
224
|
emit('done')
|
|
233
225
|
} catch (e: any) {
|
|
234
|
-
error.value = e?.
|
|
226
|
+
error.value = e?.message ?? t('common.error', 'Error')
|
|
235
227
|
} finally {
|
|
236
228
|
loading.value = false
|
|
237
229
|
}
|
|
@@ -1,69 +1,5 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import type { PropType } from 'vue'
|
|
3
|
-
import type { TDataSourceType } from '../../types/dashboard.type'
|
|
4
|
-
import type { IWidgetConfig, IWidgetDataSource } from '../../interfaces/dashboard.interface'
|
|
5
|
-
import { WIDGET_REGISTRY } from '../../consts/dashboard.const'
|
|
6
|
-
|
|
7
|
-
const props = defineProps({
|
|
8
|
-
open: {
|
|
9
|
-
type: Boolean,
|
|
10
|
-
required: true,
|
|
11
|
-
},
|
|
12
|
-
widget: {
|
|
13
|
-
type: Object as PropType<IWidgetConfig | null>,
|
|
14
|
-
default: null,
|
|
15
|
-
},
|
|
16
|
-
index: {
|
|
17
|
-
type: Number,
|
|
18
|
-
default: -1,
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const emit = defineEmits<{
|
|
23
|
-
(e: 'update:open', value: boolean): void
|
|
24
|
-
(e: 'save', value: { dataSource: IWidgetDataSource | undefined; title: string | undefined }): void
|
|
25
|
-
}>()
|
|
26
|
-
|
|
27
|
-
const { t } = useT()
|
|
28
|
-
const { visibleProjects } = useProject()
|
|
29
|
-
|
|
30
|
-
const draftSource = ref<TDataSourceType>('global')
|
|
31
|
-
const draftProjectId = ref<number | undefined>()
|
|
32
|
-
const draftTitle = ref('')
|
|
33
|
-
|
|
34
|
-
watch(
|
|
35
|
-
() => props.widget,
|
|
36
|
-
(w) => {
|
|
37
|
-
if (!w) return
|
|
38
|
-
draftSource.value = w.dataSource?.type ?? 'global'
|
|
39
|
-
draftProjectId.value = w.dataSource?.projectId
|
|
40
|
-
draftTitle.value = w.title ?? ''
|
|
41
|
-
},
|
|
42
|
-
{ immediate: true },
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
const projectItems = computed(() =>
|
|
46
|
-
visibleProjects.value
|
|
47
|
-
.filter((p: any) => !p.is_system)
|
|
48
|
-
.map((p: any) => ({ label: p.name, value: p.id })),
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
const hasDataSource = computed(() => {
|
|
52
|
-
if (!props.widget) return false
|
|
53
|
-
return WIDGET_REGISTRY[props.widget.type].hasDataSource
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
function save() {
|
|
57
|
-
const dataSource: IWidgetDataSource = draftSource.value === 'project'
|
|
58
|
-
? { type: 'project', projectId: draftProjectId.value }
|
|
59
|
-
: { type: 'global' }
|
|
60
|
-
emit('save', { dataSource, title: draftTitle.value || undefined })
|
|
61
|
-
emit('update:open', false)
|
|
62
|
-
}
|
|
63
|
-
</script>
|
|
64
|
-
|
|
65
1
|
<template>
|
|
66
|
-
<
|
|
2
|
+
<u-modal
|
|
67
3
|
:open="open"
|
|
68
4
|
:title="t('dashboard.configure_widget', 'Configure widget')"
|
|
69
5
|
@update:open="emit('update:open', $event)"
|
|
@@ -79,27 +15,27 @@ function save() {
|
|
|
79
15
|
</p>
|
|
80
16
|
<div class="flex gap-2">
|
|
81
17
|
<button
|
|
82
|
-
class="
|
|
83
|
-
:class="draftSource === 'global'
|
|
18
|
+
:class="draftSource === DATA_SOURCE_TYPE.GLOBAL
|
|
84
19
|
? 'bg-primary-500 text-white border-primary-500'
|
|
85
20
|
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'"
|
|
86
|
-
|
|
21
|
+
class="flex-1 px-3 py-2 rounded-lg text-sm font-medium border transition-colors"
|
|
22
|
+
@click="draftSource = DATA_SOURCE_TYPE.GLOBAL"
|
|
87
23
|
>
|
|
88
|
-
Global
|
|
24
|
+
{{ t('dashboard.global_project', 'Global') }}
|
|
89
25
|
</button>
|
|
90
26
|
<button
|
|
91
|
-
class="
|
|
92
|
-
:class="draftSource === 'project'
|
|
27
|
+
:class="draftSource === DATA_SOURCE_TYPE.PROJECT
|
|
93
28
|
? 'bg-primary-500 text-white border-primary-500'
|
|
94
29
|
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'"
|
|
95
|
-
|
|
30
|
+
class="flex-1 px-3 py-2 rounded-lg text-sm font-medium border transition-colors"
|
|
31
|
+
@click="draftSource = DATA_SOURCE_TYPE.PROJECT"
|
|
96
32
|
>
|
|
97
33
|
{{ t('dashboard.specific_project', 'Specific project') }}
|
|
98
34
|
</button>
|
|
99
35
|
</div>
|
|
100
36
|
|
|
101
|
-
<div v-if="draftSource ===
|
|
102
|
-
<
|
|
37
|
+
<div v-if="draftSource === DATA_SOURCE_TYPE.PROJECT">
|
|
38
|
+
<u-select
|
|
103
39
|
v-model="draftProjectId"
|
|
104
40
|
:items="projectItems"
|
|
105
41
|
:placeholder="t('dashboard.select_project', 'Select a project')"
|
|
@@ -111,7 +47,7 @@ function save() {
|
|
|
111
47
|
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
112
48
|
{{ t('dashboard.custom_title', 'Custom title') }}
|
|
113
49
|
</p>
|
|
114
|
-
<
|
|
50
|
+
<u-input
|
|
115
51
|
v-model="draftTitle"
|
|
116
52
|
:placeholder="t('dashboard.default_title', 'Default title')"
|
|
117
53
|
/>
|
|
@@ -121,17 +57,64 @@ function save() {
|
|
|
121
57
|
|
|
122
58
|
<template #footer>
|
|
123
59
|
<div class="flex justify-end gap-2">
|
|
124
|
-
<
|
|
125
|
-
variant="ghost"
|
|
60
|
+
<u-button
|
|
126
61
|
color="neutral"
|
|
62
|
+
variant="ghost"
|
|
127
63
|
@click="emit('update:open', false)"
|
|
128
64
|
>
|
|
129
65
|
{{ t('common.cancel', 'Cancel') }}
|
|
130
|
-
</
|
|
131
|
-
<
|
|
66
|
+
</u-button>
|
|
67
|
+
<u-button @click="save">
|
|
132
68
|
{{ t('common.save', 'Save') }}
|
|
133
|
-
</
|
|
69
|
+
</u-button>
|
|
134
70
|
</div>
|
|
135
71
|
</template>
|
|
136
|
-
</
|
|
72
|
+
</u-modal>
|
|
137
73
|
</template>
|
|
74
|
+
|
|
75
|
+
<script lang="ts" setup>
|
|
76
|
+
import type { TDataSourceType } from '../../types/dashboard.type'
|
|
77
|
+
import type { IWidgetDataSource, IWidgetConfigModalProps, IWidgetConfigModalEmits } from '../../interfaces/dashboard.interface'
|
|
78
|
+
import { WIDGET_REGISTRY } from '../../consts/dashboard.const'
|
|
79
|
+
import { DATA_SOURCE_TYPE } from '../../enums/dashboard.enum'
|
|
80
|
+
|
|
81
|
+
const props = defineProps<IWidgetConfigModalProps>()
|
|
82
|
+
const emit = defineEmits<IWidgetConfigModalEmits>()
|
|
83
|
+
|
|
84
|
+
const { t } = useT()
|
|
85
|
+
const { visibleProjects } = useProject()
|
|
86
|
+
|
|
87
|
+
const draftSource = ref<TDataSourceType>(DATA_SOURCE_TYPE.GLOBAL)
|
|
88
|
+
const draftProjectId = ref<number | undefined>()
|
|
89
|
+
const draftTitle = ref('')
|
|
90
|
+
|
|
91
|
+
watch(
|
|
92
|
+
() => props.widget,
|
|
93
|
+
(w) => {
|
|
94
|
+
if (!w) return
|
|
95
|
+
draftSource.value = w.dataSource?.type ?? DATA_SOURCE_TYPE.GLOBAL
|
|
96
|
+
draftProjectId.value = w.dataSource?.projectId
|
|
97
|
+
draftTitle.value = w.title ?? ''
|
|
98
|
+
},
|
|
99
|
+
{ immediate: true },
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const projectItems = computed(() =>
|
|
103
|
+
visibleProjects.value
|
|
104
|
+
.filter((p: any) => !p.is_system)
|
|
105
|
+
.map((p: any) => ({ label: p.name, value: p.id })),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const hasDataSource = computed(() => {
|
|
109
|
+
if (!props.widget) return false
|
|
110
|
+
return WIDGET_REGISTRY[props.widget.type].hasDataSource
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const save = () => {
|
|
114
|
+
const dataSource: IWidgetDataSource = draftSource.value === DATA_SOURCE_TYPE.PROJECT
|
|
115
|
+
? { type: DATA_SOURCE_TYPE.PROJECT, projectId: draftProjectId.value }
|
|
116
|
+
: { type: DATA_SOURCE_TYPE.GLOBAL }
|
|
117
|
+
emit('save', { dataSource, title: draftTitle.value || undefined })
|
|
118
|
+
emit('update:open', false)
|
|
119
|
+
}
|
|
120
|
+
</script>
|