includio-cms 0.13.0 → 0.13.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/ROADMAP.md +24 -0
  3. package/dist/admin/api/handler.js +2 -0
  4. package/dist/admin/api/replace.js +2 -1
  5. package/dist/admin/api/rest/routes/upload.js +2 -1
  6. package/dist/admin/api/upload-limit.d.ts +2 -0
  7. package/dist/admin/api/upload-limit.js +7 -0
  8. package/dist/admin/api/upload.js +2 -1
  9. package/dist/admin/client/collection/collection-entries.svelte +58 -11
  10. package/dist/admin/client/users/users-page.svelte +5 -6
  11. package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
  12. package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
  13. package/dist/admin/components/fields/blocks-field.svelte +31 -9
  14. package/dist/admin/components/fields/simple-array-field.svelte +22 -11
  15. package/dist/admin/components/layout/layout-renderer.svelte +10 -4
  16. package/dist/admin/components/media/file-preview.svelte +10 -1
  17. package/dist/admin/components/media/file-upload.svelte +66 -9
  18. package/dist/admin/components/media/files-list.svelte +12 -3
  19. package/dist/admin/components/media/media-selector.svelte +11 -5
  20. package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
  21. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +32 -1
  22. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
  23. package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
  24. package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
  25. package/dist/admin/components/tiptap/lang.d.ts +77 -0
  26. package/dist/admin/components/tiptap/lang.js +170 -0
  27. package/dist/admin/components/tiptap/link-dialog.svelte +22 -18
  28. package/dist/admin/components/tiptap/slash-command.js +26 -22
  29. package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
  30. package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
  31. package/dist/admin/remote/email.remote.d.ts +1 -0
  32. package/dist/admin/remote/email.remote.js +5 -0
  33. package/dist/admin/remote/entry.remote.d.ts +1 -0
  34. package/dist/admin/remote/entry.remote.js +6 -4
  35. package/dist/admin/remote/index.d.ts +1 -0
  36. package/dist/admin/remote/index.js +1 -0
  37. package/dist/admin/remote/reorder.d.ts +1 -0
  38. package/dist/admin/remote/reorder.js +33 -0
  39. package/dist/core/server/entries/operations/get.js +15 -3
  40. package/dist/core/server/fields/utils/imageStyles.js +7 -3
  41. package/dist/core/server/generator/fields.js +2 -2
  42. package/dist/core/server/generator/generator.js +4 -2
  43. package/dist/core/server/media/styles/operations/createMediaStyle.d.ts +2 -2
  44. package/dist/core/server/media/styles/operations/createMediaStyle.js +5 -3
  45. package/dist/core/server/media/styles/operations/generateDefaultStyles.js +33 -6
  46. package/dist/core/server/media/styles/operations/getImageStyle.d.ts +1 -0
  47. package/dist/core/server/media/styles/operations/getImageStyle.js +3 -0
  48. package/dist/core/server/media/styles/sharp/generateImageStyle.d.ts +2 -1
  49. package/dist/core/server/media/styles/sharp/generateImageStyle.js +5 -3
  50. package/dist/core/server/media/uploadLimit.d.ts +2 -0
  51. package/dist/core/server/media/uploadLimit.js +26 -0
  52. package/dist/types/entries.d.ts +1 -0
  53. package/dist/types/layout.d.ts +0 -1
  54. package/dist/updates/0.13.1/index.d.ts +2 -0
  55. package/dist/updates/0.13.1/index.js +20 -0
  56. package/dist/updates/0.13.2/index.d.ts +2 -0
  57. package/dist/updates/0.13.2/index.js +20 -0
  58. package/dist/updates/index.js +3 -1
  59. package/package.json +1 -1
@@ -119,7 +119,7 @@ export const getEntries = async (options = {}) => {
119
119
  const slugPath = getEntrySlugPath(entry.slug);
120
120
  const slug = getSlugFromEntryData(version.data, slugPath, language);
121
121
  const _url = slug ? getEntryPath(entry.slug, slug) : undefined;
122
- return {
122
+ const result = {
123
123
  _id: entry.id,
124
124
  _slug: entry.slug,
125
125
  _type: entry.type,
@@ -127,6 +127,10 @@ export const getEntries = async (options = {}) => {
127
127
  _url,
128
128
  ...populatedData
129
129
  };
130
+ if (config.type === 'collection' && config.orderable) {
131
+ result._sortOrder = entry.sortOrder;
132
+ }
133
+ return result;
130
134
  }
131
135
  catch (error) {
132
136
  console.error(`[CMS] Failed to populate entry ${entry.id} (${entry.slug}):`, error);
@@ -206,7 +210,7 @@ export const getEntries = async (options = {}) => {
206
210
  const slugPath = getEntrySlugPath(dbEntry.slug);
207
211
  const slug = getSlugFromEntryData(version.data, slugPath, language);
208
212
  const _url = slug ? getEntryPath(dbEntry.slug, slug) : undefined;
209
- return {
213
+ const result = {
210
214
  _id: dbEntry.id,
211
215
  _slug: dbEntry.slug,
212
216
  _type: dbEntry.type,
@@ -214,6 +218,10 @@ export const getEntries = async (options = {}) => {
214
218
  _url,
215
219
  ...populatedData
216
220
  };
221
+ if (config.type === 'collection' && config.orderable) {
222
+ result._sortOrder = dbEntry.sortOrder;
223
+ }
224
+ return result;
217
225
  }
218
226
  catch (error) {
219
227
  console.error(`[CMS] Failed to populate entry ${dbEntry.id} (${dbEntry.slug}):`, error);
@@ -349,7 +357,7 @@ export const getEntryVersion = async (options) => {
349
357
  const slugPath = getEntrySlugPath(dbEntry.slug);
350
358
  const entrySlug = getSlugFromEntryData(dbEntryVersion.data, slugPath, language);
351
359
  const _url = entrySlug ? getEntryPath(dbEntry.slug, entrySlug) : undefined;
352
- return {
360
+ const result = {
353
361
  _id: dbEntry.id,
354
362
  _slug: dbEntry.slug,
355
363
  _type: dbEntry.type,
@@ -357,6 +365,10 @@ export const getEntryVersion = async (options) => {
357
365
  _url,
358
366
  ...populatedData
359
367
  };
368
+ if (config.type === 'collection' && config.orderable) {
369
+ result._sortOrder = dbEntry.sortOrder;
370
+ }
371
+ return result;
360
372
  }
361
373
  catch (error) {
362
374
  console.error(`[CMS] Failed to populate entry ${dbEntry.id} (${dbEntry.slug}):`, error);
@@ -1,4 +1,4 @@
1
- import { getImageStyle } from '../../media/styles/operations/getImageStyle.js';
1
+ import { getImageStyleIfExists } from '../../media/styles/operations/getImageStyle.js';
2
2
  import { getCMS } from '../../../cms.js';
3
3
  import { generateBlurDataUrl } from '../../media/utils/generateBlurDataUrl.js';
4
4
  export const defaultStyles = [
@@ -67,7 +67,9 @@ export async function getImageStyles(field, val) {
67
67
  const [styles, blurDataUrl] = await Promise.all([
68
68
  Promise.all(stylesArr.map(async (style) => {
69
69
  try {
70
- const styleDbData = await getImageStyle(val.id, style);
70
+ const styleDbData = await getImageStyleIfExists(val.id, style);
71
+ if (!styleDbData)
72
+ return null;
71
73
  const result = {
72
74
  url: styleDbData.url,
73
75
  media: styleDbData.media,
@@ -86,7 +88,9 @@ export async function getImageStyles(field, val) {
86
88
  srcset: undefined,
87
89
  sizes: undefined
88
90
  };
89
- const variantData = await getImageStyle(val.id, variantStyle);
91
+ const variantData = await getImageStyleIfExists(val.id, variantStyle);
92
+ if (!variantData)
93
+ return null;
90
94
  return `${variantData.url} ${w}w`;
91
95
  }
92
96
  catch (e) {
@@ -103,8 +103,8 @@ function getFlatFieldTypeAsString(field) {
103
103
  switch (field.type) {
104
104
  case 'media': {
105
105
  const base = field.multiple
106
- ? '(FlatImageFieldData | FlatVideoFieldData)[]'
107
- : 'FlatImageFieldData | FlatVideoFieldData';
106
+ ? '(ImageFieldData | VideoFieldData)[]'
107
+ : 'ImageFieldData | VideoFieldData';
108
108
  return base + (field.required ? '' : ' | null');
109
109
  }
110
110
  case 'relation': {
@@ -19,6 +19,8 @@ function generateTypesStringForRecords(type, records) {
19
19
  const fieldsType = generateFlatTsTypeFromFields(getFieldsFromConfig(single));
20
20
  // Strip outer braces to inline fields into the interface
21
21
  const innerFields = fieldsType.replace(/^\s*\{/, '').replace(/\}\s*$/, '');
22
+ const sortOrderField = type === 'collection' && 'orderable' in single && single.orderable
23
+ ? '_sortOrder: number;\n\t\t\t\t' : '';
22
24
  return `
23
25
  export interface ${toPascalCase(single.slug)} {
24
26
  _id: string;
@@ -26,7 +28,7 @@ function generateTypesStringForRecords(type, records) {
26
28
  _type: string;
27
29
  _publishedAt: Date | null;
28
30
  _url?: string;
29
- ${innerFields}
31
+ ${sortOrderField}${innerFields}
30
32
  }
31
33
  `;
32
34
  })
@@ -108,7 +110,7 @@ function generateTypes(config) {
108
110
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
109
111
  const filePath = join(cmsDir, 'types.ts');
110
112
  let code = `// This file is auto-generated. Do not edit directly.\n\n`;
111
- code += `import type { MediaFile, ImageFieldData, VideoFieldData, FlatImageFieldData, FlatVideoFieldData, StructuredContentDoc, SCTypedInlineBlock } from 'includio-cms/types';\n\n`;
113
+ code += `import type { MediaFile, ImageFieldData, VideoFieldData, StructuredContentDoc, SCTypedInlineBlock } from 'includio-cms/types';\n\n`;
112
114
  if (config.singles && config.singles.length > 0) {
113
115
  code += generateTypesStringForRecords('single', Object.values(config.singles));
114
116
  }
@@ -1,3 +1,3 @@
1
1
  import type { ImageFieldStyle } from '../../../../../types/fields.js';
2
- import type { ImageStyle } from '../../../../../types/media.js';
3
- export declare function createImageStyle(mediaFileId: string, style: ImageFieldStyle): Promise<ImageStyle>;
2
+ import type { ImageStyle, MediaFile } from '../../../../../types/media.js';
3
+ export declare function createImageStyle(mediaFileId: string, style: ImageFieldStyle, buffer?: Buffer, mediaFile?: MediaFile): Promise<ImageStyle>;
@@ -1,11 +1,13 @@
1
1
  import { getCMS } from '../../../../cms.js';
2
- import { generateImageStyle } from '../sharp/generateImageStyle.js';
3
- export async function createImageStyle(mediaFileId, style) {
2
+ import { generateImageStyle, generateImageStyleFromBuffer } from '../sharp/generateImageStyle.js';
3
+ export async function createImageStyle(mediaFileId, style, buffer, mediaFile) {
4
4
  const cms = getCMS();
5
5
  // Check for existing style to clean up old file after upsert
6
6
  const existing = await cms.databaseAdapter.getImageStyle(mediaFileId, style);
7
7
  const oldUrl = existing?.url;
8
- const imageStyleFile = await generateImageStyle(mediaFileId, style);
8
+ const imageStyleFile = buffer && mediaFile
9
+ ? await generateImageStyleFromBuffer(buffer, mediaFile, style)
10
+ : await generateImageStyle(mediaFileId, style);
9
11
  const imageStyle = await cms.databaseAdapter.createImageStyle(mediaFileId, imageStyleFile, style);
10
12
  // Delete old file from disk if URL changed
11
13
  if (oldUrl && oldUrl !== imageStyle.url) {
@@ -1,16 +1,35 @@
1
+ import { getCMS } from '../../../../cms.js';
1
2
  import { defaultStyles, isProcessableImage, expandStyleFormats, getOriginalFormat } from '../../../fields/utils/imageStyles.js';
2
- import { getImageStyle } from './getImageStyle.js';
3
- export async function generateDefaultStyles(mediaFile) {
4
- if (!isProcessableImage(mediaFile))
5
- return;
3
+ import { createImageStyle } from './createMediaStyle.js';
4
+ const CONCURRENCY = 3;
5
+ async function runWithConcurrency(tasks, limit) {
6
+ const results = new Array(tasks.length);
7
+ let i = 0;
8
+ async function next() {
9
+ while (i < tasks.length) {
10
+ const idx = i++;
11
+ results[idx] = await tasks[idx]();
12
+ }
13
+ }
14
+ await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, () => next()));
15
+ return results;
16
+ }
17
+ async function downloadOriginalBuffer(mediaFile) {
18
+ const file = await getCMS().filesAdapter.downloadFile(mediaFile.url.split('/').pop() || '');
19
+ if (!file)
20
+ throw new Error('Media file not found');
21
+ return Buffer.from(await file.arrayBuffer());
22
+ }
23
+ function collectStyleTasks(mediaFile) {
6
24
  const origFormat = getOriginalFormat(mediaFile);
7
25
  const expanded = expandStyleFormats(defaultStyles, origFormat);
26
+ const tasks = [];
8
27
  for (const style of expanded) {
9
- await getImageStyle(mediaFile.id, style);
28
+ tasks.push(style);
10
29
  if (style.srcset && mediaFile.width) {
11
30
  const widths = style.srcset.filter((w) => w <= mediaFile.width);
12
31
  for (const w of widths) {
13
- await getImageStyle(mediaFile.id, {
32
+ tasks.push({
14
33
  ...style,
15
34
  name: `${style.name}_${w}w`,
16
35
  width: w,
@@ -20,6 +39,14 @@ export async function generateDefaultStyles(mediaFile) {
20
39
  }
21
40
  }
22
41
  }
42
+ return tasks;
43
+ }
44
+ export async function generateDefaultStyles(mediaFile) {
45
+ if (!isProcessableImage(mediaFile))
46
+ return;
47
+ const buffer = await downloadOriginalBuffer(mediaFile);
48
+ const styles = collectStyleTasks(mediaFile);
49
+ await runWithConcurrency(styles.map((style) => () => createImageStyle(mediaFile.id, style, buffer, mediaFile)), CONCURRENCY);
23
50
  }
24
51
  export function generateDefaultStylesInBackground(mediaFile) {
25
52
  generateDefaultStyles(mediaFile).catch((e) => console.warn('Background style generation failed:', e));
@@ -1,3 +1,4 @@
1
1
  import type { ImageFieldStyle } from '../../../../../types/fields.js';
2
2
  import type { ImageStyle } from '../../../../../types/media.js';
3
3
  export declare function getImageStyle(mediaFileId: string, style: ImageFieldStyle): Promise<ImageStyle>;
4
+ export declare function getImageStyleIfExists(mediaFileId: string, style: ImageFieldStyle): Promise<ImageStyle | null>;
@@ -6,3 +6,6 @@ export async function getImageStyle(mediaFileId, style) {
6
6
  return imageStyle;
7
7
  return createImageStyle(mediaFileId, style);
8
8
  }
9
+ export async function getImageStyleIfExists(mediaFileId, style) {
10
+ return getCMS().databaseAdapter.getImageStyle(mediaFileId, style);
11
+ }
@@ -1,3 +1,4 @@
1
1
  import type { ImageFieldStyle } from '../../../../../types/fields.js';
2
- import type { UploadedMediaFile } from '../../../../../types/media.js';
2
+ import type { MediaFile, UploadedMediaFile } from '../../../../../types/media.js';
3
3
  export declare function generateImageStyle(mediaFileId: string, style: ImageFieldStyle): Promise<UploadedMediaFile>;
4
+ export declare function generateImageStyleFromBuffer(buf: Buffer, mediaFile: MediaFile, style: ImageFieldStyle): Promise<UploadedMediaFile>;
@@ -14,8 +14,10 @@ export async function generateImageStyle(mediaFileId, style) {
14
14
  if (!file) {
15
15
  throw new Error('Media file not found');
16
16
  }
17
- const imageBuffer = await file.arrayBuffer();
18
- const buf = Buffer.from(imageBuffer);
17
+ const buf = Buffer.from(await file.arrayBuffer());
18
+ return generateImageStyleFromBuffer(buf, mediaFile, style);
19
+ }
20
+ export async function generateImageStyleFromBuffer(buf, mediaFile, style) {
19
21
  // Read EXIF orientation before processing
20
22
  const metadata = await sharp(buf).metadata();
21
23
  // .rotate() applies EXIF orientation to pixels AND strips the tag from output.
@@ -51,7 +53,7 @@ export async function generateImageStyle(mediaFileId, style) {
51
53
  const format = style.format ?? originalExt ?? 'jpeg';
52
54
  sharpInstance = sharpInstance.toFormat(format, style.quality != null ? { quality: Math.max(1, Math.min(100, style.quality)) } : undefined);
53
55
  const outputBuffer = await sharpInstance.toBuffer();
54
- return getCMS().filesAdapter.uploadFile(new File([new Uint8Array(outputBuffer)], `${mediaFileId}_${style.name}_${Date.now().toString(36)}.${format}`, {
56
+ return getCMS().filesAdapter.uploadFile(new File([new Uint8Array(outputBuffer)], `${mediaFile.id}_${style.name}_${Date.now().toString(36)}.${format}`, {
55
57
  type: `image/${format}`
56
58
  }));
57
59
  }
@@ -0,0 +1,2 @@
1
+ export declare function parseAsBytes(value: string): number;
2
+ export declare function getMaxUploadSize(): number;
@@ -0,0 +1,26 @@
1
+ const SUFFIX_MULTIPLIERS = {
2
+ K: 1024,
3
+ M: 1024 * 1024,
4
+ G: 1024 * 1024 * 1024
5
+ };
6
+ const DEFAULT_LIMIT = '50M';
7
+ export function parseAsBytes(value) {
8
+ const trimmed = value.trim();
9
+ if (!trimmed)
10
+ return 0;
11
+ const suffix = trimmed.at(-1).toUpperCase();
12
+ const multiplier = SUFFIX_MULTIPLIERS[suffix];
13
+ if (multiplier) {
14
+ const num = Number(trimmed.slice(0, -1));
15
+ if (isNaN(num) || num < 0)
16
+ return 0;
17
+ return Math.floor(num * multiplier);
18
+ }
19
+ const num = Number(trimmed);
20
+ if (isNaN(num) || num < 0)
21
+ return 0;
22
+ return Math.floor(num);
23
+ }
24
+ export function getMaxUploadSize() {
25
+ return parseAsBytes(process.env.BODY_SIZE_LIMIT || DEFAULT_LIMIT);
26
+ }
@@ -41,6 +41,7 @@ export type Entry = {
41
41
  _type: EntryType;
42
42
  _publishedAt?: Date | null;
43
43
  _url?: string;
44
+ _sortOrder?: number | null;
44
45
  } & Record<string, unknown>;
45
46
  export interface DbEntryVersion {
46
47
  id: string;
@@ -31,7 +31,6 @@ export interface ColumnsNode extends LayoutNodeBase {
31
31
  }
32
32
  export interface CardNode extends LayoutNodeBase {
33
33
  type: 'card';
34
- label: Localized;
35
34
  /**
36
35
  * Field references — supports top-level slugs and dot-notation for object fields.
37
36
  * @example ['companyInfo.name', 'companyInfo.motto'] — fields from different objects in one card
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,20 @@
1
+ export const update = {
2
+ version: '0.13.1',
3
+ date: '2026-03-20',
4
+ description: 'Admin UI i18n, codegen cleanup, docs rewrite',
5
+ features: [
6
+ 'Admin UI i18n — TipTap editor, blocks field, array field, media components support pl/en interface language',
7
+ 'Inline block accordion labels — show field value in collapsed block header',
8
+ 'Blocks field UrlFieldData label support',
9
+ 'Email configuration remote endpoint'
10
+ ],
11
+ fixes: [
12
+ 'Codegen: remove unused FlatImageFieldData/FlatVideoFieldData, use ImageFieldData/VideoFieldData',
13
+ 'Layout renderer: hide card header when label is empty',
14
+ 'Collection entries: improved relation label fetching with UUID validation and multi-version support',
15
+ 'Users page: client-side email config check, remove server load dependency',
16
+ 'Entry remote: stricter UUID validation for ids parameter',
17
+ 'Layout type: remove unused label property from layout node'
18
+ ],
19
+ breakingChanges: []
20
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,20 @@
1
+ export const update = {
2
+ version: '0.13.2',
3
+ date: '2026-03-20',
4
+ description: 'Upload limits, stable reordering, relation filters, image style optimization',
5
+ features: [
6
+ 'Configurable upload size limit via BODY_SIZE_LIMIT env var (supports K/M/G suffixes, default 50M)',
7
+ 'File upload pre-validation — rejects oversized files before upload with localized error messages',
8
+ 'Stable slot reordering — preserves positions of filtered-out entries during DnD reorder',
9
+ 'Collection relation field filtering — filter entries by relation field values',
10
+ 'Expose _sortOrder on entries for orderable collections',
11
+ 'Codegen: _sortOrder field in generated TypeScript interfaces for orderable collections',
12
+ 'getImageStyleIfExists — non-blocking image style lookup (returns null if missing)'
13
+ ],
14
+ fixes: [
15
+ 'Concurrent image style generation with buffer reuse — download original once, generate styles in parallel (limit 3)',
16
+ 'Image style resolution gracefully skips missing styles instead of blocking',
17
+ 'Upload endpoints use configurable size limit instead of hardcoded 50MB'
18
+ ],
19
+ breakingChanges: []
20
+ };
@@ -33,7 +33,9 @@ import { update as update0100 } from './0.10.0/index.js';
33
33
  import { update as update0110 } from './0.11.0/index.js';
34
34
  import { update as update0120 } from './0.12.0/index.js';
35
35
  import { update as update0130 } from './0.13.0/index.js';
36
- export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130];
36
+ import { update as update0131 } from './0.13.1/index.js';
37
+ import { update as update0132 } from './0.13.2/index.js';
38
+ export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132];
37
39
  export const getUpdatesFrom = (fromVersion) => {
38
40
  const fromParts = fromVersion.split('.').map(Number);
39
41
  return updates.filter((update) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",