feat(ui): show a notification if user try to drop a file in another section than /files
This commit is contained in:
@@ -92,6 +92,8 @@
|
|||||||
import DisplayModeControls from '$lib/components/DisplayModeControls.svelte';
|
import DisplayModeControls from '$lib/components/DisplayModeControls.svelte';
|
||||||
import UserVignette from '$lib/components/UserVignette.svelte';
|
import UserVignette from '$lib/components/UserVignette.svelte';
|
||||||
import VirtualList from '$lib/components/VirtualList.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 { t } from '$lib/i18n/index.svelte';
|
||||||
import { ui } from '$lib/stores/ui.svelte';
|
import { ui } from '$lib/stores/ui.svelte';
|
||||||
import { files as filesStore } from '$lib/stores/files.svelte';
|
import { files as filesStore } from '$lib/stores/files.svelte';
|
||||||
@@ -927,7 +929,13 @@
|
|||||||
// element accepts drops — without it, `drop` never fires and
|
// element accepts drops — without it, `drop` never fires and
|
||||||
// the pointer shows the OS "no-drop" cursor.
|
// the pointer shows the OS "no-drop" cursor.
|
||||||
e.preventDefault();
|
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) {
|
function onSystemDragLeave(e: DragEvent) {
|
||||||
if (!isSystemDrag(e)) return;
|
if (!isSystemDrag(e)) return;
|
||||||
@@ -949,7 +957,19 @@
|
|||||||
'Uploads only work in Files — open the Files section and drop there.'
|
'Uploads only work in Files — open the Files section and drop there.'
|
||||||
),
|
),
|
||||||
'warning',
|
'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)}
|
{#each ui.toasts as toast (toast.id)}
|
||||||
<div class="toast toast--{toast.kind}" role="status" data-testid={`toaster-toast-${toast.id}`}>
|
<div class="toast toast--{toast.kind}" role="status" data-testid={`toaster-toast-${toast.id}`}>
|
||||||
<span class="toast__msg">{toast.message}</span>
|
<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
|
<button
|
||||||
class="toast__close"
|
class="toast__close"
|
||||||
data-testid={`toaster-dismiss-btn-${toast.id}`}
|
data-testid={`toaster-dismiss-btn-${toast.id}`}
|
||||||
@@ -73,6 +85,22 @@
|
|||||||
flex: 1;
|
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 {
|
.toast__close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -6,10 +6,19 @@
|
|||||||
*/
|
*/
|
||||||
export type ToastKind = 'info' | 'success' | 'error' | 'warning';
|
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 {
|
export interface Toast {
|
||||||
id: number;
|
id: number;
|
||||||
message: string;
|
message: string;
|
||||||
kind: ToastKind;
|
kind: ToastKind;
|
||||||
|
/** Optional inline action (e.g. "Go to Files" on a wrong-drop-zone toast). */
|
||||||
|
action?: ToastAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
@@ -74,10 +83,21 @@ class UiStore {
|
|||||||
/**
|
/**
|
||||||
* Raise a toast and record a notification. `at` is stamped from the clock at
|
* Raise a toast and record a notification. `at` is stamped from the clock at
|
||||||
* call time; pass `record: false` for purely transient messages.
|
* 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;
|
const id = ++this.#seq;
|
||||||
this.toasts = [...this.toasts, { id, message, kind }];
|
this.toasts = [...this.toasts, { id, message, kind, action: opts.action }];
|
||||||
if (record) {
|
if (record) {
|
||||||
this.notifications = [
|
this.notifications = [
|
||||||
{ id, message, kind, at: Date.now(), read: false },
|
{ id, message, kind, at: Date.now(), read: false },
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "الموقع",
|
"location": "الموقع",
|
||||||
"wrong_drop_zone_msg": "الرفع يعمل فقط في قسم الملفات — افتح قسم الملفات وأفلت العناصر هناك."
|
"wrong_drop_zone_msg": "الرفع يعمل فقط في قسم الملفات — افتح قسم الملفات وأفلت العناصر هناك.",
|
||||||
|
"wrong_drop_zone_action": "انتقل إلى الملفات"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Speicherort",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1675,6 +1675,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Location",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1294,6 +1294,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Ubicación",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "مکان",
|
"location": "مکان",
|
||||||
"wrong_drop_zone_msg": "بارگذاری فقط در بخش پروندهها کار میکند — بخش پروندهها را باز کنید و آنجا رها کنید."
|
"wrong_drop_zone_msg": "بارگذاری فقط در بخش پروندهها کار میکند — بخش پروندهها را باز کنید و آنجا رها کنید.",
|
||||||
|
"wrong_drop_zone_action": "برو به پروندهها"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Emplacement",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "स्थान",
|
"location": "स्थान",
|
||||||
"wrong_drop_zone_msg": "अपलोड केवल फ़ाइलें अनुभाग में काम करता है — फ़ाइलें अनुभाग खोलें और वहीं छोड़ें।"
|
"wrong_drop_zone_msg": "अपलोड केवल फ़ाइलें अनुभाग में काम करता है — फ़ाइलें अनुभाग खोलें और वहीं छोड़ें।",
|
||||||
|
"wrong_drop_zone_action": "फ़ाइलों पर जाएँ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Posizione",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "場所",
|
"location": "場所",
|
||||||
"wrong_drop_zone_msg": "アップロードはファイル セクションでのみ機能します。ファイル セクションを開いてそこにドロップしてください。"
|
"wrong_drop_zone_msg": "アップロードはファイル セクションでのみ機能します。ファイル セクションを開いてそこにドロップしてください。",
|
||||||
|
"wrong_drop_zone_action": "ファイルへ移動"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1671,6 +1671,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "위치",
|
"location": "위치",
|
||||||
"wrong_drop_zone_msg": "업로드는 파일 섹션에서만 작동합니다. 파일 섹션을 열고 거기에 놓아 주세요."
|
"wrong_drop_zone_msg": "업로드는 파일 섹션에서만 작동합니다. 파일 섹션을 열고 거기에 놓아 주세요.",
|
||||||
|
"wrong_drop_zone_action": "파일로 이동"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Locatie",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Lokalizacja",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Localização",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "Расположение",
|
"location": "Расположение",
|
||||||
"wrong_drop_zone_msg": "Загрузки работают только в разделе Файлы — откройте раздел Файлы и перетащите туда."
|
"wrong_drop_zone_msg": "Загрузки работают только в разделе Файлы — откройте раздел Файлы и перетащите туда.",
|
||||||
|
"wrong_drop_zone_action": "Перейти к файлам"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "位置",
|
"location": "位置",
|
||||||
"wrong_drop_zone_msg": "上傳僅在「檔案」中有效 — 請開啟檔案區並在該處拖放。"
|
"wrong_drop_zone_msg": "上傳僅在「檔案」中有效 — 請開啟檔案區並在該處拖放。",
|
||||||
|
"wrong_drop_zone_action": "前往檔案"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1279,6 +1279,7 @@
|
|||||||
},
|
},
|
||||||
"resource_list": {
|
"resource_list": {
|
||||||
"location": "位置",
|
"location": "位置",
|
||||||
"wrong_drop_zone_msg": "上传仅在 “文件” 中生效 — 请打开文件区并在那里拖放。"
|
"wrong_drop_zone_msg": "上传仅在 “文件” 中生效 — 请打开文件区并在那里拖放。",
|
||||||
|
"wrong_drop_zone_action": "前往文件"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user