@xleddyl/nuxt-cms 0.1.61 → 0.2.1

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.
Files changed (147) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +133 -14
  3. package/dist/runtime/app/components/cms/BlocksField.vue +86 -10
  4. package/dist/runtime/app/components/cms/BrandMark.d.vue.ts +6 -0
  5. package/dist/runtime/app/components/cms/BrandMark.vue +19 -0
  6. package/dist/runtime/app/components/cms/BrandMark.vue.d.ts +6 -0
  7. package/dist/runtime/app/components/cms/Checkbox.d.vue.ts +11 -0
  8. package/dist/runtime/app/components/cms/Checkbox.vue +21 -0
  9. package/dist/runtime/app/components/cms/Checkbox.vue.d.ts +11 -0
  10. package/dist/runtime/app/components/cms/Drawer.vue +5 -9
  11. package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
  12. package/dist/runtime/app/components/cms/EntryDrawer.vue +12 -3
  13. package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
  14. package/dist/runtime/app/components/cms/EntryForm.vue +104 -30
  15. package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
  16. package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
  17. package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
  18. package/dist/runtime/app/components/cms/FormField.vue +8 -2
  19. package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
  20. package/dist/runtime/app/components/cms/Icon.vue +1 -1
  21. package/dist/runtime/app/components/cms/Input.d.vue.ts +0 -1
  22. package/dist/runtime/app/components/cms/Input.vue +7 -4
  23. package/dist/runtime/app/components/cms/Input.vue.d.ts +0 -1
  24. package/dist/runtime/app/components/cms/LocaleSwitch.vue +1 -1
  25. package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +0 -1
  26. package/dist/runtime/app/components/cms/MediaFolderPicker.vue +1 -3
  27. package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +0 -1
  28. package/dist/runtime/app/components/cms/MediaGallery.vue +24 -10
  29. package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
  30. package/dist/runtime/app/components/cms/MediaUpload.vue +71 -41
  31. package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
  32. package/dist/runtime/app/components/cms/Modal.vue +5 -9
  33. package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
  34. package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
  35. package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
  36. package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
  37. package/dist/runtime/app/components/cms/PasswordInput.vue +81 -0
  38. package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
  39. package/dist/runtime/app/components/cms/RelationField.vue +7 -2
  40. package/dist/runtime/app/components/cms/SelectMenu.d.vue.ts +0 -1
  41. package/dist/runtime/app/components/cms/SelectMenu.vue +4 -11
  42. package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
  43. package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
  44. package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
  45. package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
  46. package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
  47. package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
  48. package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
  49. package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
  50. package/dist/runtime/app/components/cms/SettingsEntry.vue +126 -0
  51. package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
  52. package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
  53. package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
  54. package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
  55. package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
  56. package/dist/runtime/app/components/cms/SettingsModal.vue +174 -0
  57. package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
  58. package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
  59. package/dist/runtime/app/components/cms/SettingsUsers.vue +274 -0
  60. package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
  61. package/dist/runtime/app/components/cms/Spinner.vue +2 -2
  62. package/dist/runtime/app/components/cms/Table.d.vue.ts +6 -8
  63. package/dist/runtime/app/components/cms/Table.vue +42 -47
  64. package/dist/runtime/app/components/cms/Table.vue.d.ts +6 -8
  65. package/dist/runtime/app/components/cms/Textarea.d.vue.ts +0 -1
  66. package/dist/runtime/app/components/cms/Textarea.vue +1 -2
  67. package/dist/runtime/app/components/cms/Textarea.vue.d.ts +0 -1
  68. package/dist/runtime/app/components/cms/Toaster.vue +1 -1
  69. package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
  70. package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
  71. package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
  72. package/dist/runtime/app/composables/cms-account.d.ts +11 -0
  73. package/dist/runtime/app/composables/cms-account.js +23 -0
  74. package/dist/runtime/app/composables/cms-entry-disabled.js +1 -1
  75. package/dist/runtime/app/composables/cms-media-keys.d.ts +1 -0
  76. package/dist/runtime/app/composables/cms-media-keys.js +5 -0
  77. package/dist/runtime/app/composables/cms-media-library.js +3 -0
  78. package/dist/runtime/app/composables/cms-media-uploader.d.ts +11 -2
  79. package/dist/runtime/app/composables/cms-media-uploader.js +139 -12
  80. package/dist/runtime/app/composables/cms-overlay.d.ts +4 -0
  81. package/dist/runtime/app/composables/cms-overlay.js +23 -0
  82. package/dist/runtime/app/composables/cms-settings.d.ts +12 -0
  83. package/dist/runtime/app/composables/cms-settings.js +59 -0
  84. package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
  85. package/dist/runtime/app/composables/cms-theme.js +27 -0
  86. package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
  87. package/dist/runtime/app/layouts/cms-admin.vue +71 -17
  88. package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
  89. package/dist/runtime/app/pages/admin-collection.vue +127 -101
  90. package/dist/runtime/app/pages/admin-entry.vue +2 -0
  91. package/dist/runtime/app/pages/admin-login.vue +18 -3
  92. package/dist/runtime/app/pages/admin-media.vue +5 -1
  93. package/dist/runtime/app/utils/ui.d.ts +2 -0
  94. package/dist/runtime/app/utils/ui.js +18 -0
  95. package/dist/runtime/assets/main.css +1 -1
  96. package/dist/runtime/generate.js +1 -1
  97. package/dist/runtime/server/api/account-password.put.d.ts +4 -0
  98. package/dist/runtime/server/api/account-password.put.js +38 -0
  99. package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
  100. package/dist/runtime/server/api/account-welcome.post.js +14 -0
  101. package/dist/runtime/server/api/account.put.d.ts +3 -0
  102. package/dist/runtime/server/api/account.put.js +25 -0
  103. package/dist/runtime/server/api/collection.get.js +1 -1
  104. package/dist/runtime/server/api/media.delete.js +2 -1
  105. package/dist/runtime/server/api/media.post.js +2 -1
  106. package/dist/runtime/server/api/media.put.js +2 -1
  107. package/dist/runtime/server/api/settings.delete.d.ts +4 -0
  108. package/dist/runtime/server/api/settings.delete.js +12 -0
  109. package/dist/runtime/server/api/settings.get.d.ts +3 -0
  110. package/dist/runtime/server/api/settings.get.js +15 -0
  111. package/dist/runtime/server/api/settings.put.d.ts +3 -0
  112. package/dist/runtime/server/api/settings.put.js +14 -0
  113. package/dist/runtime/server/api/user.delete.d.ts +4 -0
  114. package/dist/runtime/server/api/user.delete.js +12 -0
  115. package/dist/runtime/server/api/user.put.d.ts +3 -0
  116. package/dist/runtime/server/api/user.put.js +27 -0
  117. package/dist/runtime/server/api/users.get.d.ts +3 -0
  118. package/dist/runtime/server/api/users.get.js +11 -0
  119. package/dist/runtime/server/api/users.post.d.ts +3 -0
  120. package/dist/runtime/server/api/users.post.js +36 -0
  121. package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
  122. package/dist/runtime/server/plugins/session-validate.js +13 -0
  123. package/dist/runtime/server/routes/auth/login.post.js +20 -10
  124. package/dist/runtime/server/utils/graphql-depth.js +12 -5
  125. package/dist/runtime/server/utils/graphql.js +2 -2
  126. package/dist/runtime/server/utils/media-folders.js +10 -2
  127. package/dist/runtime/server/utils/media.d.ts +1 -0
  128. package/dist/runtime/server/utils/media.js +4 -1
  129. package/dist/runtime/server/utils/migrate.js +1 -1
  130. package/dist/runtime/server/utils/password.d.ts +3 -0
  131. package/dist/runtime/server/utils/password.js +31 -0
  132. package/dist/runtime/server/utils/require-admin.d.ts +4 -2
  133. package/dist/runtime/server/utils/require-admin.js +32 -28
  134. package/dist/runtime/server/utils/same-origin.d.ts +2 -0
  135. package/dist/runtime/server/utils/same-origin.js +24 -0
  136. package/dist/runtime/server/utils/settings.d.ts +16 -0
  137. package/dist/runtime/server/utils/settings.js +31 -0
  138. package/dist/runtime/server/utils/users.d.ts +15 -0
  139. package/dist/runtime/server/utils/users.js +34 -0
  140. package/dist/runtime/shared/index.d.ts +13 -1
  141. package/dist/runtime/shared/index.js +2 -0
  142. package/dist/runtime/shared/layout.d.ts +33 -0
  143. package/dist/runtime/shared/layout.js +127 -0
  144. package/dist/runtime/shared/users.d.ts +18 -0
  145. package/dist/runtime/shared/users.js +1 -0
  146. package/dist/runtime/shared/validation.js +5 -4
  147. package/package.json +3 -3
@@ -1,6 +1,13 @@
1
1
  <template>
2
2
  <div class="cms-page" :class="{ 'is-fill': config.kind === 'collection' && !error }">
3
- <CmsPageHeader :title="config.label">
3
+ <CmsPageHeader
4
+ :title="config.label"
5
+ :icon="kindMeta.icon"
6
+ :description="kindMeta.description"
7
+ >
8
+ <template v-if="config.kind !== 'single' && total" #badge>
9
+ <span :key="total" class="cms-count">{{ total }}</span>
10
+ </template>
4
11
  <CmsButton
5
12
  v-if="config.kind === 'single'"
6
13
  type="submit"
@@ -25,6 +32,7 @@
25
32
  v-model="formState"
26
33
  :fields="config.fields"
27
34
  :tabs="config.tabs"
35
+ :entry-name="name"
28
36
  :form-id="FORM_ID"
29
37
  :loading="saving"
30
38
  @submit="saveSingle"
@@ -42,27 +50,33 @@
42
50
  />
43
51
  </div>
44
52
 
45
- <div v-if="pageTree.length" class="cms-card divide-y divide-(--cms-line)">
53
+ <div v-if="pageTree.length" class="cms-card cms-tree">
46
54
  <NuxtLink
47
- v-for="node in pageTree"
55
+ v-for="(node, index) in pageTree"
48
56
  :key="String(node.row.id)"
49
57
  :to="`/cms/${name}/${node.row.id}`"
50
58
  class="cms-tree-row"
51
- :style="{ paddingLeft: `${1 + node.depth * 1.5}rem` }"
59
+ :style="{
60
+ paddingLeft: `${1 + node.depth * 1.5}rem`,
61
+ '--cms-row-delay': `${Math.min(index, 14) * 24}ms`
62
+ }"
52
63
  >
53
64
  <CmsIcon
54
65
  v-if="node.depth"
55
66
  name="arrow-turn-left-up"
56
67
  class="cms-tree-branch size-3.5"
57
68
  />
69
+ <span class="cms-tree-doc">
70
+ <CmsIcon name="document-text" class="size-4" />
71
+ </span>
58
72
  <div class="min-w-0 flex-1">
59
- <div class="truncate font-medium text-(--ui-text-highlighted)">
73
+ <div class="cms-tree-title">
60
74
  {{ node.row.label }}
61
75
  </div>
62
- <div class="cms-label truncate">{{ node.row.path }}</div>
76
+ <div class="cms-tree-path">{{ node.row.path }}</div>
63
77
  </div>
64
- <span class="cms-label shrink-0">{{ lastEdit(node.row.updatedAt) }}</span>
65
- <CmsIcon name="chevron-right" class="size-4 shrink-0 text-(--ui-text-dimmed)" />
78
+ <span class="cms-tree-meta">{{ lastEdit(node.row.updatedAt) }}</span>
79
+ <CmsIcon name="chevron-right" class="cms-tree-chevron size-4" />
66
80
  </NuxtLink>
67
81
  </div>
68
82
 
@@ -89,22 +103,54 @@
89
103
  placeholder="Search…"
90
104
  class="flex-1"
91
105
  />
92
- <div class="cms-toolbar-actions">
93
- <CmsDropdownMenu v-if="rows.length" :items="columnItems" :content="{ align: 'end' }">
94
- <CmsButton label="Columns" icon="view-columns" variant="soft" />
95
- </CmsDropdownMenu>
106
+ <div v-if="selected.length" :key="'bulk'" class="cms-toolbar-actions cms-bulk-actions">
107
+ <span class="cms-bulk-count">{{ selected.length }} selected</span>
108
+ <CmsButton
109
+ icon="x-mark"
110
+ variant="ghost"
111
+ color="neutral"
112
+ size="sm"
113
+ aria-label="Clear selection"
114
+ title="Clear selection"
115
+ @click="selected = []"
116
+ />
117
+ <template v-if="drafts">
118
+ <CmsButton
119
+ label="Draft"
120
+ icon="pencil-square"
121
+ variant="soft"
122
+ :disabled="saving"
123
+ @click="bulkStatus('draft')"
124
+ />
125
+ <CmsButton
126
+ label="Publish"
127
+ icon="check-circle"
128
+ variant="soft"
129
+ :disabled="saving"
130
+ @click="bulkStatus('published')"
131
+ />
132
+ </template>
133
+ <CmsButton
134
+ label="Delete"
135
+ icon="trash"
136
+ color="error"
137
+ :loading="saving"
138
+ @click="bulkDelete"
139
+ />
140
+ </div>
141
+ <div v-else :key="'default'" class="cms-toolbar-actions">
96
142
  <CmsButton label="New entry" icon="plus" @click="openCreate" />
97
143
  </div>
98
144
  </div>
99
145
 
100
146
  <CmsTable
101
147
  v-if="rows.length"
102
- v-model:column-visibility="columnVisibility"
148
+ v-model:selected="selected"
149
+ selectable
103
150
  v-model:sort="sort"
104
151
  :data="rows"
105
152
  :columns="columns"
106
153
  @select="onSelect"
107
- @reorder="reorderColumns"
108
154
  >
109
155
  <template v-for="key in mediaKeys" #[`${key}-cell`]="{ row }" :key="key">
110
156
  <CmsMediaThumb :value="mediaThumbValue(key, row.original[key])" />
@@ -112,17 +158,6 @@
112
158
  <template v-if="drafts" #status-cell="{ row }">
113
159
  <CmsStatusBadge :published="row.original.status === 'published'" />
114
160
  </template>
115
- <template #actions-cell="{ row }">
116
- <div class="cms-table-cell-actions">
117
- <CmsButton
118
- icon="trash"
119
- variant="ghost"
120
- color="error"
121
- size="xs"
122
- @click.stop="deleteRow(row.original)"
123
- />
124
- </div>
125
- </template>
126
161
  </CmsTable>
127
162
 
128
163
  <CmsSpinner v-else-if="status === 'pending'" />
@@ -178,6 +213,7 @@ import {
178
213
  import cmsConfig from "#cms-config";
179
214
  import { useCmsConfirm } from "../composables/cms-confirm";
180
215
  import { useCmsRuntime } from "../composables/cms-runtime";
216
+ import { entryListColumns, useCmsSettingsState } from "../composables/cms-settings";
181
217
  import { useCmsToast } from "../composables/cms-toast";
182
218
  import { cmsApi } from "../utils/api";
183
219
  import { errorMessage } from "../utils/ui";
@@ -195,6 +231,12 @@ const config = cmsConfig[name];
195
231
  if (!config) {
196
232
  throw createError({ statusCode: 404, statusMessage: "Unknown collection", fatal: true });
197
233
  }
234
+ const KIND_META = {
235
+ collection: { icon: "square-3-stack-3d", description: "Collection of entries." },
236
+ single: { icon: "document-text", description: "Single entry, edited in place." },
237
+ page: { icon: "window", description: "Content for the routes of your app." }
238
+ };
239
+ const kindMeta = { ...KIND_META[config.kind], icon: config.icon ?? KIND_META[config.kind].icon };
198
240
  const fieldKeys = Object.keys(config.fields);
199
241
  const mediaKeys = fieldKeys.filter((key) => config.fields[key].type === "media");
200
242
  const drafts = config.kind === "collection" && !!config.drafts;
@@ -347,82 +389,20 @@ function displayValue(field, key, value) {
347
389
  return truncate(String(value));
348
390
  }
349
391
  }
350
- const columnOrder = ref([...fieldKeys]);
351
- const orderedKeys = computed(() => {
352
- const known = columnOrder.value.filter((key) => fieldKeys.includes(key));
353
- return [...known, ...fieldKeys.filter((key) => !known.includes(key))];
354
- });
355
392
  function isSortable(field) {
356
393
  if (field.type === "blocks") return false;
357
394
  return !(field.type === "relation" && field.cardinality === "many-to-many");
358
395
  }
359
- const columns = computed(() => [
360
- ...orderedKeys.value.map((key) => ({
361
- id: key,
362
- accessorFn: (row) => displayValue(config.fields[key], key, row[key]),
363
- header: config.fields[key].label,
364
- reorderable: true,
365
- sortable: isSortable(config.fields[key])
366
- })),
367
- ...drafts ? [{ accessorKey: "status", header: "Status", sortable: true }] : [],
368
- { id: "actions", header: "" }
369
- ]);
370
- function reorderColumns(from, to) {
371
- const next = [...orderedKeys.value];
372
- const fromIndex = next.indexOf(from);
373
- const toIndex = next.indexOf(to);
374
- if (fromIndex === -1 || toIndex === -1) return;
375
- next.splice(toIndex, 0, ...next.splice(fromIndex, 1));
376
- columnOrder.value = next;
377
- }
378
- const DEFAULT_VISIBLE_COLUMNS = 4;
379
- const columnStorageKey = `cms:columns:${name}`;
380
- const columnOrderStorageKey = `cms:column-order:${name}`;
381
- const columnVisibility = ref(
382
- Object.fromEntries(fieldKeys.map((key, index) => [key, index < DEFAULT_VISIBLE_COLUMNS]))
383
- );
384
- function readStored(key) {
385
- const stored = localStorage.getItem(key);
386
- if (!stored) return void 0;
387
- try {
388
- return JSON.parse(stored);
389
- } catch {
390
- return void 0;
391
- }
392
- }
393
- onMounted(() => {
394
- const storedVisibility = readStored(columnStorageKey);
395
- if (storedVisibility) {
396
- columnVisibility.value = Object.fromEntries(
397
- fieldKeys.map((key) => [
398
- key,
399
- storedVisibility[key] ?? columnVisibility.value[key] ?? false
400
- ])
401
- );
402
- }
403
- const storedOrder = readStored(columnOrderStorageKey);
404
- if (Array.isArray(storedOrder)) {
405
- columnOrder.value = storedOrder.filter((key) => typeof key === "string");
406
- }
407
- watch(
408
- columnVisibility,
409
- (value) => localStorage.setItem(columnStorageKey, JSON.stringify(value)),
410
- { deep: true }
411
- );
412
- watch(orderedKeys, (value) => localStorage.setItem(columnOrderStorageKey, JSON.stringify(value)));
413
- });
414
- const columnItems = computed(
415
- () => orderedKeys.value.map((key) => ({
416
- label: config.fields[key].label,
417
- type: "checkbox",
418
- checked: columnVisibility.value[key],
419
- onUpdateChecked(checked) {
420
- columnVisibility.value = { ...columnVisibility.value, [key]: checked };
421
- },
422
- onSelect(event) {
423
- event.preventDefault();
396
+ const settings = useCmsSettingsState();
397
+ const columns = computed(
398
+ () => entryListColumns(name, settings.value).map(
399
+ (key) => key === "status" && !config.fields.status ? { accessorKey: "status", header: "Status", sortable: true } : {
400
+ id: key,
401
+ accessorFn: (row) => displayValue(config.fields[key], key, row[key]),
402
+ header: config.fields[key].label,
403
+ sortable: isSortable(config.fields[key])
424
404
  }
425
- }))
405
+ )
426
406
  );
427
407
  const drawerOpen = ref(false);
428
408
  const drawerEntryId = ref(null);
@@ -439,10 +419,56 @@ async function onDrawerDeleted() {
439
419
  else await refresh();
440
420
  }
441
421
  const confirmAction = useCmsConfirm();
442
- async function deleteRow(row) {
443
- if (!await confirmAction("Delete this row?")) return;
444
- const lastOnPage = rows.value.length === 1 && page.value > 1;
445
- const ok = await submit(() => cmsApi(`${endpoint}/${row.id}`, { method: "DELETE" }));
446
- if (ok && lastOnPage) page.value -= 1;
422
+ const selected = ref([]);
423
+ watch(rows, (current) => {
424
+ const ids = new Set(current.map((row) => String(row.id)));
425
+ selected.value = selected.value.filter((id) => ids.has(id));
426
+ });
427
+ function entriesLabel(count) {
428
+ return `${count} ${count === 1 ? "entry" : "entries"}`;
429
+ }
430
+ async function runBulk(ids, action) {
431
+ saving.value = true;
432
+ try {
433
+ const results = await Promise.allSettled(ids.map(action));
434
+ const failed = results.flatMap(
435
+ (result, index) => result.status === "rejected" ? [{ id: ids[index], reason: result.reason }] : []
436
+ );
437
+ await refresh();
438
+ selected.value = failed.map((f) => f.id);
439
+ return failed;
440
+ } finally {
441
+ saving.value = false;
442
+ }
443
+ }
444
+ function reportBulk(done, total2, failed) {
445
+ const succeeded = total2 - failed.length;
446
+ if (succeeded) toast.add({ title: `${done}: ${entriesLabel(succeeded)}`, color: "success" });
447
+ if (failed.length) {
448
+ toast.add({
449
+ title: `${entriesLabel(failed.length)} not changed`,
450
+ description: errorMessage(failed[0].reason),
451
+ color: "error"
452
+ });
453
+ }
454
+ }
455
+ async function bulkDelete() {
456
+ const ids = [...selected.value];
457
+ if (!ids.length || saving.value) return;
458
+ if (!await confirmAction(`Delete ${entriesLabel(ids.length)}?`)) return;
459
+ const emptiesPage = ids.length >= rows.value.length && page.value > 1;
460
+ const failed = await runBulk(ids, (id) => cmsApi(`${endpoint}/${id}`, { method: "DELETE" }));
461
+ reportBulk("Deleted", ids.length, failed);
462
+ if (!failed.length && emptiesPage) page.value -= 1;
463
+ }
464
+ async function bulkStatus(status2) {
465
+ const ids = [...selected.value];
466
+ if (!ids.length || saving.value) return;
467
+ const failed = await runBulk(ids, async (id) => {
468
+ const entry = await cmsApi(`${endpoint}/${id}`);
469
+ const body = Object.fromEntries(formKeys.map((key) => [key, entry[key] ?? null]));
470
+ return cmsApi(`${endpoint}/${id}`, { method: "PUT", body: { ...body, status: status2 } });
471
+ });
472
+ reportBulk(status2 === "published" ? "Published" : "Moved to draft", ids.length, failed);
447
473
  }
448
474
  </script>
@@ -23,6 +23,7 @@
23
23
  v-model="formState"
24
24
  :fields="fields"
25
25
  :tabs="config.tabs"
26
+ :entry-name="name"
26
27
  :drafts="drafts"
27
28
  :form-id="FORM_ID"
28
29
  :loading="saving"
@@ -153,6 +154,7 @@ async function save() {
153
154
  toast.add({ title: "Saved", color: "success" });
154
155
  goBack();
155
156
  } catch (error) {
157
+ revertStatus();
156
158
  toast.add({
157
159
  title: "Save failed",
158
160
  description: errorMessage(error),
@@ -1,8 +1,9 @@
1
1
  <template>
2
- <div class="cms-scope cms-canvas cms-auth">
2
+ <div class="cms-scope cms-canvas cms-auth" :data-theme="theme">
3
3
  <div class="cms-auth-panel cms-rise">
4
4
  <header class="cms-auth-header">
5
- <h1 class="cms-title cms-title-lg">Welcome back<span class="cms-accent">.</span></h1>
5
+ <CmsBrandMark size="lg" />
6
+ <h1 class="cms-title cms-title-lg">Welcome back</h1>
6
7
  <p class="cms-subtitle">Sign in to manage your content.</p>
7
8
  </header>
8
9
 
@@ -13,6 +14,7 @@
13
14
  <CmsInput
14
15
  v-model="state.email"
15
16
  type="email"
17
+ icon="envelope"
16
18
  autocomplete="username"
17
19
  placeholder="you@example.com"
18
20
  />
@@ -21,13 +23,24 @@
21
23
  <CmsInput
22
24
  v-model="state.password"
23
25
  type="password"
26
+ icon="key"
24
27
  autocomplete="current-password"
25
28
  placeholder="••••••••"
26
29
  />
27
30
  </CmsFormField>
28
- <CmsButton type="submit" label="Sign in" block :loading="loading" />
31
+ <CmsButton
32
+ type="submit"
33
+ label="Sign in"
34
+ icon="arrow-right"
35
+ trailing-icon
36
+ size="lg"
37
+ block
38
+ :loading="loading"
39
+ />
29
40
  </CmsForm>
30
41
  </div>
42
+
43
+ <p class="cms-auth-footer">Powered by nuxt-cms</p>
31
44
  </div>
32
45
  </div>
33
46
  </template>
@@ -35,7 +48,9 @@
35
48
  <script setup>
36
49
  import { definePageMeta, navigateTo, ref, useUserSession } from "#imports";
37
50
  import { errorMessage } from "../utils/ui";
51
+ import { useCmsTheme } from "../composables/cms-theme";
38
52
  definePageMeta({ layout: false });
53
+ const theme = useCmsTheme();
39
54
  const { loggedIn } = useUserSession();
40
55
  if (loggedIn.value) {
41
56
  await navigateTo("/cms", { replace: true });
@@ -1,6 +1,10 @@
1
1
  <template>
2
2
  <div class="cms-page">
3
- <CmsPageHeader title="Media">
3
+ <CmsPageHeader
4
+ title="Media"
5
+ icon="photo"
6
+ description="Images, videos and files used across your content."
7
+ >
4
8
  <template #badge>
5
9
  <span class="cms-badge is-muted">{{ runtime.mediaStorage }}</span>
6
10
  </template>
@@ -1,5 +1,7 @@
1
1
  import type { InjectionKey, Reactive } from 'vue';
2
+ import type { FieldConfig } from '#nuxt-cms';
2
3
  /** Shared reactive map of field-name -> error message, provided by CmsForm and
3
4
  * read by CmsFormField to render inline validation errors. */
4
5
  export declare const CMS_FORM_ERRORS: InjectionKey<Reactive<Record<string, string>>>;
5
6
  export declare function errorMessage(error: unknown): string | undefined;
7
+ export declare function fieldIcon(field: FieldConfig): string;
@@ -3,3 +3,21 @@ export function errorMessage(error) {
3
3
  const err = error;
4
4
  return err.data?.message ?? err.message;
5
5
  }
6
+ const FIELD_ICONS = {
7
+ text: "bars-3-bottom-left",
8
+ richtext: "document-text",
9
+ number: "hashtag",
10
+ boolean: "check-circle",
11
+ date: "calendar",
12
+ email: "at-symbol",
13
+ slug: "link",
14
+ select: "chevron-up-down",
15
+ json: "code-bracket",
16
+ media: "photo",
17
+ relation: "arrows-right-left",
18
+ blocks: "squares-2x2"
19
+ };
20
+ export function fieldIcon(field) {
21
+ if (field.type === "select" && field.multiple) return "list-bullet";
22
+ return FIELD_ICONS[field.type];
23
+ }