includio-cms 0.0.69 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/ROADMAP.md +24 -5
- package/dist/admin/client/account/lang.d.ts +0 -5
- package/dist/admin/client/account/lang.js +2 -12
- package/dist/admin/client/account/preferences-section.svelte +27 -24
- package/dist/admin/client/account/profile-section.svelte +11 -15
- package/dist/admin/client/account/security-section.svelte +0 -11
- package/dist/admin/client/account/sessions-section.svelte +2 -1
- package/dist/admin/client/collection/collection-entries.svelte +168 -43
- package/dist/admin/client/collection/collection-view.svelte.d.ts +5 -0
- package/dist/admin/client/collection/collection-view.svelte.js +22 -5
- package/dist/admin/client/collection/collection.svelte +2 -2
- package/dist/admin/client/collection/data-table.svelte +15 -3
- package/dist/admin/client/collection/data-table.svelte.d.ts +3 -0
- package/dist/admin/client/entry/entry.svelte +11 -7
- package/dist/admin/client/entry/header/status-badge.svelte +6 -3
- package/dist/admin/client/entry/header/version-history-sheet.svelte +6 -3
- package/dist/admin/client/form/form-submissions.svelte +3 -26
- package/dist/admin/components/dashboard/form-submissions-widget.svelte +2 -12
- package/dist/admin/components/dashboard/recent-activity.svelte +2 -12
- package/dist/admin/components/layout/header-actions.svelte +4 -3
- package/dist/admin/components/media/media-library.svelte +17 -6
- package/dist/admin/remote/entry.remote.d.ts +10 -1
- package/dist/admin/remote/entry.remote.js +16 -4
- package/dist/admin/state/interface-language.svelte.d.ts +4 -7
- package/dist/admin/state/interface-language.svelte.js +19 -18
- package/dist/admin/utils/formatDate.d.ts +1 -0
- package/dist/admin/utils/formatDate.js +5 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/core/server/entries/operations/get.d.ts +2 -0
- package/dist/core/server/entries/operations/get.js +6 -0
- package/dist/db-postgres/index.js +26 -1
- package/dist/sveltekit/components/hybrid-context.d.ts +4 -0
- package/dist/sveltekit/components/hybrid-context.js +9 -0
- package/dist/sveltekit/components/hybrid-target.svelte +4 -1
- package/dist/sveltekit/components/image.svelte +5 -2
- package/dist/sveltekit/components/preview.svelte +3 -0
- package/dist/sveltekit/components/video.svelte +4 -1
- package/dist/sveltekit/index.d.ts +1 -0
- package/dist/sveltekit/index.js +1 -0
- package/dist/types/adapters/db.d.ts +3 -1
- package/dist/types/entries.d.ts +10 -2
- package/dist/types/languages.d.ts +2 -1
- package/dist/updates/0.1.0/index.d.ts +2 -0
- package/dist/updates/0.1.0/index.js +25 -0
- package/dist/updates/index.js +2 -1
- package/package.json +1 -1
- package/dist/core/server/utils/sanitizeRichText.d.ts +0 -1
- package/dist/core/server/utils/sanitizeRichText.js +0 -67
|
@@ -28,6 +28,9 @@ function transformDataValuesToLocalized(dataValues, defaultLanguage) {
|
|
|
28
28
|
export const getDbEntries = async (options) => {
|
|
29
29
|
return getCMS().databaseAdapter.getEntries(options);
|
|
30
30
|
};
|
|
31
|
+
export const countDbEntries = async (options) => {
|
|
32
|
+
return getCMS().databaseAdapter.countEntries(options);
|
|
33
|
+
};
|
|
31
34
|
export const getDbEntry = async (options) => {
|
|
32
35
|
const { id, ...rest } = options;
|
|
33
36
|
const [entry] = await getCMS().databaseAdapter.getEntries({
|
|
@@ -43,6 +46,9 @@ export const getDbEntryOrThrow = async (options) => {
|
|
|
43
46
|
}
|
|
44
47
|
return entry;
|
|
45
48
|
};
|
|
49
|
+
export const countRawEntries = async (options) => {
|
|
50
|
+
return countDbEntries(options);
|
|
51
|
+
};
|
|
46
52
|
export const getRawEntries = async (options) => {
|
|
47
53
|
const dbEntries = await getDbEntries(options);
|
|
48
54
|
const entries = await Promise.all(dbEntries.map(async (entry) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
2
2
|
import postgres from 'postgres';
|
|
3
3
|
import * as schema from './schema/index.js';
|
|
4
|
-
import { eq, and, inArray, sql, isNull, desc, SQL, ilike, or } from 'drizzle-orm';
|
|
4
|
+
import { eq, and, inArray, sql, isNull, desc, asc, SQL, ilike, or, count } from 'drizzle-orm';
|
|
5
5
|
const SAFE_JSON_KEY = /^[a-zA-Z0-9_]+$/;
|
|
6
6
|
function validateJsonPathKeys(path) {
|
|
7
7
|
for (const key of path) {
|
|
@@ -109,9 +109,34 @@ export function pg(config) {
|
|
|
109
109
|
if (allConditions.length > 0) {
|
|
110
110
|
query.where(and(...allConditions));
|
|
111
111
|
}
|
|
112
|
+
if (options.orderBy) {
|
|
113
|
+
const col = schema.entriesTable[options.orderBy.column];
|
|
114
|
+
query.orderBy(options.orderBy.direction === 'asc' ? asc(col) : desc(col));
|
|
115
|
+
}
|
|
116
|
+
if (options.limit != null) {
|
|
117
|
+
query.limit(options.limit);
|
|
118
|
+
}
|
|
119
|
+
if (options.offset != null) {
|
|
120
|
+
query.offset(options.offset);
|
|
121
|
+
}
|
|
112
122
|
}
|
|
113
123
|
return await query;
|
|
114
124
|
},
|
|
125
|
+
countEntries: async (options) => {
|
|
126
|
+
const conditions = [
|
|
127
|
+
options.type ? eq(schema.entriesTable.type, options.type) : undefined,
|
|
128
|
+
options.slug ? eq(schema.entriesTable.slug, options.slug) : undefined,
|
|
129
|
+
options.ids ? inArray(schema.entriesTable.id, options.ids) : undefined,
|
|
130
|
+
options.onlyArchived
|
|
131
|
+
? sql `${schema.entriesTable.archivedAt} IS NOT NULL`
|
|
132
|
+
: isNull(schema.entriesTable.archivedAt)
|
|
133
|
+
].filter(Boolean);
|
|
134
|
+
const [result] = await db
|
|
135
|
+
.select({ count: count() })
|
|
136
|
+
.from(schema.entriesTable)
|
|
137
|
+
.where(and(...conditions));
|
|
138
|
+
return result?.count ?? 0;
|
|
139
|
+
},
|
|
115
140
|
updateEntry: async ({ id, data }) => {
|
|
116
141
|
return db.transaction(async (tx) => {
|
|
117
142
|
const [result] = await tx
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const HYBRID_CONTEXT_KEY: unique symbol;
|
|
2
|
+
/** Call in a layout/component script to enable data-hybrid-path rendering for all descendant HybridTarget/Image/Video. */
|
|
3
|
+
export declare function enableHybridEditing(): void;
|
|
4
|
+
export declare function isHybridEnabled(): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
export const HYBRID_CONTEXT_KEY = Symbol('hybrid');
|
|
3
|
+
/** Call in a layout/component script to enable data-hybrid-path rendering for all descendant HybridTarget/Image/Video. */
|
|
4
|
+
export function enableHybridEditing() {
|
|
5
|
+
setContext(HYBRID_CONTEXT_KEY, true);
|
|
6
|
+
}
|
|
7
|
+
export function isHybridEnabled() {
|
|
8
|
+
return !!getContext(HYBRID_CONTEXT_KEY);
|
|
9
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { isHybridEnabled } from './hybrid-context.js';
|
|
4
5
|
|
|
5
6
|
type Props = HTMLAttributes<HTMLElement> & {
|
|
6
7
|
path: string;
|
|
@@ -9,9 +10,11 @@
|
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
let { path, tag = 'div', class: className = '', style, children, ...rest }: Props = $props();
|
|
13
|
+
|
|
14
|
+
const hybrid = isHybridEnabled();
|
|
12
15
|
</script>
|
|
13
16
|
|
|
14
|
-
<svelte:element this={tag} data-hybrid-path={path} class={className} {style} {...rest}>
|
|
17
|
+
<svelte:element this={tag} data-hybrid-path={hybrid ? path : undefined} class={className} {style} {...rest}>
|
|
15
18
|
{#if children}
|
|
16
19
|
{@render children()}
|
|
17
20
|
{/if}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import type { ImageFieldData } from '../../types/fields.js';
|
|
4
4
|
import type { MediaFile } from '../../types/media.js';
|
|
5
5
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
6
|
+
import { isHybridEnabled } from './hybrid-context.js';
|
|
7
|
+
|
|
8
|
+
const hybrid = isHybridEnabled();
|
|
6
9
|
|
|
7
10
|
type Props = HTMLImgAttributes & {
|
|
8
11
|
data: ImageFieldData | MediaFile;
|
|
@@ -44,11 +47,11 @@
|
|
|
44
47
|
|
|
45
48
|
{#if !data}
|
|
46
49
|
<!-- No image data provided -->
|
|
47
|
-
<picture class={pictureClass} data-hybrid-path={hybridPath}>
|
|
50
|
+
<picture class={pictureClass} data-hybrid-path={hybrid ? hybridPath : undefined}>
|
|
48
51
|
<img alt="No image available" class={className} {loading} {...restProps} />
|
|
49
52
|
</picture>
|
|
50
53
|
{:else}
|
|
51
|
-
<picture class={pictureClass} data-hybrid-path={hybridPath}>
|
|
54
|
+
<picture class={pictureClass} data-hybrid-path={hybrid ? hybridPath : undefined}>
|
|
52
55
|
{#if isProperImageObject(data)}
|
|
53
56
|
{#if data.data}
|
|
54
57
|
{#each Object.values(data.styles) as style}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends { data: PopulatedEntryData }">
|
|
2
2
|
import type { PopulatedEntryData } from '../../types/entries.js';
|
|
3
3
|
import { onMount, type Snippet } from 'svelte';
|
|
4
|
+
import { enableHybridEditing } from './hybrid-context.js';
|
|
5
|
+
|
|
6
|
+
enableHybridEditing();
|
|
4
7
|
|
|
5
8
|
type Props = {
|
|
6
9
|
entry: T;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { VideoFieldData } from '../../types/fields.js';
|
|
3
3
|
import type { HTMLVideoAttributes } from 'svelte/elements';
|
|
4
|
+
import { isHybridEnabled } from './hybrid-context.js';
|
|
4
5
|
|
|
5
6
|
type Props = HTMLVideoAttributes & {
|
|
6
7
|
data: VideoFieldData;
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
data,
|
|
14
15
|
...restProps
|
|
15
16
|
}: Props = $props();
|
|
17
|
+
|
|
18
|
+
const hybrid = isHybridEnabled();
|
|
16
19
|
</script>
|
|
17
20
|
|
|
18
21
|
{#if data?.data}
|
|
@@ -22,7 +25,7 @@
|
|
|
22
25
|
width={data.data.width || undefined}
|
|
23
26
|
height={data.data.height || undefined}
|
|
24
27
|
class={className}
|
|
25
|
-
data-hybrid-path={hybridPath}
|
|
28
|
+
data-hybrid-path={hybrid ? hybridPath : undefined}
|
|
26
29
|
{...restProps}
|
|
27
30
|
>
|
|
28
31
|
<source src={data.data.url} type={data.data.mimeType || undefined} />
|
|
@@ -5,4 +5,5 @@ export { default as HybridTarget } from './components/hybrid-target.svelte';
|
|
|
5
5
|
export { default as Image } from './components/image.svelte';
|
|
6
6
|
export { default as Video } from './components/video.svelte';
|
|
7
7
|
export { default as Media } from './components/media.svelte';
|
|
8
|
+
export { enableHybridEditing } from './components/hybrid-context.js';
|
|
8
9
|
export { getLink, isImageFieldData, isVideoFieldData } from './utils/index.js';
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -5,4 +5,5 @@ export { default as HybridTarget } from './components/hybrid-target.svelte';
|
|
|
5
5
|
export { default as Image } from './components/image.svelte';
|
|
6
6
|
export { default as Video } from './components/video.svelte';
|
|
7
7
|
export { default as Media } from './components/media.svelte';
|
|
8
|
+
export { enableHybridEditing } from './components/hybrid-context.js';
|
|
8
9
|
export { getLink, isImageFieldData, isVideoFieldData } from './utils/index.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConsentLogData } from '../consent.js';
|
|
2
|
-
import type { DbEntry, DbEntryInsert, DbEntryVersion, DbEntryVersionInsert, GetDbEntriesOptions, GetDbEntryVersionsOptions } from '../entries.js';
|
|
2
|
+
import type { DbEntry, DbEntryInsert, DbEntryVersion, DbEntryVersionInsert, GetDbEntriesOptions, GetDbEntryVersionsOptions, PaginationOptions } from '../entries.js';
|
|
3
3
|
import type { ImageFieldStyle } from '../fields.js';
|
|
4
4
|
import type { FormSubmission } from '../forms.js';
|
|
5
5
|
import type { ImageStyle, MediaFile, MediaTag, UploadedMediaFile } from '../media.js';
|
|
@@ -7,6 +7,7 @@ export type { FilesAdapter } from './files.js';
|
|
|
7
7
|
export interface DatabaseAdapter {
|
|
8
8
|
createEntry: CreateEntry;
|
|
9
9
|
getEntries: GetEntries;
|
|
10
|
+
countEntries: CountEntries;
|
|
10
11
|
updateEntry: UpdateEntry;
|
|
11
12
|
archiveEntry: ArchiveEntry;
|
|
12
13
|
deleteEntry: DeleteEntry;
|
|
@@ -38,6 +39,7 @@ export interface DatabaseAdapter {
|
|
|
38
39
|
}
|
|
39
40
|
export type CreateEntry = (data: DbEntryInsert) => Promise<DbEntry>;
|
|
40
41
|
export type GetEntries = (data: GetDbEntriesOptions) => Promise<DbEntry[]>;
|
|
42
|
+
export type CountEntries = (data: Omit<GetDbEntriesOptions, keyof PaginationOptions>) => Promise<number>;
|
|
41
43
|
export type UpdateEntry = (data: {
|
|
42
44
|
id: string;
|
|
43
45
|
data: Partial<DbEntry>;
|
package/dist/types/entries.d.ts
CHANGED
|
@@ -64,7 +64,15 @@ export interface DbEntryVersionInsert {
|
|
|
64
64
|
publishedAt?: Date | null;
|
|
65
65
|
publishedBy?: string | null;
|
|
66
66
|
}
|
|
67
|
-
export interface
|
|
67
|
+
export interface PaginationOptions {
|
|
68
|
+
limit?: number;
|
|
69
|
+
offset?: number;
|
|
70
|
+
orderBy?: {
|
|
71
|
+
column: 'createdAt' | 'updatedAt';
|
|
72
|
+
direction: 'asc' | 'desc';
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface GetDbEntriesOptions extends PaginationOptions {
|
|
68
76
|
ids?: string[];
|
|
69
77
|
slug?: string;
|
|
70
78
|
type?: EntryType;
|
|
@@ -76,7 +84,7 @@ export interface GetDbEntryOptions {
|
|
|
76
84
|
type?: EntryType;
|
|
77
85
|
onlyArchived?: boolean;
|
|
78
86
|
}
|
|
79
|
-
export interface GetRawEntriesOptions {
|
|
87
|
+
export interface GetRawEntriesOptions extends PaginationOptions {
|
|
80
88
|
ids?: string[];
|
|
81
89
|
slug?: string;
|
|
82
90
|
onlyArchived?: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { locales } from '../paraglide/runtime.js';
|
|
1
2
|
export type Language = 'en' | 'pl';
|
|
2
|
-
export type InterfaceLanguage =
|
|
3
|
+
export type InterfaceLanguage = (typeof locales)[number];
|
|
3
4
|
export type Localized = Partial<Record<InterfaceLanguage, string>> | string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.1.0',
|
|
3
|
+
date: '2026-02-17',
|
|
4
|
+
description: 'Stabilization — pagination, language switcher, and more',
|
|
5
|
+
features: [
|
|
6
|
+
'Server-side pagination for collection entries (limit/offset/orderBy)',
|
|
7
|
+
'Persistent page index and active tab across navigation',
|
|
8
|
+
'countEntries adapter method for total count queries',
|
|
9
|
+
'Automatic fallback to client-side pagination for search and name sorting'
|
|
10
|
+
],
|
|
11
|
+
fixes: [
|
|
12
|
+
'Language switcher now reactive globally — all UI updates instantly without reload',
|
|
13
|
+
'Removed hardcoded en/pl — locale list derived from Paraglide config',
|
|
14
|
+
'Browser language auto-detected as default interface language',
|
|
15
|
+
'Deduplicated formatRelativeDate across dashboard and form components',
|
|
16
|
+
'Account profile: fixed name input not responding to typing',
|
|
17
|
+
'Account preferences: added aria-pressed to theme/language toggles',
|
|
18
|
+
'Removed placeholder 2FA stub from security section',
|
|
19
|
+
'Media tag counts now display correct values — TagSidebar and BulkActionBar receive all files',
|
|
20
|
+
'Hybrid target: data-hybrid-path no longer rendered on public pages — only in preview iframe'
|
|
21
|
+
],
|
|
22
|
+
breakingChanges: [
|
|
23
|
+
'getRawEntries remote now returns { entries: RawEntry[], total: number } instead of RawEntry[]'
|
|
24
|
+
]
|
|
25
|
+
};
|
package/dist/updates/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import { update as update0066 } from './0.0.66/index.js';
|
|
|
3
3
|
import { update as update0067 } from './0.0.67/index.js';
|
|
4
4
|
import { update as update0068 } from './0.0.68/index.js';
|
|
5
5
|
import { update as update0069 } from './0.0.69/index.js';
|
|
6
|
-
|
|
6
|
+
import { update as update010 } from './0.1.0/index.js';
|
|
7
|
+
export const updates = [update0065, update0066, update0067, update0068, update0069, update010];
|
|
7
8
|
export const getUpdatesFrom = (fromVersion) => {
|
|
8
9
|
const fromParts = fromVersion.split('.').map(Number);
|
|
9
10
|
return updates.filter((update) => {
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function sanitizeRichText(dirty: string): string;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import DOMPurify from 'isomorphic-dompurify';
|
|
2
|
-
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
|
|
3
|
-
const tag = node.tagName.toLowerCase();
|
|
4
|
-
if (data.attrName === 'src' && !['img', 'video'].includes(tag)) {
|
|
5
|
-
data.keepAttr = false;
|
|
6
|
-
}
|
|
7
|
-
if (data.attrName === 'style') {
|
|
8
|
-
const value = node.getAttribute('style') || '';
|
|
9
|
-
const match = value.match(/text-align\s*:\s*(left|center|right|justify)/);
|
|
10
|
-
node.setAttribute('style', match ? `text-align: ${match[1]}` : '');
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
export function sanitizeRichText(dirty) {
|
|
14
|
-
return DOMPurify.sanitize(dirty, {
|
|
15
|
-
ALLOWED_TAGS: [
|
|
16
|
-
'p',
|
|
17
|
-
'h1',
|
|
18
|
-
'h2',
|
|
19
|
-
'h3',
|
|
20
|
-
'h4',
|
|
21
|
-
'h5',
|
|
22
|
-
'h6',
|
|
23
|
-
'strong',
|
|
24
|
-
'em',
|
|
25
|
-
's',
|
|
26
|
-
'code',
|
|
27
|
-
'ul',
|
|
28
|
-
'ol',
|
|
29
|
-
'li',
|
|
30
|
-
'blockquote',
|
|
31
|
-
'br',
|
|
32
|
-
'hr',
|
|
33
|
-
'a',
|
|
34
|
-
'img',
|
|
35
|
-
'table',
|
|
36
|
-
'thead',
|
|
37
|
-
'tbody',
|
|
38
|
-
'tr',
|
|
39
|
-
'th',
|
|
40
|
-
'td',
|
|
41
|
-
'mark',
|
|
42
|
-
'u',
|
|
43
|
-
'pre',
|
|
44
|
-
'video',
|
|
45
|
-
'figure',
|
|
46
|
-
'figcaption'
|
|
47
|
-
],
|
|
48
|
-
ALLOWED_ATTR: [
|
|
49
|
-
'href',
|
|
50
|
-
'target',
|
|
51
|
-
'rel',
|
|
52
|
-
'src',
|
|
53
|
-
'alt',
|
|
54
|
-
'title',
|
|
55
|
-
'class',
|
|
56
|
-
'colspan',
|
|
57
|
-
'rowspan',
|
|
58
|
-
'colwidth',
|
|
59
|
-
'style',
|
|
60
|
-
'poster',
|
|
61
|
-
'data-media-id',
|
|
62
|
-
'controls',
|
|
63
|
-
'width',
|
|
64
|
-
'height'
|
|
65
|
-
]
|
|
66
|
-
});
|
|
67
|
-
}
|