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.
Files changed (98) hide show
  1. package/package.json +1 -1
  2. package/src/assets/locales/en.json +16 -1
  3. package/src/components/common/FormatImportModal.vue +272 -0
  4. package/src/components/common/FormatSnippet.vue +52 -0
  5. package/src/components/{ScanModal.vue → common/ScanModal.vue} +26 -34
  6. package/src/components/{dashboard → common}/WidgetConfigModal.vue +64 -81
  7. package/src/components/{dashboard → common}/WidgetPicker.vue +44 -50
  8. package/src/components/{dashboard → common}/widgets/ActivityWidget.vue +57 -75
  9. package/src/components/common/widgets/CustomIframeWidget.vue +59 -0
  10. package/src/components/common/widgets/LanguagesCoverageWidget.vue +162 -0
  11. package/src/components/{dashboard → common}/widgets/ProjectsWidget.vue +42 -43
  12. package/src/components/common/widgets/ReviewWidget.vue +133 -0
  13. package/src/components/common/widgets/StatWidget.vue +138 -0
  14. package/src/components/dashboard/WidgetGrid.vue +100 -94
  15. package/src/components/{GitRepoManager.vue → project/GitRepoManager.vue} +13 -17
  16. package/src/components/{LanguagePicker.vue → project/LanguagePicker.vue} +19 -23
  17. package/src/components/{LinkedKeyPicker.vue → project/LinkedKeyPicker.vue} +18 -26
  18. package/src/components/{PathPicker.vue → project/PathPicker.vue} +23 -49
  19. package/src/components/{PluralEditor.vue → project/PluralEditor.vue} +20 -22
  20. package/src/components/{TranslationHistoryModal.vue → project/TranslationHistoryModal.vue} +25 -19
  21. package/src/components/{TranslationRow.vue → project/TranslationRow.vue} +76 -102
  22. package/src/components/{dashboard/ProjectWidgetGrid.vue → project/WidgetGrid.vue} +99 -94
  23. package/src/composables/useAdmin.ts +127 -0
  24. package/src/composables/useAuth.ts +9 -1
  25. package/src/composables/useDashboard.ts +13 -12
  26. package/src/composables/useFormats.ts +79 -58
  27. package/src/composables/useFs.ts +24 -0
  28. package/src/composables/useKeys.ts +51 -12
  29. package/src/composables/useLanguages.ts +19 -17
  30. package/src/composables/useOnboarding.ts +45 -0
  31. package/src/composables/useProfile.ts +3 -3
  32. package/src/composables/useProject.ts +31 -5
  33. package/src/composables/useProjectDashboard.ts +15 -16
  34. package/src/composables/useReview.ts +13 -9
  35. package/src/composables/useSettings.ts +7 -3
  36. package/src/composables/useT.ts +2 -1
  37. package/src/composables/useUsers.ts +10 -5
  38. package/src/composables/useWidgetData.ts +10 -8
  39. package/src/composables/useWidgetRegistry.ts +4 -3
  40. package/src/consts/dashboard.const.ts +50 -49
  41. package/src/enums/dashboard.enum.ts +23 -0
  42. package/src/interfaces/dashboard.interface.ts +60 -2
  43. package/src/interfaces/key.interface.ts +28 -0
  44. package/src/interfaces/languages.interface.ts +14 -0
  45. package/src/interfaces/project.interface.ts +24 -0
  46. package/src/interfaces/scan.interface.ts +15 -0
  47. package/src/interfaces/scanner.interface.ts +7 -0
  48. package/src/interfaces/translation.interface.ts +16 -0
  49. package/src/layouts/auth.vue +8 -5
  50. package/src/layouts/default.vue +321 -242
  51. package/src/middleware/auth.global.ts +4 -3
  52. package/src/pages/admin/customization.vue +255 -264
  53. package/src/pages/admin/logs.vue +46 -77
  54. package/src/pages/admin/security.vue +60 -65
  55. package/src/pages/admin/smtp.vue +141 -158
  56. package/src/pages/forgot-password.vue +18 -20
  57. package/src/pages/index.vue +1 -1
  58. package/src/pages/login.vue +55 -53
  59. package/src/pages/onboarding.vue +421 -408
  60. package/src/pages/projects/[id]/formats/datetime.vue +189 -172
  61. package/src/pages/projects/[id]/formats/modifiers.vue +144 -119
  62. package/src/pages/projects/[id]/formats/number.vue +195 -168
  63. package/src/pages/projects/[id]/index.vue +1 -1
  64. package/src/pages/projects/[id]/languages.vue +378 -335
  65. package/src/pages/projects/[id]/review.vue +76 -68
  66. package/src/pages/projects/[id]/settings.vue +204 -136
  67. package/src/pages/projects/[id]/translations/[keyId].vue +448 -405
  68. package/src/pages/projects/[id]/translations/index.vue +202 -164
  69. package/src/pages/projects/[id]/users.vue +301 -266
  70. package/src/pages/projects/index.vue +122 -126
  71. package/src/pages/reset-password.vue +68 -72
  72. package/src/pages/users/[id]/profile.vue +292 -264
  73. package/src/pages/users/index.vue +63 -63
  74. package/src/server/api/formats/detect.post.ts +74 -0
  75. package/src/server/api/formats/import-from-config.post.ts +68 -0
  76. package/src/server/api/languages/[code].put.ts +25 -0
  77. package/src/server/api/projects/[id].put.ts +2 -1
  78. package/src/server/api/stats/global.get.ts +34 -23
  79. package/src/server/utils/project-config.util.ts +1 -1
  80. package/src/server/utils/scanner.util.ts +216 -1
  81. package/src/services/admin.service.ts +87 -0
  82. package/src/services/auth.service.ts +12 -0
  83. package/src/services/dashboard.service.ts +22 -0
  84. package/src/services/formats.service.ts +76 -0
  85. package/src/services/fs.service.ts +17 -0
  86. package/src/services/language.service.ts +4 -0
  87. package/src/services/locale.service.ts +9 -0
  88. package/src/services/onboarding.service.ts +60 -0
  89. package/src/services/project.service.ts +15 -0
  90. package/src/services/scan.service.ts +11 -0
  91. package/src/services/settings.service.ts +4 -0
  92. package/src/services/stats.service.ts +4 -0
  93. package/src/types/dashboard.type.ts +5 -11
  94. package/src/components/dashboard/widgets/CustomIframeWidget.vue +0 -56
  95. package/src/components/dashboard/widgets/LanguagesCoverageWidget.vue +0 -144
  96. package/src/components/dashboard/widgets/ReviewWidget.vue +0 -199
  97. package/src/components/dashboard/widgets/StatWidget.vue +0 -159
  98. package/src/server/api/languages/[id].put.ts +0 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18n-dashboard",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "description": "A web dashboard to manage vue-i18n translation keys with database persistence",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- <UModal
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
- <UIcon
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
- <UFormField :label="t('scan.local_path_label', 'Project root folder')">
37
- <PathPicker
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
- </UFormField>
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
- <GitRepoManager v-model="gitRepo" />
52
+ <project-git-repo-manager v-model="gitRepo" />
53
53
  <label class="flex items-center gap-2 cursor-pointer">
54
- <UToggle
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
- <UButton
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
- </UButton>
158
- <UButton
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
- </UButton>
165
+ </u-button>
166
166
  </div>
167
167
  </template>
168
- </UModal>
168
+ </u-modal>
169
169
  </template>
170
170
 
171
171
  <script setup lang="ts">
172
- import type { IGitRepo } from '../interfaces/project.interface'
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<{ done: [] }>()
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 function runScan() {
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 $fetch(`/api/projects/${props.projectId}`, {
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 $fetch('/api/scan', {
222
- method: 'POST',
223
- body: {
224
- project_id: props.projectId,
225
- mode: mode.value,
226
- root_path: mode.value === 'local' ? localPath.value : undefined,
227
- git_url: mode.value === 'git' ? gitRepo.value?.url : undefined,
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?.data?.message ?? t('common.error', 'Error')
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
- <UModal
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="flex-1 px-3 py-2 rounded-lg text-sm font-medium border transition-colors"
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
- @click="draftSource = 'global'"
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="flex-1 px-3 py-2 rounded-lg text-sm font-medium border transition-colors"
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
- @click="draftSource = 'project'"
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 === 'project'">
102
- <USelect
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
- <UInput
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
- <UButton
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
- </UButton>
131
- <UButton @click="save">
66
+ </u-button>
67
+ <u-button @click="save">
132
68
  {{ t('common.save', 'Save') }}
133
- </UButton>
69
+ </u-button>
134
70
  </div>
135
71
  </template>
136
- </UModal>
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>