fix: make activity view passive
This commit is contained in:
parent
8010d6c601
commit
3b7258ed3e
|
|
@ -115,76 +115,10 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function activityId() {
|
|
||||||
return 'activity-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventPayload(event) {
|
function eventPayload(event) {
|
||||||
return event && event.payload && typeof event.payload === 'object' ? event.payload : {};
|
return event && event.payload && typeof event.payload === 'object' ? event.payload : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function firstText(values) {
|
|
||||||
for (var i = 0; i < values.length; i += 1) {
|
|
||||||
var value = text(values[i]).trim();
|
|
||||||
if (value) return value;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function titleFromEvent(event, payload) {
|
|
||||||
return firstText([
|
|
||||||
payload.title,
|
|
||||||
payload.name,
|
|
||||||
payload.path,
|
|
||||||
payload.url,
|
|
||||||
payload.captureId,
|
|
||||||
event.name,
|
|
||||||
'Activity event'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function summaryFromEvent(event, payload) {
|
|
||||||
if (payload.text) return text(payload.text).trim();
|
|
||||||
return firstText([
|
|
||||||
payload.summary,
|
|
||||||
payload.description,
|
|
||||||
payload.path,
|
|
||||||
payload.url,
|
|
||||||
payload.domain,
|
|
||||||
event.name
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function eventToActivity(event, scope) {
|
|
||||||
var payload = eventPayload(event);
|
|
||||||
var workspaceRoot = workspaceFromPayload(payload) || (scope && scope.workspaceRoot) || '';
|
|
||||||
return {
|
|
||||||
activityId: activityId(),
|
|
||||||
type: text(event && event.name).trim() || 'activity.event',
|
|
||||||
title: titleFromEvent(event || {}, payload),
|
|
||||||
summary: summaryFromEvent(event || {}, payload),
|
|
||||||
occurredAt: text(payload.occurredAt || payload.capturedAt || (event && event.timestamp) || new Date().toISOString()),
|
|
||||||
receivedAt: new Date().toISOString(),
|
|
||||||
sourcePluginId: text((event && event.pluginId) || payload.pluginId || payload.sourcePluginId),
|
|
||||||
workspaceRootPath: workspaceRoot,
|
|
||||||
payload: payload
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function manualActivity(scope) {
|
|
||||||
return {
|
|
||||||
activityId: activityId(),
|
|
||||||
type: 'activity.manual',
|
|
||||||
title: 'Manual activity',
|
|
||||||
summary: 'Manually recorded activity event',
|
|
||||||
occurredAt: new Date().toISOString(),
|
|
||||||
receivedAt: new Date().toISOString(),
|
|
||||||
sourcePluginId: PLUGIN_ID,
|
|
||||||
workspaceRootPath: (scope && scope.workspaceRoot) || '',
|
|
||||||
payload: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeStoredEvents(value, storageKey) {
|
function normalizeStoredEvents(value, storageKey) {
|
||||||
if (!Array.isArray(value)) return [];
|
if (!Array.isArray(value)) return [];
|
||||||
return value.filter(function (item) {
|
return value.filter(function (item) {
|
||||||
|
|
@ -268,14 +202,6 @@
|
||||||
var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? 'Activity' : 'Activity · ' + scope.label });
|
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', {
|
|
||||||
className: 'activity-btn',
|
|
||||||
'data-activity-action': 'manual',
|
|
||||||
textContent: 'Record',
|
|
||||||
onClick: function () {
|
|
||||||
addActivity(manualActivity(scope));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var clearBtn = el('button', {
|
var clearBtn = el('button', {
|
||||||
className: 'activity-btn danger',
|
className: 'activity-btn danger',
|
||||||
'data-activity-action': 'clear',
|
'data-activity-action': 'clear',
|
||||||
|
|
@ -293,7 +219,6 @@
|
||||||
toolbar.appendChild(countEl);
|
toolbar.appendChild(countEl);
|
||||||
toolbar.appendChild(el('span', { className: 'activity-spacer' }));
|
toolbar.appendChild(el('span', { className: 'activity-spacer' }));
|
||||||
toolbar.appendChild(statusEl);
|
toolbar.appendChild(statusEl);
|
||||||
toolbar.appendChild(manualBtn);
|
|
||||||
toolbar.appendChild(clearBtn);
|
toolbar.appendChild(clearBtn);
|
||||||
|
|
||||||
var listEl = el('div', { className: 'activity-list' });
|
var listEl = el('div', { className: 'activity-list' });
|
||||||
|
|
@ -331,14 +256,6 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addActivity(activity) {
|
|
||||||
activity._storageKey = scope.key;
|
|
||||||
events = sortEvents([activity].concat(events));
|
|
||||||
statusText = 'Activity recorded';
|
|
||||||
statusClass = '';
|
|
||||||
return persist().then(render);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
listEl.innerHTML = '';
|
listEl.innerHTML = '';
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
|
|
@ -406,7 +323,7 @@
|
||||||
return api.events.subscribe(eventName, function (event) {
|
return api.events.subscribe(eventName, function (event) {
|
||||||
var eventWorkspace = workspaceFromPayload(eventPayload(event));
|
var eventWorkspace = workspaceFromPayload(eventPayload(event));
|
||||||
if (scope.mode === 'workspace' && eventWorkspace && eventWorkspace !== scope.workspaceRoot) return Promise.resolve();
|
if (scope.mode === 'workspace' && eventWorkspace && eventWorkspace !== scope.workspaceRoot) return Promise.resolve();
|
||||||
return addActivity(eventToActivity(event, scope));
|
return loadStored().then(render);
|
||||||
}).then(function (unsubscribe) {
|
}).then(function (unsubscribe) {
|
||||||
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -157,11 +157,31 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
(async () => {
|
(async () => {
|
||||||
const api = makeApi();
|
const api = makeApi();
|
||||||
const { component, container } = await mountWithApi(api);
|
const { component, container } = await mountWithApi(api);
|
||||||
|
const projectKey = 'events:workspace:Project';
|
||||||
|
const clientKey = 'events:workspace:ClientA';
|
||||||
|
const globalKey = 'events:global';
|
||||||
|
|
||||||
for (const name of ['file.opened', 'file.changed', 'note.saved', 'action.started', 'browser.capture.received', 'case.selected', 'browser.capture.selection']) {
|
for (const name of ['file.opened', 'file.changed', 'note.saved', 'action.started', 'browser.capture.received', 'case.selected', 'browser.capture.selection']) {
|
||||||
if (typeof api.handlers[name] !== 'function') throw new Error(`${name} subscription missing`);
|
if (typeof api.handlers[name] !== 'function') throw new Error(`${name} subscription missing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await api.settings.write(projectKey, [{
|
||||||
|
activityId: 'capture-1',
|
||||||
|
type: 'browser.capture.selection',
|
||||||
|
title: 'Example Article',
|
||||||
|
summary: 'Selected text',
|
||||||
|
occurredAt: '2026-06-27T00:00:00Z',
|
||||||
|
sourcePluginId: 'verstak.browser-inbox',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
payload: {
|
||||||
|
captureId: 'capture-1',
|
||||||
|
kind: 'selection',
|
||||||
|
title: 'Example Article',
|
||||||
|
url: 'https://example.com/article',
|
||||||
|
text: 'Selected text',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
},
|
||||||
|
}]);
|
||||||
await api.handlers['browser.capture.selection']({
|
await api.handlers['browser.capture.selection']({
|
||||||
name: 'browser.capture.selection',
|
name: 'browser.capture.selection',
|
||||||
pluginId: 'verstak.browser-inbox',
|
pluginId: 'verstak.browser-inbox',
|
||||||
|
|
@ -176,9 +196,6 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
});
|
});
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const projectKey = 'events:workspace:Project';
|
|
||||||
const clientKey = 'events:workspace:ClientA';
|
|
||||||
const globalKey = 'events:global';
|
|
||||||
const stored = api.storedEvents(projectKey);
|
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');
|
||||||
|
|
@ -189,6 +206,20 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
|
|
||||||
const clientView = await mountWithApi(api, { workspaceNode: { name: 'ClientA' }, workspaceRootPath: 'ClientA' });
|
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');
|
if (clientView.container.textContent.includes('Example Article')) throw new Error('Project activity leaked into ClientA workspace view');
|
||||||
|
await api.settings.write(clientKey, [{
|
||||||
|
activityId: 'client-note',
|
||||||
|
type: 'note.saved',
|
||||||
|
title: 'Client note',
|
||||||
|
summary: 'ClientA/Notes/Client.md',
|
||||||
|
occurredAt: '2026-06-27T00:10:00Z',
|
||||||
|
sourcePluginId: 'verstak.notes',
|
||||||
|
workspaceRootPath: 'ClientA',
|
||||||
|
payload: {
|
||||||
|
title: 'Client note',
|
||||||
|
path: 'ClientA/Notes/Client.md',
|
||||||
|
workspaceRootPath: 'ClientA',
|
||||||
|
},
|
||||||
|
}]);
|
||||||
await api.handlers['note.saved']({
|
await api.handlers['note.saved']({
|
||||||
name: 'note.saved',
|
name: 'note.saved',
|
||||||
pluginId: 'verstak.notes',
|
pluginId: 'verstak.notes',
|
||||||
|
|
@ -210,11 +241,7 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
component.unmount && component.unmount(globalView.container);
|
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 should not be rendered');
|
||||||
manualButton.click();
|
|
||||||
await flush();
|
|
||||||
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');
|
|
||||||
|
|
||||||
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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue