@xleddyl/nuxt-cms 0.2.3 → 0.4.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/README.md +8 -1
- package/dist/module.d.mts +3 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +113 -3
- package/dist/runtime/app/components/cms/BrandMark.vue +16 -1
- package/dist/runtime/app/components/cms/EntryForm.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +34 -3
- package/dist/runtime/app/components/cms/EntryForm.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/MediaDetail.vue +25 -13
- package/dist/runtime/app/components/cms/Search.d.vue.ts +7 -0
- package/dist/runtime/app/components/cms/Search.vue +347 -0
- package/dist/runtime/app/components/cms/Search.vue.d.ts +7 -0
- package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue +21 -1
- package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/WelcomeModal.vue +7 -1
- package/dist/runtime/app/composables/cms-field-focus.d.ts +4 -0
- package/dist/runtime/app/composables/cms-field-focus.js +20 -0
- package/dist/runtime/app/layouts/cms-admin.d.vue.ts +2 -2
- package/dist/runtime/app/layouts/cms-admin.vue +8 -3
- package/dist/runtime/app/layouts/cms-admin.vue.d.ts +2 -2
- package/dist/runtime/app/pages/admin-collection.vue +3 -0
- package/dist/runtime/app/pages/admin-content.vue +3 -0
- package/dist/runtime/app/pages/admin-entry.vue +3 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection.get.js +2 -4
- package/dist/runtime/server/api/media-download.get.d.ts +2 -0
- package/dist/runtime/server/api/media-download.get.js +90 -0
- package/dist/runtime/server/api/search.get.d.ts +3 -0
- package/dist/runtime/server/api/search.get.js +24 -0
- package/dist/runtime/server/utils/media-download.d.ts +1 -0
- package/dist/runtime/server/utils/media-download.js +8 -0
- package/dist/runtime/server/utils/media.d.ts +1 -0
- package/dist/runtime/server/utils/media.js +18 -3
- package/dist/runtime/server/utils/search.d.ts +20 -0
- package/dist/runtime/server/utils/search.js +233 -0
- package/dist/runtime/shared/search.d.ts +52 -0
- package/dist/runtime/shared/search.js +207 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ nuxt-cms is a Nuxt module that leverages the Nitro server to ship a lightweight
|
|
|
10
10
|
- **Content types in code**: a `cms.config.ts` with `defineCmsConfig()` declares collections, single documents, relations, blocks and translatable fields; database schema, migrations and TypeScript types are generated from it.
|
|
11
11
|
- **Content with blocks**: news, blog and other content types with title, slug, cover, SEO fields and a body built from your own site components, drafts and scheduled publishing; `<CmsBlocks>` renders the body with typed props.
|
|
12
12
|
- **Visual editor**: content items (and any blocks field built from site components) are edited on a live preview of your own site, with drag and drop, a block palette, per-locale and mobile previews, scheduling and publishing.
|
|
13
|
-
- **Admin panel at `/cms`**: entry editing with validation, drafts, media library (S3-compatible storage, a directory on the server's disk, or a local mode backed directly by your `public/` folder), single-admin auth from env credentials.
|
|
13
|
+
- **Admin panel at `/cms`**: entry editing with validation, drafts, media library with one-click download of the original file (S3-compatible storage, a directory on the server's disk, or a local mode backed directly by your `public/` folder), a global search (⌘K / Ctrl K) over labels and every stored value in all locales, single-admin auth from env credentials.
|
|
14
14
|
- **Public GraphQL API**: read-only, typed end-to-end via gql.tada, with filtering, sorting and pagination; fields marked `private` stay out of it.
|
|
15
15
|
- **SQLite, Postgres, libSQL/Turso or Cloudflare D1**: a local file database by default, one config line to switch (including remote SQLite over the network).
|
|
16
16
|
- **Runs on serverless too**: migrations are baked into the server bundle, uploads are presigned straight to your bucket, sessions are sealed cookies. Nothing on the request path needs a local disk or sticky instances.
|
|
@@ -56,6 +56,13 @@ build stops with an explicit message if the client for the configured driver is
|
|
|
56
56
|
|
|
57
57
|
Then declare your content types in a `cms.config.ts` at the project root with `defineCmsConfig()`.
|
|
58
58
|
|
|
59
|
+
### Admin branding
|
|
60
|
+
|
|
61
|
+
`cms.admin.title`, `cms.admin.subtitle` and `cms.admin.logo` replace the name, the tagline and the
|
|
62
|
+
mark in the admin sidebar, mobile bar and login page. `logo` takes a URL or public path, a path to
|
|
63
|
+
an `.svg` file in the project (inlined, so it follows the light and dark theme) or raw `<svg>`
|
|
64
|
+
markup; see [Configuration](docs/configuration.md#admin-branding).
|
|
65
|
+
|
|
59
66
|
### Disabling the CMS
|
|
60
67
|
|
|
61
68
|
Keep the module in `modules[]` at all times and turn it off with the `enabled` option or the
|
package/dist/module.d.mts
CHANGED
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import {
|
|
4
|
+
import { extname, resolve, isAbsolute, join, relative, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { defineNuxtModule, createResolver, useLogger, addImports, addTemplate, addServerPlugin, addTypeTemplate, addVitePlugin, addComponentsDir, addRouteMiddleware, extendPages, addServerHandler, resolvePath, addComponent } from '@nuxt/kit';
|
|
7
7
|
import tailwindcss from '@tailwindcss/vite';
|
|
@@ -18,6 +18,91 @@ import { createHash } from 'node:crypto';
|
|
|
18
18
|
import { readFile } from 'node:fs/promises';
|
|
19
19
|
import { routePathsFromDir, resolvePageRoutes } from '../dist/runtime/shared/page-routes.js';
|
|
20
20
|
|
|
21
|
+
const DEFAULT_BRAND_TITLE = "nuxt-cms";
|
|
22
|
+
const DEFAULT_BRAND_SUBTITLE = "Content studio";
|
|
23
|
+
const ROOT_ALIASES = ["~~/", "@@/"];
|
|
24
|
+
const SRC_ALIASES = ["~/", "@/"];
|
|
25
|
+
function sanitizeSvg(markup) {
|
|
26
|
+
let svg = markup.replace(/<\?xml[\s\S]*?\?>/gi, "").replace(/<!DOCTYPE[\s\S]*?>/gi, "").replace(/<!--[\s\S]*?-->/g, "").replace(/<script\b[\s\S]*?<\/script\s*>/gi, "").replace(/<script\b[^>]*\/>/gi, "").replace(/<foreignObject\b[\s\S]*?<\/foreignObject\s*>/gi, "").replace(/<foreignObject\b[^>]*\/>/gi, "").replace(/\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/\s+(?:xlink:)?href\s*=\s*("\s*javascript:[^"]*"|'\s*javascript:[^']*')/gi, "").trim();
|
|
27
|
+
const start = svg.search(/<svg\b/i);
|
|
28
|
+
if (start === -1) throw new Error("no <svg> element found");
|
|
29
|
+
svg = svg.slice(start);
|
|
30
|
+
return svg.replace(/<svg\b[^>]*>/i, (tag) => normalizeRootTag(tag));
|
|
31
|
+
}
|
|
32
|
+
function normalizeRootTag(tag) {
|
|
33
|
+
const attribute = (name) => tag.match(new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i"));
|
|
34
|
+
const width = parseFloat(attribute("width")?.[1] ?? attribute("width")?.[2] ?? "");
|
|
35
|
+
const height = parseFloat(attribute("height")?.[1] ?? attribute("height")?.[2] ?? "");
|
|
36
|
+
let next = tag.replace(/\s(?:width|height)\s*=\s*(?:"[^"]*"|'[^']*')/gi, "");
|
|
37
|
+
if (!attribute("viewBox") && width > 0 && height > 0) {
|
|
38
|
+
next = next.replace(/<svg\b/i, `<svg viewBox="0 0 ${width} ${height}"`);
|
|
39
|
+
}
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
function isPublicReference(logo) {
|
|
43
|
+
return /^(?:https?:)?\/\//i.test(logo) || /^data:image\//i.test(logo) || logo.startsWith("/");
|
|
44
|
+
}
|
|
45
|
+
function projectFile(logo, paths) {
|
|
46
|
+
const rooted = ROOT_ALIASES.find((alias) => logo.startsWith(alias));
|
|
47
|
+
if (rooted) return resolve(paths.rootDir, logo.slice(rooted.length));
|
|
48
|
+
const sourced = SRC_ALIASES.find((alias) => logo.startsWith(alias));
|
|
49
|
+
if (sourced) return resolve(paths.srcDir, logo.slice(sourced.length));
|
|
50
|
+
return isAbsolute(logo) ? logo : resolve(paths.rootDir, logo);
|
|
51
|
+
}
|
|
52
|
+
function resolveBrandLogo(logo, paths) {
|
|
53
|
+
const value = logo?.trim() ?? "";
|
|
54
|
+
if (!value) return { logo: { kind: "default" }, file: null };
|
|
55
|
+
if (/^<svg[\s>]/i.test(value)) {
|
|
56
|
+
return { logo: { kind: "svg", markup: sanitizeSvgOption(value, "admin.logo") }, file: null };
|
|
57
|
+
}
|
|
58
|
+
if (isPublicReference(value)) {
|
|
59
|
+
return { logo: { kind: "image", src: value }, file: null };
|
|
60
|
+
}
|
|
61
|
+
const file = projectFile(value, paths);
|
|
62
|
+
if (!existsSync(file)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[nuxt-cms] admin.logo (${value}) points to a file that does not exist: ${file}. Use a path to an .svg file in the project, a public path such as '/logo.png', a URL, or inline <svg> markup.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (extname(file).toLowerCase() !== ".svg") {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`[nuxt-cms] admin.logo (${value}) is not an .svg file. Only SVG files are read from the project: put the image in public/ and use its public path, for example '/${file.split(/[\\/]/).pop()}'.`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
logo: { kind: "svg", markup: sanitizeSvgOption(readFileSync(file, "utf8"), value) },
|
|
74
|
+
file
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function sanitizeSvgOption(markup, source) {
|
|
78
|
+
try {
|
|
79
|
+
return sanitizeSvg(markup);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error(`[nuxt-cms] admin.logo (${source}) does not contain an <svg> element.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function resolveBrand(options, paths) {
|
|
85
|
+
const { logo, file } = resolveBrandLogo(options?.logo, paths);
|
|
86
|
+
return {
|
|
87
|
+
brand: {
|
|
88
|
+
title: options?.title?.trim() || DEFAULT_BRAND_TITLE,
|
|
89
|
+
subtitle: options?.subtitle === void 0 ? DEFAULT_BRAND_SUBTITLE : options.subtitle.trim(),
|
|
90
|
+
logo
|
|
91
|
+
},
|
|
92
|
+
file
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function renderBrandFile(brand) {
|
|
96
|
+
return [
|
|
97
|
+
`export const cmsBrand: {`,
|
|
98
|
+
` title: string`,
|
|
99
|
+
` subtitle: string`,
|
|
100
|
+
` logo: { kind: 'default' } | { kind: 'image'; src: string } | { kind: 'svg'; markup: string }`,
|
|
101
|
+
`} = ${JSON.stringify(brand, null, 2)}`,
|
|
102
|
+
``
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
21
106
|
function componentPascalName(name) {
|
|
22
107
|
return name.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
23
108
|
}
|
|
@@ -1201,7 +1286,10 @@ function resolveModuleOptions(options) {
|
|
|
1201
1286
|
configPath: options.configPath ?? "cms.config",
|
|
1202
1287
|
admin: {
|
|
1203
1288
|
email: options.admin?.email ?? "",
|
|
1204
|
-
password: options.admin?.password ?? ""
|
|
1289
|
+
password: options.admin?.password ?? "",
|
|
1290
|
+
title: options.admin?.title ?? "",
|
|
1291
|
+
subtitle: options.admin?.subtitle,
|
|
1292
|
+
logo: options.admin?.logo ?? ""
|
|
1205
1293
|
},
|
|
1206
1294
|
database: resolveDatabaseOptions(options.database),
|
|
1207
1295
|
media: resolveMediaOptions(options.media),
|
|
@@ -1735,6 +1823,18 @@ const module$1 = defineNuxtModule({
|
|
|
1735
1823
|
},
|
|
1736
1824
|
{ nuxt: true, nitro: true }
|
|
1737
1825
|
);
|
|
1826
|
+
const brandPaths = {
|
|
1827
|
+
rootDir: nuxt.options.rootDir,
|
|
1828
|
+
srcDir: nuxt.options.srcDir ?? nuxt.options.rootDir
|
|
1829
|
+
};
|
|
1830
|
+
const { file: brandFile } = resolveBrand(resolved.admin, brandPaths);
|
|
1831
|
+
if (brandFile) nuxt.options.watch.push(brandFile);
|
|
1832
|
+
const brandTemplate = addTemplate({
|
|
1833
|
+
filename: "cms/brand.ts",
|
|
1834
|
+
write: true,
|
|
1835
|
+
getContents: () => renderBrandFile(resolveBrand(resolved.admin, brandPaths).brand)
|
|
1836
|
+
});
|
|
1837
|
+
nuxt.options.alias["#cms-brand"] = brandTemplate.dst;
|
|
1738
1838
|
addVitePlugin(tailwindcss());
|
|
1739
1839
|
addVitePlugin(svgLoader({ defaultImport: "url", svgoConfig: { plugins: ["prefixIds"] } }));
|
|
1740
1840
|
nuxt.options.css.push(
|
|
@@ -1871,6 +1971,11 @@ const module$1 = defineNuxtModule({
|
|
|
1871
1971
|
method: "post",
|
|
1872
1972
|
handler: resolver.resolve("./runtime/server/api/media-usage.post")
|
|
1873
1973
|
});
|
|
1974
|
+
addServerHandler({
|
|
1975
|
+
route: "/api/cms/admin/media/download",
|
|
1976
|
+
method: "get",
|
|
1977
|
+
handler: resolver.resolve("./runtime/server/api/media-download.get")
|
|
1978
|
+
});
|
|
1874
1979
|
if (mediaFilesystemBase) {
|
|
1875
1980
|
addServerHandler({
|
|
1876
1981
|
route: "/api/cms/admin/media/upload",
|
|
@@ -1882,6 +1987,11 @@ const module$1 = defineNuxtModule({
|
|
|
1882
1987
|
handler: resolver.resolve("./runtime/server/routes/media-file")
|
|
1883
1988
|
});
|
|
1884
1989
|
}
|
|
1990
|
+
addServerHandler({
|
|
1991
|
+
route: "/api/cms/search",
|
|
1992
|
+
method: "get",
|
|
1993
|
+
handler: resolver.resolve("./runtime/server/api/search.get")
|
|
1994
|
+
});
|
|
1885
1995
|
addServerHandler({
|
|
1886
1996
|
route: "/api/cms/users",
|
|
1887
1997
|
method: "get",
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<span
|
|
2
|
+
<span
|
|
3
|
+
v-if="cmsBrand.logo.kind === 'svg'"
|
|
4
|
+
class="cms-brand-logo"
|
|
5
|
+
:class="{ 'is-lg': size === 'lg' }"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
v-html="cmsBrand.logo.markup"
|
|
8
|
+
/>
|
|
9
|
+
<img
|
|
10
|
+
v-else-if="cmsBrand.logo.kind === 'image'"
|
|
11
|
+
class="cms-brand-logo"
|
|
12
|
+
:class="{ 'is-lg': size === 'lg' }"
|
|
13
|
+
:src="cmsBrand.logo.src"
|
|
14
|
+
:alt="cmsBrand.title"
|
|
15
|
+
/>
|
|
16
|
+
<span v-else class="cms-brand-mark" :class="{ 'is-lg': size === 'lg' }" aria-hidden="true">
|
|
3
17
|
<svg viewBox="0 0 16 16" fill="none">
|
|
4
18
|
<path
|
|
5
19
|
d="M4.5 5.5h7M4.5 8h7M4.5 10.5h4"
|
|
@@ -12,6 +26,7 @@
|
|
|
12
26
|
</template>
|
|
13
27
|
|
|
14
28
|
<script setup>
|
|
29
|
+
import { cmsBrand } from "#cms-brand";
|
|
15
30
|
defineProps({
|
|
16
31
|
size: { type: String, required: false }
|
|
17
32
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CmsEntry, CmsTab } from '#nuxt-cms';
|
|
2
|
+
import type { CmsSearchFocus } from '../../../shared/search.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
fields: CmsEntry['fields'];
|
|
4
5
|
tabs?: CmsTab[];
|
|
@@ -7,6 +8,7 @@ type __VLS_Props = {
|
|
|
7
8
|
loading?: boolean;
|
|
8
9
|
formId?: string;
|
|
9
10
|
footer?: boolean;
|
|
11
|
+
focus?: CmsSearchFocus | null;
|
|
10
12
|
};
|
|
11
13
|
type __VLS_ModelProps = {
|
|
12
14
|
modelValue: Record<string, unknown>;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<CmsForm
|
|
2
|
+
<CmsForm
|
|
3
|
+
:id="formId"
|
|
4
|
+
ref="form"
|
|
5
|
+
:state="state"
|
|
6
|
+
:schema="schema"
|
|
7
|
+
@submit="emit('submit')"
|
|
8
|
+
@error="onError"
|
|
9
|
+
>
|
|
3
10
|
<div v-if="tabList.length > 1" class="cms-tabs" role="tablist">
|
|
4
11
|
<button
|
|
5
12
|
v-for="tab in tabList"
|
|
@@ -53,6 +60,8 @@
|
|
|
53
60
|
:icon="fieldIcon(fields[key])"
|
|
54
61
|
:name="key"
|
|
55
62
|
:required="fields[key].required"
|
|
63
|
+
:data-cms-field="key"
|
|
64
|
+
:class="{ 'is-search-focus': highlighted === key }"
|
|
56
65
|
>
|
|
57
66
|
<template
|
|
58
67
|
v-if="hasLocaleSwitch(fields[key]) && i18n.locales.length > 1"
|
|
@@ -102,8 +111,9 @@ import {
|
|
|
102
111
|
resolveFormLayout,
|
|
103
112
|
slugSourceValue
|
|
104
113
|
} from "#nuxt-cms";
|
|
105
|
-
import { computed, nextTick, ref, watch } from "#imports";
|
|
114
|
+
import { computed, nextTick, onMounted, ref, watch } from "#imports";
|
|
106
115
|
import { buildEntrySchema } from "../../../shared/validation";
|
|
116
|
+
import { useCmsFieldFocus } from "../../composables/cms-field-focus";
|
|
107
117
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
108
118
|
import { entryFormLayout, useCmsSettingsState } from "../../composables/cms-settings";
|
|
109
119
|
import { fieldIcon } from "../../utils/ui";
|
|
@@ -114,7 +124,8 @@ const props = defineProps({
|
|
|
114
124
|
drafts: { type: Boolean, required: false },
|
|
115
125
|
loading: { type: Boolean, required: false },
|
|
116
126
|
formId: { type: String, required: false },
|
|
117
|
-
footer: { type: Boolean, required: false, default: true }
|
|
127
|
+
footer: { type: Boolean, required: false, default: true },
|
|
128
|
+
focus: { type: [Object, null], required: false }
|
|
118
129
|
});
|
|
119
130
|
const state = defineModel({ type: Object, ...{ required: true } });
|
|
120
131
|
const emit = defineEmits(["submit", "error"]);
|
|
@@ -194,4 +205,24 @@ const published = computed({
|
|
|
194
205
|
function togglePublished() {
|
|
195
206
|
published.value = !published.value;
|
|
196
207
|
}
|
|
208
|
+
const form = ref(null);
|
|
209
|
+
const { highlighted, reveal } = useCmsFieldFocus();
|
|
210
|
+
function applyFocus(focus) {
|
|
211
|
+
if (focus.tab && tabList.value.some((tab2) => tab2.id === focus.tab)) activeTab.value = focus.tab;
|
|
212
|
+
const key = focus.field;
|
|
213
|
+
const field = key ? props.fields[key] : void 0;
|
|
214
|
+
if (!key || !field) return;
|
|
215
|
+
const tab = tabList.value.length ? fieldTab(field, tabList.value) : void 0;
|
|
216
|
+
if (tab) activeTab.value = tab;
|
|
217
|
+
if (focus.locale && hasLocaleSwitch(field) && i18n.locales.includes(focus.locale)) {
|
|
218
|
+
setLocale(key, focus.locale);
|
|
219
|
+
}
|
|
220
|
+
void nextTick(() => {
|
|
221
|
+
revealField(key);
|
|
222
|
+
reveal(() => form.value?.$el, key);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
onMounted(() => {
|
|
226
|
+
if (props.focus) applyFocus(props.focus);
|
|
227
|
+
});
|
|
197
228
|
</script>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CmsEntry, CmsTab } from '#nuxt-cms';
|
|
2
|
+
import type { CmsSearchFocus } from '../../../shared/search.js';
|
|
2
3
|
type __VLS_Props = {
|
|
3
4
|
fields: CmsEntry['fields'];
|
|
4
5
|
tabs?: CmsTab[];
|
|
@@ -7,6 +8,7 @@ type __VLS_Props = {
|
|
|
7
8
|
loading?: boolean;
|
|
8
9
|
formId?: string;
|
|
9
10
|
footer?: boolean;
|
|
11
|
+
focus?: CmsSearchFocus | null;
|
|
10
12
|
};
|
|
11
13
|
type __VLS_ModelProps = {
|
|
12
14
|
modelValue: Record<string, unknown>;
|
|
@@ -13,22 +13,31 @@
|
|
|
13
13
|
<CmsIcon v-else :name="mediaIconFor(item.type)" class="size-12" />
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
<div
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
<div class="cms-actions">
|
|
17
|
+
<template v-if="item.url">
|
|
18
|
+
<CmsButton
|
|
19
|
+
:label="copied ? 'Copied' : 'Copy URL'"
|
|
20
|
+
:icon="copied ? 'check' : 'clipboard-document'"
|
|
21
|
+
variant="soft"
|
|
22
|
+
color="neutral"
|
|
23
|
+
size="sm"
|
|
24
|
+
@click="copyUrl"
|
|
25
|
+
/>
|
|
26
|
+
<a
|
|
27
|
+
:href="item.url"
|
|
28
|
+
target="_blank"
|
|
29
|
+
rel="noopener"
|
|
30
|
+
class="cms-btn cms-btn-sm cms-btn-neutral cms-btn-ghost"
|
|
31
|
+
>
|
|
32
|
+
<CmsIcon name="arrow-top-right-on-square" class="size-4 shrink-0" />Open
|
|
33
|
+
</a>
|
|
34
|
+
</template>
|
|
25
35
|
<a
|
|
26
|
-
:href="
|
|
27
|
-
|
|
28
|
-
rel="noopener"
|
|
36
|
+
:href="downloadHref"
|
|
37
|
+
download
|
|
29
38
|
class="cms-btn cms-btn-sm cms-btn-neutral cms-btn-ghost"
|
|
30
39
|
>
|
|
31
|
-
<CmsIcon name="arrow-
|
|
40
|
+
<CmsIcon name="arrow-down-tray" class="size-4 shrink-0" />Download
|
|
32
41
|
</a>
|
|
33
42
|
</div>
|
|
34
43
|
|
|
@@ -128,6 +137,9 @@ const dirty = computed(() => {
|
|
|
128
137
|
if (!props.item) return false;
|
|
129
138
|
return (alt.value.trim() || null) !== (props.item.alt ?? null) || !props.readOnly && folder.value !== (props.item.folder ?? null);
|
|
130
139
|
});
|
|
140
|
+
const downloadHref = computed(
|
|
141
|
+
() => props.item ? `/api/cms/admin/media/download?key=${encodeURIComponent(props.item.key)}` : ""
|
|
142
|
+
);
|
|
131
143
|
const facts = computed(() => {
|
|
132
144
|
const item = props.item;
|
|
133
145
|
if (!item) return [];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
2
|
+
open: () => any;
|
|
3
|
+
}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
|
|
4
|
+
onOpen?: (() => any) | undefined;
|
|
5
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
7
|
+
export default _default;
|