feat: refresh files and notes on file events

This commit is contained in:
mirivlad 2026-06-28 22:57:09 +08:00
parent 914c8a7204
commit 7b249fcf48
6 changed files with 106 additions and 0 deletions

View File

@ -335,6 +335,13 @@
return full.indexOf(workspaceRoot + '/') === 0 ? full.slice(workspaceRoot.length + 1) : full; return full.indexOf(workspaceRoot + '/') === 0 ? full.slice(workspaceRoot.length + 1) : full;
} }
function isWorkspaceEvent(event) {
var payload = (event && event.payload) || {};
var path = cleanPath(payload.path || '');
if (!path) return true;
return !workspaceRoot || path === workspaceRoot || path.indexOf(workspaceRoot + '/') === 0;
}
var toolbar = el('div', { className: 'files-toolbar' }); var toolbar = el('div', { className: 'files-toolbar' });
var breadcrumb = el('div', { className: 'files-breadcrumb' }); var breadcrumb = el('div', { className: 'files-breadcrumb' });
var backBtn = iconButton('back', 'Back', 'back', goBack); var backBtn = iconButton('back', 'Back', 'back', goBack);
@ -1412,8 +1419,22 @@
loadContributionActions(); loadContributionActions();
loadEntries(); loadEntries();
var fileChangedUnsubscribe = null;
if (api.events && typeof api.events.subscribe === 'function') {
api.events.subscribe('file.changed', function (event) {
if (disposed || !isWorkspaceEvent(event)) return;
if (showingTrash) loadTrashEntries();
else loadEntries();
}).then(function (unsubscribe) {
fileChangedUnsubscribe = unsubscribe;
}).catch(function (err) {
console.error('[files] file.changed subscription:', err);
});
}
containerEl.__filesCleanup = function () { containerEl.__filesCleanup = function () {
disposed = true; disposed = true;
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
document.removeEventListener('click', onDocClick); document.removeEventListener('click', onDocClick);
document.removeEventListener('keydown', onDocKeydown); document.removeEventListener('keydown', onDocKeydown);
window.removeEventListener('mousedown', handleMouseHistory, true); window.removeEventListener('mousedown', handleMouseHistory, true);

View File

@ -19,6 +19,7 @@
"files.write", "files.write",
"files.delete", "files.delete",
"files.openExternal", "files.openExternal",
"events.subscribe",
"workbench.open", "workbench.open",
"ui.register" "ui.register"
], ],

View File

@ -345,6 +345,14 @@
}); });
} }
function isNotesEvent(event) {
var payload = (event && event.payload) || {};
var changedPath = cleanPath(payload.path || '');
if (!changedPath) return true;
var parent = notesParent();
return changedPath === parent || changedPath.indexOf(parent + '/') === 0;
}
function actionInitial(action) { function actionInitial(action) {
var label = String(action && action.label || '').trim(); var label = String(action && action.label || '').trim();
return label ? label.charAt(0).toUpperCase() : '+'; return label ? label.charAt(0).toUpperCase() : '+';
@ -640,8 +648,21 @@
loadContributionActions(); loadContributionActions();
loadNotes(); loadNotes();
var fileChangedUnsubscribe = null;
if (api.events && typeof api.events.subscribe === 'function') {
api.events.subscribe('file.changed', function (event) {
if (disposed || !isNotesEvent(event)) return;
loadNotes();
}).then(function (unsubscribe) {
fileChangedUnsubscribe = unsubscribe;
}).catch(function (err) {
console.error('[notes] file.changed subscription:', err);
});
}
containerEl.__notesCleanup = function () { containerEl.__notesCleanup = function () {
disposed = true; disposed = true;
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
}; };
}, },

View File

@ -18,6 +18,7 @@
"files.read", "files.read",
"files.write", "files.write",
"files.delete", "files.delete",
"events.subscribe",
"workbench.open", "workbench.open",
"ui.register" "ui.register"
], ],

View File

@ -198,7 +198,9 @@ function loadFilesComponent(document) {
function makeApi() { function makeApi() {
const externalCalls = []; const externalCalls = [];
const contributionCalls = []; const contributionCalls = [];
const eventHandlers = {};
let restored = false; let restored = false;
let externalVisible = false;
let trashEntries = [{ let trashEntries = [{
originalPath: 'Docs/deleted.md', originalPath: 'Docs/deleted.md',
trashPath: '.verstak/trash/files/mock/deleted.md', trashPath: '.verstak/trash/files/mock/deleted.md',
@ -210,6 +212,13 @@ function makeApi() {
return { return {
externalCalls, externalCalls,
contributionCalls, contributionCalls,
emitFileChanged(payload) {
(eventHandlers['file.changed'] || []).forEach((handler) => handler({
name: 'file.changed',
payload,
timestamp: new Date().toISOString(),
}));
},
files: { files: {
list: async () => { list: async () => {
const entries = [{ const entries = [{
@ -230,6 +239,16 @@ function makeApi() {
modifiedAt: '2026-06-27T01:03:00Z', modifiedAt: '2026-06-27T01:03:00Z',
}); });
} }
if (externalVisible) {
entries.push({
name: 'external.md',
relativePath: 'Docs/external.md',
type: 'file',
extension: 'md',
size: 9,
modifiedAt: '2026-06-27T01:04:00Z',
});
}
return entries; return entries;
}, },
metadata: async () => { throw new Error('not-found'); }, metadata: async () => { throw new Error('not-found'); },
@ -279,6 +298,18 @@ function makeApi() {
return { status: 'handled' }; return { status: 'handled' };
}, },
}, },
events: {
subscribe: async (eventName, handler) => {
eventHandlers[eventName] = eventHandlers[eventName] || [];
eventHandlers[eventName].push(handler);
return () => {
eventHandlers[eventName] = (eventHandlers[eventName] || []).filter((candidate) => candidate !== handler);
};
},
},
showExternalFile() {
externalVisible = true;
},
}; };
} }
@ -351,6 +382,13 @@ async function flush() {
if (!restoredRow) { if (!restoredRow) {
throw new Error(`restored file row not rendered after restore: ${container.textContent}`); throw new Error(`restored file row not rendered after restore: ${container.textContent}`);
} }
api.showExternalFile();
api.emitFileChanged({ path: 'Docs/external.md', operation: 'external.create', type: 'file' });
await flush();
const externalRow = walk(container, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/external.md');
if (!externalRow) {
throw new Error(`external file row not rendered after file.changed: ${container.textContent}`);
}
console.log('files frontend smoke passed'); console.log('files frontend smoke passed');
})().catch((err) => { })().catch((err) => {

View File

@ -137,10 +137,18 @@ function makeApi(options = {}) {
const entries = new Map(); const entries = new Map();
const opened = []; const opened = [];
const contributionCalls = []; const contributionCalls = [];
const eventHandlers = {};
return { return {
entries, entries,
opened, opened,
contributionCalls, contributionCalls,
emitFileChanged(payload) {
(eventHandlers['file.changed'] || []).forEach((handler) => handler({
name: 'file.changed',
payload,
timestamp: new Date().toISOString(),
}));
},
files: { files: {
list: async (relativeDir) => { list: async (relativeDir) => {
const prefix = relativeDir ? `${relativeDir}/` : ''; const prefix = relativeDir ? `${relativeDir}/` : '';
@ -212,6 +220,15 @@ function makeApi(options = {}) {
return { status: 'handled' }; return { status: 'handled' };
}, },
}, },
events: {
subscribe: async (eventName, handler) => {
eventHandlers[eventName] = eventHandlers[eventName] || [];
eventHandlers[eventName].push(handler);
return () => {
eventHandlers[eventName] = (eventHandlers[eventName] || []).filter((candidate) => candidate !== handler);
};
},
},
}; };
} }
@ -292,6 +309,13 @@ async function mountNotes(api) {
sortSelect.value = 'title-asc'; sortSelect.value = 'title-asc';
sortSelect.dispatchEvent('change'); sortSelect.dispatchEvent('change');
createApi.entries.set('Project/Notes/Third_Note.md', { type: 'file', content: '# Third Note\n' });
createApi.emitFileChanged({ path: 'Project/Notes/Third_Note.md', operation: 'external.create', type: 'file' });
await flush();
if (!container.textContent.includes('Third Note')) {
throw new Error(`notes list did not refresh after file.changed: ${container.textContent}`);
}
const renameButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-action') === 'rename'); const renameButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-action') === 'rename');
if (!renameButton) throw new Error('rename note button not found'); if (!renameButton) throw new Error('rename note button not found');
renameButton.click(); renameButton.click();