diff --git a/frontend/src/lib/components/ResourceList.svelte b/frontend/src/lib/components/ResourceList.svelte index b5259066..ba645ee7 100644 --- a/frontend/src/lib/components/ResourceList.svelte +++ b/frontend/src/lib/components/ResourceList.svelte @@ -1239,14 +1239,16 @@
{#if listHeaderOverride}{@render listHeaderOverride()}{:else}{@render listHeader()}{/if} {#each sections as section (section.key)} -
- {section.label} - {#if bucketAction} - - {@render bucketAction(section.key)} - - {/if} -
+ {#if section.label} +
+ {section.label} + {#if bucketAction} + + {@render bucketAction(section.key)} + + {/if} +
+ {/if} e.id} {row} /> @@ -1262,14 +1264,16 @@ (benches/ROUND13.md §V1). -->
{#each sections as section (section.key)} -
- {section.label} - {#if bucketAction} - - {@render bucketAction(section.key)} - - {/if} -
+ {#if section.label} +
+ {section.label} + {#if bucketAction} + + {@render bucketAction(section.key)} + + {/if} +
+ {/if} (); + // Dotfile hide filter is now applied inside `rlItems` (below) directly // on the server-ordered accumulator, so a single filter pass feeds // ResourceList. Selection / batch ops iterate ResourceList's own @@ -371,6 +391,36 @@ await load(true); } + /** + * Reload + populate the "new elements" swimlane with anything that + * appeared on page 1 after the mutation. + * + * Called from mutation paths that ADD items (upload / dropped tree / + * create-folder). Renames, deletes, moves use plain `reload()` + * — nothing new to hoist. + */ + async function reloadAndTrackNew(): Promise { + const before = new SvelteSet(); + for (const it of orderedItems) before.add(it.id); + await reload(); + // `reload()` resets `pageCursor` + fetches page 1 fresh, so + // `orderedItems` is now the freshly-loaded page. Every id that + // wasn't there before this reload joins the swimlane. + newlyAdded.clear(); + for (const it of orderedItems) if (!before.has(it.id)) newlyAdded.add(it.id); + // Scroll the page back to the top so the freshly-hoisted "New + // elements" swimlane is visible without the user having to hunt + // for it — the whole point of the swimlane is to confirm "your + // upload landed". Only fires when we actually detected new items, + // so a bare reload doesn't yank the user's scroll position. + // Smooth scroll for the visual continuity — instant would feel + // like the page reloaded. `scrollTo` at (0, 0) is a no-op if + // the user was already at the top; no jitter cost. + if (newlyAdded.size > 0 && typeof window !== 'undefined') { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + } + function openFolder(folder: FolderItem) { goto(resolve(`/files/${[...pathSegments, folder.id].join('/')}`)); } @@ -384,7 +434,7 @@ if (!name) return; try { await createFolder(name, currentId); - await reload(); + await reloadAndTrackNew(); // Vanish-warning: user just made a `.folder` and it's // already hidden by their preference — otherwise the new // folder would appear to have not been created. Third hook @@ -668,7 +718,7 @@ } else { finishUpload(nid, 0, 0, 0, skipped.length); } - await reload(); + await reloadAndTrackNew(); // Storage usage changed server-side — pull the fresh figure so the // "Almacenamiento" bar moves off its login value instead of 0%. void session.refresh(); @@ -1375,7 +1425,7 @@ const { savedBytes, failures } = await uploadAll(items, nid, label); finishUpload(nid, savedBytes, failures, total, skipped.length); - await reload(); + await reloadAndTrackNew(); void session.refresh(); } catch (err) { ui.finishProgress(nid, errorMessage(err), 'error'); @@ -1418,7 +1468,25 @@ // necessary. Under order_by=name/type/size the server puts folders // first then files; under modified_at/created_at they interleave — // preserving the accumulator order is what surfaces that correctly. - const rlItems = $derived(filterDotfiles(orderedItems, preferences.hideDotfiles)); + // + // Hoist step: items in `newlyAdded` (populated by `reloadAndTrackNew` + // after an upload / create / dropped tree) are pulled OUT of their + // natural-order position and PREPENDED to the list, so the + // "__new__" bucket rendered by the composed groupBy below appears + // at the top of the swimlanes regardless of what sort/group the + // user has active. First-appearance bucketing in + // `buildResourceSections` keys off the item order in the input list. + const rlItems = $derived.by>(() => { + const filtered = filterDotfiles(orderedItems, preferences.hideDotfiles); + if (newlyAdded.size === 0) return filtered; + const hoisted: Array = []; + const rest: Array = []; + for (const it of filtered) { + if (newlyAdded.has(it.id)) hoisted.push(it); + else rest.push(it); + } + return [...hoisted, ...rest]; + }); // Group-by state (bound to ). Kept as a `string` prop // value; the current `sortField` mirrors from the picked group's @@ -1429,40 +1497,75 @@ // modifiedAt / createdAt). The `orderBy` values are what the // GROUP_BYS toolbar emits, so 's onreload gets the // legacy `sortField` name and can drive the same sort path. + // + // Every dimension composes a `__new__` branch on top of its natural + // `bucketOf` so that whenever the transient "new elements" swimlane + // is active, hoisted items get their own bucket-first-in-order + // regardless of the user's chosen group. On the default `''` (flat) + // dimension the wrapped `bucketOf` returns the empty string for + // non-new items — that renders as one unlabeled section (header + // suppressed by ResourceList when `label === ''`), preserving the + // current flat-list look with just the "New elements" header on + // top. `labelForNew` renders the localised header. + const NEW_KEY = '__new__'; + const labelForNew = $derived(t('files.new_elements', 'New elements')); + const wrapNew = + (inner?: (item: T) => string | null) => + (item: T): string | null => { + if (newlyAdded.has(item.id)) return NEW_KEY; + return inner ? inner(item) : ''; + }; + const wrapLabel = + (inner?: (key: string) => string) => + (key: string): string => { + if (key === NEW_KEY) return labelForNew; + return inner ? inner(key) : key; + }; const rlGroupBys = $derived([ - { key: '', label: t('files.name', 'Name'), orderBy: 'name', icon: 'arrow-up-a-z' }, + { + key: '', + label: t('files.name', 'Name'), + orderBy: 'name', + icon: 'arrow-up-a-z', + // Only synthesize a bucketOf when the swimlane is active; when + // no new items exist we want the plain flat-list rendering + // (no bucketing pass at all). + bucketOf: newlyAdded.size > 0 ? wrapNew() : undefined, + labelOf: newlyAdded.size > 0 ? wrapLabel() : undefined + }, { key: 'type', label: t('groupby.type', 'Type'), orderBy: 'type', icon: 'layer-group', - bucketOf: (item) => - isFile(item) ? typeLabel(item.category) : t('files.file_types.folder', 'Folders'), - labelOf: (k) => k + bucketOf: wrapNew((item) => + isFile(item) ? typeLabel(item.category) : t('files.file_types.folder', 'Folders') + ), + labelOf: wrapLabel((k) => k) }, { key: 'size', label: t('groupby.size', 'Size'), orderBy: 'size', icon: 'layer-group', - bucketOf: (item) => (isFile(item) ? sizeBucket(item.size ?? 0) : sizeBucket(-1)), - labelOf: (k) => k + bucketOf: wrapNew((item) => (isFile(item) ? sizeBucket(item.size ?? 0) : sizeBucket(-1))), + labelOf: wrapLabel((k) => k) }, { key: 'modifiedAt', label: t('groupby.modifiedAt', 'Modified date'), orderBy: 'modified_at', icon: 'layer-group', - bucketOf: (item) => dateBucket(item.modified_at), - labelOf: (k) => k + bucketOf: wrapNew((item) => dateBucket(item.modified_at)), + labelOf: wrapLabel((k) => k) }, { key: 'createdAt', label: t('groupby.createdAt', 'Created date'), orderBy: 'created_at', icon: 'layer-group', - bucketOf: (item) => dateBucket(item.created_at), - labelOf: (k) => k + bucketOf: wrapNew((item) => dateBucket(item.created_at)), + labelOf: wrapLabel((k) => k) } ]); @@ -1533,6 +1636,11 @@ void sortField; void reversed; untrack(() => { + // Route/sort change → drop the transient "new elements" + // swimlane. It's a per-folder confirmation of "here's what + // you just added"; carrying it across folders would surface + // stale ids that don't belong to the new listing. + newlyAdded.clear(); void load(true); }); }); diff --git a/frontend/static/locales/ar.json b/frontend/static/locales/ar.json index f1dc557f..00fcd333 100644 --- a/frontend/static/locales/ar.json +++ b/frontend/static/locales/ar.json @@ -382,7 +382,8 @@ "col_added": "أضيف", "col_created_by": "أنشئ بواسطة", "col_opened": "افتُح", - "col_path": "الموقع" + "col_path": "الموقع", + "new_elements": "عناصر جديدة" }, "dialogs": { "rename_folder": "إعادة تسمية المجلد", diff --git a/frontend/static/locales/de.json b/frontend/static/locales/de.json index 9a82bf7e..5b3b6915 100644 --- a/frontend/static/locales/de.json +++ b/frontend/static/locales/de.json @@ -382,7 +382,8 @@ "col_added": "Hinzugefügt", "col_created_by": "Erstellt von", "col_opened": "Geöffnet", - "col_path": "Speicherort" + "col_path": "Speicherort", + "new_elements": "Neue Elemente" }, "dialogs": { "rename_folder": "Ordner umbenennen", diff --git a/frontend/static/locales/en.json b/frontend/static/locales/en.json index 259bb7a8..3438c366 100644 --- a/frontend/static/locales/en.json +++ b/frontend/static/locales/en.json @@ -504,6 +504,7 @@ "moved": "Moved", "new_folder": "New folder", "new_folder_prompt": "New folder name", + "new_elements": "New elements", "no_home": "No home folder available.", "no_preview": "No preview available for this file type.", "no_subfolders": "No subfolders here.", diff --git a/frontend/static/locales/es.json b/frontend/static/locales/es.json index 91879ff9..5e959f44 100644 --- a/frontend/static/locales/es.json +++ b/frontend/static/locales/es.json @@ -387,7 +387,8 @@ "col_added": "Añadido", "col_created_by": "Creado por", "col_opened": "Abierto", - "col_path": "Ubicación" + "col_path": "Ubicación", + "new_elements": "Nuevos elementos" }, "dialogs": { "rename_folder": "Renombrar carpeta", diff --git a/frontend/static/locales/fa.json b/frontend/static/locales/fa.json index aa41786b..8a4f90d1 100644 --- a/frontend/static/locales/fa.json +++ b/frontend/static/locales/fa.json @@ -382,7 +382,8 @@ "col_added": "افزوده شده", "col_created_by": "ایجاد شده توسط", "col_opened": "باز شده", - "col_path": "مکان" + "col_path": "مکان", + "new_elements": "موارد جدید" }, "dialogs": { "rename_folder": "تغییر نام پوشه", diff --git a/frontend/static/locales/fr.json b/frontend/static/locales/fr.json index 9619f3f1..c285f539 100644 --- a/frontend/static/locales/fr.json +++ b/frontend/static/locales/fr.json @@ -382,7 +382,8 @@ "col_added": "Ajouté", "col_created_by": "Créé par", "col_opened": "Ouvert", - "col_path": "Emplacement" + "col_path": "Emplacement", + "new_elements": "Nouveaux éléments" }, "dialogs": { "rename_folder": "Renommer le dossier", diff --git a/frontend/static/locales/hi.json b/frontend/static/locales/hi.json index 12068545..e63f537e 100644 --- a/frontend/static/locales/hi.json +++ b/frontend/static/locales/hi.json @@ -382,7 +382,8 @@ "col_added": "जोड़ा गया", "col_created_by": "द्वारा बनाया गया", "col_opened": "खोला गया", - "col_path": "स्थान" + "col_path": "स्थान", + "new_elements": "नए तत्व" }, "dialogs": { "rename_folder": "फ़ोल्डर का नाम बदलें", diff --git a/frontend/static/locales/it.json b/frontend/static/locales/it.json index 888650c3..6a8a0335 100644 --- a/frontend/static/locales/it.json +++ b/frontend/static/locales/it.json @@ -382,7 +382,8 @@ "col_added": "Aggiunto", "col_created_by": "Creato da", "col_opened": "Aperto", - "col_path": "Posizione" + "col_path": "Posizione", + "new_elements": "Nuovi elementi" }, "dialogs": { "rename_folder": "Rinomina cartella", diff --git a/frontend/static/locales/ja.json b/frontend/static/locales/ja.json index fcf883eb..adbb1125 100644 --- a/frontend/static/locales/ja.json +++ b/frontend/static/locales/ja.json @@ -382,7 +382,8 @@ "col_added": "追加日", "col_created_by": "作成者", "col_opened": "アクセス日時", - "col_path": "場所" + "col_path": "場所", + "new_elements": "新しいアイテム" }, "dialogs": { "rename_folder": "フォルダ名を変更", diff --git a/frontend/static/locales/ko.json b/frontend/static/locales/ko.json index 90b25a0f..05939621 100644 --- a/frontend/static/locales/ko.json +++ b/frontend/static/locales/ko.json @@ -471,6 +471,7 @@ "move_title": "\"{{name}}\" 이동", "moved": "이동됨", "new_folder_prompt": "새 폴더 이름", + "new_elements": "새 항목", "no_home": "홈 폴더를 사용할 수 없습니다.", "no_preview": "이 파일 형식은 미리보기를 지원하지 않습니다.", "no_subfolders": "하위 폴더가 없습니다.", diff --git a/frontend/static/locales/nl.json b/frontend/static/locales/nl.json index 6cea32c5..916950f7 100644 --- a/frontend/static/locales/nl.json +++ b/frontend/static/locales/nl.json @@ -382,7 +382,8 @@ "col_added": "Toegevoegd", "col_created_by": "Gemaakt door", "col_opened": "Geopend", - "col_path": "Locatie" + "col_path": "Locatie", + "new_elements": "Nieuwe items" }, "dialogs": { "rename_folder": "Map hernoemen", diff --git a/frontend/static/locales/pl.json b/frontend/static/locales/pl.json index 1e8c93a4..d8fc732d 100644 --- a/frontend/static/locales/pl.json +++ b/frontend/static/locales/pl.json @@ -382,7 +382,8 @@ "col_added": "Dodano", "col_created_by": "Utworzone przez", "col_opened": "Otwarte", - "col_path": "Lokalizacja" + "col_path": "Lokalizacja", + "new_elements": "Nowe elementy" }, "dialogs": { "rename_folder": "Zmień nazwę folderu", diff --git a/frontend/static/locales/pt.json b/frontend/static/locales/pt.json index ae8fb450..c67388a8 100644 --- a/frontend/static/locales/pt.json +++ b/frontend/static/locales/pt.json @@ -382,7 +382,8 @@ "col_added": "Adicionado", "col_created_by": "Criado por", "col_opened": "Aberto", - "col_path": "Localização" + "col_path": "Localização", + "new_elements": "Novos itens" }, "dialogs": { "rename_folder": "Renomear pasta", diff --git a/frontend/static/locales/ru.json b/frontend/static/locales/ru.json index 991e0ae3..ac182e02 100644 --- a/frontend/static/locales/ru.json +++ b/frontend/static/locales/ru.json @@ -382,7 +382,8 @@ "col_added": "Добавлено", "col_created_by": "Создано", "col_opened": "Открыт", - "col_path": "Расположение" + "col_path": "Расположение", + "new_elements": "Новые элементы" }, "dialogs": { "rename_folder": "Переименовать папку", diff --git a/frontend/static/locales/zh-TW.json b/frontend/static/locales/zh-TW.json index 5ba9fb19..6e0dc07e 100644 --- a/frontend/static/locales/zh-TW.json +++ b/frontend/static/locales/zh-TW.json @@ -382,7 +382,8 @@ "col_added": "新增日期", "col_created_by": "建立者", "col_opened": "開啟日期", - "col_path": "位置" + "col_path": "位置", + "new_elements": "新項目" }, "dialogs": { "rename_folder": "重新命名資料夾", diff --git a/frontend/static/locales/zh.json b/frontend/static/locales/zh.json index 21a9ab33..48f1dca7 100644 --- a/frontend/static/locales/zh.json +++ b/frontend/static/locales/zh.json @@ -382,7 +382,8 @@ "col_added": "添加日期", "col_created_by": "创建者", "col_opened": "打开日期", - "col_path": "位置" + "col_path": "位置", + "new_elements": "新元素" }, "dialogs": { "rename_folder": "重命名文件夹",