Cover Files duplicate context action

This commit is contained in:
mirivlad 2026-07-01 08:09:28 +08:00
parent 5c47ca2480
commit 03dbede414
2 changed files with 41 additions and 0 deletions

View File

@ -156,6 +156,22 @@ test.describe('G: Files Plugin', () => {
await expect(page.locator('[data-file-name="CutMe"]')).toHaveCount(0);
});
test('files explorer duplicates a file from the context menu', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
await page.locator('[data-file-name="project-only.txt"]').click({ button: 'right' });
await page.locator('[data-files-menu-action="duplicate"]').click();
await expect(page.locator('[data-file-name="project-only.txt"]')).toBeVisible();
await expect(page.locator('[data-file-name="project-only (copy).txt"]')).toBeVisible();
await page.locator('[data-file-name="project-only.txt"]').click({ button: 'right' });
await page.locator('[data-files-menu-action="duplicate"]').click();
await expect(page.locator('[data-file-name="project-only (copy 2).txt"]')).toBeVisible();
});
test('files explorer supports multiselect and internal drag/drop move', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
await openFilesTool(page);

View File

@ -897,6 +897,30 @@
function cutSelection() { setClipboard('cut', selectedEntries()); }
function copySelection() { setClipboard('copy', selectedEntries().filter(function (item) { return item.type !== 'folder'; })); }
function uniqueName(name, occupied) { if (!occupied[name]) return name; var dot = name.lastIndexOf('.'); var b = dot > 0 ? name.slice(0, dot) : name; var x = dot > 0 ? name.slice(dot) : ''; for (var i = 2; i < 100; i++) { var c = b + ' (' + i + ')' + x; if (!occupied[c]) return c; } return b + ' (' + Date.now() + ')' + x; }
function duplicate(item) {
if (!item || item.type === 'folder') return;
var dot = item.name.lastIndexOf('.');
var stem = dot > 0 ? item.name.slice(0, dot) : item.name;
var extension = dot > 0 ? item.name.slice(dot) : '';
var folder = parent(item.relativePath);
var maxAttempts = 100;
function candidatePath(n) {
var name = n === 1 ? stem + ' (copy)' + extension : stem + ' (copy ' + n + ')' + extension;
return folder ? folder + '/' + name : name;
}
function tryDuplicate(n) {
var target = candidatePath(n);
return api.files.metadata(target).then(function () {
if (n >= maxAttempts) return Promise.reject(new Error('all duplicate names are taken'));
return tryDuplicate(n + 1);
}, function () {
return api.files.readText(item.relativePath).then(function (text) {
return api.files.writeText(target, text, { createIfMissing: true, overwrite: false });
});
});
}
tryDuplicate(1).then(load).catch(function (err) { console.error('[files] Duplicate failed:', err); });
}
function paste() {
var clip = window.__filesClipboard;
if (!clip || !clip.items || !clip.items.length) return;
@ -920,6 +944,7 @@
if (!selected[item.relativePath]) { selected = {}; selected[item.relativePath] = true; render(); }
menu.appendChild(menuItem('Open', 'open', function () { open(item); }));
menu.appendChild(menuItem('Rename', 'rename', function () { startRename(item); }));
if (item.type !== 'folder') menu.appendChild(menuItem('Duplicate', 'duplicate', function () { duplicate(item); }));
menu.appendChild(menuItem('Cut', 'cut', cutSelection));
menu.appendChild(menuItem('Copy', 'copy', copySelection));
menu.appendChild(menuItem('Trash', 'trash', trashSelection));