includio-cms 0.5.0 → 0.5.1
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 +15 -0
- package/ROADMAP.md +8 -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 +157 -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.5.1/index.d.ts +2 -0
- package/dist/updates/0.5.1/index.js +17 -0
- package/dist/updates/index.js +2 -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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.5.1 — 2026-02-24
|
|
7
|
+
|
|
8
|
+
Restore archived entries, dashboard redesign, translation fixes
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Restore archived entries from collection list (single + bulk)
|
|
12
|
+
- Archived entry page: read-only with restore banner
|
|
13
|
+
- Dashboard: changelog modal, orphaned entries redesign, grid layout
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Opening archived entry no longer returns 500 (getRawEntry now supports includeArchived)
|
|
17
|
+
- Translation flow: reactive status, switcher UX, dynamic ref, copyFrom crash, panel scroll
|
|
18
|
+
- False "Niezapisane zmiany" alert on entry load
|
|
19
|
+
- Hide translation dots for non-required empty fields
|
|
20
|
+
|
|
6
21
|
## 0.5.0 — 2026-02-22
|
|
7
22
|
|
|
8
23
|
Frontend rendering for structured content
|
package/ROADMAP.md
CHANGED
|
@@ -64,6 +64,14 @@
|
|
|
64
64
|
- **0.4.1** — Accessibility layer (ATAG Part B)
|
|
65
65
|
- **0.5.0** — Frontend rendering
|
|
66
66
|
|
|
67
|
+
## 0.5.1 — Patches & dashboard
|
|
68
|
+
|
|
69
|
+
- [x] `[feature]` `[P1]` Restore archived entries — banner, read-only mode, list actions
|
|
70
|
+
- [x] `[feature]` `[P2]` Dashboard: changelog modal, orphaned entries redesign, grid layout
|
|
71
|
+
- [x] `[fix]` `[P1]` Translation flow — reactive status, switcher UX, dynamic ref, copyFrom crash, panel scroll
|
|
72
|
+
- [x] `[fix]` `[P1]` False "Niezapisane zmiany" on entry load
|
|
73
|
+
- [x] `[fix]` `[P2]` Hide translation dots for non-required empty fields
|
|
74
|
+
|
|
67
75
|
## 0.6.0 — Plugin system _(deferred from 0.2.0)_
|
|
68
76
|
|
|
69
77
|
- [ ] `[feature]` `[P0]` Wire plugin hooks into CRUD operations (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import OrphanedEntriesNotice from '../../components/dashboard/orphaned-entries-notice.svelte';
|
|
3
3
|
import WelcomeHeader from '../../components/dashboard/welcome-header.svelte';
|
|
4
4
|
import RecentEntries from '../../components/dashboard/recent-entries.svelte';
|
|
5
5
|
import FormSubmissionsWidget from '../../components/dashboard/form-submissions-widget.svelte';
|
|
@@ -9,12 +9,8 @@
|
|
|
9
9
|
import { sidebarLang } from '../../components/layout/lang.js';
|
|
10
10
|
import { getBreadcrumbs } from '../../state/breadcrumbs.svelte.js';
|
|
11
11
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
12
|
-
import * as Alert from '../../../components/ui/alert/index.js';
|
|
13
|
-
import { Button } from '../../../components/ui/button/index.js';
|
|
14
12
|
|
|
15
13
|
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
16
|
-
import AlertTriangleIcon from '@tabler/icons-svelte/icons/alert-triangle';
|
|
17
|
-
import TrashIcon from '@tabler/icons-svelte/icons/trash';
|
|
18
14
|
|
|
19
15
|
type OrphanedEntry = { id: string; slug: string; createdAt: string };
|
|
20
16
|
|
|
@@ -39,7 +35,11 @@
|
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
async function deleteOrphaned() {
|
|
42
|
-
|
|
38
|
+
const msg: Record<InterfaceLanguage, string> = {
|
|
39
|
+
pl: 'Czy na pewno chcesz usunąć te wpisy? Tej operacji nie można cofnąć.',
|
|
40
|
+
en: 'Are you sure you want to delete these entries? This cannot be undone.'
|
|
41
|
+
};
|
|
42
|
+
if (!confirm(msg[interfaceLanguage.current])) return;
|
|
43
43
|
|
|
44
44
|
deletingOrphaned = true;
|
|
45
45
|
try {
|
|
@@ -65,78 +65,32 @@
|
|
|
65
65
|
}
|
|
66
66
|
];
|
|
67
67
|
});
|
|
68
|
-
|
|
69
|
-
const orphanedLang: Record<
|
|
70
|
-
InterfaceLanguage,
|
|
71
|
-
{ title: string; description: string; deleteBtn: string; confirmDelete: string }
|
|
72
|
-
> = {
|
|
73
|
-
pl: {
|
|
74
|
-
title: 'Znaleziono osirocone wpisy',
|
|
75
|
-
description:
|
|
76
|
-
'W bazie danych znajdują się wpisy, których konfiguracja została usunięta. Zalecane jest ich usunięcie.',
|
|
77
|
-
deleteBtn: 'Usuń osirocone wpisy',
|
|
78
|
-
confirmDelete: 'Czy na pewno chcesz usunąć wszystkie osirocone wpisy? Ta operacja jest nieodwracalna.'
|
|
79
|
-
},
|
|
80
|
-
en: {
|
|
81
|
-
title: 'Orphaned entries found',
|
|
82
|
-
description:
|
|
83
|
-
'The database contains entries whose configuration has been removed. It is recommended to delete them.',
|
|
84
|
-
deleteBtn: 'Delete orphaned entries',
|
|
85
|
-
confirmDelete: 'Are you sure you want to delete all orphaned entries? This operation is irreversible.'
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
68
|
</script>
|
|
89
69
|
|
|
90
70
|
<main class="dash-main">
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
>
|
|
98
|
-
<AlertTriangleIcon class="text-orange-600 dark:text-orange-400" />
|
|
99
|
-
<Alert.Title class="text-orange-800 dark:text-orange-200">
|
|
100
|
-
{orphanedLang[interfaceLanguage.current].title} ({orphanedEntries.length})
|
|
101
|
-
</Alert.Title>
|
|
102
|
-
<Alert.Description class="text-orange-700 dark:text-orange-300">
|
|
103
|
-
<p class="mb-3">{orphanedLang[interfaceLanguage.current].description}</p>
|
|
104
|
-
<details class="mb-3">
|
|
105
|
-
<summary class="cursor-pointer text-sm font-medium">Slugs</summary>
|
|
106
|
-
<ul class="mt-2 list-inside list-disc text-sm">
|
|
107
|
-
{#each orphanedEntries as entry}
|
|
108
|
-
<li><code class="rounded bg-orange-200/50 px-1 dark:bg-orange-800/50">{entry.slug}</code></li>
|
|
109
|
-
{/each}
|
|
110
|
-
</ul>
|
|
111
|
-
</details>
|
|
112
|
-
<Button
|
|
113
|
-
variant="destructive"
|
|
114
|
-
size="sm"
|
|
115
|
-
onclick={deleteOrphaned}
|
|
116
|
-
disabled={deletingOrphaned}
|
|
117
|
-
>
|
|
118
|
-
<TrashIcon class="mr-1 h-4 w-4" />
|
|
119
|
-
{orphanedLang[interfaceLanguage.current].deleteBtn}
|
|
120
|
-
</Button>
|
|
121
|
-
</Alert.Description>
|
|
122
|
-
</Alert.Root>
|
|
71
|
+
{#if !loadingOrphaned}
|
|
72
|
+
<OrphanedEntriesNotice
|
|
73
|
+
entries={orphanedEntries}
|
|
74
|
+
deleting={deletingOrphaned}
|
|
75
|
+
ondelete={deleteOrphaned}
|
|
76
|
+
/>
|
|
123
77
|
{/if}
|
|
124
78
|
|
|
125
79
|
<WelcomeHeader />
|
|
126
80
|
|
|
127
|
-
<div class="
|
|
128
|
-
<div class="
|
|
129
|
-
<div class="
|
|
81
|
+
<div class="flex gap-5">
|
|
82
|
+
<div class="flex-1">
|
|
83
|
+
<div class="grid grid-cols-2 gap-5">
|
|
130
84
|
<RecentEntries />
|
|
131
85
|
<FormSubmissionsWidget />
|
|
132
86
|
</div>
|
|
133
|
-
<div
|
|
87
|
+
<div class="mt-5">
|
|
134
88
|
<RecentActivity />
|
|
135
89
|
</div>
|
|
136
90
|
</div>
|
|
137
|
-
<div class="
|
|
91
|
+
<div class="max-w-80 flex-1 shrink-0">
|
|
138
92
|
<A11yGauge />
|
|
139
|
-
<div
|
|
93
|
+
<div class="mt-5">
|
|
140
94
|
<TipOfTheDay />
|
|
141
95
|
</div>
|
|
142
96
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Archive from '@tabler/icons-svelte/icons/archive';
|
|
3
|
+
import ArchiveOff from '@tabler/icons-svelte/icons/archive-off';
|
|
3
4
|
import Trash from '@tabler/icons-svelte/icons/trash';
|
|
4
5
|
import X from '@tabler/icons-svelte/icons/x';
|
|
5
6
|
import Button from '../../../components/ui/button/button.svelte';
|
|
@@ -12,25 +13,29 @@
|
|
|
12
13
|
onArchive: () => void;
|
|
13
14
|
onDelete: () => void;
|
|
14
15
|
onClear: () => void;
|
|
16
|
+
onRestore?: () => void;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
|
-
let { selectedCount, onArchive, onDelete, onClear }: Props = $props();
|
|
19
|
+
let { selectedCount, onArchive, onDelete, onClear, onRestore }: Props = $props();
|
|
18
20
|
|
|
19
21
|
const interfaceLanguage = useInterfaceLanguage();
|
|
20
22
|
|
|
21
23
|
const lang: Record<InterfaceLanguage, {
|
|
22
24
|
selected: (n: number) => string;
|
|
23
25
|
archive: string;
|
|
26
|
+
restore: string;
|
|
24
27
|
delete: string;
|
|
25
28
|
}> = {
|
|
26
29
|
en: {
|
|
27
30
|
selected: (n) => `${n} selected`,
|
|
28
31
|
archive: 'Archive',
|
|
32
|
+
restore: 'Restore',
|
|
29
33
|
delete: 'Delete'
|
|
30
34
|
},
|
|
31
35
|
pl: {
|
|
32
36
|
selected: (n) => `Zaznaczono: ${n}`,
|
|
33
37
|
archive: 'Archiwizuj',
|
|
38
|
+
restore: 'Przywróć',
|
|
34
39
|
delete: 'Usuń'
|
|
35
40
|
}
|
|
36
41
|
};
|
|
@@ -52,6 +57,17 @@
|
|
|
52
57
|
|
|
53
58
|
<div class="h-5 w-px bg-white/20"></div>
|
|
54
59
|
|
|
60
|
+
{#if onRestore}
|
|
61
|
+
<Button
|
|
62
|
+
variant="ghost"
|
|
63
|
+
size="sm"
|
|
64
|
+
class="text-white hover:bg-white/10 hover:text-white"
|
|
65
|
+
onclick={onRestore}
|
|
66
|
+
>
|
|
67
|
+
<ArchiveOff class="mr-1.5 size-3.5" />
|
|
68
|
+
{t.restore}
|
|
69
|
+
</Button>
|
|
70
|
+
{:else}
|
|
55
71
|
<Button
|
|
56
72
|
variant="ghost"
|
|
57
73
|
size="sm"
|
|
@@ -61,6 +77,7 @@
|
|
|
61
77
|
<Archive class="mr-1.5 size-3.5" />
|
|
62
78
|
{t.archive}
|
|
63
79
|
</Button>
|
|
80
|
+
{/if}
|
|
64
81
|
<Button
|
|
65
82
|
variant="ghost"
|
|
66
83
|
size="sm"
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
selectAll: string;
|
|
48
48
|
selectRow: string;
|
|
49
49
|
entriesArchived: string;
|
|
50
|
+
entryRestored: string;
|
|
51
|
+
entriesRestored: string;
|
|
50
52
|
entryDeleted: string;
|
|
51
53
|
deleteConfirmTitle: string;
|
|
52
54
|
deleteConfirmDescription: string;
|
|
@@ -65,6 +67,8 @@
|
|
|
65
67
|
selectAll: 'Select all',
|
|
66
68
|
selectRow: 'Select row',
|
|
67
69
|
entriesArchived: 'Entries archived',
|
|
70
|
+
entryRestored: 'Entry restored',
|
|
71
|
+
entriesRestored: 'Entries restored',
|
|
68
72
|
entryDeleted: 'Entry permanently deleted',
|
|
69
73
|
deleteConfirmTitle: 'Delete entry permanently?',
|
|
70
74
|
deleteConfirmDescription:
|
|
@@ -83,6 +87,8 @@
|
|
|
83
87
|
selectAll: 'Zaznacz wszystkie',
|
|
84
88
|
selectRow: 'Zaznacz wiersz',
|
|
85
89
|
entriesArchived: 'Wpisy zarchiwizowane',
|
|
90
|
+
entryRestored: 'Wpis przywrócony',
|
|
91
|
+
entriesRestored: 'Wpisy przywrócone',
|
|
86
92
|
entryDeleted: 'Wpis trwale usunięty',
|
|
87
93
|
deleteConfirmTitle: 'Usunąć wpis na stałe?',
|
|
88
94
|
deleteConfirmDescription: 'Ta akcja jest nieodwracalna. Wpis zostanie trwale usunięty.',
|
|
@@ -242,7 +248,8 @@
|
|
|
242
248
|
entryUrl: info.row.original.url,
|
|
243
249
|
entryName: info.row.original.name,
|
|
244
250
|
onArchive: () => handleArchiveSingle(info.row.original.id),
|
|
245
|
-
onDelete: () => handleDelete(info.row.original.id)
|
|
251
|
+
onDelete: () => handleDelete(info.row.original.id),
|
|
252
|
+
...(isArchivedFilter ? { onRestore: () => handleRestoreSingle(info.row.original.id) } : {})
|
|
246
253
|
}),
|
|
247
254
|
enableSorting: false,
|
|
248
255
|
size: 50
|
|
@@ -259,6 +266,12 @@
|
|
|
259
266
|
refreshQueries();
|
|
260
267
|
}
|
|
261
268
|
|
|
269
|
+
async function handleRestoreSingle(id: string) {
|
|
270
|
+
await remotes.unarchiveEntryCommand(id);
|
|
271
|
+
toast.success(lang[interfaceLanguage.current].entryRestored);
|
|
272
|
+
refreshQueries();
|
|
273
|
+
}
|
|
274
|
+
|
|
262
275
|
function handleDelete(id: string) {
|
|
263
276
|
pendingDeleteId = id;
|
|
264
277
|
deleteDialogOpen = true;
|
|
@@ -381,6 +394,16 @@
|
|
|
381
394
|
refreshQueries();
|
|
382
395
|
}
|
|
383
396
|
|
|
397
|
+
async function handleBulkRestore(items: CollectionDataTableRow[]) {
|
|
398
|
+
const idsToRestore = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
|
|
399
|
+
for (const id of idsToRestore) {
|
|
400
|
+
await remotes.unarchiveEntryCommand(id);
|
|
401
|
+
}
|
|
402
|
+
toast.success(lang[interfaceLanguage.current].entriesRestored);
|
|
403
|
+
rowSelection = {};
|
|
404
|
+
refreshQueries();
|
|
405
|
+
}
|
|
406
|
+
|
|
384
407
|
async function handleBulkDelete(items: CollectionDataTableRow[]) {
|
|
385
408
|
const idsToDelete = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
|
|
386
409
|
for (const id of idsToDelete) {
|
|
@@ -543,6 +566,7 @@
|
|
|
543
566
|
onArchive={() => handleBulkArchive(displayItems)}
|
|
544
567
|
onDelete={() => handleBulkDelete(displayItems)}
|
|
545
568
|
onClear={() => (rowSelection = {})}
|
|
569
|
+
onRestore={isArchivedFilter ? () => handleBulkRestore(displayItems) : undefined}
|
|
546
570
|
/>
|
|
547
571
|
{/await}
|
|
548
572
|
{/key}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import DotsVertical from '@tabler/icons-svelte/icons/dots-vertical';
|
|
3
3
|
import ExternalLink from '@tabler/icons-svelte/icons/external-link';
|
|
4
4
|
import Archive from '@tabler/icons-svelte/icons/archive';
|
|
5
|
+
import ArchiveOff from '@tabler/icons-svelte/icons/archive-off';
|
|
5
6
|
import Trash from '@tabler/icons-svelte/icons/trash';
|
|
6
7
|
import Button from '../../../components/ui/button/button.svelte';
|
|
7
8
|
import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
|
|
@@ -13,15 +14,16 @@
|
|
|
13
14
|
entryName: string;
|
|
14
15
|
onArchive: () => void;
|
|
15
16
|
onDelete: () => void;
|
|
17
|
+
onRestore?: () => void;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
let { entryUrl, entryName, onArchive, onDelete }: Props = $props();
|
|
20
|
+
let { entryUrl, entryName, onArchive, onDelete, onRestore }: Props = $props();
|
|
19
21
|
|
|
20
22
|
const interfaceLanguage = useInterfaceLanguage();
|
|
21
23
|
|
|
22
|
-
const lang: Record<InterfaceLanguage, { open: string; archive: string; delete: string; moreActions: string }> = {
|
|
23
|
-
en: { open: 'Open', archive: 'Archive', delete: 'Delete', moreActions: 'More actions for' },
|
|
24
|
-
pl: { open: 'Otwórz', archive: 'Archiwizuj', delete: 'Usuń', moreActions: 'Więcej akcji dla' }
|
|
24
|
+
const lang: Record<InterfaceLanguage, { open: string; archive: string; restore: string; delete: string; moreActions: string }> = {
|
|
25
|
+
en: { open: 'Open', archive: 'Archive', restore: 'Restore', delete: 'Delete', moreActions: 'More actions for' },
|
|
26
|
+
pl: { open: 'Otwórz', archive: 'Archiwizuj', restore: 'Przywróć', delete: 'Usuń', moreActions: 'Więcej akcji dla' }
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
const t = $derived(lang[interfaceLanguage.current]);
|
|
@@ -48,10 +50,17 @@
|
|
|
48
50
|
{t.open}
|
|
49
51
|
</DropdownMenu.Item>
|
|
50
52
|
<DropdownMenu.Separator />
|
|
53
|
+
{#if onRestore}
|
|
54
|
+
<DropdownMenu.Item onclick={onRestore}>
|
|
55
|
+
<ArchiveOff class="mr-2 size-4" />
|
|
56
|
+
{t.restore}
|
|
57
|
+
</DropdownMenu.Item>
|
|
58
|
+
{:else}
|
|
51
59
|
<DropdownMenu.Item onclick={onArchive}>
|
|
52
60
|
<Archive class="mr-2 size-4" />
|
|
53
61
|
{t.archive}
|
|
54
62
|
</DropdownMenu.Item>
|
|
63
|
+
{/if}
|
|
55
64
|
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={onDelete}>
|
|
56
65
|
<Trash class="mr-2 size-4" />
|
|
57
66
|
{t.delete}
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
import LayoutSidebar from '@tabler/icons-svelte/icons/layout-sidebar';
|
|
12
12
|
import SendIcon from '@tabler/icons-svelte/icons/send';
|
|
13
13
|
import ChevronDownIcon from '@tabler/icons-svelte/icons/chevron-down';
|
|
14
|
+
import LanguageIcon from '@tabler/icons-svelte/icons/language';
|
|
14
15
|
import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
|
|
15
16
|
import { hasHybridContext, getHybridContext } from './hybrid/hybrid-context.svelte.js';
|
|
16
17
|
import { getEntryStatus } from './utils.js';
|
|
17
18
|
import { onMount } from 'svelte';
|
|
19
|
+
import type { LangStatus } from '../../utils/translationStatus.js';
|
|
18
20
|
|
|
19
21
|
const contentLanguage = getContentLanguage();
|
|
20
22
|
const interfaceLanguage = useInterfaceLanguage();
|
|
@@ -50,7 +52,9 @@
|
|
|
50
52
|
onSaveDraft: () => void;
|
|
51
53
|
onArchive: () => void;
|
|
52
54
|
saveStatus?: SaveStatus;
|
|
55
|
+
isArchived?: boolean;
|
|
53
56
|
onScrollToIssue?: (fieldSlug: string, nodePos: number) => void;
|
|
57
|
+
translationStatus?: Record<string, LangStatus> | null;
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
let {
|
|
@@ -62,7 +66,9 @@
|
|
|
62
66
|
fields = [],
|
|
63
67
|
getFormData,
|
|
64
68
|
saveStatus = 'idle',
|
|
65
|
-
|
|
69
|
+
isArchived = false,
|
|
70
|
+
onScrollToIssue,
|
|
71
|
+
translationStatus = null
|
|
66
72
|
}: Props = $props();
|
|
67
73
|
let { collection } = entry;
|
|
68
74
|
|
|
@@ -86,6 +92,31 @@
|
|
|
86
92
|
isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
87
93
|
});
|
|
88
94
|
const shortcutLabel = $derived(isMac ? '⌘S' : 'Ctrl+S');
|
|
95
|
+
|
|
96
|
+
function statusDotClass(status: LangStatus | undefined): string {
|
|
97
|
+
if (!status) return 'bg-[var(--text-light)]';
|
|
98
|
+
if (status.status === 'complete') return 'bg-[var(--success)]';
|
|
99
|
+
if (status.status === 'partial') return 'bg-[var(--warning)]';
|
|
100
|
+
return 'bg-[var(--text-light)]';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function statusLabel(lang: string, status: LangStatus | undefined): string {
|
|
104
|
+
if (!status) return lang.toUpperCase();
|
|
105
|
+
if (status.status === 'complete') return `${lang.toUpperCase()} \u2713`;
|
|
106
|
+
if (status.status === 'partial') return `${lang.toUpperCase()} \u26A0`;
|
|
107
|
+
return lang.toUpperCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function statusTooltip(lang: string, status: LangStatus | undefined): string {
|
|
111
|
+
if (!status) return lang.toUpperCase();
|
|
112
|
+
if (status.status === 'complete') return `${lang.toUpperCase()}: 100%`;
|
|
113
|
+
const missing = status.missingFields.map((f) => f.label).join(', ');
|
|
114
|
+
return `${lang.toUpperCase()}: ${status.percentage}% — ${interfaceLanguage.current === 'pl' ? 'brakuje' : 'missing'}: ${missing}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const isNonDefaultLang = $derived(
|
|
118
|
+
contentLanguage.all.length > 1 && contentLanguage.current !== contentLanguage.all[0]
|
|
119
|
+
);
|
|
89
120
|
</script>
|
|
90
121
|
|
|
91
122
|
<div
|
|
@@ -103,24 +134,39 @@
|
|
|
103
134
|
{#if contentLanguage.all.length > 1}
|
|
104
135
|
<div class="border-border bg-muted inline-flex overflow-hidden rounded-lg border">
|
|
105
136
|
{#each contentLanguage.all as lang}
|
|
137
|
+
{@const langStatus = translationStatus?.[lang]}
|
|
106
138
|
<button
|
|
107
139
|
type="button"
|
|
108
140
|
role="tab"
|
|
109
141
|
aria-selected={lang === contentLanguage.current}
|
|
110
|
-
|
|
142
|
+
title={statusTooltip(lang, langStatus)}
|
|
143
|
+
class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-semibold transition-colors {lang ===
|
|
111
144
|
contentLanguage.current
|
|
112
|
-
? 'text-primary bg-white shadow-sm'
|
|
145
|
+
? 'text-primary bg-white shadow-sm ring-1 ring-primary/20'
|
|
113
146
|
: 'text-muted-foreground hover:text-foreground bg-transparent'}"
|
|
114
147
|
onclick={() => (contentLanguage.current = lang)}
|
|
115
148
|
>
|
|
116
|
-
{
|
|
149
|
+
<span class="size-2 shrink-0 rounded-full {statusDotClass(langStatus)}"></span>
|
|
150
|
+
{statusLabel(lang, langStatus)}
|
|
117
151
|
</button>
|
|
118
152
|
{/each}
|
|
119
153
|
</div>
|
|
120
154
|
|
|
155
|
+
<!-- Reference mode toggle -->
|
|
156
|
+
<Button
|
|
157
|
+
variant="ghost"
|
|
158
|
+
size="icon-sm"
|
|
159
|
+
class={contentLanguage.referenceMode ? 'text-primary bg-[var(--lavender-lighter)]' : ''}
|
|
160
|
+
onclick={() => (contentLanguage.referenceMode = !contentLanguage.referenceMode)}
|
|
161
|
+
title={interfaceLanguage.current === 'pl' ? 'Tryb referencyjny' : 'Reference mode'}
|
|
162
|
+
>
|
|
163
|
+
<LanguageIcon class="size-4" />
|
|
164
|
+
</Button>
|
|
165
|
+
|
|
121
166
|
<div class="bg-border mx-1 h-5 w-px shrink-0"></div>
|
|
122
167
|
{/if}
|
|
123
168
|
|
|
169
|
+
{#if !isArchived}
|
|
124
170
|
<!-- Split button: Publish + Save Draft -->
|
|
125
171
|
<div class="inline-flex items-stretch rounded-lg shadow-[0_1px_3px_rgba(91,74,158,0.3)]">
|
|
126
172
|
<button
|
|
@@ -155,6 +201,7 @@
|
|
|
155
201
|
</div>
|
|
156
202
|
|
|
157
203
|
<div class="bg-border mx-1 h-5 w-px shrink-0"></div>
|
|
204
|
+
{/if}
|
|
158
205
|
|
|
159
206
|
<!-- Panel triggers + Hybrid toggle -->
|
|
160
207
|
{#await import('./header/publish-panel.svelte') then { default: PublishPanel }}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DbEntryVersion, RawEntry } from '../../../types/entries.js';
|
|
2
2
|
import type { UpdateEntryVersionCommandType } from '../../../core/server/entries/operations/update.js';
|
|
3
3
|
import type { Field } from '../../../types/fields.js';
|
|
4
|
+
import type { LangStatus } from '../../utils/translationStatus.js';
|
|
4
5
|
type SaveStatus = 'idle' | 'saving' | 'saved' | 'unsaved' | 'error';
|
|
5
6
|
type Props = {
|
|
6
7
|
entry: RawEntry;
|
|
@@ -11,7 +12,9 @@ type Props = {
|
|
|
11
12
|
onSaveDraft: () => void;
|
|
12
13
|
onArchive: () => void;
|
|
13
14
|
saveStatus?: SaveStatus;
|
|
15
|
+
isArchived?: boolean;
|
|
14
16
|
onScrollToIssue?: (fieldSlug: string, nodePos: number) => void;
|
|
17
|
+
translationStatus?: Record<string, LangStatus> | null;
|
|
15
18
|
};
|
|
16
19
|
declare const EntryHeader: import("svelte").Component<Props, {}, "">;
|
|
17
20
|
type EntryHeader = ReturnType<typeof EntryHeader>;
|