@xleddyl/nuxt-cms 0.2.2 → 0.2.3
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 +3 -1
- package/dist/module.d.mts +4 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +319 -32
- package/dist/runtime/app/blocks/CmsBlocks.d.vue.ts +45 -0
- package/dist/runtime/app/blocks/CmsBlocks.vue +36 -0
- package/dist/runtime/app/blocks/CmsBlocks.vue.d.ts +45 -0
- package/dist/runtime/app/blocks/resolve.d.ts +21 -0
- package/dist/runtime/app/blocks/resolve.js +37 -0
- package/dist/runtime/app/components/cms/BlockOutline.d.vue.ts +17 -0
- package/dist/runtime/app/components/cms/BlockOutline.vue +99 -0
- package/dist/runtime/app/components/cms/BlockOutline.vue.d.ts +17 -0
- package/dist/runtime/app/components/cms/BlockPalette.d.vue.ts +12 -0
- package/dist/runtime/app/components/cms/BlockPalette.vue +29 -0
- package/dist/runtime/app/components/cms/BlockPalette.vue.d.ts +12 -0
- package/dist/runtime/app/components/cms/BlocksField.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/BlocksField.vue +83 -3
- package/dist/runtime/app/components/cms/BlocksField.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/EntryForm.vue +9 -4
- package/dist/runtime/app/components/cms/FieldInput.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/FieldInput.vue +18 -1
- package/dist/runtime/app/components/cms/FieldInput.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/SettingsEntry.vue +3 -2
- package/dist/runtime/app/components/cms/SettingsModal.vue +2 -1
- package/dist/runtime/app/components/cms/StatusBadge.d.vue.ts +2 -0
- package/dist/runtime/app/components/cms/StatusBadge.vue +9 -3
- package/dist/runtime/app/components/cms/StatusBadge.vue.d.ts +2 -0
- package/dist/runtime/app/components/cms/VisualEditor.d.vue.ts +37 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue +533 -0
- package/dist/runtime/app/components/cms/VisualEditor.vue.d.ts +37 -0
- package/dist/runtime/app/composables/cms-entry-disabled.d.ts +7 -1
- package/dist/runtime/app/composables/cms-entry-disabled.js +15 -0
- package/dist/runtime/app/composables/cms-entry.d.ts +7 -1
- package/dist/runtime/app/composables/cms-entry.js +51 -1
- package/dist/runtime/app/composables/cms-runtime.d.ts +1 -0
- package/dist/runtime/app/composables/cms-settings.js +8 -2
- package/dist/runtime/app/layouts/cms-admin.vue +5 -0
- package/dist/runtime/app/layouts/cms-editor.d.vue.ts +13 -0
- package/dist/runtime/app/layouts/cms-editor.vue +14 -0
- package/dist/runtime/app/layouts/cms-editor.vue.d.ts +13 -0
- package/dist/runtime/app/pages/admin-collection.vue +33 -4
- package/dist/runtime/app/pages/admin-content.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-content.vue +284 -0
- package/dist/runtime/app/pages/admin-content.vue.d.ts +3 -0
- package/dist/runtime/app/pages/admin-preview.d.vue.ts +3 -0
- package/dist/runtime/app/pages/admin-preview.vue +132 -0
- package/dist/runtime/app/pages/admin-preview.vue.d.ts +3 -0
- package/dist/runtime/app/preview/PreviewOverlay.d.vue.ts +15 -0
- package/dist/runtime/app/preview/PreviewOverlay.vue +362 -0
- package/dist/runtime/app/preview/PreviewOverlay.vue.d.ts +15 -0
- package/dist/runtime/app/utils/ui.js +1 -0
- package/dist/runtime/assets/main.css +1 -1
- package/dist/runtime/server/api/collection.get.js +3 -2
- package/dist/runtime/server/api/collection.post.js +4 -1
- package/dist/runtime/server/api/item.delete.js +2 -1
- package/dist/runtime/server/api/item.put.js +2 -1
- package/dist/runtime/server/utils/graphql.js +23 -8
- package/dist/runtime/server/utils/registry.d.ts +1 -0
- package/dist/runtime/server/utils/registry.js +6 -0
- package/dist/runtime/server/utils/visibility.d.ts +4 -0
- package/dist/runtime/server/utils/visibility.js +12 -0
- package/dist/runtime/shared/graphql-sdl.js +4 -1
- package/dist/runtime/shared/index.d.ts +59 -5
- package/dist/runtime/shared/index.js +133 -0
- package/dist/runtime/shared/preview.d.ts +85 -0
- package/dist/runtime/shared/preview.js +219 -0
- package/dist/runtime/shared/validation.d.ts +2 -0
- package/dist/runtime/shared/validation.js +15 -2
- package/package.json +1 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PropType } from 'vue';
|
|
2
|
+
import type { CmsBlockEntryName } from '#cms-types';
|
|
3
|
+
import type { CmsBlockItem } from './resolve.js';
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
5
|
+
blocks: {
|
|
6
|
+
type: PropType<readonly CmsBlockItem[] | null>;
|
|
7
|
+
default: null;
|
|
8
|
+
};
|
|
9
|
+
entry: {
|
|
10
|
+
type: PropType<CmsBlockEntryName>;
|
|
11
|
+
default: undefined;
|
|
12
|
+
};
|
|
13
|
+
field: {
|
|
14
|
+
type: StringConstructor;
|
|
15
|
+
default: undefined;
|
|
16
|
+
};
|
|
17
|
+
annotate: {
|
|
18
|
+
type: BooleanConstructor;
|
|
19
|
+
default: boolean;
|
|
20
|
+
};
|
|
21
|
+
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
22
|
+
blocks: {
|
|
23
|
+
type: PropType<readonly CmsBlockItem[] | null>;
|
|
24
|
+
default: null;
|
|
25
|
+
};
|
|
26
|
+
entry: {
|
|
27
|
+
type: PropType<CmsBlockEntryName>;
|
|
28
|
+
default: undefined;
|
|
29
|
+
};
|
|
30
|
+
field: {
|
|
31
|
+
type: StringConstructor;
|
|
32
|
+
default: undefined;
|
|
33
|
+
};
|
|
34
|
+
annotate: {
|
|
35
|
+
type: BooleanConstructor;
|
|
36
|
+
default: boolean;
|
|
37
|
+
};
|
|
38
|
+
}>> & Readonly<{}>, {
|
|
39
|
+
field: string;
|
|
40
|
+
blocks: readonly CmsBlockItem[] | null;
|
|
41
|
+
entry: string;
|
|
42
|
+
annotate: boolean;
|
|
43
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
44
|
+
declare const _default: typeof __VLS_export;
|
|
45
|
+
export default _default;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="block.component"
|
|
4
|
+
v-for="block in resolved"
|
|
5
|
+
:key="block.key"
|
|
6
|
+
v-bind="annotate ? { ...block.props, ...annotation(block) } : block.props"
|
|
7
|
+
/>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup>
|
|
11
|
+
import { computed } from "#imports";
|
|
12
|
+
import { cmsBlockTypeName, resolveCmsBlocks } from "./resolve";
|
|
13
|
+
const props = defineProps({
|
|
14
|
+
blocks: { type: Array, default: null },
|
|
15
|
+
entry: { type: String, default: void 0 },
|
|
16
|
+
field: { type: String, default: void 0 },
|
|
17
|
+
annotate: { type: Boolean, default: false }
|
|
18
|
+
});
|
|
19
|
+
function warnMissing(item) {
|
|
20
|
+
if (!import.meta.dev) return;
|
|
21
|
+
const name = cmsBlockTypeName(item, props.entry, props.field) ?? item.type;
|
|
22
|
+
console.warn(
|
|
23
|
+
`[nuxt-cms] <CmsBlocks> has no component for block '${name}': set component on the block in cms.config, or pass entry and field when the data has no __typename`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const resolved = computed(
|
|
27
|
+
() => resolveCmsBlocks(props.blocks, {
|
|
28
|
+
entry: props.entry,
|
|
29
|
+
field: props.field,
|
|
30
|
+
onMissing: warnMissing
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
function annotation(block) {
|
|
34
|
+
return { "data-cms-block": block.index, "data-cms-block-type": block.type };
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PropType } from 'vue';
|
|
2
|
+
import type { CmsBlockEntryName } from '#cms-types';
|
|
3
|
+
import type { CmsBlockItem } from './resolve.js';
|
|
4
|
+
declare const __VLS_export: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
5
|
+
blocks: {
|
|
6
|
+
type: PropType<readonly CmsBlockItem[] | null>;
|
|
7
|
+
default: null;
|
|
8
|
+
};
|
|
9
|
+
entry: {
|
|
10
|
+
type: PropType<CmsBlockEntryName>;
|
|
11
|
+
default: undefined;
|
|
12
|
+
};
|
|
13
|
+
field: {
|
|
14
|
+
type: StringConstructor;
|
|
15
|
+
default: undefined;
|
|
16
|
+
};
|
|
17
|
+
annotate: {
|
|
18
|
+
type: BooleanConstructor;
|
|
19
|
+
default: boolean;
|
|
20
|
+
};
|
|
21
|
+
}>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
22
|
+
blocks: {
|
|
23
|
+
type: PropType<readonly CmsBlockItem[] | null>;
|
|
24
|
+
default: null;
|
|
25
|
+
};
|
|
26
|
+
entry: {
|
|
27
|
+
type: PropType<CmsBlockEntryName>;
|
|
28
|
+
default: undefined;
|
|
29
|
+
};
|
|
30
|
+
field: {
|
|
31
|
+
type: StringConstructor;
|
|
32
|
+
default: undefined;
|
|
33
|
+
};
|
|
34
|
+
annotate: {
|
|
35
|
+
type: BooleanConstructor;
|
|
36
|
+
default: boolean;
|
|
37
|
+
};
|
|
38
|
+
}>> & Readonly<{}>, {
|
|
39
|
+
field: string;
|
|
40
|
+
blocks: readonly CmsBlockItem[] | null;
|
|
41
|
+
entry: string;
|
|
42
|
+
annotate: boolean;
|
|
43
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
44
|
+
declare const _default: typeof __VLS_export;
|
|
45
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Component } from 'vue';
|
|
2
|
+
export interface CmsBlockItem {
|
|
3
|
+
type: string;
|
|
4
|
+
__typename?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ResolvedCmsBlock {
|
|
7
|
+
key: string;
|
|
8
|
+
index: number;
|
|
9
|
+
type: string;
|
|
10
|
+
component: Component;
|
|
11
|
+
props: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export declare const isHiddenCmsBlock: (item: CmsBlockItem) => boolean;
|
|
14
|
+
export declare function cmsBlockTypeName(item: CmsBlockItem, entry?: string, field?: string): string | undefined;
|
|
15
|
+
export declare function cmsBlockComponent(item: CmsBlockItem, entry?: string, field?: string): Component | undefined;
|
|
16
|
+
export declare function cmsBlockProps(item: CmsBlockItem): Record<string, unknown>;
|
|
17
|
+
export declare function resolveCmsBlocks(blocks: readonly CmsBlockItem[] | null | undefined, options?: {
|
|
18
|
+
entry?: string;
|
|
19
|
+
field?: string;
|
|
20
|
+
onMissing?: (item: CmsBlockItem) => void;
|
|
21
|
+
}): ResolvedCmsBlock[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { cmsBlockComponents } from "#cms-blocks";
|
|
2
|
+
import { CONTENT_BODY_FIELD, blockTypeName, isHiddenBlock } from "../../shared/index.js";
|
|
3
|
+
const OMITTED_KEYS = /* @__PURE__ */ new Set(["type", "__typename"]);
|
|
4
|
+
export const isHiddenCmsBlock = isHiddenBlock;
|
|
5
|
+
export function cmsBlockTypeName(item, entry, field = CONTENT_BODY_FIELD) {
|
|
6
|
+
return entry ? blockTypeName(entry, field, item.type) : item.__typename;
|
|
7
|
+
}
|
|
8
|
+
export function cmsBlockComponent(item, entry, field) {
|
|
9
|
+
const name = cmsBlockTypeName(item, entry, field);
|
|
10
|
+
return name && Object.hasOwn(cmsBlockComponents, name) ? cmsBlockComponents[name] : void 0;
|
|
11
|
+
}
|
|
12
|
+
export function cmsBlockProps(item) {
|
|
13
|
+
return Object.fromEntries(
|
|
14
|
+
Object.entries(item).filter(
|
|
15
|
+
([key]) => !OMITTED_KEYS.has(key)
|
|
16
|
+
)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
export function resolveCmsBlocks(blocks, options = {}) {
|
|
20
|
+
const resolved = [];
|
|
21
|
+
(blocks ?? []).forEach((item, index) => {
|
|
22
|
+
if (!item || typeof item !== "object" || isHiddenCmsBlock(item)) return;
|
|
23
|
+
const component = cmsBlockComponent(item, options.entry, options.field);
|
|
24
|
+
if (!component) {
|
|
25
|
+
options.onMissing?.(item);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
resolved.push({
|
|
29
|
+
key: `${index}:${item.type}`,
|
|
30
|
+
index,
|
|
31
|
+
type: item.type,
|
|
32
|
+
component,
|
|
33
|
+
props: cmsBlockProps(item)
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FieldConfig } from '#nuxt-cms';
|
|
2
|
+
import type { CmsBlockValue } from '../../../shared/preview.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
items: CmsBlockValue[];
|
|
5
|
+
field: FieldConfig;
|
|
6
|
+
locale: string;
|
|
7
|
+
selected: number | null;
|
|
8
|
+
};
|
|
9
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
10
|
+
select: (index: number) => any;
|
|
11
|
+
move: (from: number, to: number) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
onSelect?: ((index: number) => any) | undefined;
|
|
14
|
+
onMove?: ((from: number, to: number) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ol class="cms-outline" aria-label="Blocks">
|
|
3
|
+
<li
|
|
4
|
+
v-for="(item, index) in items"
|
|
5
|
+
:key="uidFor(item)"
|
|
6
|
+
class="cms-outline-row"
|
|
7
|
+
:class="{
|
|
8
|
+
'is-selected': index === selected,
|
|
9
|
+
'is-hidden': isHiddenBlock(item),
|
|
10
|
+
'is-dragging': dragIndex === index,
|
|
11
|
+
'is-drop-target': dropIndex === index && dragIndex !== index
|
|
12
|
+
}"
|
|
13
|
+
draggable="true"
|
|
14
|
+
@dragstart="onDragStart(index, $event)"
|
|
15
|
+
@dragover.prevent="dropIndex = index"
|
|
16
|
+
@dragleave="dropIndex === index && (dropIndex = null)"
|
|
17
|
+
@drop.prevent="onDrop(index)"
|
|
18
|
+
@dragend="onDragEnd"
|
|
19
|
+
>
|
|
20
|
+
<span class="cms-outline-grip" aria-hidden="true">
|
|
21
|
+
<CmsIcon name="bars-2" class="size-3.5" />
|
|
22
|
+
</span>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="cms-outline-main"
|
|
26
|
+
:aria-current="index === selected ? 'true' : void 0"
|
|
27
|
+
:aria-label="`${labelOf(item)}, ${index + 1} of ${items.length}${isHiddenBlock(item) ? ', hidden' : ''}. Alt and the arrow keys move it`"
|
|
28
|
+
aria-keyshortcuts="Alt+ArrowUp Alt+ArrowDown"
|
|
29
|
+
@click="emit('select', index)"
|
|
30
|
+
@keydown.alt.up.prevent="index > 0 && emit('move', index, index - 1)"
|
|
31
|
+
@keydown.alt.down.prevent="index < items.length - 1 && emit('move', index, index + 1)"
|
|
32
|
+
>
|
|
33
|
+
<CmsIcon :name="blockOf(item)?.icon ?? 'rectangle-stack'" class="size-4 shrink-0" />
|
|
34
|
+
<span class="cms-outline-text">
|
|
35
|
+
<span class="cms-outline-label">{{ labelOf(item) }}</span>
|
|
36
|
+
<span v-if="summaryOf(item)" class="cms-outline-summary">
|
|
37
|
+
{{ summaryOf(item) }}
|
|
38
|
+
</span>
|
|
39
|
+
</span>
|
|
40
|
+
<CmsIcon
|
|
41
|
+
v-if="isHiddenBlock(item)"
|
|
42
|
+
name="eye-slash"
|
|
43
|
+
class="cms-outline-hidden size-4 shrink-0"
|
|
44
|
+
/>
|
|
45
|
+
</button>
|
|
46
|
+
</li>
|
|
47
|
+
</ol>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup>
|
|
51
|
+
import { isHiddenBlock } from "#nuxt-cms";
|
|
52
|
+
import { ref } from "#imports";
|
|
53
|
+
import { blockSummary } from "../../../shared/preview";
|
|
54
|
+
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
55
|
+
const props = defineProps({
|
|
56
|
+
items: { type: Array, required: true },
|
|
57
|
+
field: { type: Object, required: true },
|
|
58
|
+
locale: { type: String, required: true },
|
|
59
|
+
selected: { type: [Number, null], required: true }
|
|
60
|
+
});
|
|
61
|
+
const emit = defineEmits(["select", "move"]);
|
|
62
|
+
const { i18n } = useCmsRuntime();
|
|
63
|
+
let uidCounter = 0;
|
|
64
|
+
const uids = /* @__PURE__ */ new WeakMap();
|
|
65
|
+
function uidFor(item) {
|
|
66
|
+
let uid = uids.get(item);
|
|
67
|
+
if (uid === void 0) {
|
|
68
|
+
uid = ++uidCounter;
|
|
69
|
+
uids.set(item, uid);
|
|
70
|
+
}
|
|
71
|
+
return uid;
|
|
72
|
+
}
|
|
73
|
+
function blockOf(item) {
|
|
74
|
+
return props.field.blocks?.[String(item.type)];
|
|
75
|
+
}
|
|
76
|
+
function labelOf(item) {
|
|
77
|
+
return blockOf(item)?.label ?? String(item.type);
|
|
78
|
+
}
|
|
79
|
+
function summaryOf(item) {
|
|
80
|
+
return blockSummary(blockOf(item), item, props.locale, i18n.defaultLocale, 60);
|
|
81
|
+
}
|
|
82
|
+
const dragIndex = ref(null);
|
|
83
|
+
const dropIndex = ref(null);
|
|
84
|
+
function onDragStart(index, event) {
|
|
85
|
+
dragIndex.value = index;
|
|
86
|
+
if (event.dataTransfer) {
|
|
87
|
+
event.dataTransfer.effectAllowed = "move";
|
|
88
|
+
event.dataTransfer.setData("text/plain", String(index));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function onDrop(index) {
|
|
92
|
+
if (dragIndex.value !== null && dragIndex.value !== index) emit("move", dragIndex.value, index);
|
|
93
|
+
onDragEnd();
|
|
94
|
+
}
|
|
95
|
+
function onDragEnd() {
|
|
96
|
+
dragIndex.value = null;
|
|
97
|
+
dropIndex.value = null;
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FieldConfig } from '#nuxt-cms';
|
|
2
|
+
import type { CmsBlockValue } from '../../../shared/preview.js';
|
|
3
|
+
type __VLS_Props = {
|
|
4
|
+
items: CmsBlockValue[];
|
|
5
|
+
field: FieldConfig;
|
|
6
|
+
locale: string;
|
|
7
|
+
selected: number | null;
|
|
8
|
+
};
|
|
9
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
10
|
+
select: (index: number) => any;
|
|
11
|
+
move: (from: number, to: number) => any;
|
|
12
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
onSelect?: ((index: number) => any) | undefined;
|
|
14
|
+
onMove?: ((from: number, to: number) => any) | undefined;
|
|
15
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BlockConfig } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
blocks: Record<string, BlockConfig>;
|
|
4
|
+
label?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
pick: (type: string) => any;
|
|
8
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
onPick?: ((type: string) => any) | undefined;
|
|
10
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cms-palette" role="group" :aria-label="label">
|
|
3
|
+
<button
|
|
4
|
+
v-for="(block, type) in blocks"
|
|
5
|
+
:key="type"
|
|
6
|
+
type="button"
|
|
7
|
+
class="cms-palette-item"
|
|
8
|
+
@click="emit('pick', String(type))"
|
|
9
|
+
>
|
|
10
|
+
<span class="cms-palette-icon">
|
|
11
|
+
<CmsIcon :name="block.icon ?? 'rectangle-stack'" class="size-4" />
|
|
12
|
+
</span>
|
|
13
|
+
<span class="cms-palette-text">
|
|
14
|
+
<span class="cms-palette-label">{{ block.label }}</span>
|
|
15
|
+
<span v-if="block.description" class="cms-palette-description">
|
|
16
|
+
{{ block.description }}
|
|
17
|
+
</span>
|
|
18
|
+
</span>
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
defineProps({
|
|
25
|
+
blocks: { type: Object, required: true },
|
|
26
|
+
label: { type: String, required: false, default: "Available blocks" }
|
|
27
|
+
});
|
|
28
|
+
const emit = defineEmits(["pick"]);
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BlockConfig } from '#nuxt-cms';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
blocks: Record<string, BlockConfig>;
|
|
4
|
+
label?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
7
|
+
pick: (type: string) => any;
|
|
8
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
9
|
+
onPick?: ((type: string) => any) | undefined;
|
|
10
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -112,6 +112,45 @@
|
|
|
112
112
|
</button>
|
|
113
113
|
</div>
|
|
114
114
|
|
|
115
|
+
<CmsButton
|
|
116
|
+
v-if="visual"
|
|
117
|
+
class="self-start"
|
|
118
|
+
label="Open visual editor"
|
|
119
|
+
icon="window"
|
|
120
|
+
size="sm"
|
|
121
|
+
variant="soft"
|
|
122
|
+
color="neutral"
|
|
123
|
+
@click="visualOpen = true"
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
<Teleport to="body">
|
|
127
|
+
<div v-if="visual && visualOpen" class="cms-scope cms-editor-overlay" :data-theme="theme">
|
|
128
|
+
<CmsVisualEditor
|
|
129
|
+
:state="{ [fieldKey]: model }"
|
|
130
|
+
:entry-name="entryName"
|
|
131
|
+
:field-key="fieldKey"
|
|
132
|
+
:fields="{ [fieldKey]: field }"
|
|
133
|
+
:title="field.label"
|
|
134
|
+
@update:state="onVisualUpdate"
|
|
135
|
+
>
|
|
136
|
+
<template #start>
|
|
137
|
+
<CmsButton
|
|
138
|
+
icon="arrow-left"
|
|
139
|
+
size="sm"
|
|
140
|
+
variant="ghost"
|
|
141
|
+
color="neutral"
|
|
142
|
+
aria-label="Back to the form"
|
|
143
|
+
title="Back to the form"
|
|
144
|
+
@click="visualOpen = false"
|
|
145
|
+
/>
|
|
146
|
+
</template>
|
|
147
|
+
<template #actions>
|
|
148
|
+
<CmsButton label="Done" size="sm" @click="visualOpen = false" />
|
|
149
|
+
</template>
|
|
150
|
+
</CmsVisualEditor>
|
|
151
|
+
</div>
|
|
152
|
+
</Teleport>
|
|
153
|
+
|
|
115
154
|
<CmsModal v-if="mediaOnly" v-model:open="pickerOpen" title="Media" size="lg">
|
|
116
155
|
<template #body>
|
|
117
156
|
<CmsMediaGallery
|
|
@@ -167,6 +206,14 @@
|
|
|
167
206
|
color="neutral"
|
|
168
207
|
@click="duplicateCurrent"
|
|
169
208
|
/>
|
|
209
|
+
<CmsButton
|
|
210
|
+
v-if="editorItem && !declaresHiddenField(blockOf(editorItem))"
|
|
211
|
+
:label="isHidden(editorItem) ? 'Show' : 'Hide'"
|
|
212
|
+
:icon="isHidden(editorItem) ? 'eye' : 'eye-slash'"
|
|
213
|
+
variant="soft"
|
|
214
|
+
color="neutral"
|
|
215
|
+
@click="toggleHiddenCurrent"
|
|
216
|
+
/>
|
|
170
217
|
</div>
|
|
171
218
|
<div class="cms-actions">
|
|
172
219
|
<CmsButton
|
|
@@ -187,6 +234,8 @@
|
|
|
187
234
|
|
|
188
235
|
<script setup>
|
|
189
236
|
import {
|
|
237
|
+
declaresHiddenField,
|
|
238
|
+
isHiddenBlock,
|
|
190
239
|
isTranslatableMediaField,
|
|
191
240
|
mediaFilename,
|
|
192
241
|
mediaIconFor,
|
|
@@ -194,13 +243,17 @@ import {
|
|
|
194
243
|
mediaTypeForKey,
|
|
195
244
|
pickTranslatedMedia
|
|
196
245
|
} from "#nuxt-cms";
|
|
197
|
-
import { computed, ref, useState } from "#imports";
|
|
246
|
+
import { computed, ref, useState, watch } from "#imports";
|
|
198
247
|
import { useCmsMediaKeys } from "../../composables/cms-media-keys";
|
|
248
|
+
import { useCmsOverlay } from "../../composables/cms-overlay";
|
|
199
249
|
import { useCmsRuntime } from "../../composables/cms-runtime";
|
|
250
|
+
import { useCmsTheme } from "../../composables/cms-theme";
|
|
200
251
|
import { fieldIcon } from "../../utils/ui";
|
|
201
252
|
const props = defineProps({
|
|
202
253
|
field: { type: Object, required: true },
|
|
203
|
-
locale: { type: String, required: false }
|
|
254
|
+
locale: { type: String, required: false },
|
|
255
|
+
entryName: { type: String, required: false },
|
|
256
|
+
fieldKey: { type: String, required: false }
|
|
204
257
|
});
|
|
205
258
|
const model = defineModel({ type: [Array, null], ...{ required: true } });
|
|
206
259
|
const { mediaBaseUrl, i18n } = useCmsRuntime();
|
|
@@ -217,6 +270,23 @@ const mediaOnly = computed(() => {
|
|
|
217
270
|
if (field.type !== "media" || isTranslatableMediaField(field)) return null;
|
|
218
271
|
return { type, key, field };
|
|
219
272
|
});
|
|
273
|
+
const visual = computed(() => {
|
|
274
|
+
const list = Object.values(blocks.value);
|
|
275
|
+
return !mediaOnly.value && !!props.entryName && !!props.fieldKey && !!list.length && list.every((block) => typeof block.component === "string" && !!block.component.trim());
|
|
276
|
+
});
|
|
277
|
+
const visualOpen = ref(false);
|
|
278
|
+
const theme = useCmsTheme();
|
|
279
|
+
const visualOverlay = useCmsOverlay(() => {
|
|
280
|
+
visualOpen.value = false;
|
|
281
|
+
});
|
|
282
|
+
watch(visualOpen, (open) => {
|
|
283
|
+
if (open) visualOverlay.activate();
|
|
284
|
+
else visualOverlay.deactivate();
|
|
285
|
+
});
|
|
286
|
+
function onVisualUpdate(state) {
|
|
287
|
+
const value = props.fieldKey ? state[props.fieldKey] : null;
|
|
288
|
+
model.value = Array.isArray(value) && value.length ? value : null;
|
|
289
|
+
}
|
|
220
290
|
const addMediaLabel = computed(() => {
|
|
221
291
|
const types = [mediaOnly.value?.field.mediaType ?? []].flat();
|
|
222
292
|
return types.length === 1 && types[0] === "image" ? "Add photo" : "Add media";
|
|
@@ -293,7 +363,7 @@ function summaryOf(item) {
|
|
|
293
363
|
return "";
|
|
294
364
|
}
|
|
295
365
|
function isHidden(item) {
|
|
296
|
-
return
|
|
366
|
+
return isHiddenBlock(item);
|
|
297
367
|
}
|
|
298
368
|
function updateField(index, key, value) {
|
|
299
369
|
const current = items.value[index];
|
|
@@ -406,6 +476,16 @@ function moveCurrent(delta) {
|
|
|
406
476
|
move(editorIndex.value, delta);
|
|
407
477
|
editorIndex.value = next;
|
|
408
478
|
}
|
|
479
|
+
function toggleHiddenCurrent() {
|
|
480
|
+
if (editorIndex.value === null || !editorItem.value) return;
|
|
481
|
+
const index = editorIndex.value;
|
|
482
|
+
const current = items.value[index];
|
|
483
|
+
if (!current) return;
|
|
484
|
+
const { hidden: _hidden, ...rest } = current;
|
|
485
|
+
const updated = isHidden(current) ? rest : { ...rest, hidden: true };
|
|
486
|
+
uids.set(updated, uidFor(current));
|
|
487
|
+
model.value = items.value.map((item, i) => i === index ? updated : item);
|
|
488
|
+
}
|
|
409
489
|
function duplicateCurrent() {
|
|
410
490
|
if (editorIndex.value === null) return;
|
|
411
491
|
duplicate(editorIndex.value);
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
v-model="state[key]"
|
|
71
71
|
:field="fields[key]"
|
|
72
72
|
:locale="localeFor(key)"
|
|
73
|
-
:slug-source="
|
|
74
|
-
|
|
75
|
-
"
|
|
73
|
+
:slug-source="slugSource(fields[key])"
|
|
74
|
+
:entry-name="entryName"
|
|
75
|
+
:field-key="key"
|
|
76
76
|
/>
|
|
77
77
|
</CmsFormField>
|
|
78
78
|
</div>
|
|
@@ -99,7 +99,8 @@ import {
|
|
|
99
99
|
hasTranslatableBlockFields,
|
|
100
100
|
isFieldVisible,
|
|
101
101
|
isTranslatableField,
|
|
102
|
-
resolveFormLayout
|
|
102
|
+
resolveFormLayout,
|
|
103
|
+
slugSourceValue
|
|
103
104
|
} from "#nuxt-cms";
|
|
104
105
|
import { computed, nextTick, ref, watch } from "#imports";
|
|
105
106
|
import { buildEntrySchema } from "../../../shared/validation";
|
|
@@ -168,6 +169,10 @@ function onError() {
|
|
|
168
169
|
if (key) void nextTick(() => revealField(key));
|
|
169
170
|
emit("error");
|
|
170
171
|
}
|
|
172
|
+
function slugSource(field) {
|
|
173
|
+
if (!field.from) return void 0;
|
|
174
|
+
return slugSourceValue(props.fields[field.from], state.value[field.from], i18n.defaultLocale);
|
|
175
|
+
}
|
|
171
176
|
function hasLocaleSwitch(field) {
|
|
172
177
|
return isTranslatableField(field) || hasTranslatableBlockFields(field);
|
|
173
178
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
v-model="blocksValue"
|
|
11
11
|
:field="field"
|
|
12
12
|
:locale="locale"
|
|
13
|
+
:entry-name="entryName"
|
|
14
|
+
:field-key="fieldKey"
|
|
13
15
|
/>
|
|
14
16
|
<CmsRichTextField v-else-if="field.type === 'richtext'" v-model="strOrNull" />
|
|
15
17
|
<CmsTextarea
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
:placeholder="field.placeholder"
|
|
51
53
|
/>
|
|
52
54
|
<CmsInput v-else-if="field.type === 'date'" v-model="str" type="date" />
|
|
55
|
+
<CmsInput v-else-if="field.type === 'datetime'" v-model="localDateTime" type="datetime-local" />
|
|
53
56
|
<CmsRelationField
|
|
54
57
|
v-else-if="field.type === 'relation'"
|
|
55
58
|
v-model="relationValue"
|
|
@@ -66,7 +69,9 @@ import { computed } from "#imports";
|
|
|
66
69
|
const props = defineProps({
|
|
67
70
|
field: { type: Object, required: true },
|
|
68
71
|
slugSource: { type: [String, null], required: false },
|
|
69
|
-
locale: { type: String, required: false }
|
|
72
|
+
locale: { type: String, required: false },
|
|
73
|
+
entryName: { type: String, required: false },
|
|
74
|
+
fieldKey: { type: String, required: false }
|
|
70
75
|
});
|
|
71
76
|
const model = defineModel({ type: null, ...{ required: true } });
|
|
72
77
|
const selectItems = computed(() => [
|
|
@@ -97,6 +102,18 @@ const translatable = proxy((v) => v);
|
|
|
97
102
|
const blocksValue = proxy(
|
|
98
103
|
(v) => v
|
|
99
104
|
);
|
|
105
|
+
function toLocalDateTime(value) {
|
|
106
|
+
if (typeof value !== "string" || !value) return "";
|
|
107
|
+
const date = new Date(value);
|
|
108
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
109
|
+
return new Date(date.getTime() - date.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
|
|
110
|
+
}
|
|
111
|
+
function fromLocalDateTime(value) {
|
|
112
|
+
if (!value) return null;
|
|
113
|
+
const date = new Date(value);
|
|
114
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
115
|
+
}
|
|
116
|
+
const localDateTime = proxy(toLocalDateTime, fromLocalDateTime);
|
|
100
117
|
const relationValue = proxy((v) => v);
|
|
101
118
|
const jsonValue = proxy((v) => v);
|
|
102
119
|
</script>
|
|
@@ -69,7 +69,8 @@ import { errorMessage } from "../../utils/ui";
|
|
|
69
69
|
const KIND_LABELS = {
|
|
70
70
|
collection: "Collection",
|
|
71
71
|
single: "Single",
|
|
72
|
-
page: "Pages"
|
|
72
|
+
page: "Pages",
|
|
73
|
+
content: "Content"
|
|
73
74
|
};
|
|
74
75
|
const STATUS_LABELS = {
|
|
75
76
|
idle: "Changes save automatically",
|
|
@@ -85,7 +86,7 @@ const toast = useCmsToast();
|
|
|
85
86
|
const entry = computed(() => cmsConfig[props.name]);
|
|
86
87
|
const fields = computed(() => entryFields(props.name));
|
|
87
88
|
const parts = computed(() => [
|
|
88
|
-
...entry.value.kind === "collection" ? [{ id: "columns", label: "Table columns" }] : [],
|
|
89
|
+
...entry.value.kind === "collection" || entry.value.kind === "content" ? [{ id: "columns", label: "Table columns" }] : [],
|
|
89
90
|
{ id: "layout", label: "Form layout" }
|
|
90
91
|
]);
|
|
91
92
|
const activePart = ref(parts.value[0].id);
|
|
@@ -144,7 +144,8 @@ const emit = defineEmits(["logout"]);
|
|
|
144
144
|
const KIND_ICONS = {
|
|
145
145
|
collection: "square-3-stack-3d",
|
|
146
146
|
single: "document-text",
|
|
147
|
-
page: "window"
|
|
147
|
+
page: "window",
|
|
148
|
+
content: "newspaper"
|
|
148
149
|
};
|
|
149
150
|
const ENTRY_PREFIX = "entry:";
|
|
150
151
|
const entries = Object.entries(cmsConfig).map(([name, entry]) => ({
|