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.
Files changed (49) hide show
  1. package/CHANGELOG.md +31 -12
  2. package/ROADMAP.md +12 -0
  3. package/dist/admin/client/admin/dashboard-page.svelte +18 -64
  4. package/dist/admin/client/collection/bulk-actions-bar.svelte +18 -1
  5. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -0
  6. package/dist/admin/client/collection/collection-entries.svelte +25 -1
  7. package/dist/admin/client/collection/row-actions.svelte +13 -4
  8. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
  9. package/dist/admin/client/entry/entry-header.svelte +51 -4
  10. package/dist/admin/client/entry/entry-header.svelte.d.ts +3 -0
  11. package/dist/admin/client/entry/entry.svelte +106 -6
  12. package/dist/admin/client/entry/header/a11y-validator.d.ts +3 -2
  13. package/dist/admin/client/entry/header/a11y-validator.js +50 -9
  14. package/dist/admin/client/entry/header/publish-panel.svelte +164 -4
  15. package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -1
  16. package/dist/admin/components/dashboard/changelog-dialog.svelte +167 -0
  17. package/dist/admin/components/dashboard/changelog-dialog.svelte.d.ts +6 -0
  18. package/dist/admin/components/dashboard/orphaned-entries-notice.svelte +240 -0
  19. package/dist/admin/components/dashboard/orphaned-entries-notice.svelte.d.ts +13 -0
  20. package/dist/admin/components/fields/text-field-wrapper.svelte +134 -2
  21. package/dist/admin/components/layout/nav-footer.svelte +11 -4
  22. package/dist/admin/components/layout/nav-footer.svelte.d.ts +2 -17
  23. package/dist/admin/remote/entry.remote.d.ts +1 -0
  24. package/dist/admin/remote/entry.remote.js +5 -4
  25. package/dist/admin/state/content-language.svelte.d.ts +3 -0
  26. package/dist/admin/state/content-language.svelte.js +8 -0
  27. package/dist/admin/utils/translationStatus.d.ts +17 -0
  28. package/dist/admin/utils/translationStatus.js +134 -0
  29. package/dist/core/server/entries/operations/get.js +2 -1
  30. package/dist/db-postgres/index.js +10 -6
  31. package/dist/types/entries.d.ts +3 -0
  32. package/dist/updates/0.0.65/index.js +1 -1
  33. package/dist/updates/0.0.67/index.js +1 -1
  34. package/dist/updates/0.1.2/index.js +1 -1
  35. package/dist/updates/0.1.5/index.js +1 -1
  36. package/dist/updates/0.2.0/index.js +1 -1
  37. package/dist/updates/0.2.2/index.js +1 -1
  38. package/dist/updates/0.5.0/index.js +1 -1
  39. package/dist/updates/0.5.1/index.d.ts +2 -0
  40. package/dist/updates/0.5.1/index.js +17 -0
  41. package/dist/updates/0.5.2/index.d.ts +2 -0
  42. package/dist/updates/0.5.2/index.js +14 -0
  43. package/dist/updates/index.d.ts +2 -1
  44. package/dist/updates/index.js +3 -1
  45. package/package.json +1 -1
  46. package/dist/admin/components/dashboard/updates-banner.svelte +0 -170
  47. package/dist/admin/components/dashboard/updates-banner.svelte.d.ts +0 -3
  48. package/dist/updates/0.0.65/migration.sql +0 -55
  49. 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.onlyArchived
105
- ? sql `${schema.entriesTable.archivedAt} IS NOT NULL`
106
- : isNull(schema.entriesTable.archivedAt)
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.onlyArchived
131
- ? sql `${schema.entriesTable.archivedAt} IS NOT NULL`
132
- : isNull(schema.entriesTable.archivedAt)
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() })
@@ -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
- migration: `-- Move publish logic from entry_version to entry
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
- migration: `ALTER TABLE image_styles ADD COLUMN IF NOT EXISTS quality INTEGER;
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;`
@@ -21,7 +21,7 @@ export const update = {
21
21
  ],
22
22
  fixes: [],
23
23
  breakingChanges: [],
24
- migration: `CREATE TABLE IF NOT EXISTS invitation (
24
+ sql: `CREATE TABLE IF NOT EXISTS invitation (
25
25
  id TEXT PRIMARY KEY,
26
26
  email TEXT NOT NULL,
27
27
  role TEXT NOT NULL DEFAULT 'user',
@@ -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
- migration: 'Removes duplicate entry versions where data is identical to the previous version. Preserves published, scheduled, and latest draft versions.'
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
- migration: 'Config-only: rename type:"array" to type:"blocks" in field definitions. Stored data unchanged.'
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
- migration: 'No migration needed. Add type:"content" fields to collection/single configs. Existing richtext fields unchanged.'
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
- migration: 'No migration needed. Import `StructuredContent` from `includio/sveltekit` to render content fields.'
13
+ notes: 'No migration needed. Import `StructuredContent` from `includio/sveltekit` to render content fields.'
14
14
  };
@@ -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.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,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.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
+ };
@@ -5,7 +5,8 @@ export interface CmsUpdate {
5
5
  features: string[];
6
6
  fixes: string[];
7
7
  breakingChanges: string[];
8
- migration?: string;
8
+ sql?: string;
9
+ notes?: string;
9
10
  }
10
11
  export declare const updates: CmsUpdate[];
11
12
  export declare const getUpdatesFrom: (fromVersion: string) => CmsUpdate[];
@@ -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
- export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050];
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,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -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,3 +0,0 @@
1
- declare const UpdatesBanner: import("svelte").Component<Record<string, never>, {}, "">;
2
- type UpdatesBanner = ReturnType<typeof UpdatesBanner>;
3
- export default UpdatesBanner;
@@ -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;