mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-25 07:27:04 +00:00
feat: 'Keep Both' option in name-conflict dialogs (#3496)
Pasting or moving onto an existing name only offered Replace or Cancel. Add a macOS-style middle ground: Keep Both retries the operation with dedupe_name, landing the item as "name (1).ext" and leaving the existing item untouched. - puter-js move.js now forwards dedupeName to the wire (copy already did; move dropped it). - All four conflict dialogs offer the new button: copy_clipboard_items, copy_items and move_items in helpers.js, and the dashboard's moveClipboardItems. Multi-item selections show Replace / Replace all / Keep Both / Skip. - New keep_both translation key (other locales fall back to English).
This commit is contained in:
@@ -3062,6 +3062,7 @@ const TabFiles = {
|
||||
// Handle both object format { path, uid } and legacy string format
|
||||
const source = item.uid || item.path || item;
|
||||
let overwrite = overwrite_all;
|
||||
let keep_both = false;
|
||||
let retry;
|
||||
do {
|
||||
retry = false;
|
||||
@@ -3070,6 +3071,9 @@ const TabFiles = {
|
||||
source: source,
|
||||
destination: destPath,
|
||||
overwrite: overwrite,
|
||||
// "Keep Both" conflict resolution: move under a
|
||||
// deduped "name (1)" style name instead of overwriting
|
||||
dedupeName: keep_both,
|
||||
});
|
||||
} catch ( err ) {
|
||||
// Same conflict resolution as the desktop's move_items:
|
||||
@@ -3080,6 +3084,7 @@ const TabFiles = {
|
||||
buttons: [
|
||||
{ label: i18n('replace'), type: 'primary', value: 'replace' },
|
||||
... multiple_items ? [{ label: i18n('replace_all'), value: 'replace_all' }] : [],
|
||||
{ label: i18n('keep_both'), value: 'keep_both' },
|
||||
... multiple_items ? [{ label: i18n('skip'), value: 'skip' }] : [{ label: i18n('cancel'), value: 'cancel' }],
|
||||
],
|
||||
});
|
||||
@@ -3090,6 +3095,9 @@ const TabFiles = {
|
||||
overwrite = true;
|
||||
overwrite_all = true;
|
||||
retry = true;
|
||||
} else if ( alert_resp === 'keep_both' ) {
|
||||
keep_both = true;
|
||||
retry = true;
|
||||
}
|
||||
// skip/cancel: the item stays where it was cut from
|
||||
} else {
|
||||
|
||||
+23
-4
@@ -1277,6 +1277,7 @@ window.copy_clipboard_items = async function (dest_path, dest_container_element)
|
||||
let copy_path = window.clipboard[i].path;
|
||||
let item_with_same_name_already_exists = true;
|
||||
let overwrite = overwrite_all;
|
||||
let keep_both = false;
|
||||
latest_status = i18n('copying_file', copy_path);
|
||||
progwin?.set_status(latest_status);
|
||||
|
||||
@@ -1298,8 +1299,10 @@ window.copy_clipboard_items = async function (dest_path, dest_container_element)
|
||||
source: copy_path,
|
||||
destination: dest_path,
|
||||
overwrite: overwrite || overwrite_all,
|
||||
// if user is copying an item to where its source is, change the name so there is no conflict
|
||||
dedupeName: dest_path === path.dirname(copy_path),
|
||||
// dedupe when the user chose "Keep Both" on a conflict, or
|
||||
// when copying an item to where its source is — either way
|
||||
// the copy gets a "name (1)" style name instead of conflicting
|
||||
dedupeName: keep_both || dest_path === path.dirname(copy_path),
|
||||
});
|
||||
|
||||
// remove overwritten item from the DOM
|
||||
@@ -1323,6 +1326,7 @@ window.copy_clipboard_items = async function (dest_path, dest_container_element)
|
||||
buttons: [
|
||||
{ label: i18n('replace'), type: 'primary', value: 'replace' },
|
||||
... (window.clipboard.length > 1) ? [{ label: i18n('replace_all'), value: 'replace_all' }] : [],
|
||||
{ label: i18n('keep_both'), value: 'keep_both' },
|
||||
... (window.clipboard.length > 1) ? [{ label: i18n('skip'), value: 'skip' }] : [{ label: i18n('cancel'), value: 'cancel' }],
|
||||
],
|
||||
});
|
||||
@@ -1332,6 +1336,8 @@ window.copy_clipboard_items = async function (dest_path, dest_container_element)
|
||||
} else if ( alert_resp === 'replace_all' ) {
|
||||
overwrite = true;
|
||||
overwrite_all = true;
|
||||
} else if ( alert_resp === 'keep_both' ) {
|
||||
keep_both = true;
|
||||
} else if ( alert_resp === 'skip' || alert_resp === 'cancel' ) {
|
||||
item_with_same_name_already_exists = false;
|
||||
}
|
||||
@@ -1406,6 +1412,7 @@ window.copy_items = function (el_items, dest_path) {
|
||||
let copy_path = $(el_items[i]).attr('data-path');
|
||||
let item_with_same_name_already_exists = true;
|
||||
let overwrite = overwrite_all;
|
||||
let keep_both = false;
|
||||
latest_status = i18n('copying_file', copy_path);
|
||||
progwin?.set_status(latest_status);
|
||||
|
||||
@@ -1424,8 +1431,10 @@ window.copy_items = function (el_items, dest_path) {
|
||||
source: copy_path,
|
||||
destination: dest_path,
|
||||
overwrite: overwrite || overwrite_all,
|
||||
// if user is copying an item to where the source is, automatically change the name so there is no conflict
|
||||
dedupeName: dest_path === path.dirname(copy_path),
|
||||
// dedupe when the user chose "Keep Both" on a conflict, or
|
||||
// when copying an item to where its source is — either way
|
||||
// the copy gets a "name (1)" style name instead of conflicting
|
||||
dedupeName: keep_both || dest_path === path.dirname(copy_path),
|
||||
});
|
||||
|
||||
// remove overwritten item from the DOM
|
||||
@@ -1449,6 +1458,7 @@ window.copy_items = function (el_items, dest_path) {
|
||||
buttons: [
|
||||
{ label: i18n('replace'), type: 'primary', value: 'replace' },
|
||||
... (el_items.length > 1) ? [{ label: i18n('replace_all'), value: 'replace_all' }] : [],
|
||||
{ label: i18n('keep_both'), value: 'keep_both' },
|
||||
... (el_items.length > 1) ? [{ label: i18n('skip'), value: 'skip' }] : [{ label: i18n('cancel'), value: 'cancel' }],
|
||||
],
|
||||
});
|
||||
@@ -1458,6 +1468,8 @@ window.copy_items = function (el_items, dest_path) {
|
||||
} else if ( alert_resp === 'replace_all' ) {
|
||||
overwrite = true;
|
||||
overwrite_all = true;
|
||||
} else if ( alert_resp === 'keep_both' ) {
|
||||
keep_both = true;
|
||||
} else if ( alert_resp === 'skip' || alert_resp === 'cancel' ) {
|
||||
item_with_same_name_already_exists = false;
|
||||
}
|
||||
@@ -1765,6 +1777,7 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
// if an item with the same name already exists in the destination path
|
||||
let item_with_same_name_already_exists = false;
|
||||
let overwrite = overwrite_all;
|
||||
let keep_both = false;
|
||||
let untrashed_at_least_one_item = false;
|
||||
|
||||
// --------------------------------------------------------
|
||||
@@ -1860,6 +1873,9 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
source: $(el_item).attr('data-uid'),
|
||||
destination: dest_path,
|
||||
overwrite: overwrite || overwrite_all,
|
||||
// "Keep Both" conflict resolution: move under a deduped
|
||||
// "name (1)" style name instead of overwriting
|
||||
dedupeName: keep_both,
|
||||
newName: new_name,
|
||||
// recycling requires making all missing dirs
|
||||
createMissingParents: recycling,
|
||||
@@ -2020,6 +2036,7 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
buttons: [
|
||||
{ label: i18n('replace'), type: 'primary', value: 'replace' },
|
||||
... (el_items.length > 1) ? [{ label: i18n('replace_all'), value: 'replace_all' }] : [],
|
||||
{ label: i18n('keep_both'), value: 'keep_both' },
|
||||
... (el_items.length > 1) ? [{ label: i18n('skip'), value: 'skip' }] : [{ label: i18n('cancel'), value: 'cancel' }],
|
||||
],
|
||||
});
|
||||
@@ -2029,6 +2046,8 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
} else if ( alert_resp === 'replace_all' ) {
|
||||
overwrite = true;
|
||||
overwrite_all = true;
|
||||
} else if ( alert_resp === 'keep_both' ) {
|
||||
keep_both = true;
|
||||
} else if ( alert_resp === 'skip' || alert_resp === 'cancel' ) {
|
||||
item_with_same_name_already_exists = false;
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ const en = {
|
||||
item: 'item',
|
||||
items_in_trash_cannot_be_renamed: 'This item can\'t be renamed because it\'s in the trash. To rename this item, first drag it out of the Trash.',
|
||||
jpeg_image: 'JPEG image',
|
||||
keep_both: 'Keep Both',
|
||||
keep_in_taskbar: 'Keep in Taskbar',
|
||||
language: 'Language',
|
||||
license: 'License',
|
||||
|
||||
@@ -52,6 +52,9 @@ const move = defineOperation({
|
||||
source,
|
||||
destination,
|
||||
overwrite: options.overwrite,
|
||||
// give the moved item a deduped name ("x (1).txt") instead of
|
||||
// conflicting — the "Keep Both" conflict resolution
|
||||
dedupe_name: firstDefined(options, 'dedupeName', 'dedupe_name'),
|
||||
new_name: newName,
|
||||
create_missing_parents: firstDefined(options, 'createMissingParents', 'create_missing_parents'),
|
||||
new_metadata: firstDefined(options, 'newMetadata', 'new_metadata'),
|
||||
|
||||
Reference in New Issue
Block a user