@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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/pluto",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"description": "The layerable CMS",
|
|
6
6
|
"author": "Victor Ribeiro",
|
|
7
7
|
"license": "MIT",
|
|
@@ -42,31 +42,31 @@
|
|
|
42
42
|
"vue-demi"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@iconify-json/lucide": "^1.2.
|
|
46
|
-
"@iconify-json/simple-icons": "^1.2.
|
|
47
|
-
"@internationalized/date": "^3.12.
|
|
48
|
-
"@nuxt/eslint": "^1.
|
|
49
|
-
"@nuxt/ui": "^4.
|
|
45
|
+
"@iconify-json/lucide": "^1.2.144",
|
|
46
|
+
"@iconify-json/simple-icons": "^1.2.100",
|
|
47
|
+
"@internationalized/date": "^3.12.4",
|
|
48
|
+
"@nuxt/eslint": "^1.17.0",
|
|
49
|
+
"@nuxt/ui": "^4.12.0",
|
|
50
50
|
"@plutocms/utils": "^0.2.1",
|
|
51
|
-
"@vueuse/nuxt": "^14.
|
|
52
|
-
"reka-ui": "^2.
|
|
53
|
-
"tailwind-merge": "^3.
|
|
51
|
+
"@vueuse/nuxt": "^14.4.0",
|
|
52
|
+
"reka-ui": "^2.11.0",
|
|
53
|
+
"tailwind-merge": "^3.7.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@antfu/eslint-config": "^9.1
|
|
57
|
-
"@tailwindcss/vite": "^4.
|
|
58
|
-
"@types/bun": "^1.3
|
|
59
|
-
"eslint": "^10.
|
|
60
|
-
"mysql2": "^3.
|
|
61
|
-
"nuxt": "^4.
|
|
62
|
-
"prettier": "^3.9.
|
|
63
|
-
"prettier-plugin-tailwindcss": "^0.8.
|
|
64
|
-
"tailwindcss": "^4.3.
|
|
56
|
+
"@antfu/eslint-config": "^9.5.1",
|
|
57
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
58
|
+
"@types/bun": "^1.4.3",
|
|
59
|
+
"eslint": "^10.12.0",
|
|
60
|
+
"mysql2": "^3.24.6",
|
|
61
|
+
"nuxt": "^4.6.1",
|
|
62
|
+
"prettier": "^3.9.10",
|
|
63
|
+
"prettier-plugin-tailwindcss": "^0.8.1",
|
|
64
|
+
"tailwindcss": "^4.3.3",
|
|
65
65
|
"typescript": "6.0.3",
|
|
66
|
-
"vitest": "^5.0.
|
|
67
|
-
"vue": "^3.5.
|
|
68
|
-
"vue-router": "^5.
|
|
69
|
-
"vue-tsc": "^3.3.
|
|
66
|
+
"vitest": "^5.0.5",
|
|
67
|
+
"vue": "^3.5.44",
|
|
68
|
+
"vue-router": "^5.4.0",
|
|
69
|
+
"vue-tsc": "^3.3.13"
|
|
70
70
|
},
|
|
71
71
|
"overrides": {
|
|
72
72
|
"eslint-flat-config-utils": "^3.2.0"
|
package/shared/types/content.ts
CHANGED
|
@@ -25,6 +25,8 @@ export interface PlutoFieldBase {
|
|
|
25
25
|
description?: string
|
|
26
26
|
/** Storage column, when it differs from `name`. Falls back to `name` when unset. */
|
|
27
27
|
column?: string
|
|
28
|
+
/** This field has no storage column. Its value never reaches `mapFieldsToColumns`'s output, but still passes through validation and the generic form payload — letting a `hooks.afterCreate`/`afterUpdate` read it from `rawBody` without a matching database column. */
|
|
29
|
+
virtual?: boolean
|
|
28
30
|
required?: boolean
|
|
29
31
|
/** Show this field as a column in the generic list view. Default false. */
|
|
30
32
|
inList?: boolean
|
|
@@ -108,6 +110,8 @@ export interface PlutoContentTypeCapabilities {
|
|
|
108
110
|
read?: string
|
|
109
111
|
write?: string
|
|
110
112
|
delete?: string
|
|
113
|
+
/** Capability required to publish. Falls back to `write` when unset. */
|
|
114
|
+
publish?: string
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
export interface PlutoContentStatus {
|
|
@@ -207,6 +211,17 @@ export interface PlutoContentType {
|
|
|
207
211
|
editPath?: (id: string | number) => string
|
|
208
212
|
navOrder?: number
|
|
209
213
|
i18n?: PlutoContentI18n
|
|
214
|
+
/** Layout options for the generic content form (a later wave). */
|
|
215
|
+
form?: {
|
|
216
|
+
/** 'default' = heading + container. 'focus' = minimal chrome for a writing surface. */
|
|
217
|
+
layout?: 'default' | 'focus'
|
|
218
|
+
/** true = always show. false = never. 'auto' (default) = show when any field has region 'side'. */
|
|
219
|
+
aside?: boolean | 'auto'
|
|
220
|
+
/** Aside starts closed. Default false, or true when layout is 'focus'. */
|
|
221
|
+
asideCollapsed?: boolean
|
|
222
|
+
/** Drop the max-width container in the main region. Default false, or true when layout is 'focus'. */
|
|
223
|
+
width?: 'container' | 'full'
|
|
224
|
+
}
|
|
210
225
|
/**
|
|
211
226
|
* Server-only lifecycle hooks, run by the generic write handlers
|
|
212
227
|
* (`server/utils/pluto-content-handlers.ts`) after the adapter call
|
package/shared/types/registry.ts
CHANGED
|
@@ -113,6 +113,34 @@ export interface PlutoMediaAdapterEntry extends PlutoEntry {
|
|
|
113
113
|
adapter: PlutoMediaAdapter
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** State passed to a `PlutoContentAction`'s `show` test and `onSelect` handler. */
|
|
117
|
+
export interface PlutoContentActionContext {
|
|
118
|
+
type: PlutoContentType
|
|
119
|
+
item: Record<string, unknown>
|
|
120
|
+
editing: boolean
|
|
121
|
+
saving: boolean
|
|
122
|
+
save: () => Promise<void>
|
|
123
|
+
remove: () => Promise<void>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A button a layer adds to the generic content form (a later wave). Matched
|
|
128
|
+
* against a content type the same way `PlutoContentFieldWidget` matches a
|
|
129
|
+
* field: by exact name, or by `'*'` for every content type.
|
|
130
|
+
*/
|
|
131
|
+
export interface PlutoContentAction extends PlutoEntry {
|
|
132
|
+
/** Content type `name`, or '*' for every content type. */
|
|
133
|
+
contentType: string | '*'
|
|
134
|
+
label: string
|
|
135
|
+
icon?: string
|
|
136
|
+
color?: string
|
|
137
|
+
variant?: string
|
|
138
|
+
/** Where the action renders. Default 'toolbar'. */
|
|
139
|
+
placement?: 'toolbar' | 'aside'
|
|
140
|
+
show?: (ctx: PlutoContentActionContext) => boolean
|
|
141
|
+
onSelect: (ctx: PlutoContentActionContext) => void | Promise<void>
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
export interface PlutoExtension {
|
|
117
145
|
/** Layer name. Use the package's $meta.name, e.g. 'supabase-blog'. */
|
|
118
146
|
id: string
|
|
@@ -127,6 +155,7 @@ export interface PlutoExtension {
|
|
|
127
155
|
permissionsDriver?: PlutoPermissionsDriver
|
|
128
156
|
contentTypes?: PlutoContentTypeEntry[]
|
|
129
157
|
contentFieldWidgets?: PlutoContentFieldWidget[]
|
|
158
|
+
contentActions?: PlutoContentAction[]
|
|
130
159
|
mediaAdapters?: PlutoMediaAdapterEntry[]
|
|
131
160
|
}
|
|
132
161
|
|
|
@@ -154,5 +183,6 @@ export interface PlutoRegistry {
|
|
|
154
183
|
permissionsDrivers: PlutoRegistryBucket<PlutoPermissionsDriver>
|
|
155
184
|
contentTypes: PlutoRegistryBucket<PlutoContentTypeEntry>
|
|
156
185
|
contentFieldWidgets: PlutoRegistryBucket<PlutoContentFieldWidget>
|
|
186
|
+
contentActions: PlutoRegistryBucket<PlutoContentAction>
|
|
157
187
|
mediaAdapters: PlutoRegistryBucket<PlutoMediaAdapterEntry>
|
|
158
188
|
}
|
|
@@ -11,6 +11,9 @@ export function fieldColumn(field: PlutoField): string {
|
|
|
11
11
|
* left out of the result, so a partial update payload stays partial after
|
|
12
12
|
* mapping. An adapter (a later wave) sends the result straight to its
|
|
13
13
|
* storage layer.
|
|
14
|
+
*
|
|
15
|
+
* A field with `virtual: true` never appears in the result, even when
|
|
16
|
+
* `payload` has a value for it — it has no storage column by design.
|
|
14
17
|
*/
|
|
15
18
|
export function mapFieldsToColumns(
|
|
16
19
|
type: PlutoContentType,
|
|
@@ -19,6 +22,9 @@ export function mapFieldsToColumns(
|
|
|
19
22
|
const columns: Record<string, unknown> = {}
|
|
20
23
|
|
|
21
24
|
for (const field of type.fields) {
|
|
25
|
+
if (field.virtual) {
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
22
28
|
if (Object.hasOwn(payload, field.name)) {
|
|
23
29
|
columns[fieldColumn(field)] = payload[field.name]
|
|
24
30
|
}
|
|
@@ -32,6 +38,19 @@ export function mapFieldsToColumns(
|
|
|
32
38
|
* every field in `type.fields`, plus the primary key column (`type.primaryKey`,
|
|
33
39
|
* default `'id'`) mapped to `item.id`. A column absent from `row` is left
|
|
34
40
|
* out of the result field.
|
|
41
|
+
*
|
|
42
|
+
* A field with `virtual: true` is skipped, for symmetry with
|
|
43
|
+
* `mapFieldsToColumns` — it has no column to read back from `row` in the
|
|
44
|
+
* first place, so this only guards against a stray column that happens to
|
|
45
|
+
* share the field's name.
|
|
46
|
+
*
|
|
47
|
+
* When `type.status` is set, this also maps the status column
|
|
48
|
+
* (`type.status.column`, default `'status'`) to `item.status`, unless a
|
|
49
|
+
* declared field already set `item.status` above. This mirrors the write
|
|
50
|
+
* side: `@plutocms/supabase`'s `content-adapter.ts` already writes the
|
|
51
|
+
* status column for a content type with workflow enabled, even when no
|
|
52
|
+
* field named `status` is declared. Without this, a generic list/form
|
|
53
|
+
* read back a row with no `status` key at all.
|
|
35
54
|
*/
|
|
36
55
|
export function mapColumnsToFields(
|
|
37
56
|
type: PlutoContentType,
|
|
@@ -41,11 +60,21 @@ export function mapColumnsToFields(
|
|
|
41
60
|
const item: Record<string, unknown> = { id: row[primaryKey] }
|
|
42
61
|
|
|
43
62
|
for (const field of type.fields) {
|
|
63
|
+
if (field.virtual) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
44
66
|
const column = fieldColumn(field)
|
|
45
67
|
if (Object.hasOwn(row, column)) {
|
|
46
68
|
item[field.name] = row[column]
|
|
47
69
|
}
|
|
48
70
|
}
|
|
49
71
|
|
|
72
|
+
if (type.status) {
|
|
73
|
+
const statusColumn = type.status.column ?? 'status'
|
|
74
|
+
if (!Object.hasOwn(item, 'status') && Object.hasOwn(row, statusColumn)) {
|
|
75
|
+
item.status = row[statusColumn]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
return item as PlutoContentItem
|
|
51
80
|
}
|