includio-cms 0.5.0 → 0.5.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.
- package/CHANGELOG.md +31 -12
- package/ROADMAP.md +12 -0
- package/dist/admin/client/admin/dashboard-page.svelte +18 -64
- package/dist/admin/client/collection/bulk-actions-bar.svelte +18 -1
- package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -0
- package/dist/admin/client/collection/collection-entries.svelte +25 -1
- package/dist/admin/client/collection/row-actions.svelte +13 -4
- package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
- package/dist/admin/client/entry/entry-header.svelte +51 -4
- package/dist/admin/client/entry/entry-header.svelte.d.ts +3 -0
- package/dist/admin/client/entry/entry.svelte +106 -6
- package/dist/admin/client/entry/header/a11y-validator.d.ts +3 -2
- package/dist/admin/client/entry/header/a11y-validator.js +50 -9
- package/dist/admin/client/entry/header/publish-panel.svelte +164 -4
- package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -1
- package/dist/admin/components/dashboard/changelog-dialog.svelte +167 -0
- package/dist/admin/components/dashboard/changelog-dialog.svelte.d.ts +6 -0
- package/dist/admin/components/dashboard/orphaned-entries-notice.svelte +240 -0
- package/dist/admin/components/dashboard/orphaned-entries-notice.svelte.d.ts +13 -0
- package/dist/admin/components/fields/text-field-wrapper.svelte +134 -2
- package/dist/admin/components/layout/nav-footer.svelte +11 -4
- package/dist/admin/components/layout/nav-footer.svelte.d.ts +2 -17
- package/dist/admin/remote/entry.remote.d.ts +1 -0
- package/dist/admin/remote/entry.remote.js +5 -4
- package/dist/admin/state/content-language.svelte.d.ts +3 -0
- package/dist/admin/state/content-language.svelte.js +8 -0
- package/dist/admin/utils/translationStatus.d.ts +17 -0
- package/dist/admin/utils/translationStatus.js +134 -0
- package/dist/core/server/entries/operations/get.js +2 -1
- package/dist/db-postgres/index.js +10 -6
- package/dist/types/entries.d.ts +3 -0
- package/dist/updates/0.0.65/index.js +1 -1
- package/dist/updates/0.0.67/index.js +1 -1
- package/dist/updates/0.1.2/index.js +1 -1
- package/dist/updates/0.1.5/index.js +1 -1
- package/dist/updates/0.2.0/index.js +1 -1
- package/dist/updates/0.2.2/index.js +1 -1
- package/dist/updates/0.5.0/index.js +1 -1
- package/dist/updates/0.5.1/index.d.ts +2 -0
- package/dist/updates/0.5.1/index.js +17 -0
- package/dist/updates/0.5.2/index.d.ts +2 -0
- package/dist/updates/0.5.2/index.js +14 -0
- package/dist/updates/index.d.ts +2 -1
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
- package/dist/admin/components/dashboard/updates-banner.svelte +0 -170
- package/dist/admin/components/dashboard/updates-banner.svelte.d.ts +0 -3
- package/dist/updates/0.0.65/migration.sql +0 -55
- package/dist/updates/0.0.67/migration.sql +0 -9
|
@@ -52,7 +52,8 @@ export const createEntry = command(createEntrySchema, async (input) => {
|
|
|
52
52
|
});
|
|
53
53
|
export const getRawEntry = query(z.object({
|
|
54
54
|
id: z.string().uuid().optional(),
|
|
55
|
-
slug: z.string().optional()
|
|
55
|
+
slug: z.string().optional(),
|
|
56
|
+
includeArchived: z.boolean().optional()
|
|
56
57
|
}), async (input) => {
|
|
57
58
|
return getRawEntryOperation(input);
|
|
58
59
|
});
|
|
@@ -61,7 +62,7 @@ export const getEntryForEntryPage = query(z.string(), async (id) => {
|
|
|
61
62
|
const isUUID = parsedIdentifier.success;
|
|
62
63
|
if (isUUID) {
|
|
63
64
|
const uuid = parsedIdentifier.data;
|
|
64
|
-
return getRawEntryOrThrow({ id: uuid });
|
|
65
|
+
return getRawEntryOrThrow({ id: uuid, includeArchived: true });
|
|
65
66
|
}
|
|
66
67
|
else {
|
|
67
68
|
// We assume this is singleton
|
|
@@ -70,7 +71,7 @@ export const getEntryForEntryPage = query(z.string(), async (id) => {
|
|
|
70
71
|
if (!config) {
|
|
71
72
|
throw new Error(`Singleton with slug "${slug}" not found`);
|
|
72
73
|
}
|
|
73
|
-
const entry = await getRawEntry({ slug });
|
|
74
|
+
const entry = await getRawEntry({ slug, includeArchived: true });
|
|
74
75
|
if (entry) {
|
|
75
76
|
return entry;
|
|
76
77
|
}
|
|
@@ -80,7 +81,7 @@ export const getEntryForEntryPage = query(z.string(), async (id) => {
|
|
|
80
81
|
slug: config.slug,
|
|
81
82
|
type: 'singleton'
|
|
82
83
|
});
|
|
83
|
-
return await getRawEntryOrThrow({ slug });
|
|
84
|
+
return await getRawEntryOrThrow({ slug, includeArchived: true });
|
|
84
85
|
}
|
|
85
86
|
});
|
|
86
87
|
const updateEntryVersionCommandSchema = z.object({
|
|
@@ -2,6 +2,7 @@ export declare const getContentLanguage: () => ContentLanguage, setContentLangua
|
|
|
2
2
|
type _ContentLanguage = {
|
|
3
3
|
all: string[];
|
|
4
4
|
current: string;
|
|
5
|
+
referenceMode: boolean;
|
|
5
6
|
};
|
|
6
7
|
export declare class ContentLanguage {
|
|
7
8
|
#private;
|
|
@@ -9,5 +10,7 @@ export declare class ContentLanguage {
|
|
|
9
10
|
get all(): string[];
|
|
10
11
|
get current(): _ContentLanguage["current"];
|
|
11
12
|
set current(value: _ContentLanguage['current']);
|
|
13
|
+
get referenceMode(): boolean;
|
|
14
|
+
set referenceMode(value: boolean);
|
|
12
15
|
}
|
|
13
16
|
export {};
|
|
@@ -3,9 +3,11 @@ export const [getContentLanguage, setContentLanguage] = createContext();
|
|
|
3
3
|
export class ContentLanguage {
|
|
4
4
|
#all;
|
|
5
5
|
#current;
|
|
6
|
+
#referenceMode;
|
|
6
7
|
constructor(all, current) {
|
|
7
8
|
this.#all = $state(all);
|
|
8
9
|
this.#current = $state(current);
|
|
10
|
+
this.#referenceMode = $state(false);
|
|
9
11
|
}
|
|
10
12
|
get all() {
|
|
11
13
|
return this.#all;
|
|
@@ -16,4 +18,10 @@ export class ContentLanguage {
|
|
|
16
18
|
set current(value) {
|
|
17
19
|
this.#current = value;
|
|
18
20
|
}
|
|
21
|
+
get referenceMode() {
|
|
22
|
+
return this.#referenceMode;
|
|
23
|
+
}
|
|
24
|
+
set referenceMode(value) {
|
|
25
|
+
this.#referenceMode = value;
|
|
26
|
+
}
|
|
19
27
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Field } from '../../types/fields.js';
|
|
2
|
+
export type TranslationCompleteness = 'complete' | 'partial' | 'empty';
|
|
3
|
+
export interface LangStatus {
|
|
4
|
+
filled: number;
|
|
5
|
+
total: number;
|
|
6
|
+
percentage: number;
|
|
7
|
+
status: TranslationCompleteness;
|
|
8
|
+
missingFields: {
|
|
9
|
+
slug: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compute translation status for each language.
|
|
15
|
+
* Checks all localized fields (text, richtext, content) + seo/url sub-fields.
|
|
16
|
+
*/
|
|
17
|
+
export declare function computeTranslationStatus(data: Record<string, unknown>, fields: Field[], languages: string[]): Record<string, LangStatus>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { getAtPath } from './objectPath.js';
|
|
2
|
+
/** Extract a label string from Localized, preferring pl then en then slug. */
|
|
3
|
+
function extractLabel(label, fallback) {
|
|
4
|
+
if (!label)
|
|
5
|
+
return fallback;
|
|
6
|
+
if (typeof label === 'string')
|
|
7
|
+
return label;
|
|
8
|
+
return label.pl || label.en || fallback;
|
|
9
|
+
}
|
|
10
|
+
/** Check if a value counts as "filled" for a given field type. */
|
|
11
|
+
function isFieldFilled(value, fieldType) {
|
|
12
|
+
if (value == null)
|
|
13
|
+
return false;
|
|
14
|
+
switch (fieldType) {
|
|
15
|
+
case 'text':
|
|
16
|
+
case 'richtext':
|
|
17
|
+
return typeof value === 'string' && value.length > 0;
|
|
18
|
+
case 'content': {
|
|
19
|
+
if (typeof value !== 'object')
|
|
20
|
+
return false;
|
|
21
|
+
const doc = value;
|
|
22
|
+
return doc.type === 'doc' && Array.isArray(doc.content) && doc.content.length > 0;
|
|
23
|
+
}
|
|
24
|
+
case 'seo': {
|
|
25
|
+
if (typeof value !== 'object')
|
|
26
|
+
return false;
|
|
27
|
+
const seo = value;
|
|
28
|
+
// At least title or description filled
|
|
29
|
+
return ((typeof seo.title === 'string' && seo.title.length > 0) ||
|
|
30
|
+
(typeof seo.description === 'string' && seo.description.length > 0));
|
|
31
|
+
}
|
|
32
|
+
case 'url': {
|
|
33
|
+
if (typeof value !== 'object')
|
|
34
|
+
return false;
|
|
35
|
+
const url = value;
|
|
36
|
+
return typeof url.url === 'string' && url.url.length > 0;
|
|
37
|
+
}
|
|
38
|
+
default:
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Collect all localized fields from a field list, including nested seo/url sub-fields. */
|
|
43
|
+
function collectLocalizedFields(fields) {
|
|
44
|
+
const result = [];
|
|
45
|
+
for (const field of fields) {
|
|
46
|
+
if (field.localized === false)
|
|
47
|
+
continue;
|
|
48
|
+
const label = extractLabel(field.label, field.slug);
|
|
49
|
+
if (field.type === 'text' || field.type === 'richtext' || field.type === 'content') {
|
|
50
|
+
result.push({ slug: field.slug, label, type: field.type, required: !!field.required });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// SEO and URL fields are always localized by nature (sub-fields are lang-keyed)
|
|
54
|
+
for (const field of fields) {
|
|
55
|
+
if (field.type === 'seo') {
|
|
56
|
+
const label = extractLabel(field.label, field.slug);
|
|
57
|
+
result.push({ slug: field.slug, label, type: 'seo', required: !!field.required });
|
|
58
|
+
}
|
|
59
|
+
if (field.type === 'url') {
|
|
60
|
+
const label = extractLabel(field.label, field.slug);
|
|
61
|
+
result.push({ slug: field.slug, label, type: 'url', required: !!field.required });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** Resolve the value of a field for a given language. */
|
|
67
|
+
function resolveFieldValue(data, field, lang) {
|
|
68
|
+
if (field.type === 'seo') {
|
|
69
|
+
const seoData = getAtPath(data, field.slug);
|
|
70
|
+
if (seoData && typeof seoData === 'object') {
|
|
71
|
+
const seo = seoData;
|
|
72
|
+
const title = extractLangValue(seo.title, lang);
|
|
73
|
+
const desc = extractLangValue(seo.description, lang);
|
|
74
|
+
return { title, description: desc };
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
else if (field.type === 'url') {
|
|
79
|
+
const urlData = getAtPath(data, field.slug);
|
|
80
|
+
if (urlData && typeof urlData === 'object') {
|
|
81
|
+
const url = urlData;
|
|
82
|
+
const urlVal = url.url && typeof url.url === 'object'
|
|
83
|
+
? url.url[lang]
|
|
84
|
+
: undefined;
|
|
85
|
+
return { url: urlVal || '' };
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const fieldData = getAtPath(data, field.slug);
|
|
91
|
+
if (fieldData && typeof fieldData === 'object' && !Array.isArray(fieldData)) {
|
|
92
|
+
return fieldData[lang];
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Compute translation status for each language.
|
|
99
|
+
* Checks all localized fields (text, richtext, content) + seo/url sub-fields.
|
|
100
|
+
*/
|
|
101
|
+
export function computeTranslationStatus(data, fields, languages) {
|
|
102
|
+
const allLocalizedFields = collectLocalizedFields(fields);
|
|
103
|
+
// Pre-filter: keep field if required OR has content in at least one language
|
|
104
|
+
const localizedFields = allLocalizedFields.filter((field) => field.required ||
|
|
105
|
+
languages.some((lang) => isFieldFilled(resolveFieldValue(data, field, lang), field.type)));
|
|
106
|
+
const result = {};
|
|
107
|
+
for (const lang of languages) {
|
|
108
|
+
let filled = 0;
|
|
109
|
+
const total = localizedFields.length;
|
|
110
|
+
const missingFields = [];
|
|
111
|
+
for (const field of localizedFields) {
|
|
112
|
+
const value = resolveFieldValue(data, field, lang);
|
|
113
|
+
if (isFieldFilled(value, field.type)) {
|
|
114
|
+
filled++;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
missingFields.push({ slug: field.slug, label: field.label });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const percentage = total === 0 ? 100 : Math.round((filled / total) * 100);
|
|
121
|
+
const status = percentage === 100 ? 'complete' : percentage === 0 ? 'empty' : 'partial';
|
|
122
|
+
result[lang] = { filled, total, percentage, status, missingFields };
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
/** Extract lang value from a possibly lang-keyed value. */
|
|
127
|
+
function extractLangValue(value, lang) {
|
|
128
|
+
if (typeof value === 'string')
|
|
129
|
+
return value;
|
|
130
|
+
if (value && typeof value === 'object') {
|
|
131
|
+
return (value[lang]) || '';
|
|
132
|
+
}
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
@@ -94,7 +94,8 @@ export const getRawEntries = async (options) => {
|
|
|
94
94
|
export const getRawEntry = async (options) => {
|
|
95
95
|
const [entry] = await getRawEntries({
|
|
96
96
|
...options,
|
|
97
|
-
ids: options.id ? [options.id] : undefined
|
|
97
|
+
ids: options.id ? [options.id] : undefined,
|
|
98
|
+
includeArchived: options.includeArchived
|
|
98
99
|
});
|
|
99
100
|
return entry || null;
|
|
100
101
|
};
|
|
@@ -101,9 +101,11 @@ export function pg(config) {
|
|
|
101
101
|
options.type ? eq(schema.entriesTable.type, options.type) : undefined,
|
|
102
102
|
options.slug ? eq(schema.entriesTable.slug, options.slug) : undefined,
|
|
103
103
|
options.ids ? inArray(schema.entriesTable.id, options.ids) : undefined,
|
|
104
|
-
options.
|
|
105
|
-
?
|
|
106
|
-
:
|
|
104
|
+
options.includeArchived
|
|
105
|
+
? undefined
|
|
106
|
+
: options.onlyArchived
|
|
107
|
+
? sql `${schema.entriesTable.archivedAt} IS NOT NULL`
|
|
108
|
+
: isNull(schema.entriesTable.archivedAt)
|
|
107
109
|
].filter(Boolean);
|
|
108
110
|
const allConditions = [...baseConditions];
|
|
109
111
|
if (allConditions.length > 0) {
|
|
@@ -127,9 +129,11 @@ export function pg(config) {
|
|
|
127
129
|
options.type ? eq(schema.entriesTable.type, options.type) : undefined,
|
|
128
130
|
options.slug ? eq(schema.entriesTable.slug, options.slug) : undefined,
|
|
129
131
|
options.ids ? inArray(schema.entriesTable.id, options.ids) : undefined,
|
|
130
|
-
options.
|
|
131
|
-
?
|
|
132
|
-
:
|
|
132
|
+
options.includeArchived
|
|
133
|
+
? undefined
|
|
134
|
+
: options.onlyArchived
|
|
135
|
+
? sql `${schema.entriesTable.archivedAt} IS NOT NULL`
|
|
136
|
+
: isNull(schema.entriesTable.archivedAt)
|
|
133
137
|
].filter(Boolean);
|
|
134
138
|
const [result] = await db
|
|
135
139
|
.select({ count: count() })
|
package/dist/types/entries.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export interface GetDbEntriesOptions extends PaginationOptions {
|
|
|
77
77
|
slug?: string;
|
|
78
78
|
type?: EntryType;
|
|
79
79
|
onlyArchived?: boolean;
|
|
80
|
+
includeArchived?: boolean;
|
|
80
81
|
}
|
|
81
82
|
export interface GetDbEntryOptions {
|
|
82
83
|
id?: string;
|
|
@@ -88,10 +89,12 @@ export interface GetRawEntriesOptions extends PaginationOptions {
|
|
|
88
89
|
ids?: string[];
|
|
89
90
|
slug?: string;
|
|
90
91
|
onlyArchived?: boolean;
|
|
92
|
+
includeArchived?: boolean;
|
|
91
93
|
}
|
|
92
94
|
export interface getRawEntryOptions {
|
|
93
95
|
id?: string;
|
|
94
96
|
slug?: string;
|
|
97
|
+
includeArchived?: boolean;
|
|
95
98
|
}
|
|
96
99
|
export interface GetEntriesOptions {
|
|
97
100
|
ids?: string[];
|
|
@@ -10,7 +10,7 @@ export const update = {
|
|
|
10
10
|
],
|
|
11
11
|
fixes: [],
|
|
12
12
|
breakingChanges: ['Media folders replaced with tags — existing folder assignments will be lost'],
|
|
13
|
-
|
|
13
|
+
sql: `-- Move publish logic from entry_version to entry
|
|
14
14
|
|
|
15
15
|
ALTER TABLE entry ADD COLUMN published_at TIMESTAMP;
|
|
16
16
|
ALTER TABLE entry ADD COLUMN published_version_id UUID
|
|
@@ -33,7 +33,7 @@ export const update = {
|
|
|
33
33
|
breakingChanges: [
|
|
34
34
|
'getImageStyles() now returns { styles, blurDataUrl } instead of plain styles record'
|
|
35
35
|
],
|
|
36
|
-
|
|
36
|
+
sql: `ALTER TABLE image_styles ADD COLUMN IF NOT EXISTS quality INTEGER;
|
|
37
37
|
ALTER TABLE media_file ADD COLUMN IF NOT EXISTS blur_data_url TEXT;
|
|
38
38
|
ALTER TABLE media_file ADD COLUMN IF NOT EXISTS focal_x REAL;
|
|
39
39
|
ALTER TABLE media_file ADD COLUMN IF NOT EXISTS focal_y REAL;`
|
|
@@ -14,5 +14,5 @@ export const update = {
|
|
|
14
14
|
'Pruning no longer deletes important versions to make room for empty ones'
|
|
15
15
|
],
|
|
16
16
|
breakingChanges: [],
|
|
17
|
-
|
|
17
|
+
notes: 'Removes duplicate entry versions where data is identical to the previous version. Preserves published, scheduled, and latest draft versions.'
|
|
18
18
|
};
|
|
@@ -7,5 +7,5 @@ export const update = {
|
|
|
7
7
|
breakingChanges: [
|
|
8
8
|
'type:"array" with objects renamed to type:"blocks". Update collection/single configs.'
|
|
9
9
|
],
|
|
10
|
-
|
|
10
|
+
notes: 'Config-only: rename type:"array" to type:"blocks" in field definitions. Stored data unchanged.'
|
|
11
11
|
};
|
|
@@ -9,5 +9,5 @@ export const update = {
|
|
|
9
9
|
],
|
|
10
10
|
fixes: [],
|
|
11
11
|
breakingChanges: [],
|
|
12
|
-
|
|
12
|
+
notes: 'No migration needed. Add type:"content" fields to collection/single configs. Existing richtext fields unchanged.'
|
|
13
13
|
};
|
|
@@ -10,5 +10,5 @@ export const update = {
|
|
|
10
10
|
],
|
|
11
11
|
fixes: [],
|
|
12
12
|
breakingChanges: [],
|
|
13
|
-
|
|
13
|
+
notes: 'No migration needed. Import `StructuredContent` from `includio/sveltekit` to render content fields.'
|
|
14
14
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.5.1',
|
|
3
|
+
date: '2026-02-24',
|
|
4
|
+
description: 'Restore archived entries, dashboard redesign, translation fixes',
|
|
5
|
+
features: [
|
|
6
|
+
'Restore archived entries from collection list (single + bulk)',
|
|
7
|
+
'Archived entry page: read-only with restore banner',
|
|
8
|
+
'Dashboard: changelog modal, orphaned entries redesign, grid layout'
|
|
9
|
+
],
|
|
10
|
+
fixes: [
|
|
11
|
+
'Opening archived entry no longer returns 500 (getRawEntry now supports includeArchived)',
|
|
12
|
+
'Translation flow: reactive status, switcher UX, dynamic ref, copyFrom crash, panel scroll',
|
|
13
|
+
'False "Niezapisane zmiany" alert on entry load',
|
|
14
|
+
'Hide translation dots for non-required empty fields'
|
|
15
|
+
],
|
|
16
|
+
breakingChanges: []
|
|
17
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.5.2',
|
|
3
|
+
date: '2026-02-25',
|
|
4
|
+
description: 'Update system: split migration into sql + notes',
|
|
5
|
+
features: [],
|
|
6
|
+
fixes: [
|
|
7
|
+
'CmsUpdate.migration split into sql (executable SQL) and notes (manual steps) — fixes CLI crash on text descriptions',
|
|
8
|
+
'Changelog dialog: SQL accordion hidden when no SQL, notes shown as inline text',
|
|
9
|
+
'Changelog script: SQL in ```sql blocks, notes as plain text'
|
|
10
|
+
],
|
|
11
|
+
breakingChanges: [
|
|
12
|
+
'CmsUpdate interface: migration field replaced by sql + notes fields'
|
|
13
|
+
]
|
|
14
|
+
};
|
package/dist/updates/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export interface CmsUpdate {
|
|
|
5
5
|
features: string[];
|
|
6
6
|
fixes: string[];
|
|
7
7
|
breakingChanges: string[];
|
|
8
|
-
|
|
8
|
+
sql?: string;
|
|
9
|
+
notes?: string;
|
|
9
10
|
}
|
|
10
11
|
export declare const updates: CmsUpdate[];
|
|
11
12
|
export declare const getUpdatesFrom: (fromVersion: string) => CmsUpdate[];
|
package/dist/updates/index.js
CHANGED
|
@@ -12,7 +12,9 @@ import { update as update015 } from './0.1.5/index.js';
|
|
|
12
12
|
import { update as update020 } from './0.2.0/index.js';
|
|
13
13
|
import { update as update022 } from './0.2.2/index.js';
|
|
14
14
|
import { update as update050 } from './0.5.0/index.js';
|
|
15
|
-
|
|
15
|
+
import { update as update051 } from './0.5.1/index.js';
|
|
16
|
+
import { update as update052 } from './0.5.2/index.js';
|
|
17
|
+
export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052];
|
|
16
18
|
export const getUpdatesFrom = (fromVersion) => {
|
|
17
19
|
const fromParts = fromVersion.split('.').map(Number);
|
|
18
20
|
return updates.filter((update) => {
|
package/package.json
CHANGED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { updates } from '../../../updates/index.js';
|
|
3
|
-
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
4
|
-
import * as Alert from '../../../components/ui/alert/index.js';
|
|
5
|
-
import { Button } from '../../../components/ui/button/index.js';
|
|
6
|
-
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
7
|
-
import AlertTriangleIcon from '@tabler/icons-svelte/icons/alert-triangle';
|
|
8
|
-
import SparklesIcon from '@tabler/icons-svelte/icons/sparkles';
|
|
9
|
-
import XIcon from '@tabler/icons-svelte/icons/x';
|
|
10
|
-
import CopyIcon from '@tabler/icons-svelte/icons/copy';
|
|
11
|
-
import CheckIcon from '@tabler/icons-svelte/icons/check';
|
|
12
|
-
|
|
13
|
-
const STORAGE_KEY = 'includio-dismissed-version';
|
|
14
|
-
const latest = updates[0];
|
|
15
|
-
const interfaceLanguage = useInterfaceLanguage();
|
|
16
|
-
const hasBreaking = latest.breakingChanges.length > 0;
|
|
17
|
-
|
|
18
|
-
let dismissed = $state(false);
|
|
19
|
-
let copied = $state(false);
|
|
20
|
-
|
|
21
|
-
$effect(() => {
|
|
22
|
-
if (typeof localStorage !== 'undefined') {
|
|
23
|
-
dismissed = localStorage.getItem(STORAGE_KEY) === latest.version;
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
function dismiss() {
|
|
28
|
-
localStorage.setItem(STORAGE_KEY, latest.version);
|
|
29
|
-
dismissed = true;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function copyMigration() {
|
|
33
|
-
if (!latest.migration) return;
|
|
34
|
-
await navigator.clipboard.writeText(latest.migration);
|
|
35
|
-
copied = true;
|
|
36
|
-
setTimeout(() => (copied = false), 2000);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const lang: Record<
|
|
40
|
-
InterfaceLanguage,
|
|
41
|
-
{
|
|
42
|
-
newVersion: string;
|
|
43
|
-
whatsNew: string;
|
|
44
|
-
features: string;
|
|
45
|
-
fixes: string;
|
|
46
|
-
breaking: string;
|
|
47
|
-
migration: string;
|
|
48
|
-
copy: string;
|
|
49
|
-
copied: string;
|
|
50
|
-
dismiss: string;
|
|
51
|
-
}
|
|
52
|
-
> = {
|
|
53
|
-
pl: {
|
|
54
|
-
newVersion: 'Nowa wersja',
|
|
55
|
-
whatsNew: 'Co nowego',
|
|
56
|
-
features: 'Nowe funkcje',
|
|
57
|
-
fixes: 'Poprawki',
|
|
58
|
-
breaking: 'Zmiany wymagające uwagi',
|
|
59
|
-
migration: 'Migracja SQL',
|
|
60
|
-
copy: 'Kopiuj',
|
|
61
|
-
copied: 'Skopiowano',
|
|
62
|
-
dismiss: 'Schowaj'
|
|
63
|
-
},
|
|
64
|
-
en: {
|
|
65
|
-
newVersion: 'New version',
|
|
66
|
-
whatsNew: "What's new",
|
|
67
|
-
features: 'Features',
|
|
68
|
-
fixes: 'Fixes',
|
|
69
|
-
breaking: 'Breaking changes',
|
|
70
|
-
migration: 'SQL Migration',
|
|
71
|
-
copy: 'Copy',
|
|
72
|
-
copied: 'Copied',
|
|
73
|
-
dismiss: 'Dismiss'
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
</script>
|
|
77
|
-
|
|
78
|
-
{#if !dismissed}
|
|
79
|
-
{@const t = lang[interfaceLanguage.current]}
|
|
80
|
-
{#if hasBreaking}
|
|
81
|
-
<Alert.Root
|
|
82
|
-
variant="destructive"
|
|
83
|
-
class="rounded-2xl border-orange-300/50 bg-orange-50 dark:border-orange-500/30 dark:bg-orange-950/60"
|
|
84
|
-
>
|
|
85
|
-
<AlertTriangleIcon class="text-orange-600 dark:text-orange-400" />
|
|
86
|
-
<Alert.Title class="text-orange-800 dark:text-orange-200">
|
|
87
|
-
{t.newVersion}: v{latest.version}
|
|
88
|
-
</Alert.Title>
|
|
89
|
-
<Alert.Description class="text-orange-700 dark:text-orange-300">
|
|
90
|
-
<p class="mb-2 text-sm">{latest.description}</p>
|
|
91
|
-
|
|
92
|
-
{#if latest.features.length > 0}
|
|
93
|
-
<p class="mt-3 mb-1 text-sm font-semibold">{t.features}</p>
|
|
94
|
-
<ul class="list-inside list-disc text-sm">
|
|
95
|
-
{#each latest.features as feature}
|
|
96
|
-
<li>{feature}</li>
|
|
97
|
-
{/each}
|
|
98
|
-
</ul>
|
|
99
|
-
{/if}
|
|
100
|
-
|
|
101
|
-
<p class="mt-3 mb-1 text-sm font-semibold text-orange-800 dark:text-orange-200">{t.breaking}</p>
|
|
102
|
-
<ul class="list-inside list-disc text-sm">
|
|
103
|
-
{#each latest.breakingChanges as change}
|
|
104
|
-
<li>{change}</li>
|
|
105
|
-
{/each}
|
|
106
|
-
</ul>
|
|
107
|
-
|
|
108
|
-
{#if latest.migration}
|
|
109
|
-
<details class="mt-3">
|
|
110
|
-
<summary class="cursor-pointer text-sm font-semibold">{t.migration}</summary>
|
|
111
|
-
<pre class="mt-2 overflow-x-auto rounded-lg bg-orange-100/80 p-3 text-xs dark:bg-orange-900/40">{latest.migration}</pre>
|
|
112
|
-
<Button variant="outline" size="sm" class="mt-2" onclick={copyMigration}>
|
|
113
|
-
{#if copied}
|
|
114
|
-
<CheckIcon class="mr-1 h-3.5 w-3.5" />
|
|
115
|
-
{t.copied}
|
|
116
|
-
{:else}
|
|
117
|
-
<CopyIcon class="mr-1 h-3.5 w-3.5" />
|
|
118
|
-
{t.copy}
|
|
119
|
-
{/if}
|
|
120
|
-
</Button>
|
|
121
|
-
</details>
|
|
122
|
-
{/if}
|
|
123
|
-
|
|
124
|
-
<div class="mt-4">
|
|
125
|
-
<Button variant="outline" size="sm" onclick={dismiss}>
|
|
126
|
-
<XIcon class="mr-1 h-3.5 w-3.5" />
|
|
127
|
-
{t.dismiss}
|
|
128
|
-
</Button>
|
|
129
|
-
</div>
|
|
130
|
-
</Alert.Description>
|
|
131
|
-
</Alert.Root>
|
|
132
|
-
{:else}
|
|
133
|
-
<Alert.Root
|
|
134
|
-
class="rounded-2xl border-blue-300/50 bg-blue-50 dark:border-blue-500/30 dark:bg-blue-950/60"
|
|
135
|
-
>
|
|
136
|
-
<SparklesIcon class="text-blue-600 dark:text-blue-400" />
|
|
137
|
-
<Alert.Title class="text-blue-800 dark:text-blue-200">
|
|
138
|
-
{t.newVersion}: v{latest.version}
|
|
139
|
-
</Alert.Title>
|
|
140
|
-
<Alert.Description class="text-blue-700 dark:text-blue-300">
|
|
141
|
-
<p class="mb-2 text-sm">{latest.description}</p>
|
|
142
|
-
|
|
143
|
-
{#if latest.features.length > 0}
|
|
144
|
-
<p class="mt-3 mb-1 text-sm font-semibold">{t.features}</p>
|
|
145
|
-
<ul class="list-inside list-disc text-sm">
|
|
146
|
-
{#each latest.features as feature}
|
|
147
|
-
<li>{feature}</li>
|
|
148
|
-
{/each}
|
|
149
|
-
</ul>
|
|
150
|
-
{/if}
|
|
151
|
-
|
|
152
|
-
{#if latest.fixes.length > 0}
|
|
153
|
-
<p class="mt-3 mb-1 text-sm font-semibold">{t.fixes}</p>
|
|
154
|
-
<ul class="list-inside list-disc text-sm">
|
|
155
|
-
{#each latest.fixes as fix}
|
|
156
|
-
<li>{fix}</li>
|
|
157
|
-
{/each}
|
|
158
|
-
</ul>
|
|
159
|
-
{/if}
|
|
160
|
-
|
|
161
|
-
<div class="mt-4">
|
|
162
|
-
<Button variant="outline" size="sm" onclick={dismiss}>
|
|
163
|
-
<XIcon class="mr-1 h-3.5 w-3.5" />
|
|
164
|
-
{t.dismiss}
|
|
165
|
-
</Button>
|
|
166
|
-
</div>
|
|
167
|
-
</Alert.Description>
|
|
168
|
-
</Alert.Root>
|
|
169
|
-
{/if}
|
|
170
|
-
{/if}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
-- Move publish logic from entry_version to entry
|
|
2
|
-
|
|
3
|
-
ALTER TABLE entry ADD COLUMN published_at TIMESTAMP;
|
|
4
|
-
ALTER TABLE entry ADD COLUMN published_version_id UUID
|
|
5
|
-
REFERENCES entry_version(id) ON DELETE SET NULL;
|
|
6
|
-
ALTER TABLE entry ADD COLUMN published_by TEXT;
|
|
7
|
-
|
|
8
|
-
UPDATE entry e SET
|
|
9
|
-
published_version_id = latest.id,
|
|
10
|
-
published_at = first_pub.first_published_at,
|
|
11
|
-
published_by = latest.published_by
|
|
12
|
-
FROM (
|
|
13
|
-
SELECT DISTINCT ON (entry_id) id, entry_id, published_by
|
|
14
|
-
FROM entry_version
|
|
15
|
-
WHERE published_at IS NOT NULL AND published_at <= NOW()
|
|
16
|
-
ORDER BY entry_id, version_number DESC
|
|
17
|
-
) latest
|
|
18
|
-
JOIN (
|
|
19
|
-
SELECT entry_id, MIN(published_at) as first_published_at
|
|
20
|
-
FROM entry_version
|
|
21
|
-
WHERE published_at IS NOT NULL AND published_at <= NOW()
|
|
22
|
-
GROUP BY entry_id
|
|
23
|
-
) first_pub ON first_pub.entry_id = latest.entry_id
|
|
24
|
-
WHERE e.id = latest.entry_id;
|
|
25
|
-
|
|
26
|
-
UPDATE entry e SET
|
|
27
|
-
published_version_id = COALESCE(e.published_version_id, sub.id),
|
|
28
|
-
published_at = CASE WHEN e.published_at IS NULL THEN sub.published_at ELSE e.published_at END,
|
|
29
|
-
published_by = COALESCE(e.published_by, sub.published_by)
|
|
30
|
-
FROM (
|
|
31
|
-
SELECT DISTINCT ON (entry_id) id, entry_id, published_at, published_by
|
|
32
|
-
FROM entry_version
|
|
33
|
-
WHERE published_at IS NOT NULL AND published_at > NOW()
|
|
34
|
-
ORDER BY entry_id, published_at ASC
|
|
35
|
-
) sub
|
|
36
|
-
WHERE e.id = sub.entry_id AND e.published_version_id IS NULL;
|
|
37
|
-
|
|
38
|
-
-- Replace folder-based media with tag-based media
|
|
39
|
-
|
|
40
|
-
CREATE TABLE IF NOT EXISTS media_tag (
|
|
41
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
42
|
-
name TEXT NOT NULL UNIQUE,
|
|
43
|
-
color TEXT NOT NULL DEFAULT '#3b82f6',
|
|
44
|
-
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
CREATE TABLE IF NOT EXISTS media_file_tag (
|
|
48
|
-
file_id UUID NOT NULL REFERENCES media_file(id) ON DELETE CASCADE,
|
|
49
|
-
tag_id UUID NOT NULL REFERENCES media_tag(id) ON DELETE CASCADE,
|
|
50
|
-
PRIMARY KEY (file_id, tag_id)
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
ALTER TABLE media_file DROP COLUMN IF EXISTS folder_id;
|
|
54
|
-
|
|
55
|
-
DROP TABLE IF EXISTS media_folder;
|