@motor-cms/ui-media 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.
@@ -0,0 +1,7 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
@@ -0,0 +1,24 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ ### Feb 18, 2026
7
+
8
+ | ID | Time | T | Title | Read |
9
+ |----|------|---|-------|------|
10
+ | #27256 | 4:59 PM | 🔄 | Major codebase cleanup and component consolidation | ~433 |
11
+ | #26878 | 12:18 PM | 🟣 | Motor-Media Files Module Implementation Complete | ~429 |
12
+
13
+ ### Feb 27, 2026
14
+
15
+ | ID | Time | T | Title | Read |
16
+ |----|------|---|-------|------|
17
+ | #30755 | 1:37 PM | 🔵 | Comprehensive Media Management System in Motor Admin Frontend | ~594 |
18
+
19
+ ### Mar 7, 2026
20
+
21
+ | ID | Time | T | Title | Read |
22
+ |----|------|---|-------|------|
23
+ | #34014 | 10:53 AM | 🔴 | Fixed permission naming in motor-media pages to align with backend API | ~289 |
24
+ </claude-mem-context>
@@ -0,0 +1,236 @@
1
+ <script setup lang="ts">
2
+ import { fileFormMeta } from '../../../../types/generated/form-meta'
3
+ import { fileEditFormConfig, fileSelectOptionConfigs } from '@motor-cms/ui-core/app/types/config/file'
4
+
5
+ definePageMeta({ layout: 'default', permission: 'files.write' })
6
+
7
+ const route = useRoute()
8
+ const { t } = useI18n()
9
+ const client = useSanctumClient()
10
+ const { success, error: notifyError } = useNotify()
11
+ const router = useRouter()
12
+
13
+ const { fields, schema, groups, state, loading, fetching, fetchError, formRef, selectOptions, selectOptionsLoading, deleteRecord, deleting } = await useEntityForm({
14
+ apiEndpoint: '/api/v2/files',
15
+ routePrefix: '/motor-media/files',
16
+ translationPrefix: 'motor-media.files',
17
+ formMeta: fileFormMeta,
18
+ formConfig: fileEditFormConfig,
19
+ mode: 'edit',
20
+ id: route.params.id as string,
21
+ selectOptionConfigs: fileSelectOptionConfigs
22
+ })
23
+
24
+ const fileId = route.params.id as string
25
+ const replacementFile = ref<File | null>(null)
26
+
27
+ const usageModalOpen = ref(false)
28
+ const usageEndpoint = computed(() => `/api/v2/files/${fileId}/usage`)
29
+
30
+ // Reuse the record already fetched by useEntityForm (no duplicate request)
31
+ const { data: fileRecord } = useNuxtData<{ data: Record<string, unknown> }>(
32
+ `entity-form-/api/v2/files-${fileId}`
33
+ )
34
+
35
+ const currentFile = computed(() => {
36
+ const file = fileRecord.value?.data?.file as { url?: string, file_name?: string, mime_type?: string } | null
37
+ return file ?? null
38
+ })
39
+
40
+ const selectedCategories = ref<number[]>([])
41
+
42
+ watch(fileRecord, (res) => {
43
+ if (res?.data) {
44
+ const cats = res.data.categories as Array<{ id: number }> | null
45
+ if (cats) {
46
+ selectedCategories.value = cats.map(c => c.id)
47
+ }
48
+ }
49
+ }, { immediate: true })
50
+
51
+ function onFileChange(event: Event) {
52
+ const input = event.target as HTMLInputElement
53
+ if (input.files?.[0]) {
54
+ replacementFile.value = input.files[0]
55
+ }
56
+ }
57
+
58
+ async function submitFile(eventData: Record<string, unknown>): Promise<void> {
59
+ const body: Record<string, unknown> = { ...eventData, categories: selectedCategories.value }
60
+ if (replacementFile.value) {
61
+ body.file = await fileToDataUrl(replacementFile.value)
62
+ }
63
+ await client(`/api/v2/files/${fileId}`, { method: 'PATCH', body })
64
+ success(t('motor-media.files.edit_title'), t('motor-media.files.updated_success'))
65
+ }
66
+
67
+ function handleSubmitError(err: unknown) {
68
+ const fetchErr = err as { response?: { status?: number, _data?: { message?: string, errors?: Record<string, string[]> } } }
69
+ if (fetchErr.response?.status === 422 && fetchErr.response._data?.errors) {
70
+ const serverErrors = fetchErr.response._data.errors
71
+ const formErrors = Object.entries(serverErrors).map(([path, messages]) => ({
72
+ path,
73
+ message: messages[0] ?? ''
74
+ }))
75
+ formRef.value?.setErrors(formErrors)
76
+
77
+ const count = formErrors.length
78
+ const summary = count === 1
79
+ ? formErrors[0]!.message
80
+ : t('motor-core.global.validation_errors', { count })
81
+ notifyError(t('motor-core.global.validation_failed'), summary)
82
+
83
+ nextTick(() => {
84
+ const errorEl = document.querySelector('[class*="error"]')
85
+ errorEl?.scrollIntoView({ behavior: 'smooth', block: 'center' })
86
+ })
87
+ } else {
88
+ const message = err instanceof Error ? err.message : 'Failed to update file'
89
+ notifyError(t('motor-media.files.edit_title'), message)
90
+ }
91
+ }
92
+
93
+ async function onSubmit(event: { data: Record<string, unknown> }) {
94
+ loading.value = true
95
+ try {
96
+ await submitFile(event.data)
97
+ router.push('/motor-media/files')
98
+ } catch (err: unknown) {
99
+ handleSubmitError(err)
100
+ } finally {
101
+ loading.value = false
102
+ }
103
+ }
104
+
105
+ async function onSaveAndNew(event: { data: Record<string, unknown> }) {
106
+ loading.value = true
107
+ try {
108
+ await submitFile(event.data)
109
+ router.push('/motor-media/files/create')
110
+ } catch (err: unknown) {
111
+ handleSubmitError(err)
112
+ } finally {
113
+ loading.value = false
114
+ }
115
+ }
116
+
117
+ async function onSaveAndContinue(event: { data: Record<string, unknown> }) {
118
+ loading.value = true
119
+ try {
120
+ await submitFile(event.data)
121
+ nextTick(() => formRef.value?.captureSnapshot())
122
+ } catch (err: unknown) {
123
+ handleSubmitError(err)
124
+ } finally {
125
+ loading.value = false
126
+ }
127
+ }
128
+ </script>
129
+
130
+ <template>
131
+ <div>
132
+ <FormPage
133
+ :title="t('motor-media.files.edit_title')"
134
+ back-route="/motor-media/files"
135
+ :loading="fetching"
136
+ :error="fetchError"
137
+ >
138
+ <FormBase
139
+ ref="formRef"
140
+ v-model:state="state"
141
+ :fields="fields"
142
+ :schema="schema"
143
+ :groups="groups"
144
+ :select-options="selectOptions"
145
+ :select-options-loading="selectOptionsLoading"
146
+ :loading="loading"
147
+ :delete-record="deleteRecord"
148
+ :deleting="deleting"
149
+ cancel-route="/motor-media/files"
150
+ show-save-and-continue
151
+ show-save-and-new
152
+ @submit="onSubmit"
153
+ @save-and-continue="onSaveAndContinue"
154
+ @save-and-new="onSaveAndNew"
155
+ >
156
+ <template #after-fields>
157
+ <UPageCard :title="t('motor-media.files.group_categories')">
158
+ <FormInputsCategoryTreeInput
159
+ v-model="selectedCategories"
160
+ scope="media"
161
+ />
162
+ </UPageCard>
163
+
164
+ <UPageCard :title="t('motor-media.files.usage_title')">
165
+ <UButton
166
+ :label="t('motor-media.files.usage_title')"
167
+ icon="i-lucide-network"
168
+ variant="outline"
169
+ @click="usageModalOpen = true"
170
+ />
171
+ </UPageCard>
172
+
173
+ <UPageCard :title="t('motor-media.files.current_file')">
174
+ <div class="space-y-4">
175
+ <!-- Current file preview -->
176
+ <div
177
+ v-if="currentFile?.url"
178
+ class="flex items-center gap-4"
179
+ >
180
+ <img
181
+ v-if="currentFile.mime_type?.startsWith('image/')"
182
+ :src="currentFile.url"
183
+ :alt="currentFile.file_name ?? ''"
184
+ class="max-h-40 rounded object-contain"
185
+ >
186
+ <div
187
+ v-else
188
+ class="flex items-center gap-2"
189
+ >
190
+ <UIcon
191
+ name="i-lucide-file"
192
+ class="size-8 text-muted"
193
+ />
194
+ <span class="text-sm">{{ currentFile.file_name }}</span>
195
+ </div>
196
+ <UButton
197
+ icon="i-lucide-download"
198
+ variant="outline"
199
+ size="sm"
200
+ :to="currentFile.url"
201
+ target="_blank"
202
+ />
203
+ </div>
204
+
205
+ <!-- Replacement file input -->
206
+ <div class="space-y-2">
207
+ <UFormField
208
+ name="replacement_file"
209
+ :label="t('motor-media.files.replace_file')"
210
+ >
211
+ <UInput
212
+ type="file"
213
+ class="w-full"
214
+ @change="onFileChange"
215
+ />
216
+ </UFormField>
217
+ <p
218
+ v-if="replacementFile"
219
+ class="text-sm text-muted"
220
+ >
221
+ {{ replacementFile.name }} ({{ (replacementFile.size / 1024).toFixed(1) }} KB)
222
+ </p>
223
+ </div>
224
+ </div>
225
+ </UPageCard>
226
+ </template>
227
+ </FormBase>
228
+ </FormPage>
229
+
230
+ <EntityUsageModal
231
+ v-model:open="usageModalOpen"
232
+ :endpoint="usageEndpoint"
233
+ :title="t('motor-media.files.usage_title')"
234
+ />
235
+ </div>
236
+ </template>
@@ -0,0 +1,201 @@
1
+ <script setup lang="ts">
2
+ import type { PendingFile } from '../../../components/form/inputs/MultiFileUpload.vue'
3
+ import { fileFormMeta } from '../../../types/generated/form-meta'
4
+ import { fileCreateFormConfig, fileSelectOptionConfigs } from '@motor-cms/ui-core/app/types/config/file'
5
+
6
+ definePageMeta({ layout: 'default', permission: 'files.write' })
7
+
8
+ const { t } = useI18n()
9
+ const router = useRouter()
10
+ const client = useSanctumClient()
11
+ const { success, error: notifyError, warning } = useNotify()
12
+
13
+ const formRef = ref<{ captureSnapshot: () => void, isDirty: boolean, setErrors: (errors: Array<{ path: string, message: string }>) => void } | null>(null)
14
+
15
+ const { fields, schema, groups, state, selectOptions, selectOptionsLoading } = await useEntityForm({
16
+ apiEndpoint: '/api/v2/files',
17
+ routePrefix: '/motor-media/files',
18
+ translationPrefix: 'motor-media.files',
19
+ formMeta: fileFormMeta,
20
+ formConfig: fileCreateFormConfig,
21
+ mode: 'create',
22
+ selectOptionConfigs: fileSelectOptionConfigs
23
+ })
24
+
25
+ const selectedCategories = ref<number[]>([])
26
+
27
+ const pendingFiles = ref<PendingFile[]>([])
28
+ const uploading = ref(false)
29
+ const uploadProgress = ref('')
30
+
31
+ const defaults = computed(() => ({
32
+ description: (state.description as string) || '',
33
+ alt_text: (state.alt_text as string) || ''
34
+ }))
35
+
36
+ const submitted = ref(false)
37
+ const categoriesError = computed(() =>
38
+ submitted.value && selectedCategories.value.length === 0
39
+ ? t('motor-media.files.categories_required')
40
+ : undefined
41
+ )
42
+ const filesError = computed(() =>
43
+ submitted.value && pendingFiles.value.length === 0
44
+ ? t('motor-media.files.files_required')
45
+ : undefined
46
+ )
47
+
48
+ // Clear errors once requirements are met
49
+ watch(selectedCategories, () => {
50
+ if (selectedCategories.value.length > 0 && pendingFiles.value.length > 0) submitted.value = false
51
+ })
52
+ watch(pendingFiles, () => {
53
+ if (pendingFiles.value.length > 0 && selectedCategories.value.length > 0) submitted.value = false
54
+ })
55
+
56
+ function scrollToFirstError() {
57
+ nextTick(() => {
58
+ const errorEl = document.querySelector('.text-error')
59
+ if (errorEl) {
60
+ errorEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
61
+ }
62
+ })
63
+ }
64
+
65
+ function handleServerError(err: unknown) {
66
+ const fetchErr = err as { response?: { status?: number, _data?: { message?: string, errors?: Record<string, string[]> } } }
67
+ if (fetchErr.response?.status === 422 && fetchErr.response._data?.errors) {
68
+ const serverErrors = fetchErr.response._data.errors
69
+ const formErrors = Object.entries(serverErrors).map(([path, messages]) => ({
70
+ path,
71
+ message: messages[0] ?? ''
72
+ }))
73
+ formRef.value?.setErrors(formErrors)
74
+
75
+ const count = formErrors.length
76
+ const summary = count === 1
77
+ ? formErrors[0]!.message
78
+ : t('motor-core.global.validation_errors', { count })
79
+ notifyError(t('motor-core.global.validation_failed'), summary)
80
+ scrollToFirstError()
81
+ return true
82
+ }
83
+ return false
84
+ }
85
+
86
+ async function onSubmit(event: { data: Record<string, unknown> }) {
87
+ submitted.value = true
88
+ if (selectedCategories.value.length === 0 || pendingFiles.value.length === 0) {
89
+ const missingFields: string[] = []
90
+ if (selectedCategories.value.length === 0) missingFields.push(t('motor-media.files.group_categories'))
91
+ if (pendingFiles.value.length === 0) missingFields.push(t('motor-media.files.pending_files'))
92
+ warning(t('motor-core.global.validation_failed'), missingFields.join(', '))
93
+ scrollToFirstError()
94
+ return
95
+ }
96
+
97
+ uploading.value = true
98
+ const total = pendingFiles.value.length
99
+ const errors: string[] = []
100
+
101
+ try {
102
+ for (let i = 0; i < total; i++) {
103
+ const pending = pendingFiles.value[i]!
104
+ uploadProgress.value = t('motor-media.files.upload_progress', { current: i + 1, total })
105
+
106
+ const fileData = await fileToDataUrl(pending.file)
107
+ const body = {
108
+ ...event.data,
109
+ description: pending.description,
110
+ alt_text: pending.alt_text,
111
+ categories: selectedCategories.value,
112
+ files: [fileData]
113
+ }
114
+
115
+ try {
116
+ await client('/api/v2/files', {
117
+ method: 'POST',
118
+ body
119
+ })
120
+ } catch (err: unknown) {
121
+ if (!handleServerError(err)) {
122
+ const message = err instanceof Error ? err.message : `Failed to upload ${pending.file.name}`
123
+ errors.push(`${pending.file.name}: ${message}`)
124
+ }
125
+ }
126
+ }
127
+
128
+ if (errors.length === 0) {
129
+ success(t('motor-media.files.create_title'), t('motor-media.files.created_success'))
130
+ router.push('/motor-media/files')
131
+ } else if (errors.length < total) {
132
+ const uploaded = total - errors.length
133
+ success(t('motor-media.files.create_title'), `${uploaded}/${total} files uploaded`)
134
+ notifyError(t('motor-media.files.create_title'), errors.join('\n'))
135
+ router.push('/motor-media/files')
136
+ } else {
137
+ notifyError(t('motor-media.files.create_title'), errors.join('\n'))
138
+ }
139
+ } finally {
140
+ uploading.value = false
141
+ uploadProgress.value = ''
142
+ }
143
+ }
144
+ </script>
145
+
146
+ <template>
147
+ <FormPage
148
+ :title="t('motor-media.files.create_title')"
149
+ back-route="/motor-media/files"
150
+ >
151
+ <FormBase
152
+ ref="formRef"
153
+ v-model:state="state"
154
+ :fields="fields"
155
+ :schema="schema"
156
+ :groups="groups"
157
+ :select-options="selectOptions"
158
+ :select-options-loading="selectOptionsLoading"
159
+ :loading="uploading"
160
+ cancel-route="/motor-media/files"
161
+ @submit="onSubmit"
162
+ >
163
+ <template #after-fields>
164
+ <UPageCard :title="t('motor-media.files.group_categories')">
165
+ <FormInputsCategoryTreeInput
166
+ v-model="selectedCategories"
167
+ scope="media"
168
+ />
169
+ <p
170
+ v-if="categoriesError"
171
+ class="mt-2 text-sm text-error"
172
+ >
173
+ {{ categoriesError }}
174
+ </p>
175
+ </UPageCard>
176
+
177
+ <UPageCard :title="t('motor-media.files.pending_files')">
178
+ <p
179
+ v-if="uploadProgress"
180
+ class="mb-3 text-sm font-medium text-primary"
181
+ >
182
+ {{ uploadProgress }}
183
+ </p>
184
+ <FormInputsMultiFileUpload
185
+ :field="{ key: 'files', label: '', input: 'multi-file-upload', required: true }"
186
+ :model-value="(pendingFiles as any)"
187
+ :defaults="defaults"
188
+ accept="*/*"
189
+ @update:model-value="pendingFiles = $event"
190
+ />
191
+ <p
192
+ v-if="filesError"
193
+ class="mt-2 text-sm text-error"
194
+ >
195
+ {{ filesError }}
196
+ </p>
197
+ </UPageCard>
198
+ </template>
199
+ </FormBase>
200
+ </FormPage>
201
+ </template>
@@ -0,0 +1,164 @@
1
+ <script setup lang="ts">
2
+ import type { components } from '@motor-cms/ui-core/app/types/generated/api'
3
+ import type { BulkActionDef, RowActionDef } from '@motor-cms/ui-core/app/types/grid'
4
+ import { fileMeta } from '../../../types/generated/grid-meta'
5
+ import { fileGridConfig } from '@motor-cms/ui-core/app/types/config/file'
6
+
7
+ type File = components['schemas']['FileResource']
8
+
9
+ definePageMeta({ permission: 'files.read' })
10
+
11
+ const client = useSanctumClient()
12
+ const { t } = useI18n()
13
+
14
+ const usageModalOpen = ref(false)
15
+ const usageEndpoint = ref('')
16
+
17
+ function openUsageModal(fileId: number) {
18
+ usageEndpoint.value = `/api/v2/files/${fileId}/usage`
19
+ usageModalOpen.value = true
20
+ }
21
+
22
+ // View mode toggle (persisted in cookie for SSR safety)
23
+ const viewMode = useCookie<'gallery' | 'table'>('media-view-mode', { default: () => 'gallery' })
24
+
25
+ // Table columns (only needed for table view)
26
+ const columns = columnsFromMeta<File>(fileMeta, t, fileGridConfig)
27
+
28
+ columns.unshift({
29
+ key: 'file',
30
+ label: t('motor-media.files.file'),
31
+ renderer: 'image',
32
+ width: 'w-36'
33
+ })
34
+
35
+ const descIdx = columns.findIndex(c => c.key === 'description')
36
+ columns.splice(descIdx + 1, 0, {
37
+ key: 'file.file_name',
38
+ label: t('motor-media.files.file_name')
39
+ })
40
+
41
+ columns.push({
42
+ key: 'file.mime_type',
43
+ label: t('motor-media.files.mime_type')
44
+ })
45
+
46
+ columns.push(createdAtColumn(t, { key: 'file.created_at', sortKey: 'created_at' }))
47
+
48
+ const rowActions: RowActionDef<File>[] = [
49
+ {
50
+ key: 'usage',
51
+ label: t('motor-media.files.usage_title'),
52
+ icon: 'i-lucide-link',
53
+ silent: true,
54
+ handler: (row: File) => {
55
+ openUsageModal(row.id as number)
56
+ }
57
+ }
58
+ ]
59
+
60
+ const filters = [useClientFilter(), useCategoryFilter('media')]
61
+
62
+ const bulkActions: BulkActionDef[] = [
63
+ {
64
+ key: 'delete',
65
+ label: t('motor-core.grid.delete_selected'),
66
+ icon: 'i-lucide-trash-2',
67
+ color: 'error',
68
+ permission: 'files.delete',
69
+ confirm: count => t('motor-core.grid.confirm_delete', { count }),
70
+ handler: async (ids) => {
71
+ await Promise.all(
72
+ ids.map(id => client(`/api/v2/files/${id}`, { method: 'DELETE' }))
73
+ )
74
+ }
75
+ }
76
+ ]
77
+
78
+ const fetchFiles = useGridFetch<File>('/api/v2/files')
79
+ </script>
80
+
81
+ <template>
82
+ <div>
83
+ <GridPage
84
+ :title="t('motor-media.files.title')"
85
+ :subtitle="t('motor-media.files.subtitle')"
86
+ add-route="/motor-media/files/create"
87
+ :add-label="t('motor-media.files.add')"
88
+ write-permission="files.write"
89
+ >
90
+ <template #actions>
91
+ <div class="inline-flex rounded-lg ring-1 ring-[var(--ui-border)] overflow-hidden">
92
+ <button
93
+ class="flex items-center justify-center size-9 transition-colors"
94
+ :class="viewMode === 'gallery'
95
+ ? 'bg-[var(--ui-primary)] text-white'
96
+ : 'bg-[var(--ui-bg)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text)] hover:bg-[var(--ui-bg-elevated)]'"
97
+ :title="t('motor-media.files.view_gallery')"
98
+ @click="viewMode = 'gallery'"
99
+ >
100
+ <UIcon
101
+ name="i-lucide-layout-grid"
102
+ class="size-4"
103
+ />
104
+ </button>
105
+ <button
106
+ class="flex items-center justify-center size-9 transition-colors border-l border-[var(--ui-border)]"
107
+ :class="viewMode === 'table'
108
+ ? 'bg-[var(--ui-primary)] text-white'
109
+ : 'bg-[var(--ui-bg)] text-[var(--ui-text-muted)] hover:text-[var(--ui-text)] hover:bg-[var(--ui-bg-elevated)]'"
110
+ :title="t('motor-media.files.view_table')"
111
+ @click="viewMode = 'table'"
112
+ >
113
+ <UIcon
114
+ name="i-lucide-table-2"
115
+ class="size-4"
116
+ />
117
+ </button>
118
+ </div>
119
+ </template>
120
+
121
+ <!-- Gallery view -->
122
+ <MediaGallery
123
+ v-if="viewMode === 'gallery'"
124
+ id="files-gallery"
125
+ :fetch="fetchFiles"
126
+ :filters="filters"
127
+ :bulk-actions="bulkActions"
128
+ :per-page="25"
129
+ @show-usage="openUsageModal"
130
+ >
131
+ <template #empty-action>
132
+ <UButton
133
+ to="/motor-media/files/create"
134
+ :label="t('motor-media.files.add')"
135
+ icon="i-lucide-plus"
136
+ size="sm"
137
+ />
138
+ </template>
139
+ </MediaGallery>
140
+
141
+ <!-- Table view -->
142
+ <GridBase
143
+ v-else
144
+ id="files-grid"
145
+ :fetch="fetchFiles"
146
+ :columns="columns"
147
+ :filters="filters"
148
+ :bulk-actions="bulkActions"
149
+ base-path="/motor-media/files"
150
+ name-key="description"
151
+ :row-actions="rowActions"
152
+ write-permission="files.write"
153
+ delete-permission="files.delete"
154
+ :row-click-to="(row: any) => `/motor-media/files/${row.id}/edit`"
155
+ />
156
+ </GridPage>
157
+
158
+ <EntityUsageModal
159
+ v-model:open="usageModalOpen"
160
+ :endpoint="usageEndpoint"
161
+ :title="t('motor-media.files.usage_title')"
162
+ />
163
+ </div>
164
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ definePageMeta({ permission: 'files.read' })
3
+ </script>
4
+
5
+ <template>
6
+ <SectionLanding
7
+ section-prefix="motor-media"
8
+ title-key="motor-media.global.media"
9
+ subtitle-key="motor-media.global.section_subtitle"
10
+ />
11
+ </template>
@@ -0,0 +1,10 @@
1
+ import { registerGridRenderers } from '#imports'
2
+ import ImageRenderer from '../components/grid/renderers/ImageRenderer.vue'
3
+
4
+ export default defineNuxtPlugin((nuxtApp) => {
5
+ nuxtApp.hook('app:created', () => {
6
+ registerGridRenderers({
7
+ 'image': ImageRenderer,
8
+ })
9
+ })
10
+ })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Form field metadata — auto-generated from OpenAPI spec.
3
+ * Do not edit manually. Run `npm run sync:api` to regenerate.
4
+ */
5
+
6
+ export const fileFormMeta = {
7
+ post: {
8
+ schemaName: 'FilePostRequest',
9
+ fields: {
10
+ client_id: { input: 'select' },
11
+ description: { input: 'textarea', required: true },
12
+ author: { input: 'text', required: true },
13
+ source: { input: 'text', required: true },
14
+ alt_text: { input: 'text', required: true },
15
+ is_global: { input: 'toggle' },
16
+ is_excluded_from_search_index: { input: 'toggle' },
17
+ file: { input: 'text' },
18
+ tags: { input: 'text' },
19
+ categories: { input: 'multi-select', required: true },
20
+ files: { input: 'text', required: true }
21
+ }
22
+ },
23
+ patch: {
24
+ schemaName: 'FilePatchRequest',
25
+ fields: {
26
+ client_id: { input: 'select' },
27
+ description: { input: 'textarea', required: true },
28
+ author: { input: 'text', required: true },
29
+ source: { input: 'text', required: true },
30
+ alt_text: { input: 'text', required: true },
31
+ is_global: { input: 'toggle' },
32
+ is_excluded_from_search_index: { input: 'toggle' },
33
+ tags: { input: 'text' },
34
+ categories: { input: 'multi-select', required: true },
35
+ file: { input: 'text' }
36
+ }
37
+ }
38
+ } as const