@plutocms/pluto 0.9.1 → 0.11.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/CHANGELOG.md +14 -0
- package/FEATURES.md +4 -0
- package/app/assets/css/tailwind.css +15 -0
- package/app/components/Pluto/Content/PlutoContentForm.vue +324 -50
- package/app/components/Pluto/Content/PlutoContentList.vue +124 -106
- package/app/components/Pluto/Navbar/PlutoNavbarAdmin.vue +1 -1
- package/app/components/Pluto/View/PlutoViewAdminPanel.vue +41 -0
- package/app/components/Pluto/View/PlutoViewAside.vue +69 -0
- package/app/components/Pluto/View/PlutoViewToolbar.vue +64 -0
- package/app/composables/pluto-content-actions.ts +17 -0
- package/app/composables/pluto-extension.ts +1 -0
- package/app/composables/pluto-registry.ts +1 -0
- package/app/layouts/admin.vue +38 -65
- package/package.json +22 -22
- package/shared/types/content.ts +15 -0
- package/shared/types/registry.ts +30 -0
- package/shared/utils/content-map.ts +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [0.11.0](https://github.com/plutocms/pluto/compare/v0.10.0...v0.11.0) (2026-10-16)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **admin:** rebuild admin layout on Nuxt UI dashboard components ([#145](https://github.com/plutocms/pluto/issues/145)) ([621e57b](https://github.com/plutocms/pluto/commit/621e57b83d7650b49242c4b83c5374b33c7236bc))
|
|
10
|
+
|
|
11
|
+
## [0.10.0](https://github.com/plutocms/pluto/compare/v0.9.1...v0.10.0) (2026-09-13)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **content:** add virtual field flag for column-less content-type fields ([#143](https://github.com/plutocms/pluto/issues/143)) ([cf9091e](https://github.com/plutocms/pluto/commit/cf9091e57d21520d92197ba562eef073ef469ee1))
|
|
17
|
+
|
|
4
18
|
## [0.9.1](https://github.com/plutocms/pluto/compare/v0.9.0...v0.9.1) (2026-09-13)
|
|
5
19
|
|
|
6
20
|
|
package/FEATURES.md
CHANGED
|
@@ -19,3 +19,7 @@ policies shared by every project here.
|
|
|
19
19
|
admin UI (list/form components, eight built-in field widgets, generated
|
|
20
20
|
`/admin/content/**` pages, and nav/pages derivation). No real adapter yet.
|
|
21
21
|
@.claude/skills/content-model/SKILL.md
|
|
22
|
+
- Admin view chrome: per-view shell components (`PlutoViewAdminPanel`, `PlutoViewToolbar`,
|
|
23
|
+
`PlutoViewAside`), the generic content form's publish workflow, and the `contentActions`
|
|
24
|
+
registry bucket that lets a layer add its own buttons to that form.
|
|
25
|
+
@.claude/skills/admin-view-chrome/SKILL.md
|
|
@@ -9,3 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
--font-outfit: 'Outfit', sans-serif;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Set the header height outside a layer. This overrides Nuxt UI's own
|
|
15
|
+
* `@layer theme` value for the same variable. The values match the
|
|
16
|
+
* navbar's own height classes (`h-14` / `lg:h-10`).
|
|
17
|
+
*/
|
|
18
|
+
:root {
|
|
19
|
+
--ui-header-height: 3.5rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@media (width >= 64rem) {
|
|
23
|
+
:root {
|
|
24
|
+
--ui-header-height: 2.5rem;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { ButtonProps } from '@nuxt/ui'
|
|
3
|
+
|
|
2
4
|
const props = defineProps<{
|
|
3
5
|
/** A content type's `name` (not a registry entry id) — see `usePlutoContentTypes`. */
|
|
4
6
|
type: string
|
|
@@ -13,7 +15,9 @@ if (!contentType.value) {
|
|
|
13
15
|
// The generated page (`app/pages/admin/content/[type]/{new,[id]}.vue`)
|
|
14
16
|
// already resolves the type and 404s before this component ever mounts,
|
|
15
17
|
// so this should not normally happen.
|
|
16
|
-
console.error(
|
|
18
|
+
console.error(
|
|
19
|
+
`PlutoContentForm: content type "${props.type}" was not found in the registry.`
|
|
20
|
+
)
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
const { can } = usePlutoPermissions()
|
|
@@ -21,11 +25,26 @@ const { can } = usePlutoPermissions()
|
|
|
21
25
|
// "No capability declared" means the operation is open to anyone who can
|
|
22
26
|
// reach this page — never call can('') as a stand-in for that.
|
|
23
27
|
const canWrite = computed(
|
|
24
|
-
() =>
|
|
28
|
+
() =>
|
|
29
|
+
!contentType.value?.capabilities?.write ||
|
|
30
|
+
can(contentType.value.capabilities.write)
|
|
31
|
+
)
|
|
32
|
+
const canPublish = computed(() => {
|
|
33
|
+
const cap =
|
|
34
|
+
contentType.value?.capabilities?.publish ??
|
|
35
|
+
contentType.value?.capabilities?.write
|
|
36
|
+
return !cap || can(cap)
|
|
37
|
+
})
|
|
38
|
+
const canDelete = computed(
|
|
39
|
+
() =>
|
|
40
|
+
!contentType.value?.capabilities?.delete ||
|
|
41
|
+
can(contentType.value.capabilities.delete)
|
|
25
42
|
)
|
|
26
43
|
|
|
27
44
|
const editing = computed(() => props.id !== undefined)
|
|
28
|
-
const basePath = computed(
|
|
45
|
+
const basePath = computed(
|
|
46
|
+
() => contentType.value?.basePath ?? `/admin/content/${props.type}`
|
|
47
|
+
)
|
|
29
48
|
|
|
30
49
|
// editPath exists so an already-deployed layer's edit URL never has to
|
|
31
50
|
// change to adopt this component — see the doc comment on
|
|
@@ -35,12 +54,14 @@ function editPath(id: string | number) {
|
|
|
35
54
|
return contentType.value?.editPath?.(id) ?? `${basePath.value}/${id}`
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
const { item, save } = usePlutoContentItem(props.type, () => props.id)
|
|
57
|
+
const { item, save, remove } = usePlutoContentItem(props.type, () => props.id)
|
|
39
58
|
|
|
40
59
|
function sortedFields(region: 'main' | 'side') {
|
|
41
60
|
return computed(() =>
|
|
42
61
|
(contentType.value?.fields ?? [])
|
|
43
|
-
.filter(
|
|
62
|
+
.filter(
|
|
63
|
+
(field) => field.inForm !== false && (field.region ?? 'main') === region
|
|
64
|
+
)
|
|
44
65
|
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100))
|
|
45
66
|
)
|
|
46
67
|
}
|
|
@@ -63,7 +84,9 @@ const sideFields = sortedFields('side')
|
|
|
63
84
|
// the moment a 'slug' field's own `update:modelValue` fires.
|
|
64
85
|
const slugTouched = ref(false)
|
|
65
86
|
const slugField = computed(() =>
|
|
66
|
-
contentType.value?.fields.find(
|
|
87
|
+
contentType.value?.fields.find(
|
|
88
|
+
(field): field is PlutoSlugField => field.type === 'slug'
|
|
89
|
+
)
|
|
67
90
|
)
|
|
68
91
|
|
|
69
92
|
function slugify(value: string): string {
|
|
@@ -96,13 +119,40 @@ function updateField(field: PlutoField, value: unknown) {
|
|
|
96
119
|
item.value[field.name] = value
|
|
97
120
|
}
|
|
98
121
|
|
|
122
|
+
// --- Status / publish workflow ----------------------------------------------
|
|
123
|
+
|
|
124
|
+
const statusConfig = computed(() => contentType.value?.status || null)
|
|
125
|
+
const publishedValue = computed(
|
|
126
|
+
() => statusConfig.value?.publishedValue ?? 'published'
|
|
127
|
+
)
|
|
128
|
+
const draftValue = computed(
|
|
129
|
+
() =>
|
|
130
|
+
statusConfig.value?.default ??
|
|
131
|
+
statusConfig.value?.values.find(
|
|
132
|
+
(value) => value.value !== publishedValue.value
|
|
133
|
+
)?.value
|
|
134
|
+
)
|
|
135
|
+
const currentStatus = computed(
|
|
136
|
+
() => (item.value.status as string | undefined) ?? statusConfig.value?.default
|
|
137
|
+
)
|
|
138
|
+
const isPublished = computed(
|
|
139
|
+
() =>
|
|
140
|
+
statusConfig.value !== null && currentStatus.value === publishedValue.value
|
|
141
|
+
)
|
|
142
|
+
const statusLabel = computed(
|
|
143
|
+
() =>
|
|
144
|
+
statusConfig.value?.values.find(
|
|
145
|
+
(value) => value.value === currentStatus.value
|
|
146
|
+
)?.label ?? currentStatus.value
|
|
147
|
+
)
|
|
148
|
+
|
|
99
149
|
// --- Save -------------------------------------------------------------------
|
|
100
150
|
|
|
101
151
|
const toast = useToast()
|
|
102
152
|
const saving = ref(false)
|
|
103
153
|
const validationErrors = ref<ContentValidationError[]>([])
|
|
104
154
|
|
|
105
|
-
function buildPayload(): Record<string, unknown> {
|
|
155
|
+
function buildPayload(statusOverride?: string): Record<string, unknown> {
|
|
106
156
|
const type = contentType.value
|
|
107
157
|
if (!type) {
|
|
108
158
|
return {}
|
|
@@ -115,17 +165,23 @@ function buildPayload(): Record<string, unknown> {
|
|
|
115
165
|
}
|
|
116
166
|
}
|
|
117
167
|
|
|
168
|
+
if (statusOverride !== undefined) {
|
|
169
|
+
payload.status = statusOverride
|
|
170
|
+
}
|
|
171
|
+
|
|
118
172
|
return payload
|
|
119
173
|
}
|
|
120
174
|
|
|
121
|
-
async function onSave() {
|
|
175
|
+
async function onSave(statusOverride?: string) {
|
|
122
176
|
const type = contentType.value
|
|
123
177
|
if (!type) {
|
|
124
178
|
return
|
|
125
179
|
}
|
|
126
180
|
|
|
127
|
-
const payload = buildPayload()
|
|
128
|
-
const errors = validateContentPayload(type, payload, {
|
|
181
|
+
const payload = buildPayload(statusOverride)
|
|
182
|
+
const errors = validateContentPayload(type, payload, {
|
|
183
|
+
partial: editing.value,
|
|
184
|
+
})
|
|
129
185
|
|
|
130
186
|
if (errors.length > 0) {
|
|
131
187
|
validationErrors.value = errors
|
|
@@ -143,8 +199,15 @@ async function onSave() {
|
|
|
143
199
|
return
|
|
144
200
|
}
|
|
145
201
|
|
|
202
|
+
const title =
|
|
203
|
+
statusOverride === publishedValue.value
|
|
204
|
+
? 'Published'
|
|
205
|
+
: statusOverride === draftValue.value
|
|
206
|
+
? 'Unpublished'
|
|
207
|
+
: 'Saved'
|
|
208
|
+
|
|
146
209
|
toast.add({
|
|
147
|
-
title
|
|
210
|
+
title,
|
|
148
211
|
description: `${type.label} updated successfully.`,
|
|
149
212
|
color: 'success',
|
|
150
213
|
})
|
|
@@ -161,52 +224,231 @@ async function onSave() {
|
|
|
161
224
|
saving.value = false
|
|
162
225
|
}
|
|
163
226
|
}
|
|
227
|
+
|
|
228
|
+
// --- Delete -------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
const isDeleteModalOpen = ref(false)
|
|
231
|
+
const deleting = ref(false)
|
|
232
|
+
|
|
233
|
+
function openDeleteModal() {
|
|
234
|
+
isDeleteModalOpen.value = true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function closeDeleteModal() {
|
|
238
|
+
isDeleteModalOpen.value = false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function confirmDelete() {
|
|
242
|
+
deleting.value = true
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await remove()
|
|
246
|
+
closeDeleteModal()
|
|
247
|
+
await navigateTo(basePath.value)
|
|
248
|
+
} catch {
|
|
249
|
+
toast.add({
|
|
250
|
+
title: 'Error',
|
|
251
|
+
description: 'An error occurred while removing.',
|
|
252
|
+
color: 'error',
|
|
253
|
+
})
|
|
254
|
+
} finally {
|
|
255
|
+
deleting.value = false
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Layout / aside -----------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
const layout = computed(() => contentType.value?.form?.layout ?? 'default')
|
|
262
|
+
const showAside = computed(() => {
|
|
263
|
+
const configured = contentType.value?.form?.aside
|
|
264
|
+
if (configured === true || configured === false) {
|
|
265
|
+
return configured
|
|
266
|
+
}
|
|
267
|
+
return sideFields.value.length > 0
|
|
268
|
+
})
|
|
269
|
+
const asideCollapsedByDefault = computed(
|
|
270
|
+
() => contentType.value?.form?.asideCollapsed ?? layout.value === 'focus'
|
|
271
|
+
)
|
|
272
|
+
// Remember the open/closed state per content type across visits.
|
|
273
|
+
const asideOpen = useCookie<boolean>(`pluto-content-aside-${props.type}`, {
|
|
274
|
+
default: () => !asideCollapsedByDefault.value,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
function toggleAside() {
|
|
278
|
+
asideOpen.value = !asideOpen.value
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- Registry content actions --------------------------------------------------
|
|
282
|
+
|
|
283
|
+
const toolbarActions = usePlutoContentActions(props.type, 'toolbar')
|
|
284
|
+
|
|
285
|
+
// Only rendered from the template inside the `v-if="contentType"` branch, so
|
|
286
|
+
// `contentType.value` is always set by the time this is read.
|
|
287
|
+
function buildActionContext(): PlutoContentActionContext {
|
|
288
|
+
return {
|
|
289
|
+
type: contentType.value as PlutoContentType,
|
|
290
|
+
item: item.value,
|
|
291
|
+
editing: editing.value,
|
|
292
|
+
saving: saving.value,
|
|
293
|
+
save: () => onSave(),
|
|
294
|
+
remove: () => confirmDelete(),
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// No explicit `DropdownMenuItem` annotation. A consumer that links this
|
|
299
|
+
// layer from source can resolve a second copy of `@nuxt/ui`. A named type
|
|
300
|
+
// from one copy then fails to match the component from the other copy. An
|
|
301
|
+
// inferred, structural type matches both.
|
|
302
|
+
const unpublishDeleteItems = computed(() => [
|
|
303
|
+
[
|
|
304
|
+
...(statusConfig.value && isPublished.value && canPublish.value
|
|
305
|
+
? [
|
|
306
|
+
{
|
|
307
|
+
label: 'Unpublish',
|
|
308
|
+
icon: 'lucide:eye-off',
|
|
309
|
+
onSelect: () => onSave(draftValue.value),
|
|
310
|
+
},
|
|
311
|
+
]
|
|
312
|
+
: []),
|
|
313
|
+
...(editing.value && canDelete.value
|
|
314
|
+
? [
|
|
315
|
+
{
|
|
316
|
+
label: 'Delete',
|
|
317
|
+
icon: 'lucide:trash',
|
|
318
|
+
color: 'error' as const,
|
|
319
|
+
onSelect: () => openDeleteModal(),
|
|
320
|
+
},
|
|
321
|
+
]
|
|
322
|
+
: []),
|
|
323
|
+
],
|
|
324
|
+
])
|
|
164
325
|
</script>
|
|
165
326
|
|
|
166
327
|
<template>
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
328
|
+
<PlutoViewAdminPanel
|
|
329
|
+
v-if="contentType"
|
|
330
|
+
:width="layout === 'focus' ? 'full' : 'container'"
|
|
331
|
+
>
|
|
332
|
+
<template #toolbar>
|
|
333
|
+
<PlutoViewToolbar
|
|
334
|
+
:back-to="basePath"
|
|
335
|
+
:title="
|
|
336
|
+
layout === 'focus'
|
|
337
|
+
? editing
|
|
338
|
+
? `Edit ${contentType.label}`
|
|
339
|
+
: `New ${contentType.label}`
|
|
340
|
+
: undefined
|
|
341
|
+
"
|
|
173
342
|
>
|
|
174
|
-
<template #
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
343
|
+
<template #right>
|
|
344
|
+
<UBadge
|
|
345
|
+
v-if="statusConfig"
|
|
346
|
+
:color="isPublished ? 'success' : 'neutral'"
|
|
347
|
+
variant="subtle"
|
|
348
|
+
>
|
|
349
|
+
{{ statusLabel }}
|
|
350
|
+
</UBadge>
|
|
351
|
+
|
|
352
|
+
<template v-for="action in toolbarActions" :key="action.id">
|
|
353
|
+
<!-- prettier-ignore -->
|
|
354
|
+
<UButton
|
|
355
|
+
v-if="action.show?.(buildActionContext()) !== false"
|
|
356
|
+
:icon="action.icon"
|
|
357
|
+
:color="(action.color as ButtonProps['color'])"
|
|
358
|
+
:variant="(action.variant as ButtonProps['variant'])"
|
|
359
|
+
@click="action.onSelect(buildActionContext())"
|
|
360
|
+
>
|
|
361
|
+
{{ action.label }}
|
|
362
|
+
</UButton>
|
|
363
|
+
</template>
|
|
188
364
|
|
|
189
|
-
<
|
|
190
|
-
v-
|
|
191
|
-
:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
365
|
+
<UButton
|
|
366
|
+
v-if="canWrite && (!statusConfig || !isPublished)"
|
|
367
|
+
:loading="saving"
|
|
368
|
+
icon="lucide:save"
|
|
369
|
+
variant="subtle"
|
|
370
|
+
@click="onSave()"
|
|
371
|
+
>
|
|
372
|
+
Save{{ statusConfig ? ' draft' : '' }}
|
|
373
|
+
</UButton>
|
|
198
374
|
|
|
199
|
-
<div class="flex w-full shrink-0 flex-col gap-6 lg:max-w-xs">
|
|
200
375
|
<UButton
|
|
201
|
-
v-if="canWrite"
|
|
376
|
+
v-if="canWrite && statusConfig && isPublished"
|
|
202
377
|
:loading="saving"
|
|
203
378
|
icon="lucide:save"
|
|
204
|
-
|
|
205
|
-
|
|
379
|
+
@click="onSave()"
|
|
380
|
+
>
|
|
381
|
+
Update
|
|
382
|
+
</UButton>
|
|
383
|
+
|
|
384
|
+
<UButton
|
|
385
|
+
v-if="canPublish && statusConfig && !isPublished"
|
|
386
|
+
:loading="saving"
|
|
387
|
+
icon="lucide:upload"
|
|
388
|
+
@click="onSave(publishedValue)"
|
|
206
389
|
>
|
|
207
|
-
|
|
390
|
+
Publish
|
|
208
391
|
</UButton>
|
|
209
392
|
|
|
393
|
+
<UDropdownMenu
|
|
394
|
+
v-if="
|
|
395
|
+
(editing && canDelete) ||
|
|
396
|
+
(statusConfig && isPublished && canPublish)
|
|
397
|
+
"
|
|
398
|
+
:items="unpublishDeleteItems"
|
|
399
|
+
>
|
|
400
|
+
<UButton
|
|
401
|
+
icon="lucide:ellipsis-vertical"
|
|
402
|
+
color="neutral"
|
|
403
|
+
variant="ghost"
|
|
404
|
+
square
|
|
405
|
+
/>
|
|
406
|
+
</UDropdownMenu>
|
|
407
|
+
|
|
408
|
+
<UButton
|
|
409
|
+
v-if="showAside"
|
|
410
|
+
:icon="
|
|
411
|
+
asideOpen ? 'lucide:panel-right-close' : 'lucide:panel-right-open'
|
|
412
|
+
"
|
|
413
|
+
color="neutral"
|
|
414
|
+
variant="ghost"
|
|
415
|
+
square
|
|
416
|
+
@click="toggleAside"
|
|
417
|
+
/>
|
|
418
|
+
</template>
|
|
419
|
+
</PlutoViewToolbar>
|
|
420
|
+
</template>
|
|
421
|
+
|
|
422
|
+
<UAlert
|
|
423
|
+
v-if="validationErrors.length > 0"
|
|
424
|
+
color="error"
|
|
425
|
+
title="Please fix the following before saving"
|
|
426
|
+
>
|
|
427
|
+
<template #description>
|
|
428
|
+
<ul class="list-disc pl-4">
|
|
429
|
+
<li v-for="fieldError in validationErrors" :key="fieldError.field">
|
|
430
|
+
{{ fieldError.message }}
|
|
431
|
+
</li>
|
|
432
|
+
</ul>
|
|
433
|
+
</template>
|
|
434
|
+
</UAlert>
|
|
435
|
+
|
|
436
|
+
<h1 v-if="layout !== 'focus'" class="text-3xl font-bold lg:text-4xl">
|
|
437
|
+
{{ editing ? `Edit ${contentType.label}` : `New ${contentType.label}` }}
|
|
438
|
+
</h1>
|
|
439
|
+
|
|
440
|
+
<PlutoContentField
|
|
441
|
+
v-for="field in mainFields"
|
|
442
|
+
:key="field.name"
|
|
443
|
+
:field="field"
|
|
444
|
+
:model-value="item[field.name]"
|
|
445
|
+
:disabled="!canWrite || saving"
|
|
446
|
+
@update:model-value="updateField(field, $event)"
|
|
447
|
+
/>
|
|
448
|
+
|
|
449
|
+
<template v-if="showAside" #aside>
|
|
450
|
+
<PlutoViewAside v-model:open="asideOpen" title="Settings">
|
|
451
|
+
<div class="flex flex-col gap-6">
|
|
210
452
|
<PlutoContentField
|
|
211
453
|
v-for="field in sideFields"
|
|
212
454
|
:key="field.name"
|
|
@@ -216,15 +458,47 @@ async function onSave() {
|
|
|
216
458
|
@update:model-value="updateField(field, $event)"
|
|
217
459
|
/>
|
|
218
460
|
</div>
|
|
219
|
-
</
|
|
220
|
-
</
|
|
221
|
-
</
|
|
461
|
+
</PlutoViewAside>
|
|
462
|
+
</template>
|
|
463
|
+
</PlutoViewAdminPanel>
|
|
222
464
|
|
|
223
|
-
<
|
|
465
|
+
<PlutoViewAdminPanel v-else>
|
|
224
466
|
<UAlert
|
|
225
467
|
:description="`No content type named "${type}" is registered.`"
|
|
226
468
|
color="error"
|
|
227
469
|
title="Content type not found"
|
|
228
470
|
/>
|
|
229
|
-
</
|
|
471
|
+
</PlutoViewAdminPanel>
|
|
472
|
+
|
|
473
|
+
<Modal v-model="isDeleteModalOpen" :custom-size="480">
|
|
474
|
+
<ModalHeader @close="closeDeleteModal">
|
|
475
|
+
Remove {{ contentType?.label }}
|
|
476
|
+
</ModalHeader>
|
|
477
|
+
|
|
478
|
+
<ModalContent>
|
|
479
|
+
<p>Do you really want to remove this item?</p>
|
|
480
|
+
</ModalContent>
|
|
481
|
+
|
|
482
|
+
<ModalFooter>
|
|
483
|
+
<div class="flex items-center gap-4">
|
|
484
|
+
<UButton
|
|
485
|
+
icon="lucide:x"
|
|
486
|
+
variant="ghost"
|
|
487
|
+
color="neutral"
|
|
488
|
+
@click="closeDeleteModal"
|
|
489
|
+
>
|
|
490
|
+
Cancel
|
|
491
|
+
</UButton>
|
|
492
|
+
|
|
493
|
+
<UButton
|
|
494
|
+
:loading="deleting"
|
|
495
|
+
icon="lucide:trash"
|
|
496
|
+
color="error"
|
|
497
|
+
@click="confirmDelete"
|
|
498
|
+
>
|
|
499
|
+
Remove
|
|
500
|
+
</UButton>
|
|
501
|
+
</div>
|
|
502
|
+
</ModalFooter>
|
|
503
|
+
</Modal>
|
|
230
504
|
</template>
|