Scope inbox and activity by workspace
This commit is contained in:
parent
2c19dd2132
commit
b4ef3b38b0
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
var PLUGIN_ID = 'verstak.activity';
|
var PLUGIN_ID = 'verstak.activity';
|
||||||
var MAX_EVENTS = 250;
|
var MAX_EVENTS = 250;
|
||||||
|
var GLOBAL_KEY = 'events:global';
|
||||||
|
var WORKSPACE_PREFIX = 'events:workspace:';
|
||||||
var ACTIVITY_EVENTS = [
|
var ACTIVITY_EVENTS = [
|
||||||
'file.opened',
|
'file.opened',
|
||||||
'file.changed',
|
'file.changed',
|
||||||
|
|
@ -77,6 +79,41 @@
|
||||||
return String(value == null ? '' : value);
|
return String(value == null ? '' : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeKey(value) {
|
||||||
|
return encodeURIComponent(text(value).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanWorkspace(value) {
|
||||||
|
return text(value).trim().replace(/^\/+|\/+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceFromProps(props) {
|
||||||
|
var node = props && props.workspaceNode;
|
||||||
|
return cleanWorkspace((props && (props.workspaceRootPath || props.workspaceName || props.workspaceNodeId))
|
||||||
|
|| (node && (node.rootPath || node.name || node.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceFromPayload(payload) {
|
||||||
|
var explicit = cleanWorkspace(payload && (payload.workspaceRootPath || payload.workspaceName || payload.workspaceNodeId));
|
||||||
|
if (explicit) return explicit;
|
||||||
|
var path = cleanWorkspace(payload && payload.path);
|
||||||
|
if (!path || path.indexOf('/') === -1) return '';
|
||||||
|
return cleanWorkspace(path.split('/')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scopeFromProps(props) {
|
||||||
|
var workspaceRoot = workspaceFromProps(props);
|
||||||
|
if (!workspaceRoot) {
|
||||||
|
return { mode: 'global', key: GLOBAL_KEY, label: 'All workspaces', workspaceRoot: '' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mode: 'workspace',
|
||||||
|
key: WORKSPACE_PREFIX + encodeKey(workspaceRoot),
|
||||||
|
label: workspaceRoot,
|
||||||
|
workspaceRoot: workspaceRoot
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function activityId() {
|
function activityId() {
|
||||||
return 'activity-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
return 'activity-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
||||||
}
|
}
|
||||||
|
|
@ -117,8 +154,9 @@
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventToActivity(event) {
|
function eventToActivity(event, scope) {
|
||||||
var payload = eventPayload(event);
|
var payload = eventPayload(event);
|
||||||
|
var workspaceRoot = workspaceFromPayload(payload) || (scope && scope.workspaceRoot) || '';
|
||||||
return {
|
return {
|
||||||
activityId: activityId(),
|
activityId: activityId(),
|
||||||
type: text(event && event.name).trim() || 'activity.event',
|
type: text(event && event.name).trim() || 'activity.event',
|
||||||
|
|
@ -127,11 +165,12 @@
|
||||||
occurredAt: text(payload.occurredAt || payload.capturedAt || (event && event.timestamp) || new Date().toISOString()),
|
occurredAt: text(payload.occurredAt || payload.capturedAt || (event && event.timestamp) || new Date().toISOString()),
|
||||||
receivedAt: new Date().toISOString(),
|
receivedAt: new Date().toISOString(),
|
||||||
sourcePluginId: text((event && event.pluginId) || payload.pluginId || payload.sourcePluginId),
|
sourcePluginId: text((event && event.pluginId) || payload.pluginId || payload.sourcePluginId),
|
||||||
|
workspaceRootPath: workspaceRoot,
|
||||||
payload: payload
|
payload: payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function manualActivity() {
|
function manualActivity(scope) {
|
||||||
return {
|
return {
|
||||||
activityId: activityId(),
|
activityId: activityId(),
|
||||||
type: 'activity.manual',
|
type: 'activity.manual',
|
||||||
|
|
@ -140,11 +179,12 @@
|
||||||
occurredAt: new Date().toISOString(),
|
occurredAt: new Date().toISOString(),
|
||||||
receivedAt: new Date().toISOString(),
|
receivedAt: new Date().toISOString(),
|
||||||
sourcePluginId: PLUGIN_ID,
|
sourcePluginId: PLUGIN_ID,
|
||||||
|
workspaceRootPath: (scope && scope.workspaceRoot) || '',
|
||||||
payload: {}
|
payload: {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStoredEvents(value) {
|
function normalizeStoredEvents(value, storageKey) {
|
||||||
if (!Array.isArray(value)) return [];
|
if (!Array.isArray(value)) return [];
|
||||||
return value.filter(function (item) {
|
return value.filter(function (item) {
|
||||||
return item && typeof item === 'object' && item.activityId;
|
return item && typeof item === 'object' && item.activityId;
|
||||||
|
|
@ -157,11 +197,43 @@
|
||||||
occurredAt: text(item.occurredAt || item.timestamp || item.receivedAt),
|
occurredAt: text(item.occurredAt || item.timestamp || item.receivedAt),
|
||||||
receivedAt: text(item.receivedAt),
|
receivedAt: text(item.receivedAt),
|
||||||
sourcePluginId: text(item.sourcePluginId || item.pluginId),
|
sourcePluginId: text(item.sourcePluginId || item.pluginId),
|
||||||
|
workspaceRootPath: cleanWorkspace(item.workspaceRootPath || (item.payload && (item.payload.workspaceRootPath || item.payload.workspaceName))),
|
||||||
|
_storageKey: storageKey || '',
|
||||||
payload: item.payload && typeof item.payload === 'object' ? item.payload : {}
|
payload: item.payload && typeof item.payload === 'object' ? item.payload : {}
|
||||||
};
|
};
|
||||||
}).slice(0, MAX_EVENTS);
|
}).slice(0, MAX_EVENTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function storageEvents(activityList) {
|
||||||
|
return activityList.map(function (item) {
|
||||||
|
return {
|
||||||
|
activityId: item.activityId,
|
||||||
|
type: item.type,
|
||||||
|
title: item.title,
|
||||||
|
summary: item.summary,
|
||||||
|
occurredAt: item.occurredAt,
|
||||||
|
receivedAt: item.receivedAt,
|
||||||
|
sourcePluginId: item.sourcePluginId,
|
||||||
|
workspaceRootPath: item.workspaceRootPath,
|
||||||
|
payload: item.payload || {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortEvents(activityList) {
|
||||||
|
return activityList.slice().sort(function (a, b) {
|
||||||
|
return text(b.occurredAt || b.receivedAt).localeCompare(text(a.occurredAt || a.receivedAt));
|
||||||
|
}).slice(0, MAX_EVENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function globalEventKeys(settings) {
|
||||||
|
var keys = [GLOBAL_KEY];
|
||||||
|
Object.keys(settings || {}).forEach(function (key) {
|
||||||
|
if (key.indexOf(WORKSPACE_PREFIX) === 0 && keys.indexOf(key) === -1) keys.push(key);
|
||||||
|
});
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
var date = new Date(value);
|
var date = new Date(value);
|
||||||
|
|
@ -177,6 +249,7 @@
|
||||||
containerEl.className = 'activity-root';
|
containerEl.className = 'activity-root';
|
||||||
containerEl.setAttribute('data-plugin-id', PLUGIN_ID);
|
containerEl.setAttribute('data-plugin-id', PLUGIN_ID);
|
||||||
|
|
||||||
|
var scope = scopeFromProps(props || {});
|
||||||
var events = [];
|
var events = [];
|
||||||
var statusText = 'Loading activity...';
|
var statusText = 'Loading activity...';
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
|
|
@ -184,7 +257,7 @@
|
||||||
var unsubscribers = [];
|
var unsubscribers = [];
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'activity-toolbar' });
|
var toolbar = el('div', { className: 'activity-toolbar' });
|
||||||
var titleEl = el('span', { className: 'activity-title', textContent: 'Activity' });
|
var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? 'Activity' : 'Activity · ' + scope.label });
|
||||||
var countEl = el('span', { className: 'activity-count' });
|
var countEl = el('span', { className: 'activity-count' });
|
||||||
var statusEl = el('span', { className: 'activity-status' });
|
var statusEl = el('span', { className: 'activity-status' });
|
||||||
var manualBtn = el('button', {
|
var manualBtn = el('button', {
|
||||||
|
|
@ -192,7 +265,7 @@
|
||||||
'data-activity-action': 'manual',
|
'data-activity-action': 'manual',
|
||||||
textContent: 'Record',
|
textContent: 'Record',
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
addActivity(manualActivity());
|
addActivity(manualActivity(scope));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var clearBtn = el('button', {
|
var clearBtn = el('button', {
|
||||||
|
|
@ -200,6 +273,10 @@
|
||||||
'data-activity-action': 'clear',
|
'data-activity-action': 'clear',
|
||||||
textContent: 'Clear',
|
textContent: 'Clear',
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
|
if (scope.mode === 'global') {
|
||||||
|
clearGlobal().then(render);
|
||||||
|
return;
|
||||||
|
}
|
||||||
events = [];
|
events = [];
|
||||||
persist().then(render);
|
persist().then(render);
|
||||||
}
|
}
|
||||||
|
|
@ -217,14 +294,38 @@
|
||||||
|
|
||||||
function persist() {
|
function persist() {
|
||||||
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
||||||
return api.settings.write('events', events).catch(function (err) {
|
var toStore = scope.mode === 'global'
|
||||||
|
? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; })
|
||||||
|
: events;
|
||||||
|
return api.settings.write(scope.key, storageEvents(toStore)).catch(function (err) {
|
||||||
statusText = 'Could not save activity: ' + (err && err.message ? err.message : String(err));
|
statusText = 'Could not save activity: ' + (err && err.message ? err.message : String(err));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearGlobal() {
|
||||||
|
if (!api || !api.settings || typeof api.settings.read !== 'function' || typeof api.settings.write !== 'function') {
|
||||||
|
events = [];
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return api.settings.read().then(function (settings) {
|
||||||
|
var keys = globalEventKeys(settings || {});
|
||||||
|
events = [];
|
||||||
|
return Promise.all(keys.map(function (key) {
|
||||||
|
return api.settings.write(key, []);
|
||||||
|
}));
|
||||||
|
}).then(function () {
|
||||||
|
statusText = 'Activity cleared';
|
||||||
|
statusClass = '';
|
||||||
|
}).catch(function (err) {
|
||||||
|
statusText = 'Could not clear activity: ' + (err && err.message ? err.message : String(err));
|
||||||
|
statusClass = 'error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addActivity(activity) {
|
function addActivity(activity) {
|
||||||
events = [activity].concat(events).slice(0, MAX_EVENTS);
|
activity._storageKey = scope.key;
|
||||||
|
events = sortEvents([activity].concat(events));
|
||||||
statusText = 'Activity recorded';
|
statusText = 'Activity recorded';
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
return persist().then(render);
|
return persist().then(render);
|
||||||
|
|
@ -264,8 +365,20 @@
|
||||||
|
|
||||||
function loadStored() {
|
function loadStored() {
|
||||||
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
||||||
return api.settings.read('events').then(function (stored) {
|
if (scope.mode === 'global') {
|
||||||
events = normalizeStoredEvents(stored);
|
return api.settings.read().then(function (settings) {
|
||||||
|
var all = [];
|
||||||
|
globalEventKeys(settings || {}).forEach(function (key) {
|
||||||
|
all = all.concat(normalizeStoredEvents((settings || {})[key], key));
|
||||||
|
});
|
||||||
|
events = sortEvents(all);
|
||||||
|
}).catch(function (err) {
|
||||||
|
statusText = 'Could not load activity: ' + (err && err.message ? err.message : String(err));
|
||||||
|
statusClass = 'error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return api.settings.read(scope.key).then(function (stored) {
|
||||||
|
events = normalizeStoredEvents(stored, scope.key);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not load activity: ' + (err && err.message ? err.message : String(err));
|
statusText = 'Could not load activity: ' + (err && err.message ? err.message : String(err));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
|
|
@ -276,12 +389,14 @@
|
||||||
if (!api || !api.events || typeof api.events.subscribe !== 'function') return Promise.resolve();
|
if (!api || !api.events || typeof api.events.subscribe !== 'function') return Promise.resolve();
|
||||||
return Promise.all(ACTIVITY_EVENTS.map(function (eventName) {
|
return Promise.all(ACTIVITY_EVENTS.map(function (eventName) {
|
||||||
return api.events.subscribe(eventName, function (event) {
|
return api.events.subscribe(eventName, function (event) {
|
||||||
return addActivity(eventToActivity(event));
|
var eventWorkspace = workspaceFromPayload(eventPayload(event));
|
||||||
|
if (scope.mode === 'workspace' && eventWorkspace && eventWorkspace !== scope.workspaceRoot) return Promise.resolve();
|
||||||
|
return addActivity(eventToActivity(event, scope));
|
||||||
}).then(function (unsubscribe) {
|
}).then(function (unsubscribe) {
|
||||||
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
||||||
});
|
});
|
||||||
})).then(function () {
|
})).then(function () {
|
||||||
statusText = 'Listening for activity';
|
statusText = scope.mode === 'global' ? 'Listening for all activity' : 'Listening for workspace activity';
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Activity subscriptions unavailable: ' + (err && err.message ? err.message : String(err));
|
statusText = 'Activity subscriptions unavailable: ' + (err && err.message ? err.message : String(err));
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
var PLUGIN_ID = 'verstak.browser-inbox';
|
var PLUGIN_ID = 'verstak.browser-inbox';
|
||||||
var CAPTURE_EVENTS = ['browser.capture.page', 'browser.capture.selection', 'browser.capture.link'];
|
var CAPTURE_EVENTS = ['browser.capture.page', 'browser.capture.selection', 'browser.capture.link'];
|
||||||
var MAX_CAPTURES = 100;
|
var MAX_CAPTURES = 100;
|
||||||
|
var GLOBAL_KEY = 'captures:global';
|
||||||
|
var WORKSPACE_PREFIX = 'captures:workspace:';
|
||||||
|
|
||||||
function injectStyles() {
|
function injectStyles() {
|
||||||
if (document.getElementById('browser-inbox-style-injected')) return;
|
if (document.getElementById('browser-inbox-style-injected')) return;
|
||||||
|
|
@ -76,6 +78,37 @@
|
||||||
return String(value == null ? '' : value);
|
return String(value == null ? '' : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encodeKey(value) {
|
||||||
|
return encodeURIComponent(text(value).trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanWorkspace(value) {
|
||||||
|
return text(value).trim().replace(/^\/+|\/+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceFromProps(props) {
|
||||||
|
var node = props && props.workspaceNode;
|
||||||
|
return cleanWorkspace((props && (props.workspaceRootPath || props.workspaceName || props.workspaceNodeId))
|
||||||
|
|| (node && (node.rootPath || node.name || node.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceFromPayload(payload) {
|
||||||
|
return cleanWorkspace(payload && (payload.workspaceRootPath || payload.workspaceName || payload.workspaceNodeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function scopeFromProps(props) {
|
||||||
|
var workspaceRoot = workspaceFromProps(props);
|
||||||
|
if (!workspaceRoot) {
|
||||||
|
return { mode: 'global', key: GLOBAL_KEY, label: 'All workspaces', workspaceRoot: '' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mode: 'workspace',
|
||||||
|
key: WORKSPACE_PREFIX + encodeKey(workspaceRoot),
|
||||||
|
label: workspaceRoot,
|
||||||
|
workspaceRoot: workspaceRoot
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function cleanKind(value) {
|
function cleanKind(value) {
|
||||||
value = text(value).trim();
|
value = text(value).trim();
|
||||||
return value === 'selection' || value === 'link' || value === 'page' ? value : 'page';
|
return value === 'selection' || value === 'link' || value === 'page' ? value : 'page';
|
||||||
|
|
@ -90,7 +123,7 @@
|
||||||
return event.payload;
|
return event.payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureFromEvent(event) {
|
function captureFromEvent(event, scope) {
|
||||||
var payload = eventPayload(event);
|
var payload = eventPayload(event);
|
||||||
var captureId = text(payload.captureId).trim();
|
var captureId = text(payload.captureId).trim();
|
||||||
if (!captureId) {
|
if (!captureId) {
|
||||||
|
|
@ -106,11 +139,12 @@
|
||||||
domain: text(payload.domain).trim(),
|
domain: text(payload.domain).trim(),
|
||||||
text: text(payload.text).trim(),
|
text: text(payload.text).trim(),
|
||||||
source: text(payload.source).trim(),
|
source: text(payload.source).trim(),
|
||||||
browserName: text(payload.browserName).trim()
|
browserName: text(payload.browserName).trim(),
|
||||||
|
workspaceRootPath: workspaceFromPayload(payload) || (scope && scope.workspaceRoot) || ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStoredCaptures(value) {
|
function normalizeStoredCaptures(value, storageKey) {
|
||||||
if (!Array.isArray(value)) return [];
|
if (!Array.isArray(value)) return [];
|
||||||
return value.filter(function (item) {
|
return value.filter(function (item) {
|
||||||
return item && typeof item === 'object' && item.captureId;
|
return item && typeof item === 'object' && item.captureId;
|
||||||
|
|
@ -125,11 +159,45 @@
|
||||||
domain: text(item.domain),
|
domain: text(item.domain),
|
||||||
text: text(item.text),
|
text: text(item.text),
|
||||||
source: text(item.source),
|
source: text(item.source),
|
||||||
browserName: text(item.browserName)
|
browserName: text(item.browserName),
|
||||||
|
workspaceRootPath: cleanWorkspace(item.workspaceRootPath),
|
||||||
|
_storageKey: storageKey || ''
|
||||||
};
|
};
|
||||||
}).slice(0, MAX_CAPTURES);
|
}).slice(0, MAX_CAPTURES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function storageCaptures(captureList) {
|
||||||
|
return captureList.map(function (item) {
|
||||||
|
return {
|
||||||
|
captureId: item.captureId,
|
||||||
|
capturedAt: item.capturedAt,
|
||||||
|
receivedAt: item.receivedAt,
|
||||||
|
kind: item.kind,
|
||||||
|
url: item.url,
|
||||||
|
title: item.title,
|
||||||
|
domain: item.domain,
|
||||||
|
text: item.text,
|
||||||
|
source: item.source,
|
||||||
|
browserName: item.browserName,
|
||||||
|
workspaceRootPath: item.workspaceRootPath
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortCaptures(captureList) {
|
||||||
|
return captureList.slice().sort(function (a, b) {
|
||||||
|
return text(b.capturedAt || b.receivedAt).localeCompare(text(a.capturedAt || a.receivedAt));
|
||||||
|
}).slice(0, MAX_CAPTURES);
|
||||||
|
}
|
||||||
|
|
||||||
|
function globalCaptureKeys(settings) {
|
||||||
|
var keys = [GLOBAL_KEY];
|
||||||
|
Object.keys(settings || {}).forEach(function (key) {
|
||||||
|
if (key.indexOf(WORKSPACE_PREFIX) === 0 && keys.indexOf(key) === -1) keys.push(key);
|
||||||
|
});
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
var date = new Date(value);
|
var date = new Date(value);
|
||||||
|
|
@ -145,6 +213,7 @@
|
||||||
containerEl.className = 'browser-inbox-root';
|
containerEl.className = 'browser-inbox-root';
|
||||||
containerEl.setAttribute('data-plugin-id', PLUGIN_ID);
|
containerEl.setAttribute('data-plugin-id', PLUGIN_ID);
|
||||||
|
|
||||||
|
var scope = scopeFromProps(props || {});
|
||||||
var captures = [];
|
var captures = [];
|
||||||
var selectedId = '';
|
var selectedId = '';
|
||||||
var statusText = 'Connecting to receiver events...';
|
var statusText = 'Connecting to receiver events...';
|
||||||
|
|
@ -153,7 +222,7 @@
|
||||||
var unsubscribers = [];
|
var unsubscribers = [];
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
|
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
|
||||||
var titleEl = el('span', { className: 'browser-inbox-title', textContent: 'Browser Inbox' });
|
var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? 'Browser Inbox' : 'Browser Inbox · ' + scope.label });
|
||||||
var countEl = el('span', { className: 'browser-inbox-count' });
|
var countEl = el('span', { className: 'browser-inbox-count' });
|
||||||
var statusEl = el('span', { className: 'browser-inbox-status' });
|
var statusEl = el('span', { className: 'browser-inbox-status' });
|
||||||
var clearBtn = el('button', {
|
var clearBtn = el('button', {
|
||||||
|
|
@ -161,6 +230,10 @@
|
||||||
'data-browser-inbox-action': 'clear',
|
'data-browser-inbox-action': 'clear',
|
||||||
textContent: 'Clear',
|
textContent: 'Clear',
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
|
if (scope.mode === 'global') {
|
||||||
|
clearGlobal().then(render);
|
||||||
|
return;
|
||||||
|
}
|
||||||
captures = [];
|
captures = [];
|
||||||
selectedId = '';
|
selectedId = '';
|
||||||
persist().then(render);
|
persist().then(render);
|
||||||
|
|
@ -182,12 +255,37 @@
|
||||||
|
|
||||||
function persist() {
|
function persist() {
|
||||||
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
|
||||||
return api.settings.write('captures', captures).catch(function (err) {
|
var toStore = scope.mode === 'global'
|
||||||
|
? captures.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; })
|
||||||
|
: captures;
|
||||||
|
return api.settings.write(scope.key, storageCaptures(toStore)).catch(function (err) {
|
||||||
statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err));
|
statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err));
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearGlobal() {
|
||||||
|
if (!api || !api.settings || typeof api.settings.read !== 'function' || typeof api.settings.write !== 'function') {
|
||||||
|
captures = [];
|
||||||
|
selectedId = '';
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return api.settings.read().then(function (settings) {
|
||||||
|
var keys = globalCaptureKeys(settings || {});
|
||||||
|
captures = [];
|
||||||
|
selectedId = '';
|
||||||
|
return Promise.all(keys.map(function (key) {
|
||||||
|
return api.settings.write(key, []);
|
||||||
|
}));
|
||||||
|
}).then(function () {
|
||||||
|
statusText = 'Inbox cleared';
|
||||||
|
statusClass = '';
|
||||||
|
}).catch(function (err) {
|
||||||
|
statusText = 'Could not clear inbox: ' + (err && err.message ? err.message : String(err));
|
||||||
|
statusClass = 'error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function selectedCapture() {
|
function selectedCapture() {
|
||||||
for (var i = 0; i < captures.length; i += 1) {
|
for (var i = 0; i < captures.length; i += 1) {
|
||||||
if (captures[i].captureId === selectedId) return captures[i];
|
if (captures[i].captureId === selectedId) return captures[i];
|
||||||
|
|
@ -200,7 +298,8 @@
|
||||||
return item.captureId === capture.captureId;
|
return item.captureId === capture.captureId;
|
||||||
});
|
});
|
||||||
if (existing) return Promise.resolve();
|
if (existing) return Promise.resolve();
|
||||||
captures = [capture].concat(captures).slice(0, MAX_CAPTURES);
|
capture._storageKey = scope.key;
|
||||||
|
captures = sortCaptures([capture].concat(captures));
|
||||||
selectedId = capture.captureId;
|
selectedId = capture.captureId;
|
||||||
statusText = 'Capture received';
|
statusText = 'Capture received';
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
|
|
@ -212,7 +311,16 @@
|
||||||
return item.captureId !== captureId;
|
return item.captureId !== captureId;
|
||||||
});
|
});
|
||||||
if (selectedId === captureId) selectedId = captures[0] ? captures[0].captureId : '';
|
if (selectedId === captureId) selectedId = captures[0] ? captures[0].captureId : '';
|
||||||
return persist().then(render);
|
if (scope.mode !== 'global') return persist().then(render);
|
||||||
|
if (!api || !api.settings || typeof api.settings.read !== 'function' || typeof api.settings.write !== 'function') return persist().then(render);
|
||||||
|
return api.settings.read().then(function (settings) {
|
||||||
|
return Promise.all(globalCaptureKeys(settings || {}).map(function (key) {
|
||||||
|
var next = normalizeStoredCaptures((settings || {})[key], key).filter(function (item) {
|
||||||
|
return item.captureId !== captureId;
|
||||||
|
});
|
||||||
|
return api.settings.write(key, storageCaptures(next));
|
||||||
|
}));
|
||||||
|
}).then(render);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
|
|
@ -290,8 +398,21 @@
|
||||||
|
|
||||||
function loadStored() {
|
function loadStored() {
|
||||||
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
||||||
return api.settings.read('captures').then(function (stored) {
|
if (scope.mode === 'global') {
|
||||||
captures = normalizeStoredCaptures(stored);
|
return api.settings.read().then(function (settings) {
|
||||||
|
var all = [];
|
||||||
|
globalCaptureKeys(settings || {}).forEach(function (key) {
|
||||||
|
all = all.concat(normalizeStoredCaptures((settings || {})[key], key));
|
||||||
|
});
|
||||||
|
captures = sortCaptures(all);
|
||||||
|
if (!selectedId && captures[0]) selectedId = captures[0].captureId;
|
||||||
|
}).catch(function (err) {
|
||||||
|
statusText = 'Could not load inbox: ' + (err && err.message ? err.message : String(err));
|
||||||
|
statusClass = 'error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return api.settings.read(scope.key).then(function (stored) {
|
||||||
|
captures = normalizeStoredCaptures(stored, scope.key);
|
||||||
if (!selectedId && captures[0]) selectedId = captures[0].captureId;
|
if (!selectedId && captures[0]) selectedId = captures[0].captureId;
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not load inbox: ' + (err && err.message ? err.message : String(err));
|
statusText = 'Could not load inbox: ' + (err && err.message ? err.message : String(err));
|
||||||
|
|
@ -303,12 +424,14 @@
|
||||||
if (!api || !api.events || typeof api.events.subscribe !== 'function') return Promise.resolve();
|
if (!api || !api.events || typeof api.events.subscribe !== 'function') return Promise.resolve();
|
||||||
return Promise.all(CAPTURE_EVENTS.map(function (eventName) {
|
return Promise.all(CAPTURE_EVENTS.map(function (eventName) {
|
||||||
return api.events.subscribe(eventName, function (event) {
|
return api.events.subscribe(eventName, function (event) {
|
||||||
return addCapture(captureFromEvent(event));
|
var eventWorkspace = workspaceFromPayload(eventPayload(event));
|
||||||
|
if (scope.mode === 'workspace' && eventWorkspace && eventWorkspace !== scope.workspaceRoot) return Promise.resolve();
|
||||||
|
return addCapture(captureFromEvent(event, scope));
|
||||||
}).then(function (unsubscribe) {
|
}).then(function (unsubscribe) {
|
||||||
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
||||||
});
|
});
|
||||||
})).then(function () {
|
})).then(function () {
|
||||||
statusText = 'Receiver ready';
|
statusText = scope.mode === 'global' ? 'Receiver ready for all workspaces' : 'Receiver ready for workspace';
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Receiver unavailable: ' + (err && err.message ? err.message : String(err));
|
statusText = 'Receiver unavailable: ' + (err && err.message ? err.message : String(err));
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,8 @@ function makeApi(initialSettings = {}) {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storedEvents() {
|
storedEvents(key = 'events') {
|
||||||
return settings.events || [];
|
return settings[key] || [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -146,10 +146,10 @@ async function flush() {
|
||||||
for (let i = 0; i < 10; i += 1) await Promise.resolve();
|
for (let i = 0; i < 10; i += 1) await Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mountWithApi(api, document = makeDocument()) {
|
async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, workspaceRootPath: 'Project' }, document = makeDocument()) {
|
||||||
const component = loadComponent(document);
|
const component = loadComponent(document);
|
||||||
const container = new FakeNode('div');
|
const container = new FakeNode('div');
|
||||||
component.mount(container, { workspaceNode: { name: 'Project' } }, api);
|
component.mount(container, props, api);
|
||||||
await flush();
|
await flush();
|
||||||
return { component, container, document };
|
return { component, container, document };
|
||||||
}
|
}
|
||||||
|
|
@ -176,31 +176,57 @@ async function mountWithApi(api, document = makeDocument()) {
|
||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const stored = api.storedEvents();
|
const projectKey = 'events:workspace:Project';
|
||||||
|
const clientKey = 'events:workspace:ClientA';
|
||||||
|
const globalKey = 'events:global';
|
||||||
|
const stored = api.storedEvents(projectKey);
|
||||||
if (stored.length !== 1) throw new Error(`expected one stored activity event, got ${stored.length}`);
|
if (stored.length !== 1) throw new Error(`expected one stored activity event, got ${stored.length}`);
|
||||||
if (stored[0].type !== 'browser.capture.selection') throw new Error('stored event type mismatch');
|
if (stored[0].type !== 'browser.capture.selection') throw new Error('stored event type mismatch');
|
||||||
if (stored[0].sourcePluginId !== 'verstak.browser-inbox') throw new Error('stored event source plugin mismatch');
|
if (stored[0].sourcePluginId !== 'verstak.browser-inbox') throw new Error('stored event source plugin mismatch');
|
||||||
|
if (api.storedEvents(globalKey).length !== 0) throw new Error('workspace activity leaked into global storage');
|
||||||
if (!container.textContent.includes('Example Article')) throw new Error('browser capture title was not rendered');
|
if (!container.textContent.includes('Example Article')) throw new Error('browser capture title was not rendered');
|
||||||
if (!container.textContent.includes('browser.capture.selection')) throw new Error('event type was not rendered');
|
if (!container.textContent.includes('browser.capture.selection')) throw new Error('event type was not rendered');
|
||||||
|
|
||||||
|
const clientView = await mountWithApi(api, { workspaceNode: { name: 'ClientA' }, workspaceRootPath: 'ClientA' });
|
||||||
|
if (clientView.container.textContent.includes('Example Article')) throw new Error('Project activity leaked into ClientA workspace view');
|
||||||
|
await api.handlers['note.saved']({
|
||||||
|
name: 'note.saved',
|
||||||
|
pluginId: 'verstak.notes',
|
||||||
|
timestamp: '2026-06-27T00:10:00Z',
|
||||||
|
payload: {
|
||||||
|
title: 'Client note',
|
||||||
|
path: 'ClientA/Notes/Client.md',
|
||||||
|
workspaceRootPath: 'ClientA',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
if (api.storedEvents(clientKey).length !== 1) throw new Error('ClientA activity was not stored under ClientA workspace key');
|
||||||
|
if (!clientView.container.textContent.includes('Client note')) throw new Error('ClientA activity was not rendered');
|
||||||
|
component.unmount && component.unmount(clientView.container);
|
||||||
|
|
||||||
|
const globalView = await mountWithApi(api, {});
|
||||||
|
if (!globalView.container.textContent.includes('Example Article')) throw new Error('global activity did not aggregate Project activity');
|
||||||
|
if (!globalView.container.textContent.includes('Client note')) throw new Error('global activity did not aggregate ClientA activity');
|
||||||
|
component.unmount && component.unmount(globalView.container);
|
||||||
|
|
||||||
const manualButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'manual');
|
const manualButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'manual');
|
||||||
if (!manualButton) throw new Error('manual activity button not found');
|
if (!manualButton) throw new Error('manual activity button not found');
|
||||||
manualButton.click();
|
manualButton.click();
|
||||||
await flush();
|
await flush();
|
||||||
if (api.storedEvents().length !== 2) throw new Error('manual activity was not stored');
|
if (api.storedEvents(projectKey).length !== 2) throw new Error('manual activity was not stored');
|
||||||
if (!container.textContent.includes('Manual activity')) throw new Error('manual activity was not rendered');
|
if (!container.textContent.includes('Manual activity')) throw new Error('manual activity was not rendered');
|
||||||
|
|
||||||
const clearButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'clear');
|
const clearButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'clear');
|
||||||
if (!clearButton) throw new Error('clear activity button not found');
|
if (!clearButton) throw new Error('clear activity button not found');
|
||||||
clearButton.click();
|
clearButton.click();
|
||||||
await flush();
|
await flush();
|
||||||
if (api.storedEvents().length !== 0) throw new Error('clear action did not remove activity events');
|
if (api.storedEvents(projectKey).length !== 0) throw new Error('clear action did not remove activity events');
|
||||||
|
|
||||||
component.unmount && component.unmount(container);
|
component.unmount && component.unmount(container);
|
||||||
if (api.unsubscribed.length !== 9) throw new Error(`expected 9 unsubscribers, got ${api.unsubscribed.length}`);
|
if (api.unsubscribed.length !== 27) throw new Error(`expected 27 unsubscribers, got ${api.unsubscribed.length}`);
|
||||||
|
|
||||||
const persistedApi = makeApi({
|
const persistedApi = makeApi({
|
||||||
events: [{
|
'events:workspace:Project': [{
|
||||||
activityId: 'persisted-1',
|
activityId: 'persisted-1',
|
||||||
type: 'note.saved',
|
type: 'note.saved',
|
||||||
title: 'Saved note',
|
title: 'Saved note',
|
||||||
|
|
|
||||||
|
|
@ -154,8 +154,8 @@ function makeApi(initialSettings = {}) {
|
||||||
return { ...settings };
|
return { ...settings };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getStoredCaptures() {
|
getStoredCaptures(key = 'captures') {
|
||||||
return settings.captures || [];
|
return settings[key] || [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -164,10 +164,10 @@ async function flush() {
|
||||||
for (let i = 0; i < 8; i += 1) await Promise.resolve();
|
for (let i = 0; i < 8; i += 1) await Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mountWithApi(api, document = makeDocument()) {
|
async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, workspaceRootPath: 'Project' }, document = makeDocument()) {
|
||||||
const component = loadComponent(document);
|
const component = loadComponent(document);
|
||||||
const container = new FakeNode('div');
|
const container = new FakeNode('div');
|
||||||
component.mount(container, { workspaceNode: { name: 'Project' } }, api);
|
component.mount(container, props, api);
|
||||||
await flush();
|
await flush();
|
||||||
return { component, container, document };
|
return { component, container, document };
|
||||||
}
|
}
|
||||||
|
|
@ -196,9 +196,13 @@ async function mountWithApi(api, document = makeDocument()) {
|
||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const captures = api.getStoredCaptures();
|
const projectKey = 'captures:workspace:Project';
|
||||||
|
const clientKey = 'captures:workspace:ClientA';
|
||||||
|
const globalKey = 'captures:global';
|
||||||
|
const captures = api.getStoredCaptures(projectKey);
|
||||||
if (captures.length !== 1) throw new Error(`expected one stored capture, got ${captures.length}`);
|
if (captures.length !== 1) throw new Error(`expected one stored capture, got ${captures.length}`);
|
||||||
if (captures[0].captureId !== 'capture-1') throw new Error('stored capture id mismatch');
|
if (captures[0].captureId !== 'capture-1') throw new Error('stored capture id mismatch');
|
||||||
|
if (api.getStoredCaptures(globalKey).length !== 0) throw new Error('workspace capture leaked into global storage');
|
||||||
|
|
||||||
const row = walk(container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1');
|
const row = walk(container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1');
|
||||||
if (!row) throw new Error('capture row was not rendered');
|
if (!row) throw new Error('capture row was not rendered');
|
||||||
|
|
@ -220,18 +224,51 @@ async function mountWithApi(api, document = makeDocument()) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
if (api.getStoredCaptures().length !== 1) throw new Error('duplicate capture was stored');
|
if (api.getStoredCaptures(projectKey).length !== 1) throw new Error('duplicate capture was stored');
|
||||||
|
|
||||||
|
const clientView = await mountWithApi(api, { workspaceNode: { name: 'ClientA' }, workspaceRootPath: 'ClientA' });
|
||||||
|
if (walk(clientView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1')) {
|
||||||
|
throw new Error('Project capture leaked into ClientA workspace view');
|
||||||
|
}
|
||||||
|
await api.handlers['browser.capture.page']({
|
||||||
|
name: 'browser.capture.page',
|
||||||
|
timestamp: '2026-06-27T00:10:00Z',
|
||||||
|
payload: {
|
||||||
|
captureId: 'capture-2',
|
||||||
|
capturedAt: '2026-06-27T00:10:00.000Z',
|
||||||
|
kind: 'page',
|
||||||
|
url: 'https://client.example.com/',
|
||||||
|
title: 'Client Page',
|
||||||
|
domain: 'client.example.com',
|
||||||
|
workspaceRootPath: 'ClientA',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await flush();
|
||||||
|
if (api.getStoredCaptures(clientKey).length !== 1) throw new Error('ClientA capture was not stored under ClientA workspace key');
|
||||||
|
if (!walk(clientView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-2')) {
|
||||||
|
throw new Error('ClientA capture was not rendered');
|
||||||
|
}
|
||||||
|
component.unmount && component.unmount(clientView.container);
|
||||||
|
|
||||||
|
const globalView = await mountWithApi(api, {});
|
||||||
|
if (!walk(globalView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1')) {
|
||||||
|
throw new Error('global browser inbox did not aggregate Project capture');
|
||||||
|
}
|
||||||
|
if (!walk(globalView.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-2')) {
|
||||||
|
throw new Error('global browser inbox did not aggregate ClientA capture');
|
||||||
|
}
|
||||||
|
component.unmount && component.unmount(globalView.container);
|
||||||
|
|
||||||
const clearButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'clear');
|
const clearButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-action') === 'clear');
|
||||||
if (!clearButton) throw new Error('clear button not found');
|
if (!clearButton) throw new Error('clear button not found');
|
||||||
clearButton.click();
|
clearButton.click();
|
||||||
await flush();
|
await flush();
|
||||||
if (api.getStoredCaptures().length !== 0) throw new Error('clear action did not empty stored captures');
|
if (api.getStoredCaptures(projectKey).length !== 0) throw new Error('clear action did not empty stored captures');
|
||||||
|
|
||||||
component.unmount && component.unmount(container);
|
component.unmount && component.unmount(container);
|
||||||
if (api.unsubscribed.length !== 3) throw new Error('component did not unsubscribe all capture handlers');
|
if (api.unsubscribed.length !== 9) throw new Error('component did not unsubscribe all capture handlers');
|
||||||
|
|
||||||
const persistedApi = makeApi({ captures: [captures[0]] });
|
const persistedApi = makeApi({ 'captures:workspace:Project': [captures[0]] });
|
||||||
const persisted = await mountWithApi(persistedApi);
|
const persisted = await mountWithApi(persistedApi);
|
||||||
if (!walk(persisted.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1')) {
|
if (!walk(persisted.container, (node) => node.getAttribute && node.getAttribute('data-browser-capture-id') === 'capture-1')) {
|
||||||
throw new Error('persisted capture was not rendered on mount');
|
throw new Error('persisted capture was not rendered on mount');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue