feat: host file and note actions
This commit is contained in:
parent
3b7258ed3e
commit
48fe942028
|
|
@ -310,6 +310,8 @@
|
||||||
var createMode = '';
|
var createMode = '';
|
||||||
var renameTarget = null;
|
var renameTarget = null;
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
|
var fileActions = [];
|
||||||
|
var contextMenuEntries = [];
|
||||||
var historyStack = Array.isArray(savedHistory.stack) && savedHistory.stack.length ? savedHistory.stack.map(cleanPath) : [currentPath];
|
var historyStack = Array.isArray(savedHistory.stack) && savedHistory.stack.length ? savedHistory.stack.map(cleanPath) : [currentPath];
|
||||||
var historyIndex = Math.max(0, Math.min(Number(savedHistory.index) || 0, historyStack.length - 1));
|
var historyIndex = Math.max(0, Math.min(Number(savedHistory.index) || 0, historyStack.length - 1));
|
||||||
if (historyStack[historyIndex] !== currentPath) {
|
if (historyStack[historyIndex] !== currentPath) {
|
||||||
|
|
@ -589,6 +591,34 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function contributionContextMatches(item, entry) {
|
||||||
|
var context = String(item && item.context || '').toLowerCase();
|
||||||
|
if (!context || context === '*' || context === 'files' || context === 'vault-entry') return true;
|
||||||
|
if (context === 'file') return !entry || entry.type !== 'folder';
|
||||||
|
if (context === 'folder' || context === 'directory') return !entry || entry.type === 'folder';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadContributionActions() {
|
||||||
|
var contributions = api && api.contributions;
|
||||||
|
if (!contributions || typeof contributions.list !== 'function') return;
|
||||||
|
Promise.all([
|
||||||
|
contributions.list('fileActions'),
|
||||||
|
contributions.list('contextMenuEntries')
|
||||||
|
]).then(function (result) {
|
||||||
|
if (disposed) return;
|
||||||
|
fileActions = (result[0] || []).filter(function (item) {
|
||||||
|
return item && item.pluginId && item.handler && item.label;
|
||||||
|
});
|
||||||
|
contextMenuEntries = (result[1] || []).filter(function (item) {
|
||||||
|
return item && item.pluginId && item.handler && item.label && contributionContextMatches(item, null);
|
||||||
|
});
|
||||||
|
renderList();
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('[files] contribution actions:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function navigateTo(path) {
|
function navigateTo(path) {
|
||||||
var newPath = cleanPath(path);
|
var newPath = cleanPath(path);
|
||||||
if (!navigatingHistory) {
|
if (!navigatingHistory) {
|
||||||
|
|
@ -809,6 +839,37 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function executeContributionAction(action, entry) {
|
||||||
|
if (!action || !entry || !api.commands || typeof api.commands.executeFor !== 'function') return;
|
||||||
|
api.commands.executeFor(action.pluginId, action.handler, {
|
||||||
|
source: 'files',
|
||||||
|
actionId: action.id,
|
||||||
|
path: entry.relativePath,
|
||||||
|
entry: entry,
|
||||||
|
currentPath: scopedPath(currentPath),
|
||||||
|
workspaceRootPath: workspaceRoot
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('[files] contribution action failed:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendContributionMenuItems(entry) {
|
||||||
|
var menuItems = [];
|
||||||
|
fileActions.forEach(function (action) {
|
||||||
|
menuItems.push(action);
|
||||||
|
});
|
||||||
|
contextMenuEntries.forEach(function (action) {
|
||||||
|
if (contributionContextMatches(action, entry)) menuItems.push(action);
|
||||||
|
});
|
||||||
|
if (menuItems.length === 0) return;
|
||||||
|
ctxMenu.appendChild(ctxSep());
|
||||||
|
menuItems.forEach(function (action) {
|
||||||
|
ctxMenu.appendChild(ctxItem(action.label, '', function () {
|
||||||
|
executeContributionAction(action, entry);
|
||||||
|
}, 'contribution-' + action.id, action.icon || 'open'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showCtxMenu(x, y, entry) {
|
function showCtxMenu(x, y, entry) {
|
||||||
ctxTarget = entry;
|
ctxTarget = entry;
|
||||||
ctxMenu.innerHTML = '';
|
ctxMenu.innerHTML = '';
|
||||||
|
|
@ -823,6 +884,7 @@
|
||||||
ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }, 'open', 'open'));
|
ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }, 'open', 'open'));
|
||||||
ctxMenu.appendChild(ctxItem('Open External', '', function () { openExternalEntry(entry, 'external'); }, 'open-external', 'external'));
|
ctxMenu.appendChild(ctxItem('Open External', '', function () { openExternalEntry(entry, 'external'); }, 'open-external', 'external'));
|
||||||
ctxMenu.appendChild(ctxItem('Show in Explorer', '', function () { openExternalEntry(entry, 'explorer'); }, 'show-in-explorer', 'explorer'));
|
ctxMenu.appendChild(ctxItem('Show in Explorer', '', function () { openExternalEntry(entry, 'explorer'); }, 'show-in-explorer', 'explorer'));
|
||||||
|
appendContributionMenuItems(entry);
|
||||||
ctxMenu.appendChild(ctxSep());
|
ctxMenu.appendChild(ctxSep());
|
||||||
ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename'));
|
ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename'));
|
||||||
if (entry.type !== 'folder') {
|
if (entry.type !== 'folder') {
|
||||||
|
|
@ -1248,6 +1310,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
updateHistoryButtons();
|
updateHistoryButtons();
|
||||||
|
loadContributionActions();
|
||||||
loadEntries();
|
loadEntries();
|
||||||
|
|
||||||
containerEl.__filesCleanup = function () {
|
containerEl.__filesCleanup = function () {
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@
|
||||||
var statusText = '';
|
var statusText = '';
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
|
var noteActions = [];
|
||||||
|
|
||||||
function notesParent() {
|
function notesParent() {
|
||||||
return workspaceRoot || '';
|
return workspaceRoot || '';
|
||||||
|
|
@ -313,6 +314,43 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadContributionActions() {
|
||||||
|
var contributions = api && api.contributions;
|
||||||
|
if (!contributions || typeof contributions.list !== 'function') return;
|
||||||
|
contributions.list('noteActions').then(function (result) {
|
||||||
|
if (disposed) return;
|
||||||
|
noteActions = (result || []).filter(function (item) {
|
||||||
|
return item && item.pluginId && item.handler && item.label;
|
||||||
|
});
|
||||||
|
renderList();
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('[notes] contribution actions:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionInitial(action) {
|
||||||
|
var label = String(action && action.label || '').trim();
|
||||||
|
return label ? label.charAt(0).toUpperCase() : '+';
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeContributionAction(action, note) {
|
||||||
|
if (!action || !note || !api.commands || typeof api.commands.executeFor !== 'function') return;
|
||||||
|
setStatus(action.label + '...', 'loading');
|
||||||
|
api.commands.executeFor(action.pluginId, action.handler, {
|
||||||
|
source: 'notes',
|
||||||
|
actionId: action.id,
|
||||||
|
path: note.path,
|
||||||
|
note: note,
|
||||||
|
notesScopePath: notesParent(),
|
||||||
|
workspaceRootPath: workspaceRoot
|
||||||
|
}).then(function () {
|
||||||
|
if (!disposed) setStatus(action.label + ' complete', 'success');
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('[notes] contribution action failed:', err);
|
||||||
|
if (!disposed) setStatus('Error: ' + (err && err.message ? err.message : err), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
if (!notes || notes.length === 0) {
|
if (!notes || notes.length === 0) {
|
||||||
|
|
@ -320,6 +358,38 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
notes.forEach(function (note) {
|
notes.forEach(function (note) {
|
||||||
|
var actionButtons = [
|
||||||
|
el('button', {
|
||||||
|
className: 'notes-item-btn',
|
||||||
|
title: 'Open',
|
||||||
|
innerHTML: iconSvg('open'),
|
||||||
|
onClick: function (e) { e.stopPropagation(); openNote(note); }
|
||||||
|
}),
|
||||||
|
el('button', {
|
||||||
|
className: 'notes-item-btn',
|
||||||
|
title: 'Rename',
|
||||||
|
'data-note-action': 'rename',
|
||||||
|
innerHTML: iconSvg('rename'),
|
||||||
|
onClick: function (e) { e.stopPropagation(); beginRename(note); }
|
||||||
|
}),
|
||||||
|
el('button', {
|
||||||
|
className: 'notes-item-btn',
|
||||||
|
title: 'Move to Trash',
|
||||||
|
'data-note-action': 'trash',
|
||||||
|
innerHTML: iconSvg('trash'),
|
||||||
|
onClick: function (e) { e.stopPropagation(); confirmTrashNote(note); }
|
||||||
|
})
|
||||||
|
];
|
||||||
|
noteActions.forEach(function (action) {
|
||||||
|
actionButtons.push(el('button', {
|
||||||
|
className: 'notes-item-btn',
|
||||||
|
title: action.label,
|
||||||
|
'aria-label': action.label,
|
||||||
|
'data-note-contribution-action': action.id,
|
||||||
|
textContent: actionInitial(action),
|
||||||
|
onClick: function (e) { e.stopPropagation(); executeContributionAction(action, note); }
|
||||||
|
}));
|
||||||
|
});
|
||||||
var row = el('div', {
|
var row = el('div', {
|
||||||
className: 'notes-item' + (note.path === selectedPath ? ' selected' : ''),
|
className: 'notes-item' + (note.path === selectedPath ? ' selected' : ''),
|
||||||
'data-note-path': note.path,
|
'data-note-path': note.path,
|
||||||
|
|
@ -329,28 +399,7 @@
|
||||||
}, [
|
}, [
|
||||||
el('span', { className: 'notes-item-icon', innerHTML: iconSvg('note') }),
|
el('span', { className: 'notes-item-icon', innerHTML: iconSvg('note') }),
|
||||||
el('span', { className: 'notes-item-name', textContent: note.title || fileName(note.path), title: note.title || note.path }),
|
el('span', { className: 'notes-item-name', textContent: note.title || fileName(note.path), title: note.title || note.path }),
|
||||||
el('span', { className: 'notes-item-actions' }, [
|
el('span', { className: 'notes-item-actions' }, actionButtons)
|
||||||
el('button', {
|
|
||||||
className: 'notes-item-btn',
|
|
||||||
title: 'Open',
|
|
||||||
innerHTML: iconSvg('open'),
|
|
||||||
onClick: function (e) { e.stopPropagation(); openNote(note); }
|
|
||||||
}),
|
|
||||||
el('button', {
|
|
||||||
className: 'notes-item-btn',
|
|
||||||
title: 'Rename',
|
|
||||||
'data-note-action': 'rename',
|
|
||||||
innerHTML: iconSvg('rename'),
|
|
||||||
onClick: function (e) { e.stopPropagation(); beginRename(note); }
|
|
||||||
}),
|
|
||||||
el('button', {
|
|
||||||
className: 'notes-item-btn',
|
|
||||||
title: 'Move to Trash',
|
|
||||||
'data-note-action': 'trash',
|
|
||||||
innerHTML: iconSvg('trash'),
|
|
||||||
onClick: function (e) { e.stopPropagation(); confirmTrashNote(note); }
|
|
||||||
})
|
|
||||||
])
|
|
||||||
]);
|
]);
|
||||||
listContainer.appendChild(row);
|
listContainer.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
@ -547,6 +596,7 @@
|
||||||
|
|
||||||
// ─── Init ───────────────────────────────────────────────
|
// ─── Init ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
loadContributionActions();
|
||||||
loadNotes();
|
loadNotes();
|
||||||
|
|
||||||
containerEl.__notesCleanup = function () {
|
containerEl.__notesCleanup = function () {
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,10 @@ function loadFilesComponent(document) {
|
||||||
|
|
||||||
function makeApi() {
|
function makeApi() {
|
||||||
const externalCalls = [];
|
const externalCalls = [];
|
||||||
|
const contributionCalls = [];
|
||||||
return {
|
return {
|
||||||
externalCalls,
|
externalCalls,
|
||||||
|
contributionCalls,
|
||||||
files: {
|
files: {
|
||||||
list: async () => [
|
list: async () => [
|
||||||
{
|
{
|
||||||
|
|
@ -222,6 +224,34 @@ function makeApi() {
|
||||||
workbench: {
|
workbench: {
|
||||||
openResource: async () => ({ status: 'opened' }),
|
openResource: async () => ({ status: 'opened' }),
|
||||||
},
|
},
|
||||||
|
contributions: {
|
||||||
|
list: async (point) => {
|
||||||
|
if (point === 'fileActions') {
|
||||||
|
return [{
|
||||||
|
pluginId: 'provider.plugin',
|
||||||
|
id: 'provider.file.action',
|
||||||
|
label: 'Provider File Action',
|
||||||
|
handler: 'provider.command',
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
if (point === 'contextMenuEntries') {
|
||||||
|
return [{
|
||||||
|
pluginId: 'provider.plugin',
|
||||||
|
id: 'provider.file.context',
|
||||||
|
label: 'Provider Context Action',
|
||||||
|
context: 'file',
|
||||||
|
handler: 'provider.context',
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commands: {
|
||||||
|
executeFor: async (pluginId, commandId, args) => {
|
||||||
|
contributionCalls.push({ pluginId, commandId, args });
|
||||||
|
return { status: 'handled' };
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,6 +297,17 @@ async function flush() {
|
||||||
throw new Error(`expected no copied path after successful external API calls, got ${clipboard.written.join(', ')}`);
|
throw new Error(`expected no copied path after successful external API calls, got ${clipboard.written.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list.dispatchEvent('contextmenu', { target: row, clientX: 20, clientY: 20 });
|
||||||
|
const providerAction = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-menu-action') === 'contribution-provider.file.action');
|
||||||
|
if (!providerAction) throw new Error('provider file action menu item not found');
|
||||||
|
const providerContext = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-menu-action') === 'contribution-provider.file.context');
|
||||||
|
if (!providerContext) throw new Error('provider context menu item not found');
|
||||||
|
providerAction.click();
|
||||||
|
await flush();
|
||||||
|
if (!api.contributionCalls.some((call) => call.pluginId === 'provider.plugin' && call.commandId === 'provider.command' && call.args.path === 'Docs/readme.md')) {
|
||||||
|
throw new Error(`expected provider file action call, got ${JSON.stringify(api.contributionCalls)}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('files frontend smoke passed');
|
console.log('files frontend smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
|
|
@ -132,9 +132,11 @@ function loadNotesComponent(document) {
|
||||||
function makeApi(options = {}) {
|
function makeApi(options = {}) {
|
||||||
const entries = new Map();
|
const entries = new Map();
|
||||||
const opened = [];
|
const opened = [];
|
||||||
|
const contributionCalls = [];
|
||||||
return {
|
return {
|
||||||
entries,
|
entries,
|
||||||
opened,
|
opened,
|
||||||
|
contributionCalls,
|
||||||
files: {
|
files: {
|
||||||
list: async (relativeDir) => {
|
list: async (relativeDir) => {
|
||||||
const prefix = relativeDir ? `${relativeDir}/` : '';
|
const prefix = relativeDir ? `${relativeDir}/` : '';
|
||||||
|
|
@ -187,6 +189,25 @@ function makeApi(options = {}) {
|
||||||
return { status: 'opened' };
|
return { status: 'opened' };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
contributions: {
|
||||||
|
list: async (point) => {
|
||||||
|
if (point === 'noteActions') {
|
||||||
|
return [{
|
||||||
|
pluginId: 'provider.plugin',
|
||||||
|
id: 'provider.note.action',
|
||||||
|
label: 'Provider Note Action',
|
||||||
|
handler: 'provider.noteCommand',
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commands: {
|
||||||
|
executeFor: async (pluginId, commandId, args) => {
|
||||||
|
contributionCalls.push({ pluginId, commandId, args });
|
||||||
|
return { status: 'handled' };
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,6 +260,14 @@ async function mountNotes(api) {
|
||||||
throw new Error('create note did not open the newly created file');
|
throw new Error('create note did not open the newly created file');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providerAction = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-contribution-action') === 'provider.note.action');
|
||||||
|
if (!providerAction) throw new Error('provider note action button not found');
|
||||||
|
providerAction.click();
|
||||||
|
await flush();
|
||||||
|
if (!createApi.contributionCalls.some((call) => call.pluginId === 'provider.plugin' && call.commandId === 'provider.noteCommand' && call.args.path === 'Project/Notes/First_Note.md')) {
|
||||||
|
throw new Error(`expected provider note action call, got ${JSON.stringify(createApi.contributionCalls)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const trashButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-action') === 'trash');
|
const trashButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-action') === 'trash');
|
||||||
if (!trashButton) throw new Error('trash note button not found');
|
if (!trashButton) throw new Error('trash note button not found');
|
||||||
trashButton.click();
|
trashButton.click();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue