Dashboard files: clicking an open file returns to its window
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s

In dashboard mode there is no taskbar, so a minimized app window's only
switcher was its Apps-tab tile — invisible from the Files tab. Opening a
file, minimizing, and clicking the file again launched a second instance
of the app, stranding the first (and any unsaved edits) somewhere
unreachable.

Make the file row itself the switcher:

- launch_app stamps the opened file's uid on the window (data-file_uid;
  the signature's uid wins so shortcuts resolve to their target)
- open_item (dashboard mode) restores/focuses an existing window that
  has the file open instead of launching a duplicate, mirroring the
  Apps-tab tile's single-instance behavior — keyed by file, not app, so
  opening a different file still gets its own instance
- re-clicks while a launch's fetches are still in flight are swallowed
  (same idea as TabApps._launchingApps, keyed by file uid, TTL'd so a
  failed launch can't swallow clicks forever)
- dashboard file opens now default to maximized, so they get the same
  headless full-tab chrome + control drawer as tile launches instead of
  a floating titlebar window
- rows show a dot while their file is open in a (possibly minimized)
  window — under the name in grid view, inline after it in list view —
  driven by the existing dashboard-app-windows-changed event

Desktop mode is untouched: the reuse branch and maximized default are
gated on is_dashboard_mode, and opening the same file twice there still
creates two windows as before.
This commit is contained in:
jelveh
2026-07-29 19:03:33 -07:00
parent 0e1f617401
commit 32e417fe79
5 changed files with 116 additions and 4 deletions
+31
View File
@@ -564,6 +564,14 @@ const TabFiles = {
// Setup keyboard shortcuts
this.setupKeyboardShortcuts();
// Rows double as the switcher for app windows opened from this tab
// (open_item restores a file's existing window instead of launching
// a duplicate): a dot marks files that are open in a — possibly
// minimized — app window. UIWindow fires this on window open/close.
document.addEventListener('dashboard-app-windows-changed', () => {
this.updateOpenFileDots();
});
// Refresh current directory when the user returns to this browser tab
document.addEventListener('visibilitychange', async () => {
if ( document.visibilityState !== 'visible' || !this.currentPath ) return;
@@ -2145,6 +2153,7 @@ const TabFiles = {
this.applyColumnWidths();
this.updateFooterStats();
this.updateNavButtonStates();
this.updateOpenFileDots();
this.hideSpinner();
this.renderingDirectory = false;
},
@@ -2253,6 +2262,28 @@ const TabFiles = {
this.createItemListeners(row, file);
},
/**
* Toggles the open-file dot on rows whose file is currently open in an
* app window — visible or minimized. Matches each row's uid (shortcuts
* resolve to their target) against the data-file_uid that launch_app
* stamps on app windows. In dashboard mode the row doubles as that
* window's switcher (clicking it restores instead of relaunching — see
* open_item.js), so the dot marks where a click will return, not launch.
*
* @returns {void}
*/
updateOpenFileDots () {
if ( ! this.$el_window ) return;
const open_uids = new Set();
$('.window[data-file_uid]').each(function () {
open_uids.add($(this).attr('data-file_uid'));
});
this.$el_window.find('.files-tab .files .row').each(function () {
const uid = ($(this).attr('data-shortcut_to') || $(this).attr('data-uid') || '').toLowerCase();
$(this).toggleClass('file-is-open', open_uids.has(uid));
});
},
/**
* Attaches event listeners to a file/folder row element.
*
+1
View File
@@ -275,6 +275,7 @@ async function UIWindow (options) {
data-element_uuid="${html_encode(options.element_uuid)}"
data-parent_uuid="${html_encode(options.parent_uuid)}"
${options.parent_instance_id ? `data-parent_instance_id="${options.parent_instance_id}"` : ''}
${options.file_uid ? `data-file_uid="${html_encode(String(options.file_uid).toLowerCase())}"` : ''}
data-id ="${win_id}"
data-iframe_msg_uid ="${html_encode(options.iframe_msg_uid)}"
data-is_dir ="${options.is_dir}"
+20
View File
@@ -1766,6 +1766,26 @@ body.myapps-reordering .myapps-tile {
line-height: 32px;
}
/* Open-file dot: marks rows whose file is open in an app window (visible or
minimized) the row doubles as that window's switcher (clicking it
restores the window; see open_item.js), sibling of the Apps-tab tiles'
running dot. Kept current by updateOpenFileDots in TabFiles.js. */
.dashboard-section-files .files-tab .files .row.file-is-open .item-name-wrapper::after {
content: '';
flex: 0 0 auto;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--dashboard-text-tertiary, #71717a);
}
/* Grid view: the name wrapper is a block, not a flex row center the dot
under the (up to two-line) name, mirroring the tile dot under its icon. */
.dashboard-section-files .files-tab .files.files-grid-view .row.file-is-open .item-name-wrapper::after {
display: block;
margin: 4px auto 0;
}
/* On touch devices, item-name/icon/badges must not be direct event targets
so that pointerdown's isDragHandle check can't match them and accidentally
select items when the user is scrolling. Events pass through to the .row. */
+5
View File
@@ -648,6 +648,11 @@ const launch_app = async (options) => {
width: window_width,
app: options.name,
iframe_credentialless: credentialless,
// The file this instance was launched to open, stamped on the
// window as data-file_uid so open_item can restore this window
// when the same file is opened again (the signature's uid wins:
// it's resolved, e.g. a shortcut's uid becomes its target's).
file_uid: file_signature?.uid ?? options.file_uid,
is_visible: !app_info.background,
is_maximized: options.maximized,
is_fullpage: options.is_fullpage,
+59 -4
View File
@@ -24,6 +24,24 @@ import launch_app from './launch_app.js';
import path from '../lib/path.js';
import item_icon from './item_icon.js';
// Files whose app launch is still in flight, by file uid. Between the click
// and the window's creation there is nothing in the DOM to restore, so a
// re-click during that stretch would mint a duplicate instance — mark the
// launch and swallow re-clicks instead (same idea as TabApps._launchingApps).
// Entries carry a timestamp so a launch that dies without settling (an
// unexpected throw before launch_app is reached) can't swallow clicks forever.
const launching_file_uids = new Map();
const LAUNCH_GUARD_TTL = 15000;
const file_launch_in_flight = (uid) => {
const ts = launching_file_uids.get(uid);
if ( ! ts ) return false;
if ( Date.now() - ts > LAUNCH_GUARD_TTL ) {
launching_file_uids.delete(uid);
return false;
}
return true;
};
const open_item = async function (options) {
let el_item = options.item;
const $el_parent_window = $(el_item).closest('.window');
@@ -35,6 +53,10 @@ const open_item = async function (options) {
const shortcut_to_path = $(el_item).attr('data-shortcut_to_path');
const associated_app_name = $(el_item).attr('data-associated_app_name');
const file_uid = $(el_item).attr('data-uid');
// Normalized identity of the file being opened (shortcuts resolve to
// their target) — matched against the data-file_uid that launch_app
// stamps on app windows.
const target_file_uid = (!is_dir && uid) ? String(uid).toLowerCase() : null;
//----------------------------------------------------------------
// Is this an app shortcut?
@@ -235,16 +257,40 @@ Please try recreating the link.`);
}
}
//----------------------------------------------------------------
// Dashboard: is this file already open in an app window? In
// dashboard mode there is no taskbar — the file's row is where a
// user goes looking for a window they minimized — so restore that
// window (edits intact) instead of minting a second instance.
//----------------------------------------------------------------
else if ( window.is_dashboard_mode && !associated_app_name && target_file_uid
&& ($(`.window[data-file_uid="${html_encode(target_file_uid)}"]`).length || file_launch_in_flight(target_file_uid)) ) {
const $win = $(`.window[data-file_uid="${html_encode(target_file_uid)}"]`).last();
// No window yet means the first click's launch is still in
// flight — swallow the re-click instead of duplicating it.
if ( $win.length ) {
const minimized = $win.attr('data-is_minimized');
if ( minimized === '1' || minimized === 'true' ) {
$win.showWindow();
} else {
$win.focusWindow();
}
}
}
//----------------------------------------------------------------
// Does the user have a preference for this file type?
//----------------------------------------------------------------
else if ( !associated_app_name && !is_dir && window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`] ) {
launch_app({
const launch_promise = launch_app({
name: window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`],
file_path: item_path,
window_title: path.basename(item_path),
maximized: options.maximized,
maximized: options.maximized ?? window.is_dashboard_mode,
file_uid: file_uid,
});
if ( target_file_uid ) {
launching_file_uids.set(target_file_uid, Date.now());
launch_promise.finally(() => launching_file_uids.delete(target_file_uid));
}
}
//----------------------------------------------------------------
// Is there an app associated with this item?
@@ -252,6 +298,7 @@ Please try recreating the link.`);
else if ( associated_app_name !== '' ) {
launch_app({
name: associated_app_name,
maximized: options.maximized ?? window.is_dashboard_mode,
});
}
//----------------------------------------------------------------
@@ -289,6 +336,11 @@ Please try recreating the link.`);
const fsuid = uid.toLowerCase();
let open_item_meta;
// The stretch from here to the launch is where a re-click would
// find no window to restore yet — mark the file as launching so
// the dashboard reuse branch above swallows re-clicks meanwhile.
if ( target_file_uid ) launching_file_uids.set(target_file_uid, Date.now());
// get all info needed to open an item
try {
open_item_meta = await $.ajax({
@@ -320,6 +372,8 @@ Please try recreating the link.`);
// download
//---------------------------------------------
if ( suggested_apps.length === 0 ) {
// Not launching after all — lift the in-flight guard.
if ( target_file_uid ) launching_file_uids.delete(target_file_uid);
//---------------------------------------------
// If .zip file, unzip it
//---------------------------------------------
@@ -355,16 +409,17 @@ Please try recreating the link.`);
// First suggested app is default app to open this item
//---------------------------------------------
else {
launch_app({
const launch_promise = launch_app({
name: suggested_apps[0].name,
token: open_item_meta.token,
file_path: item_path,
app_obj: suggested_apps[0],
window_title: path.basename(item_path),
file_uid: fsuid,
maximized: options.maximized,
maximized: options.maximized ?? window.is_dashboard_mode,
file_signature: open_item_meta.signature,
});
if ( target_file_uid ) launch_promise.finally(() => launching_file_uids.delete(target_file_uid));
}
}
};