includio-cms 0.7.2 → 0.13.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 +128 -0
- package/ROADMAP.md +54 -2
- package/dist/admin/api/generate-styles.d.ts +2 -0
- package/dist/admin/api/generate-styles.js +32 -0
- package/dist/admin/api/handler.js +33 -0
- package/dist/admin/api/media-gc.js +10 -4
- package/dist/admin/api/rest/handler.js +17 -0
- package/dist/admin/api/rest/routes/collections.js +25 -13
- package/dist/admin/api/rest/routes/entries.d.ts +1 -1
- package/dist/admin/api/rest/routes/entries.js +10 -10
- package/dist/admin/api/rest/routes/media.d.ts +2 -0
- package/dist/admin/api/rest/routes/media.js +9 -0
- package/dist/admin/api/rest/routes/schema.d.ts +5 -0
- package/dist/admin/api/rest/routes/schema.js +152 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
- package/dist/admin/api/rest/routes/singletons.js +8 -7
- package/dist/admin/api/rest/routes/upload.d.ts +2 -0
- package/dist/admin/api/rest/routes/upload.js +28 -0
- package/dist/admin/api/upload.js +13 -0
- package/dist/admin/client/collection/collection-entries.svelte +35 -13
- package/dist/admin/client/entry/entry.svelte +21 -23
- package/dist/admin/client/entry/header/a11y-validator.js +2 -2
- package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
- package/dist/admin/client/entry/header/status-badge.svelte +2 -2
- package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
- package/dist/admin/client/entry/header/visibility.svelte +16 -10
- package/dist/admin/client/entry/utils.d.ts +3 -0
- package/dist/admin/client/entry/utils.js +22 -4
- package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
- package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
- package/dist/admin/client/users/users-page.svelte +5 -6
- package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
- package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
- package/dist/admin/components/fields/blocks-field.svelte +40 -19
- package/dist/admin/components/fields/field-renderer.svelte +4 -8
- package/dist/admin/components/fields/object-field.svelte +7 -12
- package/dist/admin/components/fields/select-field.svelte +8 -2
- package/dist/admin/components/fields/seo-field.svelte +40 -93
- package/dist/admin/components/fields/simple-array-field.svelte +27 -16
- package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
- package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
- package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
- package/dist/admin/components/fields/url-field.svelte +61 -72
- package/dist/admin/components/layout/layout-renderer.svelte +10 -4
- package/dist/admin/components/media/file-preview.svelte +10 -1
- package/dist/admin/components/media/file-upload.svelte +5 -1
- package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
- package/dist/admin/components/media/files-list.svelte +12 -3
- package/dist/admin/components/media/media-library.svelte +109 -37
- package/dist/admin/components/media/media-selector.svelte +90 -16
- package/dist/admin/components/media/tag-sidebar.svelte +10 -6
- package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
- package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +53 -94
- package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
- package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
- package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
- package/dist/admin/components/tiptap/inline-block-node.js +6 -5
- package/dist/admin/components/tiptap/lang.d.ts +77 -0
- package/dist/admin/components/tiptap/lang.js +170 -0
- package/dist/admin/components/tiptap/link-dialog.svelte +31 -28
- package/dist/admin/components/tiptap/slash-command.js +27 -23
- package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
- package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
- package/dist/admin/remote/email.remote.d.ts +1 -0
- package/dist/admin/remote/email.remote.js +5 -0
- package/dist/admin/remote/entry.remote.d.ts +2 -5
- package/dist/admin/remote/entry.remote.js +23 -28
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- package/dist/admin/remote/media.remote.d.ts +15 -0
- package/dist/admin/remote/media.remote.js +18 -2
- package/dist/admin/remote/preview.remote.js +3 -1
- package/dist/admin/utils/entryLabel.js +9 -6
- package/dist/admin/utils/translationStatus.js +1 -2
- package/dist/cli/scaffold/admin.js +34 -2
- package/dist/cms/runtime/api.d.ts +16 -12
- package/dist/cms/runtime/api.js +7 -6
- package/dist/cms/runtime/remote.js +2 -2
- package/dist/cms/runtime/schemas.d.ts +1 -1
- package/dist/cms/runtime/schemas.js +1 -1
- package/dist/cms/runtime/types.d.ts +118 -112
- package/dist/cms/runtime/types.js +0 -12
- package/dist/core/cms.d.ts +3 -1
- package/dist/core/cms.js +30 -0
- package/dist/core/fields/fieldSchemaToTs.js +9 -15
- package/dist/core/fields/formFieldSchemaToTs.js +7 -0
- package/dist/core/server/entries/operations/create.js +10 -4
- package/dist/core/server/entries/operations/get.d.ts +1 -0
- package/dist/core/server/entries/operations/get.js +186 -191
- package/dist/core/server/entries/operations/update.d.ts +6 -7
- package/dist/core/server/entries/operations/update.js +20 -38
- package/dist/core/server/fields/populateEntry.js +16 -52
- package/dist/core/server/fields/resolveImageFields.js +69 -120
- package/dist/core/server/fields/resolveRelationFields.js +30 -51
- package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
- package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
- package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
- package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
- package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
- package/dist/core/server/fields/resolveUrlFields.js +47 -56
- package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
- package/dist/core/server/fields/utils/fixOrphans.js +12 -0
- package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
- package/dist/core/server/fields/utils/imageStyles.js +41 -25
- package/dist/core/server/fields/utils/resolveMedia.js +1 -6
- package/dist/core/server/forms/submissions/operations/delete.js +26 -2
- package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
- package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
- package/dist/core/server/generator/fields.d.ts +6 -0
- package/dist/core/server/generator/fields.js +43 -5
- package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
- package/dist/core/server/generator/formFields.js +1 -0
- package/dist/core/server/generator/generator.js +98 -30
- package/dist/core/server/media/operations/getFiles.d.ts +5 -0
- package/dist/core/server/media/operations/getFiles.js +6 -0
- package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
- package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
- package/dist/db-postgres/index.js +303 -37
- package/dist/db-postgres/schema/entry.d.ts +0 -94
- package/dist/db-postgres/schema/entry.js +0 -6
- package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
- package/dist/db-postgres/schema/entryVersion.js +1 -0
- package/dist/entity/index.d.ts +9 -4
- package/dist/entity/index.js +24 -24
- package/dist/files-local/index.js +43 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/sveltekit/components/preview.svelte +2 -326
- package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
- package/dist/sveltekit/server/index.d.ts +2 -1
- package/dist/sveltekit/server/index.js +2 -1
- package/dist/sveltekit/server/preview.js +4 -7
- package/dist/types/adapters/db.d.ts +15 -1
- package/dist/types/adapters/files.d.ts +6 -0
- package/dist/types/cms.d.ts +5 -0
- package/dist/types/entries.d.ts +54 -18
- package/dist/types/fields.d.ts +14 -24
- package/dist/types/formFields.d.ts +7 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/layout.d.ts +0 -1
- package/dist/types/structured-content.d.ts +5 -0
- package/dist/updates/0.10.0/index.d.ts +2 -0
- package/dist/updates/0.10.0/index.js +15 -0
- package/dist/updates/0.11.0/index.d.ts +2 -0
- package/dist/updates/0.11.0/index.js +12 -0
- package/dist/updates/0.12.0/index.d.ts +2 -0
- package/dist/updates/0.12.0/index.js +12 -0
- package/dist/updates/0.13.0/index.d.ts +2 -0
- package/dist/updates/0.13.0/index.js +10 -0
- package/dist/updates/0.13.1/index.d.ts +2 -0
- package/dist/updates/0.13.1/index.js +20 -0
- package/dist/updates/0.7.3/index.d.ts +2 -0
- package/dist/updates/0.7.3/index.js +10 -0
- package/dist/updates/0.8.0/index.d.ts +2 -0
- package/dist/updates/0.8.0/index.js +18 -0
- package/dist/updates/0.8.0/migrate.d.ts +2 -0
- package/dist/updates/0.8.0/migrate.js +101 -0
- package/dist/updates/0.9.0/index.d.ts +2 -0
- package/dist/updates/0.9.0/index.js +38 -0
- package/dist/updates/index.js +9 -1
- package/package.json +7 -6
- package/dist/admin/components/fields/image-field.svelte +0 -198
- package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
- package/dist/admin/components/fields/richtext-field.svelte +0 -13
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
- package/dist/admin/components/tiptap.svelte +0 -11
- package/dist/admin/components/tiptap.svelte.d.ts +0 -6
- package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
- package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
accept?: string;
|
|
32
32
|
onUpload?: () => void;
|
|
33
33
|
dropZoneRef?: HTMLElement | null;
|
|
34
|
+
tagIds?: string[];
|
|
34
35
|
};
|
|
35
36
|
|
|
36
|
-
let { accept, onUpload, dropZoneRef = $bindable(null) }: Props =
|
|
37
|
+
let { accept, onUpload, dropZoneRef = $bindable(null), tagIds }: Props =
|
|
37
38
|
$props();
|
|
38
39
|
|
|
39
40
|
let inputElement: HTMLInputElement;
|
|
@@ -43,6 +44,9 @@
|
|
|
43
44
|
async function uploadFile(file: File, index: number) {
|
|
44
45
|
const form = new FormData();
|
|
45
46
|
form.append('file', file);
|
|
47
|
+
if (tagIds && tagIds.length > 0) {
|
|
48
|
+
form.append('tagIds', JSON.stringify(tagIds));
|
|
49
|
+
}
|
|
46
50
|
|
|
47
51
|
return new Promise<void>((resolve) => {
|
|
48
52
|
const xhr = new XMLHttpRequest();
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
import Music from '@tabler/icons-svelte/icons/music';
|
|
10
10
|
import Pdf from '@tabler/icons-svelte/icons/pdf';
|
|
11
11
|
import File from '@tabler/icons-svelte/icons/file';
|
|
12
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
13
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
14
|
+
|
|
15
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
16
|
+
const filesLang: Record<InterfaceLanguage, { noFiles: string; uploadOrFilter: string; tags: string }> = {
|
|
17
|
+
pl: { noFiles: 'Brak plików', uploadOrFilter: 'Prześlij pliki lub zmień filtry', tags: 'Tagi' },
|
|
18
|
+
en: { noFiles: 'No files', uploadOrFilter: 'Upload files or change filters', tags: 'Tags' }
|
|
19
|
+
};
|
|
20
|
+
const ft = $derived(filesLang[interfaceLanguage.current]);
|
|
12
21
|
|
|
13
22
|
type Props = {
|
|
14
23
|
files: MediaFile[];
|
|
@@ -94,8 +103,8 @@
|
|
|
94
103
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
95
104
|
</svg>
|
|
96
105
|
</div>
|
|
97
|
-
<p class="text-sm font-medium text-foreground">
|
|
98
|
-
<p class="text-xs text-muted-foreground mt-1">
|
|
106
|
+
<p class="text-sm font-medium text-foreground">{ft.noFiles}</p>
|
|
107
|
+
<p class="text-xs text-muted-foreground mt-1">{ft.uploadOrFilter}</p>
|
|
99
108
|
</div>
|
|
100
109
|
{:else}
|
|
101
110
|
{#each sortedFiles as file, i (file.id)}
|
|
@@ -146,7 +155,7 @@
|
|
|
146
155
|
<span class="text-[11px] font-medium text-text-light">{formatFileSize(file.size)}</span>
|
|
147
156
|
{/if}
|
|
148
157
|
{#if file.tags && file.tags.length > 0}
|
|
149
|
-
<span class="flex items-center gap-0.5 ml-auto" role="list" aria-label=
|
|
158
|
+
<span class="flex items-center gap-0.5 ml-auto" role="list" aria-label={ft.tags}>
|
|
150
159
|
{#each file.tags as tag (tag.id)}
|
|
151
160
|
<span
|
|
152
161
|
class="h-2 w-2 rounded-full"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import FileUpload from './file-upload.svelte';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
3
|
+
import { onMount, untrack } from 'svelte';
|
|
4
4
|
import type { MediaFile, MediaTag } from '../../../types/media.js';
|
|
5
5
|
import { getRemotes } from '../../context/remotes.js';
|
|
6
6
|
import { toast } from 'svelte-sonner';
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
selectModeActive: string;
|
|
29
29
|
selectModeAnnounce: string;
|
|
30
30
|
selectModeOffAnnounce: string;
|
|
31
|
+
loadMore: string;
|
|
31
32
|
}
|
|
32
33
|
> = {
|
|
33
34
|
pl: {
|
|
@@ -38,7 +39,8 @@
|
|
|
38
39
|
selectMode: 'Wybierz',
|
|
39
40
|
selectModeActive: 'Zaznaczanie',
|
|
40
41
|
selectModeAnnounce: 'Tryb zaznaczania włączony. Klikaj pliki, aby je zaznaczyć.',
|
|
41
|
-
selectModeOffAnnounce: 'Tryb zaznaczania wyłączony.'
|
|
42
|
+
selectModeOffAnnounce: 'Tryb zaznaczania wyłączony.',
|
|
43
|
+
loadMore: 'Wczytaj więcej'
|
|
42
44
|
},
|
|
43
45
|
en: {
|
|
44
46
|
fileDeletedToast: 'File has been deleted',
|
|
@@ -48,7 +50,8 @@
|
|
|
48
50
|
selectMode: 'Select',
|
|
49
51
|
selectModeActive: 'Selecting',
|
|
50
52
|
selectModeAnnounce: 'Selection mode enabled. Click files to select them.',
|
|
51
|
-
selectModeOffAnnounce: 'Selection mode disabled.'
|
|
53
|
+
selectModeOffAnnounce: 'Selection mode disabled.',
|
|
54
|
+
loadMore: 'Load more'
|
|
52
55
|
}
|
|
53
56
|
};
|
|
54
57
|
|
|
@@ -63,6 +66,8 @@
|
|
|
63
66
|
|
|
64
67
|
let { selected = $bindable([]), multiple = false, accept }: Props = $props();
|
|
65
68
|
|
|
69
|
+
const PAGE_SIZE = 48;
|
|
70
|
+
|
|
66
71
|
let currentFile: MediaFile | null = $state(null);
|
|
67
72
|
let activeTagFilter = $state<string | null>(null);
|
|
68
73
|
let searchQuery = $state('');
|
|
@@ -70,6 +75,8 @@
|
|
|
70
75
|
let dropZoneRef = $state<HTMLElement | null>(null);
|
|
71
76
|
let selectionMode = $state(false);
|
|
72
77
|
let selectionAnnouncement = $state('');
|
|
78
|
+
let page = $state(0);
|
|
79
|
+
let loadedFiles = $state<MediaFile[]>([]);
|
|
73
80
|
|
|
74
81
|
function exitSelectionMode() {
|
|
75
82
|
selectionMode = false;
|
|
@@ -83,21 +90,68 @@
|
|
|
83
90
|
selectionAnnouncement = lang[interfaceLanguage.current].selectModeAnnounce;
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
const filterData = $derived({
|
|
94
|
+
tagIds: activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined,
|
|
95
|
+
untagged: activeTagFilter === 'untagged' ? true : undefined,
|
|
96
|
+
mimeTypes: accept?.split(','),
|
|
97
|
+
search: searchQuery || undefined
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Reset page on filter/search change — track primitive sources only
|
|
101
|
+
$effect(() => {
|
|
102
|
+
void activeTagFilter;
|
|
103
|
+
void searchQuery;
|
|
104
|
+
void accept;
|
|
105
|
+
untrack(() => {
|
|
106
|
+
page = 0;
|
|
107
|
+
loadedFiles = [];
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
86
111
|
const filesQuery = $derived(
|
|
87
112
|
remotes.getMediaFiles({
|
|
88
113
|
data: {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
search: searchQuery || undefined
|
|
114
|
+
...filterData,
|
|
115
|
+
limit: PAGE_SIZE,
|
|
116
|
+
offset: page * PAGE_SIZE
|
|
93
117
|
}
|
|
94
118
|
})
|
|
95
119
|
);
|
|
96
120
|
|
|
97
|
-
const
|
|
98
|
-
remotes.
|
|
121
|
+
const countQuery = $derived(
|
|
122
|
+
remotes.countMediaFiles({
|
|
123
|
+
data: {
|
|
124
|
+
tagIds: filterData.tagIds,
|
|
125
|
+
untagged: filterData.untagged,
|
|
126
|
+
mimeTypes: filterData.mimeTypes,
|
|
127
|
+
search: filterData.search
|
|
128
|
+
}
|
|
129
|
+
})
|
|
99
130
|
);
|
|
100
131
|
|
|
132
|
+
const tagCountsQuery = $derived(remotes.getMediaTagsWithCounts());
|
|
133
|
+
const totalCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(',') } }));
|
|
134
|
+
const untaggedCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(','), untagged: true } }));
|
|
135
|
+
|
|
136
|
+
// Append new page results — only react to filesQuery changes
|
|
137
|
+
$effect(() => {
|
|
138
|
+
const ready = filesQuery.ready;
|
|
139
|
+
const current = filesQuery.current;
|
|
140
|
+
if (ready && current) {
|
|
141
|
+
untrack(() => {
|
|
142
|
+
if (page === 0) {
|
|
143
|
+
loadedFiles = current;
|
|
144
|
+
} else {
|
|
145
|
+
const existingIds = new Set(loadedFiles.map((f) => f.id));
|
|
146
|
+
loadedFiles = [...loadedFiles, ...current.filter((f) => !existingIds.has(f.id))];
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const totalCount = $derived(countQuery.ready ? (countQuery.current as number) : 0);
|
|
153
|
+
const hasMore = $derived(loadedFiles.length < totalCount);
|
|
154
|
+
|
|
101
155
|
let tagsQuery = $derived(remotes.getMediaTags());
|
|
102
156
|
|
|
103
157
|
function handleFileSelect(file: MediaFile, event?: MouseEvent) {
|
|
@@ -136,12 +190,21 @@
|
|
|
136
190
|
selectedFileIds = [...merged];
|
|
137
191
|
}
|
|
138
192
|
|
|
193
|
+
function refreshAll() {
|
|
194
|
+
page = 0;
|
|
195
|
+
loadedFiles = [];
|
|
196
|
+
filesQuery.refresh();
|
|
197
|
+
countQuery.refresh();
|
|
198
|
+
tagCountsQuery.refresh();
|
|
199
|
+
totalCountQuery.refresh();
|
|
200
|
+
untaggedCountQuery.refresh();
|
|
201
|
+
}
|
|
202
|
+
|
|
139
203
|
async function deleteFileCommand() {
|
|
140
204
|
if (currentFile) {
|
|
141
205
|
await remotes.deleteMediaFile(currentFile.id);
|
|
142
206
|
toast.success(lang[interfaceLanguage.current].fileDeletedToast);
|
|
143
|
-
|
|
144
|
-
allFilesQuery.refresh();
|
|
207
|
+
refreshAll();
|
|
145
208
|
currentFile = null;
|
|
146
209
|
}
|
|
147
210
|
}
|
|
@@ -153,8 +216,7 @@
|
|
|
153
216
|
if (tags) {
|
|
154
217
|
currentFile = { ...currentFile, tags: tags.filter((t) => tagIds.includes(t.id)) };
|
|
155
218
|
}
|
|
156
|
-
|
|
157
|
-
await allFilesQuery.refresh();
|
|
219
|
+
refreshAll();
|
|
158
220
|
}
|
|
159
221
|
}
|
|
160
222
|
|
|
@@ -163,8 +225,7 @@
|
|
|
163
225
|
const result = await remotes.renameMediaFile({ fileId: currentFile.id, newName });
|
|
164
226
|
if (result.success === true) {
|
|
165
227
|
currentFile = { ...currentFile, name: result.name, url: result.url };
|
|
166
|
-
|
|
167
|
-
await allFilesQuery.refresh();
|
|
228
|
+
refreshAll();
|
|
168
229
|
}
|
|
169
230
|
return result;
|
|
170
231
|
}
|
|
@@ -174,27 +235,25 @@
|
|
|
174
235
|
async function handleCreateTag(name: string, color: string) {
|
|
175
236
|
await remotes.createMediaTag({ name, color });
|
|
176
237
|
await tagsQuery.refresh();
|
|
238
|
+
await tagCountsQuery.refresh();
|
|
177
239
|
}
|
|
178
240
|
|
|
179
241
|
async function handleUpdateTag(id: string, name: string, color: string) {
|
|
180
242
|
await remotes.updateMediaTag({ id, name, color });
|
|
181
243
|
await tagsQuery.refresh();
|
|
182
|
-
|
|
183
|
-
await allFilesQuery.refresh();
|
|
244
|
+
refreshAll();
|
|
184
245
|
}
|
|
185
246
|
|
|
186
247
|
async function handleDeleteTag(id: string) {
|
|
187
248
|
await remotes.deleteMediaTag(id);
|
|
188
249
|
await tagsQuery.refresh();
|
|
189
|
-
|
|
190
|
-
await allFilesQuery.refresh();
|
|
250
|
+
refreshAll();
|
|
191
251
|
}
|
|
192
252
|
|
|
193
253
|
async function handleBulkTag(tagIds: string[]) {
|
|
194
254
|
const fileIds = selectedFileIds;
|
|
195
255
|
await remotes.bulkSetMediaFileTags({ fileIds, tagIds });
|
|
196
|
-
|
|
197
|
-
await allFilesQuery.refresh();
|
|
256
|
+
refreshAll();
|
|
198
257
|
}
|
|
199
258
|
|
|
200
259
|
async function handleBulkDelete() {
|
|
@@ -202,8 +261,7 @@
|
|
|
202
261
|
toast.success(lang[interfaceLanguage.current].bulkDeletedToast);
|
|
203
262
|
selectedFileIds = [];
|
|
204
263
|
currentFile = null;
|
|
205
|
-
|
|
206
|
-
await allFilesQuery.refresh();
|
|
264
|
+
refreshAll();
|
|
207
265
|
}
|
|
208
266
|
|
|
209
267
|
onMount(() => {
|
|
@@ -218,10 +276,12 @@
|
|
|
218
276
|
<div class="flex h-full overflow-hidden" bind:this={dropZoneRef}>
|
|
219
277
|
<!-- Tag sidebar (192px) -->
|
|
220
278
|
<aside class="w-48 min-w-48 shrink-0 border-r bg-card flex flex-col overflow-hidden" aria-label="Filtry tagów">
|
|
221
|
-
{#if tagsQuery.ready &&
|
|
279
|
+
{#if tagsQuery.ready && tagsQuery.current}
|
|
222
280
|
<TagSidebar
|
|
223
281
|
tags={tagsQuery.current}
|
|
224
|
-
|
|
282
|
+
tagCounts={tagCountsQuery.ready ? tagCountsQuery.current : []}
|
|
283
|
+
totalCount={totalCountQuery.ready ? totalCountQuery.current : 0}
|
|
284
|
+
untaggedCount={untaggedCountQuery.ready ? untaggedCountQuery.current : 0}
|
|
225
285
|
activeFilter={activeTagFilter}
|
|
226
286
|
onFilterChange={(f) => (activeTagFilter = f)}
|
|
227
287
|
onCreateTag={handleCreateTag}
|
|
@@ -250,9 +310,10 @@
|
|
|
250
310
|
</Toggle>
|
|
251
311
|
<div class="flex-1"></div>
|
|
252
312
|
<FileUpload
|
|
253
|
-
onUpload={() =>
|
|
313
|
+
onUpload={() => refreshAll()}
|
|
254
314
|
{accept}
|
|
255
315
|
bind:dropZoneRef
|
|
316
|
+
tagIds={activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined}
|
|
256
317
|
/>
|
|
257
318
|
</div>
|
|
258
319
|
|
|
@@ -261,14 +322,14 @@
|
|
|
261
322
|
|
|
262
323
|
<!-- File grid -->
|
|
263
324
|
<div class="flex-1 overflow-y-auto px-5 pt-4 pb-24 scrollbar-thin" role="listbox" aria-multiselectable="true" aria-label="Siatka plików">
|
|
264
|
-
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3" class:opacity-60={filesQuery.loading} style="transition: opacity 200ms">
|
|
265
|
-
{#if !filesQuery.ready}
|
|
325
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3" class:opacity-60={filesQuery.loading && page === 0} style="transition: opacity 200ms">
|
|
326
|
+
{#if !filesQuery.ready && page === 0}
|
|
266
327
|
{#each Array(8) as _}
|
|
267
328
|
<Skeleton class="block h-[168px] rounded-xl" />
|
|
268
329
|
{/each}
|
|
269
|
-
{:else
|
|
330
|
+
{:else}
|
|
270
331
|
<FilesList
|
|
271
|
-
files={
|
|
332
|
+
files={loadedFiles}
|
|
272
333
|
selected={selectedFileIds.length > 0 ? selectedFileIds : (selected ?? '')}
|
|
273
334
|
onSelect={handleFileSelect}
|
|
274
335
|
onRangeSelect={handleRangeSelect}
|
|
@@ -276,15 +337,27 @@
|
|
|
276
337
|
/>
|
|
277
338
|
{/if}
|
|
278
339
|
</div>
|
|
340
|
+
{#if hasMore}
|
|
341
|
+
<div class="flex justify-center py-6">
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
class="inline-flex items-center gap-2 rounded-lg border border-border px-5 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted disabled:opacity-50"
|
|
345
|
+
disabled={filesQuery.loading}
|
|
346
|
+
onclick={() => page++}
|
|
347
|
+
>
|
|
348
|
+
{lang[interfaceLanguage.current].loadMore} ({loadedFiles.length} / {totalCount})
|
|
349
|
+
</button>
|
|
350
|
+
</div>
|
|
351
|
+
{/if}
|
|
279
352
|
</div>
|
|
280
353
|
</section>
|
|
281
354
|
|
|
282
355
|
<!-- Detail panel (384px) -->
|
|
283
356
|
<aside class="w-96 min-w-96 shrink-0 border-l bg-card flex flex-col overflow-hidden" aria-label="Szczegóły pliku">
|
|
284
357
|
{#if selectionMode}
|
|
285
|
-
{#if
|
|
358
|
+
{#if tagsQuery.ready && tagsQuery.current}
|
|
286
359
|
<MultiFileSummary
|
|
287
|
-
files={
|
|
360
|
+
files={loadedFiles.filter((f) => selectedFileIds.includes(f.id))}
|
|
288
361
|
allTags={tagsQuery.current}
|
|
289
362
|
onBulkTag={handleBulkTag}
|
|
290
363
|
onBulkDelete={handleBulkDelete}
|
|
@@ -299,8 +372,7 @@
|
|
|
299
372
|
onDelete={deleteFileCommand}
|
|
300
373
|
onReplace={(updated) => {
|
|
301
374
|
currentFile = updated;
|
|
302
|
-
|
|
303
|
-
allFilesQuery.refresh();
|
|
375
|
+
refreshAll();
|
|
304
376
|
}}
|
|
305
377
|
{onTagUpdate}
|
|
306
378
|
{onNameUpdate}
|
|
@@ -323,15 +395,15 @@
|
|
|
323
395
|
</div>
|
|
324
396
|
|
|
325
397
|
<!-- Bulk action bar -->
|
|
326
|
-
{#if selectedFileIds.length > 0 && tagsQuery.ready && tagsQuery.current
|
|
398
|
+
{#if selectedFileIds.length > 0 && tagsQuery.ready && tagsQuery.current}
|
|
327
399
|
<BulkActionBar
|
|
328
400
|
selectedCount={selectedFileIds.length}
|
|
329
|
-
totalCount={
|
|
401
|
+
totalCount={totalCount}
|
|
330
402
|
tags={tagsQuery.current}
|
|
331
403
|
onBulkTag={handleBulkTag}
|
|
332
404
|
onBulkDelete={handleBulkDelete}
|
|
333
405
|
onClear={exitSelectionMode}
|
|
334
|
-
onSelectAll={() => { selectedFileIds =
|
|
406
|
+
onSelectAll={() => { selectedFileIds = loadedFiles.map((f) => f.id); }}
|
|
335
407
|
onCreateTag={handleCreateTag}
|
|
336
408
|
onUpdateTagColor={async (id, color) => {
|
|
337
409
|
const tag = tagsQuery.current?.find((t) => t.id === id);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import FileUpload from './file-upload.svelte';
|
|
3
|
-
import { onMount } from 'svelte';
|
|
3
|
+
import { onMount, untrack } from 'svelte';
|
|
4
4
|
import type { MediaFile, MediaTag } from '../../../types/media.js';
|
|
5
5
|
import { getRemotes } from '../../context/remotes.js';
|
|
6
6
|
import Button from '../../../components/ui/button/button.svelte';
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
cancel: string;
|
|
30
30
|
confirm: string;
|
|
31
31
|
selectedCount: string;
|
|
32
|
+
clickToSelect: string;
|
|
33
|
+
selectedFile: string;
|
|
32
34
|
}
|
|
33
35
|
> = {
|
|
34
36
|
pl: {
|
|
@@ -39,7 +41,9 @@
|
|
|
39
41
|
selectionResetLabel: 'Resetuj wybór',
|
|
40
42
|
cancel: 'Anuluj',
|
|
41
43
|
confirm: 'Potwierdź',
|
|
42
|
-
selectedCount: 'Wybrano'
|
|
44
|
+
selectedCount: 'Wybrano',
|
|
45
|
+
clickToSelect: '{lang[interfaceLanguage.current].clickToSelect}',
|
|
46
|
+
selectedFile: 'Wybrany plik:'
|
|
43
47
|
},
|
|
44
48
|
en: {
|
|
45
49
|
fileDeletedToast: 'File has been deleted',
|
|
@@ -49,7 +53,9 @@
|
|
|
49
53
|
selectionResetLabel: 'Reset selection',
|
|
50
54
|
cancel: 'Cancel',
|
|
51
55
|
confirm: 'Confirm',
|
|
52
|
-
selectedCount: 'Selected'
|
|
56
|
+
selectedCount: 'Selected',
|
|
57
|
+
clickToSelect: 'Click a file to select it',
|
|
58
|
+
selectedFile: 'Selected file:'
|
|
53
59
|
}
|
|
54
60
|
};
|
|
55
61
|
|
|
@@ -66,10 +72,14 @@
|
|
|
66
72
|
|
|
67
73
|
let { selected = $bindable([]), multiple = false, accept, onConfirm, onCancel }: Props = $props();
|
|
68
74
|
|
|
75
|
+
const PAGE_SIZE = 48;
|
|
76
|
+
|
|
69
77
|
let currentFile: MediaFile | null = $state(null);
|
|
70
78
|
let activeTagFilter = $state<string | null>(null);
|
|
71
79
|
let searchQuery = $state('');
|
|
72
80
|
let dropZoneRef = $state<HTMLElement | null>(null);
|
|
81
|
+
let page = $state(0);
|
|
82
|
+
let loadedFiles = $state<MediaFile[]>([]);
|
|
73
83
|
|
|
74
84
|
// Staged selection - changes are only committed on confirm
|
|
75
85
|
let stagedSelection = $state<string[] | string>(
|
|
@@ -86,17 +96,66 @@
|
|
|
86
96
|
onCancel?.();
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
const filterData = $derived({
|
|
100
|
+
tagIds: activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined,
|
|
101
|
+
untagged: activeTagFilter === 'untagged' ? true : undefined,
|
|
102
|
+
mimeTypes: accept?.split(','),
|
|
103
|
+
search: searchQuery || undefined
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
$effect(() => {
|
|
107
|
+
void activeTagFilter;
|
|
108
|
+
void searchQuery;
|
|
109
|
+
void accept;
|
|
110
|
+
untrack(() => {
|
|
111
|
+
page = 0;
|
|
112
|
+
loadedFiles = [];
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
89
116
|
const filesQuery = $derived(
|
|
90
117
|
remotes.getMediaFiles({
|
|
91
118
|
data: {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
...filterData,
|
|
120
|
+
limit: PAGE_SIZE,
|
|
121
|
+
offset: page * PAGE_SIZE
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const countQuery = $derived(
|
|
127
|
+
remotes.countMediaFiles({
|
|
128
|
+
data: {
|
|
129
|
+
tagIds: filterData.tagIds,
|
|
130
|
+
untagged: filterData.untagged,
|
|
131
|
+
mimeTypes: filterData.mimeTypes,
|
|
132
|
+
search: filterData.search
|
|
96
133
|
}
|
|
97
134
|
})
|
|
98
135
|
);
|
|
99
136
|
|
|
137
|
+
const tagCountsQuery = $derived(remotes.getMediaTagsWithCounts());
|
|
138
|
+
const totalCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(',') } }));
|
|
139
|
+
const untaggedCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(','), untagged: true } }));
|
|
140
|
+
|
|
141
|
+
$effect(() => {
|
|
142
|
+
const ready = filesQuery.ready;
|
|
143
|
+
const current = filesQuery.current;
|
|
144
|
+
if (ready && current) {
|
|
145
|
+
untrack(() => {
|
|
146
|
+
if (page === 0) {
|
|
147
|
+
loadedFiles = current;
|
|
148
|
+
} else {
|
|
149
|
+
const existingIds = new Set(loadedFiles.map((f) => f.id));
|
|
150
|
+
loadedFiles = [...loadedFiles, ...current.filter((f) => !existingIds.has(f.id))];
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const totalFilteredCount = $derived(countQuery.ready ? (countQuery.current as number) : 0);
|
|
157
|
+
const hasMore = $derived(loadedFiles.length < totalFilteredCount);
|
|
158
|
+
|
|
100
159
|
let tagsQuery = $derived(remotes.getMediaTags());
|
|
101
160
|
|
|
102
161
|
onMount(() => {
|
|
@@ -115,7 +174,9 @@
|
|
|
115
174
|
{#if tagsQuery.ready && tagsQuery.current}
|
|
116
175
|
<TagSidebar
|
|
117
176
|
tags={tagsQuery.current}
|
|
118
|
-
|
|
177
|
+
tagCounts={tagCountsQuery.ready ? tagCountsQuery.current : []}
|
|
178
|
+
totalCount={totalCountQuery.ready ? totalCountQuery.current : 0}
|
|
179
|
+
untaggedCount={untaggedCountQuery.ready ? untaggedCountQuery.current : 0}
|
|
119
180
|
activeFilter={activeTagFilter}
|
|
120
181
|
onFilterChange={(f) => (activeTagFilter = f)}
|
|
121
182
|
onCreateTag={async (name, color) => {
|
|
@@ -141,22 +202,23 @@
|
|
|
141
202
|
<MediaSearch bind:value={searchQuery} />
|
|
142
203
|
<MediaSort />
|
|
143
204
|
<FileUpload
|
|
144
|
-
onUpload={() => filesQuery.refresh()}
|
|
205
|
+
onUpload={() => { page = 0; loadedFiles = []; filesQuery.refresh(); countQuery.refresh(); tagCountsQuery.refresh(); totalCountQuery.refresh(); untaggedCountQuery.refresh(); }}
|
|
145
206
|
{accept}
|
|
146
207
|
bind:dropZoneRef
|
|
208
|
+
tagIds={activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined}
|
|
147
209
|
/>
|
|
148
210
|
</div>
|
|
149
211
|
|
|
150
212
|
<!-- Grid z plikami -->
|
|
151
213
|
<div class="flex-1 overflow-y-auto pr-2">
|
|
152
|
-
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3 p-4" class:opacity-60={filesQuery.loading} style="transition: opacity 200ms">
|
|
153
|
-
{#if !filesQuery.ready}
|
|
214
|
+
<div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3 p-4" class:opacity-60={filesQuery.loading && page === 0} style="transition: opacity 200ms">
|
|
215
|
+
{#if !filesQuery.ready && page === 0}
|
|
154
216
|
{#each Array(8) as _}
|
|
155
217
|
<Skeleton class="block aspect-square rounded-2xl" />
|
|
156
218
|
{/each}
|
|
157
|
-
{:else
|
|
219
|
+
{:else}
|
|
158
220
|
<FilesList
|
|
159
|
-
files={
|
|
221
|
+
files={loadedFiles}
|
|
160
222
|
selected={stagedSelection}
|
|
161
223
|
onSelect={(file) => {
|
|
162
224
|
currentFile = file;
|
|
@@ -171,6 +233,18 @@
|
|
|
171
233
|
/>
|
|
172
234
|
{/if}
|
|
173
235
|
</div>
|
|
236
|
+
{#if hasMore}
|
|
237
|
+
<div class="flex justify-center py-4">
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
class="inline-flex items-center gap-2 rounded-lg border border-border px-5 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted disabled:opacity-50"
|
|
241
|
+
disabled={filesQuery.loading}
|
|
242
|
+
onclick={() => page++}
|
|
243
|
+
>
|
|
244
|
+
{loadedFiles.length} / {totalFilteredCount}
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
{/if}
|
|
174
248
|
</div>
|
|
175
249
|
</div>
|
|
176
250
|
|
|
@@ -215,15 +289,15 @@
|
|
|
215
289
|
{/each}
|
|
216
290
|
|
|
217
291
|
{#if stagedSelection.length === 0}
|
|
218
|
-
<p class="text-sm text-muted-foreground py-8 text-center">
|
|
292
|
+
<p class="text-sm text-muted-foreground py-8 text-center">{lang[interfaceLanguage.current].clickToSelect}</p>
|
|
219
293
|
{/if}
|
|
220
294
|
{:else}
|
|
221
|
-
<p class="text-sm font-medium text-muted-foreground">
|
|
295
|
+
<p class="text-sm font-medium text-muted-foreground">{lang[interfaceLanguage.current].selectedFile}</p>
|
|
222
296
|
|
|
223
297
|
{#if stagedSelection}
|
|
224
298
|
<FilePreview fileId={stagedSelection} />
|
|
225
299
|
{:else}
|
|
226
|
-
<p class="text-sm text-muted-foreground py-8 text-center">
|
|
300
|
+
<p class="text-sm text-muted-foreground py-8 text-center">{lang[interfaceLanguage.current].clickToSelect}</p>
|
|
227
301
|
{/if}
|
|
228
302
|
{/if}
|
|
229
303
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
3
3
|
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { MediaTag } from '../../../types/media.js';
|
|
5
5
|
import Input from '../../../components/ui/input/input.svelte';
|
|
6
6
|
import * as Popover from '../../../components/ui/popover/index.js';
|
|
7
7
|
import * as AlertDialog from '../../../components/ui/alert-dialog/index.js';
|
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
|
|
66
66
|
type Props = {
|
|
67
67
|
tags: MediaTag[];
|
|
68
|
-
|
|
68
|
+
tagCounts?: { tag: MediaTag; count: number }[];
|
|
69
|
+
totalCount?: number;
|
|
70
|
+
untaggedCount?: number;
|
|
69
71
|
activeFilter: string | null;
|
|
70
72
|
onFilterChange: (filter: string | null) => void;
|
|
71
73
|
onCreateTag: (name: string, color: string) => Promise<void>;
|
|
@@ -75,7 +77,9 @@
|
|
|
75
77
|
|
|
76
78
|
let {
|
|
77
79
|
tags,
|
|
78
|
-
|
|
80
|
+
tagCounts = [],
|
|
81
|
+
totalCount = 0,
|
|
82
|
+
untaggedCount: untaggedCountProp = 0,
|
|
79
83
|
activeFilter,
|
|
80
84
|
onFilterChange,
|
|
81
85
|
onCreateTag,
|
|
@@ -92,10 +96,10 @@
|
|
|
92
96
|
let menuOpenTagId = $state<string | null>(null);
|
|
93
97
|
|
|
94
98
|
function getTagFileCount(tagId: string): number {
|
|
95
|
-
return
|
|
99
|
+
return tagCounts.find((tc) => tc.tag.id === tagId)?.count ?? 0;
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
const untaggedCount = $derived(
|
|
102
|
+
const untaggedCount = $derived(untaggedCountProp);
|
|
99
103
|
|
|
100
104
|
function getNextColor(): string {
|
|
101
105
|
const usedColors = tags.map((t) => t.color);
|
|
@@ -152,7 +156,7 @@
|
|
|
152
156
|
<Tag class="h-4 w-4" />
|
|
153
157
|
</span>
|
|
154
158
|
<span class="flex-1 truncate">{lang[interfaceLanguage.current].all}</span>
|
|
155
|
-
<span class="text-[11px] font-semibold tabular-nums {activeFilter === null ? 'text-primary' : 'text-text-light'}">{
|
|
159
|
+
<span class="text-[11px] font-semibold tabular-nums {activeFilter === null ? 'text-primary' : 'text-text-light'}">{totalCount}</span>
|
|
156
160
|
</button>
|
|
157
161
|
</li>
|
|
158
162
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MediaTag } from '../../../types/media.js';
|
|
2
2
|
type Props = {
|
|
3
3
|
tags: MediaTag[];
|
|
4
|
-
|
|
4
|
+
tagCounts?: {
|
|
5
|
+
tag: MediaTag;
|
|
6
|
+
count: number;
|
|
7
|
+
}[];
|
|
8
|
+
totalCount?: number;
|
|
9
|
+
untaggedCount?: number;
|
|
5
10
|
activeFilter: string | null;
|
|
6
11
|
onFilterChange: (filter: string | null) => void;
|
|
7
12
|
onCreateTag: (name: string, color: string) => Promise<void>;
|