@motor-cms/ui-admin 1.0.1-alpha.0
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/README.md +77 -0
- package/app/components/form/inputs/CategoryTreeInput.vue +154 -0
- package/app/components/form/inputs/CategoryTreePicker.vue +355 -0
- package/app/components/form/inputs/NestedDraggable.vue +217 -0
- package/app/components/form/inputs/QuicklinksInput.vue +186 -0
- package/app/lang/de/motor-admin/CLAUDE.md +21 -0
- package/app/lang/de/motor-admin/ai_system_prompts.json +12 -0
- package/app/lang/de/motor-admin/categories.json +12 -0
- package/app/lang/de/motor-admin/category_trees.json +14 -0
- package/app/lang/de/motor-admin/clients.json +26 -0
- package/app/lang/de/motor-admin/config_variables.json +14 -0
- package/app/lang/de/motor-admin/domains.json +19 -0
- package/app/lang/de/motor-admin/email_templates.json +38 -0
- package/app/lang/de/motor-admin/global.json +5 -0
- package/app/lang/de/motor-admin/languages.json +16 -0
- package/app/lang/de/motor-admin/permissions.json +14 -0
- package/app/lang/de/motor-admin/roles.json +15 -0
- package/app/lang/de/motor-admin/users.json +22 -0
- package/app/lang/en/motor-admin/CLAUDE.md +7 -0
- package/app/lang/en/motor-admin/ai_system_prompts.json +12 -0
- package/app/lang/en/motor-admin/categories.json +12 -0
- package/app/lang/en/motor-admin/category_trees.json +14 -0
- package/app/lang/en/motor-admin/clients.json +26 -0
- package/app/lang/en/motor-admin/config_variables.json +14 -0
- package/app/lang/en/motor-admin/domains.json +18 -0
- package/app/lang/en/motor-admin/email_templates.json +33 -0
- package/app/lang/en/motor-admin/global.json +5 -0
- package/app/lang/en/motor-admin/languages.json +16 -0
- package/app/lang/en/motor-admin/permissions.json +14 -0
- package/app/lang/en/motor-admin/roles.json +15 -0
- package/app/lang/en/motor-admin/users.json +22 -0
- package/app/pages/dashboard.vue +5 -0
- package/app/pages/index.vue +39 -0
- package/app/pages/login.vue +85 -0
- package/app/pages/motor-admin/ai-system-prompts/CLAUDE.md +7 -0
- package/app/pages/motor-admin/ai-system-prompts/[id]/edit.vue +48 -0
- package/app/pages/motor-admin/ai-system-prompts/create.vue +40 -0
- package/app/pages/motor-admin/ai-system-prompts/index.vue +68 -0
- package/app/pages/motor-admin/category-trees/CLAUDE.md +7 -0
- package/app/pages/motor-admin/category-trees/[id]/CLAUDE.md +7 -0
- package/app/pages/motor-admin/category-trees/[id]/categories/[categoryId]/edit.vue +73 -0
- package/app/pages/motor-admin/category-trees/[id]/categories/create.vue +64 -0
- package/app/pages/motor-admin/category-trees/[id]/edit.vue +45 -0
- package/app/pages/motor-admin/category-trees/[id]/index.vue +81 -0
- package/app/pages/motor-admin/category-trees/create.vue +37 -0
- package/app/pages/motor-admin/category-trees/index.vue +54 -0
- package/app/pages/motor-admin/clients/CLAUDE.md +11 -0
- package/app/pages/motor-admin/clients/[id]/CLAUDE.md +11 -0
- package/app/pages/motor-admin/clients/[id]/edit.vue +45 -0
- package/app/pages/motor-admin/clients/create.vue +37 -0
- package/app/pages/motor-admin/clients/index.vue +46 -0
- package/app/pages/motor-admin/config-variables/CLAUDE.md +11 -0
- package/app/pages/motor-admin/config-variables/[id]/edit.vue +44 -0
- package/app/pages/motor-admin/config-variables/create.vue +36 -0
- package/app/pages/motor-admin/config-variables/index.vue +66 -0
- package/app/pages/motor-admin/domains/CLAUDE.md +11 -0
- package/app/pages/motor-admin/domains/[id]/edit.vue +54 -0
- package/app/pages/motor-admin/domains/create.vue +46 -0
- package/app/pages/motor-admin/domains/index.vue +98 -0
- package/app/pages/motor-admin/email-templates/CLAUDE.md +12 -0
- package/app/pages/motor-admin/email-templates/[id]/CLAUDE.md +7 -0
- package/app/pages/motor-admin/email-templates/[id]/edit.vue +56 -0
- package/app/pages/motor-admin/email-templates/create.vue +48 -0
- package/app/pages/motor-admin/email-templates/index.vue +67 -0
- package/app/pages/motor-admin/index.vue +12 -0
- package/app/pages/motor-admin/languages/CLAUDE.md +7 -0
- package/app/pages/motor-admin/languages/[id]/edit.vue +44 -0
- package/app/pages/motor-admin/languages/create.vue +36 -0
- package/app/pages/motor-admin/languages/index.vue +44 -0
- package/app/pages/motor-admin/permission-groups/CLAUDE.md +14 -0
- package/app/pages/motor-admin/permission-groups/[id]/CLAUDE.md +11 -0
- package/app/pages/motor-admin/permission-groups/[id]/edit.vue +49 -0
- package/app/pages/motor-admin/permission-groups/create.vue +41 -0
- package/app/pages/motor-admin/permission-groups/index.vue +43 -0
- package/app/pages/motor-admin/roles/CLAUDE.md +7 -0
- package/app/pages/motor-admin/roles/[id]/edit.vue +47 -0
- package/app/pages/motor-admin/roles/create.vue +40 -0
- package/app/pages/motor-admin/roles/index.vue +45 -0
- package/app/pages/motor-admin/theme-preview/CLAUDE.md +7 -0
- package/app/pages/motor-admin/theme-preview/index.vue +4801 -0
- package/app/pages/motor-admin/theme-preview/themes/CLAUDE.md +11 -0
- package/app/pages/motor-admin/theme-preview/themes/asymmetric-brutalist.md +381 -0
- package/app/pages/motor-admin/theme-preview/themes/bold-modern.md +231 -0
- package/app/pages/motor-admin/theme-preview/themes/geometric-minimal.md +778 -0
- package/app/pages/motor-admin/theme-preview/themes/gradient-flow.md +1057 -0
- package/app/pages/motor-admin/theme-preview/themes/liquid-glass.md +823 -0
- package/app/pages/motor-admin/theme-preview/themes/neon-amber.md +1223 -0
- package/app/pages/motor-admin/theme-preview/themes/neon-terminal.md +779 -0
- package/app/pages/motor-admin/theme-preview/themes/neon-violet.md +1134 -0
- package/app/pages/motor-admin/theme-preview/themes/professional-clean.md +232 -0
- package/app/pages/motor-admin/theme-preview/themes/refined-brutalist.md +462 -0
- package/app/pages/motor-admin/theme-preview/themes/wild-card.md +263 -0
- package/app/pages/motor-admin/users/CLAUDE.md +17 -0
- package/app/pages/motor-admin/users/[id]/CLAUDE.md +11 -0
- package/app/pages/motor-admin/users/[id]/edit.vue +83 -0
- package/app/pages/motor-admin/users/create.vue +40 -0
- package/app/pages/motor-admin/users/index.vue +66 -0
- package/app/pages/profile.vue +363 -0
- package/app/pages/search.vue +91 -0
- package/app/types/generated/form-meta.ts +258 -0
- package/app/types/generated/grid-meta.ts +172 -0
- package/nuxt.config.ts +1 -0
- package/package.json +26 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { FormSubmitEvent } from '@nuxt/ui'
|
|
4
|
+
import type { User } from '@motor-cms/ui-core/app/types/auth'
|
|
5
|
+
|
|
6
|
+
definePageMeta({
|
|
7
|
+
layout: 'default',
|
|
8
|
+
permission: 'profile.read'
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
const { user, refreshIdentity } = useSanctumAuth<User>()
|
|
13
|
+
const { updateProfile } = useProfileApi()
|
|
14
|
+
const { success, error: notifyError, info } = useNotify()
|
|
15
|
+
|
|
16
|
+
// Test function to demonstrate error notifications
|
|
17
|
+
function testError() {
|
|
18
|
+
try {
|
|
19
|
+
throw new Error('This is a test error to demonstrate error notifications with stack traces')
|
|
20
|
+
} catch (err: unknown) {
|
|
21
|
+
const error = err as Error
|
|
22
|
+
notifyError(t('motor-core.profile.toast_test_error_title'), error.message, {
|
|
23
|
+
message: error.message,
|
|
24
|
+
stack: error.stack,
|
|
25
|
+
status: 500,
|
|
26
|
+
url: '/api/test',
|
|
27
|
+
responseBody: {
|
|
28
|
+
error: 'Internal Server Error',
|
|
29
|
+
details: 'This is mock response data for testing'
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Helper to access user data
|
|
36
|
+
const userData = computed(() => user.value?.data)
|
|
37
|
+
|
|
38
|
+
// ============================================
|
|
39
|
+
// Profile Form
|
|
40
|
+
// ============================================
|
|
41
|
+
|
|
42
|
+
const profileSchema = z.object({
|
|
43
|
+
name: z.string().min(1, t('motor-core.profile.validation_name_required')),
|
|
44
|
+
email: z.string().email(t('motor-core.profile.validation_email_invalid')),
|
|
45
|
+
avatar: z.string().nullable().optional()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
type ProfileSchema = z.output<typeof profileSchema>
|
|
49
|
+
|
|
50
|
+
const profileState = reactive({
|
|
51
|
+
name: '',
|
|
52
|
+
email: ''
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Track original values for dirty checking
|
|
56
|
+
const originalProfileState = reactive({
|
|
57
|
+
name: '',
|
|
58
|
+
email: ''
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const avatarFile = ref<File | null>(null)
|
|
62
|
+
|
|
63
|
+
// Check if profile form has changes
|
|
64
|
+
const isProfileDirty = computed(() => {
|
|
65
|
+
return profileState.name !== originalProfileState.name
|
|
66
|
+
|| profileState.email !== originalProfileState.email
|
|
67
|
+
|| avatarFile.value !== null
|
|
68
|
+
})
|
|
69
|
+
const avatarPreviewUrl = computed(() => {
|
|
70
|
+
if (avatarFile.value) {
|
|
71
|
+
return URL.createObjectURL(avatarFile.value)
|
|
72
|
+
}
|
|
73
|
+
const avatar = userData.value?.avatar
|
|
74
|
+
if (!avatar) return null
|
|
75
|
+
// Handle conversions as object (e.g., { thumb: { url: '...' } }) or array
|
|
76
|
+
const conversions = avatar.conversions
|
|
77
|
+
if (conversions && typeof conversions === 'object' && !Array.isArray(conversions)) {
|
|
78
|
+
return (conversions as Record<string, { url: string }>).thumb?.url ?? avatar.url
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(conversions)) {
|
|
81
|
+
const thumbConversion = conversions.find(c => c.name === 'thumb')
|
|
82
|
+
return thumbConversion?.url ?? avatar.url
|
|
83
|
+
}
|
|
84
|
+
return avatar.url
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const profileLoading = ref(false)
|
|
88
|
+
|
|
89
|
+
// Initialize form with user data
|
|
90
|
+
watchEffect(() => {
|
|
91
|
+
if (userData.value) {
|
|
92
|
+
profileState.name = userData.value.name || ''
|
|
93
|
+
profileState.email = userData.value.email || ''
|
|
94
|
+
// Store original values for dirty checking
|
|
95
|
+
originalProfileState.name = userData.value.name || ''
|
|
96
|
+
originalProfileState.email = userData.value.email || ''
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
async function onProfileSubmit(event: FormSubmitEvent<ProfileSchema>) {
|
|
101
|
+
// Skip if no changes
|
|
102
|
+
if (!isProfileDirty.value) {
|
|
103
|
+
info(t('motor-core.profile.toast_no_changes_title'), t('motor-core.profile.toast_no_changes_message'))
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
profileLoading.value = true
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
let avatarPayload: { name: string, dataUrl: string } | null = null
|
|
111
|
+
|
|
112
|
+
if (avatarFile.value) {
|
|
113
|
+
const fullDataUrl = await fileToBase64(avatarFile.value)
|
|
114
|
+
// Extract just the base64 part, removing "data:image/...;base64," prefix
|
|
115
|
+
const base64Data = fullDataUrl.split(',')[1] || fullDataUrl
|
|
116
|
+
avatarPayload = {
|
|
117
|
+
name: avatarFile.value.name,
|
|
118
|
+
dataUrl: base64Data
|
|
119
|
+
}
|
|
120
|
+
} else if (!avatarPreviewUrl.value) {
|
|
121
|
+
// Avatar was removed
|
|
122
|
+
avatarPayload = null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = await updateProfile({
|
|
126
|
+
name: event.data.name,
|
|
127
|
+
email: event.data.email,
|
|
128
|
+
avatar: avatarPayload
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Repopulate form from response and update original state
|
|
132
|
+
profileState.name = response.data.name
|
|
133
|
+
profileState.email = response.data.email
|
|
134
|
+
originalProfileState.name = response.data.name
|
|
135
|
+
originalProfileState.email = response.data.email
|
|
136
|
+
avatarFile.value = null
|
|
137
|
+
|
|
138
|
+
// Refresh global auth state
|
|
139
|
+
await refreshIdentity()
|
|
140
|
+
|
|
141
|
+
success(t('motor-core.profile.toast_profile_updated_title'), t('motor-core.profile.toast_profile_updated_message'))
|
|
142
|
+
} catch (err: unknown) {
|
|
143
|
+
const message = err instanceof Error ? err.message : t('motor-core.profile.toast_profile_error')
|
|
144
|
+
notifyError(t('motor-core.profile.toast_profile_error'), message, {
|
|
145
|
+
message,
|
|
146
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
147
|
+
url: '/api/profile'
|
|
148
|
+
})
|
|
149
|
+
} finally {
|
|
150
|
+
profileLoading.value = false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================
|
|
155
|
+
// Password Form
|
|
156
|
+
// ============================================
|
|
157
|
+
|
|
158
|
+
const passwordSchema = z.object({
|
|
159
|
+
password: z.string().min(8, t('motor-core.profile.validation_password_min')),
|
|
160
|
+
password_confirmation: z.string()
|
|
161
|
+
}).refine(data => data.password === data.password_confirmation, {
|
|
162
|
+
message: t('motor-core.profile.validation_passwords_mismatch'),
|
|
163
|
+
path: ['password_confirmation']
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
type PasswordSchema = z.output<typeof passwordSchema>
|
|
167
|
+
|
|
168
|
+
const passwordState = reactive({
|
|
169
|
+
password: '',
|
|
170
|
+
password_confirmation: ''
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const passwordLoading = ref(false)
|
|
174
|
+
|
|
175
|
+
async function onPasswordSubmit(event: FormSubmitEvent<PasswordSchema>) {
|
|
176
|
+
passwordLoading.value = true
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await updateProfile({
|
|
180
|
+
name: profileState.name,
|
|
181
|
+
email: profileState.email,
|
|
182
|
+
password: event.data.password
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Clear password fields
|
|
186
|
+
passwordState.password = ''
|
|
187
|
+
passwordState.password_confirmation = ''
|
|
188
|
+
|
|
189
|
+
success(t('motor-core.profile.toast_password_updated_title'), t('motor-core.profile.toast_password_updated_message'))
|
|
190
|
+
} catch (err: unknown) {
|
|
191
|
+
const message = err instanceof Error ? err.message : t('motor-core.profile.toast_password_error')
|
|
192
|
+
notifyError(t('motor-core.profile.toast_password_error'), message, {
|
|
193
|
+
message,
|
|
194
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
195
|
+
url: '/api/profile'
|
|
196
|
+
})
|
|
197
|
+
} finally {
|
|
198
|
+
passwordLoading.value = false
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
</script>
|
|
202
|
+
|
|
203
|
+
<template>
|
|
204
|
+
<UDashboardPanel id="profile">
|
|
205
|
+
<template #header>
|
|
206
|
+
<UDashboardNavbar :title="t('motor-core.profile.title')">
|
|
207
|
+
<template #leading>
|
|
208
|
+
<UDashboardSidebarCollapse />
|
|
209
|
+
</template>
|
|
210
|
+
</UDashboardNavbar>
|
|
211
|
+
</template>
|
|
212
|
+
|
|
213
|
+
<template #body>
|
|
214
|
+
<div class="flex flex-col gap-6">
|
|
215
|
+
<!-- Profile Information Card -->
|
|
216
|
+
<UPageCard
|
|
217
|
+
:title="t('motor-core.profile.profile_info_title')"
|
|
218
|
+
:description="t('motor-core.profile.profile_info_description')"
|
|
219
|
+
>
|
|
220
|
+
<UForm
|
|
221
|
+
:schema="profileSchema"
|
|
222
|
+
:state="profileState"
|
|
223
|
+
class="space-y-4"
|
|
224
|
+
@submit="onProfileSubmit"
|
|
225
|
+
>
|
|
226
|
+
<!-- Avatar Row -->
|
|
227
|
+
<div class="grid grid-cols-[150px_1fr] gap-6 items-start">
|
|
228
|
+
<label class="text-sm font-medium text-default pt-2">{{ t('motor-core.profile.avatar_label') }}</label>
|
|
229
|
+
<FileUpload
|
|
230
|
+
v-model="avatarFile"
|
|
231
|
+
:preview-url="avatarPreviewUrl"
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<!-- Name Row -->
|
|
236
|
+
<div class="grid grid-cols-[150px_1fr] gap-6 items-start">
|
|
237
|
+
<label class="text-sm font-medium text-default pt-2">
|
|
238
|
+
{{ t('motor-core.profile.name_label') }} <span class="text-error">*</span>
|
|
239
|
+
</label>
|
|
240
|
+
<UFormField
|
|
241
|
+
name="name"
|
|
242
|
+
:ui="{ wrapper: 'w-full' }"
|
|
243
|
+
>
|
|
244
|
+
<UInput
|
|
245
|
+
v-model="profileState.name"
|
|
246
|
+
:placeholder="t('motor-core.profile.name_placeholder')"
|
|
247
|
+
icon="i-lucide-user"
|
|
248
|
+
class="w-full"
|
|
249
|
+
/>
|
|
250
|
+
</UFormField>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<!-- Email Row -->
|
|
254
|
+
<div class="grid grid-cols-[150px_1fr] gap-6 items-start">
|
|
255
|
+
<label class="text-sm font-medium text-default pt-2">
|
|
256
|
+
{{ t('motor-core.profile.email_label') }} <span class="text-error">*</span>
|
|
257
|
+
</label>
|
|
258
|
+
<UFormField
|
|
259
|
+
name="email"
|
|
260
|
+
:ui="{ wrapper: 'w-full' }"
|
|
261
|
+
>
|
|
262
|
+
<UInput
|
|
263
|
+
v-model="profileState.email"
|
|
264
|
+
type="email"
|
|
265
|
+
:placeholder="t('motor-core.profile.email_placeholder')"
|
|
266
|
+
icon="i-lucide-mail"
|
|
267
|
+
class="w-full"
|
|
268
|
+
/>
|
|
269
|
+
</UFormField>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<!-- Submit Row -->
|
|
273
|
+
<div class="grid grid-cols-[150px_1fr] gap-6">
|
|
274
|
+
<div />
|
|
275
|
+
<div class="flex justify-end gap-2">
|
|
276
|
+
<UButton
|
|
277
|
+
color="error"
|
|
278
|
+
variant="outline"
|
|
279
|
+
icon="i-lucide-bug"
|
|
280
|
+
@click="testError"
|
|
281
|
+
>
|
|
282
|
+
{{ t('motor-core.profile.test_error') }}
|
|
283
|
+
</UButton>
|
|
284
|
+
<UButton
|
|
285
|
+
type="submit"
|
|
286
|
+
:loading="profileLoading"
|
|
287
|
+
icon="i-lucide-save"
|
|
288
|
+
>
|
|
289
|
+
{{ t('motor-core.profile.save_profile') }}
|
|
290
|
+
</UButton>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</UForm>
|
|
294
|
+
</UPageCard>
|
|
295
|
+
|
|
296
|
+
<!-- Change Password Card -->
|
|
297
|
+
<UPageCard
|
|
298
|
+
:title="t('motor-core.profile.change_password_title')"
|
|
299
|
+
:description="t('motor-core.profile.change_password_description')"
|
|
300
|
+
>
|
|
301
|
+
<UForm
|
|
302
|
+
:schema="passwordSchema"
|
|
303
|
+
:state="passwordState"
|
|
304
|
+
class="space-y-4"
|
|
305
|
+
@submit="onPasswordSubmit"
|
|
306
|
+
>
|
|
307
|
+
<!-- New Password Row -->
|
|
308
|
+
<div class="grid grid-cols-[150px_1fr] gap-6 items-start">
|
|
309
|
+
<label class="text-sm font-medium text-default pt-2">
|
|
310
|
+
{{ t('motor-core.profile.new_password_label') }} <span class="text-error">*</span>
|
|
311
|
+
</label>
|
|
312
|
+
<UFormField
|
|
313
|
+
name="password"
|
|
314
|
+
:ui="{ wrapper: 'w-full' }"
|
|
315
|
+
>
|
|
316
|
+
<UInput
|
|
317
|
+
v-model="passwordState.password"
|
|
318
|
+
type="password"
|
|
319
|
+
:placeholder="t('motor-core.profile.new_password_placeholder')"
|
|
320
|
+
icon="i-lucide-lock"
|
|
321
|
+
class="w-full"
|
|
322
|
+
/>
|
|
323
|
+
</UFormField>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<!-- Confirm Password Row -->
|
|
327
|
+
<div class="grid grid-cols-[150px_1fr] gap-6 items-start">
|
|
328
|
+
<label class="text-sm font-medium text-default pt-2">
|
|
329
|
+
{{ t('motor-core.profile.confirm_password_label') }} <span class="text-error">*</span>
|
|
330
|
+
</label>
|
|
331
|
+
<UFormField
|
|
332
|
+
name="password_confirmation"
|
|
333
|
+
:ui="{ wrapper: 'w-full' }"
|
|
334
|
+
>
|
|
335
|
+
<UInput
|
|
336
|
+
v-model="passwordState.password_confirmation"
|
|
337
|
+
type="password"
|
|
338
|
+
:placeholder="t('motor-core.profile.confirm_password_placeholder')"
|
|
339
|
+
icon="i-lucide-lock"
|
|
340
|
+
class="w-full"
|
|
341
|
+
/>
|
|
342
|
+
</UFormField>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<!-- Submit Row -->
|
|
346
|
+
<div class="grid grid-cols-[150px_1fr] gap-6">
|
|
347
|
+
<div />
|
|
348
|
+
<div class="flex justify-end">
|
|
349
|
+
<UButton
|
|
350
|
+
type="submit"
|
|
351
|
+
:loading="passwordLoading"
|
|
352
|
+
icon="i-lucide-key"
|
|
353
|
+
>
|
|
354
|
+
{{ t('motor-core.profile.update_password') }}
|
|
355
|
+
</UButton>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</UForm>
|
|
359
|
+
</UPageCard>
|
|
360
|
+
</div>
|
|
361
|
+
</template>
|
|
362
|
+
</UDashboardPanel>
|
|
363
|
+
</template>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!-- app/pages/search.vue -->
|
|
2
|
+
<script setup lang="ts">
|
|
3
|
+
import type { SearchGridRow } from '@motor-cms/ui-core/app/types/search'
|
|
4
|
+
import type { ColumnDef } from '@motor-cms/ui-core/app/types/grid'
|
|
5
|
+
|
|
6
|
+
const { t } = useI18n()
|
|
7
|
+
|
|
8
|
+
const ALL_MODULES = '_all'
|
|
9
|
+
const moduleFilter = ref(ALL_MODULES)
|
|
10
|
+
|
|
11
|
+
const moduleFacets = useModuleFacets()
|
|
12
|
+
const moduleOptions = computed(() => {
|
|
13
|
+
const options = [{ label: t('motor-core.search.filter_all_modules'), value: ALL_MODULES }]
|
|
14
|
+
for (const [key, count] of Object.entries(moduleFacets.value)) {
|
|
15
|
+
options.push({ label: `${resolveModuleLabel(key, t)} (${count})`, value: key })
|
|
16
|
+
}
|
|
17
|
+
return options
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const activeModule = computed(() => moduleFilter.value === ALL_MODULES ? undefined : moduleFilter.value)
|
|
21
|
+
const fetchResults = computed(() => fetchSearchGrid(t, activeModule.value))
|
|
22
|
+
|
|
23
|
+
const columns: ColumnDef<SearchGridRow>[] = [
|
|
24
|
+
{
|
|
25
|
+
key: 'module',
|
|
26
|
+
label: t('motor-core.search.column_module'),
|
|
27
|
+
renderer: 'badge',
|
|
28
|
+
width: 'w-[12%]'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'index_label',
|
|
32
|
+
label: t('motor-core.search.column_type'),
|
|
33
|
+
renderer: 'badge',
|
|
34
|
+
rendererProps: { color: 'neutral' },
|
|
35
|
+
width: 'w-[12%]'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'title',
|
|
39
|
+
label: t('motor-core.search.column_title'),
|
|
40
|
+
width: 'w-[30%]'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: 'excerpt',
|
|
44
|
+
label: t('motor-core.search.column_excerpt')
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<GridPage
|
|
51
|
+
:title="t('motor-core.search.title')"
|
|
52
|
+
:subtitle="t('motor-core.search.subtitle')"
|
|
53
|
+
>
|
|
54
|
+
<GridBase
|
|
55
|
+
id="search-grid"
|
|
56
|
+
:key="moduleFilter"
|
|
57
|
+
:fetch="fetchResults"
|
|
58
|
+
:columns="columns"
|
|
59
|
+
:searchable="true"
|
|
60
|
+
:row-click-to="(row: any) => row.to"
|
|
61
|
+
:disable-default-actions="true"
|
|
62
|
+
>
|
|
63
|
+
<template #toolbar-extra>
|
|
64
|
+
<USelectMenu
|
|
65
|
+
v-model="moduleFilter"
|
|
66
|
+
:items="moduleOptions"
|
|
67
|
+
value-key="value"
|
|
68
|
+
label-key="label"
|
|
69
|
+
class="w-48"
|
|
70
|
+
:clear="moduleFilter !== ALL_MODULES"
|
|
71
|
+
@clear="moduleFilter = ALL_MODULES"
|
|
72
|
+
/>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<template #empty>
|
|
76
|
+
<div class="flex flex-col items-center justify-center py-12 gap-3">
|
|
77
|
+
<UIcon
|
|
78
|
+
name="i-lucide-search"
|
|
79
|
+
class="size-8 text-muted"
|
|
80
|
+
/>
|
|
81
|
+
<p class="text-sm font-medium text-muted">
|
|
82
|
+
{{ t('motor-core.search.min_chars') }}
|
|
83
|
+
</p>
|
|
84
|
+
<p class="text-xs text-dimmed">
|
|
85
|
+
{{ t('motor-core.search.no_results_hint') }}
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
</GridBase>
|
|
90
|
+
</GridPage>
|
|
91
|
+
</template>
|