@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutocms/pluto",
3
3
  "type": "module",
4
- "version": "0.9.1",
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.116",
46
- "@iconify-json/simple-icons": "^1.2.89",
47
- "@internationalized/date": "^3.12.2",
48
- "@nuxt/eslint": "^1.16.0",
49
- "@nuxt/ui": "^4.9.0",
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.3.0",
52
- "reka-ui": "^2.10.1",
53
- "tailwind-merge": "^3.6.0"
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.0",
57
- "@tailwindcss/vite": "^4.2.1",
58
- "@types/bun": "^1.3.14",
59
- "eslint": "^10.6.0",
60
- "mysql2": "^3.22.6",
61
- "nuxt": "^4.3.1",
62
- "prettier": "^3.9.4",
63
- "prettier-plugin-tailwindcss": "^0.8.0",
64
- "tailwindcss": "^4.3.2",
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.0",
67
- "vue": "^3.5.39",
68
- "vue-router": "^5.1.0",
69
- "vue-tsc": "^3.3.7"
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"
@@ -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
@@ -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
  }