feat(ui): show a notification if user try to drop a file in another section than /files

This commit is contained in:
Edouard Vanbelle
2026-07-20 22:35:30 +02:00
parent c286eed3b2
commit 0cc77f7a36
19 changed files with 104 additions and 20 deletions
@@ -92,6 +92,8 @@
import DisplayModeControls from '$lib/components/DisplayModeControls.svelte';
import UserVignette from '$lib/components/UserVignette.svelte';
import VirtualList from '$lib/components/VirtualList.svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { t } from '$lib/i18n/index.svelte';
import { ui } from '$lib/stores/ui.svelte';
import { files as filesStore } from '$lib/stores/files.svelte';
@@ -927,7 +929,13 @@
// element accepts drops — without it, `drop` never fires and
// the pointer shows the OS "no-drop" cursor.
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = enableSystemDrop ? 'copy' : 'none';
// `dropEffect = 'none'` would tell the browser to REJECT the
// drop before `drop` fires — the toast/notification path in
// `onSystemDrop` would never run for wrong-zone drops. Always
// accept at the pointer level; the drop handler decides
// whether to upload (`enableSystemDrop`) or fire the
// "go to Files" toast.
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
}
function onSystemDragLeave(e: DragEvent) {
if (!isSystemDrag(e)) return;
@@ -949,7 +957,19 @@
'Uploads only work in Files — open the Files section and drop there.'
),
'warning',
6000
6000,
true,
{
action: {
label: t('resource_list.wrong_drop_zone_action', 'Go to Files'),
// One-click recovery from a mis-drop: land the user in
// /files so they can re-drag from the OS. We don't
// re-attach the dropped files (browsers throw away
// DataTransfer once the drop event returns), so this
// is the best we can offer without a second drag.
onClick: () => goto(resolve('/files'))
}
}
);
}
}
@@ -12,6 +12,18 @@
{#each ui.toasts as toast (toast.id)}
<div class="toast toast--{toast.kind}" role="status" data-testid={`toaster-toast-${toast.id}`}>
<span class="toast__msg">{toast.message}</span>
{#if toast.action}
<button
class="toast__action"
data-testid={`toaster-action-btn-${toast.id}`}
onclick={() => {
toast.action?.onClick();
ui.dismiss(toast.id);
}}
>
{toast.action.label}
</button>
{/if}
<button
class="toast__close"
data-testid={`toaster-dismiss-btn-${toast.id}`}
@@ -73,6 +85,22 @@
flex: 1;
}
.toast__action {
flex-shrink: 0;
background: var(--color-accent);
color: var(--color-accent-contrast);
border: none;
border-radius: var(--radius-md);
padding: var(--space-1-5) var(--space-3);
font-size: var(--text-sm);
font-weight: var(--weight-medium);
cursor: pointer;
}
.toast__action:hover {
filter: brightness(0.95);
}
.toast__close {
background: none;
border: none;
+22 -2
View File
@@ -6,10 +6,19 @@
*/
export type ToastKind = 'info' | 'success' | 'error' | 'warning';
export interface ToastAction {
/** Button label — should be short (≤ 20 chars). */
label: string;
/** Invoked when the button is clicked; the toast auto-dismisses after. */
onClick: () => void;
}
export interface Toast {
id: number;
message: string;
kind: ToastKind;
/** Optional inline action (e.g. "Go to Files" on a wrong-drop-zone toast). */
action?: ToastAction;
}
export interface Notification {
@@ -74,10 +83,21 @@ class UiStore {
/**
* Raise a toast and record a notification. `at` is stamped from the clock at
* call time; pass `record: false` for purely transient messages.
*
* The optional `opts.action` renders an inline button in the toast (e.g.
* "Go to Files" on a wrong-drop-zone warning); the callback fires on
* click and the toast auto-dismisses right after so a caller doesn't have
* to manage the id.
*/
notify(message: string, kind: ToastKind = 'info', timeoutMs = 4000, record = true): number {
notify(
message: string,
kind: ToastKind = 'info',
timeoutMs = 4000,
record = true,
opts: { action?: ToastAction } = {}
): number {
const id = ++this.#seq;
this.toasts = [...this.toasts, { id, message, kind }];
this.toasts = [...this.toasts, { id, message, kind, action: opts.action }];
if (record) {
this.notifications = [
{ id, message, kind, at: Date.now(), read: false },
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "الموقع",
"wrong_drop_zone_msg": "الرفع يعمل فقط في قسم الملفات — افتح قسم الملفات وأفلت العناصر هناك."
"wrong_drop_zone_msg": "الرفع يعمل فقط في قسم الملفات — افتح قسم الملفات وأفلت العناصر هناك.",
"wrong_drop_zone_action": "انتقل إلى الملفات"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "Speicherort",
"wrong_drop_zone_msg": "Uploads funktionieren nur in Dateien — öffne den Bereich Dateien und lege die Elemente dort ab."
"wrong_drop_zone_msg": "Uploads funktionieren nur in Dateien — öffne den Bereich Dateien und lege die Elemente dort ab.",
"wrong_drop_zone_action": "Zu Dateien wechseln"
}
}
+2 -1
View File
@@ -1675,6 +1675,7 @@
},
"resource_list": {
"location": "Location",
"wrong_drop_zone_msg": "Uploads only work in Files — open the Files section and drop there."
"wrong_drop_zone_msg": "Uploads only work in Files — open the Files section and drop there.",
"wrong_drop_zone_action": "Go to Files"
}
}
+2 -1
View File
@@ -1294,6 +1294,7 @@
},
"resource_list": {
"location": "Ubicación",
"wrong_drop_zone_msg": "Las subidas solo funcionan en Archivos — abre la sección Archivos y suelta ahí los elementos."
"wrong_drop_zone_msg": "Las subidas solo funcionan en Archivos — abre la sección Archivos y suelta ahí los elementos.",
"wrong_drop_zone_action": "Ir a Archivos"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "مکان",
"wrong_drop_zone_msg": "بارگذاری فقط در بخش پرونده‌ها کار می‌کند — بخش پرونده‌ها را باز کنید و آنجا رها کنید."
"wrong_drop_zone_msg": "بارگذاری فقط در بخش پرونده‌ها کار می‌کند — بخش پرونده‌ها را باز کنید و آنجا رها کنید.",
"wrong_drop_zone_action": "برو به پرونده‌ها"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "Emplacement",
"wrong_drop_zone_msg": "Les envois ne fonctionnent que dans Fichiers — ouvrez la section Fichiers et déposez-y vos éléments."
"wrong_drop_zone_msg": "Les envois ne fonctionnent que dans Fichiers — ouvrez la section Fichiers et déposez-y vos éléments.",
"wrong_drop_zone_action": "Aller aux Fichiers"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "स्थान",
"wrong_drop_zone_msg": "अपलोड केवल फ़ाइलें अनुभाग में काम करता है — फ़ाइलें अनुभाग खोलें और वहीं छोड़ें।"
"wrong_drop_zone_msg": "अपलोड केवल फ़ाइलें अनुभाग में काम करता है — फ़ाइलें अनुभाग खोलें और वहीं छोड़ें।",
"wrong_drop_zone_action": "फ़ाइलों पर जाएँ"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "Posizione",
"wrong_drop_zone_msg": "I caricamenti funzionano solo in File — apri la sezione File e trascina lì gli elementi."
"wrong_drop_zone_msg": "I caricamenti funzionano solo in File — apri la sezione File e trascina lì gli elementi.",
"wrong_drop_zone_action": "Vai a File"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "場所",
"wrong_drop_zone_msg": "アップロードはファイル セクションでのみ機能します。ファイル セクションを開いてそこにドロップしてください。"
"wrong_drop_zone_msg": "アップロードはファイル セクションでのみ機能します。ファイル セクションを開いてそこにドロップしてください。",
"wrong_drop_zone_action": "ファイルへ移動"
}
}
+2 -1
View File
@@ -1671,6 +1671,7 @@
},
"resource_list": {
"location": "위치",
"wrong_drop_zone_msg": "업로드는 파일 섹션에서만 작동합니다. 파일 섹션을 열고 거기에 놓아 주세요."
"wrong_drop_zone_msg": "업로드는 파일 섹션에서만 작동합니다. 파일 섹션을 열고 거기에 놓아 주세요.",
"wrong_drop_zone_action": "파일로 이동"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "Locatie",
"wrong_drop_zone_msg": "Uploaden werkt alleen in Bestanden — open het onderdeel Bestanden en zet ze daar neer."
"wrong_drop_zone_msg": "Uploaden werkt alleen in Bestanden — open het onderdeel Bestanden en zet ze daar neer.",
"wrong_drop_zone_action": "Naar Bestanden"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "Lokalizacja",
"wrong_drop_zone_msg": "Przesyłanie działa tylko w Plikach — otwórz sekcję Pliki i upuść tam pliki."
"wrong_drop_zone_msg": "Przesyłanie działa tylko w Plikach — otwórz sekcję Pliki i upuść tam pliki.",
"wrong_drop_zone_action": "Przejdź do Plików"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "Localização",
"wrong_drop_zone_msg": "Os envios só funcionam em Ficheiros — abra a secção Ficheiros e largue-os aí."
"wrong_drop_zone_msg": "Os envios só funcionam em Ficheiros — abra a secção Ficheiros e largue-os aí.",
"wrong_drop_zone_action": "Ir para Arquivos"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "Расположение",
"wrong_drop_zone_msg": "Загрузки работают только в разделе Файлы — откройте раздел Файлы и перетащите туда."
"wrong_drop_zone_msg": "Загрузки работают только в разделе Файлы — откройте раздел Файлы и перетащите туда.",
"wrong_drop_zone_action": "Перейти к файлам"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "位置",
"wrong_drop_zone_msg": "上傳僅在「檔案」中有效 — 請開啟檔案區並在該處拖放。"
"wrong_drop_zone_msg": "上傳僅在「檔案」中有效 — 請開啟檔案區並在該處拖放。",
"wrong_drop_zone_action": "前往檔案"
}
}
+2 -1
View File
@@ -1279,6 +1279,7 @@
},
"resource_list": {
"location": "位置",
"wrong_drop_zone_msg": "上传仅在 “文件” 中生效 — 请打开文件区并在那里拖放。"
"wrong_drop_zone_msg": "上传仅在 “文件” 中生效 — 请打开文件区并在那里拖放。",
"wrong_drop_zone_action": "前往文件"
}
}