feat: open browser links and resolve filename collisions
This commit is contained in:
parent
b45a56b09d
commit
50c4fc5e68
|
|
@ -183,6 +183,12 @@
|
||||||
return safeNoteFilename(title).replace(/\.md$/, '.url');
|
return safeNoteFilename(title).replace(/\.md$/, '.url');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function numberedLinkFilename(title, number) {
|
||||||
|
var filename = safeLinkFilename(title);
|
||||||
|
if (number <= 1) return filename;
|
||||||
|
return filename.replace(/\.url$/, ' (' + number + ').url');
|
||||||
|
}
|
||||||
|
|
||||||
function safeFileFilename(name) {
|
function safeFileFilename(name) {
|
||||||
var base = text(name).trim()
|
var base = text(name).trim()
|
||||||
.replace(/[\\/:*?"<>|\r\n\t]+/g, '_')
|
.replace(/[\\/:*?"<>|\r\n\t]+/g, '_')
|
||||||
|
|
@ -686,14 +692,24 @@
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
var title = noteTitle(capture);
|
var title = noteTitle(capture);
|
||||||
var linkPath = capture.workspaceRootPath + '/Links/' + safeLinkFilename(title);
|
|
||||||
statusText = 'Creating link...';
|
statusText = 'Creating link...';
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
return api.files.writeText(linkPath, captureToUrlShortcut(capture), {
|
function writeLink(number) {
|
||||||
createIfMissing: true,
|
var linkPath = capture.workspaceRootPath + '/Links/' + numberedLinkFilename(title, number);
|
||||||
overwrite: false
|
return api.files.writeText(linkPath, captureToUrlShortcut(capture), {
|
||||||
}).then(function () {
|
createIfMissing: true,
|
||||||
|
overwrite: false
|
||||||
|
}).then(function () {
|
||||||
|
return linkPath;
|
||||||
|
}).catch(function (error) {
|
||||||
|
if (number < 99 && /^conflict:/.test(text(error && error.message ? error.message : error))) {
|
||||||
|
return writeLink(number + 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return writeLink(1).then(function (linkPath) {
|
||||||
if (api.events && typeof api.events.publish === 'function') {
|
if (api.events && typeof api.events.publish === 'function') {
|
||||||
return api.events.publish('browser.capture.converted', {
|
return api.events.publish('browser.capture.converted', {
|
||||||
captureId: capture.captureId,
|
captureId: capture.captureId,
|
||||||
|
|
@ -703,11 +719,13 @@
|
||||||
title: title,
|
title: title,
|
||||||
url: capture.url || '',
|
url: capture.url || '',
|
||||||
sourcePluginId: PLUGIN_ID
|
sourcePluginId: PLUGIN_ID
|
||||||
|
}).then(function () {
|
||||||
|
return linkPath;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return undefined;
|
return linkPath;
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
statusText = 'Created link: ' + linkPath;
|
statusText = 'Created link';
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
return archiveCapture(capture.captureId);
|
return archiveCapture(capture.captureId);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
|
|
@ -717,6 +735,15 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCaptureURL(capture) {
|
||||||
|
if (!capture || !capture.url || !api || !api.files || typeof api.files.openURL !== 'function') return Promise.resolve();
|
||||||
|
return api.files.openURL(capture.url).catch(function (err) {
|
||||||
|
statusText = 'Could not open link: ' + (err && err.message ? err.message : String(err));
|
||||||
|
statusClass = 'error';
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createFileFromCapture(capture) {
|
function createFileFromCapture(capture) {
|
||||||
if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || (!capture.fileText && !capture.fileDataBase64)) return Promise.resolve();
|
if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || (!capture.fileText && !capture.fileDataBase64)) return Promise.resolve();
|
||||||
if (!api || !api.files || (capture.fileDataBase64 ? typeof api.files.writeBytes !== 'function' : typeof api.files.writeText !== 'function')) {
|
if (!api || !api.files || (capture.fileDataBase64 ? typeof api.files.writeBytes !== 'function' : typeof api.files.writeText !== 'function')) {
|
||||||
|
|
@ -879,6 +906,16 @@
|
||||||
setProcessed(capture.captureId, !capture.processed);
|
setProcessed(capture.captureId, !capture.processed);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
if (capture.url) {
|
||||||
|
actionButtons.push(el('button', {
|
||||||
|
className: 'browser-inbox-btn',
|
||||||
|
'data-browser-inbox-action': 'open-link',
|
||||||
|
textContent: 'Open link',
|
||||||
|
onClick: function () {
|
||||||
|
openCaptureURL(capture);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (capture.workspaceRootPath) {
|
if (capture.workspaceRootPath) {
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ function makeApi(initialSettings = {}) {
|
||||||
const unsubscribed = [];
|
const unsubscribed = [];
|
||||||
const fileWrites = [];
|
const fileWrites = [];
|
||||||
const fileByteWrites = [];
|
const fileByteWrites = [];
|
||||||
|
const openedURLs = [];
|
||||||
const publishedEvents = [];
|
const publishedEvents = [];
|
||||||
const workspaceEntries = [
|
const workspaceEntries = [
|
||||||
{ name: 'ClientA', relativePath: 'ClientA', type: 'folder' },
|
{ name: 'ClientA', relativePath: 'ClientA', type: 'folder' },
|
||||||
|
|
@ -202,6 +203,7 @@ function makeApi(initialSettings = {}) {
|
||||||
unsubscribed,
|
unsubscribed,
|
||||||
fileWrites,
|
fileWrites,
|
||||||
fileByteWrites,
|
fileByteWrites,
|
||||||
|
openedURLs,
|
||||||
publishedEvents,
|
publishedEvents,
|
||||||
failNextWrite(message) {
|
failNextWrite(message) {
|
||||||
nextWriteError = new Error(message || 'write failed');
|
nextWriteError = new Error(message || 'write failed');
|
||||||
|
|
@ -247,6 +249,9 @@ function makeApi(initialSettings = {}) {
|
||||||
}
|
}
|
||||||
fileByteWrites.push({ relativePath, dataBase64, options });
|
fileByteWrites.push({ relativePath, dataBase64, options });
|
||||||
},
|
},
|
||||||
|
openURL: async (url) => {
|
||||||
|
openedURLs.push(url);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
browserReceiver: {
|
browserReceiver: {
|
||||||
pairing: async () => ({ ...receiverPairing }),
|
pairing: async () => ({ ...receiverPairing }),
|
||||||
|
|
@ -756,6 +761,11 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||||
const linkConversionView = await mountWithApi(linkConversionApi);
|
const linkConversionView = await mountWithApi(linkConversionApi);
|
||||||
const createLinkButton = walk(linkConversionView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'create-link');
|
const createLinkButton = walk(linkConversionView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'create-link');
|
||||||
if (!createLinkButton) throw new Error('create link button was not rendered');
|
if (!createLinkButton) throw new Error('create link button was not rendered');
|
||||||
|
const openLinkButton = walk(linkConversionView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'open-link');
|
||||||
|
if (!openLinkButton) throw new Error('open link action was not rendered');
|
||||||
|
openLinkButton.click();
|
||||||
|
await flush();
|
||||||
|
if (linkConversionApi.openedURLs.join(',') !== 'https://example.com/article') throw new Error('open link did not use browser URL capability');
|
||||||
createLinkButton.click();
|
createLinkButton.click();
|
||||||
await flush();
|
await flush();
|
||||||
if (linkConversionApi.fileWrites.length !== 1) throw new Error(`expected one link write, got ${linkConversionApi.fileWrites.length}`);
|
if (linkConversionApi.fileWrites.length !== 1) throw new Error(`expected one link write, got ${linkConversionApi.fileWrites.length}`);
|
||||||
|
|
@ -777,6 +787,25 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||||
if (convertedLinkEvent.payload.linkPath !== 'Project/Links/Example_Article.url') throw new Error('converted link event linkPath mismatch');
|
if (convertedLinkEvent.payload.linkPath !== 'Project/Links/Example_Article.url') throw new Error('converted link event linkPath mismatch');
|
||||||
component.unmount && component.unmount(linkConversionView.container);
|
component.unmount && component.unmount(linkConversionView.container);
|
||||||
|
|
||||||
|
const collisionLinkApi = makeApi({
|
||||||
|
'captures:workspace:Project': [{
|
||||||
|
captureId: 'convert-link-collision',
|
||||||
|
capturedAt: '2026-06-29T01:25:00.000Z',
|
||||||
|
kind: 'link',
|
||||||
|
url: 'https://example.com/collision',
|
||||||
|
title: 'Example Article',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
collisionLinkApi.failNextWrite('conflict: Project/Links/Example_Article.url');
|
||||||
|
const collisionLinkView = await mountWithApi(collisionLinkApi);
|
||||||
|
walk(collisionLinkView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'create-link').click();
|
||||||
|
await flush();
|
||||||
|
if (collisionLinkApi.fileWrites.length !== 1 || collisionLinkApi.fileWrites[0].relativePath !== 'Project/Links/Example_Article (2).url') {
|
||||||
|
throw new Error('link filename collision did not use the numbered suffix');
|
||||||
|
}
|
||||||
|
component.unmount && component.unmount(collisionLinkView.container);
|
||||||
|
|
||||||
const failedLinkApi = makeApi({
|
const failedLinkApi = makeApi({
|
||||||
'captures:workspace:Project': [{
|
'captures:workspace:Project': [{
|
||||||
captureId: 'convert-link-conflict',
|
captureId: 'convert-link-conflict',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue