includio-cms 0.0.67 → 0.0.69
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 +157 -0
- package/ROADMAP.md +73 -0
- package/dist/admin/client/entry/entry-form.svelte +1 -1
- package/dist/admin/client/entry/entry-form.svelte.d.ts +1 -1
- package/dist/admin/client/entry/entry.svelte +17 -6
- package/dist/admin/client/entry/hybrid/hybrid-context.svelte.d.ts +1 -0
- package/dist/admin/components/fields/array-field.svelte +126 -71
- package/dist/admin/components/fields/relation-field.svelte +6 -10
- package/dist/admin/components/layout/nav-search.svelte +43 -31
- package/dist/admin/remote/media.remote.js +3 -3
- package/dist/admin/utils/arrayMove.d.ts +5 -0
- package/dist/admin/utils/arrayMove.js +12 -0
- package/dist/cms/runtime/types.d.ts +8 -0
- package/dist/core/server/generator/fields.js +9 -3
- package/dist/core/server/generator/generator.js +4 -7
- package/dist/core/server/generator/utils.d.ts +1 -0
- package/dist/core/server/generator/utils.js +6 -0
- package/dist/core/server/media/styles/operations/generateDefaultStyles.d.ts +1 -0
- package/dist/core/server/media/styles/operations/generateDefaultStyles.js +17 -16
- package/dist/core/server/media/styles/sharp/generateImageStyle.js +15 -5
- package/dist/core/server/utils/sanitizeRichText.d.ts +1 -0
- package/dist/core/server/utils/sanitizeRichText.js +67 -0
- package/dist/sveltekit/components/image.svelte +11 -2
- package/dist/sveltekit/components/preview.svelte +22 -14
- package/dist/sveltekit/components/preview.svelte.d.ts +38 -8
- package/dist/sveltekit/index.d.ts +1 -0
- package/dist/sveltekit/index.js +1 -0
- package/dist/sveltekit/utils/getLink.d.ts +7 -0
- package/dist/sveltekit/utils/getLink.js +32 -0
- package/dist/sveltekit/utils/index.d.ts +2 -0
- package/dist/sveltekit/utils/index.js +2 -0
- package/dist/sveltekit/utils/media.d.ts +3 -0
- package/dist/sveltekit/utils/media.js +6 -0
- package/dist/types/index.d.ts +8 -1
- package/dist/types/index.js +7 -0
- package/dist/updates/0.0.68/index.d.ts +2 -0
- package/dist/updates/0.0.68/index.js +21 -0
- package/dist/updates/0.0.69/index.d.ts +2 -0
- package/dist/updates/0.0.69/index.js +12 -0
- package/dist/updates/index.js +3 -1
- package/package.json +7 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to includio-cms are documented here.
|
|
4
|
+
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
|
+
|
|
6
|
+
## 0.0.69 — 2026-02-17
|
|
7
|
+
|
|
8
|
+
DnD array reordering, nav-search batch fetch
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Drag-and-drop reordering in array fields with arrayMove utility
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Nav-search: batch-fetch data before rendering Command dialog — fixes missing data on open
|
|
15
|
+
|
|
16
|
+
## 0.0.68 — 2026-02-11
|
|
17
|
+
|
|
18
|
+
Fix EXIF/blur/focal bugs, expand type exports, add utility functions
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Export Entry, EntryType, CMSConfig, CollectionConfig, SingleConfig, FormConfig, FormSubmission, Language, Localized, MediaFileType, ImageStyle, MediaTag from includio-cms/types
|
|
22
|
+
- Add getLink() utility for resolving URL field data to strings
|
|
23
|
+
- Add isImageFieldData() / isVideoFieldData() type guards for media fields
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Blur placeholder (LQIP) now removed after image loads — fixes visible blur behind transparent PNGs
|
|
27
|
+
- Focal point change now awaits style regeneration before returning — fixes stale/missing styles after save
|
|
28
|
+
- Explicit .rotate() in sharp pipeline — fixes upside-down WebP/JPEG when source has EXIF orientation
|
|
29
|
+
- Use oriented dimensions (post-EXIF) for focal crop calculation — fixes wrong crop on rotated images
|
|
30
|
+
- Codegen: media field type now generates ImageFieldData | VideoFieldData instead of any
|
|
31
|
+
- Codegen: hyphenated collection slugs now produce correct PascalCase (article-category → ArticleCategory)
|
|
32
|
+
- Codegen: generated interfaces now include id and slug fields
|
|
33
|
+
- Preview deep merge now preserves resolved references without url (e.g. category relations)
|
|
34
|
+
|
|
35
|
+
## 0.0.67 — 2026-02-11
|
|
36
|
+
|
|
37
|
+
Image quality, LQIP, focal point, responsive srcset, auto-downscale
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- Image styles: configurable quality parameter for format output
|
|
41
|
+
- LQIP: auto-generated blur placeholder (base64 WebP) on image upload
|
|
42
|
+
- LQIP: lazy backfill for existing images without blur placeholder
|
|
43
|
+
- Image component: LQIP blur placeholder via background-image on img element
|
|
44
|
+
- Focal point picker in media file details
|
|
45
|
+
- Smart crop: styles with crop=true use focal point to calculate crop region
|
|
46
|
+
- Style cache invalidation on focal point change
|
|
47
|
+
- Default WebP/AVIF styles auto-generate 640/1024/1920w srcset variants
|
|
48
|
+
- Custom styles support srcset[] and sizes config
|
|
49
|
+
- Image component renders srcset + sizes on <source> elements
|
|
50
|
+
- Image component: sizes prop to override style-level sizes on <source> elements
|
|
51
|
+
- Skips srcset widths larger than original image
|
|
52
|
+
- CMSConfig: media.maxOriginalWidth / maxOriginalHeight settings
|
|
53
|
+
- Auto-downscale oversized images on upload (fit: inside)
|
|
54
|
+
- SVG and non-image files are not affected
|
|
55
|
+
- Auto-format expansion: styles without explicit format auto-expand to AVIF/WebP/original
|
|
56
|
+
- Eager background generation of default image styles on upload/replace/focal-point change
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- Case-insensitive SVG extension check in upload/downscale/LQIP
|
|
60
|
+
- Quality value clamped to 1-100 in generateImageStyle
|
|
61
|
+
- Silent errors replaced with console.warn for LQIP/downscale/file cleanup
|
|
62
|
+
- Remove hardcoded sizes: 100vw from default image styles
|
|
63
|
+
- Lazy load skeleton + error handling for hybrid layout imports
|
|
64
|
+
- AVIF <source> rendered before WebP for correct browser priority
|
|
65
|
+
- Cache-busting suffix in generated style filenames prevents stale CDN/browser cache
|
|
66
|
+
|
|
67
|
+
### Breaking
|
|
68
|
+
- getImageStyles() now returns { styles, blurDataUrl } instead of plain styles record
|
|
69
|
+
|
|
70
|
+
### Migration
|
|
71
|
+
|
|
72
|
+
```sql
|
|
73
|
+
ALTER TABLE image_styles ADD COLUMN IF NOT EXISTS quality INTEGER;
|
|
74
|
+
ALTER TABLE media_file ADD COLUMN IF NOT EXISTS blur_data_url TEXT;
|
|
75
|
+
ALTER TABLE media_file ADD COLUMN IF NOT EXISTS focal_x REAL;
|
|
76
|
+
ALTER TABLE media_file ADD COLUMN IF NOT EXISTS focal_y REAL;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 0.0.66 — 2025-02-10
|
|
80
|
+
|
|
81
|
+
Fix URL field validation for linked entries
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
- URL field: linked entries no longer require manual URL per language to save
|
|
85
|
+
|
|
86
|
+
## 0.0.65 — 2025-02-09
|
|
87
|
+
|
|
88
|
+
Move publish logic to entry table, replace folder-based media with tags
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
- Account settings page with session management
|
|
92
|
+
- Redesigned sidebar with collapsible groups
|
|
93
|
+
- Tag-based media library replacing folder structure
|
|
94
|
+
- Unified UI components and styling
|
|
95
|
+
|
|
96
|
+
### Breaking
|
|
97
|
+
- Media folders replaced with tags — existing folder assignments will be lost
|
|
98
|
+
|
|
99
|
+
### Migration
|
|
100
|
+
|
|
101
|
+
```sql
|
|
102
|
+
-- Move publish logic from entry_version to entry
|
|
103
|
+
|
|
104
|
+
ALTER TABLE entry ADD COLUMN published_at TIMESTAMP;
|
|
105
|
+
ALTER TABLE entry ADD COLUMN published_version_id UUID
|
|
106
|
+
REFERENCES entry_version(id) ON DELETE SET NULL;
|
|
107
|
+
ALTER TABLE entry ADD COLUMN published_by TEXT;
|
|
108
|
+
|
|
109
|
+
UPDATE entry e SET
|
|
110
|
+
published_version_id = latest.id,
|
|
111
|
+
published_at = first_pub.first_published_at,
|
|
112
|
+
published_by = latest.published_by
|
|
113
|
+
FROM (
|
|
114
|
+
SELECT DISTINCT ON (entry_id) id, entry_id, published_by
|
|
115
|
+
FROM entry_version
|
|
116
|
+
WHERE published_at IS NOT NULL AND published_at <= NOW()
|
|
117
|
+
ORDER BY entry_id, version_number DESC
|
|
118
|
+
) latest
|
|
119
|
+
JOIN (
|
|
120
|
+
SELECT entry_id, MIN(published_at) as first_published_at
|
|
121
|
+
FROM entry_version
|
|
122
|
+
WHERE published_at IS NOT NULL AND published_at <= NOW()
|
|
123
|
+
GROUP BY entry_id
|
|
124
|
+
) first_pub ON first_pub.entry_id = latest.entry_id
|
|
125
|
+
WHERE e.id = latest.entry_id;
|
|
126
|
+
|
|
127
|
+
UPDATE entry e SET
|
|
128
|
+
published_version_id = COALESCE(e.published_version_id, sub.id),
|
|
129
|
+
published_at = CASE WHEN e.published_at IS NULL THEN sub.published_at ELSE e.published_at END,
|
|
130
|
+
published_by = COALESCE(e.published_by, sub.published_by)
|
|
131
|
+
FROM (
|
|
132
|
+
SELECT DISTINCT ON (entry_id) id, entry_id, published_at, published_by
|
|
133
|
+
FROM entry_version
|
|
134
|
+
WHERE published_at IS NOT NULL AND published_at > NOW()
|
|
135
|
+
ORDER BY entry_id, published_at ASC
|
|
136
|
+
) sub
|
|
137
|
+
WHERE e.id = sub.entry_id AND e.published_version_id IS NULL;
|
|
138
|
+
|
|
139
|
+
-- Replace folder-based media with tag-based media
|
|
140
|
+
|
|
141
|
+
CREATE TABLE IF NOT EXISTS media_tag (
|
|
142
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
143
|
+
name TEXT NOT NULL UNIQUE,
|
|
144
|
+
color TEXT NOT NULL DEFAULT '#3b82f6',
|
|
145
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE TABLE IF NOT EXISTS media_file_tag (
|
|
149
|
+
file_id UUID NOT NULL REFERENCES media_file(id) ON DELETE CASCADE,
|
|
150
|
+
tag_id UUID NOT NULL REFERENCES media_tag(id) ON DELETE CASCADE,
|
|
151
|
+
PRIMARY KEY (file_id, tag_id)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
ALTER TABLE media_file DROP COLUMN IF EXISTS folder_id;
|
|
155
|
+
|
|
156
|
+
DROP TABLE IF EXISTS media_folder;
|
|
157
|
+
```
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
> `- [ ]` planned | `- [~]` in-progress | `- [x]` done
|
|
4
|
+
> `[feature]` `[fix]` `[breaking]` `[chore]`
|
|
5
|
+
> `[P0]` critical | `[P1]` important | `[P2]` nice-to-have
|
|
6
|
+
> `<!-- files: path/to/file.ts -->` optionally linked files
|
|
7
|
+
>
|
|
8
|
+
> **Versioning:** 0.0.69 is the last `0.0.x` release. From 0.1.0 onward: `0.MINOR.0` = features/changes, `0.MINOR.PATCH` = fixes.
|
|
9
|
+
|
|
10
|
+
## 0.0.69 _(last 0.0.x)_
|
|
11
|
+
|
|
12
|
+
- [x] `[feature]` `[P1]` DnD reordering in array fields + arrayMove utility <!-- files: src/cms/fields/array-field -->
|
|
13
|
+
- [x] `[fix]` `[P1]` Nav-search: batch-fetch data before rendering Command dialog <!-- files: src/cms/nav-search -->
|
|
14
|
+
|
|
15
|
+
## 0.1.0 — Stabilization
|
|
16
|
+
|
|
17
|
+
- [ ] `[fix]` `[P0]` Collection table pagination — server-side, persistent page state, archived tab <!-- files: src/lib/admin/client/collection/collection-entries.svelte, src/lib/admin/client/collection/table-pagination.svelte -->
|
|
18
|
+
- [ ] `[fix]` `[P0]` Language switcher — reactive globally without reload, remove hardcoded en/pl <!-- files: src/lib/admin/state/interface-language.svelte.ts, src/lib/admin/components/layout/header-actions.svelte -->
|
|
19
|
+
- [ ] `[fix]` `[P0]` User account section — email change, aria-pressed on prefs, avatar upload, clean up 2FA stub <!-- files: src/lib/admin/client/account/ -->
|
|
20
|
+
- [ ] `[fix]` `[P0]` Media tag counts always 0 — pass actual files to TagSidebar <!-- files: src/lib/admin/components/media/media-library.svelte, src/lib/admin/components/media/tag-sidebar.svelte -->
|
|
21
|
+
- [ ] `[fix]` `[P0]` Hybrid target — don't render `data-hybrid-path` for non-logged-in users <!-- files: src/lib/sveltekit/components/hybrid-target.svelte -->
|
|
22
|
+
|
|
23
|
+
## 0.1.1 — Input integrity
|
|
24
|
+
|
|
25
|
+
- [ ] `[fix]` `[P1]` Input constraints UI — HTML maxlength, character counter, pattern feedback <!-- files: src/lib/admin/components/fields/text-field.svelte -->
|
|
26
|
+
- [ ] `[fix]` `[P1]` Array field maxItems — disable Add button when max reached <!-- files: src/lib/admin/components/fields/array-field.svelte -->
|
|
27
|
+
- [ ] `[feature]` `[P1]` Array field fixed length — fixed item count, no add/remove, reorder only
|
|
28
|
+
- [ ] `[feature]` `[P1]` Field constraint info display — show constraints before validation error (WCAG/ATAG)
|
|
29
|
+
|
|
30
|
+
## 0.2.0 — Plugin system
|
|
31
|
+
|
|
32
|
+
- [ ] `[feature]` `[P0]` Wire plugin hooks into CRUD operations (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
|
|
33
|
+
- [ ] `[feature]` `[P0]` Plugin registration API — public surface for external plugins
|
|
34
|
+
- [ ] `[chore]` `[P1]` Plugin system documentation
|
|
35
|
+
|
|
36
|
+
## 0.2.1 — CMS API improvements
|
|
37
|
+
|
|
38
|
+
- [ ] `[feature]` `[P1]` Server-side pagination API (formalize 0.1.0 fix)
|
|
39
|
+
- [ ] `[feature]` `[P1]` Improved type generation <!-- files: src/lib/core/server/generator/ -->
|
|
40
|
+
- [ ] `[feature]` `[P1]` Proper filtering API (SQL-level, not JS post-query)
|
|
41
|
+
|
|
42
|
+
## 0.3.0 — Admin experience
|
|
43
|
+
|
|
44
|
+
- [ ] `[feature]` `[P1]` Admin overlay — built-in admin bar on site (quick edit, preview, audit)
|
|
45
|
+
- [ ] `[feature]` `[P1]` Media gallery virtualization/pagination <!-- files: src/lib/admin/components/media/files-list.svelte -->
|
|
46
|
+
- [ ] `[feature]` `[P2]` Image styles UI — preview generated variants in editor
|
|
47
|
+
|
|
48
|
+
## 0.3.1 — SEO module
|
|
49
|
+
|
|
50
|
+
- [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
|
|
51
|
+
- [ ] `[feature]` `[P1]` Global SEO settings
|
|
52
|
+
- [ ] `[feature]` `[P1]` Dedicated frontend SEO components <!-- files: src/lib/sveltekit/components/seo.svelte -->
|
|
53
|
+
- [ ] `[feature]` `[P2]` Sitemap generation
|
|
54
|
+
|
|
55
|
+
## 0.4.0 — WCAG/ATAG compliance
|
|
56
|
+
|
|
57
|
+
- [ ] `[chore]` `[P0]` Full WCAG/ATAG audit
|
|
58
|
+
- [ ] `[feature]` `[P0]` Accessibility rework based on audit findings
|
|
59
|
+
|
|
60
|
+
## Security hardening
|
|
61
|
+
|
|
62
|
+
- [ ] `[feature]` `[P1]` `sanitizeHTML` utility — general HTML sanitization outside richtext (text fields, SEO fields)
|
|
63
|
+
- [ ] `[feature]` `[P1]` CSP headers — Content-Security-Policy middleware
|
|
64
|
+
- [ ] `[feature]` `[P1]` CSRF protection — tokens for mutating operations
|
|
65
|
+
- [ ] `[feature]` `[P1]` Rate limiting — API/auth endpoints
|
|
66
|
+
- [ ] `[chore]` `[P1]` Security audit — review `{@html}` usage, innerHTML, injection vectors
|
|
67
|
+
- [ ] `[chore]` `[P1]` Input sanitization audit — review all fields for XSS
|
|
68
|
+
|
|
69
|
+
## Backlog
|
|
70
|
+
|
|
71
|
+
- [ ] `[feature]` `[P2]` Alternative richtext editor — Word-like mode, single richtext field instead of blocks
|
|
72
|
+
- [ ] `[chore]` `[P2]` Caching/performance layer (scope TBD)
|
|
73
|
+
- [ ] `[feature]` `[P2]` API/CLI for configuration (setup DX for less technical users)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RawEntry } from '../../../types/entries.js';
|
|
2
2
|
import type { SuperForm } from 'sveltekit-superforms';
|
|
3
3
|
type Props = {
|
|
4
|
-
form: SuperForm<
|
|
4
|
+
form: SuperForm<Record<string, unknown>>;
|
|
5
5
|
entry: RawEntry;
|
|
6
6
|
focusedPath?: string | null;
|
|
7
7
|
onPathSelect?: (path: string) => void;
|
|
@@ -290,8 +290,18 @@
|
|
|
290
290
|
|
|
291
291
|
let sizePreset: SizePreset = $state('responsive');
|
|
292
292
|
|
|
293
|
+
function getOriginFromUrl(url: string): string {
|
|
294
|
+
try {
|
|
295
|
+
return new URL(url, window.location.origin).origin;
|
|
296
|
+
} catch {
|
|
297
|
+
return window.location.origin;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const previewOrigin = collection.previewUrl ? getOriginFromUrl(collection.previewUrl) : window.location.origin;
|
|
302
|
+
|
|
293
303
|
const updatePreview = useDebounce(
|
|
294
|
-
async (window: Window, form: SuperForm<
|
|
304
|
+
async (window: Window, form: SuperForm<Record<string, unknown>>) => {
|
|
295
305
|
const data = await form.validateForm();
|
|
296
306
|
|
|
297
307
|
if (data.valid) {
|
|
@@ -306,7 +316,7 @@
|
|
|
306
316
|
type: 'preview-update',
|
|
307
317
|
data: updatedData
|
|
308
318
|
},
|
|
309
|
-
|
|
319
|
+
previewOrigin
|
|
310
320
|
);
|
|
311
321
|
}
|
|
312
322
|
},
|
|
@@ -340,7 +350,7 @@
|
|
|
340
350
|
// Send hybrid mode state first
|
|
341
351
|
previewIframe.contentWindow.postMessage(
|
|
342
352
|
{ type: 'hybrid-mode-enable', enabled: hybridContext.mode === 'hybrid' },
|
|
343
|
-
|
|
353
|
+
previewOrigin
|
|
344
354
|
);
|
|
345
355
|
|
|
346
356
|
// Then send form data
|
|
@@ -353,7 +363,7 @@
|
|
|
353
363
|
});
|
|
354
364
|
previewIframe.contentWindow.postMessage(
|
|
355
365
|
{ type: 'preview-update', data: updatedData },
|
|
356
|
-
|
|
366
|
+
previewOrigin
|
|
357
367
|
);
|
|
358
368
|
}
|
|
359
369
|
}
|
|
@@ -369,6 +379,7 @@
|
|
|
369
379
|
// Listen for messages from iframe
|
|
370
380
|
onMount(() => {
|
|
371
381
|
function handleMessage(e: MessageEvent) {
|
|
382
|
+
if (e.origin !== previewOrigin) return;
|
|
372
383
|
// Click on preview element - debounced to prevent scroll jumping
|
|
373
384
|
if (e.data.type === 'hybrid-focus' && e.data.path) {
|
|
374
385
|
debouncedSetFocusPath(e.data.path);
|
|
@@ -387,7 +398,7 @@
|
|
|
387
398
|
if (previewIframe?.contentWindow) {
|
|
388
399
|
previewIframe.contentWindow.postMessage(
|
|
389
400
|
{ type: 'hybrid-highlight', path: hybridContext.focusedPath },
|
|
390
|
-
|
|
401
|
+
previewOrigin
|
|
391
402
|
);
|
|
392
403
|
}
|
|
393
404
|
});
|
|
@@ -400,7 +411,7 @@
|
|
|
400
411
|
if (previewIframe?.contentWindow) {
|
|
401
412
|
previewIframe.contentWindow.postMessage(
|
|
402
413
|
{ type: 'hybrid-mode-enable', enabled: hybridContext.mode === 'hybrid' },
|
|
403
|
-
|
|
414
|
+
previewOrigin
|
|
404
415
|
);
|
|
405
416
|
}
|
|
406
417
|
}
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
import RequiredLabel from './required-label.svelte';
|
|
31
31
|
import { cn } from '../../../utils.js';
|
|
32
32
|
import BlockPickerModal from './block-picker-modal.svelte';
|
|
33
|
+
import GripVertical from '@tabler/icons-svelte/icons/grip-vertical';
|
|
34
|
+
import { droppable, draggable, dndState } from '@thisux/sveltednd';
|
|
35
|
+
import { flip } from 'svelte/animate';
|
|
36
|
+
import { arrayMove } from '../../utils/arrayMove.js';
|
|
33
37
|
|
|
34
38
|
const contentLanguage = getContentLanguage();
|
|
35
39
|
const interfaceLanguage = useInterfaceLanguage();
|
|
@@ -119,6 +123,17 @@
|
|
|
119
123
|
});
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
let dropProcessing = false;
|
|
127
|
+
|
|
128
|
+
function moveItem(from: number, to: number) {
|
|
129
|
+
if (!$value || from === to) return;
|
|
130
|
+
$value = arrayMove($value, from, to);
|
|
131
|
+
|
|
132
|
+
tick().then(() => {
|
|
133
|
+
openAndCloseOthers(to);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
122
137
|
function removeItem(index: number) {
|
|
123
138
|
if (!$value) return;
|
|
124
139
|
|
|
@@ -208,80 +223,120 @@
|
|
|
208
223
|
<Accordion.Root type="multiple" class="w-full space-y-4" bind:value={accordionOpenState}>
|
|
209
224
|
{#if $value && $value.length > 0}
|
|
210
225
|
{#each $value as item, index (item._id ?? index)}
|
|
211
|
-
|
|
212
|
-
{
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
226
|
+
<div
|
|
227
|
+
use:droppable={{
|
|
228
|
+
container: index.toString(),
|
|
229
|
+
callbacks: {
|
|
230
|
+
onDrop: (state) => {
|
|
231
|
+
if (dropProcessing) return;
|
|
232
|
+
dropProcessing = true;
|
|
233
|
+
|
|
234
|
+
const dragIndex = parseInt(state.sourceContainer ?? '');
|
|
235
|
+
const dropIndex = parseInt(state.targetContainer ?? '');
|
|
236
|
+
if (!isNaN(dragIndex) && !isNaN(dropIndex)) {
|
|
237
|
+
moveItem(dragIndex, dropIndex);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// {#key index} destroys the old draggable before dragend fires,
|
|
241
|
+
// leaving isDragging stuck at true. Reset manually.
|
|
242
|
+
dndState.isDragging = false;
|
|
243
|
+
dndState.draggedItem = null;
|
|
244
|
+
dndState.sourceContainer = '';
|
|
245
|
+
dndState.targetContainer = null;
|
|
246
|
+
dndState.targetElement = null;
|
|
247
|
+
|
|
248
|
+
setTimeout(() => { dropProcessing = false; }, 50);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}}
|
|
252
|
+
animate:flip={{ duration: 200 }}
|
|
253
|
+
>
|
|
254
|
+
{#key index}
|
|
255
|
+
{#if $value[index].data && $value[index].slug}
|
|
256
|
+
{@const item = $value[index]}
|
|
257
|
+
{@const objectField = field.of.find((option) => option.slug === item.slug)}
|
|
258
|
+
|
|
259
|
+
{#if objectField}
|
|
260
|
+
<Accordion.Item value={index.toString()} class="border-0" data-depth={depth + 1}>
|
|
261
|
+
<Accordion.Trigger
|
|
262
|
+
class="items-center border px-4 text-base font-normal data-[state=open]:rounded-b-none dark:bg-slate-800/30 dark:hover:bg-slate-700/40 dark:border-white/[0.08]"
|
|
263
|
+
>
|
|
264
|
+
<div class="flex grow items-center justify-between gap-4">
|
|
265
|
+
<div class="flex items-center gap-4">
|
|
266
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
267
|
+
<div
|
|
268
|
+
use:draggable={{ container: index.toString(), dragData: { id: item._id } }}
|
|
269
|
+
class="cursor-grab text-muted-foreground hover:text-foreground"
|
|
270
|
+
onmousedown={(e) => e.stopPropagation()}
|
|
271
|
+
onclick={(e) => e.stopPropagation()}
|
|
248
272
|
>
|
|
249
|
-
|
|
250
|
-
</
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
</
|
|
254
|
-
</
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
<GripVertical class="h-4 w-4" />
|
|
274
|
+
</div>
|
|
275
|
+
<span>{index < 10 ? '0' : ''}{index + 1}</span>
|
|
276
|
+
<Badge variant="outline">{getLocalizedLabel(objectField.label, interfaceLanguage.current) || objectField.slug}</Badge>
|
|
277
|
+
<span>{getAccordionLabel($value[index])}</span>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<DropdownMenu.Root>
|
|
281
|
+
<DropdownMenu.Trigger
|
|
282
|
+
class="data-[state=open]:bg-muted text-muted-foreground flex size-8"
|
|
283
|
+
>
|
|
284
|
+
{#snippet child({ props })}
|
|
285
|
+
<Button variant="ghost" size="icon" {...props}>
|
|
286
|
+
<DotsVerticalIcon />
|
|
287
|
+
<span class="sr-only">Open menu</span>
|
|
288
|
+
</Button>
|
|
289
|
+
{/snippet}
|
|
290
|
+
</DropdownMenu.Trigger>
|
|
291
|
+
<DropdownMenu.Content align="end" class="w-32">
|
|
292
|
+
<DropdownMenu.Item onclick={() => duplicateItem(index)}>
|
|
293
|
+
Duplicate
|
|
294
|
+
</DropdownMenu.Item>
|
|
295
|
+
<DropdownMenu.Item onclick={() => moveItemUp(index)} disabled={index === 0}>
|
|
296
|
+
Move up
|
|
297
|
+
</DropdownMenu.Item>
|
|
298
|
+
<DropdownMenu.Item
|
|
299
|
+
onclick={() => moveItemDown(index)}
|
|
300
|
+
disabled={$value && index === $value.length - 1}
|
|
301
|
+
>
|
|
302
|
+
Move down
|
|
303
|
+
</DropdownMenu.Item>
|
|
304
|
+
<DropdownMenu.Item variant="destructive" onclick={() => removeItem(index)}>
|
|
305
|
+
Delete
|
|
306
|
+
</DropdownMenu.Item>
|
|
307
|
+
</DropdownMenu.Content>
|
|
308
|
+
</DropdownMenu.Root>
|
|
309
|
+
</div>
|
|
310
|
+
</Accordion.Trigger>
|
|
311
|
+
<Accordion.Content
|
|
312
|
+
class="space-y-4 rounded-b-md border border-t-0 dark:bg-slate-900/30 dark:shadow-[inset_0_2px_4px_rgb(0_0_0/0.1)] dark:border-white/[0.08]"
|
|
313
|
+
style="padding: {Math.max(4, 16 - depth * 3)}px;"
|
|
314
|
+
>
|
|
315
|
+
{@const itemPath = joinPath(path, index)}
|
|
316
|
+
<div data-field-path={itemPath}>
|
|
317
|
+
<FieldRenderer
|
|
318
|
+
objectFieldType="inline"
|
|
319
|
+
field={objectField}
|
|
320
|
+
form={form as SuperForm<Record<string, unknown>>}
|
|
321
|
+
path={itemPath as FormPathLeaves<T, ObjectFieldData>}
|
|
322
|
+
{focusedPath}
|
|
323
|
+
{flashingPath}
|
|
324
|
+
depth={depth + 1}
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
</Accordion.Content>
|
|
328
|
+
</Accordion.Item>
|
|
329
|
+
{:else}
|
|
330
|
+
<p class="text-red-500">
|
|
331
|
+
Invalid field configuration. Unknown slug:
|
|
332
|
+
{$value[index].slug}
|
|
333
|
+
</p>
|
|
334
|
+
{/if}
|
|
276
335
|
{:else}
|
|
277
|
-
<p class="text-red-500">
|
|
278
|
-
Invalid field configuration. Unknown slug:
|
|
279
|
-
{$value[index].slug}
|
|
280
|
-
</p>
|
|
336
|
+
<p class="text-red-500">Invalid field configuration. Index: {index}</p>
|
|
281
337
|
{/if}
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
{/if}
|
|
338
|
+
{/key}
|
|
339
|
+
</div>
|
|
285
340
|
{/each}
|
|
286
341
|
{/if}
|
|
287
342
|
</Accordion.Root>
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
28
28
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
29
29
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
30
|
-
import { droppable } from '@thisux/sveltednd';
|
|
31
|
-
import {
|
|
30
|
+
import { droppable, draggable } from '@thisux/sveltednd';
|
|
31
|
+
import { arrayMove } from '../../utils/arrayMove.js';
|
|
32
32
|
import { flip } from 'svelte/animate';
|
|
33
33
|
import { fade } from 'svelte/transition';
|
|
34
34
|
|
|
@@ -195,15 +195,11 @@
|
|
|
195
195
|
container: index.toString(),
|
|
196
196
|
callbacks: {
|
|
197
197
|
onDrop: (state) => {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const dropIndex = parseInt(targetContainer ?? '0');
|
|
198
|
+
const dragIndex = parseInt(state.sourceContainer ?? '');
|
|
199
|
+
const dropIndex = parseInt(state.targetContainer ?? '');
|
|
201
200
|
|
|
202
|
-
if (dragIndex
|
|
203
|
-
|
|
204
|
-
const [moved] = arr.splice(dragIndex, 1);
|
|
205
|
-
arr.splice(dropIndex, 0, moved);
|
|
206
|
-
$value = [...arr];
|
|
201
|
+
if (!isNaN(dragIndex) && !isNaN(dropIndex)) {
|
|
202
|
+
$value = [...arrayMove(getArrayValue(), dragIndex, dropIndex)];
|
|
207
203
|
}
|
|
208
204
|
}
|
|
209
205
|
}
|