feat: convert completed todos into journal entries
This commit is contained in:
parent
31c93bdf5b
commit
16e1407440
|
|
@ -131,6 +131,7 @@
|
||||||
minutes: Math.max(0, Number(value.minutes || 0)),
|
minutes: Math.max(0, Number(value.minutes || 0)),
|
||||||
billable: value.billable === true,
|
billable: value.billable === true,
|
||||||
sourceCandidateId: text(value.sourceCandidateId || value.sourceSuggestionId),
|
sourceCandidateId: text(value.sourceCandidateId || value.sourceSuggestionId),
|
||||||
|
sourceTodoId: text(value.sourceTodoId),
|
||||||
activityIds: Array.isArray(value.activityIds)
|
activityIds: Array.isArray(value.activityIds)
|
||||||
? value.activityIds.map(text)
|
? value.activityIds.map(text)
|
||||||
: (Array.isArray(value.eventIds) ? value.eventIds.map(text) : [])
|
: (Array.isArray(value.eventIds) ? value.eventIds.map(text) : [])
|
||||||
|
|
@ -162,6 +163,7 @@
|
||||||
minutes: entry.minutes,
|
minutes: entry.minutes,
|
||||||
billable: entry.billable,
|
billable: entry.billable,
|
||||||
sourceCandidateId: entry.sourceCandidateId,
|
sourceCandidateId: entry.sourceCandidateId,
|
||||||
|
sourceTodoId: entry.sourceTodoId,
|
||||||
activityIds: entry.activityIds || []
|
activityIds: entry.activityIds || []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -170,7 +172,9 @@
|
||||||
function sortEntries(entryList) {
|
function sortEntries(entryList) {
|
||||||
var seen = {};
|
var seen = {};
|
||||||
return entryList.filter(function (entry) {
|
return entryList.filter(function (entry) {
|
||||||
var key = entry.sourceCandidateId || entry.entryId;
|
var key = entry.sourceCandidateId
|
||||||
|
? 'candidate:' + entry.sourceCandidateId
|
||||||
|
: (entry.sourceTodoId ? 'todo:' + entry.sourceTodoId : 'entry:' + entry.entryId);
|
||||||
if (!key || seen[key]) return false;
|
if (!key || seen[key]) return false;
|
||||||
seen[key] = true;
|
seen[key] = true;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -233,6 +237,22 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function completedTodoFromRequest(request, workspaceRoot) {
|
||||||
|
var value = request && request.type === 'completed-todo' ? request.todo : null;
|
||||||
|
if (!value || typeof value !== 'object') return null;
|
||||||
|
var workspace = cleanWorkspace(value.workspaceRootPath);
|
||||||
|
var todoId = text(value.id).trim();
|
||||||
|
var title = text(value.title).trim();
|
||||||
|
if (!todoId || !title || !workspace || workspace !== cleanWorkspace(workspaceRoot)) return null;
|
||||||
|
return {
|
||||||
|
id: todoId,
|
||||||
|
title: title,
|
||||||
|
description: text(value.description || value.body),
|
||||||
|
workspaceRootPath: workspace,
|
||||||
|
completedAt: text(value.completedAt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var ICONS = {
|
var ICONS = {
|
||||||
add: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
|
add: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
|
||||||
edit: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
|
edit: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z',
|
||||||
|
|
@ -318,15 +338,16 @@
|
||||||
modalHost.setAttribute('hidden', 'hidden');
|
modalHost.setAttribute('hidden', 'hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEntryModal(existingEntry, candidate) {
|
function showEntryModal(existingEntry, candidate, completedTodo) {
|
||||||
if (scope.mode !== 'workspace') return;
|
if (scope.mode !== 'workspace') return;
|
||||||
var editing = !!existingEntry;
|
var editing = !!existingEntry;
|
||||||
var reviewingCandidate = !editing && !!candidate;
|
var reviewingCandidate = !editing && !!candidate;
|
||||||
var dateInput = el('input', { className: 'journal-input', type: 'date', value: editing ? existingEntry.date : (reviewingCandidate ? candidateDate(candidate.startedAt) : today()), 'data-journal-input': 'date' });
|
var reviewingTodo = !editing && !!completedTodo;
|
||||||
var titleInput = el('input', { className: 'journal-input', type: 'text', placeholder: 'Work item', value: editing ? existingEntry.title : '', 'data-journal-input': 'title' });
|
var dateInput = el('input', { className: 'journal-input', type: 'date', value: editing ? existingEntry.date : (reviewingCandidate ? candidateDate(candidate.startedAt) : (reviewingTodo ? candidateDate(completedTodo.completedAt) : today())), 'data-journal-input': 'date' });
|
||||||
|
var titleInput = el('input', { className: 'journal-input', type: 'text', placeholder: 'Work item', value: editing ? existingEntry.title : (reviewingTodo ? completedTodo.title : ''), 'data-journal-input': 'title' });
|
||||||
var summaryInput = el('textarea', { className: 'journal-input textarea', placeholder: 'Body', 'data-journal-input': 'summary' });
|
var summaryInput = el('textarea', { className: 'journal-input textarea', placeholder: 'Body', 'data-journal-input': 'summary' });
|
||||||
summaryInput.value = editing ? existingEntry.summary : '';
|
summaryInput.value = editing ? existingEntry.summary : (reviewingTodo ? completedTodo.description : '');
|
||||||
var minutesInput = el('input', { className: 'journal-input', type: 'number', min: '0', step: '1', value: editing ? existingEntry.minutes : (reviewingCandidate ? candidate.estimatedMinutes : '30'), 'data-journal-input': 'minutes' });
|
var minutesInput = el('input', { className: 'journal-input', type: 'number', min: '0', step: '1', value: editing ? existingEntry.minutes : (reviewingCandidate ? candidate.estimatedMinutes : (reviewingTodo ? '0' : '30')), 'data-journal-input': 'minutes' });
|
||||||
var billableInput = el('input', { type: 'checkbox', 'data-journal-input': 'billable' });
|
var billableInput = el('input', { type: 'checkbox', 'data-journal-input': 'billable' });
|
||||||
billableInput.checked = editing ? existingEntry.billable === true : false;
|
billableInput.checked = editing ? existingEntry.billable === true : false;
|
||||||
var activityInputs = reviewingCandidate ? candidate.activities.map(function (activity) {
|
var activityInputs = reviewingCandidate ? candidate.activities.map(function (activity) {
|
||||||
|
|
@ -343,6 +364,7 @@
|
||||||
minutes: minutesInput.value,
|
minutes: minutesInput.value,
|
||||||
billable: billableInput.checked === true,
|
billable: billableInput.checked === true,
|
||||||
sourceCandidateId: reviewingCandidate ? candidate.candidateId : (existingEntry ? existingEntry.sourceCandidateId : ''),
|
sourceCandidateId: reviewingCandidate ? candidate.candidateId : (existingEntry ? existingEntry.sourceCandidateId : ''),
|
||||||
|
sourceTodoId: reviewingTodo ? completedTodo.id : (existingEntry ? existingEntry.sourceTodoId : ''),
|
||||||
activityIds: reviewingCandidate
|
activityIds: reviewingCandidate
|
||||||
? activityInputs.filter(function (item) { return item.input.checked === true; }).map(function (item) { return item.activity.activityId; })
|
? activityInputs.filter(function (item) { return item.input.checked === true; }).map(function (item) { return item.activity.activityId; })
|
||||||
: (existingEntry ? existingEntry.activityIds : [])
|
: (existingEntry ? existingEntry.activityIds : [])
|
||||||
|
|
@ -362,6 +384,11 @@
|
||||||
var detail = (item.activity.occurredAt ? candidateTime(item.activity.occurredAt) + ' · ' : '') + item.activity.type + ' · ' + item.activity.activityId;
|
var detail = (item.activity.occurredAt ? candidateTime(item.activity.occurredAt) + ' · ' : '') + item.activity.type + ' · ' + item.activity.activityId;
|
||||||
return el('label', { className: 'journal-candidate-activity' }, [item.input, detail]);
|
return el('label', { className: 'journal-candidate-activity' }, [item.input, detail]);
|
||||||
}))) : null;
|
}))) : null;
|
||||||
|
var todoContext = reviewingTodo ? el('div', { className: 'journal-candidate-context', 'data-journal-todo': completedTodo.id }, [
|
||||||
|
el('strong', { textContent: 'Completed todo' }),
|
||||||
|
el('div', { textContent: 'Workspace: ' + completedTodo.workspaceRootPath }),
|
||||||
|
completedTodo.completedAt ? el('div', { textContent: 'Completed: ' + candidateTime(completedTodo.completedAt) }) : null
|
||||||
|
]) : null;
|
||||||
|
|
||||||
modalHost.innerHTML = '';
|
modalHost.innerHTML = '';
|
||||||
if (typeof modalHost.removeAttribute === 'function') modalHost.removeAttribute('hidden');
|
if (typeof modalHost.removeAttribute === 'function') modalHost.removeAttribute('hidden');
|
||||||
|
|
@ -370,8 +397,9 @@
|
||||||
if (event.target === event.currentTarget) closeEntryModal();
|
if (event.target === event.currentTarget) closeEntryModal();
|
||||||
} }, [
|
} }, [
|
||||||
el('div', { className: 'journal-modal' }, [
|
el('div', { className: 'journal-modal' }, [
|
||||||
el('div', { className: 'journal-modal-title', textContent: editing ? 'Edit journal entry' : (reviewingCandidate ? 'Review possible journal entry' : 'Add journal entry') }),
|
el('div', { className: 'journal-modal-title', textContent: editing ? 'Edit journal entry' : (reviewingCandidate ? 'Review possible journal entry' : (reviewingTodo ? 'Create journal entry from completed todo' : 'Add journal entry')) }),
|
||||||
candidateContext,
|
candidateContext,
|
||||||
|
todoContext,
|
||||||
el('div', { className: 'journal-modal-grid' }, [
|
el('div', { className: 'journal-modal-grid' }, [
|
||||||
el('label', { className: 'journal-field' }, ['Date', dateInput]),
|
el('label', { className: 'journal-field' }, ['Date', dateInput]),
|
||||||
el('label', { className: 'journal-field' }, ['Minutes', minutesInput]),
|
el('label', { className: 'journal-field' }, ['Minutes', minutesInput]),
|
||||||
|
|
@ -399,12 +427,19 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var sourceCandidateId = text(formValue && formValue.sourceCandidateId || (existingEntry && existingEntry.sourceCandidateId)).trim();
|
var sourceCandidateId = text(formValue && formValue.sourceCandidateId || (existingEntry && existingEntry.sourceCandidateId)).trim();
|
||||||
|
var sourceTodoId = text(formValue && formValue.sourceTodoId || (existingEntry && existingEntry.sourceTodoId)).trim();
|
||||||
if (!existingEntry && sourceCandidateId && entries.some(function (entry) { return entry.sourceCandidateId === sourceCandidateId; })) {
|
if (!existingEntry && sourceCandidateId && entries.some(function (entry) { return entry.sourceCandidateId === sourceCandidateId; })) {
|
||||||
statusText = 'A journal entry already references this candidate';
|
statusText = 'A journal entry already references this candidate';
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!existingEntry && sourceTodoId && entries.some(function (entry) { return entry.sourceTodoId === sourceTodoId; })) {
|
||||||
|
statusText = 'A journal entry already references this todo';
|
||||||
|
statusClass = 'error';
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var entry = normalizeEntry({
|
var entry = normalizeEntry({
|
||||||
entryId: existingEntry ? existingEntry.entryId : entryId(scope.workspaceRoot, formValue.date || today(), title),
|
entryId: existingEntry ? existingEntry.entryId : entryId(scope.workspaceRoot, formValue.date || today(), title),
|
||||||
workspaceRootPath: scope.workspaceRoot,
|
workspaceRootPath: scope.workspaceRoot,
|
||||||
|
|
@ -414,6 +449,7 @@
|
||||||
minutes: Number(formValue.minutes || 0),
|
minutes: Number(formValue.minutes || 0),
|
||||||
billable: formValue.billable === true,
|
billable: formValue.billable === true,
|
||||||
sourceCandidateId: sourceCandidateId,
|
sourceCandidateId: sourceCandidateId,
|
||||||
|
sourceTodoId: sourceTodoId,
|
||||||
activityIds: Array.isArray(formValue.activityIds) ? formValue.activityIds : (existingEntry ? existingEntry.activityIds : [])
|
activityIds: Array.isArray(formValue.activityIds) ? formValue.activityIds : (existingEntry ? existingEntry.activityIds : [])
|
||||||
}, scope.key);
|
}, scope.key);
|
||||||
if (existingEntry) {
|
if (existingEntry) {
|
||||||
|
|
@ -453,7 +489,7 @@
|
||||||
el('div', { className: 'journal-main' }, [
|
el('div', { className: 'journal-main' }, [
|
||||||
el('div', { className: 'journal-entry-title', textContent: entry.title }),
|
el('div', { className: 'journal-entry-title', textContent: entry.title }),
|
||||||
entry.summary ? el('div', { className: 'journal-summary', textContent: entry.summary }) : null,
|
entry.summary ? el('div', { className: 'journal-summary', textContent: entry.summary }) : null,
|
||||||
el('div', { className: 'journal-meta', textContent: entry.workspaceRootPath + (entry.billable ? ' · billable' : ' · non-billable') + (entry.activityIds.length ? ' · ' + entry.activityIds.length + ' linked activities' : '') })
|
el('div', { className: 'journal-meta', textContent: entry.workspaceRootPath + (entry.billable ? ' · billable' : ' · non-billable') + (entry.activityIds.length ? ' · ' + entry.activityIds.length + ' linked activities' : '') + (entry.sourceTodoId ? ' · linked todo' : '') })
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' }),
|
el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' }),
|
||||||
scope.mode === 'workspace' ? el('div', { className: 'journal-row-actions' }, [
|
scope.mode === 'workspace' ? el('div', { className: 'journal-row-actions' }, [
|
||||||
|
|
@ -476,7 +512,9 @@
|
||||||
loadStored().then(function () {
|
loadStored().then(function () {
|
||||||
render();
|
render();
|
||||||
var candidate = candidateFromRequest(props && props.toolRequest, scope.workspaceRoot);
|
var candidate = candidateFromRequest(props && props.toolRequest, scope.workspaceRoot);
|
||||||
|
var completedTodo = completedTodoFromRequest(props && props.toolRequest, scope.workspaceRoot);
|
||||||
if (candidate) showEntryModal(null, candidate);
|
if (candidate) showEntryModal(null, candidate);
|
||||||
|
else if (completedTodo) showEntryModal(null, null, completedTodo);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -488,6 +488,26 @@
|
||||||
window.dispatchEvent(new CustomEvent('verstak:workspace-open-tool', { detail: { kind: 'todo' } }));
|
window.dispatchEvent(new CustomEvent('verstak:workspace-open-tool', { detail: { kind: 'todo' } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createJournalEntry(todo) {
|
||||||
|
if (!todo || todo.status !== 'done' || scope.mode !== 'workspace') return;
|
||||||
|
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function' || typeof CustomEvent === 'undefined') return;
|
||||||
|
window.dispatchEvent(new CustomEvent('verstak:workspace-open-tool', {
|
||||||
|
detail: {
|
||||||
|
kind: 'journal',
|
||||||
|
toolRequest: {
|
||||||
|
type: 'completed-todo',
|
||||||
|
todo: {
|
||||||
|
id: todo.id,
|
||||||
|
title: todo.title,
|
||||||
|
description: todo.description,
|
||||||
|
workspaceRootPath: scope.workspaceRoot,
|
||||||
|
completedAt: todo.completedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function renderTodoMeta(todo) {
|
function renderTodoMeta(todo) {
|
||||||
var meta = [];
|
var meta = [];
|
||||||
var workspace = cleanWorkspace(todo.workspaceRootPath);
|
var workspace = cleanWorkspace(todo.workspaceRootPath);
|
||||||
|
|
@ -522,6 +542,9 @@
|
||||||
} else {
|
} else {
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'reopen', textContent: 'Reopen', onClick: function () { setTodoStatus(todo, 'open'); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'reopen', textContent: 'Reopen', onClick: function () { setTodoStatus(todo, 'open'); } }));
|
||||||
}
|
}
|
||||||
|
if (scope.mode === 'workspace' && todo.status === 'done') {
|
||||||
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'create-journal-entry', textContent: 'Create Journal Entry', onClick: function () { createJournalEntry(todo); } }));
|
||||||
|
}
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'edit', textContent: 'Edit', onClick: function () { showTodoModal(todo); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'edit', textContent: 'Edit', onClick: function () { showTodoModal(todo); } }));
|
||||||
actionButtons.push(el('button', { className: 'todo-btn danger', type: 'button', 'data-todo-action': 'delete', textContent: 'Delete', onClick: function () { deleteTodo(todo); } }));
|
actionButtons.push(el('button', { className: 'todo-btn danger', type: 'button', 'data-todo-action': 'delete', textContent: 'Delete', onClick: function () { deleteTodo(todo); } }));
|
||||||
listEl.appendChild(el('div', { className: 'todo-row' + (todo.status === 'done' ? ' done' : ''), 'data-todo-id': todo.id }, [
|
listEl.appendChild(el('div', { className: 'todo-row' + (todo.status === 'done' ? ' done' : ''), 'data-todo-id': todo.id }, [
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,56 @@ function byData(container, attr, value) {
|
||||||
await flush();
|
await flush();
|
||||||
if (api.storedEntries(projectKey).length !== 1) throw new Error('journal entry was not deleted');
|
if (api.storedEntries(projectKey).length !== 1) throw new Error('journal entry was not deleted');
|
||||||
|
|
||||||
|
const completedTodo = {
|
||||||
|
id: 'todo:Project:project-review',
|
||||||
|
title: 'Prepare project review',
|
||||||
|
description: 'Collect factual review notes.',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
completedAt: '2026-06-27T11:15:00.000Z',
|
||||||
|
};
|
||||||
|
const todoView = await mountWithApi(api, {
|
||||||
|
workspaceNode: { name: 'Project' },
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
toolRequest: { type: 'completed-todo', todo: completedTodo },
|
||||||
|
});
|
||||||
|
if (!todoView.container.textContent.includes('Create journal entry from completed todo')) {
|
||||||
|
throw new Error('completed Todo did not open the Journal conversion form');
|
||||||
|
}
|
||||||
|
if (!byData(todoView.container, 'data-journal-todo', completedTodo.id)) {
|
||||||
|
throw new Error('completed Todo context was not shown in the Journal form');
|
||||||
|
}
|
||||||
|
if (byData(todoView.container, 'data-journal-input', 'title').value !== completedTodo.title) {
|
||||||
|
throw new Error('completed Todo Journal form did not prefill the exact title');
|
||||||
|
}
|
||||||
|
if (byData(todoView.container, 'data-journal-input', 'summary').value !== completedTodo.description) {
|
||||||
|
throw new Error('completed Todo Journal form did not prefill the exact description');
|
||||||
|
}
|
||||||
|
if (byData(todoView.container, 'data-journal-input', 'minutes').value !== '0') {
|
||||||
|
throw new Error('completed Todo Journal form must not invent a duration');
|
||||||
|
}
|
||||||
|
byData(todoView.container, 'data-journal-input', 'title').value = 'Prepare project review for handoff';
|
||||||
|
byData(todoView.container, 'data-journal-input', 'summary').value = 'Reviewed factual project notes before handoff.';
|
||||||
|
byData(todoView.container, 'data-journal-action', 'save-entry').click();
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
if (api.storedEntries(projectKey).length !== 2) throw new Error('completed Todo Journal entry was not saved');
|
||||||
|
const todoEntry = api.storedEntries(projectKey).find((entry) => entry.sourceTodoId === completedTodo.id);
|
||||||
|
if (!todoEntry) throw new Error('completed Todo reference was not stored on the Journal entry');
|
||||||
|
if (todoEntry.title !== 'Prepare project review for handoff' || todoEntry.summary !== 'Reviewed factual project notes before handoff.') {
|
||||||
|
throw new Error('completed Todo Journal form did not preserve the user-edited fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateTodoView = await mountWithApi(api, {
|
||||||
|
workspaceNode: { name: 'Project' },
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
toolRequest: { type: 'completed-todo', todo: completedTodo },
|
||||||
|
});
|
||||||
|
byData(duplicateTodoView.container, 'data-journal-action', 'save-entry').click();
|
||||||
|
await flush();
|
||||||
|
if (api.storedEntries(projectKey).filter((entry) => entry.sourceTodoId === completedTodo.id).length !== 1) {
|
||||||
|
throw new Error('completed Todo Journal conversion created a duplicate entry');
|
||||||
|
}
|
||||||
|
|
||||||
const globalView = await mountWithApi(api, {});
|
const globalView = await mountWithApi(api, {});
|
||||||
if (!globalView.container.textContent.includes('Review research capture') && !globalView.container.textContent.includes('Draft brief updated')) {
|
if (!globalView.container.textContent.includes('Review research capture') && !globalView.container.textContent.includes('Draft brief updated')) {
|
||||||
throw new Error('global journal did not aggregate remaining entries');
|
throw new Error('global journal did not aggregate remaining entries');
|
||||||
|
|
@ -264,6 +314,8 @@ function byData(container, attr, value) {
|
||||||
|
|
||||||
component.unmount && component.unmount(container);
|
component.unmount && component.unmount(container);
|
||||||
component.unmount && component.unmount(candidateView.container);
|
component.unmount && component.unmount(candidateView.container);
|
||||||
|
component.unmount && component.unmount(todoView.container);
|
||||||
|
component.unmount && component.unmount(duplicateTodoView.container);
|
||||||
component.unmount && component.unmount(globalView.container);
|
component.unmount && component.unmount(globalView.container);
|
||||||
|
|
||||||
console.log('journal plugin smoke passed');
|
console.log('journal plugin smoke passed');
|
||||||
|
|
|
||||||
|
|
@ -116,16 +116,25 @@ function makeDocument() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadComponent(document) {
|
function FakeCustomEvent(type, options = {}) {
|
||||||
|
this.type = type;
|
||||||
|
this.detail = options.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadComponent(document, emittedEvents) {
|
||||||
const registry = {};
|
const registry = {};
|
||||||
const window = {
|
const window = {
|
||||||
VerstakPluginRegister(pluginId, bundle) {
|
VerstakPluginRegister(pluginId, bundle) {
|
||||||
registry[pluginId] = bundle.components || {};
|
registry[pluginId] = bundle.components || {};
|
||||||
},
|
},
|
||||||
|
dispatchEvent(event) {
|
||||||
|
emittedEvents.push(event);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
window.window = window;
|
window.window = window;
|
||||||
window.document = document;
|
window.document = document;
|
||||||
vm.runInNewContext(source, { console, Date, Math, document, window }, { filename: sourcePath });
|
window.CustomEvent = FakeCustomEvent;
|
||||||
|
vm.runInNewContext(source, { console, Date, Math, CustomEvent: FakeCustomEvent, document, window }, { filename: sourcePath });
|
||||||
const component = registry['verstak.todo'] && registry['verstak.todo'].TodoView;
|
const component = registry['verstak.todo'] && registry['verstak.todo'].TodoView;
|
||||||
if (!component) throw new Error('TodoView was not registered');
|
if (!component) throw new Error('TodoView was not registered');
|
||||||
return component;
|
return component;
|
||||||
|
|
@ -158,12 +167,12 @@ async function flush() {
|
||||||
for (let index = 0; index < 10; index += 1) await Promise.resolve();
|
for (let index = 0; index < 10; index += 1) await Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mountWithApi(apiState, props, document = makeDocument()) {
|
async function mountWithApi(apiState, props, emittedEvents = [], document = makeDocument()) {
|
||||||
const component = loadComponent(document);
|
const component = loadComponent(document, emittedEvents);
|
||||||
const container = new FakeNode('div');
|
const container = new FakeNode('div');
|
||||||
component.mount(container, props, apiState.api);
|
component.mount(container, props, apiState.api);
|
||||||
await flush();
|
await flush();
|
||||||
return { component, container, document };
|
return { component, container, document, emittedEvents };
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
@ -208,6 +217,24 @@ async function mountWithApi(apiState, props, document = makeDocument()) {
|
||||||
throw new Error('mark done did not persist completed state');
|
throw new Error('mark done did not persist completed state');
|
||||||
}
|
}
|
||||||
if (!byData(container, 'data-todo-action', 'reopen')) throw new Error('done Todo did not expose reopen action');
|
if (!byData(container, 'data-todo-action', 'reopen')) throw new Error('done Todo did not expose reopen action');
|
||||||
|
if (!byData(container, 'data-todo-action', 'create-journal-entry')) throw new Error('done workspace Todo did not expose Journal conversion');
|
||||||
|
|
||||||
|
byData(container, 'data-todo-action', 'create-journal-entry').click();
|
||||||
|
const journalEvent = workspaceView.emittedEvents.find((event) => event.type === 'verstak:workspace-open-tool');
|
||||||
|
if (!journalEvent || !journalEvent.detail || journalEvent.detail.kind !== 'journal') {
|
||||||
|
throw new Error('Todo Journal conversion did not request the Journal workspace tool');
|
||||||
|
}
|
||||||
|
const journalRequest = journalEvent.detail.toolRequest;
|
||||||
|
if (!journalRequest || journalRequest.type !== 'completed-todo' || !journalRequest.todo) {
|
||||||
|
throw new Error('Todo Journal conversion did not send a completed-todo request');
|
||||||
|
}
|
||||||
|
if (journalRequest.todo.id !== createdTodo.id
|
||||||
|
|| journalRequest.todo.title !== 'Prepare project review updated'
|
||||||
|
|| journalRequest.todo.description !== 'Collect factual review notes.'
|
||||||
|
|| journalRequest.todo.workspaceRootPath !== 'Project'
|
||||||
|
|| !journalRequest.todo.completedAt) {
|
||||||
|
throw new Error('Todo Journal conversion did not preserve factual completed Todo fields');
|
||||||
|
}
|
||||||
|
|
||||||
apiState.settings['todos:global'].push({
|
apiState.settings['todos:global'].push({
|
||||||
id: 'todo-client',
|
id: 'todo-client',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue