includio-cms 0.5.2 → 0.5.5
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/CHANGELOG.md +60 -0
- package/ROADMAP.md +29 -0
- package/dist/admin/api/rest/handler.d.ts +7 -0
- package/dist/admin/api/rest/handler.js +116 -0
- package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
- package/dist/admin/api/rest/middleware/apiKey.js +45 -0
- package/dist/admin/api/rest/routes/collections.d.ts +5 -0
- package/dist/admin/api/rest/routes/collections.js +104 -0
- package/dist/admin/api/rest/routes/entries.d.ts +2 -0
- package/dist/admin/api/rest/routes/entries.js +37 -0
- package/dist/admin/api/rest/routes/languages.d.ts +1 -0
- package/dist/admin/api/rest/routes/languages.js +5 -0
- package/dist/admin/api/rest/routes/schema.d.ts +2 -0
- package/dist/admin/api/rest/routes/schema.js +78 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
- package/dist/admin/api/rest/routes/singletons.js +60 -0
- package/dist/admin/auth-client.d.ts +7 -7
- package/dist/admin/client/collection/collection-entries.svelte +56 -5
- package/dist/admin/client/collection/data-table.svelte +127 -18
- package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
- package/dist/admin/client/entry/entry-form.svelte +1 -0
- package/dist/admin/client/entry/entry.svelte +130 -123
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
- package/dist/admin/components/fields/blocks-field.svelte +142 -112
- package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
- package/dist/admin/components/fields/boolean-field.svelte +28 -38
- package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
- package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/content-field.svelte +4 -17
- package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/date-field.svelte +8 -21
- package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/datetime-field.svelte +8 -21
- package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/field-renderer.svelte +32 -19
- package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
- package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
- package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
- package/dist/admin/components/fields/fields-form.svelte +13 -10
- package/dist/admin/components/fields/file-field.svelte +12 -27
- package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/image-field.svelte +13 -28
- package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/media-field.svelte +15 -30
- package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/number-field.svelte +6 -20
- package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/object-field.svelte +26 -29
- package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
- package/dist/admin/components/fields/radio-field.svelte +8 -20
- package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/relation-field.svelte +28 -40
- package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/richtext-field.svelte +4 -17
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/select-field.svelte +14 -28
- package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/seo-field.svelte +5 -12
- package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
- package/dist/admin/components/fields/simple-array-field.svelte +29 -42
- package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/slug-field.svelte +6 -11
- package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
- package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
- package/dist/admin/components/fields/text-field.svelte +7 -19
- package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
- package/dist/admin/components/fields/url-field.svelte +294 -128
- package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
- package/dist/admin/components/layout/layout-renderer.svelte +8 -6
- package/dist/admin/components/layout/nav-collections.svelte +2 -1
- package/dist/admin/components/layout/nav-forms.svelte +2 -1
- package/dist/admin/components/layout/nav-singletons.svelte +2 -1
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
- package/dist/admin/components/tiptap/content-editor.svelte +13 -2
- package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
- package/dist/admin/components/tiptap/inline-block-node.js +18 -1
- package/dist/admin/components/tiptap/slash-command.js +2 -3
- package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
- package/dist/admin/components/tiptap/standalone-form.js +31 -0
- package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
- package/dist/admin/remote/entry.remote.d.ts +9 -1
- package/dist/admin/remote/entry.remote.js +30 -2
- package/dist/admin/styles/admin.css +10 -0
- package/dist/admin/utils/fieldCondition.d.ts +6 -0
- package/dist/admin/utils/fieldCondition.js +20 -0
- package/dist/cli/scaffold/admin.js +8 -0
- package/dist/components/ui/switch/index.d.ts +2 -0
- package/dist/components/ui/switch/index.js +4 -0
- package/dist/components/ui/switch/switch.svelte +26 -0
- package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
- package/dist/core/cms.d.ts +2 -1
- package/dist/core/cms.js +2 -0
- package/dist/core/fields/fieldSchemaToTs.js +15 -3
- package/dist/core/fields/formFieldSchemaToTs.js +22 -6
- package/dist/core/fields/urlUtils.d.ts +14 -0
- package/dist/core/fields/urlUtils.js +21 -0
- package/dist/core/server/entries/operations/get.js +2 -1
- package/dist/core/server/entries/operations/update.d.ts +1 -0
- package/dist/core/server/entries/operations/update.js +5 -1
- package/dist/core/server/fields/populateEntry.js +43 -0
- package/dist/core/server/fields/resolveImageFields.js +33 -1
- package/dist/core/server/fields/resolveRelationFields.js +46 -0
- package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
- package/dist/core/server/fields/resolveUrlFields.js +65 -0
- package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
- package/dist/core/server/generator/formFields.js +2 -0
- package/dist/core/server/generator/generator.js +25 -1
- package/dist/db-postgres/schema/entry.d.ts +17 -0
- package/dist/db-postgres/schema/entry.js +4 -2
- package/dist/schemas/field/url.d.ts +2 -0
- package/dist/schemas/field/url.js +4 -2
- package/dist/server/auth.d.ts +6 -6
- package/dist/sveltekit/server/handle.js +1 -0
- package/dist/types/cms.d.ts +7 -0
- package/dist/types/collections.d.ts +2 -0
- package/dist/types/entries.d.ts +7 -1
- package/dist/types/fields.d.ts +9 -0
- package/dist/types/formFields.d.ts +15 -2
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +1 -0
- package/dist/updates/0.5.3/index.d.ts +2 -0
- package/dist/updates/0.5.3/index.js +19 -0
- package/dist/updates/0.5.4/index.d.ts +2 -0
- package/dist/updates/0.5.4/index.js +15 -0
- package/dist/updates/0.5.5/index.d.ts +2 -0
- package/dist/updates/0.5.5/index.js +20 -0
- package/dist/updates/index.js +4 -1
- package/package.json +7 -1
- package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
- package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +0 -9
|
@@ -1,25 +1,144 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { NodeViewWrapper } from 'svelte-tiptap';
|
|
3
3
|
import type { NodeViewProps } from '@tiptap/core';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import FieldRenderer from '../fields/field-renderer.svelte';
|
|
5
|
+
import { createStandaloneForm } from './standalone-form.js';
|
|
6
|
+
import type { ObjectField, BlocksField, Field, FieldType } from '../../../types/fields.js';
|
|
7
|
+
import { getContentLanguage } from '../../state/content-language.svelte.js';
|
|
6
8
|
import GripVertical from '@tabler/icons-svelte/icons/grip-vertical';
|
|
7
9
|
import ArrowUp from '@tabler/icons-svelte/icons/arrow-up';
|
|
8
10
|
import ArrowDown from '@tabler/icons-svelte/icons/arrow-down';
|
|
9
11
|
import Trash from '@tabler/icons-svelte/icons/trash';
|
|
12
|
+
import ChevronDown from '@tabler/icons-svelte/icons/chevron-down';
|
|
13
|
+
import { slide } from 'svelte/transition';
|
|
14
|
+
import { onMount, onDestroy } from 'svelte';
|
|
15
|
+
import type { FormPathLeaves } from 'sveltekit-superforms';
|
|
16
|
+
import { evaluateCondition } from '../../utils/fieldCondition.js';
|
|
10
17
|
|
|
11
18
|
let { node, updateAttributes, editor, getPos, deleteNode, selected }: NodeViewProps = $props();
|
|
12
19
|
|
|
20
|
+
let collapsed = $state(false);
|
|
21
|
+
|
|
22
|
+
const contentLanguage = getContentLanguage();
|
|
23
|
+
|
|
24
|
+
const SKIP_TYPES: Set<FieldType> = new Set(['slug', 'seo']);
|
|
25
|
+
|
|
26
|
+
function normalizeBlockData(
|
|
27
|
+
data: Record<string, unknown>,
|
|
28
|
+
fields: Field[],
|
|
29
|
+
langs: string[]
|
|
30
|
+
): Record<string, unknown> {
|
|
31
|
+
const result = { ...data };
|
|
32
|
+
for (const f of fields) {
|
|
33
|
+
const key = f.slug;
|
|
34
|
+
if (['text', 'richtext', 'content'].includes(f.type)) {
|
|
35
|
+
if (typeof result[key] !== 'object' || result[key] == null) {
|
|
36
|
+
const val = result[key] ?? (f.type === 'content' ? null : '');
|
|
37
|
+
result[key] = Object.fromEntries(langs.map((l) => [l, val]));
|
|
38
|
+
}
|
|
39
|
+
} else if (f.type === 'url') {
|
|
40
|
+
const v = result[key] as Record<string, unknown> | undefined;
|
|
41
|
+
if (!v || typeof v !== 'object') {
|
|
42
|
+
result[key] = { id: '', url: {} };
|
|
43
|
+
} else if (!('url' in v) || v.url == null) {
|
|
44
|
+
result[key] = { ...v, url: {} };
|
|
45
|
+
} else if (typeof v.url === 'string') {
|
|
46
|
+
const urlStr = v.url as string;
|
|
47
|
+
const textStr = typeof v.text === 'string' ? v.text : '';
|
|
48
|
+
result[key] = {
|
|
49
|
+
...v,
|
|
50
|
+
url: Object.fromEntries(langs.map((l) => [l, urlStr])),
|
|
51
|
+
text: Object.fromEntries(langs.map((l) => [l, textStr]))
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const urlField = f as import('../../../types/fields.js').UrlField;
|
|
55
|
+
const d = result[key] as Record<string, unknown>;
|
|
56
|
+
if (urlField.text && !d.text) d.text = {};
|
|
57
|
+
if (urlField.newTab && d.newTab === undefined) d.newTab = false;
|
|
58
|
+
if (urlField.rel && d.rel === undefined) d.rel = '';
|
|
59
|
+
} else if (f.type === 'blocks') {
|
|
60
|
+
const bf = f as BlocksField;
|
|
61
|
+
if (Array.isArray(result[key])) {
|
|
62
|
+
result[key] = (result[key] as any[]).map((item) => ({
|
|
63
|
+
...item,
|
|
64
|
+
data: normalizeBlockData(
|
|
65
|
+
item.data ?? {},
|
|
66
|
+
bf.of.find((d: ObjectField) => d.slug === item.slug)?.fields ?? [],
|
|
67
|
+
langs
|
|
68
|
+
)
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
} else if (f.type === 'object') {
|
|
72
|
+
const of_ = f as ObjectField;
|
|
73
|
+
if (result[key] && typeof result[key] === 'object' && 'data' in (result[key] as object)) {
|
|
74
|
+
const obj = result[key] as Record<string, unknown>;
|
|
75
|
+
result[key] = { ...obj, data: normalizeBlockData(obj.data as Record<string, unknown>, of_.fields, langs) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function denormalizeBlockData(
|
|
83
|
+
data: Record<string, unknown>,
|
|
84
|
+
fields: Field[],
|
|
85
|
+
currentLang: string
|
|
86
|
+
): Record<string, unknown> {
|
|
87
|
+
const result = { ...data };
|
|
88
|
+
for (const f of fields) {
|
|
89
|
+
const key = f.slug;
|
|
90
|
+
const val = result[key];
|
|
91
|
+
if (['text', 'richtext'].includes(f.type)) {
|
|
92
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
93
|
+
result[key] = (val as Record<string, unknown>)[currentLang] ?? '';
|
|
94
|
+
}
|
|
95
|
+
} else if (f.type === 'url' && val && typeof val === 'object') {
|
|
96
|
+
const v = val as Record<string, unknown>;
|
|
97
|
+
const flat: Record<string, unknown> = {};
|
|
98
|
+
if (v.id !== undefined) flat.id = v.id;
|
|
99
|
+
flat.url =
|
|
100
|
+
v.url && typeof v.url === 'object'
|
|
101
|
+
? (v.url as Record<string, string>)[currentLang] ?? ''
|
|
102
|
+
: v.url ?? '';
|
|
103
|
+
if ('text' in v) {
|
|
104
|
+
flat.text =
|
|
105
|
+
v.text && typeof v.text === 'object'
|
|
106
|
+
? (v.text as Record<string, string>)[currentLang] ?? ''
|
|
107
|
+
: v.text ?? '';
|
|
108
|
+
}
|
|
109
|
+
if (v.newTab !== undefined) flat.newTab = v.newTab;
|
|
110
|
+
if (v.rel !== undefined) flat.rel = v.rel;
|
|
111
|
+
result[key] = flat;
|
|
112
|
+
} else if (f.type === 'blocks') {
|
|
113
|
+
const bf = f as BlocksField;
|
|
114
|
+
if (Array.isArray(val)) {
|
|
115
|
+
result[key] = (val as any[]).map((item) => ({
|
|
116
|
+
...item,
|
|
117
|
+
data: denormalizeBlockData(
|
|
118
|
+
item.data ?? {},
|
|
119
|
+
bf.of.find((d: ObjectField) => d.slug === item.slug)?.fields ?? [],
|
|
120
|
+
currentLang
|
|
121
|
+
)
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
} else if (f.type === 'object') {
|
|
125
|
+
const of_ = f as ObjectField;
|
|
126
|
+
if (val && typeof val === 'object' && 'data' in (val as object)) {
|
|
127
|
+
const obj = val as Record<string, unknown>;
|
|
128
|
+
result[key] = { ...obj, data: denormalizeBlockData(obj.data as Record<string, unknown>, of_.fields, currentLang) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
13
135
|
const inlineBlocks: ObjectField[] = $derived(
|
|
14
136
|
(editor.extensionManager.extensions.find((e) => e.name === 'inlineBlock')?.options as { inlineBlocks: ObjectField[] })?.inlineBlocks ?? []
|
|
15
137
|
);
|
|
16
138
|
|
|
17
139
|
const blockDef = $derived(inlineBlocks.find((b) => b.slug === node.attrs.blockType));
|
|
18
140
|
const blockLabel = $derived(blockDef?.label ? (Object.values(blockDef.label)[0] ?? blockDef.slug) : node.attrs.blockType);
|
|
19
|
-
|
|
20
|
-
let blockData = $state<Record<string, unknown>>(parseBlockData(node.attrs.blockData));
|
|
21
|
-
// Track last JSON we wrote to PM to avoid re-parsing our own writes
|
|
22
|
-
let lastWrittenJson = '';
|
|
141
|
+
const supportedFields = $derived(blockDef?.fields.filter((f) => !SKIP_TYPES.has(f.type)) ?? []);
|
|
23
142
|
|
|
24
143
|
function parseBlockData(raw: unknown): Record<string, unknown> {
|
|
25
144
|
if (typeof raw === 'string') {
|
|
@@ -29,25 +148,41 @@
|
|
|
29
148
|
return {};
|
|
30
149
|
}
|
|
31
150
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (rawJson !== lastWrittenJson) {
|
|
37
|
-
blockData = parseBlockData(raw);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
151
|
+
const langs = contentLanguage.all;
|
|
152
|
+
const allFields = blockDef?.fields ?? [];
|
|
153
|
+
const standaloneForm = createStandaloneForm(normalizeBlockData(parseBlockData(node.attrs.blockData), allFields, langs));
|
|
154
|
+
const formStore = standaloneForm.form;
|
|
40
155
|
|
|
156
|
+
// Track last JSON we wrote to PM to avoid re-parsing our own writes
|
|
157
|
+
let lastWrittenJson = '';
|
|
41
158
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
42
159
|
|
|
43
|
-
|
|
44
|
-
|
|
160
|
+
// Form store → ProseMirror (user edits)
|
|
161
|
+
const unsubscribe = formStore.subscribe((data) => {
|
|
45
162
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
46
163
|
debounceTimer = setTimeout(() => {
|
|
47
|
-
|
|
48
|
-
|
|
164
|
+
const denormalized = denormalizeBlockData(data, allFields, contentLanguage.current);
|
|
165
|
+
const json = JSON.stringify(denormalized);
|
|
166
|
+
if (json !== lastWrittenJson) {
|
|
167
|
+
lastWrittenJson = json;
|
|
168
|
+
updateAttributes({ blockData: json });
|
|
169
|
+
}
|
|
49
170
|
}, 300);
|
|
50
|
-
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
onDestroy(() => {
|
|
174
|
+
unsubscribe();
|
|
175
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ProseMirror → Form store (undo/redo, external changes)
|
|
179
|
+
$effect(() => {
|
|
180
|
+
const raw = node.attrs.blockData;
|
|
181
|
+
const rawJson = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
182
|
+
if (rawJson !== lastWrittenJson) {
|
|
183
|
+
formStore.set(normalizeBlockData(parseBlockData(raw), allFields, langs));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
51
186
|
|
|
52
187
|
function moveUp() {
|
|
53
188
|
const pos = getPos();
|
|
@@ -116,7 +251,18 @@
|
|
|
116
251
|
<button type="button" class="grip-handle" aria-label="Przeciągnij" data-drag-handle>
|
|
117
252
|
<GripVertical size={16} />
|
|
118
253
|
</button>
|
|
119
|
-
<
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
class="inline-block-collapse-toggle"
|
|
257
|
+
onclick={() => (collapsed = !collapsed)}
|
|
258
|
+
aria-expanded={!collapsed}
|
|
259
|
+
aria-label={collapsed ? 'Rozwiń blok' : 'Zwiń blok'}
|
|
260
|
+
>
|
|
261
|
+
<span class="inline-block-label">{blockLabel}</span>
|
|
262
|
+
<span class="collapse-chevron" class:collapsed>
|
|
263
|
+
<ChevronDown size={14} />
|
|
264
|
+
</span>
|
|
265
|
+
</button>
|
|
120
266
|
</div>
|
|
121
267
|
<div class="inline-block-header-actions">
|
|
122
268
|
<button type="button" class="action-btn" onclick={moveUp} aria-label="Przenieś wyżej" title="Przenieś wyżej">
|
|
@@ -132,17 +278,25 @@
|
|
|
132
278
|
</div>
|
|
133
279
|
|
|
134
280
|
<!-- Body: field renderer -->
|
|
135
|
-
{#if
|
|
136
|
-
<div
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
281
|
+
{#if !collapsed}
|
|
282
|
+
<div transition:slide={{ duration: 150 }}>
|
|
283
|
+
{#if blockDef}
|
|
284
|
+
<div class="inline-block-body space-y-3">
|
|
285
|
+
{#each supportedFields as f}
|
|
286
|
+
{#if evaluateCondition(f.showWhen, (slug) => $formStore[slug])}
|
|
287
|
+
<FieldRenderer
|
|
288
|
+
field={f}
|
|
289
|
+
form={standaloneForm}
|
|
290
|
+
path={f.slug as FormPathLeaves<Record<string, unknown>>}
|
|
291
|
+
/>
|
|
292
|
+
{/if}
|
|
293
|
+
{/each}
|
|
294
|
+
</div>
|
|
295
|
+
{:else}
|
|
296
|
+
<div class="inline-block-body inline-block-unknown">
|
|
297
|
+
Nieznany typ bloku: {node.attrs.blockType}
|
|
298
|
+
</div>
|
|
299
|
+
{/if}
|
|
146
300
|
</div>
|
|
147
301
|
{/if}
|
|
148
302
|
</div>
|
|
@@ -175,18 +329,54 @@
|
|
|
175
329
|
min-height: 36px;
|
|
176
330
|
}
|
|
177
331
|
|
|
332
|
+
.inline-block-wrapper:has(.collapse-chevron.collapsed) .inline-block-header {
|
|
333
|
+
border-bottom-color: transparent;
|
|
334
|
+
}
|
|
335
|
+
|
|
178
336
|
.inline-block-header-left {
|
|
179
337
|
display: flex;
|
|
180
338
|
align-items: center;
|
|
181
339
|
gap: 0.25rem;
|
|
182
340
|
}
|
|
183
341
|
|
|
342
|
+
.inline-block-collapse-toggle {
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
gap: 0.25rem;
|
|
346
|
+
border: none;
|
|
347
|
+
background: transparent;
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
padding: 2px 4px;
|
|
350
|
+
border-radius: 4px;
|
|
351
|
+
color: inherit;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.inline-block-collapse-toggle:hover {
|
|
355
|
+
background: var(--accent);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.inline-block-collapse-toggle:focus-visible {
|
|
359
|
+
outline: 2px solid var(--ring);
|
|
360
|
+
outline-offset: 1px;
|
|
361
|
+
}
|
|
362
|
+
|
|
184
363
|
.inline-block-label {
|
|
185
364
|
font-size: 0.8125rem;
|
|
186
365
|
font-weight: 600;
|
|
187
366
|
color: var(--foreground);
|
|
188
367
|
}
|
|
189
368
|
|
|
369
|
+
.collapse-chevron {
|
|
370
|
+
display: flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
color: var(--muted-foreground);
|
|
373
|
+
transition: transform 0.15s ease;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.collapse-chevron.collapsed {
|
|
377
|
+
transform: rotate(-90deg);
|
|
378
|
+
}
|
|
379
|
+
|
|
190
380
|
.inline-block-header-actions {
|
|
191
381
|
display: flex;
|
|
192
382
|
align-items: center;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
2
|
+
import { onMount, getAllContexts } from 'svelte';
|
|
3
3
|
import { Editor, type Editor as EditorType } from '@tiptap/core';
|
|
4
4
|
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
|
5
5
|
import { extensions } from './extensions.js';
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { InlineBlockNode } from './inline-block-node.js';
|
|
18
18
|
import { SlashCommand } from './slash-command.js';
|
|
19
19
|
import { HeadingA11yPlugin } from './heading-a11y-plugin.js';
|
|
20
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
20
21
|
|
|
21
22
|
// Icons (bubble menu only)
|
|
22
23
|
import Bold from '@tabler/icons-svelte/icons/bold';
|
|
@@ -43,6 +44,10 @@
|
|
|
43
44
|
|
|
44
45
|
let { value = $bindable(undefined), inlineBlocks = [] }: Props = $props();
|
|
45
46
|
|
|
47
|
+
// Capture parent contexts at init time (before onMount) so SvelteNodeViewRenderer
|
|
48
|
+
// can propagate them to inline block node views (needed for getRemotes() etc.)
|
|
49
|
+
const parentContexts = getAllContexts();
|
|
50
|
+
|
|
46
51
|
const hasInlineBlocks = $derived(inlineBlocks.length > 0);
|
|
47
52
|
|
|
48
53
|
let element = $state<HTMLDivElement | null>(null);
|
|
@@ -64,7 +69,7 @@
|
|
|
64
69
|
|
|
65
70
|
const extraExtensions = hasInlineBlocks
|
|
66
71
|
? [
|
|
67
|
-
InlineBlockNode.configure({ inlineBlocks }),
|
|
72
|
+
InlineBlockNode.configure({ inlineBlocks, context: parentContexts }),
|
|
68
73
|
SlashCommand.configure({ inlineBlocks })
|
|
69
74
|
]
|
|
70
75
|
: [];
|
|
@@ -77,6 +82,10 @@
|
|
|
77
82
|
HeadingA11yPlugin,
|
|
78
83
|
BubbleMenu.configure({
|
|
79
84
|
element: bubbleMenu!
|
|
85
|
+
}),
|
|
86
|
+
Placeholder.configure({
|
|
87
|
+
placeholder: 'Wpisz treść lub "/" by wstawić element',
|
|
88
|
+
showOnlyCurrent: true
|
|
80
89
|
})
|
|
81
90
|
],
|
|
82
91
|
content: initialContent,
|
|
@@ -271,6 +280,8 @@
|
|
|
271
280
|
|
|
272
281
|
<style>
|
|
273
282
|
.bubble-menu {
|
|
283
|
+
height: 0;
|
|
284
|
+
overflow: visible;
|
|
274
285
|
visibility: hidden;
|
|
275
286
|
opacity: 0;
|
|
276
287
|
transition:
|
|
@@ -3,6 +3,7 @@ import type { ObjectField } from '../../../types/fields.js';
|
|
|
3
3
|
export interface InlineBlockOptions {
|
|
4
4
|
HTMLAttributes: Record<string, string>;
|
|
5
5
|
inlineBlocks: ObjectField[];
|
|
6
|
+
context?: Map<unknown, unknown>;
|
|
6
7
|
}
|
|
7
8
|
declare module '@tiptap/core' {
|
|
8
9
|
interface Commands<ReturnType> {
|
|
@@ -17,6 +17,9 @@ function getFieldDefault(field) {
|
|
|
17
17
|
return field.defaultValue;
|
|
18
18
|
switch (field.type) {
|
|
19
19
|
case 'text':
|
|
20
|
+
case 'richtext':
|
|
21
|
+
case 'date':
|
|
22
|
+
case 'datetime':
|
|
20
23
|
return '';
|
|
21
24
|
case 'number':
|
|
22
25
|
return 0;
|
|
@@ -25,8 +28,20 @@ function getFieldDefault(field) {
|
|
|
25
28
|
case 'select':
|
|
26
29
|
case 'radio':
|
|
27
30
|
return field.options[0]?.value ?? '';
|
|
31
|
+
case 'checkboxes':
|
|
32
|
+
return [];
|
|
28
33
|
case 'url':
|
|
29
34
|
return { url: '' };
|
|
35
|
+
case 'image':
|
|
36
|
+
case 'file':
|
|
37
|
+
case 'media':
|
|
38
|
+
case 'relation':
|
|
39
|
+
return field.multiple ? [] : '';
|
|
40
|
+
case 'object':
|
|
41
|
+
return { slug: field.slug, data: {} };
|
|
42
|
+
case 'blocks':
|
|
43
|
+
case 'array':
|
|
44
|
+
return [];
|
|
30
45
|
default:
|
|
31
46
|
return '';
|
|
32
47
|
}
|
|
@@ -93,6 +108,8 @@ export const InlineBlockNode = Node.create({
|
|
|
93
108
|
};
|
|
94
109
|
},
|
|
95
110
|
addNodeView() {
|
|
96
|
-
return SvelteNodeViewRenderer(InlineBlockNodeView
|
|
111
|
+
return SvelteNodeViewRenderer(InlineBlockNodeView, {
|
|
112
|
+
context: this.options.context
|
|
113
|
+
});
|
|
97
114
|
}
|
|
98
115
|
});
|
|
@@ -104,8 +104,7 @@ export const SlashCommand = Extension.create({
|
|
|
104
104
|
if (!query)
|
|
105
105
|
return allItems;
|
|
106
106
|
const q = query.toLowerCase();
|
|
107
|
-
return allItems.filter((item) => item.label.toLowerCase().includes(q) ||
|
|
108
|
-
item.group.toLowerCase().includes(q));
|
|
107
|
+
return allItems.filter((item) => item.label.toLowerCase().includes(q) || item.group.toLowerCase().includes(q));
|
|
109
108
|
},
|
|
110
109
|
render: () => {
|
|
111
110
|
let popup;
|
|
@@ -167,7 +166,7 @@ export const SlashCommand = Extension.create({
|
|
|
167
166
|
return false;
|
|
168
167
|
},
|
|
169
168
|
onExit: () => {
|
|
170
|
-
popup?.destroy();
|
|
169
|
+
// popup?.destroy();
|
|
171
170
|
if (component)
|
|
172
171
|
unmount(component);
|
|
173
172
|
container = undefined;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SuperForm } from 'sveltekit-superforms';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a minimal SuperForm-compatible object for use outside SvelteKit form actions.
|
|
4
|
+
* Only implements the 4 stores that formFieldProxy and formsnap actually use:
|
|
5
|
+
* form, errors, constraints, tainted.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createStandaloneForm(data: Record<string, unknown>): SuperForm<Record<string, unknown>>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { writable, readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a minimal SuperForm-compatible object for use outside SvelteKit form actions.
|
|
4
|
+
* Only implements the 4 stores that formFieldProxy and formsnap actually use:
|
|
5
|
+
* form, errors, constraints, tainted.
|
|
6
|
+
*/
|
|
7
|
+
export function createStandaloneForm(data) {
|
|
8
|
+
return {
|
|
9
|
+
form: writable(data),
|
|
10
|
+
errors: writable({}),
|
|
11
|
+
constraints: writable({}),
|
|
12
|
+
tainted: writable(undefined),
|
|
13
|
+
// Stubs required by SuperForm type — not used by formFieldProxy/formsnap
|
|
14
|
+
formId: writable('standalone'),
|
|
15
|
+
message: writable(undefined),
|
|
16
|
+
submitting: readable(false),
|
|
17
|
+
delayed: readable(false),
|
|
18
|
+
timeout: readable(false),
|
|
19
|
+
posted: readable(false),
|
|
20
|
+
allErrors: readable([]),
|
|
21
|
+
options: {},
|
|
22
|
+
enhance: (() => ({ destroy() { } })),
|
|
23
|
+
validate: (async () => undefined),
|
|
24
|
+
validateForm: (async () => ({ valid: true })),
|
|
25
|
+
isTainted: (() => false),
|
|
26
|
+
reset: (() => { }),
|
|
27
|
+
submit: (() => { }),
|
|
28
|
+
capture: (() => ({})),
|
|
29
|
+
restore: (() => { })
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Editor, type Editor as EditorType } from '@tiptap/core';
|
|
4
4
|
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
|
5
5
|
import { extensions } from './extensions.js';
|
|
6
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
6
7
|
import ToolbarButton from './toolbar-button.svelte';
|
|
7
8
|
import EditorToolbar from './editor-toolbar.svelte';
|
|
8
9
|
import LinkDialog from './link-dialog.svelte';
|
|
@@ -55,6 +56,10 @@
|
|
|
55
56
|
...extensions,
|
|
56
57
|
BubbleMenu.configure({
|
|
57
58
|
element: bubbleMenu!
|
|
59
|
+
}),
|
|
60
|
+
Placeholder.configure({
|
|
61
|
+
placeholder: 'Wpisz treść...',
|
|
62
|
+
showOnlyCurrent: true
|
|
58
63
|
})
|
|
59
64
|
],
|
|
60
65
|
content: value || '',
|
|
@@ -246,6 +251,8 @@
|
|
|
246
251
|
|
|
247
252
|
<style>
|
|
248
253
|
.bubble-menu {
|
|
254
|
+
height: 0;
|
|
255
|
+
overflow: visible;
|
|
249
256
|
visibility: hidden;
|
|
250
257
|
opacity: 0;
|
|
251
258
|
transition:
|
|
@@ -6,7 +6,7 @@ export declare const getRawEntries: import("@sveltejs/kit").RemoteQueryFunction<
|
|
|
6
6
|
limit?: number | undefined;
|
|
7
7
|
offset?: number | undefined;
|
|
8
8
|
orderBy?: {
|
|
9
|
-
column: "createdAt" | "updatedAt";
|
|
9
|
+
column: "createdAt" | "updatedAt" | "sortOrder";
|
|
10
10
|
direction: "asc" | "desc";
|
|
11
11
|
} | undefined;
|
|
12
12
|
}, {
|
|
@@ -20,6 +20,10 @@ export declare const getEntries: import("@sveltejs/kit").RemoteQueryFunction<{
|
|
|
20
20
|
language?: string | undefined;
|
|
21
21
|
status?: "draft" | "published" | "scheduled" | "archived" | undefined;
|
|
22
22
|
slug?: string | undefined;
|
|
23
|
+
orderBy?: {
|
|
24
|
+
column: "createdAt" | "updatedAt" | "sortOrder";
|
|
25
|
+
direction: "asc" | "desc";
|
|
26
|
+
} | undefined;
|
|
23
27
|
}, import("../../types/entries.js").Entry[]>;
|
|
24
28
|
export declare const getEntry: import("@sveltejs/kit").RemoteQueryFunction<{
|
|
25
29
|
id?: string | undefined;
|
|
@@ -53,11 +57,15 @@ export declare const updateEntryCommand: import("@sveltejs/kit").RemoteCommand<{
|
|
|
53
57
|
publishedAt?: Date | null | undefined;
|
|
54
58
|
publishedVersionId?: string | null | undefined;
|
|
55
59
|
publishedBy?: string | null | undefined;
|
|
60
|
+
sortOrder?: number | null | undefined;
|
|
56
61
|
};
|
|
57
62
|
}, Promise<import("../../types/entries.js").DbEntry>>;
|
|
58
63
|
export declare const archiveEntryCommand: import("@sveltejs/kit").RemoteCommand<string, Promise<import("../../types/entries.js").DbEntry>>;
|
|
59
64
|
export declare const unarchiveEntryCommand: import("@sveltejs/kit").RemoteCommand<string, Promise<import("../../types/entries.js").DbEntry>>;
|
|
60
65
|
export declare const deleteEntryCommand: import("@sveltejs/kit").RemoteCommand<string, Promise<void>>;
|
|
66
|
+
export declare const reorderEntriesCommand: import("@sveltejs/kit").RemoteCommand<{
|
|
67
|
+
orderedIds: string[];
|
|
68
|
+
}, Promise<void>>;
|
|
61
69
|
export declare const getEntryVersion: import("@sveltejs/kit").RemoteQueryFunction<{
|
|
62
70
|
id: string;
|
|
63
71
|
language: string;
|
|
@@ -14,7 +14,7 @@ export const getRawEntries = query(z.object({
|
|
|
14
14
|
offset: z.number().int().nonnegative().optional(),
|
|
15
15
|
orderBy: z
|
|
16
16
|
.object({
|
|
17
|
-
column: z.enum(['createdAt', 'updatedAt']),
|
|
17
|
+
column: z.enum(['createdAt', 'updatedAt', 'sortOrder']),
|
|
18
18
|
direction: z.enum(['asc', 'desc'])
|
|
19
19
|
})
|
|
20
20
|
.optional()
|
|
@@ -32,7 +32,13 @@ export const getEntries = query(z.object({
|
|
|
32
32
|
dataLike: z.record(z.string(), z.unknown()).optional(),
|
|
33
33
|
language: z.string().optional(),
|
|
34
34
|
status: z.enum(entryStatuses).optional(),
|
|
35
|
-
slug: z.string().optional()
|
|
35
|
+
slug: z.string().optional(),
|
|
36
|
+
orderBy: z
|
|
37
|
+
.object({
|
|
38
|
+
column: z.enum(['createdAt', 'updatedAt', 'sortOrder']),
|
|
39
|
+
direction: z.enum(['asc', 'desc'])
|
|
40
|
+
})
|
|
41
|
+
.optional()
|
|
36
42
|
}), async (input) => {
|
|
37
43
|
return getEntriesOperation(input);
|
|
38
44
|
});
|
|
@@ -172,6 +178,12 @@ export const deleteEntryCommand = command(z.string().uuid(), async (id) => {
|
|
|
172
178
|
}
|
|
173
179
|
return getCMS().databaseAdapter.deleteEntry({ id });
|
|
174
180
|
});
|
|
181
|
+
export const reorderEntriesCommand = command(z.object({
|
|
182
|
+
orderedIds: z.array(z.string().uuid())
|
|
183
|
+
}), async ({ orderedIds }) => {
|
|
184
|
+
requireAuth();
|
|
185
|
+
await Promise.all(orderedIds.map((id, index) => updateEntry(id, { sortOrder: index })));
|
|
186
|
+
});
|
|
175
187
|
export const getEntryVersion = query(z.object({
|
|
176
188
|
id: z.string().uuid(),
|
|
177
189
|
language: z.string()
|
|
@@ -199,6 +211,14 @@ export const getRecentEntries = query(z.number().default(6), async (limit) => {
|
|
|
199
211
|
label = String(Object.values(titleData)[0] || '');
|
|
200
212
|
}
|
|
201
213
|
}
|
|
214
|
+
if (config && config.type === 'single' && config.label) {
|
|
215
|
+
if (typeof config.label === 'object') {
|
|
216
|
+
label = String(Object.values(config.label)[0] || '');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
label = config.label;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
202
222
|
let collectionLabel = { pl: entry.slug, en: entry.slug };
|
|
203
223
|
if (config) {
|
|
204
224
|
if (config.type === 'collection' && config.labels) {
|
|
@@ -237,6 +257,14 @@ export const getRecentActivity = query(z.number().default(10), async (limit) =>
|
|
|
237
257
|
label = String(Object.values(titleData)[0] || '');
|
|
238
258
|
}
|
|
239
259
|
}
|
|
260
|
+
if (config && config.type === 'single' && config.label) {
|
|
261
|
+
if (typeof config.label === 'object') {
|
|
262
|
+
label = String(Object.values(config.label)[0] || '');
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
label = config.label;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
240
268
|
activity.push({
|
|
241
269
|
type: 'entry_edit',
|
|
242
270
|
entryId: entry.id,
|
|
@@ -308,6 +308,16 @@ div[data-collapsible]:not([data-collapsible='icon'])
|
|
|
308
308
|
flex-shrink: 0;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/* Placeholder — Notion-style */
|
|
312
|
+
.ProseMirror .is-empty.is-editor-empty::before,
|
|
313
|
+
.ProseMirror .has-focus.is-empty::before {
|
|
314
|
+
color: var(--text-light);
|
|
315
|
+
content: attr(data-placeholder);
|
|
316
|
+
float: left;
|
|
317
|
+
height: 0;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
}
|
|
320
|
+
|
|
311
321
|
.tiptap-highlight {
|
|
312
322
|
background-color: rgb(250 204 21 / 0.4);
|
|
313
323
|
padding: 0.125em 0.25em;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FieldCondition } from '../../types/fields.js';
|
|
2
|
+
/**
|
|
3
|
+
* Evaluate whether a field should be visible based on its showWhen condition.
|
|
4
|
+
* Returns true (visible) when no condition is set.
|
|
5
|
+
*/
|
|
6
|
+
export declare function evaluateCondition(condition: FieldCondition | undefined, getValue: (slug: string) => unknown): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluate whether a field should be visible based on its showWhen condition.
|
|
3
|
+
* Returns true (visible) when no condition is set.
|
|
4
|
+
*/
|
|
5
|
+
export function evaluateCondition(condition, getValue) {
|
|
6
|
+
if (!condition)
|
|
7
|
+
return true;
|
|
8
|
+
const value = getValue(condition.field);
|
|
9
|
+
if (condition.equals !== undefined) {
|
|
10
|
+
const allowed = Array.isArray(condition.equals) ? condition.equals : [condition.equals];
|
|
11
|
+
return allowed.includes(value);
|
|
12
|
+
}
|
|
13
|
+
if (condition.notEquals !== undefined) {
|
|
14
|
+
const disallowed = Array.isArray(condition.notEquals)
|
|
15
|
+
? condition.notEquals
|
|
16
|
+
: [condition.notEquals];
|
|
17
|
+
return !disallowed.includes(value);
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
@@ -156,6 +156,14 @@ export * from 'includio-cms/admin/remote';
|
|
|
156
156
|
import { createAdminApiHandler } from 'includio-cms/admin/api/handler';
|
|
157
157
|
|
|
158
158
|
export const { GET, POST, PATCH, PUT, DELETE } = createAdminApiHandler();
|
|
159
|
+
`
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
path: 'admin/api/rest/[...restPath]/+server.ts',
|
|
163
|
+
content: `${GENERATED_COMMENT_TS}
|
|
164
|
+
import { createRestApiHandler } from 'includio-cms/admin/api/rest/handler';
|
|
165
|
+
|
|
166
|
+
export const { GET, POST, PUT, DELETE } = createRestApiHandler();
|
|
159
167
|
`
|
|
160
168
|
}
|
|
161
169
|
];
|