refator(ui): merge of ResourceList part1

This commit is contained in:
Edouard Vanbelle
2026-07-19 23:58:26 +02:00
parent aec9b8f037
commit 75ae837f1a
22 changed files with 598 additions and 89 deletions
@@ -0,0 +1,37 @@
<script lang="ts">
/**
* Two-slot action-bar layout used above every ResourceList surface.
*
* [ start-slot ] ← page actions / batch actions
* [ end-slot ] ← <DisplayModeControls /> — group-by, sort, view, dotfile
*
* This is pure layout — no state, no visual variation per section.
* It reuses the existing `.actions-bar` / `.action-buttons` classes
* defined globally in `styles/ported/content.css` so it renders
* identically to `<ListToolbar>` (the component it's replacing).
*
* Consumers pass whatever they want on either side; `<ResourceList>`
* uses this internally to wire its `actions` / `batchActions` /
* display-mode-controls snippets, and pages can also use it directly
* when they need a bespoke layout that doesn't fit ResourceList's
* default (e.g. `/files` upload split-button).
*/
import type { Snippet } from 'svelte';
interface Props {
/** Left cluster — page action buttons (or batch actions on
* selection). Omit to render an empty placeholder that still
* reserves the space, so the end cluster stays right-aligned. */
start?: Snippet;
/** Right cluster — usually a `<DisplayModeControls />` instance,
* but any content works. */
end?: Snippet;
}
let { start, end }: Props = $props();
</script>
<div class="actions-bar">
{#if start}{@render start()}{:else}<div class="action-buttons"></div>{/if}
{#if end}{@render end()}{/if}
</div>
@@ -0,0 +1,194 @@
<script lang="ts" module>
/** Reuses the existing GroupOption shape from `<ListToolbar>` so
* callers can pass the same `groupBys` arrays their pages already
* define. Duplicated here so consumers can import a coherent set
* without pulling in the legacy toolbar. */
export interface GroupOption {
key: string;
label: string;
icon?: string;
}
</script>
<script lang="ts">
/**
* Right-hand cluster of display-mode controls: group-by menu,
* sort-direction toggle, grid/list view toggle, hide-dotfiles eye.
*
* Every control is opt-in via its own `show…` prop so a section
* without one (e.g. `/trash` has no dotfile toggle by design) can
* omit the prop rather than pass an empty array or a no-op
* callback. State bindings pass through — the parent still owns
* `groupBy`, `reversed`, `viewMode`, etc.
*
* Style-wise this reuses the ported `.view-toggle` block + child
* classes (buttons.css) so it renders identically to the
* `<ListToolbar>` right cluster. That keeps every page's look
* consistent across the ResourceList migration.
*/
import type { Snippet } from 'svelte';
import Icon from '$lib/icons/Icon.svelte';
import { t } from '$lib/i18n/index.svelte';
import { files as filesStore } from '$lib/stores/files.svelte';
import { preferences } from '$lib/stores/preferences.svelte';
interface Props {
// ── Group-by ─────────────────────────────────────────────
/** Group-by dimensions; omit or empty array = hide the control. */
groups?: GroupOption[];
/** Active group-by key (controlled by the parent). */
groupBy?: string;
/** Fired when a group-by dimension is chosen. */
ongroup?: (key: string) => void;
// ── Sort direction ───────────────────────────────────────
/** Whether the sort direction is reversed. */
reversed?: boolean;
/** Fired when the sort-direction toggle is clicked. */
ondirection?: () => void;
/** Show the sort-direction toggle. Defaults to `true` when
* `groups` is non-empty (there's nothing to reverse otherwise). */
showSort?: boolean;
// ── View mode (grid / list) ─────────────────────────────
/** Show the grid/list view toggle. */
showViewMode?: boolean;
// ── Dotfile visibility ──────────────────────────────────
/** Show the hide-dotfiles eye toggle. Only makes sense on
* algorithmic listings (files, recent); off by default. */
showDotfileToggle?: boolean;
// ── Extension slot ──────────────────────────────────────
/** Rendered immediately before the group-by button, still
* inside `.view-toggle`. Kind-filter dropdowns and other
* page-local controls that want to sit alongside the
* built-ins land here. */
beforeGroupBy?: Snippet;
}
let {
groups,
groupBy = '',
ongroup,
reversed = false,
ondirection,
showSort,
showViewMode = false,
showDotfileToggle = false,
beforeGroupBy
}: Props = $props();
// Sort toggle defaults ON when a group-by list is provided —
// there's nothing to reverse without it.
const sortVisible = $derived(showSort ?? (groups?.length ?? 0) > 0);
const active = $derived(groups?.find((g) => g.key === groupBy) ?? groups?.[0]);
let menuOpen = $state(false);
$effect(() => {
if (!menuOpen) return;
const onDown = (e: MouseEvent) => {
if (!(e.target as HTMLElement).closest('.group-by-selector')) menuOpen = false;
};
window.addEventListener('pointerdown', onDown);
return () => window.removeEventListener('pointerdown', onDown);
});
function pick(key: string) {
menuOpen = false;
ongroup?.(key);
}
// Hide the whole cluster if nothing is enabled — the ActionBar
// then collapses to its start-only layout without an empty
// right block occupying space.
const anyVisible = $derived(
(groups?.length ?? 0) > 0 || showViewMode || showDotfileToggle || !!beforeGroupBy
);
</script>
{#if anyVisible}
<div class="view-toggle" role="group" aria-label={t('view.label', 'View options')}>
{#if beforeGroupBy}{@render beforeGroupBy()}{/if}
{#if groups?.length}
<div class="group-by-selector" data-testid="display-mode-groupby-menu">
<button
class="toggle-btn group-by-btn active"
title={t('groupby.title', 'Group by')}
aria-haspopup="true"
aria-expanded={menuOpen}
data-testid="display-mode-groupby-btn"
onclick={() => (menuOpen = !menuOpen)}
>
<Icon name={active?.icon ?? 'layer-group'} />
<span class="group-by-label">{active?.label ?? ''}</span>
</button>
{#if sortVisible}
<button
class="toggle-btn sort-dir-btn"
class:active={reversed}
title={t('sortdir.title', 'Sort direction')}
aria-label={t('sort.direction', 'Sort direction')}
data-testid="display-mode-sort-direction-btn"
onclick={() => ondirection?.()}
>
<Icon name="arrow-up" />
</button>
{/if}
{#if menuOpen}
<div class="group-by-menu">
{#each groups as g (g.key)}
<button
class="group-by-option"
class:active={groupBy === g.key}
data-testid={`display-mode-groupby-${g.key}-item`}
onclick={() => pick(g.key)}
>
<Icon name={g.icon ?? 'layer-group'} />
{g.label}
</button>
{/each}
</div>
{/if}
</div>
{#if showViewMode}<span class="view-toggle-separator"></span>{/if}
{/if}
{#if showViewMode}
<button
class="toggle-btn"
class:active={filesStore.viewMode === 'grid'}
title={t('view.grid', 'Grid view')}
aria-pressed={filesStore.viewMode === 'grid'}
data-testid="display-mode-view-grid-btn"
onclick={() => filesStore.setViewMode('grid')}
>
<Icon name="th" />
</button>
<button
class="toggle-btn"
class:active={filesStore.viewMode === 'list'}
title={t('view.list', 'List view')}
aria-pressed={filesStore.viewMode === 'list'}
data-testid="display-mode-view-list-btn"
onclick={() => filesStore.setViewMode('list')}
>
<Icon name="list" />
</button>
{/if}
{#if showDotfileToggle}
<button
class="toggle-btn"
class:active={preferences.hideDotfiles}
title={preferences.hideDotfiles
? t('view.show_dotfiles', 'Show hidden files')
: t('view.hide_dotfiles', 'Hide hidden files')}
aria-pressed={preferences.hideDotfiles}
data-testid="display-mode-dotfile-toggle-btn"
onclick={() => preferences.toggleHideDotfiles()}
>
<Icon name={preferences.hideDotfiles ? 'eye-slash' : 'eye'} />
</button>
{/if}
</div>
{/if}
+227 -57
View File
@@ -69,10 +69,12 @@
import Icon from '$lib/icons/Icon.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import SkeletonList from '$lib/components/SkeletonList.svelte';
import ListToolbar from '$lib/components/ListToolbar.svelte';
import ActionBar from '$lib/components/ActionBar.svelte';
import DisplayModeControls from '$lib/components/DisplayModeControls.svelte';
import UserVignette from '$lib/components/UserVignette.svelte';
import VirtualList from '$lib/components/VirtualList.svelte';
import { t } from '$lib/i18n/index.svelte';
import { ui } from '$lib/stores/ui.svelte';
import { files as filesStore } from '$lib/stores/files.svelte';
import { preferences } from '$lib/stores/preferences.svelte';
import { formatBytes } from '$lib/utils/format';
@@ -176,10 +178,62 @@
onfavorite?: (item: FileItem | FolderItem) => void;
/** Selection changed (set of selected item ids). */
onselectionchange?: (ids: Set<string>) => void;
actions?: Snippet<[FileItem | FolderItem]>;
toolbar?: Snippet;
/** Batch toolbar shown when items are selected; receives selected items. */
batchToolbar?: Snippet<[Array<FileItem | FolderItem>]>;
/**
* Per-item action cell (renders at the end of a row). Kept as a
* distinct slot from the action-bar snippets below so callers
* that want an item-scoped affordance (a per-row overflow menu)
* don't have to piggyback on the bar.
*/
itemActions?: Snippet<[FileItem | FolderItem]>;
/**
* Action-bar left cluster — always-visible page action buttons
* (Upload / New folder / Empty trash / Clear recent / …). Swaps
* to `batchActions` when the selection is non-empty. Every
* section provides its own buttons; ResourceList doesn't ship
* any defaults.
*/
actions?: Snippet;
/**
* Action-bar left cluster when selection is non-empty —
* replaces `actions`. Receives the selected items so buttons
* can be scoped to the batch. Replaces the phase-1
* `batchToolbar` floating strip pattern.
*/
batchActions?: Snippet<[Array<FileItem | FolderItem>]>;
/**
* Rendered next to the item name in each row. `/trash` uses
* this for its expiration badge; other sections omit it.
* ResourceList stays ignorant of what the badge means — the
* page decides. Empty return = no badge.
*/
rowBadge?: Snippet<[FileItem | FolderItem, ItemContext | undefined]>;
/**
* Rendered above the toolbar in the sticky header. Only
* `/files` wires this today; every other section leaves the
* snippet undefined so no breadcrumb strip appears. Kept as a
* snippet (not a boolean) so the page owns crumb rendering and
* their click / drag-drop behavior.
*/
breadcrumb?: Snippet;
/**
* When true, drops from the OS file system on the ResourceList
* wrapper are forwarded to `onsystemdrop` (upload path). When
* false (default), the wrapper still intercepts the OS drop —
* `preventDefault` so the browser doesn't navigate to the file
* — and fires a "wrong section" `ui.notify()` pointing the user
* at the Files section (the legacy behaviour). Item-drag drops
* (row → folder) are unaffected either way; those go through
* `onitemdrop` per the existing row hooks.
*/
enableSystemDrop?: boolean;
/**
* Called with the OS-dropped files when `enableSystemDrop` is
* true. The page keeps ownership of the upload code (walking
* webkitGetAsEntry trees, chunked uploader, etc.) — this
* component just delivers the payload. Ignored when
* `enableSystemDrop` is false.
*/
onsystemdrop?: (e: DragEvent) => void;
/**
* Render `<img>` thumbnails on file rows and fall back to
* client-side generation when the server doesn't have one
@@ -266,9 +320,13 @@
onopen,
onfavorite,
onselectionchange,
itemActions,
actions,
toolbar,
batchToolbar,
batchActions,
rowBadge,
breadcrumb,
enableSystemDrop = false,
onsystemdrop,
enableThumbnails = true,
isDraggable,
isDropTarget,
@@ -345,7 +403,7 @@
showType ? '120px' : '',
showSize ? '110px' : '',
showDate ? '160px' : '',
actions ? '120px' : ''
itemActions ? '120px' : ''
]
.filter(Boolean)
.join(' ')
@@ -562,6 +620,63 @@
.filter(Boolean)
.join('\n');
}
// ── System-drop handling (OS files onto the wrapper) ───────────────────────
// Two modes:
//
// * `enableSystemDrop = true`: the page has an upload code path
// ready (the `/files` browser). We `preventDefault` the browser's
// default (which would open the dragged file as a top-level
// navigation), highlight the drop zone, and hand the DragEvent
// off to the page via `onsystemdrop`. The page walks the entries
// (webkitGetAsEntry / DataTransferItemList) and drives the upload.
//
// * `enableSystemDrop = false` (default): the page has no upload
// path. Still `preventDefault` so the browser doesn't navigate
// away, but instead of forwarding, fire a `ui.notify()` that
// points the user at `/files` — this restores the legacy vanilla
// frontend's "wrong drop zone" behaviour so users don't wonder
// why their drag was silently ignored.
//
// Row-scoped drops (dragging an in-app row onto a folder row / the
// breadcrumb) are handled by the existing `onitemdrop` hooks and use
// a private `application/x-oxi-item` MIME so the `Files` type check
// below never matches them.
let systemDropOver = $state(false);
function isSystemDrag(e: DragEvent): boolean {
return !!e.dataTransfer?.types?.includes('Files');
}
function onSystemDragEnter(e: DragEvent) {
if (!isSystemDrag(e)) return;
e.preventDefault();
systemDropOver = true;
}
function onSystemDragOver(e: DragEvent) {
if (!isSystemDrag(e)) return;
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = enableSystemDrop ? 'copy' : 'none';
}
function onSystemDragLeave(e: DragEvent) {
if (!isSystemDrag(e)) return;
systemDropOver = false;
}
function onSystemDrop(e: DragEvent) {
if (!isSystemDrag(e)) return;
e.preventDefault();
systemDropOver = false;
if (enableSystemDrop && onsystemdrop) {
onsystemdrop(e);
} else if (!enableSystemDrop) {
ui.notify(
t(
'resource_list.wrong_drop_zone_msg',
'Uploads only work in Files — open the Files section and drop there.'
),
'warning',
6000
);
}
}
</script>
{#snippet row(item: FileItem | FolderItem)}
@@ -644,6 +759,7 @@
{/if}
</span>
<span class="name-cell__text">{item.name}</span>
{#if rowBadge}<span class="name-cell__badge">{@render rowBadge(item, ctx)}</span>{/if}
</div>
{#if showOwner}
<div class="owner-cell">
@@ -687,50 +803,82 @@
}}><Icon name={isFav ? 'star' : 'star-outline'} /></button
>
{/if}
{#if actions}
<div class="action-cell">{@render actions(item)}</div>
{#if itemActions}
<div class="action-cell">{@render itemActions(item)}</div>
{/if}
</div>
{/snippet}
<!--
`.rl-root` is the drop-target boundary for OS file drops. Every
descendant listens through here — dragging in from the desktop over
the sticky header or the content area both hit the same handler and
either forward to the page's upload code (files browser) or fire the
"wrong section" toast (everywhere else).
svelte-ignore a11y_no_static_element_interactions — the drop handlers
are pointer-only fallbacks; keyboard users don't have a "drop a
file" action, and the content inside is separately keyboard-nav'd.
-->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="rl-root"
class:rl-root--drop-over={systemDropOver && enableSystemDrop}
ondragenter={onSystemDragEnter}
ondragover={onSystemDragOver}
ondragleave={onSystemDragLeave}
ondrop={onSystemDrop}
>
<div class="page-sticky-header">
<h1 class="page-title">{title}</h1>
<ListToolbar
groups={groupBys}
{groupBy}
{reversed}
ongroup={selectGroup}
ondirection={toggleDirection}
{showViewToggle}
{showDotfileToggle}
>
{#if breadcrumb}
<div class="rl-breadcrumb">{@render breadcrumb()}</div>
{/if}
<ActionBar>
{#snippet start()}
<div class="action-buttons">{@render toolbar?.()}</div>
{/snippet}
</ListToolbar>
</div>
{#if selectable && selected.size > 0 && batchToolbar}
<div
class="rl-batch"
role="region"
aria-label={t('files.selection', 'Selection')}
data-testid="resource-list-batch-toolbar"
>
<div class="action-buttons">
<!--
The action-bar left cluster has two states:
1. `batchActions` — when the user has selected items, the
page's batch buttons (Move / Delete / Restore / …)
replace the default cluster, prefixed with a "clear
selection" close button + count label so the batch is
dismissable without unchecking every row by hand.
2. `actions` — the page's default cluster
(Upload / New folder / Empty trash / Clear recent).
-->
{#if selectable && selected.size > 0 && batchActions}
<button
class="rl-batch__close"
title={t('common.clear', 'Clear')}
class="rl-batch-close"
title={t('common.clear', 'Clear selection')}
aria-label={t('common.clear', 'Clear selection')}
data-testid="resource-list-batch-close-btn"
onclick={clearSelection}
>
<Icon name="times" />
</button>
<span class="rl-batch__count"
<span class="rl-batch-count"
>{t('files.selected_count', { count: selected.size }, '{{count}} selected')}</span
>
<div class="rl-batch__actions">{@render batchToolbar(selectedItems)}</div>
</div>
{@render batchActions(selectedItems)}
{:else if actions}
{@render actions()}
{/if}
</div>
{/snippet}
{#snippet end()}
<DisplayModeControls
groups={groupBys}
{groupBy}
{reversed}
ongroup={selectGroup}
ondirection={toggleDirection}
showViewMode={showViewToggle}
{showDotfileToggle}
/>
{/snippet}
</ActionBar>
</div>
{#if error}
<EmptyState icon="exclamation-circle" title={error} error />
@@ -822,6 +970,7 @@
<div bind:this={sentinel} class="rl-sentinel" aria-hidden="true"></div>
</div>
{/if}
</div><!-- /.rl-root -->
{#snippet listHeader()}
<div class="list-header">
@@ -842,7 +991,7 @@
{#if showType}<div>{t('files.col_type', 'Type')}</div>{/if}
{#if showSize}<div>{t('files.col_size', 'Size')}</div>{/if}
{#if showDate}<div>{dateLabel ?? t('files.col_modified', 'Date')}</div>{/if}
{#if onfavorite || actions}<div></div>{/if}
{#if onfavorite || itemActions}<div></div>{/if}
</div>
{/snippet}
@@ -889,19 +1038,47 @@
width: 100%;
}
/* ── Batch toolbar ── */
.rl-batch {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-4);
margin-bottom: var(--space-3);
background: var(--color-accent-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
/* ── OS-drop wrapper ──
`.rl-root` catches drops that miss a specific in-app drop target
(row → folder). Its highlight fires ONLY when
`enableSystemDrop && dragging` — the "wrong drop zone" toast path
deliberately leaves the surface unhighlighted so users don't get a
false accept cue. */
.rl-root {
position: relative;
}
.rl-batch__close {
.rl-root--drop-over::after {
content: '';
position: absolute;
inset: 0;
border: 2px dashed var(--color-accent);
border-radius: var(--radius-md);
pointer-events: none;
}
/* ── Breadcrumb strip inside the sticky header ── */
.rl-breadcrumb {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
min-height: 28px;
}
/* ── Row badge (trash expiration, etc.) ── */
.name-cell__badge {
display: inline-flex;
align-items: center;
margin-left: var(--space-2);
}
/* ── Selection controls inside the action bar ──
Replaces the deprecated `.rl-batch` floating strip. When items
are selected, the close button + count sit before the page's
`batchActions` snippet inside `.action-buttons`, so the whole
cluster reads as one row of the action bar. */
.rl-batch-close {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -914,22 +1091,15 @@
cursor: pointer;
}
.rl-batch__close:hover {
.rl-batch-close:hover {
background: var(--color-bg-hover);
}
.rl-batch__count {
.rl-batch-count {
font-weight: var(--weight-semibold);
color: var(--color-text);
}
.rl-batch__actions {
display: flex;
align-items: center;
gap: var(--space-2);
margin-left: auto;
}
/* ── Selection column ── */
.select-cell {
display: flex;
+3 -2
View File
@@ -254,7 +254,7 @@
];
// ── Selection + batch ─────────────────────────────────────────────────────
// Selected items arrive via the batchToolbar snippet param —
// Selected items arrive via the batchActions snippet param —
// ResourceList already derives them (O(selection), not O(N)); a
// host-side `items.filter(...)` shadow would re-run a second full scan
// per selection toggle, and its id mirror is unnecessary (the component
@@ -307,6 +307,7 @@
onopen={open}
onfavorite={unfavorite}
showOwner
showPath
selectable
{contextActions}
{groupBys}
@@ -317,7 +318,7 @@
load(true, orderBy, rev);
}}
>
{#snippet batchToolbar(sel)}
{#snippet batchActions(sel)}
<Button
icon="download"
data-testid="favorites-batch-download-btn"
+4 -3
View File
@@ -292,7 +292,7 @@
];
// ── Selection + batch ─────────────────────────────────────────────────────
// Selected items arrive via the batchToolbar snippet param —
// Selected items arrive via the batchActions snippet param —
// ResourceList already derives them (O(selection), not O(N)); a
// host-side `items.filter(...)` shadow would re-run a second full scan
// per selection toggle, and its id mirror is unnecessary (the component
@@ -356,6 +356,7 @@
onopen={open}
onfavorite={toggleFavorite}
showOwner
showPath
showDotfileToggle
selectable
{contextActions}
@@ -367,14 +368,14 @@
load(true, orderBy, rev);
}}
>
{#snippet toolbar()}
{#snippet actions()}
{#if items.length > 0}
<Button icon="broom" data-testid="recent-clear-btn" onclick={clearAll}
>{t('recent.clear', 'Clear recent')}</Button
>
{/if}
{/snippet}
{#snippet batchToolbar(sel)}
{#snippet batchActions(sel)}
<Button
icon="download"
data-testid="recent-batch-download-btn"
+30 -4
View File
@@ -13,6 +13,7 @@
restoreTrashItem
} from '$lib/api/endpoints/trash';
import { dateBucket, sizeBucket, typeLabel } from '$lib/api/endpoints/favorites';
import { formatDate } from '$lib/utils/display';
import type { Drive, FileItem, FolderItem, TrashResourceItem } from '$lib/api/types';
import Icon from '$lib/icons/Icon.svelte';
import ResourceList, {
@@ -268,8 +269,10 @@
emptyText={t('trash.empty_state', 'Trash is empty')}
hasMore={!!cursor}
onloadmore={() => load(false, orderByForGroup())}
selectable
showPath
pathLabel={t('trash.original_location', 'Original location')}
dateLabel={t('trash.remaining', 'Remaining')}
dateLabel={t('trash.expires_at', 'Expires at')}
{groupBys}
bind:groupBy
bind:reversed
@@ -278,7 +281,7 @@
load(true, orderBy, rev);
}}
>
{#snippet toolbar()}
{#snippet actions()}
{#if items.length > 0}
<button class="btn btn-danger" data-testid="trash-empty-btn" onclick={purgeAll}>
<Icon name="trash" />
@@ -286,13 +289,36 @@
</button>
{/if}
{/snippet}
{#snippet dateCell(_item, ctx)}
{#snippet batchActions(sel)}
<button
class="btn-action"
data-testid="trash-batch-restore-btn"
title={t('trash.restore', 'Restore')}
onclick={() => sel.forEach(restore)}
>
<Icon name="undo" />
{t('trash.restore', 'Restore')}
</button>
<button
class="btn-action btn-action--delete"
data-testid="trash-batch-delete-btn"
title={t('trash.delete', 'Delete permanently')}
onclick={() => sel.forEach(purge)}
>
<Icon name="trash" />
{t('trash.delete', 'Delete permanently')}
</button>
{/snippet}
{#snippet rowBadge(_item, ctx)}
{@const chip = expiryChip(ctx?.date)}
<span class="expiry-chip expiry-chip--{chip.tier}">
<Icon name={chip.icon} class="expiry-chip__icon" />
{chip.label}
</span>
{/snippet}
{#snippet dateCell(_item, ctx)}
{formatDate(ctx?.date)}
{/snippet}
{#snippet bucketAction(bucketKey: string)}
{#if showPerDriveEmpty}
{@const driveId = driveIdFromBucketKey(bucketKey)}
@@ -310,7 +336,7 @@
{/if}
{/if}
{/snippet}
{#snippet actions(item)}
{#snippet itemActions(item)}
<button
class="btn-action"
data-testid={`trash-restore-btn-${item.id}`}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "وقت الحذف"
},
"delete": "حذف نهائياً",
"empty_action": "تفريغ سلة المهملات"
"empty_action": "تفريغ سلة المهملات",
"expires_at": "ينتهي في"
},
"daysRemaining": {
"expired": "منتهية الصلاحية",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "لا يوجد أعضاء",
"member_count": "{{n}} أعضاء"
},
"resource_list": {
"location": "الموقع",
"wrong_drop_zone_msg": "الرفع يعمل فقط في قسم الملفات — افتح قسم الملفات وأفلت العناصر هناك."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "Löschzeit"
},
"delete": "Endgültig löschen",
"empty_action": "Papierkorb leeren"
"empty_action": "Papierkorb leeren",
"expires_at": "Läuft ab"
},
"daysRemaining": {
"expired": "Abgelaufen",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "Keine Mitglieder",
"member_count": "{{n}} Mitglieder"
},
"resource_list": {
"location": "Speicherort",
"wrong_drop_zone_msg": "Uploads funktionieren nur in Dateien — öffne den Bereich Dateien und lege die Elemente dort ab."
}
}
+6 -1
View File
@@ -618,7 +618,8 @@
"confirm_empty": "Empty the trash? This cannot be undone.",
"delete": "Delete permanently",
"empty_action": "Empty trash",
"restored": "Restored"
"restored": "Restored",
"expires_at": "Expires at"
},
"daysRemaining": {
"expired": "Expired",
@@ -1663,5 +1664,9 @@
"group": {
"members_empty": "No members",
"member_count": "{{n}} members"
},
"resource_list": {
"location": "Location",
"wrong_drop_zone_msg": "Uploads only work in Files — open the Files section and drop there."
}
}
+6 -1
View File
@@ -466,7 +466,8 @@
"trashed_time": "Fecha de eliminación"
},
"delete": "Eliminar permanentemente",
"empty_action": "Vaciar papelera"
"empty_action": "Vaciar papelera",
"expires_at": "Expira"
},
"daysRemaining": {
"expired": "Caducado",
@@ -1280,5 +1281,9 @@
"group": {
"members_empty": "Sin miembros",
"member_count": "{{n}} miembros"
},
"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."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "زمان حذف"
},
"delete": "حذف دائمی",
"empty_action": "Empty trash"
"empty_action": "Empty trash",
"expires_at": "انقضا در"
},
"daysRemaining": {
"expired": "منقضی شده",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "بدون عضو",
"member_count": "{{n}} عضو"
},
"resource_list": {
"location": "مکان",
"wrong_drop_zone_msg": "بارگذاری فقط در بخش پرونده‌ها کار می‌کند — بخش پرونده‌ها را باز کنید و آنجا رها کنید."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "Date de suppression"
},
"delete": "Supprimer définitivement",
"empty_action": "Vider la corbeille"
"empty_action": "Vider la corbeille",
"expires_at": "Expiration"
},
"daysRemaining": {
"expired": "Expiré",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "Aucun membre",
"member_count": "{{n}} membres"
},
"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."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "हटाने का समय"
},
"delete": "स्थायी रूप से हटाएँ",
"empty_action": "रद्दी खाली करें"
"empty_action": "रद्दी खाली करें",
"expires_at": "समाप्ति"
},
"daysRemaining": {
"expired": "समाप्त",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "कोई सदस्य नहीं",
"member_count": "{{n}} सदस्य"
},
"resource_list": {
"location": "स्थान",
"wrong_drop_zone_msg": "अपलोड केवल फ़ाइलें अनुभाग में काम करता है — फ़ाइलें अनुभाग खोलें और वहीं छोड़ें।"
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "Data di eliminazione"
},
"delete": "Elimina definitivamente",
"empty_action": "Svuota il cestino"
"empty_action": "Svuota il cestino",
"expires_at": "Scadenza"
},
"daysRemaining": {
"expired": "Scaduto",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "Nessun membro",
"member_count": "{{n}} membri"
},
"resource_list": {
"location": "Posizione",
"wrong_drop_zone_msg": "I caricamenti funzionano solo in File — apri la sezione File e trascina lì gli elementi."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "削除日時"
},
"delete": "完全に削除",
"empty_action": "ゴミ箱を空にする"
"empty_action": "ゴミ箱を空にする",
"expires_at": "期限"
},
"daysRemaining": {
"expired": "期限切れ",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "メンバーなし",
"member_count": "{{n}} 人のメンバー"
},
"resource_list": {
"location": "場所",
"wrong_drop_zone_msg": "アップロードはファイル セクションでのみ機能します。ファイル セクションを開いてそこにドロップしてください。"
}
}
+6 -1
View File
@@ -583,7 +583,8 @@
"empty_action": "휴지통 비우기",
"confirm_delete": "이 항목을 영구적으로 삭제하시겠습니까? 되돌릴 수 없습니다.",
"confirm_empty": "휴지통을 비우시겠습니까? 되돌릴 수 없습니다.",
"restored": "복원됨"
"restored": "복원됨",
"expires_at": "만료일"
},
"daysRemaining": {
"expired": "만료됨",
@@ -1659,5 +1660,9 @@
"group": {
"members_empty": "구성원 없음",
"member_count": "구성원 {{n}}명"
},
"resource_list": {
"location": "위치",
"wrong_drop_zone_msg": "업로드는 파일 섹션에서만 작동합니다. 파일 섹션을 열고 거기에 놓아 주세요."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "Verwijderd op"
},
"delete": "Permanent verwijderen",
"empty_action": "Prullenbak legen"
"empty_action": "Prullenbak legen",
"expires_at": "Verloopt op"
},
"daysRemaining": {
"expired": "Verlopen",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "Geen leden",
"member_count": "{{n}} leden"
},
"resource_list": {
"location": "Locatie",
"wrong_drop_zone_msg": "Uploaden werkt alleen in Bestanden — open het onderdeel Bestanden en zet ze daar neer."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "Czas usunięcia"
},
"delete": "Usuń trwale",
"empty_action": "Opróżnij kosz"
"empty_action": "Opróżnij kosz",
"expires_at": "Wygasa"
},
"daysRemaining": {
"expired": "Wygasł",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "Brak członków",
"member_count": "{{n}} członków"
},
"resource_list": {
"location": "Lokalizacja",
"wrong_drop_zone_msg": "Przesyłanie działa tylko w Plikach — otwórz sekcję Pliki i upuść tam pliki."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "Data de exclusão"
},
"delete": "Excluir permanentemente",
"empty_action": "Esvaziar lixeira"
"empty_action": "Esvaziar lixeira",
"expires_at": "Expira em"
},
"daysRemaining": {
"expired": "Expirado",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "Sem membros",
"member_count": "{{n}} membros"
},
"resource_list": {
"location": "Localização",
"wrong_drop_zone_msg": "Os envios só funcionam em Ficheiros — abra a secção Ficheiros e largue-os aí."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "Время удаления"
},
"delete": "Удалить навсегда",
"empty_action": "Очистить корзину"
"empty_action": "Очистить корзину",
"expires_at": "Истекает"
},
"daysRemaining": {
"expired": "Истёк",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "Нет участников",
"member_count": "{{n}} участников"
},
"resource_list": {
"location": "Расположение",
"wrong_drop_zone_msg": "Загрузки работают только в разделе Файлы — откройте раздел Файлы и перетащите туда."
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "刪除時間"
},
"delete": "永久刪除",
"empty_action": "清空回收站"
"empty_action": "清空回收站",
"expires_at": "到期時間"
},
"daysRemaining": {
"expired": "已過期",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "無成員",
"member_count": "{{n}} 位成員"
},
"resource_list": {
"location": "位置",
"wrong_drop_zone_msg": "上傳僅在「檔案」中有效 — 請開啟檔案區並在該處拖放。"
}
}
+6 -1
View File
@@ -461,7 +461,8 @@
"trashed_time": "删除时间"
},
"delete": "永久删除",
"empty_action": "Empty trash"
"empty_action": "Empty trash",
"expires_at": "到期时间"
},
"daysRemaining": {
"expired": "已过期",
@@ -1265,5 +1266,9 @@
"group": {
"members_empty": "无成员",
"member_count": "{{n}} 名成员"
},
"resource_list": {
"location": "位置",
"wrong_drop_zone_msg": "上传仅在 “文件” 中生效 — 请打开文件区并在那里拖放。"
}
}