feat: suggest worklogs from activity
This commit is contained in:
parent
5011ddf89c
commit
018dacc805
|
|
@ -8,9 +8,11 @@
|
||||||
|
|
||||||
var PLUGIN_ID = 'verstak.activity';
|
var PLUGIN_ID = 'verstak.activity';
|
||||||
var MAX_EVENTS = 250;
|
var MAX_EVENTS = 250;
|
||||||
|
var MAX_SUGGESTIONS = 12;
|
||||||
var LEGACY_KEY = 'events';
|
var LEGACY_KEY = 'events';
|
||||||
var GLOBAL_KEY = 'events:global';
|
var GLOBAL_KEY = 'events:global';
|
||||||
var WORKSPACE_PREFIX = 'events:workspace:';
|
var WORKSPACE_PREFIX = 'events:workspace:';
|
||||||
|
var WORKLOG_COMMAND_ID = 'verstak.activity.suggestWorklog';
|
||||||
var ACTIVITY_EVENTS = [
|
var ACTIVITY_EVENTS = [
|
||||||
'file.opened',
|
'file.opened',
|
||||||
'file.changed',
|
'file.changed',
|
||||||
|
|
@ -43,6 +45,12 @@
|
||||||
'.activity-btn.danger{border-color:#633;color:#ff9a9a}',
|
'.activity-btn.danger{border-color:#633;color:#ff9a9a}',
|
||||||
'.activity-status{font-size:.72rem;color:#8b8ba8;white-space:nowrap}',
|
'.activity-status{font-size:.72rem;color:#8b8ba8;white-space:nowrap}',
|
||||||
'.activity-status.error{color:#e74c3c}',
|
'.activity-status.error{color:#e74c3c}',
|
||||||
|
'.activity-suggestions{border-bottom:1px solid rgba(22,33,62,.65);background:#111126;padding:.55rem .75rem;display:grid;gap:.5rem}',
|
||||||
|
'.activity-suggestions-title{font-size:.76rem;font-weight:600;color:#8b8ba8;text-transform:uppercase;letter-spacing:.04em}',
|
||||||
|
'.activity-suggestion{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.65rem;align-items:start;padding:.55rem .65rem;border:1px solid rgba(78,204,163,.25);border-radius:4px;background:#14142c}',
|
||||||
|
'.activity-suggestion-title{font-size:.84rem;color:#e0e0e0;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||||
|
'.activity-suggestion-summary{margin-top:.22rem;font-size:.76rem;color:#aaa;line-height:1.4;white-space:pre-wrap;overflow-wrap:anywhere}',
|
||||||
|
'.activity-suggestion-minutes{font-size:.76rem;color:#4ecca3;white-space:nowrap}',
|
||||||
'.activity-list{flex:1;min-height:0;overflow:auto;background:#101020}',
|
'.activity-list{flex:1;min-height:0;overflow:auto;background:#101020}',
|
||||||
'.activity-empty{height:100%;display:flex;align-items:center;justify-content:center;color:#666;font-size:.86rem;padding:2rem;text-align:center}',
|
'.activity-empty{height:100%;display:flex;align-items:center;justify-content:center;color:#666;font-size:.86rem;padding:2rem;text-align:center}',
|
||||||
'.activity-row{display:grid;grid-template-columns:9.5rem minmax(0,1fr);gap:.75rem;padding:.72rem .85rem;border-bottom:1px solid rgba(22,33,62,.6)}',
|
'.activity-row{display:grid;grid-template-columns:9.5rem minmax(0,1fr);gap:.75rem;padding:.72rem .85rem;border-bottom:1px solid rgba(22,33,62,.6)}',
|
||||||
|
|
@ -53,7 +61,7 @@
|
||||||
'.activity-title-text{font-size:.86rem;color:#e0e0e0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
'.activity-title-text{font-size:.86rem;color:#e0e0e0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||||
'.activity-summary{margin-top:.25rem;font-size:.78rem;line-height:1.4;color:#aaa;white-space:pre-wrap;overflow-wrap:anywhere}',
|
'.activity-summary{margin-top:.25rem;font-size:.78rem;line-height:1.4;color:#aaa;white-space:pre-wrap;overflow-wrap:anywhere}',
|
||||||
'.activity-source{margin-top:.25rem;font-size:.72rem;color:#777}',
|
'.activity-source{margin-top:.25rem;font-size:.72rem;color:#777}',
|
||||||
'@media(max-width:760px){.activity-row{grid-template-columns:1fr;gap:.25rem}.activity-toolbar{align-items:stretch}.activity-status{width:100%}}'
|
'@media(max-width:760px){.activity-row,.activity-suggestion{grid-template-columns:1fr;gap:.25rem}.activity-toolbar{align-items:stretch}.activity-status{width:100%}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
function el(tag, attrs, children) {
|
function el(tag, attrs, children) {
|
||||||
|
|
@ -168,6 +176,83 @@
|
||||||
}).slice(0, MAX_EVENTS);
|
}).slice(0, MAX_EVENTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function eventTimeMs(activity) {
|
||||||
|
var value = activity && (activity.occurredAt || activity.receivedAt);
|
||||||
|
var date = value ? new Date(value) : null;
|
||||||
|
return date && !isNaN(date.getTime()) ? date.getTime() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventDay(activity) {
|
||||||
|
var value = activity && (activity.occurredAt || activity.receivedAt);
|
||||||
|
var date = value ? new Date(value) : null;
|
||||||
|
if (date && !isNaN(date.getTime())) return date.toISOString().slice(0, 10);
|
||||||
|
return text(value).slice(0, 10) || 'unknown-date';
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestionWorkspace(activity) {
|
||||||
|
return cleanWorkspace(activity && (activity.workspaceRootPath || workspaceFromPayload(activity.payload || {}))) || 'Global';
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundUpQuarterHour(minutes) {
|
||||||
|
return Math.ceil(minutes / 15) * 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateMinutes(groupEvents) {
|
||||||
|
if (!groupEvents.length) return 0;
|
||||||
|
if (groupEvents.length === 1) return 15;
|
||||||
|
var first = eventTimeMs(groupEvents[0]);
|
||||||
|
var last = eventTimeMs(groupEvents[groupEvents.length - 1]);
|
||||||
|
if (!first || !last || last <= first) return 15;
|
||||||
|
return Math.max(15, Math.min(480, roundUpQuarterHour((last - first) / 60000)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventLabel(activity) {
|
||||||
|
return text(activity && (activity.title || activity.summary || activity.type || activity.activityId)).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeEvents(groupEvents) {
|
||||||
|
var labels = [];
|
||||||
|
groupEvents.forEach(function (activity) {
|
||||||
|
var label = eventLabel(activity);
|
||||||
|
if (label && labels.indexOf(label) === -1) labels.push(label);
|
||||||
|
});
|
||||||
|
var visible = labels.slice(0, 3);
|
||||||
|
var suffix = labels.length > 3 ? ' +' + (labels.length - 3) + ' more' : '';
|
||||||
|
return (visible.join('; ') || groupEvents.length + ' activity events') + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorklogSuggestions(activityList, workspaceFilter) {
|
||||||
|
var filter = cleanWorkspace(workspaceFilter);
|
||||||
|
var groups = {};
|
||||||
|
sortEvents(activityList || []).forEach(function (activity) {
|
||||||
|
var workspace = suggestionWorkspace(activity);
|
||||||
|
if (filter && workspace !== filter) return;
|
||||||
|
var day = eventDay(activity);
|
||||||
|
var key = workspace + '|' + day;
|
||||||
|
groups[key] = groups[key] || { workspaceRootPath: workspace, date: day, events: [] };
|
||||||
|
groups[key].events.push(activity);
|
||||||
|
});
|
||||||
|
return Object.keys(groups).map(function (key) {
|
||||||
|
var group = groups[key];
|
||||||
|
var ordered = group.events.slice().sort(function (a, b) {
|
||||||
|
return eventTimeMs(a) - eventTimeMs(b);
|
||||||
|
});
|
||||||
|
var eventIds = ordered.map(function (activity) { return activity.activityId; }).filter(Boolean);
|
||||||
|
var suggestionId = 'worklog:' + group.workspaceRootPath + ':' + group.date;
|
||||||
|
return {
|
||||||
|
suggestionId: suggestionId,
|
||||||
|
workspaceRootPath: group.workspaceRootPath,
|
||||||
|
date: group.date,
|
||||||
|
title: group.workspaceRootPath + ' work on ' + group.date,
|
||||||
|
summary: summarizeEvents(ordered),
|
||||||
|
minutes: estimateMinutes(ordered),
|
||||||
|
eventIds: eventIds
|
||||||
|
};
|
||||||
|
}).sort(function (a, b) {
|
||||||
|
return b.date.localeCompare(a.date) || a.workspaceRootPath.localeCompare(b.workspaceRootPath);
|
||||||
|
}).slice(0, MAX_SUGGESTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
function globalEventKeys(settings) {
|
function globalEventKeys(settings) {
|
||||||
var keys = [LEGACY_KEY, GLOBAL_KEY];
|
var keys = [LEGACY_KEY, GLOBAL_KEY];
|
||||||
Object.keys(settings || {}).forEach(function (key) {
|
Object.keys(settings || {}).forEach(function (key) {
|
||||||
|
|
@ -176,6 +261,27 @@
|
||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function eventsFromSettings(settings, workspaceRoot) {
|
||||||
|
settings = settings || {};
|
||||||
|
var workspace = cleanWorkspace(workspaceRoot);
|
||||||
|
if (!workspace) {
|
||||||
|
var all = [];
|
||||||
|
globalEventKeys(settings).forEach(function (key) {
|
||||||
|
all = all.concat(normalizeStoredEvents(settings[key], key));
|
||||||
|
});
|
||||||
|
return sortEvents(all);
|
||||||
|
}
|
||||||
|
var workspaceKey = WORKSPACE_PREFIX + encodeKey(workspace);
|
||||||
|
var scopedEvents = normalizeStoredEvents(settings[workspaceKey], workspaceKey);
|
||||||
|
var globalEvents = normalizeStoredEvents(settings[GLOBAL_KEY], GLOBAL_KEY).filter(function (item) {
|
||||||
|
return item.workspaceRootPath === workspace;
|
||||||
|
});
|
||||||
|
var legacyEvents = normalizeStoredEvents(settings[LEGACY_KEY], LEGACY_KEY).filter(function (item) {
|
||||||
|
return item.workspaceRootPath === workspace;
|
||||||
|
});
|
||||||
|
return sortEvents(scopedEvents.concat(globalEvents, legacyEvents));
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
var date = new Date(value);
|
var date = new Date(value);
|
||||||
|
|
@ -193,6 +299,7 @@
|
||||||
|
|
||||||
var scope = scopeFromProps(props || {});
|
var scope = scopeFromProps(props || {});
|
||||||
var events = [];
|
var events = [];
|
||||||
|
var suggestions = [];
|
||||||
var statusText = 'Loading activity...';
|
var statusText = 'Loading activity...';
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
|
|
@ -212,6 +319,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
events = [];
|
events = [];
|
||||||
|
updateSuggestions();
|
||||||
persist().then(render);
|
persist().then(render);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -221,10 +329,19 @@
|
||||||
toolbar.appendChild(statusEl);
|
toolbar.appendChild(statusEl);
|
||||||
toolbar.appendChild(clearBtn);
|
toolbar.appendChild(clearBtn);
|
||||||
|
|
||||||
|
var suggestionsEl = el('div', {
|
||||||
|
className: 'activity-suggestions',
|
||||||
|
'data-activity-section': 'worklog-suggestions'
|
||||||
|
});
|
||||||
var listEl = el('div', { className: 'activity-list' });
|
var listEl = el('div', { className: 'activity-list' });
|
||||||
containerEl.appendChild(toolbar);
|
containerEl.appendChild(toolbar);
|
||||||
|
containerEl.appendChild(suggestionsEl);
|
||||||
containerEl.appendChild(listEl);
|
containerEl.appendChild(listEl);
|
||||||
|
|
||||||
|
function updateSuggestions() {
|
||||||
|
suggestions = buildWorklogSuggestions(events, scope.mode === 'workspace' ? scope.workspaceRoot : '');
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
var toStore = scope.mode === 'global'
|
var toStore = scope.mode === 'global'
|
||||||
|
|
@ -280,11 +397,36 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSuggestions() {
|
||||||
|
suggestionsEl.innerHTML = '';
|
||||||
|
if (!suggestions.length) {
|
||||||
|
suggestionsEl.setAttribute('hidden', 'hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof suggestionsEl.removeAttribute === 'function') suggestionsEl.removeAttribute('hidden');
|
||||||
|
else delete suggestionsEl.attributes.hidden;
|
||||||
|
suggestionsEl.appendChild(el('div', { className: 'activity-suggestions-title', textContent: 'Worklog suggestions' }));
|
||||||
|
suggestions.forEach(function (suggestion) {
|
||||||
|
suggestionsEl.appendChild(el('div', {
|
||||||
|
className: 'activity-suggestion',
|
||||||
|
'data-worklog-suggestion': suggestion.suggestionId
|
||||||
|
}, [
|
||||||
|
el('div', {}, [
|
||||||
|
el('div', { className: 'activity-suggestion-title', textContent: suggestion.title }),
|
||||||
|
el('div', { className: 'activity-suggestion-summary', textContent: suggestion.summary })
|
||||||
|
]),
|
||||||
|
el('div', { className: 'activity-suggestion-minutes', textContent: suggestion.minutes + ' min' })
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
countEl.textContent = events.length + ' event' + (events.length === 1 ? '' : 's');
|
countEl.textContent = events.length + ' event' + (events.length === 1 ? '' : 's');
|
||||||
clearBtn.disabled = events.length === 0;
|
clearBtn.disabled = events.length === 0;
|
||||||
statusEl.textContent = statusText;
|
statusEl.textContent = statusText;
|
||||||
statusEl.className = 'activity-status' + (statusClass ? ' ' + statusClass : '');
|
statusEl.className = 'activity-status' + (statusClass ? ' ' + statusClass : '');
|
||||||
|
updateSuggestions();
|
||||||
|
renderSuggestions();
|
||||||
renderList();
|
renderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,31 +434,43 @@
|
||||||
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve();
|
||||||
if (scope.mode === 'global') {
|
if (scope.mode === 'global') {
|
||||||
return api.settings.read().then(function (settings) {
|
return api.settings.read().then(function (settings) {
|
||||||
var all = [];
|
events = eventsFromSettings(settings || {}, '');
|
||||||
globalEventKeys(settings || {}).forEach(function (key) {
|
|
||||||
all = all.concat(normalizeStoredEvents((settings || {})[key], key));
|
|
||||||
});
|
|
||||||
events = sortEvents(all);
|
|
||||||
}).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';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return api.settings.read().then(function (settings) {
|
return api.settings.read().then(function (settings) {
|
||||||
var scopedEvents = normalizeStoredEvents((settings || {})[scope.key], scope.key);
|
events = eventsFromSettings(settings || {}, scope.workspaceRoot);
|
||||||
var globalEvents = normalizeStoredEvents((settings || {})[GLOBAL_KEY], GLOBAL_KEY).filter(function (item) {
|
|
||||||
return item.workspaceRootPath === scope.workspaceRoot;
|
|
||||||
});
|
|
||||||
var legacyEvents = normalizeStoredEvents((settings || {})[LEGACY_KEY], LEGACY_KEY).filter(function (item) {
|
|
||||||
return item.workspaceRootPath === scope.workspaceRoot;
|
|
||||||
});
|
|
||||||
events = sortEvents(scopedEvents.concat(globalEvents, legacyEvents));
|
|
||||||
}).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';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function suggestWorklog(args) {
|
||||||
|
var workspace = cleanWorkspace(args && args.workspaceRootPath);
|
||||||
|
if (!api || !api.settings || typeof api.settings.read !== 'function') {
|
||||||
|
return Promise.resolve({ suggestions: buildWorklogSuggestions(events, workspace || (scope.mode === 'workspace' ? scope.workspaceRoot : '')) });
|
||||||
|
}
|
||||||
|
return api.settings.read().then(function (settings) {
|
||||||
|
var sourceEvents = eventsFromSettings(settings || {}, workspace);
|
||||||
|
return { suggestions: buildWorklogSuggestions(sourceEvents, workspace) };
|
||||||
|
}).catch(function () {
|
||||||
|
return { suggestions: [] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerCommands() {
|
||||||
|
if (!api || !api.commands || typeof api.commands.register !== 'function') return Promise.resolve();
|
||||||
|
return api.commands.register(WORKLOG_COMMAND_ID, suggestWorklog).then(function (unregister) {
|
||||||
|
if (typeof unregister === 'function') unsubscribers.push(unregister);
|
||||||
|
}).catch(function (err) {
|
||||||
|
statusText = 'Activity commands unavailable: ' + (err && err.message ? err.message : String(err));
|
||||||
|
statusClass = 'error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function subscribeEvents() {
|
function subscribeEvents() {
|
||||||
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) {
|
||||||
|
|
@ -340,6 +494,9 @@
|
||||||
loadStored().then(function () {
|
loadStored().then(function () {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
render();
|
render();
|
||||||
|
return registerCommands();
|
||||||
|
}).then(function () {
|
||||||
|
if (disposed) return;
|
||||||
return subscribeEvents();
|
return subscribeEvents();
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
if (!disposed) render();
|
if (!disposed) render();
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"events.subscribe",
|
"events.subscribe",
|
||||||
"storage.namespace",
|
"storage.namespace",
|
||||||
"ui.register"
|
"ui.register",
|
||||||
|
"commands.register"
|
||||||
],
|
],
|
||||||
"frontend": {
|
"frontend": {
|
||||||
"entry": "frontend/src/index.js"
|
"entry": "frontend/src/index.js"
|
||||||
|
|
@ -46,6 +47,13 @@
|
||||||
"component": "ActivityView"
|
"component": "ActivityView"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"id": "verstak.activity.suggestWorklog",
|
||||||
|
"title": "Suggest Worklog From Activity",
|
||||||
|
"handler": "verstak.activity.suggestWorklog"
|
||||||
|
}
|
||||||
|
],
|
||||||
"activityProviders": [
|
"activityProviders": [
|
||||||
{
|
{
|
||||||
"id": "verstak.activity.log",
|
"id": "verstak.activity.log",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ const vm = require('vm');
|
||||||
|
|
||||||
const root = path.resolve(__dirname, '..');
|
const root = path.resolve(__dirname, '..');
|
||||||
const sourcePath = path.join(root, 'plugins', 'activity', 'frontend', 'src', 'index.js');
|
const sourcePath = path.join(root, 'plugins', 'activity', 'frontend', 'src', 'index.js');
|
||||||
|
const manifestPath = path.join(root, 'plugins', 'activity', 'plugin.json');
|
||||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
const WORKLOG_COMMAND_ID = 'verstak.activity.suggestWorklog';
|
||||||
|
|
||||||
class FakeNode {
|
class FakeNode {
|
||||||
constructor(tagName) {
|
constructor(tagName) {
|
||||||
|
|
@ -116,9 +119,11 @@ function loadComponent(document) {
|
||||||
function makeApi(initialSettings = {}) {
|
function makeApi(initialSettings = {}) {
|
||||||
const settings = { ...initialSettings };
|
const settings = { ...initialSettings };
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
|
const commandHandlers = new Map();
|
||||||
const unsubscribed = [];
|
const unsubscribed = [];
|
||||||
return {
|
return {
|
||||||
handlers,
|
handlers,
|
||||||
|
commandHandlers,
|
||||||
unsubscribed,
|
unsubscribed,
|
||||||
settings: {
|
settings: {
|
||||||
read: async (key) => (key ? settings[key] : { ...settings }),
|
read: async (key) => (key ? settings[key] : { ...settings }),
|
||||||
|
|
@ -136,6 +141,12 @@ function makeApi(initialSettings = {}) {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
commands: {
|
||||||
|
register: async (commandId, handler) => {
|
||||||
|
commandHandlers.set(commandId, handler);
|
||||||
|
return () => commandHandlers.delete(commandId);
|
||||||
|
},
|
||||||
|
},
|
||||||
storedEvents(key = 'events') {
|
storedEvents(key = 'events') {
|
||||||
return settings[key] || [];
|
return settings[key] || [];
|
||||||
},
|
},
|
||||||
|
|
@ -161,6 +172,11 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
const clientKey = 'events:workspace:ClientA';
|
const clientKey = 'events:workspace:ClientA';
|
||||||
const globalKey = 'events:global';
|
const globalKey = 'events:global';
|
||||||
|
|
||||||
|
if (!manifest.permissions.includes('commands.register')) throw new Error('activity manifest must request commands.register');
|
||||||
|
const worklogCommand = (manifest.contributes.commands || []).find((item) => item.id === WORKLOG_COMMAND_ID);
|
||||||
|
if (!worklogCommand || worklogCommand.handler !== WORKLOG_COMMAND_ID) throw new Error('activity worklog suggestion command contribution is missing');
|
||||||
|
if (typeof api.commandHandlers.get(WORKLOG_COMMAND_ID) !== 'function') throw new Error('activity worklog suggestion command was not registered');
|
||||||
|
|
||||||
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`);
|
||||||
}
|
}
|
||||||
|
|
@ -181,6 +197,19 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
text: 'Selected text',
|
text: 'Selected text',
|
||||||
workspaceRootPath: 'Project',
|
workspaceRootPath: 'Project',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
activityId: 'note-1',
|
||||||
|
type: 'note.saved',
|
||||||
|
title: 'Saved note',
|
||||||
|
summary: 'Project/Notes/Case.md',
|
||||||
|
occurredAt: '2026-06-27T00:20:00Z',
|
||||||
|
sourcePluginId: 'verstak.notes',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
payload: {
|
||||||
|
title: 'Saved note',
|
||||||
|
path: 'Project/Notes/Case.md',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
},
|
||||||
}]);
|
}]);
|
||||||
await api.handlers['browser.capture.selection']({
|
await api.handlers['browser.capture.selection']({
|
||||||
name: 'browser.capture.selection',
|
name: 'browser.capture.selection',
|
||||||
|
|
@ -197,12 +226,25 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
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 !== 2) throw new Error(`expected two stored activity events, 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 (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');
|
||||||
|
if (!container.textContent.includes('Worklog suggestions')) throw new Error('worklog suggestions section was not rendered');
|
||||||
|
if (!container.textContent.includes('Project work on 2026-06-27')) throw new Error('workspace worklog suggestion title was not rendered');
|
||||||
|
if (!container.textContent.includes('30 min')) throw new Error('workspace worklog suggestion duration was not rendered');
|
||||||
|
const suggestionNode = walk(container, (node) => node.getAttribute && node.getAttribute('data-worklog-suggestion') === 'worklog:Project:2026-06-27');
|
||||||
|
if (!suggestionNode) throw new Error('worklog suggestion data attribute was not rendered');
|
||||||
|
|
||||||
|
const commandResult = await api.commandHandlers.get(WORKLOG_COMMAND_ID)({ workspaceRootPath: 'Project' });
|
||||||
|
const suggestions = commandResult && commandResult.suggestions;
|
||||||
|
if (!Array.isArray(suggestions) || suggestions.length !== 1) throw new Error('worklog suggestion command returned unexpected suggestions');
|
||||||
|
if (suggestions[0].suggestionId !== 'worklog:Project:2026-06-27') throw new Error('worklog suggestion id mismatch');
|
||||||
|
if (suggestions[0].minutes !== 30) throw new Error(`expected 30 suggested minutes, got ${suggestions[0].minutes}`);
|
||||||
|
if (!suggestions[0].summary.includes('Example Article') || !suggestions[0].summary.includes('Saved note')) throw new Error('worklog suggestion summary did not include event titles');
|
||||||
|
if (suggestions[0].eventIds.join(',') !== 'capture-1,note-1') throw new Error('worklog suggestion event ids mismatch');
|
||||||
|
|
||||||
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');
|
||||||
|
|
@ -233,11 +275,14 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
await flush();
|
await flush();
|
||||||
if (api.storedEvents(clientKey).length !== 1) throw new Error('ClientA activity was not stored under ClientA workspace key');
|
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');
|
if (!clientView.container.textContent.includes('Client note')) throw new Error('ClientA activity was not rendered');
|
||||||
|
if (!clientView.container.textContent.includes('ClientA work on 2026-06-27')) throw new Error('ClientA worklog suggestion was not rendered');
|
||||||
component.unmount && component.unmount(clientView.container);
|
component.unmount && component.unmount(clientView.container);
|
||||||
|
|
||||||
const globalView = await mountWithApi(api, {});
|
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('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');
|
if (!globalView.container.textContent.includes('Client note')) throw new Error('global activity did not aggregate ClientA activity');
|
||||||
|
if (!globalView.container.textContent.includes('Project work on 2026-06-27')) throw new Error('global activity did not render Project worklog suggestion');
|
||||||
|
if (!globalView.container.textContent.includes('ClientA work on 2026-06-27')) throw new Error('global activity did not render ClientA worklog suggestion');
|
||||||
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');
|
||||||
|
|
@ -248,6 +293,7 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
clearButton.click();
|
clearButton.click();
|
||||||
await flush();
|
await flush();
|
||||||
if (api.storedEvents(projectKey).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');
|
||||||
|
if (container.textContent.includes('Project work on 2026-06-27')) throw new Error('clear action did not remove worklog suggestions');
|
||||||
|
|
||||||
component.unmount && component.unmount(container);
|
component.unmount && component.unmount(container);
|
||||||
if (api.unsubscribed.length !== 27) throw new Error(`expected 27 unsubscribers, got ${api.unsubscribed.length}`);
|
if (api.unsubscribed.length !== 27) throw new Error(`expected 27 unsubscribers, got ${api.unsubscribed.length}`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue