@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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +133 -14
- package/dist/runtime/app/components/cms/BlocksField.vue +86 -10
- package/dist/runtime/app/components/cms/BrandMark.d.vue.ts +6 -0
- package/dist/runtime/app/components/cms/BrandMark.vue +19 -0
- package/dist/runtime/app/components/cms/BrandMark.vue.d.ts +6 -0
- package/dist/runtime/app/components/cms/Checkbox.d.vue.ts +11 -0
- package/dist/runtime/app/components/cms/Checkbox.vue +21 -0
- package/dist/runtime/app/components/cms/Checkbox.vue.d.ts +11 -0
- package/dist/runtime/app/components/cms/Drawer.vue +5 -9
- package/dist/runtime/app/components/cms/EmptyState.vue +4 -2
- package/dist/runtime/app/components/cms/EntryDrawer.vue +12 -3
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +104 -30
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +20 -5
- package/dist/runtime/app/components/cms/FormField.d.vue.ts +5 -3
- package/dist/runtime/app/components/cms/FormField.vue +8 -2
- package/dist/runtime/app/components/cms/FormField.vue.d.ts +5 -3
- package/dist/runtime/app/components/cms/Icon.vue +1 -1
- package/dist/runtime/app/components/cms/Input.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/Input.vue +7 -4
- package/dist/runtime/app/components/cms/Input.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/LocaleSwitch.vue +1 -1
- package/dist/runtime/app/components/cms/MediaFolderPicker.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue +1 -3
- package/dist/runtime/app/components/cms/MediaFolderPicker.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/MediaGallery.vue +24 -10
- package/dist/runtime/app/components/cms/MediaUpload.d.vue.ts +1 -0
- package/dist/runtime/app/components/cms/MediaUpload.vue +71 -41
- package/dist/runtime/app/components/cms/MediaUpload.vue.d.ts +1 -0
- package/dist/runtime/app/components/cms/Modal.vue +5 -9
- package/dist/runtime/app/components/cms/PageHeader.d.vue.ts +5 -3
- package/dist/runtime/app/components/cms/PageHeader.vue +16 -6
- package/dist/runtime/app/components/cms/PageHeader.vue.d.ts +5 -3
- package/dist/runtime/app/components/cms/PasswordInput.d.vue.ts +17 -0
- package/dist/runtime/app/components/cms/PasswordInput.vue +81 -0
- package/dist/runtime/app/components/cms/PasswordInput.vue.d.ts +17 -0
- package/dist/runtime/app/components/cms/RelationField.vue +7 -2
- package/dist/runtime/app/components/cms/SelectMenu.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/SelectMenu.vue +4 -11
- package/dist/runtime/app/components/cms/SelectMenu.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/SettingsAccount.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsAccount.vue +138 -0
- package/dist/runtime/app/components/cms/SettingsAccount.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsColumns.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsColumns.vue +102 -0
- package/dist/runtime/app/components/cms/SettingsColumns.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsEntry.d.vue.ts +6 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue +126 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue.d.ts +6 -0
- package/dist/runtime/app/components/cms/SettingsLayout.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsLayout.vue +177 -0
- package/dist/runtime/app/components/cms/SettingsLayout.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsModal.d.vue.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsModal.vue +174 -0
- package/dist/runtime/app/components/cms/SettingsModal.vue.d.ts +16 -0
- package/dist/runtime/app/components/cms/SettingsUsers.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/SettingsUsers.vue +274 -0
- package/dist/runtime/app/components/cms/SettingsUsers.vue.d.ts +3 -0
- package/dist/runtime/app/components/cms/Spinner.vue +2 -2
- package/dist/runtime/app/components/cms/Table.d.vue.ts +6 -8
- package/dist/runtime/app/components/cms/Table.vue +42 -47
- package/dist/runtime/app/components/cms/Table.vue.d.ts +6 -8
- package/dist/runtime/app/components/cms/Textarea.d.vue.ts +0 -1
- package/dist/runtime/app/components/cms/Textarea.vue +1 -2
- package/dist/runtime/app/components/cms/Textarea.vue.d.ts +0 -1
- package/dist/runtime/app/components/cms/Toaster.vue +1 -1
- package/dist/runtime/app/components/cms/WelcomeModal.d.vue.ts +3 -0
- package/dist/runtime/app/components/cms/WelcomeModal.vue +69 -0
- package/dist/runtime/app/components/cms/WelcomeModal.vue.d.ts +3 -0
- package/dist/runtime/app/composables/cms-account.d.ts +11 -0
- package/dist/runtime/app/composables/cms-account.js +23 -0
- package/dist/runtime/app/composables/cms-entry-disabled.js +1 -1
- package/dist/runtime/app/composables/cms-media-keys.d.ts +1 -0
- package/dist/runtime/app/composables/cms-media-keys.js +5 -0
- package/dist/runtime/app/composables/cms-media-library.js +3 -0
- package/dist/runtime/app/composables/cms-media-uploader.d.ts +11 -2
- package/dist/runtime/app/composables/cms-media-uploader.js +139 -12
- package/dist/runtime/app/composables/cms-overlay.d.ts +4 -0
- package/dist/runtime/app/composables/cms-overlay.js +23 -0
- package/dist/runtime/app/composables/cms-settings.d.ts +12 -0
- package/dist/runtime/app/composables/cms-settings.js +59 -0
- package/dist/runtime/app/composables/cms-theme.d.ts +9 -0
- package/dist/runtime/app/composables/cms-theme.js +27 -0
- package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
- package/dist/runtime/app/layouts/cms-admin.vue +71 -17
- package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
- package/dist/runtime/app/pages/admin-collection.vue +127 -101
- package/dist/runtime/app/pages/admin-entry.vue +2 -0
- package/dist/runtime/app/pages/admin-login.vue +18 -3
- package/dist/runtime/app/pages/admin-media.vue +5 -1
- package/dist/runtime/app/utils/ui.d.ts +2 -0
- package/dist/runtime/app/utils/ui.js +18 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/generate.js +1 -1
- package/dist/runtime/server/api/account-password.put.d.ts +4 -0
- package/dist/runtime/server/api/account-password.put.js +38 -0
- package/dist/runtime/server/api/account-welcome.post.d.ts +4 -0
- package/dist/runtime/server/api/account-welcome.post.js +14 -0
- package/dist/runtime/server/api/account.put.d.ts +3 -0
- package/dist/runtime/server/api/account.put.js +25 -0
- package/dist/runtime/server/api/collection.get.js +1 -1
- package/dist/runtime/server/api/media.delete.js +2 -1
- package/dist/runtime/server/api/media.post.js +2 -1
- package/dist/runtime/server/api/media.put.js +2 -1
- package/dist/runtime/server/api/settings.delete.d.ts +4 -0
- package/dist/runtime/server/api/settings.delete.js +12 -0
- package/dist/runtime/server/api/settings.get.d.ts +3 -0
- package/dist/runtime/server/api/settings.get.js +15 -0
- package/dist/runtime/server/api/settings.put.d.ts +3 -0
- package/dist/runtime/server/api/settings.put.js +14 -0
- package/dist/runtime/server/api/user.delete.d.ts +4 -0
- package/dist/runtime/server/api/user.delete.js +12 -0
- package/dist/runtime/server/api/user.put.d.ts +3 -0
- package/dist/runtime/server/api/user.put.js +27 -0
- package/dist/runtime/server/api/users.get.d.ts +3 -0
- package/dist/runtime/server/api/users.get.js +11 -0
- package/dist/runtime/server/api/users.post.d.ts +3 -0
- package/dist/runtime/server/api/users.post.js +36 -0
- package/dist/runtime/server/plugins/session-validate.d.ts +2 -0
- package/dist/runtime/server/plugins/session-validate.js +13 -0
- package/dist/runtime/server/routes/auth/login.post.js +20 -10
- package/dist/runtime/server/utils/graphql-depth.js +12 -5
- package/dist/runtime/server/utils/graphql.js +2 -2
- package/dist/runtime/server/utils/media-folders.js +10 -2
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +4 -1
- package/dist/runtime/server/utils/migrate.js +1 -1
- package/dist/runtime/server/utils/password.d.ts +3 -0
- package/dist/runtime/server/utils/password.js +31 -0
- package/dist/runtime/server/utils/require-admin.d.ts +4 -2
- package/dist/runtime/server/utils/require-admin.js +32 -28
- package/dist/runtime/server/utils/same-origin.d.ts +2 -0
- package/dist/runtime/server/utils/same-origin.js +24 -0
- package/dist/runtime/server/utils/settings.d.ts +16 -0
- package/dist/runtime/server/utils/settings.js +31 -0
- package/dist/runtime/server/utils/users.d.ts +15 -0
- package/dist/runtime/server/utils/users.js +34 -0
- package/dist/runtime/shared/index.d.ts +13 -1
- package/dist/runtime/shared/index.js +2 -0
- package/dist/runtime/shared/layout.d.ts +33 -0
- package/dist/runtime/shared/layout.js +127 -0
- package/dist/runtime/shared/users.d.ts +18 -0
- package/dist/runtime/shared/users.js +1 -0
- package/dist/runtime/shared/validation.js +5 -4
- package/package.json +3 -3
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { minifyIntrospection, outputIntrospectionFile } from 'gql.tada/internal'
|
|
|
11
11
|
import { createJiti } from 'jiti';
|
|
12
12
|
import { typeName, blockTypeName, blocksFieldTypeName, renderGraphqlSdl } from '../dist/runtime/shared/graphql-sdl.js';
|
|
13
13
|
import { migrationsDirFor } from '../dist/runtime/shared/migrations-dir.js';
|
|
14
|
-
import { pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, mobileMediaKey, fieldConditions, pageFieldsTableName, pageMediaTableName, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, expandMobileMedia } from '../dist/runtime/shared/index.js';
|
|
14
|
+
import { pageColumnFields, isTranslatableField, isTranslatableMediaField, isMultiSelect, pageAllFields, PAGE_PATH_FIELD, pageRoutes, layoutErrors, listErrors, mobileMediaKey, fieldConditions, pageFieldsTableName, pageMediaTableName, isRequiredField, pageFields, isPrivateField, entryFieldsFor, mediaTypeFilter, DEFAULT_MEDIA_MAX_FILE_SIZE, expandMobileMedia } from '../dist/runtime/shared/index.js';
|
|
15
15
|
import { scanMediaDirectory, readMediaFileMeta } from '../dist/runtime/server/utils/media-sync.js';
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
17
|
import { readFile } from 'node:fs/promises';
|
|
@@ -80,7 +80,17 @@ function renderMigrationsTypes(typesPath) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const IDENTIFIER = /^[a-z_]\w*$/i;
|
|
83
|
-
const RESERVED_ENTRY_KEYS = [
|
|
83
|
+
const RESERVED_ENTRY_KEYS = [
|
|
84
|
+
"admin",
|
|
85
|
+
"auth",
|
|
86
|
+
"login",
|
|
87
|
+
"media",
|
|
88
|
+
"graphql",
|
|
89
|
+
"settings",
|
|
90
|
+
"cms_media",
|
|
91
|
+
"cms_settings",
|
|
92
|
+
"cms_users"
|
|
93
|
+
];
|
|
84
94
|
const RESERVED_COLUMNS = ["id", "status", "created_at", "updated_at"];
|
|
85
95
|
const TITLE_FIELD_TYPES = ["text", "slug", "email", "number", "date", "select"];
|
|
86
96
|
const TRANSLATABLE_FIELD_TYPES = ["text", "richtext", "media"];
|
|
@@ -225,6 +235,12 @@ function validateConfig(config, i18n) {
|
|
|
225
235
|
}
|
|
226
236
|
}
|
|
227
237
|
const allFields = entry.kind === "page" ? pageAllFields(entry) : entry.fields ?? {};
|
|
238
|
+
errors.push(...layoutErrors(at, allFields, entry.layout));
|
|
239
|
+
errors.push(...listErrors(at, allFields, entry.list, entry.drafts ? ["status"] : []));
|
|
240
|
+
if (entry.list && entry.kind !== "collection")
|
|
241
|
+
errors.push(`${at}: list needs kind 'collection'`);
|
|
242
|
+
if (entry.icon !== void 0 && (typeof entry.icon !== "string" || !entry.icon))
|
|
243
|
+
errors.push(`${at}: icon must be a non-empty string`);
|
|
228
244
|
const columnNames = /* @__PURE__ */ new Set();
|
|
229
245
|
for (const [key, field] of Object.entries(allFields)) {
|
|
230
246
|
const fat = `${at}, field '${key}'`;
|
|
@@ -364,7 +380,7 @@ function validateConfig(config, i18n) {
|
|
|
364
380
|
errors.push(`${fat}: relation target '${field.to}' must be a collection`);
|
|
365
381
|
if (!entry.table && target.table)
|
|
366
382
|
errors.push(
|
|
367
|
-
`${fat}: relation target '${field.to}' uses a custom table
|
|
383
|
+
`${fat}: relation target '${field.to}' uses a custom table: derived relations can only target derived entries`
|
|
368
384
|
);
|
|
369
385
|
}
|
|
370
386
|
if (field.cardinality && !["many-to-one", "one-to-one", "many-to-many"].includes(field.cardinality)) {
|
|
@@ -383,7 +399,11 @@ function validateConfig(config, i18n) {
|
|
|
383
399
|
}
|
|
384
400
|
}
|
|
385
401
|
}
|
|
386
|
-
const sqlTableNames = /* @__PURE__ */ new Map([
|
|
402
|
+
const sqlTableNames = /* @__PURE__ */ new Map([
|
|
403
|
+
["cms_media", "the built-in media table"],
|
|
404
|
+
["cms_settings", "the built-in settings table"],
|
|
405
|
+
["cms_users", "the built-in users table"]
|
|
406
|
+
]);
|
|
387
407
|
for (const [name, entry] of Object.entries(config)) {
|
|
388
408
|
if (entry.table) continue;
|
|
389
409
|
const registerTable = (sqlName, source) => {
|
|
@@ -535,6 +555,30 @@ function pageRowsTableExprs(name, dialect) {
|
|
|
535
555
|
rowsTable(pageMediaTableName(name), `mediaKey: text('media_key').notNull()`)
|
|
536
556
|
];
|
|
537
557
|
}
|
|
558
|
+
function settingsTableExpr(dialect) {
|
|
559
|
+
const pg = dialect === "postgres";
|
|
560
|
+
const tableFn = pg ? "pgTable" : "sqliteTable";
|
|
561
|
+
return `export const cms_settings = ${tableFn}('cms_settings', {
|
|
562
|
+
key: text('key').primaryKey(),
|
|
563
|
+
value: text('value').notNull(),
|
|
564
|
+
updatedAt: ${nowExpr(pg, "updated_at")},
|
|
565
|
+
})`;
|
|
566
|
+
}
|
|
567
|
+
function usersTableExpr(dialect) {
|
|
568
|
+
const pg = dialect === "postgres";
|
|
569
|
+
const tableFn = pg ? "pgTable" : "sqliteTable";
|
|
570
|
+
return `export const cms_users = ${tableFn}('cms_users', {
|
|
571
|
+
id: text('id').primaryKey(),
|
|
572
|
+
email: text('email').notNull().unique(),
|
|
573
|
+
name: text('name'),
|
|
574
|
+
role: text('role').notNull().default('admin'),
|
|
575
|
+
passwordHash: text('password_hash').notNull(),
|
|
576
|
+
mustChangePassword: ${pg ? `boolean('must_change_password')` : `integer('must_change_password', { mode: 'boolean' })`}.notNull().default(true),
|
|
577
|
+
lastLoginAt: text('last_login_at'),
|
|
578
|
+
createdAt: ${nowExpr(pg, "created_at")},
|
|
579
|
+
updatedAt: ${nowExpr(pg, "updated_at")},
|
|
580
|
+
})`;
|
|
581
|
+
}
|
|
538
582
|
function mediaTableExpr(dialect) {
|
|
539
583
|
const pg = dialect === "postgres";
|
|
540
584
|
const tableFn = pg ? "pgTable" : "sqliteTable";
|
|
@@ -561,7 +605,7 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
|
|
|
561
605
|
if (fields.some((f) => f.type === "number" && !f.integer))
|
|
562
606
|
core.add(pg ? "doublePrecision" : "real");
|
|
563
607
|
if (pg && fields.some((f) => f.type === "date")) core.add("date");
|
|
564
|
-
if (pg
|
|
608
|
+
if (pg) core.add("boolean");
|
|
565
609
|
if (pg && fields.some(
|
|
566
610
|
(f) => f.type === "json" || f.type === "blocks" || isTranslatableField(f) && !isTranslatableMediaField(f) || isMultiSelect(f)
|
|
567
611
|
))
|
|
@@ -581,9 +625,14 @@ function renderSchemaFile(config, dialect, resolveImport = (s) => s) {
|
|
|
581
625
|
const rows = rowsEntries.flatMap(([name]) => pageRowsTableExprs(name, dialect));
|
|
582
626
|
return `${imports.join("\n")}
|
|
583
627
|
|
|
584
|
-
${[
|
|
585
|
-
|
|
586
|
-
|
|
628
|
+
${[
|
|
629
|
+
mediaTableExpr(dialect),
|
|
630
|
+
settingsTableExpr(dialect),
|
|
631
|
+
usersTableExpr(dialect),
|
|
632
|
+
...tables,
|
|
633
|
+
...joins,
|
|
634
|
+
...rows
|
|
635
|
+
].join("\n\n")}
|
|
587
636
|
`;
|
|
588
637
|
}
|
|
589
638
|
|
|
@@ -786,7 +835,12 @@ ${lines.join(
|
|
|
786
835
|
function pageTypesTs(name, entry) {
|
|
787
836
|
const auto = `${typeName(name)}Auto`;
|
|
788
837
|
const lines = pageRoutes(entry).map((route) => {
|
|
789
|
-
const keys = [
|
|
838
|
+
const keys = [
|
|
839
|
+
"id",
|
|
840
|
+
"path",
|
|
841
|
+
"updatedAt",
|
|
842
|
+
...Object.entries(pageFields(entry, route.path)).filter(([, field]) => !isPrivateField(field)).map(([key]) => key)
|
|
843
|
+
];
|
|
790
844
|
return ` ${JSON.stringify(route.path)}: Pick<${auto}, ${quotedKeys(keys)}>`;
|
|
791
845
|
});
|
|
792
846
|
return [
|
|
@@ -978,7 +1032,7 @@ async function loadCmsConfig(nuxt, resolver, configPathOption, i18n, logger) {
|
|
|
978
1032
|
cmsConfig = expandMobileMedia(await jiti.import(configPath, { default: true }));
|
|
979
1033
|
} else {
|
|
980
1034
|
logger.warn(
|
|
981
|
-
`[nuxt-cms] Config file not found: ${configPath}. Using an empty registry
|
|
1035
|
+
`[nuxt-cms] Config file not found: ${configPath}. Using an empty registry. Create a ${configPathOption}.ts with defineCmsConfig().`
|
|
982
1036
|
);
|
|
983
1037
|
nuxt.options.alias["#cms-config"] = resolver.resolve("./runtime/shared/empty-config");
|
|
984
1038
|
}
|
|
@@ -1239,7 +1293,13 @@ const module$1 = defineNuxtModule({
|
|
|
1239
1293
|
` dialect: '${dialect}',`,
|
|
1240
1294
|
` schema: '${toPosix(schemaTemplate.dst)}',`,
|
|
1241
1295
|
` out: '${toPosix(relativeMigrationsDir)}',`,
|
|
1242
|
-
driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ??
|
|
1296
|
+
driver === "postgres" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? ${JSON.stringify(
|
|
1297
|
+
databaseUrl
|
|
1298
|
+
)} },` : driver === "libsql" ? ` dbCredentials: { url: process.env.NUXT_CMS_DATABASE_URL ?? ${JSON.stringify(
|
|
1299
|
+
databaseUrl || `file:${toPosix(resolvedDbPath)}`
|
|
1300
|
+
)}, authToken: (process.env.NUXT_CMS_DATABASE_AUTH_TOKEN ?? ${JSON.stringify(
|
|
1301
|
+
databaseAuthToken
|
|
1302
|
+
)}) || undefined },` : driver === "d1" ? `` : ` dbCredentials: { url: ${JSON.stringify(toPosix(resolvedDbPath))} },`,
|
|
1243
1303
|
`}`,
|
|
1244
1304
|
``
|
|
1245
1305
|
].join("\n")
|
|
@@ -1256,7 +1316,7 @@ const module$1 = defineNuxtModule({
|
|
|
1256
1316
|
const migrations = await collectMigrations(migrationsDir);
|
|
1257
1317
|
if (!migrations && !nuxt.options.dev && Object.keys(cmsConfig).length) {
|
|
1258
1318
|
logger.warn(
|
|
1259
|
-
`[nuxt-cms] No migrations found at ${migrationsDir}. The CMS tables will not be created
|
|
1319
|
+
`[nuxt-cms] No migrations found at ${migrationsDir}. The CMS tables will not be created. Run the dev server once to generate them and commit ${toPosix(
|
|
1260
1320
|
relativeMigrationsDir
|
|
1261
1321
|
)}.`
|
|
1262
1322
|
);
|
|
@@ -1302,6 +1362,7 @@ const module$1 = defineNuxtModule({
|
|
|
1302
1362
|
driver === "postgres" ? "./runtime/server/plugins/migrate-postgres" : driver === "libsql" ? "./runtime/server/plugins/migrate-libsql" : driver === "d1" ? "./runtime/server/plugins/d1-binding" : "./runtime/server/plugins/migrate-sqlite"
|
|
1303
1363
|
)
|
|
1304
1364
|
);
|
|
1365
|
+
addServerPlugin(resolver.resolve("./runtime/server/plugins/session-validate"));
|
|
1305
1366
|
if (!nuxt.options.dev) {
|
|
1306
1367
|
addServerPlugin(resolver.resolve("./runtime/server/plugins/session-check"));
|
|
1307
1368
|
}
|
|
@@ -1382,13 +1443,21 @@ const module$1 = defineNuxtModule({
|
|
|
1382
1443
|
"",
|
|
1383
1444
|
"declare module '#auth-utils' {",
|
|
1384
1445
|
" interface User {",
|
|
1446
|
+
" id?: string",
|
|
1385
1447
|
" email: string",
|
|
1448
|
+
" name?: string | null",
|
|
1449
|
+
" role?: 'superadmin' | 'admin'",
|
|
1450
|
+
" firstLogin?: boolean",
|
|
1451
|
+
" passwordStamp?: string",
|
|
1386
1452
|
" }",
|
|
1387
1453
|
"}",
|
|
1388
1454
|
"",
|
|
1389
1455
|
"declare module '#imports' {",
|
|
1390
1456
|
" function getUserSession(event: H3Event): Promise<{ user?: import('#auth-utils').User }>",
|
|
1391
1457
|
" function setUserSession(event: H3Event, session: { user: import('#auth-utils').User }): Promise<unknown>",
|
|
1458
|
+
" function replaceUserSession(event: H3Event, session: { user: import('#auth-utils').User }): Promise<unknown>",
|
|
1459
|
+
" function clearUserSession(event: H3Event): Promise<boolean>",
|
|
1460
|
+
" const sessionHooks: { hook(name: 'fetch', fn: (session: { user?: import('#auth-utils').User }, event: H3Event) => void | Promise<void>): () => void }",
|
|
1392
1461
|
" function useStorage(base?: string): {",
|
|
1393
1462
|
" getItem<T>(key: string): Promise<T | null>",
|
|
1394
1463
|
" setItem<T>(key: string, value: T, options?: { ttl?: number }): Promise<void>",
|
|
@@ -1406,8 +1475,8 @@ const module$1 = defineNuxtModule({
|
|
|
1406
1475
|
addVitePlugin(tailwindcss());
|
|
1407
1476
|
addVitePlugin(svgLoader({ defaultImport: "url", svgoConfig: { plugins: ["prefixIds"] } }));
|
|
1408
1477
|
nuxt.options.css.push(
|
|
1409
|
-
resolveImport("@fontsource-variable/
|
|
1410
|
-
resolveImport("@fontsource/
|
|
1478
|
+
resolveImport("@fontsource-variable/geist/index.css"),
|
|
1479
|
+
resolveImport("@fontsource-variable/geist-mono/index.css"),
|
|
1411
1480
|
resolver.resolve("./runtime/assets/main.css")
|
|
1412
1481
|
);
|
|
1413
1482
|
nuxt.hook("app:templates", (app) => {
|
|
@@ -1526,6 +1595,56 @@ const module$1 = defineNuxtModule({
|
|
|
1526
1595
|
handler: resolver.resolve("./runtime/server/routes/media-file")
|
|
1527
1596
|
});
|
|
1528
1597
|
}
|
|
1598
|
+
addServerHandler({
|
|
1599
|
+
route: "/api/cms/users",
|
|
1600
|
+
method: "get",
|
|
1601
|
+
handler: resolver.resolve("./runtime/server/api/users.get")
|
|
1602
|
+
});
|
|
1603
|
+
addServerHandler({
|
|
1604
|
+
route: "/api/cms/users",
|
|
1605
|
+
method: "post",
|
|
1606
|
+
handler: resolver.resolve("./runtime/server/api/users.post")
|
|
1607
|
+
});
|
|
1608
|
+
addServerHandler({
|
|
1609
|
+
route: "/api/cms/users/:id",
|
|
1610
|
+
method: "put",
|
|
1611
|
+
handler: resolver.resolve("./runtime/server/api/user.put")
|
|
1612
|
+
});
|
|
1613
|
+
addServerHandler({
|
|
1614
|
+
route: "/api/cms/users/:id",
|
|
1615
|
+
method: "delete",
|
|
1616
|
+
handler: resolver.resolve("./runtime/server/api/user.delete")
|
|
1617
|
+
});
|
|
1618
|
+
addServerHandler({
|
|
1619
|
+
route: "/api/cms/account",
|
|
1620
|
+
method: "put",
|
|
1621
|
+
handler: resolver.resolve("./runtime/server/api/account.put")
|
|
1622
|
+
});
|
|
1623
|
+
addServerHandler({
|
|
1624
|
+
route: "/api/cms/account/password",
|
|
1625
|
+
method: "put",
|
|
1626
|
+
handler: resolver.resolve("./runtime/server/api/account-password.put")
|
|
1627
|
+
});
|
|
1628
|
+
addServerHandler({
|
|
1629
|
+
route: "/api/cms/account/welcome",
|
|
1630
|
+
method: "post",
|
|
1631
|
+
handler: resolver.resolve("./runtime/server/api/account-welcome.post")
|
|
1632
|
+
});
|
|
1633
|
+
addServerHandler({
|
|
1634
|
+
route: "/api/cms/admin/settings",
|
|
1635
|
+
method: "get",
|
|
1636
|
+
handler: resolver.resolve("./runtime/server/api/settings.get")
|
|
1637
|
+
});
|
|
1638
|
+
addServerHandler({
|
|
1639
|
+
route: "/api/cms/admin/settings/:key",
|
|
1640
|
+
method: "put",
|
|
1641
|
+
handler: resolver.resolve("./runtime/server/api/settings.put")
|
|
1642
|
+
});
|
|
1643
|
+
addServerHandler({
|
|
1644
|
+
route: "/api/cms/admin/settings/:key",
|
|
1645
|
+
method: "delete",
|
|
1646
|
+
handler: resolver.resolve("./runtime/server/api/settings.delete")
|
|
1647
|
+
});
|
|
1529
1648
|
const api = "/api/cms/admin/:collection";
|
|
1530
1649
|
addServerHandler({
|
|
1531
1650
|
route: api,
|
|
@@ -9,20 +9,25 @@
|
|
|
9
9
|
'is-muted': isHidden(item),
|
|
10
10
|
'is-dragging': dragIndex === index,
|
|
11
11
|
'is-drop-target': dropIndex === index && dragIndex !== index,
|
|
12
|
-
'is-missing': hasMissingMedia(item)
|
|
12
|
+
'is-missing': hasMissingMedia(item),
|
|
13
|
+
'is-media-only': mediaOnly
|
|
13
14
|
}"
|
|
14
15
|
draggable="true"
|
|
16
|
+
:tabindex="mediaOnly ? 0 : void 0"
|
|
17
|
+
:aria-label="mediaOnly ? `${barTitle(item)}, use the arrow keys to reorder` : void 0"
|
|
15
18
|
@dragstart="onDragStart(index, $event)"
|
|
16
19
|
@dragover.prevent="dropIndex = index"
|
|
17
20
|
@dragleave="onDragLeave(index)"
|
|
18
21
|
@drop.prevent="onDrop(index)"
|
|
19
22
|
@dragend="onDragEnd"
|
|
23
|
+
@keydown.self="onTileKey($event, index)"
|
|
20
24
|
>
|
|
21
|
-
<
|
|
22
|
-
|
|
25
|
+
<component
|
|
26
|
+
:is="mediaOnly ? 'div' : 'button'"
|
|
27
|
+
:type="mediaOnly ? void 0 : 'button'"
|
|
23
28
|
class="cms-block-preview"
|
|
24
|
-
:aria-label="`Edit ${labelOf(item)}`"
|
|
25
|
-
@click="
|
|
29
|
+
:aria-label="mediaOnly ? void 0 : `Edit ${labelOf(item)}`"
|
|
30
|
+
@click="onPreviewClick(index)"
|
|
26
31
|
>
|
|
27
32
|
<img
|
|
28
33
|
v-if="previewOf(item)?.kind === 'image'"
|
|
@@ -43,9 +48,28 @@
|
|
|
43
48
|
<CmsIcon v-else :name="iconOf(item)" class="size-7" />
|
|
44
49
|
|
|
45
50
|
<CmsIcon v-if="isHidden(item)" name="eye-slash" class="cms-block-hidden size-4" />
|
|
46
|
-
</button>
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
<template v-if="mediaOnly">
|
|
53
|
+
<span
|
|
54
|
+
v-if="hasMissingMedia(item)"
|
|
55
|
+
class="cms-block-missing"
|
|
56
|
+
title="Not found in the media library"
|
|
57
|
+
>
|
|
58
|
+
<CmsIcon name="exclamation-triangle" class="size-3.5" />
|
|
59
|
+
</span>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="cms-block-remove"
|
|
63
|
+
:aria-label="`Remove ${barTitle(item)}`"
|
|
64
|
+
title="Remove"
|
|
65
|
+
@click.stop="remove(index)"
|
|
66
|
+
>
|
|
67
|
+
<CmsIcon name="x-mark" class="size-3.5" />
|
|
68
|
+
</button>
|
|
69
|
+
</template>
|
|
70
|
+
</component>
|
|
71
|
+
|
|
72
|
+
<div v-if="!mediaOnly" class="cms-block-bar">
|
|
49
73
|
<button
|
|
50
74
|
type="button"
|
|
51
75
|
class="cms-block-grip"
|
|
@@ -84,10 +108,22 @@
|
|
|
84
108
|
</CmsDropdownMenu>
|
|
85
109
|
<button v-else type="button" class="cms-block-add" @click="addFirst">
|
|
86
110
|
<CmsIcon name="plus" class="size-5" />
|
|
87
|
-
<span>Add</span>
|
|
111
|
+
<span>{{ mediaOnly ? addMediaLabel : "Add" }}</span>
|
|
88
112
|
</button>
|
|
89
113
|
</div>
|
|
90
114
|
|
|
115
|
+
<CmsModal v-if="mediaOnly" v-model:open="pickerOpen" title="Media" size="lg">
|
|
116
|
+
<template #body>
|
|
117
|
+
<CmsMediaGallery
|
|
118
|
+
v-model:folder="pickerFolder"
|
|
119
|
+
selectable
|
|
120
|
+
:media-type="mediaOnly.field.mediaType"
|
|
121
|
+
:accept="mediaOnly.field.accept"
|
|
122
|
+
@select="addMedia"
|
|
123
|
+
/>
|
|
124
|
+
</template>
|
|
125
|
+
</CmsModal>
|
|
126
|
+
|
|
91
127
|
<CmsModal v-model:open="editorOpen" :title="editorTitle">
|
|
92
128
|
<template #body>
|
|
93
129
|
<div class="flex flex-col gap-4">
|
|
@@ -95,6 +131,7 @@
|
|
|
95
131
|
v-for="(blockField, blockKey) in editorFields"
|
|
96
132
|
:key="blockKey"
|
|
97
133
|
:label="blockField.label"
|
|
134
|
+
:icon="fieldIcon(blockField)"
|
|
98
135
|
:required="blockField.required"
|
|
99
136
|
>
|
|
100
137
|
<CmsFieldInput
|
|
@@ -157,9 +194,10 @@ import {
|
|
|
157
194
|
mediaTypeForKey,
|
|
158
195
|
pickTranslatedMedia
|
|
159
196
|
} from "#nuxt-cms";
|
|
160
|
-
import { computed, ref } from "#imports";
|
|
197
|
+
import { computed, ref, useState } from "#imports";
|
|
161
198
|
import { useCmsMediaKeys } from "../../composables/cms-media-keys";
|
|
162
199
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
200
|
+
import { fieldIcon } from "../../utils/ui";
|
|
163
201
|
const props = defineProps({
|
|
164
202
|
field: { type: Object, required: true },
|
|
165
203
|
locale: { type: String, required: false }
|
|
@@ -169,6 +207,29 @@ const { mediaBaseUrl, i18n } = useCmsRuntime();
|
|
|
169
207
|
const mediaKeys = useCmsMediaKeys();
|
|
170
208
|
const blocks = computed(() => props.field.blocks ?? {});
|
|
171
209
|
const items = computed(() => Array.isArray(model.value) ? model.value : []);
|
|
210
|
+
const mediaOnly = computed(() => {
|
|
211
|
+
const types = Object.entries(blocks.value);
|
|
212
|
+
if (types.length !== 1) return null;
|
|
213
|
+
const [type, block] = types[0];
|
|
214
|
+
const fields = Object.entries(block.fields);
|
|
215
|
+
if (fields.length !== 1) return null;
|
|
216
|
+
const [key, field] = fields[0];
|
|
217
|
+
if (field.type !== "media" || isTranslatableMediaField(field)) return null;
|
|
218
|
+
return { type, key, field };
|
|
219
|
+
});
|
|
220
|
+
const addMediaLabel = computed(() => {
|
|
221
|
+
const types = [mediaOnly.value?.field.mediaType ?? []].flat();
|
|
222
|
+
return types.length === 1 && types[0] === "image" ? "Add photo" : "Add media";
|
|
223
|
+
});
|
|
224
|
+
const pickerOpen = ref(false);
|
|
225
|
+
const pickerFolder = useState("cms-media-picker-folder", () => null);
|
|
226
|
+
function addMedia(media) {
|
|
227
|
+
const target = mediaOnly.value;
|
|
228
|
+
if (!target) return;
|
|
229
|
+
mediaKeys.remember(media.key);
|
|
230
|
+
model.value = [...items.value, { type: target.type, [target.key]: media.key }];
|
|
231
|
+
pickerOpen.value = false;
|
|
232
|
+
}
|
|
172
233
|
let uidCounter = 0;
|
|
173
234
|
const uids = /* @__PURE__ */ new WeakMap();
|
|
174
235
|
function uidFor(item) {
|
|
@@ -245,10 +306,25 @@ function add(type) {
|
|
|
245
306
|
const block = blocks.value[type];
|
|
246
307
|
if (!block) return;
|
|
247
308
|
const empty = Object.fromEntries(Object.keys(block.fields).map((k) => [k, null]));
|
|
309
|
+
const index = items.value.length;
|
|
248
310
|
model.value = [...items.value, { type, ...empty }];
|
|
249
|
-
openEditor(
|
|
311
|
+
openEditor(index);
|
|
312
|
+
}
|
|
313
|
+
function onTileKey(event, index) {
|
|
314
|
+
if (!mediaOnly.value) return;
|
|
315
|
+
const action = event.key === "ArrowLeft" ? () => move(index, -1) : event.key === "ArrowRight" ? () => move(index, 1) : event.key === "Delete" || event.key === "Backspace" ? () => remove(index) : null;
|
|
316
|
+
if (!action) return;
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
action();
|
|
319
|
+
}
|
|
320
|
+
function onPreviewClick(index) {
|
|
321
|
+
if (!mediaOnly.value) openEditor(index);
|
|
250
322
|
}
|
|
251
323
|
function addFirst() {
|
|
324
|
+
if (mediaOnly.value) {
|
|
325
|
+
pickerOpen.value = true;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
252
328
|
const first = Object.keys(blocks.value)[0];
|
|
253
329
|
if (first) add(first);
|
|
254
330
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
size?: 'md' | 'lg';
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
5
|
+
declare const _default: typeof __VLS_export;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="cms-brand-mark" :class="{ 'is-lg': size === 'lg' }" aria-hidden="true">
|
|
3
|
+
<svg viewBox="0 0 16 16" fill="none">
|
|
4
|
+
<path
|
|
5
|
+
d="M4.25 12.25V7.5a3.75 3.75 0 0 1 7.5 0v4.75"
|
|
6
|
+
stroke="currentColor"
|
|
7
|
+
stroke-width="2"
|
|
8
|
+
stroke-linecap="round"
|
|
9
|
+
/>
|
|
10
|
+
<circle cx="8" cy="12.25" r="1.25" fill="currentColor" />
|
|
11
|
+
</svg>
|
|
12
|
+
</span>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup>
|
|
16
|
+
defineProps({
|
|
17
|
+
size: { type: String, required: false }
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
size?: 'md' | 'lg';
|
|
3
|
+
};
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
5
|
+
declare const _default: typeof __VLS_export;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
checked?: boolean;
|
|
3
|
+
indeterminate?: boolean;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
toggle: () => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
|
+
onToggle?: (() => any) | undefined;
|
|
9
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
10
|
+
declare const _default: typeof __VLS_export;
|
|
11
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
role="checkbox"
|
|
5
|
+
:aria-checked="indeterminate ? 'mixed' : checked"
|
|
6
|
+
class="cms-checkbox"
|
|
7
|
+
:class="{ 'is-on': checked || indeterminate }"
|
|
8
|
+
@click.stop="emit('toggle')"
|
|
9
|
+
>
|
|
10
|
+
<CmsIcon v-if="indeterminate" name="minus" class="size-3" />
|
|
11
|
+
<CmsIcon v-else-if="checked" name="check" class="size-3" />
|
|
12
|
+
</button>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup>
|
|
16
|
+
defineProps({
|
|
17
|
+
checked: { type: Boolean, required: false },
|
|
18
|
+
indeterminate: { type: Boolean, required: false }
|
|
19
|
+
});
|
|
20
|
+
const emit = defineEmits(["toggle"]);
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
checked?: boolean;
|
|
3
|
+
indeterminate?: boolean;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
toggle: () => any;
|
|
7
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
|
+
onToggle?: (() => any) | undefined;
|
|
9
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
10
|
+
declare const _default: typeof __VLS_export;
|
|
11
|
+
export default _default;
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
<script setup>
|
|
37
37
|
import { onBeforeUnmount, watch } from "#imports";
|
|
38
|
+
import { useCmsOverlay } from "../../composables/cms-overlay";
|
|
38
39
|
defineProps({
|
|
39
40
|
title: { type: String, required: false }
|
|
40
41
|
});
|
|
@@ -43,23 +44,18 @@ const emit = defineEmits(["close"]);
|
|
|
43
44
|
function close() {
|
|
44
45
|
emit("close");
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
if (event.key === "Escape") close();
|
|
48
|
-
}
|
|
47
|
+
const overlay = useCmsOverlay(close);
|
|
49
48
|
watch(open, (value) => {
|
|
50
49
|
if (import.meta.server) return;
|
|
51
50
|
if (value) {
|
|
52
|
-
|
|
51
|
+
overlay.activate();
|
|
53
52
|
document.body.style.overflow = "hidden";
|
|
54
53
|
} else {
|
|
55
|
-
|
|
54
|
+
overlay.deactivate();
|
|
56
55
|
document.body.style.overflow = "";
|
|
57
56
|
}
|
|
58
57
|
});
|
|
59
58
|
onBeforeUnmount(() => {
|
|
60
|
-
if (import.meta.client)
|
|
61
|
-
document.removeEventListener("keydown", onKeydown);
|
|
62
|
-
document.body.style.overflow = "";
|
|
63
|
-
}
|
|
59
|
+
if (import.meta.client) document.body.style.overflow = "";
|
|
64
60
|
});
|
|
65
61
|
</script>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="cms-empty" :class="{ 'is-fill': fill }">
|
|
3
|
-
<
|
|
4
|
-
|
|
3
|
+
<span class="cms-empty-icon">
|
|
4
|
+
<CmsIcon :name="icon" class="size-5" />
|
|
5
|
+
</span>
|
|
6
|
+
<p class="cms-title cms-title-sm">{{ title }}</p>
|
|
5
7
|
<p v-if="body" class="cms-subtitle max-w-md">
|
|
6
8
|
{{ body }}
|
|
7
9
|
</p>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
v-model="formState"
|
|
12
12
|
:fields="config.fields"
|
|
13
13
|
:tabs="config.tabs"
|
|
14
|
+
:entry-name="collection"
|
|
14
15
|
:drafts="drafts"
|
|
15
16
|
:form-id="FORM_ID"
|
|
16
17
|
:loading="saving"
|
|
@@ -120,16 +121,23 @@ function revertStatus() {
|
|
|
120
121
|
if (!drafts.value) return;
|
|
121
122
|
formState.value.status = JSON.parse(snapshot.value).status ?? null;
|
|
122
123
|
}
|
|
124
|
+
let loadRequest = 0;
|
|
123
125
|
async function load() {
|
|
124
|
-
|
|
126
|
+
const request = ++loadRequest;
|
|
127
|
+
const id = props.entryId;
|
|
128
|
+
if (id === null) {
|
|
129
|
+
loading.value = false;
|
|
125
130
|
setState(emptyState());
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
133
|
+
const isStale = () => request !== loadRequest || props.entryId !== id || !open.value;
|
|
128
134
|
loading.value = true;
|
|
129
135
|
try {
|
|
130
|
-
const entry = await cmsApi(`${endpoint.value}/${
|
|
136
|
+
const entry = await cmsApi(`${endpoint.value}/${id}`);
|
|
137
|
+
if (isStale()) return;
|
|
131
138
|
setState(pickFields(entry));
|
|
132
139
|
} catch (error) {
|
|
140
|
+
if (isStale()) return;
|
|
133
141
|
toast.add({
|
|
134
142
|
title: "Could not load entry",
|
|
135
143
|
description: errorMessage(error),
|
|
@@ -137,7 +145,7 @@ async function load() {
|
|
|
137
145
|
});
|
|
138
146
|
open.value = false;
|
|
139
147
|
} finally {
|
|
140
|
-
loading.value = false;
|
|
148
|
+
if (request === loadRequest) loading.value = false;
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
watch(
|
|
@@ -164,6 +172,7 @@ async function save() {
|
|
|
164
172
|
emit("saved");
|
|
165
173
|
open.value = false;
|
|
166
174
|
} catch (error) {
|
|
175
|
+
revertStatus();
|
|
167
176
|
toast.add({
|
|
168
177
|
title: "Save failed",
|
|
169
178
|
description: errorMessage(error),
|