mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-13 20:12:57 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b44aed144 | ||
|
|
647f28db37 | ||
|
|
30006e0108 | ||
|
|
71675d6eb7 | ||
|
|
6ec568d0b1 |
No files matched your search
+3
-2
@@ -20,6 +20,7 @@
|
||||
import download from './helpers/download.js';
|
||||
import ensureConfirmedEmail from './helpers/ensureConfirmedEmail.js';
|
||||
import item_icon from './helpers/itemIcon.js';
|
||||
import { atPath } from './helpers/pathElements.js';
|
||||
import socialLink from './helpers/socialLink.js';
|
||||
import update_mouse_position from './helpers/updateMousePosition.js';
|
||||
import path from './lib/path.js';
|
||||
@@ -1702,7 +1703,7 @@ const ipc_listener = async (event, handled) => {
|
||||
$(`.item[data-uid="${res.uid}"]`).removeItems();
|
||||
// now add new items
|
||||
UIItem({
|
||||
appendTo: $(`.item-container[data-path="${html_encode(path.dirname(target_path))}" i]`),
|
||||
appendTo: atPath('.item-container', path.dirname(target_path)),
|
||||
immutable: res.immutable || res.writable === false,
|
||||
associated_app_name: res.associated_app?.name,
|
||||
path: target_path,
|
||||
@@ -1716,7 +1717,7 @@ const ipc_listener = async (event, handled) => {
|
||||
suggested_apps: res.suggested_apps,
|
||||
});
|
||||
// sort each window
|
||||
$(`.item-container[data-path="${html_encode(path.dirname(target_path))}" i]`).each(function () {
|
||||
atPath('.item-container', path.dirname(target_path)).each(function () {
|
||||
window.sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order'));
|
||||
});
|
||||
$(el_filedialog_window).close();
|
||||
|
||||
+17
-42
@@ -34,6 +34,7 @@ import UIWindowProgress from './UIWindowProgress.js';
|
||||
import UITaskbar from './UITaskbar.js';
|
||||
import new_context_menu_item from '../helpers/newContextMenuItem.js';
|
||||
import refresh_item_container from '../helpers/refreshItemContainer.js';
|
||||
import { atPath, retargetPaths, underPath } from '../helpers/pathElements.js';
|
||||
import changeLanguage from '../i18n/i18nChangeLanguage.js';
|
||||
import UIWindowTaskManager from './UIWindowTaskManager.js';
|
||||
import truncate_filename from '../helpers/truncateFilename.js';
|
||||
@@ -416,12 +417,12 @@ async function UIDesktop (options) {
|
||||
}
|
||||
|
||||
// hide all UIItems with matching uids
|
||||
$(`.item[data-path='${item.path}']`).fadeOut(150, function () {
|
||||
atPath('.item', item.path).fadeOut(150, function () {
|
||||
// close all windows with matching uids
|
||||
// $('.window-' + item.uid).close();
|
||||
// close all windows that belong to a descendant of this item
|
||||
// todo this has to be case-insensitive but the `i` selector doesn't work on ^=
|
||||
$(`.window[data-path^="${item.path}/"]`).close();
|
||||
// todo this has to be case-insensitive
|
||||
underPath('.window', item.path).close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -459,20 +460,7 @@ async function UIDesktop (options) {
|
||||
$(`.item[data-uid='${item.uid}']`).attr('data-path', new_path);
|
||||
$(`.window-${item.uid}`).attr('data-path', new_path);
|
||||
|
||||
// Update all elements that have matching paths
|
||||
$(`[data-path="${html_encode(item.old_path)}" i]`).each(function () {
|
||||
$(this).attr('data-path', new_path);
|
||||
if ( $(this).hasClass('window-navbar-path-dirname') )
|
||||
{
|
||||
$(this).text(item.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Update all elements whose paths start with old_path
|
||||
$(`[data-path^="${`${html_encode(item.old_path) }/`}"]`).each(function () {
|
||||
const new_el_path = $(this).attr('data-path').replace(`${item.old_path }/`, `${new_path }/`);
|
||||
$(this).attr('data-path', new_el_path);
|
||||
});
|
||||
retargetPaths(item.old_path, new_path, item.name);
|
||||
|
||||
// Update all exact-matching windows
|
||||
$(`.window-${item.uid}`).each(function () {
|
||||
@@ -507,7 +495,7 @@ async function UIDesktop (options) {
|
||||
const metadata = parse_item_metadata(fsentry.metadata);
|
||||
|
||||
// update all shortcut_to_path
|
||||
$(`.item[data-shortcut_to_path="${html_encode(resp.old_path)}" i]`).attr('data-shortcut_to_path', html_encode(fsentry.path));
|
||||
atPath('.item', resp.old_path, 'data-shortcut_to_path').attr('data-shortcut_to_path', fsentry.path);
|
||||
|
||||
// remove all items with matching uids
|
||||
$(`.item[data-uid='${fsentry.uid}']`).fadeOut(150, function () {
|
||||
@@ -524,16 +512,16 @@ async function UIDesktop (options) {
|
||||
|
||||
// if trashing, close windows of trashed items and its descendants
|
||||
if ( dest_path === window.trash_path ) {
|
||||
$(`.window[data-path="${html_encode(resp.old_path)}" i]`).close();
|
||||
// todo this has to be case-insensitive but the `i` selector doesn't work on ^=
|
||||
$(`.window[data-path^="${html_encode(resp.old_path)}/"]`).close();
|
||||
atPath('.window', resp.old_path).close();
|
||||
// todo this has to be case-insensitive
|
||||
underPath('.window', resp.old_path).close();
|
||||
}
|
||||
|
||||
// update all paths of its and its descendants' open windows
|
||||
else {
|
||||
// todo this has to be case-insensitive but the `i` selector doesn't work on ^=
|
||||
$(`.window[data-path^="${html_encode(resp.old_path)}/"], .window[data-path="${html_encode(resp.old_path)}" i]`).each(function () {
|
||||
window.update_window_path(this, $(this).attr('data-path').replace(resp.old_path, fsentry.path));
|
||||
// todo this has to be case-insensitive
|
||||
underPath('.window', resp.old_path).add(atPath('.window', resp.old_path)).each(function () {
|
||||
window.update_window_path(this, fsentry.path + $(this).attr('data-path').slice(resp.old_path.length));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -560,7 +548,7 @@ async function UIDesktop (options) {
|
||||
|
||||
// create new item on matching containers
|
||||
UIItem({
|
||||
appendTo: $(`.item-container[data-path='${html_encode(dest_path)}' i]`),
|
||||
appendTo: atPath('.item-container', dest_path),
|
||||
immutable: fsentry.immutable || fsentry.writable === false,
|
||||
uid: fsentry.uid,
|
||||
path: fsentry.path,
|
||||
@@ -585,8 +573,8 @@ async function UIDesktop (options) {
|
||||
// if these new path have any open parents that need to be updated
|
||||
|
||||
fsentry.parent_dirs_created.forEach(async dir => {
|
||||
let item_container = $(`.item-container[data-path='${html_encode(path.dirname(dir.path))}' i]`);
|
||||
if ( item_container.length > 0 && $(`.item[data-path="${html_encode(dir.path)}" i]`).length === 0 ) {
|
||||
let item_container = atPath('.item-container', path.dirname(dir.path));
|
||||
if ( item_container.length > 0 && atPath('.item', dir.path).length === 0 ) {
|
||||
UIItem({
|
||||
appendTo: item_container,
|
||||
immutable: false,
|
||||
@@ -606,7 +594,7 @@ async function UIDesktop (options) {
|
||||
});
|
||||
}
|
||||
//sort each container
|
||||
$(`.item-container[data-path='${html_encode(dest_path)}' i]`).each(function () {
|
||||
atPath('.item-container', dest_path).each(function () {
|
||||
window.sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order'));
|
||||
});
|
||||
});
|
||||
@@ -693,20 +681,7 @@ async function UIDesktop (options) {
|
||||
$(`.item[data-uid='${item.uid}']`).attr('data-path', new_path);
|
||||
$(`.window-${item.uid}`).attr('data-path', new_path);
|
||||
|
||||
// Update all elements that have matching paths
|
||||
$(`[data-path="${html_encode(item.old_path)}" i]`).each(function () {
|
||||
$(this).attr('data-path', new_path);
|
||||
if ( $(this).hasClass('window-navbar-path-dirname') )
|
||||
{
|
||||
$(this).text(item.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Update all elements whose paths start with old_path
|
||||
$(`[data-path^="${`${html_encode(item.old_path) }/`}"]`).each(function () {
|
||||
const new_el_path = $(this).attr('data-path').replace(`${item.old_path }/`, `${new_path }/`);
|
||||
$(this).attr('data-path', new_el_path);
|
||||
});
|
||||
retargetPaths(item.old_path, new_path, item.name);
|
||||
|
||||
// Update all exact-matching windows
|
||||
$(`.window-${item.uid}`).each(function () {
|
||||
|
||||
@@ -25,6 +25,7 @@ import UIAuthLogin from './Auth/UIAuthLogin.js';
|
||||
import UIWindowItemProperties from './UIWindowItemProperties.js';
|
||||
import new_context_menu_item from '../helpers/newContextMenuItem.js';
|
||||
import refresh_item_container from '../helpers/refreshItemContainer.js';
|
||||
import { atPath } from '../helpers/pathElements.js';
|
||||
import UIWindowAppFeedback from './UIWindowAppFeedback.js';
|
||||
import launch_app from '../helpers/launchApp.js';
|
||||
import publish_as_website from '../helpers/publishAsWebsite.js';
|
||||
@@ -3348,7 +3349,7 @@ window.update_window_path = async function (el_window, target_path) {
|
||||
|
||||
// update sidebar items' active status
|
||||
$(el_window).find('.window-sidebar-item').removeClass('window-sidebar-item-active');
|
||||
$(el_window).find(`.window-sidebar-item[data-path="${html_encode(target_path)}"]`).addClass('window-sidebar-item-active');
|
||||
atPath($(el_window).find('.window-sidebar-item'), target_path).addClass('window-sidebar-item-active');
|
||||
|
||||
// clean
|
||||
$(el_window).find('.explore-table-headers-th > .header-sort-icon').html('');
|
||||
@@ -3432,8 +3433,8 @@ window.update_window_path = async function (el_window, target_path) {
|
||||
}
|
||||
}
|
||||
|
||||
$(el_window).attr('data-path', html_encode(target_path));
|
||||
$(el_window).attr('data-name', html_encode(path.basename(target_path)));
|
||||
$(el_window).attr('data-path', target_path);
|
||||
$(el_window).attr('data-name', path.basename(target_path));
|
||||
|
||||
// /stat
|
||||
if ( target_path === window.shared_path ) {
|
||||
@@ -3447,7 +3448,7 @@ window.update_window_path = async function (el_window, target_path) {
|
||||
try {
|
||||
puter.fs.stat({ path: target_path, consistency: 'eventual' }).then(fsentry => {
|
||||
// the window may have moved on while this stat was in flight
|
||||
if ( $(el_window).attr('data-path') !== html_encode(target_path) ) {
|
||||
if ( $(el_window).attr('data-path') !== target_path ) {
|
||||
return;
|
||||
}
|
||||
$(el_window).removeClass(`window-${ $(el_window).attr('data-uid')}`);
|
||||
@@ -3491,9 +3492,9 @@ window.update_window_path = async function (el_window, target_path) {
|
||||
$(el_window).find('.window-head-title').text(fsentry.name);
|
||||
}
|
||||
// data-name
|
||||
$(el_window).attr('data-name', html_encode(fsentry.name));
|
||||
$(el_window).attr('data-name', fsentry.name);
|
||||
// data-path
|
||||
$(el_window).attr('data-path', html_encode(target_path));
|
||||
$(el_window).attr('data-path', target_path);
|
||||
$(el_window_navbar_path_input).val(target_path);
|
||||
$(el_window_navbar_path_input).attr('data-path', target_path);
|
||||
// update layout
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import UIWindow from './UIWindow.js';
|
||||
import UIWindowMyWebsites from './UIWindowMyWebsites.js';
|
||||
import { underPath } from '../helpers/pathElements.js';
|
||||
|
||||
async function UIWindowPublishWebsite (target_dir_uid, target_dir_name, target_dir_path) {
|
||||
let h = '';
|
||||
@@ -110,7 +111,7 @@ async function UIWindowPublishWebsite (target_dir_uid, target_dir_name, target_d
|
||||
});
|
||||
|
||||
// find all items whose path starts with target_dir_path
|
||||
$(`.item[data-path^="${target_dir_path}/"]`).each(function () {
|
||||
underPath('.item', target_dir_path).each(function () {
|
||||
// show the link badge
|
||||
$(this).find('.item-has-website-url-badge').show();
|
||||
// update item's website_url attribute
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import UIWindow from './UIWindow.js';
|
||||
import UIWindowMyWebsites from './UIWindowMyWebsites.js';
|
||||
import { underPath } from '../helpers/pathElements.js';
|
||||
|
||||
async function UIWindowPublishWorker (target_dir_uid, target_dir_name, target_dir_path) {
|
||||
let h = '';
|
||||
@@ -117,7 +118,7 @@ async function UIWindowPublishWorker (target_dir_uid, target_dir_name, target_di
|
||||
});
|
||||
|
||||
// find all items whose path starts with target_dir_path
|
||||
$(`.item[data-path^="${target_dir_path}/"]`).each(function () {
|
||||
underPath('.item', target_dir_path).each(function () {
|
||||
// show the link badge
|
||||
// $(this).find('.item-has-website-url-badge').show();
|
||||
// update item's website_url attribute
|
||||
|
||||
+26
-46
@@ -23,6 +23,7 @@ import { archiveEntryPath } from './helpers/archiveEntryPath.js';
|
||||
import get_html_element_from_options from './helpers/getHtmlElementFromOptions.js';
|
||||
import globToRegExp from './helpers/globToRegExp.js';
|
||||
import item_icon from './helpers/itemIcon.js';
|
||||
import { atPath, retargetPaths, underPath } from './helpers/pathElements.js';
|
||||
import { is_owned_by_me, trash_path_for } from './helpers/pathOwner.js';
|
||||
import { invalidate_shared_roots } from './helpers/sharedAccess.js';
|
||||
import truncate_filename from './helpers/truncateFilename.js';
|
||||
@@ -1178,7 +1179,7 @@ window.create_folder = async (basedir, appendto_element) => {
|
||||
rename: true,
|
||||
overwrite: false,
|
||||
success: function (data) {
|
||||
const el_created_dir = $(appendto_element).find(`.item[data-path="${html_encode(dirname)}/${html_encode(data.name)}"]`);
|
||||
const el_created_dir = atPath($(appendto_element).find('.item'), `${dirname}/${data.name}`);
|
||||
if ( el_created_dir.length > 0 ) {
|
||||
window.activate_item_name_editor(el_created_dir);
|
||||
|
||||
@@ -1211,7 +1212,7 @@ window.create_file = async (options) => {
|
||||
generateThumbnails: true,
|
||||
thumbnailGenerator: createUploadThumbnailGenerator(),
|
||||
success: async function (data) {
|
||||
const created_file = $(appendto_element).find(`.item[data-path="${html_encode(dirname)}/${html_encode(data.name)}"]`);
|
||||
const created_file = atPath($(appendto_element).find('.item'), `${dirname}/${data.name}`);
|
||||
if ( created_file.length > 0 ) {
|
||||
window.activate_item_name_editor(created_file);
|
||||
|
||||
@@ -1583,8 +1584,8 @@ window.delete_item = async function (el_item, descendants_only = false) {
|
||||
// close all windows with matching uids
|
||||
$(`.window-${ $(el_item).attr('data-uid')}`).close();
|
||||
// close all windows that belong to a descendant of this item
|
||||
// todo this has to be case-insensitive but the `i` selector doesn't work on ^=
|
||||
$(`.window[data-path^="${$(el_item).attr('data-path')}/"]`).close();
|
||||
// todo this has to be case-insensitive
|
||||
underPath('.window', $(el_item).attr('data-path')).close();
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -1605,7 +1606,7 @@ window.delete_item = async function (el_item, descendants_only = false) {
|
||||
window.update_explorer_footer_selected_items_count(this);
|
||||
});
|
||||
// update all shortcuts to this item
|
||||
$(`.item[data-shortcut_to_path="${html_encode($(el_item).attr('data-path'))}" i]`).attr('data-shortcut_to_path', '');
|
||||
atPath('.item', $(el_item).attr('data-path'), 'data-shortcut_to_path').attr('data-shortcut_to_path', '');
|
||||
});
|
||||
} catch ( err ) {
|
||||
// Nothing was deleted: bring back what was faded out above.
|
||||
@@ -1619,7 +1620,7 @@ window.move_clipboard_items = function (el_target_container, target_path) {
|
||||
let el_items = [];
|
||||
if ( window.clipboard.length > 0 ) {
|
||||
for ( let i = 0; i < window.clipboard.length; i++ ) {
|
||||
el_items.push($(`.item[data-path="${html_encode(window.clipboard[i])}" i]`));
|
||||
el_items.push(atPath('.item', window.clipboard[i]));
|
||||
}
|
||||
if ( el_items.length > 0 )
|
||||
{
|
||||
@@ -1961,24 +1962,18 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
// skip next loop iteration because this iteration was successful
|
||||
item_with_same_name_already_exists = false;
|
||||
|
||||
// update all shortcut_to_path — compare raw attribute values
|
||||
// (item rows store paths unencoded, so an html_encode()d
|
||||
// selector misses names containing & < > " ')
|
||||
const moved_from_path_lc = String($(el_item).attr('data-path') || '').toLowerCase();
|
||||
$('.item[data-shortcut_to_path]').filter(function () {
|
||||
return String($(this).attr('data-shortcut_to_path')).toLowerCase() === moved_from_path_lc;
|
||||
}).attr('data-shortcut_to_path', fsentry.path);
|
||||
const moved_from_path = $(el_item).attr('data-path');
|
||||
|
||||
// update all shortcut_to_path
|
||||
atPath('.item', moved_from_path, 'data-shortcut_to_path').attr('data-shortcut_to_path', fsentry.path);
|
||||
|
||||
// Remove all items with matching uids from their OLD location(s).
|
||||
// Exclude any row already at the item's new path: a concurrent
|
||||
// item.moved socket handler may have just created a row at the
|
||||
// destination (e.g. the dashboard file view showing the target
|
||||
// directory), and removing by uid alone would delete it too.
|
||||
// Raw case-insensitive compare, for the same reason as above.
|
||||
const dest_item_path_lc = fsentry.path.toLowerCase();
|
||||
$(`.item[data-uid='${$(el_item).attr('data-uid')}']`).not(function () {
|
||||
return String($(this).attr('data-path') || '').toLowerCase() === dest_item_path_lc;
|
||||
}).fadeOut(150, function () {
|
||||
const $moved_items = $(`.item[data-uid='${$(el_item).attr('data-uid')}']`);
|
||||
$moved_items.not(atPath($moved_items, fsentry.path)).fadeOut(150, function () {
|
||||
// find all parent windows that contain this item
|
||||
let parent_windows = $(`.item[data-uid='${$(el_item).attr('data-uid')}']`).closest('.window');
|
||||
// remove this item
|
||||
@@ -1992,23 +1987,23 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
|
||||
// if trashing, close windows of trashed items and its descendants
|
||||
if ( is_trashing ) {
|
||||
$(`.window[data-path="${html_encode($(el_item).attr('data-path'))}" i]`).close();
|
||||
// todo this has to be case-insensitive but the `i` selector doesn't work on ^=
|
||||
$(`.window[data-path^="${html_encode($(el_item).attr('data-path'))}/"]`).close();
|
||||
atPath('.window', moved_from_path).close();
|
||||
// todo this has to be case-insensitive
|
||||
underPath('.window', moved_from_path).close();
|
||||
}
|
||||
|
||||
// update all paths of its and its descendants' open windows
|
||||
else {
|
||||
// todo this has to be case-insensitive but the `i` selector doesn't work on ^=
|
||||
$(`.window[data-path^="${html_encode($(el_item).attr('data-path'))}/"], .window[data-path="${html_encode($(el_item).attr('data-path'))}" i]`).each(function () {
|
||||
window.update_window_path(this, $(this).attr('data-path').replace($(el_item).attr('data-path'), path.join(item_dest_path, fsentry.name)));
|
||||
// todo this has to be case-insensitive
|
||||
underPath('.window', moved_from_path).add(atPath('.window', moved_from_path)).each(function () {
|
||||
window.update_window_path(this, path.join(item_dest_path, fsentry.name) + $(this).attr('data-path').slice(moved_from_path.length));
|
||||
});
|
||||
}
|
||||
|
||||
// The dashboard's Back/Forward follows a moved folder, and
|
||||
// forgets a trashed one.
|
||||
if ( $(el_item).attr('data-is_dir') === '1' ) {
|
||||
window.dashboard_object?.folderMoved?.($(el_item).attr('data-path'), is_trashing ? null : fsentry.path);
|
||||
window.dashboard_object?.folderMoved?.(moved_from_path, is_trashing ? null : fsentry.path);
|
||||
}
|
||||
|
||||
if ( is_trashing ) {
|
||||
@@ -2032,7 +2027,7 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
|
||||
// create new item on matching containers
|
||||
const options = {
|
||||
appendTo: $(`.item-container[data-path="${html_encode(item_dest_path)}" i]`),
|
||||
appendTo: atPath('.item-container', item_dest_path),
|
||||
immutable: fsentry.immutable || (fsentry.writable === false),
|
||||
associated_app_name: fsentry.associated_app?.name,
|
||||
uid: fsentry.uid,
|
||||
@@ -2063,8 +2058,8 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
// see if any of the directories in the path of this file is new AND
|
||||
// if these new path have any open parents that need to be updated
|
||||
resp.parent_dirs_created?.forEach(async dir => {
|
||||
let item_container = $(`.item-container[data-path="${html_encode(path.dirname(dir.path))}" i]`);
|
||||
if ( item_container.length > 0 && $(`.item[data-path="${html_encode(dir.path)}" i]`).length === 0 ) {
|
||||
let item_container = atPath('.item-container', path.dirname(dir.path));
|
||||
if ( item_container.length > 0 && atPath('.item', dir.path).length === 0 ) {
|
||||
UIItem({
|
||||
appendTo: item_container,
|
||||
immutable: false,
|
||||
@@ -2090,7 +2085,7 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
});
|
||||
|
||||
//sort each container
|
||||
$(`.item-container[data-path="${html_encode(item_dest_path)}" i]`).each(function () {
|
||||
atPath('.item-container', item_dest_path).each(function () {
|
||||
window.sort_items(this, $(this).attr('data-sort_by'), $(this).attr('data-sort_order'));
|
||||
});
|
||||
} catch ( err ) {
|
||||
@@ -2285,9 +2280,7 @@ window.updateSubdomainsForItems = async function (fsentries, container) {
|
||||
|
||||
// If not found by uid, try path-based search
|
||||
if ( $item.length === 0 && fsentry.path ) {
|
||||
// Escape special characters in path for jQuery selector
|
||||
const escapedPath = fsentry.path.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, '\\$&');
|
||||
$item = $(document).find(`.item[data-path="${escapedPath}"]`);
|
||||
$item = atPath('.item', fsentry.path);
|
||||
}
|
||||
|
||||
if ( $item.length > 0 ) {
|
||||
@@ -3301,20 +3294,7 @@ window.rename_file = async (options, new_name, old_name, old_path, el_item, el_i
|
||||
$(`.item[data-uid='${$(el_item).attr('data-uid')}']`).attr('data-path', new_path);
|
||||
$(`.window-${options.uid}`).attr('data-path', new_path);
|
||||
|
||||
// Update all elements that have matching paths
|
||||
$(`[data-path="${html_encode(old_path)}" i]`).each(function () {
|
||||
$(this).attr('data-path', new_path);
|
||||
if ( $(this).hasClass('window-navbar-path-dirname') )
|
||||
{
|
||||
$(this).text(new_name);
|
||||
}
|
||||
});
|
||||
|
||||
// Update the paths of all elements whose paths start with `old_path`
|
||||
$(`[data-path^="${`${html_encode(old_path) }/`}"]`).each(function () {
|
||||
const new_el_path = $(this).attr('data-path').replace(`${old_path }/`, `${new_path}/`);
|
||||
$(this).attr('data-path', new_el_path);
|
||||
});
|
||||
retargetPaths(old_path, new_path, new_name);
|
||||
|
||||
// Update the 'Sites Cache'
|
||||
if ( $(el_item).attr('data-has_website') === '1' )
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// @vitest-environment jsdom
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import jQuery from './lib/jquery-3.6.1/jquery-3.6.1.min.js';
|
||||
|
||||
vi.mock('./UI/UIAlert.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UI/UIItem.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UI/Auth/UIAuthLogin.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UI/UIWindowProgress.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UI/UIWindowSaveAccount.js', () => ({ default: vi.fn() }));
|
||||
|
||||
globalThis.$ = jQuery;
|
||||
globalThis.jQuery = jQuery;
|
||||
globalThis.i18n = key => key;
|
||||
globalThis.def = globalThis.def ?? (value => value);
|
||||
globalThis.use = globalThis.use ?? (() => class {});
|
||||
globalThis.html_encode = value => jQuery('<div>').text(value).html();
|
||||
|
||||
await import('./helpers.js');
|
||||
const { default: apply_item_added_to_containers } = await import('./helpers/applyItemAddedToContainers.js');
|
||||
const { default: UIItem } = await import('./UI/UIItem.js');
|
||||
const { atPath, retargetPaths, underPath } = await import('./helpers/pathElements.js');
|
||||
|
||||
// A plain name, then names holding characters a selector or HTML treats specially.
|
||||
const NAMES = ['Reports 2024', 'R&D', "Dan's files", 'the "big" one', 'a<b>c', 'back\\slash'];
|
||||
|
||||
const el = (className, attrs) => jQuery('<div>').addClass(className).attr(attrs).appendTo(document.body);
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
jQuery.fx.off = true;
|
||||
jQuery.fn.close = vi.fn();
|
||||
jQuery.fn.removeItems = vi.fn();
|
||||
window.icons = { 'folder.svg': 'folder.svg' };
|
||||
window.actions_history = [];
|
||||
window.user = { username: 'alice' };
|
||||
window.update_window_path = vi.fn();
|
||||
window.sort_items = vi.fn();
|
||||
window.update_explorer_footer_item_count = vi.fn();
|
||||
window.update_explorer_footer_selected_items_count = vi.fn();
|
||||
window.determine_website_url = () => '';
|
||||
UIItem.mockClear();
|
||||
});
|
||||
|
||||
describe.each(NAMES)('a folder named %s', (name) => {
|
||||
const folderPath = `/alice/${name}`;
|
||||
|
||||
it('keeps its open subfolders and their items on the new path when renamed', async () => {
|
||||
const $item = el('item', { 'data-uid': 'u1', 'data-path': folderPath, 'data-name': name });
|
||||
const $crumb = el('window-navbar-path-dirname', { 'data-path': folderPath });
|
||||
const $subWindow = el('window', { 'data-path': `${folderPath}/sub` });
|
||||
const $subItem = el('item', { 'data-uid': 'u2', 'data-path': `${folderPath}/sub/a.txt` });
|
||||
globalThis.puter = { fs: { rename: ({ success }) => success({ uid: 'u1', name: 'Plain', is_dir: true }) } };
|
||||
|
||||
await new Promise(resolve => {
|
||||
window.rename_file({ uid: 'u1', name, path: folderPath, is_dir: true }, 'Plain', name, folderPath,
|
||||
$item[0], null, null, null, '', false, resolve);
|
||||
});
|
||||
|
||||
expect($item.attr('data-path')).toBe('/alice/Plain');
|
||||
expect($crumb.attr('data-path')).toBe('/alice/Plain');
|
||||
expect($crumb.text()).toBe('Plain');
|
||||
expect($subWindow.attr('data-path')).toBe('/alice/Plain/sub');
|
||||
expect($subItem.attr('data-path')).toBe('/alice/Plain/sub/a.txt');
|
||||
});
|
||||
|
||||
it('closes the windows of its subfolders when deleted', async () => {
|
||||
const $item = el('item', { 'data-uid': 'u1', 'data-path': folderPath });
|
||||
el('window', { 'data-path': `${folderPath}/sub` });
|
||||
el('window', { 'data-path': '/alice/Other/sub' });
|
||||
el('item', { 'data-uid': 'u3', 'data-shortcut_to_path': folderPath });
|
||||
globalThis.puter = { fs: { delete: vi.fn().mockResolvedValue() } };
|
||||
|
||||
await window.delete_item($item[0]);
|
||||
|
||||
const closed = jQuery.fn.close.mock.contexts.flatMap($set => $set.toArray().map(w => jQuery(w).attr('data-path')));
|
||||
expect(closed).toEqual([`${folderPath}/sub`]);
|
||||
expect(puter.fs.delete).toHaveBeenCalled();
|
||||
expect(jQuery('[data-uid="u3"]').attr('data-shortcut_to_path')).toBe('');
|
||||
});
|
||||
|
||||
it('shows an item another tab added to it', async () => {
|
||||
const $container = el('item-container', { 'data-path': folderPath });
|
||||
|
||||
await apply_item_added_to_containers({ uid: 'u9', dirpath: folderPath, path: `${folderPath}/new.txt`, name: 'new.txt', is_dir: true });
|
||||
|
||||
expect(UIItem).toHaveBeenCalledTimes(1);
|
||||
expect(UIItem.mock.calls[0][0].appendTo.toArray()).toEqual([$container[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('retargetPaths', () => {
|
||||
it('ignores the case of the renamed path itself, and leaves its siblings alone', () => {
|
||||
const $window = el('window', { 'data-path': '/alice/docs' });
|
||||
const $sibling = el('item', { 'data-path': '/alice/Docs 2/a.txt' });
|
||||
|
||||
retargetPaths('/alice/Docs', '/alice/Papers', 'Papers');
|
||||
|
||||
expect($window.attr('data-path')).toBe('/alice/Papers');
|
||||
expect($sibling.attr('data-path')).toBe('/alice/Docs 2/a.txt');
|
||||
});
|
||||
|
||||
it('writes a new name holding a replacement pattern as it is', () => {
|
||||
const $item = el('item', { 'data-path': '/alice/Docs/a.txt' });
|
||||
|
||||
retargetPaths('/alice/Docs', '/alice/$& and $1', '$& and $1');
|
||||
|
||||
expect($item.attr('data-path')).toBe('/alice/$& and $1/a.txt');
|
||||
});
|
||||
|
||||
it('changes nothing when the old path is unknown', () => {
|
||||
const $item = el('item', { 'data-path': '/alice/Docs/a.txt' });
|
||||
|
||||
retargetPaths(undefined, '/alice/Papers', 'Papers');
|
||||
|
||||
expect($item.attr('data-path')).toBe('/alice/Docs/a.txt');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([undefined, null, ''])('a lookup for the path %s', (missing) => {
|
||||
it('matches nothing, including elements that have no path', () => {
|
||||
el('item', { 'data-path': '/alice/a.txt', 'data-shortcut_to_path': '' });
|
||||
el('window', { 'data-path': '' });
|
||||
el('window', { 'data-path': '/alice/Docs' });
|
||||
|
||||
expect(atPath('.item', missing, 'data-shortcut_to_path')).toHaveLength(0);
|
||||
expect(atPath('.window', missing)).toHaveLength(0);
|
||||
expect(underPath('.window', missing)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import UIItem from '../UI/UIItem.js';
|
||||
import item_icon from './itemIcon.js';
|
||||
import { atPath } from './pathElements.js';
|
||||
import { select_added_item_if_pending } from './uploadSelection.js';
|
||||
|
||||
/**
|
||||
@@ -37,7 +38,7 @@ import { select_added_item_if_pending } from './uploadSelection.js';
|
||||
* @param {object} item fsentry from the `item.added` event
|
||||
*/
|
||||
const apply_item_added_to_containers = async function (item) {
|
||||
const $containers = $(`.item-container[data-path='${html_encode(item.dirpath)}' i]`);
|
||||
const $containers = atPath('.item-container', item.dirpath);
|
||||
if ( $containers.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Narrows `elements` to those whose `attr` is `path`, ignoring case. Reads the
|
||||
* attribute, because a path can hold any character a selector can't.
|
||||
*
|
||||
* @param {string | Element | JQuery} elements - a selector or a set of elements
|
||||
* @param {string} path
|
||||
* @param {string} [attr]
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
export const atPath = (elements, path, attr = 'data-path') => {
|
||||
// Without this, a missing path matches every element whose attribute is empty.
|
||||
if ( ! path ) return $();
|
||||
const wanted = String(path).toLowerCase();
|
||||
return $(elements).filter(function () {
|
||||
const value = this.getAttribute(attr);
|
||||
return value !== null && value.toLowerCase() === wanted;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Narrows `elements` to those whose `data-path` is inside the folder at `path`.
|
||||
*
|
||||
* @param {string | Element | JQuery} elements - a selector or a set of elements
|
||||
* @param {string} path
|
||||
* @returns {JQuery}
|
||||
*/
|
||||
export const underPath = (elements, path) => {
|
||||
if ( ! path ) return $();
|
||||
const prefix = `${path}/`;
|
||||
return $(elements).filter(function () {
|
||||
return this.getAttribute('data-path')?.startsWith(prefix) === true;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Points every element at `oldPath`, or inside it, to `newPath`.
|
||||
*
|
||||
* @param {string} oldPath
|
||||
* @param {string} newPath
|
||||
* @param {string} newName - shown by the breadcrumbs that pointed at `oldPath`
|
||||
*/
|
||||
export const retargetPaths = (oldPath, newPath, newName) => {
|
||||
if ( ! oldPath ) return;
|
||||
const $inside = underPath('[data-path]', oldPath);
|
||||
atPath('[data-path]', oldPath).each(function () {
|
||||
$(this).attr('data-path', newPath);
|
||||
if ( $(this).hasClass('window-navbar-path-dirname') ) {
|
||||
$(this).text(newName);
|
||||
}
|
||||
});
|
||||
$inside.each(function () {
|
||||
$(this).attr('data-path', newPath + $(this).attr('data-path').slice(oldPath.length));
|
||||
});
|
||||
};
|
||||
@@ -103,9 +103,9 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
$(el_window).attr('data-sort_order', fsentry.sort_order ?? 'asc');
|
||||
$(el_window).attr('data-layout', fsentry.layout ?? 'icons');
|
||||
// data-name
|
||||
$(el_window).attr('data-name', html_encode(fsentry.name));
|
||||
$(el_window).attr('data-name', fsentry.name);
|
||||
// data-path
|
||||
$(el_window).attr('data-path', html_encode(container_path));
|
||||
$(el_window).attr('data-path', container_path);
|
||||
$(el_window).find('.window-navbar-path-input').val(container_path);
|
||||
$(el_window).find('.window-navbar-path-input').attr('data-path', container_path);
|
||||
}
|
||||
|
||||
@@ -167,6 +167,15 @@ describe('refresh_item_container after the window moves to another folder', () =
|
||||
expect($('.window').attr('data-uid')).toBe('pictures');
|
||||
});
|
||||
|
||||
it('stores a folder name holding HTML characters on the window as it is', async () => {
|
||||
navigate('/alice/R&D');
|
||||
|
||||
pending.stat['/alice/R&D'].resolve({ id: 'rd', name: 'R&D' });
|
||||
await vi.waitFor(() => expect($('.window').attr('data-uid')).toBe('rd'));
|
||||
|
||||
expect($('.window').attr('data-name')).toBe('R&D');
|
||||
});
|
||||
|
||||
it('drops a listing that arrives just before the window moves on', async () => {
|
||||
vi.useFakeTimers();
|
||||
navigate('/alice/Documents');
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { atPath } from './pathElements.js';
|
||||
|
||||
/**
|
||||
* Whether a `getShares()` list means this item is shared.
|
||||
*
|
||||
@@ -33,10 +35,7 @@ export const has_direct_share = (shares) =>
|
||||
* can be on the desktop and in any number of open windows.
|
||||
*/
|
||||
export const mark_item_shared = (path, is_shared) => {
|
||||
if ( ! path ) return;
|
||||
const $items = $('.item').filter(function() {
|
||||
return ($(this).attr('data-path') || '').toLowerCase() === path.toLowerCase();
|
||||
});
|
||||
const $items = atPath('.item', path);
|
||||
$items.attr('data-is_shared', is_shared ? 1 : 0);
|
||||
$items
|
||||
.find('.item-shared-marker')
|
||||
|
||||
Reference in new issue
Block a user