feat: add browser inbox archive flow
This commit is contained in:
parent
0db9f896ce
commit
b45a56b09d
|
|
@ -238,8 +238,10 @@
|
|||
fileDataBase64: text(payload.fileDataBase64).trim(),
|
||||
source: text(payload.source).trim(),
|
||||
browserName: text(payload.browserName).trim(),
|
||||
// The receiver provides the active workspace. Untagged captures remain unassigned.
|
||||
workspaceRootPath: workspaceFromPayload(payload)
|
||||
workspaceRootPath: workspaceFromPayload(payload),
|
||||
workspaceId: text(payload.workspaceId).trim(),
|
||||
workspaceState: text(payload.workspaceState || 'unassigned').trim(),
|
||||
workspaceTrashId: text(payload.workspaceTrashId).trim()
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +260,6 @@
|
|||
return value.filter(function (item) {
|
||||
return item && typeof item === 'object' && item.captureId;
|
||||
}).map(function (item) {
|
||||
// Workspace root paths are the current stable identifiers; core has no immutable workspace ID yet.
|
||||
var workspaceRootPath = cleanWorkspace(item.workspaceRootPath) || workspaceFromStorageKey(storageKey);
|
||||
return {
|
||||
captureId: text(item.captureId),
|
||||
|
|
@ -278,6 +279,10 @@
|
|||
browserName: text(item.browserName),
|
||||
workspaceRootPath: workspaceRootPath,
|
||||
workspaceName: cleanWorkspace(item.workspaceName || workspaceRootPath),
|
||||
workspaceId: text(item.workspaceId),
|
||||
workspaceState: text(item.workspaceState || (workspaceRootPath ? 'active' : 'unassigned')),
|
||||
workspaceTrashId: text(item.workspaceTrashId),
|
||||
globalState: text(item.globalState || 'inbox') === 'archived' ? 'archived' : 'inbox',
|
||||
processed: item.processed === true,
|
||||
_storageKey: storageKey || ''
|
||||
};
|
||||
|
|
@ -304,6 +309,10 @@
|
|||
browserName: item.browserName,
|
||||
workspaceRootPath: item.workspaceRootPath,
|
||||
workspaceName: item.workspaceName || item.workspaceRootPath || '',
|
||||
workspaceId: item.workspaceId || '',
|
||||
workspaceState: item.workspaceState || (item.workspaceRootPath ? 'active' : 'unassigned'),
|
||||
workspaceTrashId: item.workspaceTrashId || '',
|
||||
globalState: item.globalState === 'archived' ? 'archived' : 'inbox',
|
||||
processed: item.processed === true
|
||||
};
|
||||
});
|
||||
|
|
@ -381,7 +390,8 @@
|
|||
el('option', { value: 'all', textContent: tr('ui.allCaptures', null, 'All captures') }),
|
||||
el('option', { value: 'unassigned', textContent: tr('ui.unassigned', null, 'Unassigned') }),
|
||||
el('option', { value: 'unprocessed', textContent: tr('ui.unprocessed', null, 'Unprocessed') }),
|
||||
el('option', { value: 'processed', textContent: tr('ui.processed', null, 'Processed') })
|
||||
el('option', { value: 'processed', textContent: tr('ui.processed', null, 'Processed') }),
|
||||
el('option', { value: 'archived', textContent: tr('ui.archive', null, 'Archive') })
|
||||
]);
|
||||
var workspaceFilterEl = el('select', {
|
||||
className: 'browser-inbox-select',
|
||||
|
|
@ -468,6 +478,8 @@
|
|||
var workspaceRoot = cleanWorkspace(capture && capture.workspaceRootPath);
|
||||
if (scope.mode === 'workspace' && workspaceRoot !== scope.workspaceRoot) return false;
|
||||
if (scope.mode === 'global' && workspaceFilter && workspaceRoot !== workspaceFilter) return false;
|
||||
if (statusFilter === 'archived' && capture.globalState !== 'archived') return false;
|
||||
if (statusFilter !== 'archived' && capture.globalState === 'archived') return false;
|
||||
if (statusFilter === 'unassigned' && workspaceRoot) return false;
|
||||
if (statusFilter === 'unprocessed' && capture.processed === true) return false;
|
||||
if (statusFilter === 'processed' && capture.processed !== true) return false;
|
||||
|
|
@ -505,13 +517,15 @@
|
|||
});
|
||||
}
|
||||
|
||||
function removeCaptures(captureIds, successText) {
|
||||
function archiveCaptures(captureIds, successText) {
|
||||
var ids = {};
|
||||
captureIds.forEach(function (captureId) {
|
||||
ids[captureId] = true;
|
||||
});
|
||||
return publishMutation('delete', { captureIds: captureIds }, function () {
|
||||
return !captures.some(function (capture) { return ids[capture.captureId]; });
|
||||
return publishMutation('archive', { captureIds: captureIds }, function () {
|
||||
return captures.every(function (capture) {
|
||||
return !ids[capture.captureId] || capture.globalState === 'archived';
|
||||
});
|
||||
}).then(function (saved) {
|
||||
if (!saved) return;
|
||||
if (ids[selectedId]) selectedId = '';
|
||||
|
|
@ -525,7 +539,7 @@
|
|||
var ids = scope.mode === 'global'
|
||||
? captures.map(function (capture) { return capture.captureId; })
|
||||
: captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).map(function (capture) { return capture.captureId; });
|
||||
return removeCaptures(ids, scope.mode === 'global' ? 'Inbox cleared' : 'Workspace captures cleared');
|
||||
return archiveCaptures(ids, scope.mode === 'global' ? 'Inbox archived' : 'Workspace captures archived');
|
||||
}
|
||||
|
||||
function selectedCapture() {
|
||||
|
|
@ -578,8 +592,33 @@
|
|||
});
|
||||
}
|
||||
|
||||
function removeCapture(captureId) {
|
||||
return removeCaptures([captureId], 'Capture deleted');
|
||||
function archiveCapture(captureId) {
|
||||
return archiveCaptures([captureId], 'Capture archived');
|
||||
}
|
||||
|
||||
function restoreCapture(captureId) {
|
||||
return publishMutation('restore', { captureId: captureId }, function () {
|
||||
return captures.some(function (capture) {
|
||||
return capture.captureId === captureId && capture.globalState === 'inbox';
|
||||
});
|
||||
}).then(function (saved) {
|
||||
if (!saved) return;
|
||||
statusText = 'Capture restored to Inbox';
|
||||
statusClass = '';
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
function permanentlyDeleteCapture(captureId) {
|
||||
return publishMutation('delete', { captureId: captureId, permanent: true }, function () {
|
||||
return !captures.some(function (capture) { return capture.captureId === captureId; });
|
||||
}).then(function (saved) {
|
||||
if (!saved) return;
|
||||
if (selectedId === captureId) selectedId = '';
|
||||
statusText = 'Capture permanently deleted';
|
||||
statusClass = '';
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
function setProcessed(captureId, processed) {
|
||||
|
|
@ -630,7 +669,7 @@
|
|||
}).then(function () {
|
||||
statusText = 'Created note: ' + notePath;
|
||||
statusClass = '';
|
||||
return removeCapture(capture.captureId);
|
||||
return archiveCapture(capture.captureId);
|
||||
}).catch(function (err) {
|
||||
statusText = 'Could not create note: ' + (err && err.message ? err.message : String(err));
|
||||
statusClass = 'error';
|
||||
|
|
@ -670,7 +709,7 @@
|
|||
}).then(function () {
|
||||
statusText = 'Created link: ' + linkPath;
|
||||
statusClass = '';
|
||||
return removeCapture(capture.captureId);
|
||||
return archiveCapture(capture.captureId);
|
||||
}).catch(function (err) {
|
||||
statusText = 'Could not create link: ' + (err && err.message ? err.message : String(err));
|
||||
statusClass = 'error';
|
||||
|
|
@ -717,7 +756,7 @@
|
|||
}).then(function () {
|
||||
statusText = 'Created file: ' + filePath;
|
||||
statusClass = '';
|
||||
return removeCapture(capture.captureId);
|
||||
return archiveCapture(capture.captureId);
|
||||
}).catch(function (err) {
|
||||
statusText = 'Could not create file: ' + (err && err.message ? err.message : String(err));
|
||||
statusClass = 'error';
|
||||
|
|
@ -759,7 +798,8 @@
|
|||
el('span', {
|
||||
className: 'browser-inbox-badge' + (capture.processed ? ' processed' : ''),
|
||||
textContent: capture.processed ? 'Processed' : 'Unprocessed'
|
||||
})
|
||||
}),
|
||||
capture.globalState === 'archived' ? el('span', { className: 'browser-inbox-badge', textContent: 'Archived' }) : null
|
||||
]));
|
||||
if (capture.text) {
|
||||
row.appendChild(el('div', { className: 'browser-inbox-row-text', textContent: capture.text }));
|
||||
|
|
@ -869,12 +909,31 @@
|
|||
}));
|
||||
}
|
||||
}
|
||||
if (capture.globalState === 'archived') {
|
||||
actionButtons.push(el('button', {
|
||||
className: 'browser-inbox-btn',
|
||||
'data-browser-inbox-action': 'restore',
|
||||
textContent: 'Restore to Inbox',
|
||||
onClick: function () {
|
||||
restoreCapture(capture.captureId);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
actionButtons.push(el('button', {
|
||||
className: 'browser-inbox-btn',
|
||||
'data-browser-inbox-action': 'archive',
|
||||
textContent: 'Archive',
|
||||
onClick: function () {
|
||||
archiveCapture(capture.captureId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
actionButtons.push(el('button', {
|
||||
className: 'browser-inbox-btn danger',
|
||||
'data-browser-inbox-action': 'remove',
|
||||
textContent: tr('ui.delete', null, 'Delete'),
|
||||
'data-browser-inbox-action': 'delete-permanently',
|
||||
textContent: 'Delete permanently',
|
||||
onClick: function () {
|
||||
removeCapture(capture.captureId);
|
||||
permanentlyDeleteCapture(capture.captureId);
|
||||
}
|
||||
}));
|
||||
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-actions' }, actionButtons));
|
||||
|
|
|
|||
|
|
@ -182,6 +182,8 @@ function makeApi(initialSettings = {}) {
|
|||
const next = captures.flatMap((capture) => {
|
||||
if (!ids.has(capture.captureId)) return [capture];
|
||||
if (payload.action === 'delete') return [];
|
||||
if (payload.action === 'archive') return [{ ...capture, globalState: 'archived' }];
|
||||
if (payload.action === 'restore') return [{ ...capture, globalState: 'inbox' }];
|
||||
if (payload.action === 'assign') return [{ ...capture, workspaceRootPath: payload.workspaceRootPath || '', workspaceName: payload.workspaceRootPath || '' }];
|
||||
if (payload.action === 'processed') return [{ ...capture, processed: payload.processed === true }];
|
||||
return [capture];
|
||||
|
|
@ -424,8 +426,8 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
|||
if (!clearButton) throw new Error('clear button not found');
|
||||
clearButton.click();
|
||||
await flush();
|
||||
if (api.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'capture-1')) {
|
||||
throw new Error('workspace clear action did not remove the Project capture');
|
||||
if (!api.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'capture-1' && capture.globalState === 'archived')) {
|
||||
throw new Error('workspace clear action did not archive the Project capture');
|
||||
}
|
||||
if (!api.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'capture-2')) {
|
||||
throw new Error('workspace clear action removed a capture from another workspace');
|
||||
|
|
@ -632,7 +634,8 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
|||
if (!assignmentApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'assignment-unassigned' && capture.processed === false)) {
|
||||
throw new Error('mark unprocessed did not persist state');
|
||||
}
|
||||
const deleteButton = walk(assignmentView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'remove');
|
||||
const deleteButton = walk(assignmentView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'delete-permanently');
|
||||
if (!deleteButton) throw new Error('permanent delete action was not rendered');
|
||||
deleteButton.click();
|
||||
await flush();
|
||||
if (assignmentApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'assignment-unassigned')) {
|
||||
|
|
@ -640,6 +643,37 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
|||
}
|
||||
component.unmount && component.unmount(assignmentView.container);
|
||||
|
||||
const archiveApi = makeApi({
|
||||
'captures:global': [{
|
||||
captureId: 'archived-capture',
|
||||
capturedAt: '2026-07-12T10:00:00.000Z',
|
||||
kind: 'page',
|
||||
url: 'https://example.com/archive',
|
||||
title: 'Archived capture',
|
||||
workspaceRootPath: 'Project',
|
||||
globalState: 'archived',
|
||||
}],
|
||||
});
|
||||
const archiveView = await mountWithApi(archiveApi, {});
|
||||
if (walk(archiveView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'archived-capture')) {
|
||||
throw new Error('archived capture leaked into the active inbox');
|
||||
}
|
||||
const archiveFilter = walk(archiveView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-filter') === 'status');
|
||||
archiveFilter.value = 'archived';
|
||||
archiveFilter.dispatchEvent('change');
|
||||
await flush();
|
||||
if (!walk(archiveView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'archived-capture')) {
|
||||
throw new Error('archive filter did not reveal archived capture');
|
||||
}
|
||||
const restoreButton = walk(archiveView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'restore');
|
||||
if (!restoreButton) throw new Error('restore archived capture action was not rendered');
|
||||
restoreButton.click();
|
||||
await flush();
|
||||
if (!archiveApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'archived-capture' && capture.globalState === 'inbox')) {
|
||||
throw new Error('restore archived capture did not return it to Inbox');
|
||||
}
|
||||
component.unmount && component.unmount(archiveView.container);
|
||||
|
||||
const conversionApi = makeApi({
|
||||
'captures:workspace:Project': [{
|
||||
captureId: 'convert-selection',
|
||||
|
|
@ -669,8 +703,8 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
|||
if (!noteWrite.content.includes('# Example Article')) throw new Error('note content missing heading');
|
||||
if (!noteWrite.content.includes('Source: https://example.com/article')) throw new Error('note content missing source URL');
|
||||
if (!noteWrite.content.includes('Selected text from the page')) throw new Error('note content missing selected text');
|
||||
if (conversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-selection')) {
|
||||
throw new Error('converted capture was not removed from queue');
|
||||
if (!conversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-selection' && capture.globalState === 'archived')) {
|
||||
throw new Error('converted capture was not archived');
|
||||
}
|
||||
const convertedEvent = conversionApi.publishedEvents.find((event) => event.name === 'browser.capture.converted');
|
||||
if (!convertedEvent) throw new Error('browser.capture.converted event was not published');
|
||||
|
|
@ -734,8 +768,8 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
|||
}
|
||||
if (!linkWrite.content.includes('[InternetShortcut]')) throw new Error('link content missing InternetShortcut header');
|
||||
if (!linkWrite.content.includes('URL=https://example.com/article')) throw new Error('link content missing URL');
|
||||
if (linkConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-link')) {
|
||||
throw new Error('converted link capture was not removed from queue');
|
||||
if (!linkConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-link' && capture.globalState === 'archived')) {
|
||||
throw new Error('converted link capture was not archived');
|
||||
}
|
||||
const convertedLinkEvent = linkConversionApi.publishedEvents.find((event) => event.name === 'browser.capture.converted');
|
||||
if (!convertedLinkEvent) throw new Error('browser.capture.converted link event was not published');
|
||||
|
|
@ -802,8 +836,8 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
|||
if (fileWrite.options.createIfMissing !== true || fileWrite.options.overwrite !== false) {
|
||||
throw new Error(`file write options mismatch: ${JSON.stringify(fileWrite.options)}`);
|
||||
}
|
||||
if (fileConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-file')) {
|
||||
throw new Error('converted file capture was not removed from queue');
|
||||
if (!fileConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-file' && capture.globalState === 'archived')) {
|
||||
throw new Error('converted file capture was not archived');
|
||||
}
|
||||
const convertedFileEvent = fileConversionApi.publishedEvents.find((event) => event.name === 'browser.capture.converted');
|
||||
if (!convertedFileEvent) throw new Error('browser.capture.converted file event was not published');
|
||||
|
|
|
|||
Loading…
Reference in New Issue