rimelight-components 2.1.11 → 2.1.13
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 +1 -1
- package/dist/runtime/components/page/PageEditor.d.vue.ts +8 -1
- package/dist/runtime/components/page/PageEditor.vue +236 -72
- package/dist/runtime/components/page/PageEditor.vue.d.ts +8 -1
- package/dist/runtime/components/page/PagePropertiesEditor.vue +172 -81
- package/dist/runtime/components/page/PagePropertiesRenderer.vue +6 -23
- package/dist/runtime/components/page/PageRenderer.d.vue.ts +1 -1
- package/dist/runtime/components/page/PageRenderer.vue +2 -2
- package/dist/runtime/components/page/PageRenderer.vue.d.ts +1 -1
- package/dist/runtime/components/page/PageTOC.vue +1 -1
- package/dist/runtime/components/page/modals/CreatePageModal.d.vue.ts +35 -0
- package/dist/runtime/components/page/modals/CreatePageModal.vue +108 -0
- package/dist/runtime/components/page/modals/CreatePageModal.vue.d.ts +35 -0
- package/dist/runtime/components/page/modals/DeletePageModal.d.vue.ts +28 -0
- package/dist/runtime/components/page/modals/DeletePageModal.vue +84 -0
- package/dist/runtime/components/page/modals/DeletePageModal.vue.d.ts +28 -0
- package/dist/runtime/composables/index.d.ts +1 -0
- package/dist/runtime/composables/index.js +1 -0
- package/dist/runtime/composables/index.mjs +1 -0
- package/dist/runtime/composables/useInfobox.d.ts +8 -0
- package/dist/runtime/composables/useInfobox.js +31 -0
- package/dist/runtime/composables/useInfobox.mjs +31 -0
- package/dist/runtime/composables/usePageEditor.js +2 -4
- package/dist/runtime/composables/usePageEditor.mjs +2 -4
- package/dist/runtime/types/pages.d.ts +1 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { readdirSync } from 'node:fs';
|
|
|
4
4
|
import { basename } from 'node:path';
|
|
5
5
|
|
|
6
6
|
const name = "rimelight-components";
|
|
7
|
-
const version = "2.1.
|
|
7
|
+
const version = "2.1.13";
|
|
8
8
|
const homepage = "https://rimelight.com/tools/rimelight-components";
|
|
9
9
|
|
|
10
10
|
const defaultOptions = {
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Page, type PageSurround, type PageDefinition } from "../../types/index.js";
|
|
2
2
|
interface PageEditorProps {
|
|
3
3
|
isSaving: boolean;
|
|
4
|
+
useSurround?: boolean;
|
|
5
|
+
surround?: PageSurround | null;
|
|
6
|
+
surroundStatus?: 'idle' | 'pending' | 'success' | 'error';
|
|
7
|
+
resolvePage: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
|
|
8
|
+
pageDefinitions: Record<string, PageDefinition>;
|
|
9
|
+
onCreatePage?: (page: Partial<Page>) => Promise<void>;
|
|
10
|
+
onDeletePage?: (id: string) => Promise<void>;
|
|
4
11
|
}
|
|
5
12
|
type __VLS_Props = PageEditorProps;
|
|
6
13
|
type __VLS_ModelProps = {
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { ref, computed, useTemplateRef } from "vue";
|
|
3
|
-
import {
|
|
2
|
+
import { ref, computed, useTemplateRef, provide } from "vue";
|
|
3
|
+
import {} from "../../types";
|
|
4
|
+
import { usePageEditor, usePageRegistry } from "../../composables";
|
|
4
5
|
import { getLocalizedContent } from "../../utils";
|
|
6
|
+
import { useI18n } from "vue-i18n";
|
|
7
|
+
const { getTypeLabelKey } = usePageRegistry();
|
|
8
|
+
const { t, locale } = useI18n();
|
|
5
9
|
const page = defineModel({ type: null, ...{ required: true } });
|
|
6
10
|
const { undo, redo, canUndo, canRedo, captureSnapshot } = usePageEditor(page);
|
|
7
|
-
const { isSaving } = defineProps({
|
|
8
|
-
isSaving: { type: Boolean, required: true }
|
|
11
|
+
const { isSaving, useSurround = false, surroundStatus = "idle", surround = null, resolvePage, onCreatePage, onDeletePage } = defineProps({
|
|
12
|
+
isSaving: { type: Boolean, required: true },
|
|
13
|
+
useSurround: { type: Boolean, required: false },
|
|
14
|
+
surround: { type: [Object, null], required: false },
|
|
15
|
+
surroundStatus: { type: String, required: false },
|
|
16
|
+
resolvePage: { type: Function, required: true },
|
|
17
|
+
pageDefinitions: { type: Object, required: true },
|
|
18
|
+
onCreatePage: { type: Function, required: false },
|
|
19
|
+
onDeletePage: { type: Function, required: false }
|
|
9
20
|
});
|
|
10
21
|
const emit = defineEmits(["save"]);
|
|
11
22
|
const handleSave = () => {
|
|
@@ -20,12 +31,69 @@ defineExpose({
|
|
|
20
31
|
});
|
|
21
32
|
const editorRef = useTemplateRef("editor");
|
|
22
33
|
const showPreview = ref(false);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
provide("page-resolver", resolvePage);
|
|
35
|
+
const previousPage = computed(() => surround?.previous);
|
|
36
|
+
const nextPage = computed(() => surround?.next);
|
|
37
|
+
const hasSurround = computed(() => !!(surround?.previous || surround?.next));
|
|
38
|
+
const containerRef = useTemplateRef("split-container");
|
|
39
|
+
const editorWidth = ref(50);
|
|
40
|
+
const isResizing = ref(false);
|
|
41
|
+
const SNAP_THRESHOLD = 1.5;
|
|
42
|
+
const handleMouseMove = (e) => {
|
|
43
|
+
if (!isResizing.value || !containerRef.value) return;
|
|
44
|
+
const containerRect = containerRef.value.getBoundingClientRect();
|
|
45
|
+
let newWidth = (e.clientX - containerRect.left) / containerRect.width * 100;
|
|
46
|
+
newWidth = Math.min(Math.max(newWidth, 20), 80);
|
|
47
|
+
if (Math.abs(newWidth - 50) < SNAP_THRESHOLD) {
|
|
48
|
+
editorWidth.value = 50;
|
|
49
|
+
} else {
|
|
50
|
+
editorWidth.value = newWidth;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const cursorClass = computed(() => {
|
|
54
|
+
if (isResizing.value) return "cursor-grabbing";
|
|
55
|
+
return "cursor-grab";
|
|
56
|
+
});
|
|
57
|
+
const startResizing = (e) => {
|
|
58
|
+
isResizing.value = true;
|
|
59
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
60
|
+
window.addEventListener("mouseup", stopResizing);
|
|
61
|
+
document.body.style.cursor = "col-resize";
|
|
62
|
+
document.body.style.userSelect = "none";
|
|
63
|
+
};
|
|
64
|
+
const stopResizing = () => {
|
|
65
|
+
isResizing.value = false;
|
|
66
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
67
|
+
window.removeEventListener("mouseup", stopResizing);
|
|
68
|
+
document.body.style.cursor = "";
|
|
69
|
+
document.body.style.userSelect = "";
|
|
70
|
+
};
|
|
71
|
+
const isCreateModalOpen = ref(false);
|
|
72
|
+
const isCreating = ref(false);
|
|
73
|
+
const handleCreateConfirm = async (newPageData) => {
|
|
74
|
+
if (!onCreatePage) return;
|
|
75
|
+
try {
|
|
76
|
+
isCreating.value = true;
|
|
77
|
+
await onCreatePage(newPageData);
|
|
78
|
+
isCreateModalOpen.value = false;
|
|
79
|
+
} finally {
|
|
80
|
+
isCreating.value = false;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const isDeleteModalOpen = ref(false);
|
|
84
|
+
const isDeleting = ref(false);
|
|
85
|
+
const handleDeleteConfirm = async () => {
|
|
86
|
+
if (!onDeletePage || !page.value.id) return;
|
|
87
|
+
try {
|
|
88
|
+
isDeleting.value = true;
|
|
89
|
+
await onDeletePage(page.value.id);
|
|
90
|
+
isDeleteModalOpen.value = false;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error("Failed to delete page:", err);
|
|
93
|
+
} finally {
|
|
94
|
+
isDeleting.value = false;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
29
97
|
</script>
|
|
30
98
|
|
|
31
99
|
<template>
|
|
@@ -69,73 +137,169 @@ const editorPanelClass = computed(() => ({
|
|
|
69
137
|
:loading="isSaving"
|
|
70
138
|
@click="handleSave"
|
|
71
139
|
/>
|
|
140
|
+
<RCCreatePageModal
|
|
141
|
+
:is-open="isCreateModalOpen"
|
|
142
|
+
:definitions="pageDefinitions"
|
|
143
|
+
:loading="isCreating"
|
|
144
|
+
@close="isCreateModalOpen = false"
|
|
145
|
+
@confirm="handleCreateConfirm"
|
|
146
|
+
>
|
|
147
|
+
<UButton
|
|
148
|
+
icon="lucide:file-plus"
|
|
149
|
+
label="Create Page"
|
|
150
|
+
color="primary"
|
|
151
|
+
size="xs"
|
|
152
|
+
/>
|
|
153
|
+
</RCCreatePageModal>
|
|
154
|
+
<RCDeletePageModal
|
|
155
|
+
:is-open="isDeleteModalOpen"
|
|
156
|
+
:loading="isDeleting"
|
|
157
|
+
:page-title="getLocalizedContent(page.title, locale)"
|
|
158
|
+
@close="isDeleteModalOpen = false"
|
|
159
|
+
@confirm="handleDeleteConfirm"
|
|
160
|
+
>
|
|
161
|
+
<UButton
|
|
162
|
+
icon="lucide:file-plus"
|
|
163
|
+
label="Delete Page"
|
|
164
|
+
color="error"
|
|
165
|
+
size="xs"
|
|
166
|
+
/>
|
|
167
|
+
</RCDeletePageModal>
|
|
72
168
|
</div>
|
|
73
169
|
</template>
|
|
74
170
|
</UHeader>
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
171
|
+
<main
|
|
172
|
+
ref="split-container"
|
|
173
|
+
class="flex w-full overflow-hidden"
|
|
78
174
|
>
|
|
79
|
-
<div
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
175
|
+
<div
|
|
176
|
+
class="h-full overflow-y-auto"
|
|
177
|
+
:style="{ width: showPreview ? `${editorWidth}%` : '100%' }"
|
|
178
|
+
>
|
|
179
|
+
<UContainer class="flex flex-col py-16">
|
|
180
|
+
<div class="grid grid-cols-1 lg:grid-cols-24 gap-xl items-start">
|
|
181
|
+
<RCPageTOC
|
|
182
|
+
:page-blocks="page.blocks"
|
|
183
|
+
:levels="[2, 3, 4]"
|
|
184
|
+
class="hidden lg:flex lg:col-span-4 sticky top-20 self-start"
|
|
185
|
+
>
|
|
186
|
+
<template #bottom> </template>
|
|
187
|
+
</RCPageTOC>
|
|
188
|
+
<RCPagePropertiesEditor v-model="page" class="order-1 lg:order-2 lg:col-span-6" />
|
|
189
|
+
<div class="order-2 lg:order-1 lg:col-span-14 flex flex-col gap-xl">
|
|
190
|
+
<NuxtImg
|
|
191
|
+
v-if="page.banner?.src"
|
|
192
|
+
:src="page.banner?.src"
|
|
193
|
+
:alt="page.banner?.alt"
|
|
194
|
+
class="rounded-xl w-full object-cover"
|
|
195
|
+
/>
|
|
196
|
+
<UPageHeader
|
|
197
|
+
:headline="t(getTypeLabelKey(page.type))"
|
|
198
|
+
:description="getLocalizedContent(page.description, 'en') ?? ''"
|
|
199
|
+
:ui="{ root: 'pt-0' }"
|
|
200
|
+
>
|
|
201
|
+
<template #title>
|
|
202
|
+
<div class="flex flex-row gap-sm">
|
|
203
|
+
<NuxtImg
|
|
204
|
+
v-if="page.icon?.src"
|
|
205
|
+
:src="page.icon?.src"
|
|
206
|
+
:alt="page.icon?.alt"
|
|
207
|
+
class="rounded-full w-12 h-12 object-cover"
|
|
208
|
+
/>
|
|
209
|
+
<h1>{{ getLocalizedContent(page.title, locale) }}</h1>
|
|
210
|
+
</div>
|
|
211
|
+
</template>
|
|
212
|
+
</UPageHeader>
|
|
213
|
+
<RCBlockEditor
|
|
214
|
+
ref="editor"
|
|
215
|
+
v-model="page.blocks"
|
|
216
|
+
@mutation="captureSnapshot"
|
|
217
|
+
/>
|
|
218
|
+
<template v-if="useSurround">
|
|
219
|
+
<div v-if="surroundStatus === 'pending'" class="grid grid-cols-1 gap-md sm:grid-cols-2">
|
|
220
|
+
<USkeleton class="h-48 w-full rounded-xl" />
|
|
221
|
+
<USkeleton class="h-48 w-full rounded-xl" />
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<LazyRCPageSurround
|
|
225
|
+
v-else-if="surroundStatus === 'success' && hasSurround"
|
|
226
|
+
hydrate-on-visible
|
|
227
|
+
:pageType="getTypeLabelKey(page.type)"
|
|
228
|
+
:previousTitle="getLocalizedContent(previousPage?.title, locale)"
|
|
229
|
+
:previousDescription="getLocalizedContent(previousPage?.description, locale)"
|
|
230
|
+
:previousTo="`/${previousPage?.slug}`"
|
|
231
|
+
:nextTitle="getLocalizedContent(nextPage?.title, locale)"
|
|
232
|
+
:nextDescription="getLocalizedContent(nextPage?.description, locale)"
|
|
233
|
+
:nextTo="`/${nextPage?.slug}`"
|
|
234
|
+
/>
|
|
235
|
+
|
|
236
|
+
<USeparator />
|
|
237
|
+
|
|
238
|
+
<div class="flex flex-col gap-xs text-xs text-dimmed p-xl">
|
|
239
|
+
<h6>Metadata</h6>
|
|
240
|
+
<span>Page ID: {{ page.id }}</span>
|
|
241
|
+
<span
|
|
242
|
+
>Created At:
|
|
243
|
+
<NuxtTime
|
|
244
|
+
:datetime="page.created_at ?? ''"
|
|
245
|
+
year="numeric"
|
|
246
|
+
month="numeric"
|
|
247
|
+
day="numeric"
|
|
248
|
+
hour="numeric"
|
|
249
|
+
minute="numeric"
|
|
250
|
+
second="numeric"
|
|
251
|
+
time-zone-name="short"
|
|
252
|
+
/></span>
|
|
253
|
+
<span
|
|
254
|
+
>Posted At:
|
|
255
|
+
<NuxtTime
|
|
256
|
+
:datetime="page.created_at ?? ''"
|
|
257
|
+
year="numeric"
|
|
258
|
+
month="numeric"
|
|
259
|
+
day="numeric"
|
|
260
|
+
hour="numeric"
|
|
261
|
+
minute="numeric"
|
|
262
|
+
second="numeric"
|
|
263
|
+
time-zone-name="short"
|
|
264
|
+
/></span>
|
|
265
|
+
<span
|
|
266
|
+
>Updated At:
|
|
267
|
+
<NuxtTime
|
|
268
|
+
:datetime="page.created_at ?? ''"
|
|
269
|
+
year="numeric"
|
|
270
|
+
month="numeric"
|
|
271
|
+
day="numeric"
|
|
272
|
+
hour="numeric"
|
|
273
|
+
minute="numeric"
|
|
274
|
+
second="numeric"
|
|
275
|
+
time-zone-name="short"
|
|
276
|
+
/></span>
|
|
277
|
+
</div>
|
|
278
|
+
</template>
|
|
279
|
+
</div>
|
|
133
280
|
</div>
|
|
134
|
-
</
|
|
281
|
+
</UContainer>
|
|
135
282
|
</div>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
283
|
+
|
|
284
|
+
<div
|
|
285
|
+
v-if="showPreview"
|
|
286
|
+
:class="cursorClass"
|
|
287
|
+
class="relative flex flex-col items-center justify-center w-6 cursor-col-resize group px-1 py-16"
|
|
288
|
+
@mousedown="startResizing"
|
|
289
|
+
@dblclick="editorWidth = 50"
|
|
290
|
+
>
|
|
291
|
+
<USeparator
|
|
292
|
+
orientation="vertical"
|
|
293
|
+
:ui="{}"
|
|
294
|
+
/>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div
|
|
298
|
+
v-if="showPreview"
|
|
299
|
+
class="h-full overflow-y-auto"
|
|
300
|
+
:style="{ width: `${100 - editorWidth}%` }"
|
|
301
|
+
>
|
|
302
|
+
<RCPageRenderer v-model="page" :resolve-page="resolvePage" />
|
|
139
303
|
</div>
|
|
140
|
-
</
|
|
304
|
+
</main>
|
|
141
305
|
</template>
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Page, type PageSurround, type PageDefinition } from "../../types/index.js";
|
|
2
2
|
interface PageEditorProps {
|
|
3
3
|
isSaving: boolean;
|
|
4
|
+
useSurround?: boolean;
|
|
5
|
+
surround?: PageSurround | null;
|
|
6
|
+
surroundStatus?: 'idle' | 'pending' | 'success' | 'error';
|
|
7
|
+
resolvePage: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
|
|
8
|
+
pageDefinitions: Record<string, PageDefinition>;
|
|
9
|
+
onCreatePage?: (page: Partial<Page>) => Promise<void>;
|
|
10
|
+
onDeletePage?: (id: string) => Promise<void>;
|
|
4
11
|
}
|
|
5
12
|
type __VLS_Props = PageEditorProps;
|
|
6
13
|
type __VLS_ModelProps = {
|
|
@@ -1,101 +1,192 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import { useI18n } from "vue-i18n";
|
|
3
|
-
import { usePageRegistry } from "../../composables";
|
|
4
|
+
import { usePageRegistry, useInfobox } from "../../composables";
|
|
4
5
|
import { getLocalizedContent } from "../../utils";
|
|
6
|
+
import {} from "@nuxt/ui/components/Tabs.vue";
|
|
5
7
|
import {} from "../../types";
|
|
6
|
-
const { getTypeLabelKey } = usePageRegistry();
|
|
7
8
|
const page = defineModel({ type: null, ...{ required: true } });
|
|
9
|
+
const { getTypeLabelKey } = usePageRegistry();
|
|
10
|
+
const { isFieldVisible, shouldRenderGroup, getSortedFields, getSortedGroups } = useInfobox(page.value.properties);
|
|
8
11
|
const { locale, t } = useI18n();
|
|
9
|
-
const
|
|
10
|
-
if (!
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const imageTabs = computed(() => {
|
|
13
|
+
if (!page.value.images?.length) return [];
|
|
14
|
+
return page.value.images.map((img, index) => {
|
|
15
|
+
const localizedName = getLocalizedContent(img.name, locale.value);
|
|
16
|
+
return {
|
|
17
|
+
label: localizedName || `${t("label_image")} ${index + 1}`,
|
|
18
|
+
value: `image-${index}`,
|
|
19
|
+
img
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
const updateTextArray = (schema, vals) => {
|
|
24
|
+
schema.value = vals.map((str) => ({
|
|
25
|
+
...schema.value.find((i) => i.en === str),
|
|
26
|
+
// Preserve other locales if they exist
|
|
27
|
+
en: str
|
|
28
|
+
}));
|
|
15
29
|
};
|
|
16
30
|
</script>
|
|
17
31
|
|
|
18
32
|
<template>
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
<aside class="flex flex-col gap-md">
|
|
34
|
+
<UCard
|
|
35
|
+
variant="soft"
|
|
36
|
+
:ui="{ root: 'divide-none', header: 'bg-accented text-center', body: 'p-0 sm:p-0 bg-muted' }"
|
|
37
|
+
>
|
|
38
|
+
<template #header>
|
|
39
|
+
<div class="flex flex-col gap-xs items-center">
|
|
40
|
+
<NuxtImg
|
|
41
|
+
v-if="page.icon?.src"
|
|
42
|
+
:src="page.icon?.src"
|
|
43
|
+
:alt="page.icon?.alt"
|
|
44
|
+
class="rounded-full w-12 h-12 object-cover"
|
|
45
|
+
/>
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{{ group.label[locale] }}
|
|
34
|
-
</span>
|
|
35
|
-
<div class="h-px flex-1 bg-border/50"></div>
|
|
36
|
-
</div>
|
|
47
|
+
<h3>
|
|
48
|
+
{{ getLocalizedContent(page.title, locale) }}
|
|
49
|
+
</h3>
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
<template v-for="[fieldKey, schema] in getSortedFields(group.fields)" :key="fieldKey">
|
|
40
|
-
<UFormField
|
|
41
|
-
v-if="isFieldVisible(schema)"
|
|
42
|
-
:label="getLocalizedContent(schema.label, locale)"
|
|
43
|
-
:name="fieldKey"
|
|
44
|
-
>
|
|
45
|
-
<UInput
|
|
46
|
-
v-if="schema.type === 'text'"
|
|
47
|
-
v-model="schema.value[locale]"
|
|
48
|
-
variant="subtle"
|
|
49
|
-
placeholder="..."
|
|
50
|
-
/>
|
|
51
|
+
<span class="text-sm">{{ t(getTypeLabelKey(page.type)) }}</span>
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
v-
|
|
55
|
-
|
|
56
|
-
variant="
|
|
57
|
-
|
|
53
|
+
<div v-if="page.tags?.length" class="flex flex-row flex-wrap gap-xs">
|
|
54
|
+
<UBadge
|
|
55
|
+
v-for="tag in page.tags"
|
|
56
|
+
:key="tag[locale]"
|
|
57
|
+
variant="soft"
|
|
58
|
+
size="xs"
|
|
59
|
+
color="neutral"
|
|
60
|
+
>
|
|
61
|
+
{{ tag[locale] }}
|
|
62
|
+
</UBadge>
|
|
63
|
+
</div>
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
v-
|
|
62
|
-
:items="
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
<div v-if="page.images?.length" class="w-full">
|
|
66
|
+
<UTabs
|
|
67
|
+
v-if="page.images.length > 1"
|
|
68
|
+
:items="imageTabs"
|
|
69
|
+
default-value="image-0"
|
|
70
|
+
variant="link"
|
|
71
|
+
size="xs"
|
|
72
|
+
color="neutral"
|
|
73
|
+
class="w-full"
|
|
74
|
+
>
|
|
75
|
+
<template #content="{ item }">
|
|
76
|
+
<NuxtImg :src="item.img.src" :alt="item.img.alt" class="w-full object-cover" />
|
|
77
|
+
</template>
|
|
78
|
+
</UTabs>
|
|
65
79
|
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
<div v-else-if="page.images[0]">
|
|
81
|
+
<NuxtImg
|
|
82
|
+
:src="page.images[0].src"
|
|
83
|
+
:alt="page.images[0].alt"
|
|
84
|
+
class="w-full object-cover"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
<template #default>
|
|
92
|
+
<template v-for="[groupId, group] in getSortedGroups(page.properties)" :key="groupId">
|
|
93
|
+
<UCollapsible v-if="shouldRenderGroup(group, false)" :default-open="group.defaultOpen">
|
|
94
|
+
<template #default>
|
|
95
|
+
<UButton
|
|
96
|
+
:label="getLocalizedContent(group.label, locale)"
|
|
97
|
+
variant="soft"
|
|
98
|
+
trailing-icon="lucide:chevron-down"
|
|
99
|
+
:ui="{
|
|
100
|
+
trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200'
|
|
101
|
+
}"
|
|
102
|
+
block
|
|
103
|
+
class="group rounded-none bg-elevated text-default"
|
|
104
|
+
/>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<template #content>
|
|
108
|
+
<dl class="p-sm flex flex-col gap-xs">
|
|
109
|
+
<template v-for="[fieldKey, schema] in getSortedFields(group.fields)" :key="fieldKey">
|
|
110
|
+
<UFormField
|
|
111
|
+
v-if="isFieldVisible(schema, false)"
|
|
112
|
+
:label="getLocalizedContent(schema.label, locale)"
|
|
113
|
+
:name="fieldKey"
|
|
114
|
+
>
|
|
115
|
+
<UInput
|
|
116
|
+
v-if="schema.type === 'text'"
|
|
117
|
+
v-model="schema.value.en"
|
|
118
|
+
variant="subtle"
|
|
119
|
+
placeholder="Type here..."
|
|
120
|
+
class="w-full"
|
|
121
|
+
/>
|
|
87
122
|
|
|
88
|
-
|
|
123
|
+
<UInput
|
|
124
|
+
v-else-if="schema.type === 'number'"
|
|
125
|
+
v-model.number="schema.value"
|
|
126
|
+
type="number"
|
|
127
|
+
variant="subtle"
|
|
128
|
+
class="w-full"
|
|
129
|
+
/>
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
<USelect
|
|
132
|
+
v-else-if="schema.type === 'enum'"
|
|
133
|
+
v-model="schema.value"
|
|
134
|
+
:items="schema.options || []"
|
|
135
|
+
variant="subtle"
|
|
136
|
+
class="w-full"
|
|
137
|
+
/>
|
|
138
|
+
|
|
139
|
+
<UInputMenu
|
|
140
|
+
v-else-if="schema.type === 'text-array'"
|
|
141
|
+
:model-value="schema.value.map((v) => v.en)"
|
|
142
|
+
@update:model-value="(vals) => updateTextArray(schema, vals)"
|
|
143
|
+
multiple
|
|
144
|
+
creatable
|
|
145
|
+
variant="subtle"
|
|
146
|
+
placeholder="Add item..."
|
|
147
|
+
class="w-full"
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<UInput
|
|
151
|
+
v-else-if="schema.type === 'page'"
|
|
152
|
+
v-model="schema.value"
|
|
153
|
+
icon="lucide:link-2"
|
|
154
|
+
variant="subtle"
|
|
155
|
+
:placeholder="`Select ${schema.allowedPageTypes?.join('/')}`"
|
|
156
|
+
class="w-full"
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
<USelectMenu
|
|
160
|
+
v-else-if="schema.type === 'page-array'"
|
|
161
|
+
v-model="schema.value"
|
|
162
|
+
icon="lucide:link-2"
|
|
163
|
+
variant="subtle"
|
|
164
|
+
:placeholder="`Select ${schema.allowedPageTypes?.join('/')}`"
|
|
165
|
+
class="w-full"
|
|
166
|
+
/>
|
|
167
|
+
</UFormField>
|
|
168
|
+
</template>
|
|
169
|
+
</dl>
|
|
170
|
+
</template>
|
|
171
|
+
</UCollapsible>
|
|
172
|
+
</template>
|
|
173
|
+
</template>
|
|
174
|
+
</UCard>
|
|
175
|
+
<div class="flex flex-col gap-xs">
|
|
176
|
+
<h6>Links</h6>
|
|
177
|
+
<UButton
|
|
178
|
+
v-for="(link, index) in page.links"
|
|
179
|
+
:key="index"
|
|
180
|
+
:label="link.label"
|
|
181
|
+
:icon="link.icon"
|
|
182
|
+
:to="link.to"
|
|
183
|
+
:target="link.to ? '_blank' : void 0"
|
|
184
|
+
:external="!!link.to"
|
|
185
|
+
:variant="link.variant || 'link'"
|
|
186
|
+
:color="link.color || 'neutral'"
|
|
187
|
+
size="sm"
|
|
188
|
+
:ui="{ base: 'pl-0' }"
|
|
98
189
|
/>
|
|
99
|
-
</
|
|
100
|
-
</
|
|
190
|
+
</div>
|
|
191
|
+
</aside>
|
|
101
192
|
</template>
|
|
@@ -1,36 +1,19 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import { getLocalizedContent } from "../../utils";
|
|
4
|
-
import { usePageRegistry } from "../../composables";
|
|
4
|
+
import { usePageRegistry, useInfobox } from "../../composables";
|
|
5
5
|
import { useToast } from "@nuxt/ui/composables";
|
|
6
6
|
import {} from "@nuxt/ui/components/Tabs.vue";
|
|
7
7
|
import { useI18n } from "vue-i18n";
|
|
8
8
|
import { useShare, useClipboard } from "@vueuse/core";
|
|
9
9
|
import {} from "../../types";
|
|
10
|
-
const { getTypeLabelKey } = usePageRegistry();
|
|
11
10
|
const page = defineModel({ type: null, ...{ required: true } });
|
|
11
|
+
const { getTypeLabelKey } = usePageRegistry();
|
|
12
|
+
const { isFieldVisible, shouldRenderGroup, getSortedFields, getSortedGroups } = useInfobox(page.value.properties);
|
|
12
13
|
const { t, locale } = useI18n();
|
|
13
14
|
const { share } = useShare();
|
|
14
15
|
const { copy } = useClipboard();
|
|
15
16
|
const toast = useToast();
|
|
16
|
-
const shouldRenderField = (schema) => {
|
|
17
|
-
const isVisible = !schema.visibleIf || schema.visibleIf(page.value.properties);
|
|
18
|
-
if (!isVisible) return false;
|
|
19
|
-
const val = schema.value;
|
|
20
|
-
if (schema.type === "text") {
|
|
21
|
-
return !!val?.[locale.value];
|
|
22
|
-
}
|
|
23
|
-
if (schema.type === "text-array") {
|
|
24
|
-
return Array.isArray(val) && val.length > 0;
|
|
25
|
-
}
|
|
26
|
-
return val !== void 0 && val !== null && val !== "";
|
|
27
|
-
};
|
|
28
|
-
const shouldRenderGroup = (group) => {
|
|
29
|
-
return Object.values(group.fields || {}).some((schema) => shouldRenderField(schema));
|
|
30
|
-
};
|
|
31
|
-
const getSortedFields = (fields) => {
|
|
32
|
-
return Object.entries(fields).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
|
|
33
|
-
};
|
|
34
17
|
const sharePage = async () => {
|
|
35
18
|
if (!page.value) {
|
|
36
19
|
return;
|
|
@@ -142,8 +125,8 @@ const imageTabs = computed(() => {
|
|
|
142
125
|
</template>
|
|
143
126
|
|
|
144
127
|
<template #default>
|
|
145
|
-
<template v-for="
|
|
146
|
-
<UCollapsible v-if="shouldRenderGroup(group)" :default-open="group.defaultOpen">
|
|
128
|
+
<template v-for="[groupId, group] in getSortedGroups(page.properties)" :key="groupId">
|
|
129
|
+
<UCollapsible v-if="shouldRenderGroup(group, true)" :default-open="group.defaultOpen">
|
|
147
130
|
<template #default>
|
|
148
131
|
<UButton
|
|
149
132
|
:label="getLocalizedContent(group.label, locale)"
|
|
@@ -164,7 +147,7 @@ const imageTabs = computed(() => {
|
|
|
164
147
|
:key="fieldKey"
|
|
165
148
|
>
|
|
166
149
|
<div
|
|
167
|
-
v-if="
|
|
150
|
+
v-if="isFieldVisible(schema, true)"
|
|
168
151
|
class="grid grid-cols-3 gap-xs items-baseline"
|
|
169
152
|
>
|
|
170
153
|
<dt class="text-xs font-semibold text-dimmed">
|
|
@@ -3,7 +3,7 @@ interface PageRendererProps {
|
|
|
3
3
|
useSurround?: boolean;
|
|
4
4
|
surround?: PageSurround | null;
|
|
5
5
|
surroundStatus?: 'idle' | 'pending' | 'success' | 'error';
|
|
6
|
-
resolvePage
|
|
6
|
+
resolvePage: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
|
|
7
7
|
}
|
|
8
8
|
type __VLS_Props = PageRendererProps;
|
|
9
9
|
type __VLS_ModelProps = {
|
|
@@ -11,7 +11,7 @@ const { useSurround = false, surroundStatus = "idle", surround = null, resolvePa
|
|
|
11
11
|
useSurround: { type: Boolean, required: false },
|
|
12
12
|
surround: { type: [Object, null], required: false },
|
|
13
13
|
surroundStatus: { type: String, required: false },
|
|
14
|
-
resolvePage: { type: Function, required:
|
|
14
|
+
resolvePage: { type: Function, required: true }
|
|
15
15
|
});
|
|
16
16
|
const emit = defineEmits([]);
|
|
17
17
|
provide("page-resolver", resolvePage);
|
|
@@ -26,7 +26,7 @@ const hasSurround = computed(() => !!(surround?.previous || surround?.next));
|
|
|
26
26
|
<RCPageTOC
|
|
27
27
|
:page-blocks="page.blocks"
|
|
28
28
|
:levels="[2, 3, 4]"
|
|
29
|
-
class="hidden lg:flex lg:col-span-4 sticky top-
|
|
29
|
+
class="hidden lg:flex lg:col-span-4 sticky top-16"
|
|
30
30
|
>
|
|
31
31
|
<template #bottom> </template>
|
|
32
32
|
</RCPageTOC>
|
|
@@ -3,7 +3,7 @@ interface PageRendererProps {
|
|
|
3
3
|
useSurround?: boolean;
|
|
4
4
|
surround?: PageSurround | null;
|
|
5
5
|
surroundStatus?: 'idle' | 'pending' | 'success' | 'error';
|
|
6
|
-
resolvePage
|
|
6
|
+
resolvePage: (id: string) => Promise<Pick<Page, 'title' | 'icon' | 'slug'>>;
|
|
7
7
|
}
|
|
8
8
|
type __VLS_Props = PageRendererProps;
|
|
9
9
|
type __VLS_ModelProps = {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type PageDefinition, type Page } from "../../../types/index.js";
|
|
2
|
+
export interface CreatePageModalProps {
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
definitions: Record<string, PageDefinition>;
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface CreatePageModalEmits {
|
|
8
|
+
close: [];
|
|
9
|
+
confirm: [page: Partial<Page>];
|
|
10
|
+
}
|
|
11
|
+
declare var __VLS_10: {};
|
|
12
|
+
type __VLS_Slots = {} & {
|
|
13
|
+
default?: (props: typeof __VLS_10) => any;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_base: import("vue").DefineComponent<CreatePageModalProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
|
+
close: () => any;
|
|
17
|
+
confirm: (page: Partial<{
|
|
18
|
+
type: "Default";
|
|
19
|
+
properties: import("../../../types/index.js").BasePageProperties;
|
|
20
|
+
} & import("../../../types/index.js").BasePage>) => any;
|
|
21
|
+
}, string, import("vue").PublicProps, Readonly<CreatePageModalProps> & Readonly<{
|
|
22
|
+
onClose?: (() => any) | undefined;
|
|
23
|
+
onConfirm?: ((page: Partial<{
|
|
24
|
+
type: "Default";
|
|
25
|
+
properties: import("../../../types/index.js").BasePageProperties;
|
|
26
|
+
} & import("../../../types/index.js").BasePage>) => any) | undefined;
|
|
27
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
29
|
+
declare const _default: typeof __VLS_export;
|
|
30
|
+
export default _default;
|
|
31
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
32
|
+
new (): {
|
|
33
|
+
$slots: S;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch } from "vue";
|
|
3
|
+
import {} from "../../../types";
|
|
4
|
+
import { tv } from "tailwind-variants";
|
|
5
|
+
import { useI18n } from "vue-i18n";
|
|
6
|
+
const { t } = useI18n();
|
|
7
|
+
const { isOpen, loading, definitions } = defineProps({
|
|
8
|
+
isOpen: { type: Boolean, required: true },
|
|
9
|
+
definitions: { type: Object, required: true },
|
|
10
|
+
loading: { type: Boolean, required: false }
|
|
11
|
+
});
|
|
12
|
+
const emits = defineEmits(["close", "confirm"]);
|
|
13
|
+
const createPageModalStyles = tv({
|
|
14
|
+
slots: {
|
|
15
|
+
base: "flex flex-col gap-6",
|
|
16
|
+
group: "border border-neutral-200 rounded-lg p-4 bg-white shadow-sm",
|
|
17
|
+
groupTitle: "text-lg font-bold text-neutral-900 mb-4",
|
|
18
|
+
field: "flex flex-col gap-1.5",
|
|
19
|
+
label: "text-sm font-medium text-neutral-700",
|
|
20
|
+
input: "w-full px-3 py-2 rounded-md border border-neutral-300 focus:ring-2 focus:ring-primary-500 transition-all outline-hidden",
|
|
21
|
+
select: "w-full px-3 py-2 rounded-md border border-neutral-300 bg-white"
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const styles = createPageModalStyles();
|
|
25
|
+
const selectedType = ref("");
|
|
26
|
+
const title = ref("");
|
|
27
|
+
const slug = ref("");
|
|
28
|
+
const typeOptions = computed(() => {
|
|
29
|
+
return Object.entries(definitions).map(([key, def]) => ({
|
|
30
|
+
label: t(def.typeLabelKey),
|
|
31
|
+
value: key
|
|
32
|
+
}));
|
|
33
|
+
});
|
|
34
|
+
watch(title, (newTitle) => {
|
|
35
|
+
slug.value = newTitle.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
36
|
+
});
|
|
37
|
+
const handleConfirm = () => {
|
|
38
|
+
if (!selectedType.value) return;
|
|
39
|
+
const definition = definitions[selectedType.value];
|
|
40
|
+
if (!definition) {
|
|
41
|
+
console.error(`Definition for type "${selectedType.value}" not found.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const properties = {};
|
|
45
|
+
Object.entries(definition.properties).forEach(([groupKey, group]) => {
|
|
46
|
+
properties[groupKey] = {};
|
|
47
|
+
Object.entries(group.fields).forEach(([fieldKey, field]) => {
|
|
48
|
+
properties[groupKey][fieldKey] = field.value;
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
const newPage = {
|
|
52
|
+
type: selectedType.value,
|
|
53
|
+
title: { en: title.value },
|
|
54
|
+
slug: slug.value,
|
|
55
|
+
properties,
|
|
56
|
+
blocks: definition.initialBlocks ? definition.initialBlocks() : []
|
|
57
|
+
};
|
|
58
|
+
emits("confirm", newPage);
|
|
59
|
+
};
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<UModal :model-value="isOpen" @update:model-value="emits('close')">
|
|
64
|
+
<slot/>
|
|
65
|
+
<template #content>
|
|
66
|
+
<UCard>
|
|
67
|
+
<template #header>
|
|
68
|
+
<div class="flex items-center justify-between">
|
|
69
|
+
<h3 class="text-base font-semibold leading-6">Create New Page</h3>
|
|
70
|
+
<UButton color="neutral" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="emits('close')" />
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<div class="space-y-4 py-4">
|
|
75
|
+
<UFormField label="Page Template" required>
|
|
76
|
+
<USelect
|
|
77
|
+
v-model="selectedType"
|
|
78
|
+
:items="typeOptions"
|
|
79
|
+
:placeholder="t('editor.template_placeholder', 'Select a template...')"
|
|
80
|
+
class="w-full"
|
|
81
|
+
/>
|
|
82
|
+
</UFormField>
|
|
83
|
+
|
|
84
|
+
<UFormField label="Title" required>
|
|
85
|
+
<UInput v-model="title" placeholder="e.g. My Awesome Movie" />
|
|
86
|
+
</UFormField>
|
|
87
|
+
|
|
88
|
+
<UFormField label="Slug" required>
|
|
89
|
+
<UInput v-model="slug" placeholder="my-awesome-movie" />
|
|
90
|
+
</UFormField>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<template #footer>
|
|
94
|
+
<div class="flex justify-end gap-2">
|
|
95
|
+
<UButton color="neutral" variant="ghost" label="Cancel" @click="emits('close')" />
|
|
96
|
+
<UButton
|
|
97
|
+
color="primary"
|
|
98
|
+
label="Create & Edit"
|
|
99
|
+
:loading="loading"
|
|
100
|
+
:disabled="!selectedType || !title"
|
|
101
|
+
@click="handleConfirm"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
105
|
+
</UCard>
|
|
106
|
+
</template>
|
|
107
|
+
</UModal>
|
|
108
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type PageDefinition, type Page } from "../../../types/index.js";
|
|
2
|
+
export interface CreatePageModalProps {
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
definitions: Record<string, PageDefinition>;
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface CreatePageModalEmits {
|
|
8
|
+
close: [];
|
|
9
|
+
confirm: [page: Partial<Page>];
|
|
10
|
+
}
|
|
11
|
+
declare var __VLS_10: {};
|
|
12
|
+
type __VLS_Slots = {} & {
|
|
13
|
+
default?: (props: typeof __VLS_10) => any;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_base: import("vue").DefineComponent<CreatePageModalProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
16
|
+
close: () => any;
|
|
17
|
+
confirm: (page: Partial<{
|
|
18
|
+
type: "Default";
|
|
19
|
+
properties: import("../../../types/index.js").BasePageProperties;
|
|
20
|
+
} & import("../../../types/index.js").BasePage>) => any;
|
|
21
|
+
}, string, import("vue").PublicProps, Readonly<CreatePageModalProps> & Readonly<{
|
|
22
|
+
onClose?: (() => any) | undefined;
|
|
23
|
+
onConfirm?: ((page: Partial<{
|
|
24
|
+
type: "Default";
|
|
25
|
+
properties: import("../../../types/index.js").BasePageProperties;
|
|
26
|
+
} & import("../../../types/index.js").BasePage>) => any) | undefined;
|
|
27
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
29
|
+
declare const _default: typeof __VLS_export;
|
|
30
|
+
export default _default;
|
|
31
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
32
|
+
new (): {
|
|
33
|
+
$slots: S;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface DeletePageModalProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
loading?: boolean;
|
|
4
|
+
pageTitle: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DeletePageModalEmits {
|
|
7
|
+
(e: 'close'): void;
|
|
8
|
+
(e: 'confirm'): void;
|
|
9
|
+
}
|
|
10
|
+
declare var __VLS_10: {};
|
|
11
|
+
type __VLS_Slots = {} & {
|
|
12
|
+
default?: (props: typeof __VLS_10) => any;
|
|
13
|
+
};
|
|
14
|
+
declare const __VLS_base: import("vue").DefineComponent<DeletePageModalProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
15
|
+
close: () => any;
|
|
16
|
+
confirm: () => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<DeletePageModalProps> & Readonly<{
|
|
18
|
+
onClose?: (() => any) | undefined;
|
|
19
|
+
onConfirm?: (() => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
22
|
+
declare const _default: typeof __VLS_export;
|
|
23
|
+
export default _default;
|
|
24
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
25
|
+
new (): {
|
|
26
|
+
$slots: S;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, watch } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
const { isOpen, loading, pageTitle } = defineProps({
|
|
5
|
+
isOpen: { type: Boolean, required: true },
|
|
6
|
+
loading: { type: Boolean, required: false },
|
|
7
|
+
pageTitle: { type: String, required: true }
|
|
8
|
+
});
|
|
9
|
+
const emits = defineEmits(["close", "confirm"]);
|
|
10
|
+
const { t } = useI18n();
|
|
11
|
+
const confirmationInput = ref("");
|
|
12
|
+
const CONFIRMATION_TEXT = "DELETE";
|
|
13
|
+
watch(() => isOpen, (val) => {
|
|
14
|
+
if (!val) confirmationInput.value = "";
|
|
15
|
+
});
|
|
16
|
+
const handleConfirm = () => {
|
|
17
|
+
if (confirmationInput.value === CONFIRMATION_TEXT) {
|
|
18
|
+
emits("confirm");
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<UModal :model-value="isOpen" @update:model-value="emits('close')">
|
|
25
|
+
<slot/>
|
|
26
|
+
<template #content>
|
|
27
|
+
<UCard :ui="{ body: 'space-y-4' }">
|
|
28
|
+
<template #header>
|
|
29
|
+
<div class="flex items-center justify-between">
|
|
30
|
+
<h3 class="text-base font-semibold leading-6 text-error-600">
|
|
31
|
+
{{ t("editor.delete_page_title", "Delete Page") }}
|
|
32
|
+
</h3>
|
|
33
|
+
<UButton
|
|
34
|
+
color="neutral"
|
|
35
|
+
variant="ghost"
|
|
36
|
+
icon="lucide:x"
|
|
37
|
+
class="-my-1"
|
|
38
|
+
@click="emits('close')"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<div class="text-sm text-neutral-600 dark:text-neutral-400">
|
|
44
|
+
<p>
|
|
45
|
+
Are you sure you want to delete <strong>{{ pageTitle }}</strong>?
|
|
46
|
+
This action is permanent and cannot be undone.
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<UFormField
|
|
51
|
+
:label="t('editor.delete_confirm_label', `Please type ${CONFIRMATION_TEXT} to confirm`)"
|
|
52
|
+
required
|
|
53
|
+
>
|
|
54
|
+
<UInput
|
|
55
|
+
v-model="confirmationInput"
|
|
56
|
+
:placeholder="CONFIRMATION_TEXT"
|
|
57
|
+
color="error"
|
|
58
|
+
autocomplete="off"
|
|
59
|
+
@keyup.enter="handleConfirm"
|
|
60
|
+
/>
|
|
61
|
+
</UFormField>
|
|
62
|
+
|
|
63
|
+
<template #footer>
|
|
64
|
+
<div class="flex justify-end gap-2">
|
|
65
|
+
<UButton
|
|
66
|
+
color="neutral"
|
|
67
|
+
variant="ghost"
|
|
68
|
+
:label="t('common.cancel', 'Cancel')"
|
|
69
|
+
@click="emits('close')"
|
|
70
|
+
/>
|
|
71
|
+
<UButton
|
|
72
|
+
color="error"
|
|
73
|
+
icon="lucide:trash-2"
|
|
74
|
+
:label="t('editor.delete_button', 'Delete Permanently')"
|
|
75
|
+
:loading="loading"
|
|
76
|
+
:disabled="confirmationInput !== CONFIRMATION_TEXT"
|
|
77
|
+
@click="handleConfirm"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
</UCard>
|
|
82
|
+
</template>
|
|
83
|
+
</UModal>
|
|
84
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface DeletePageModalProps {
|
|
2
|
+
isOpen: boolean;
|
|
3
|
+
loading?: boolean;
|
|
4
|
+
pageTitle: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DeletePageModalEmits {
|
|
7
|
+
(e: 'close'): void;
|
|
8
|
+
(e: 'confirm'): void;
|
|
9
|
+
}
|
|
10
|
+
declare var __VLS_10: {};
|
|
11
|
+
type __VLS_Slots = {} & {
|
|
12
|
+
default?: (props: typeof __VLS_10) => any;
|
|
13
|
+
};
|
|
14
|
+
declare const __VLS_base: import("vue").DefineComponent<DeletePageModalProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
15
|
+
close: () => any;
|
|
16
|
+
confirm: () => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<DeletePageModalProps> & Readonly<{
|
|
18
|
+
onClose?: (() => any) | undefined;
|
|
19
|
+
onConfirm?: (() => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
22
|
+
declare const _default: typeof __VLS_export;
|
|
23
|
+
export default _default;
|
|
24
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
25
|
+
new (): {
|
|
26
|
+
$slots: S;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Property, PropertyGroup, BasePageProperties } from '../types/index.js';
|
|
2
|
+
export declare const useInfobox: (properties: BasePageProperties) => {
|
|
3
|
+
isFieldVisible: (schema: Property, isReadOnly: boolean) => boolean;
|
|
4
|
+
shouldRenderGroup: (group: PropertyGroup, isReadOnly: boolean) => boolean;
|
|
5
|
+
getSortedFields: (fields: Record<string, Property>) => [string, Property<any>][];
|
|
6
|
+
getSortedGroups: (props: BasePageProperties) => [string, PropertyGroup][];
|
|
7
|
+
locale: import("vue").WritableComputedRef<string, string>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useI18n } from "vue-i18n";
|
|
2
|
+
export const useInfobox = (properties) => {
|
|
3
|
+
const { locale } = useI18n();
|
|
4
|
+
const isFieldVisible = (schema, isReadOnly) => {
|
|
5
|
+
const passesLogic = !schema.visibleIf || schema.visibleIf(properties);
|
|
6
|
+
if (!passesLogic) return false;
|
|
7
|
+
if (isReadOnly) {
|
|
8
|
+
const val = schema.value;
|
|
9
|
+
if (schema.type === "text") return !!val?.[locale.value];
|
|
10
|
+
if (schema.type === "text-array") return Array.isArray(val) && val.length > 0;
|
|
11
|
+
return val !== void 0 && val !== null && val !== "";
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
};
|
|
15
|
+
const shouldRenderGroup = (group, isReadOnly) => {
|
|
16
|
+
return Object.values(group.fields).some((schema) => isFieldVisible(schema, isReadOnly));
|
|
17
|
+
};
|
|
18
|
+
const getSortedFields = (fields) => {
|
|
19
|
+
return Object.entries(fields).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
|
|
20
|
+
};
|
|
21
|
+
const getSortedGroups = (props) => {
|
|
22
|
+
return Object.entries(props).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
isFieldVisible,
|
|
26
|
+
shouldRenderGroup,
|
|
27
|
+
getSortedFields,
|
|
28
|
+
getSortedGroups,
|
|
29
|
+
locale
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useI18n } from "vue-i18n";
|
|
2
|
+
export const useInfobox = (properties) => {
|
|
3
|
+
const { locale } = useI18n();
|
|
4
|
+
const isFieldVisible = (schema, isReadOnly) => {
|
|
5
|
+
const passesLogic = !schema.visibleIf || schema.visibleIf(properties);
|
|
6
|
+
if (!passesLogic) return false;
|
|
7
|
+
if (isReadOnly) {
|
|
8
|
+
const val = schema.value;
|
|
9
|
+
if (schema.type === "text") return !!val?.[locale.value];
|
|
10
|
+
if (schema.type === "text-array") return Array.isArray(val) && val.length > 0;
|
|
11
|
+
return val !== void 0 && val !== null && val !== "";
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
};
|
|
15
|
+
const shouldRenderGroup = (group, isReadOnly) => {
|
|
16
|
+
return Object.values(group.fields).some((schema) => isFieldVisible(schema, isReadOnly));
|
|
17
|
+
};
|
|
18
|
+
const getSortedFields = (fields) => {
|
|
19
|
+
return Object.entries(fields).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
|
|
20
|
+
};
|
|
21
|
+
const getSortedGroups = (props) => {
|
|
22
|
+
return Object.entries(props).sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0));
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
isFieldVisible,
|
|
26
|
+
shouldRenderGroup,
|
|
27
|
+
getSortedFields,
|
|
28
|
+
getSortedGroups,
|
|
29
|
+
locale
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -13,14 +13,12 @@ export function usePageEditor(page, maxHistorySize = 100) {
|
|
|
13
13
|
const undo = () => {
|
|
14
14
|
if (history.value.length === 0) return;
|
|
15
15
|
future.value = [JSON.stringify(page.value), ...future.value];
|
|
16
|
-
|
|
17
|
-
page.value = previous;
|
|
16
|
+
page.value = JSON.parse(history.value.pop());
|
|
18
17
|
};
|
|
19
18
|
const redo = () => {
|
|
20
19
|
if (future.value.length === 0) return;
|
|
21
20
|
history.value = [...history.value, JSON.stringify(page.value)];
|
|
22
|
-
|
|
23
|
-
page.value = next;
|
|
21
|
+
page.value = JSON.parse(future.value.shift());
|
|
24
22
|
};
|
|
25
23
|
const canUndo = computed(() => history.value.length > 0);
|
|
26
24
|
const canRedo = computed(() => future.value.length > 0);
|
|
@@ -13,14 +13,12 @@ export function usePageEditor(page, maxHistorySize = 100) {
|
|
|
13
13
|
const undo = () => {
|
|
14
14
|
if (history.value.length === 0) return;
|
|
15
15
|
future.value = [JSON.stringify(page.value), ...future.value];
|
|
16
|
-
|
|
17
|
-
page.value = previous;
|
|
16
|
+
page.value = JSON.parse(history.value.pop());
|
|
18
17
|
};
|
|
19
18
|
const redo = () => {
|
|
20
19
|
if (future.value.length === 0) return;
|
|
21
20
|
history.value = [...history.value, JSON.stringify(page.value)];
|
|
22
|
-
|
|
23
|
-
page.value = next;
|
|
21
|
+
page.value = JSON.parse(future.value.shift());
|
|
24
22
|
};
|
|
25
23
|
const canUndo = computed(() => history.value.length > 0);
|
|
26
24
|
const canRedo = computed(() => future.value.length > 0);
|