includio-cms 0.5.3 → 0.5.7

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 (109) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/ROADMAP.md +33 -0
  3. package/dist/admin/api/rest/handler.d.ts +7 -0
  4. package/dist/admin/api/rest/handler.js +116 -0
  5. package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
  6. package/dist/admin/api/rest/middleware/apiKey.js +45 -0
  7. package/dist/admin/api/rest/routes/collections.d.ts +5 -0
  8. package/dist/admin/api/rest/routes/collections.js +104 -0
  9. package/dist/admin/api/rest/routes/entries.d.ts +2 -0
  10. package/dist/admin/api/rest/routes/entries.js +37 -0
  11. package/dist/admin/api/rest/routes/languages.d.ts +1 -0
  12. package/dist/admin/api/rest/routes/languages.js +5 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +2 -0
  14. package/dist/admin/api/rest/routes/schema.js +78 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
  16. package/dist/admin/api/rest/routes/singletons.js +60 -0
  17. package/dist/admin/auth-client.d.ts +48 -48
  18. package/dist/admin/client/admin/admin-after-login-layout.svelte +5 -3
  19. package/dist/admin/client/admin/admin-layout.svelte +0 -2
  20. package/dist/admin/client/collection/bulk-actions-bar.svelte +3 -1
  21. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -1
  22. package/dist/admin/client/collection/collection-entries.svelte +334 -166
  23. package/dist/admin/client/collection/collection-page.svelte +6 -3
  24. package/dist/admin/client/collection/data-table.svelte +127 -18
  25. package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
  26. package/dist/admin/client/collection/row-actions.svelte +3 -1
  27. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -1
  28. package/dist/admin/client/entry/entry-page.svelte +5 -4
  29. package/dist/admin/client/form/form-config-wrapper.svelte +5 -3
  30. package/dist/admin/client/form/form-page.svelte +5 -5
  31. package/dist/admin/client/form/form-submission/form-submission-page.svelte +30 -32
  32. package/dist/admin/components/accessibility/accessibility-overview.svelte +7 -5
  33. package/dist/admin/components/accessibility/accessibility-overview.svelte.d.ts +2 -17
  34. package/dist/admin/components/dashboard/a11y-gauge.svelte +7 -4
  35. package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +2 -17
  36. package/dist/admin/components/dashboard/accessibility-hub.svelte +7 -4
  37. package/dist/admin/components/dashboard/accessibility-hub.svelte.d.ts +2 -17
  38. package/dist/admin/components/dashboard/form-submissions-widget.svelte +8 -3
  39. package/dist/admin/components/dashboard/form-submissions-widget.svelte.d.ts +2 -17
  40. package/dist/admin/components/dashboard/recent-activity.svelte +8 -3
  41. package/dist/admin/components/dashboard/recent-activity.svelte.d.ts +2 -17
  42. package/dist/admin/components/dashboard/recent-entries.svelte +8 -3
  43. package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +2 -17
  44. package/dist/admin/components/fields/image-field.svelte +21 -20
  45. package/dist/admin/components/fields/media-field.svelte +45 -44
  46. package/dist/admin/components/fields/relation-field.svelte +428 -149
  47. package/dist/admin/components/fields/relation-picker-dialog.svelte +112 -0
  48. package/dist/admin/components/fields/relation-picker-dialog.svelte.d.ts +22 -0
  49. package/dist/admin/components/fields/seo-field.svelte +66 -10
  50. package/dist/admin/components/icons/icon-map.d.ts +4 -0
  51. package/dist/admin/components/icons/icon-map.js +143 -0
  52. package/dist/admin/components/layout/header-actions.svelte +0 -10
  53. package/dist/admin/components/layout/nav-collections.svelte +55 -57
  54. package/dist/admin/components/layout/nav-collections.svelte.d.ts +2 -17
  55. package/dist/admin/components/layout/nav-forms.svelte +49 -49
  56. package/dist/admin/components/layout/nav-forms.svelte.d.ts +2 -17
  57. package/dist/admin/components/layout/nav-search.svelte +15 -22
  58. package/dist/admin/components/layout/nav-singletons.svelte +35 -38
  59. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +2 -17
  60. package/dist/admin/components/media/file/file-preview.svelte +92 -91
  61. package/dist/admin/components/media/file-preview.svelte +17 -18
  62. package/dist/admin/components/media/media-library.svelte +39 -41
  63. package/dist/admin/components/media/media-selector.svelte +8 -8
  64. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +6 -8
  65. package/dist/admin/components/tiptap/content-editor.svelte +2 -2
  66. package/dist/admin/components/tiptap/editor-toolbar.svelte +1 -1
  67. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  68. package/dist/admin/components/tiptap/inline-block-node.js +12 -1
  69. package/dist/admin/components/tiptap/tiptap-editor.svelte +1 -1
  70. package/dist/admin/remote/entry.remote.d.ts +20 -1
  71. package/dist/admin/remote/entry.remote.js +24 -3
  72. package/dist/admin/remote/invite.d.ts +2 -2
  73. package/dist/cli/scaffold/admin.js +8 -0
  74. package/dist/cms/runtime/remote.d.ts +8 -0
  75. package/dist/cms/runtime/remote.js +7 -0
  76. package/dist/cms/runtime/types.d.ts +1 -1
  77. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  78. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  79. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  80. package/dist/components/ui/sonner/sonner.svelte +1 -2
  81. package/dist/core/cms.d.ts +2 -1
  82. package/dist/core/cms.js +2 -0
  83. package/dist/core/server/entries/operations/get.d.ts +11 -0
  84. package/dist/core/server/entries/operations/get.js +63 -1
  85. package/dist/core/server/entries/operations/update.d.ts +1 -0
  86. package/dist/core/server/entries/operations/update.js +5 -1
  87. package/dist/core/server/fields/resolveUrlFields.js +49 -18
  88. package/dist/db-postgres/schema/entry.d.ts +17 -0
  89. package/dist/db-postgres/schema/entry.js +4 -2
  90. package/dist/server/auth.d.ts +4 -4
  91. package/dist/sveltekit/components/structured-content.svelte +18 -5
  92. package/dist/sveltekit/server/handle.js +1 -0
  93. package/dist/types/cms.d.ts +7 -0
  94. package/dist/types/collections.d.ts +2 -0
  95. package/dist/types/config.d.ts +2 -2
  96. package/dist/types/entries.d.ts +7 -1
  97. package/dist/types/forms.d.ts +2 -0
  98. package/dist/types/index.d.ts +2 -1
  99. package/dist/types/index.js +1 -0
  100. package/dist/updates/0.5.4/index.d.ts +2 -0
  101. package/dist/updates/0.5.4/index.js +15 -0
  102. package/dist/updates/0.5.5/index.d.ts +2 -0
  103. package/dist/updates/0.5.5/index.js +20 -0
  104. package/dist/updates/0.5.6/index.d.ts +2 -0
  105. package/dist/updates/0.5.6/index.js +17 -0
  106. package/dist/updates/0.5.7/index.d.ts +2 -0
  107. package/dist/updates/0.5.7/index.js +14 -0
  108. package/dist/updates/index.js +5 -1
  109. package/package.json +6 -2
@@ -161,7 +161,8 @@ export const getEntries = async (options = {}) => {
161
161
  // Get entries that match the slug/ids filter
162
162
  const dbEntries = await getCMS().databaseAdapter.getEntries({
163
163
  ids,
164
- slug
164
+ slug,
165
+ orderBy: options.orderBy
165
166
  });
166
167
  if (dbEntries.length === 0) {
167
168
  return [];
@@ -227,6 +228,9 @@ export const getEntries = async (options = {}) => {
227
228
  }
228
229
  versionEntries = Array.from(latestByEntry.values());
229
230
  }
231
+ // Re-sort to match original entry order (DB returns sorted by sortOrder, but version fetch may reorder)
232
+ const entryOrder = new Map(filteredEntries.map((e, i) => [e.id, i]));
233
+ versionEntries.sort((a, b) => (entryOrder.get(a.dbEntry.id) ?? 0) - (entryOrder.get(b.dbEntry.id) ?? 0));
230
234
  // Post-filter: apply dataValues/dataLike on versions
231
235
  if (dataValues) {
232
236
  versionEntries = versionEntries.filter((e) => matchesDataValues(e.version.data, dataValues));
@@ -267,6 +271,64 @@ export const getEntryOrThrow = async (options = {}) => {
267
271
  }
268
272
  return entry;
269
273
  };
274
+ export const getEntryLabels = async (options) => {
275
+ const cms = getCMS();
276
+ const config = cms.getBySlug(options.slug);
277
+ if (!config || config.type !== 'collection')
278
+ return [];
279
+ const dbEntries = await getDbEntries({
280
+ slug: options.slug,
281
+ ids: options.ids
282
+ });
283
+ if (dbEntries.length === 0)
284
+ return [];
285
+ // Filter by status at entry level
286
+ const now = new Date();
287
+ const statusFilter = options.status ?? 'all';
288
+ const filteredDbEntries = statusFilter === 'all'
289
+ ? dbEntries
290
+ : dbEntries.filter((entry) => {
291
+ if (statusFilter === 'published') {
292
+ return entry.publishedVersionId != null && entry.publishedAt != null && entry.publishedAt <= now;
293
+ }
294
+ // draft
295
+ return entry.publishedVersionId == null;
296
+ });
297
+ if (filteredDbEntries.length === 0)
298
+ return [];
299
+ const entryIds = filteredDbEntries.map((e) => e.id);
300
+ const allVersions = await cms.databaseAdapter.getEntryVersions({ entryIds });
301
+ const language = cms.languages[0];
302
+ const entryAdminTitle = config.entryAdminTitle;
303
+ let results = filteredDbEntries.map((entry) => {
304
+ // Pick published version or latest draft
305
+ const entryVersions = allVersions.filter((v) => v.entryId === entry.id);
306
+ const publishedVersion = entry.publishedVersionId
307
+ ? entryVersions.find((v) => v.id === entry.publishedVersionId)
308
+ : null;
309
+ const latestVersion = publishedVersion ??
310
+ entryVersions.sort((a, b) => b.versionNumber - a.versionNumber)[0] ?? null;
311
+ let label = entry.id;
312
+ if (entryAdminTitle && latestVersion) {
313
+ const titleData = latestVersion.data[entryAdminTitle];
314
+ if (titleData && typeof titleData === 'object') {
315
+ label = String(titleData[language] || Object.values(titleData)[0] || entry.id);
316
+ }
317
+ }
318
+ return { id: entry.id, label };
319
+ });
320
+ // Post-query search filtering (case-insensitive includes)
321
+ if (options.search) {
322
+ const searchLower = options.search.toLowerCase();
323
+ results = results.filter((r) => r.label.toLowerCase().includes(searchLower));
324
+ }
325
+ const total = results.length;
326
+ // Apply limit
327
+ if (options.limit && options.limit > 0) {
328
+ results = results.slice(0, options.limit);
329
+ }
330
+ return results.map((r) => ({ ...r, total }));
331
+ };
270
332
  export const getDbEntryVersions = async (options) => {
271
333
  return getCMS().databaseAdapter.getEntryVersions(options);
272
334
  };
@@ -6,6 +6,7 @@ export declare const updateEntrySchema: z.ZodObject<{
6
6
  publishedAt: z.ZodOptional<z.ZodNullable<z.ZodDate>>;
7
7
  publishedVersionId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
8
8
  publishedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
9
+ sortOrder: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
9
10
  }, z.z.core.$strip>;
10
11
  export declare const updateEntry: (id: string, data: Partial<DbEntry>) => Promise<DbEntry>;
11
12
  export declare const updateEntryVersionSchema: z.ZodObject<{
@@ -9,7 +9,8 @@ export const updateEntrySchema = z.object({
9
9
  archivedAt: z.date().nullable().optional(),
10
10
  publishedAt: z.date().nullable().optional(),
11
11
  publishedVersionId: z.string().uuid().nullable().optional(),
12
- publishedBy: z.string().nullable().optional()
12
+ publishedBy: z.string().nullable().optional(),
13
+ sortOrder: z.number().int().nullable().optional()
13
14
  });
14
15
  export const updateEntry = async (id, data) => {
15
16
  const filteredDataParse = updateEntrySchema.safeParse(data);
@@ -33,6 +34,9 @@ export const updateEntry = async (id, data) => {
33
34
  if (filteredData.publishedBy !== undefined) {
34
35
  dataToUpdate.publishedBy = filteredData.publishedBy;
35
36
  }
37
+ if (filteredData.sortOrder !== undefined) {
38
+ dataToUpdate.sortOrder = filteredData.sortOrder;
39
+ }
36
40
  const updatedEntry = await getCMS().databaseAdapter.updateEntry({
37
41
  id,
38
42
  data: {
@@ -1,8 +1,29 @@
1
1
  import { urlFieldDataSchema, urlFieldDataWithRelationSchema } from '../../../schemas/field/url.js';
2
2
  import { walkInlineBlockNodes, cloneDoc } from '../../../admin/components/tiptap/structured-content-utils.js';
3
- import { getEntries } from '../entries/operations/get.js';
3
+ import { getDbEntries, getDbEntryVersions } from '../entries/operations/get.js';
4
4
  import { getEntrySlugPath, getSlugFromEntryData } from './slugResolver.js';
5
5
  import { isExternalUrl, mergeRel } from '../../fields/urlUtils.js';
6
+ const FLAT_KEY = '_flat';
7
+ /**
8
+ * Inline block blockData stores url/text as flat strings (denormalized per-language).
9
+ * Schemas expect Record<string, string>. Wrap flat strings so safeParse succeeds.
10
+ */
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ function normalizeUrlFieldData(val) {
13
+ if (!val || typeof val !== 'object' || Array.isArray(val))
14
+ return val;
15
+ const obj = { ...val };
16
+ if (typeof obj.url === 'string')
17
+ obj.url = { [FLAT_KEY]: obj.url };
18
+ if (typeof obj.text === 'string')
19
+ obj.text = { [FLAT_KEY]: obj.text };
20
+ return obj;
21
+ }
22
+ function resolveFlatOrLocalized(record, language) {
23
+ if (FLAT_KEY in record)
24
+ return record[FLAT_KEY];
25
+ return language ? record[language] : record;
26
+ }
6
27
  function applyExternalAutoDetect(resolvedUrl, extras) {
7
28
  const url = typeof resolvedUrl === 'string' ? resolvedUrl : '';
8
29
  if (url && isExternalUrl(url)) {
@@ -30,6 +51,7 @@ export async function resolveUrlFields(data, fields, language) {
30
51
  if (allStrings)
31
52
  normalizedVal = { url: val };
32
53
  }
54
+ normalizedVal = normalizeUrlFieldData(normalizedVal);
33
55
  const parsedVal = urlFieldDataWithRelationSchema.safeParse(normalizedVal);
34
56
  if (parsedVal.success) {
35
57
  entriesIds.push(parsedVal.data.id);
@@ -71,7 +93,7 @@ export async function resolveUrlFields(data, fields, language) {
71
93
  case 'array':
72
94
  if (field.of === 'url' && Array.isArray(val)) {
73
95
  for (const item of val) {
74
- const parsedVal = urlFieldDataWithRelationSchema.safeParse(item);
96
+ const parsedVal = urlFieldDataWithRelationSchema.safeParse(normalizeUrlFieldData(item));
75
97
  if (parsedVal.success) {
76
98
  entriesIds.push(parsedVal.data.id);
77
99
  }
@@ -84,12 +106,23 @@ export async function resolveUrlFields(data, fields, language) {
84
106
  collectIds(data, fields);
85
107
  const slugMap = {};
86
108
  if (entriesIds.length > 0) {
87
- const entries = await getEntries({ ids: entriesIds, language });
88
- for (const entry of entries) {
89
- const slugPath = getEntrySlugPath(entry.slug);
90
- const slug = getSlugFromEntryData(entry.data, slugPath);
91
- if (slug)
92
- slugMap[entry.id] = slug;
109
+ // Use raw DB calls to avoid recursive populateEntryData → resolveUrlFields loop
110
+ const dbEntries = await getDbEntries({ ids: entriesIds });
111
+ const publishedVersionIds = dbEntries
112
+ .map((e) => e.publishedVersionId)
113
+ .filter((id) => id != null);
114
+ if (publishedVersionIds.length > 0) {
115
+ const versions = await getDbEntryVersions({ ids: publishedVersionIds });
116
+ const versionByEntryId = new Map(versions.map((v) => [v.entryId, v]));
117
+ for (const entry of dbEntries) {
118
+ const version = versionByEntryId.get(entry.id);
119
+ if (!version)
120
+ continue;
121
+ const slugPath = getEntrySlugPath(entry.slug);
122
+ const slug = getSlugFromEntryData(version.data, slugPath, language);
123
+ if (slug)
124
+ slugMap[entry.id] = slug;
125
+ }
93
126
  }
94
127
  }
95
128
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -112,6 +145,7 @@ export async function resolveUrlFields(data, fields, language) {
112
145
  if (allStrings)
113
146
  normalizedVal = { url: val };
114
147
  }
148
+ normalizedVal = normalizeUrlFieldData(normalizedVal);
115
149
  const parsedValWithRelation = urlFieldDataWithRelationSchema.safeParse(normalizedVal);
116
150
  const parsedVal = urlFieldDataSchema.safeParse(normalizedVal);
117
151
  const parsed = parsedValWithRelation.success
@@ -123,7 +157,7 @@ export async function resolveUrlFields(data, fields, language) {
123
157
  break;
124
158
  const extras = {};
125
159
  if (parsed.text) {
126
- extras.text = language ? parsed.text[language] : parsed.text;
160
+ extras.text = resolveFlatOrLocalized(parsed.text, language);
127
161
  }
128
162
  if (parsed.newTab !== undefined) {
129
163
  extras.newTab = parsed.newTab;
@@ -138,9 +172,7 @@ export async function resolveUrlFields(data, fields, language) {
138
172
  break;
139
173
  }
140
174
  }
141
- const resolvedUrl = language
142
- ? parsed.url[language]
143
- : parsed.url;
175
+ const resolvedUrl = resolveFlatOrLocalized(parsed.url, language);
144
176
  applyExternalAutoDetect(resolvedUrl, extras);
145
177
  result[field.slug] = { url: resolvedUrl, ...extras };
146
178
  break;
@@ -197,8 +229,9 @@ export async function resolveUrlFields(data, fields, language) {
197
229
  if (field.of === 'url' && Array.isArray(val)) {
198
230
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
199
231
  result[field.slug] = val.map((item) => {
200
- const parsedValWithRelation = urlFieldDataWithRelationSchema.safeParse(item);
201
- const parsedVal = urlFieldDataSchema.safeParse(item);
232
+ const normalized = normalizeUrlFieldData(item);
233
+ const parsedValWithRelation = urlFieldDataWithRelationSchema.safeParse(normalized);
234
+ const parsedVal = urlFieldDataSchema.safeParse(normalized);
202
235
  const parsed = parsedValWithRelation.success
203
236
  ? parsedValWithRelation.data
204
237
  : parsedVal.success
@@ -208,7 +241,7 @@ export async function resolveUrlFields(data, fields, language) {
208
241
  return item;
209
242
  const extras = {};
210
243
  if (parsed.text) {
211
- extras.text = language ? parsed.text[language] : parsed.text;
244
+ extras.text = resolveFlatOrLocalized(parsed.text, language);
212
245
  }
213
246
  if (parsed.newTab !== undefined) {
214
247
  extras.newTab = parsed.newTab;
@@ -222,9 +255,7 @@ export async function resolveUrlFields(data, fields, language) {
222
255
  return { url: slug, ...extras };
223
256
  }
224
257
  }
225
- const resolvedUrl = language
226
- ? parsed.url[language]
227
- : parsed.url;
258
+ const resolvedUrl = resolveFlatOrLocalized(parsed.url, language);
228
259
  applyExternalAutoDetect(resolvedUrl, extras);
229
260
  return { url: resolvedUrl, ...extras };
230
261
  });
@@ -200,6 +200,23 @@ export declare const entriesTable: import("drizzle-orm/pg-core/table", { with: {
200
200
  identity: undefined;
201
201
  generated: undefined;
202
202
  }, {}, {}>;
203
+ sortOrder: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
204
+ name: "sort_order";
205
+ tableName: "entry";
206
+ dataType: "number";
207
+ columnType: "PgInteger";
208
+ data: number;
209
+ driverParam: string | number;
210
+ notNull: false;
211
+ hasDefault: false;
212
+ isPrimaryKey: false;
213
+ isAutoincrement: false;
214
+ hasRuntimeDefault: false;
215
+ enumValues: undefined;
216
+ baseColumn: never;
217
+ identity: undefined;
218
+ generated: undefined;
219
+ }, {}, {}>;
203
220
  };
204
221
  dialect: "pg";
205
222
  }>;
@@ -1,4 +1,4 @@
1
- import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
1
+ import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
2
  import { entryVersionsTable } from './entryVersion.js';
3
3
  export const entriesTable = pgTable('entry', {
4
4
  id: uuid('id').primaryKey().defaultRandom(),
@@ -12,5 +12,7 @@ export const entriesTable = pgTable('entry', {
12
12
  // Publish state
13
13
  publishedAt: timestamp('published_at'),
14
14
  publishedVersionId: uuid('published_version_id').references(() => entryVersionsTable.id, { onDelete: 'set null' }),
15
- publishedBy: text('published_by')
15
+ publishedBy: text('published_by'),
16
+ // Manual ordering
17
+ sortOrder: integer('sort_order')
16
18
  });
@@ -1172,13 +1172,13 @@ export declare const auth: import("better-auth", { with: { "resolution-mode": "r
1172
1172
  <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
1173
1173
  body: ({
1174
1174
  permission: {
1175
- readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1175
+ readonly user?: ("list" | "create" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1176
1176
  readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1177
1177
  };
1178
1178
  permissions?: never;
1179
1179
  } | {
1180
1180
  permissions: {
1181
- readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1181
+ readonly user?: ("list" | "create" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1182
1182
  readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1183
1183
  };
1184
1184
  permission?: never;
@@ -1275,13 +1275,13 @@ export declare const auth: import("better-auth", { with: { "resolution-mode": "r
1275
1275
  $Infer: {
1276
1276
  body: ({
1277
1277
  permission: {
1278
- readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1278
+ readonly user?: ("list" | "create" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1279
1279
  readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1280
1280
  };
1281
1281
  permissions?: never;
1282
1282
  } | {
1283
1283
  permissions: {
1284
- readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1284
+ readonly user?: ("list" | "create" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1285
1285
  readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1286
1286
  };
1287
1287
  permission?: never;
@@ -38,6 +38,15 @@
38
38
  if (node.type === 'text') return node.text ?? '';
39
39
  return node.content?.map(getTextContent).join('') ?? '';
40
40
  }
41
+
42
+ function isEmptyParagraph(node: SCNode): boolean {
43
+ if (!node.content || node.content.length === 0) return true;
44
+ return node.content.every(
45
+ (child) =>
46
+ (child.type === 'text' && !child.text?.trim()) ||
47
+ child.type === 'hardBreak'
48
+ );
49
+ }
41
50
  </script>
42
51
 
43
52
  {#snippet renderMark(text: string, marks: SCMark[], index: number)}
@@ -83,10 +92,12 @@
83
92
  {node.text}
84
93
  {/if}
85
94
  {:else if node.type === 'paragraph'}
86
- {#if paragraphSnippet}
87
- {@render paragraphSnippet(renderChildren)}
88
- {:else}
89
- <p>{@render renderChildren()}</p>
95
+ {#if !isEmptyParagraph(node)}
96
+ {#if paragraphSnippet}
97
+ {@render paragraphSnippet(renderChildren)}
98
+ {:else}
99
+ <p>{@render renderChildren()}</p>
100
+ {/if}
90
101
  {/if}
91
102
  {:else if node.type === 'heading'}
92
103
  {@const level = (node.attrs?.level as number) ?? 2}
@@ -180,7 +191,9 @@
180
191
  {:else if node.type === 'inlineBlock'}
181
192
  {#if inlineBlockSnippet}
182
193
  {@const attrs = node.attrs as unknown as SCInlineBlockAttrs}
183
- {@render inlineBlockSnippet(attrs.blockType, attrs.blockData, attrs.blockId)}
194
+ <div data-inline-block={attrs.blockType}>
195
+ {@render inlineBlockSnippet(attrs.blockType, attrs.blockData, attrs.blockId)}
196
+ </div>
184
197
  {/if}
185
198
  {:else}
186
199
  {@render renderChildren()}
@@ -11,6 +11,7 @@ const adminGuard = async ({ event, resolve }) => {
11
11
  !event.url.pathname.startsWith('/admin/accept-invite') &&
12
12
  !event.url.pathname.startsWith('/admin/api/accept-invite') &&
13
13
  !event.url.pathname.startsWith('/admin/reset-password') &&
14
+ !event.url.pathname.startsWith('/admin/api/rest/') &&
14
15
  (!user || !session)) {
15
16
  setFlash({
16
17
  message: 'You must be logged in to access the admin panel.',
@@ -27,6 +27,11 @@ export interface AuthAdapter {
27
27
  } | null>;
28
28
  };
29
29
  }
30
+ export interface ApiKeyConfig {
31
+ key: string;
32
+ name?: string;
33
+ role?: 'admin' | 'editor';
34
+ }
30
35
  export interface CMSConfig {
31
36
  languages: Language[];
32
37
  collections?: CollectionConfig[];
@@ -39,6 +44,7 @@ export interface CMSConfig {
39
44
  plugins?: PluginConfig[];
40
45
  ai?: AIAdapter;
41
46
  media?: MediaConfig;
47
+ apiKeys?: ApiKeyConfig[];
42
48
  }
43
49
  export interface ICMS {
44
50
  collections: Record<string, CollectionConfigWithType>;
@@ -52,4 +58,5 @@ export interface ICMS {
52
58
  plugins: PluginConfig[];
53
59
  aiAdapter: AIAdapter | null;
54
60
  mediaConfig: MediaConfig;
61
+ apiKeys: ApiKeyConfig[];
55
62
  }
@@ -6,6 +6,8 @@ export interface CollectionConfig extends ConfigBase {
6
6
  singular?: Localized;
7
7
  plural?: Localized;
8
8
  };
9
+ orderable?: boolean;
10
+ listColumns?: string[];
9
11
  }
10
12
  export interface CollectionConfigWithType extends CollectionConfig {
11
13
  type: 'collection';
@@ -1,8 +1,8 @@
1
1
  import type { Field } from './fields.js';
2
- import type { Icon } from '@tabler/icons-svelte';
2
+ import type { IconName } from '../admin/components/icons/icon-map.js';
3
3
  import type { Layout } from './layout.js';
4
4
  export interface ConfigBase {
5
- sidebarIcon?: Icon;
5
+ sidebarIcon?: IconName;
6
6
  slug: string;
7
7
  previewUrl?: string;
8
8
  fields: Field[];
@@ -19,6 +19,7 @@ export interface DbEntry {
19
19
  publishedAt: Date | null;
20
20
  publishedVersionId: string | null;
21
21
  publishedBy: string | null;
22
+ sortOrder: number | null;
22
23
  }
23
24
  export interface DbEntryInsert {
24
25
  slug: string;
@@ -30,6 +31,7 @@ export interface DbEntryInsert {
30
31
  publishedAt?: Date | null;
31
32
  publishedVersionId?: string | null;
32
33
  publishedBy?: string | null;
34
+ sortOrder?: number | null;
33
35
  }
34
36
  export interface RawEntry extends DbEntry {
35
37
  collection: CollectionConfigWithType | SingleConfigWithType;
@@ -68,7 +70,7 @@ export interface PaginationOptions {
68
70
  limit?: number;
69
71
  offset?: number;
70
72
  orderBy?: {
71
- column: 'createdAt' | 'updatedAt';
73
+ column: 'createdAt' | 'updatedAt' | 'sortOrder';
72
74
  direction: 'asc' | 'desc';
73
75
  };
74
76
  }
@@ -103,6 +105,10 @@ export interface GetEntriesOptions {
103
105
  dataLike?: Record<string, unknown>;
104
106
  language?: string;
105
107
  status?: EntryStatus;
108
+ orderBy?: {
109
+ column: 'createdAt' | 'updatedAt' | 'sortOrder';
110
+ direction: 'asc' | 'desc';
111
+ };
106
112
  }
107
113
  export interface GetEntryOptions {
108
114
  id?: string;
@@ -1,7 +1,9 @@
1
1
  import type { FormField } from './formFields.js';
2
+ import type { IconName } from '../admin/components/icons/icon-map.js';
2
3
  import type { Localized } from './languages.js';
3
4
  export interface FormConfig {
4
5
  slug: string;
6
+ sidebarIcon?: IconName;
5
7
  fields: FormField[];
6
8
  label: Localized;
7
9
  notificationEmailAddresses?: string[];
@@ -4,10 +4,11 @@ export { type MediaFile, type MediaFileType, type ImageStyle, type MediaTag } fr
4
4
  export { type ConsentLogData } from './consent.js';
5
5
  export { type Entry, type EntryType, type EntryStatus, type EntryVersionStatus } from './entries.js';
6
6
  export { type ConfigBase } from './config.js';
7
+ export { type IconName } from '../admin/components/icons/icon-map.js';
7
8
  export { type CollectionConfig } from './collections.js';
8
9
  export { type SingleConfig } from './singles.js';
9
10
  export { type FormConfig, type FormSubmission } from './forms.js';
10
11
  export { type FormField, type FormFieldType, type FormBaseField, type FormTextField, type FormEmailField, type FormTextareaField, type FormCheckboxField, type FormSelectField } from './formFields.js';
11
- export { type CMSConfig } from './cms.js';
12
+ export { type CMSConfig, type ApiKeyConfig } from './cms.js';
12
13
  export { type Language, type Localized } from './languages.js';
13
14
  export { type Layout, type LayoutNode, type LayoutPreset, type LayoutNodeType, type ColumnRatio, type SectionNode, type ColumnsNode, type CardNode, type AccordionNode, type StackNode } from './layout.js';
@@ -4,6 +4,7 @@ export {} from './media.js';
4
4
  export {} from './consent.js';
5
5
  export {} from './entries.js';
6
6
  export {} from './config.js';
7
+ export {} from '../admin/components/icons/icon-map.js';
7
8
  export {} from './collections.js';
8
9
  export {} from './singles.js';
9
10
  export {} from './forms.js';
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,15 @@
1
+ export const update = {
2
+ version: '0.5.4',
3
+ date: '2026-03-04',
4
+ description: 'Collection ordering (DnD), custom list columns',
5
+ features: [
6
+ 'Orderable collections: DnD reordering of entries in admin list with keyboard alternative (WCAG 2.1)',
7
+ 'Custom list columns: display arbitrary entry fields as columns in collection list',
8
+ 'sortOrder column on entries table for persistent manual ordering',
9
+ 'reorderEntriesCommand remote for bulk sort order updates'
10
+ ],
11
+ fixes: [],
12
+ breakingChanges: [],
13
+ sql: 'ALTER TABLE entry ADD COLUMN sort_order INTEGER;',
14
+ notes: 'Add `orderable: true` to collections that need manual ordering. Add `listColumns: ["fieldSlug"]` to show fields in the entry list.'
15
+ };
@@ -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.5.5',
3
+ date: '2026-03-04',
4
+ description: 'REST API with API key auth, orderable collection fixes',
5
+ features: [
6
+ 'REST API with API key authentication — CRUD for collections, singletons, entries, schema introspection, language listing',
7
+ 'API key configuration in CMS config (`apiKeys` array)',
8
+ 'getEntries supports `orderBy` parameter (column + direction)',
9
+ 'Admin scaffold generates REST API catch-all route'
10
+ ],
11
+ fixes: [
12
+ 'Orderable collections: DnD rows use proper `<tr>` element instead of `<div>` wrapper',
13
+ 'Reorder buttons always visible (not sr-only) for better discoverability',
14
+ 'Selection enabled for orderable collections',
15
+ 'Relation field respects orderable collection sort order',
16
+ 'Nav active state: exact match prevents false highlights on similarly-named routes'
17
+ ],
18
+ breakingChanges: [],
19
+ notes: 'Add `apiKeys: [{ key: "your-secret", name: "My App" }]` to CMS config. REST API available at `/admin/api/rest/`.'
20
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,17 @@
1
+ export const update = {
2
+ version: '0.5.6',
3
+ date: '2026-03-05',
4
+ description: 'Relation field UX overhaul, collection list improvements, icon map refactor',
5
+ features: [
6
+ 'Relation field: dialog picker with search, reorder, multi-select UX',
7
+ '`getEntryLabels` — lightweight labels endpoint for relation field options',
8
+ 'Collection list: relation labels displayed in columns, delete restricted to archived entries',
9
+ 'Icon map: string-based sidebar icons replace component references'
10
+ ],
11
+ fixes: [
12
+ 'TipTap: auto-expand new inline blocks, fix editor overflow, toolbar background',
13
+ 'Runtime types: FormEntryMap rename, add remote.ts typings'
14
+ ],
15
+ breakingChanges: [],
16
+ notes: 'Removed `mode-watcher` dependency — light theme is now hardcoded.'
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,14 @@
1
+ export const update = {
2
+ version: '0.5.7',
3
+ date: '2026-03-06',
4
+ description: 'SEO field with slug toggle, structured-content & URL field fixes',
5
+ features: [
6
+ 'SEO field: auto/manual slug toggle with Switch component'
7
+ ],
8
+ fixes: [
9
+ 'Structured content: skip empty paragraphs, wrap inline blocks in `<div>`',
10
+ '`resolveUrlFields`: normalize flat string url/text values, use raw DB calls for performance',
11
+ 'Admin UI: migrate `{#await}` blocks to reactive query pattern for consistent loading states'
12
+ ],
13
+ breakingChanges: []
14
+ };
@@ -15,7 +15,11 @@ import { update as update050 } from './0.5.0/index.js';
15
15
  import { update as update051 } from './0.5.1/index.js';
16
16
  import { update as update052 } from './0.5.2/index.js';
17
17
  import { update as update053 } from './0.5.3/index.js';
18
- export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053];
18
+ import { update as update054 } from './0.5.4/index.js';
19
+ import { update as update055 } from './0.5.5/index.js';
20
+ import { update as update056 } from './0.5.6/index.js';
21
+ import { update as update057 } from './0.5.7/index.js';
22
+ export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057];
19
23
  export const getUpdatesFrom = (fromVersion) => {
20
24
  const fromParts = fromVersion.split('.').map(Number);
21
25
  return updates.filter((update) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.5.3",
3
+ "version": "0.5.7",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -63,6 +63,11 @@
63
63
  "svelte": "./dist/admin/api/*.js",
64
64
  "node": "./dist/admin/api/*.js"
65
65
  },
66
+ "./admin/api/rest/handler": {
67
+ "types": "./dist/admin/api/rest/handler.d.ts",
68
+ "svelte": "./dist/admin/api/rest/handler.js",
69
+ "node": "./dist/admin/api/rest/handler.js"
70
+ },
66
71
  "./admin/client/account": {
67
72
  "types": "./dist/admin/client/account/index.d.ts",
68
73
  "svelte": "./dist/admin/client/account/index.js"
@@ -217,7 +222,6 @@
217
222
  "fluent-ffmpeg": "^2.1.3",
218
223
  "isomorphic-dompurify": "3.0.0-rc.2",
219
224
  "lowlight": "^3.3.0",
220
- "mode-watcher": "^1.0.8",
221
225
  "nodemailer": "^7.0.11",
222
226
  "openai": "^6.9.0",
223
227
  "paneforge": "^1.0.2",