fix: localize journal candidate review
This commit is contained in:
parent
713b95b5a8
commit
3a1920d104
|
|
@ -363,6 +363,32 @@
|
|||
modalHost.setAttribute('hidden', 'hidden');
|
||||
}
|
||||
|
||||
function activityLabel(activity) {
|
||||
var type = text(activity && activity.type);
|
||||
var labels = {
|
||||
'browser.capture.link': ['ui.activity.captureLink', 'Link captured'],
|
||||
'browser.capture.selection': ['ui.activity.captureSelection', 'Selection captured'],
|
||||
'browser.capture.page': ['ui.activity.capturePage', 'Page captured'],
|
||||
'file.created': ['ui.activity.fileCreated', 'File created'],
|
||||
'file.updated': ['ui.activity.fileUpdated', 'File updated'],
|
||||
'file.deleted': ['ui.activity.fileDeleted', 'File deleted'],
|
||||
'note.saved': ['ui.activity.noteSaved', 'Note saved'],
|
||||
'workspace.created': ['ui.activity.dealCreated', 'Deal created'],
|
||||
'workspace.renamed': ['ui.activity.dealRenamed', 'Deal renamed']
|
||||
};
|
||||
var label = labels[type] || ['ui.activity.generic', 'Activity'];
|
||||
return tr(label[0], null, label[1]);
|
||||
}
|
||||
|
||||
function entryMeta(entry) {
|
||||
var facts = [entry.workspaceRootPath, entry.billable ? tr('ui.meta.billable', null, 'billable') : tr('ui.meta.nonBillable', null, 'non-billable')];
|
||||
if (entry.activityIds.length) {
|
||||
facts.push(tr(entry.activityIds.length === 1 ? 'ui.meta.activities.one' : 'ui.meta.activities.many', { count: entry.activityIds.length }, entry.activityIds.length + ' linked activities'));
|
||||
}
|
||||
if (entry.sourceTodoId) facts.push(tr('ui.meta.todo', null, 'linked todo'));
|
||||
return facts.join(' · ');
|
||||
}
|
||||
|
||||
function showEntryModal(existingEntry, candidate, completedTodo) {
|
||||
var editing = !!existingEntry;
|
||||
var reviewingCandidate = !editing && !!candidate;
|
||||
|
|
@ -406,22 +432,22 @@
|
|||
}
|
||||
|
||||
var candidateContext = reviewingCandidate ? el('div', { className: 'journal-candidate-context', 'data-journal-candidate': candidate.candidateId }, [
|
||||
el('strong', { textContent: 'Possible journal entry' }),
|
||||
el('div', { textContent: 'Workspace: ' + candidate.workspaceRootPath }),
|
||||
el('div', { textContent: 'Time: ' + candidateTime(candidate.startedAt) + ' - ' + candidateTime(candidate.endedAt) }),
|
||||
el('div', { textContent: 'Estimated duration: ' + candidate.estimatedMinutes + ' min' }),
|
||||
el('div', { textContent: 'Activities: ' + candidate.activityCount })
|
||||
el('strong', { textContent: tr('ui.candidate.title', null, 'Possible journal entry') }),
|
||||
el('div', { textContent: tr('ui.candidate.deal', { deal: candidate.workspaceRootPath }, 'Deal: ' + candidate.workspaceRootPath) }),
|
||||
el('div', { textContent: tr('ui.candidate.time', { start: candidateTime(candidate.startedAt), end: candidateTime(candidate.endedAt) }, 'Time: ' + candidateTime(candidate.startedAt) + ' – ' + candidateTime(candidate.endedAt)) }),
|
||||
el('div', { textContent: tr('ui.candidate.duration', { minutes: candidate.estimatedMinutes }, 'Estimated duration: ' + candidate.estimatedMinutes + ' min') }),
|
||||
el('div', { textContent: tr('ui.candidate.activities', { count: candidate.activityCount }, 'Activities: ' + candidate.activityCount) })
|
||||
]) : null;
|
||||
var candidateActivities = reviewingCandidate ? el('fieldset', { className: 'journal-candidate-activities' }, [
|
||||
el('legend', { textContent: 'Linked activities' })
|
||||
el('legend', { textContent: tr('ui.candidate.linkedActivities', null, 'Linked activities') })
|
||||
].concat(activityInputs.map(function (item) {
|
||||
var detail = (item.activity.occurredAt ? candidateTime(item.activity.occurredAt) + ' · ' : '') + item.activity.type + ' · ' + item.activity.activityId;
|
||||
var detail = (item.activity.occurredAt ? candidateTime(item.activity.occurredAt) + ' · ' : '') + activityLabel(item.activity);
|
||||
return el('label', { className: 'journal-candidate-activity' }, [item.input, detail]);
|
||||
}))) : 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
|
||||
el('strong', { textContent: tr('ui.todo.title', null, 'Completed todo') }),
|
||||
el('div', { textContent: tr('ui.candidate.deal', { deal: completedTodo.workspaceRootPath }, 'Deal: ' + completedTodo.workspaceRootPath) }),
|
||||
completedTodo.completedAt ? el('div', { textContent: tr('ui.todo.completed', { time: candidateTime(completedTodo.completedAt) }, 'Completed: ' + candidateTime(completedTodo.completedAt)) }) : null
|
||||
]) : null;
|
||||
|
||||
modalHost.innerHTML = '';
|
||||
|
|
@ -467,13 +493,13 @@
|
|||
var handledThrough = text(formValue && formValue.handledThrough).trim();
|
||||
var sourceTodoId = text(formValue && formValue.sourceTodoId || (existingEntry && existingEntry.sourceTodoId)).trim();
|
||||
if (!existingEntry && sourceCandidateId && entries.some(function (entry) { return entry.sourceCandidateId === sourceCandidateId; })) {
|
||||
statusText = 'A journal entry already references this candidate';
|
||||
statusText = tr('ui.candidate.duplicate', null, 'A journal entry already references this candidate');
|
||||
statusClass = 'error';
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (!existingEntry && sourceTodoId && entries.some(function (entry) { return entry.sourceTodoId === sourceTodoId; })) {
|
||||
statusText = 'A journal entry already references this todo';
|
||||
statusText = tr('ui.todo.duplicate', null, 'A journal entry already references this todo');
|
||||
statusClass = 'error';
|
||||
render();
|
||||
return;
|
||||
|
|
@ -500,7 +526,7 @@
|
|||
entries = sortEntries(entries);
|
||||
var targetEntries = entries.filter(function (item) { return item.workspaceRootPath === workspaceRoot; });
|
||||
closeEntryModal();
|
||||
statusText = existingEntry ? 'Entry updated' : 'Entry added';
|
||||
statusText = existingEntry ? tr('ui.updated', null, 'Entry updated') : tr('ui.added', null, 'Entry added');
|
||||
statusClass = '';
|
||||
persist(workspaceRoot, targetEntries).then(function () {
|
||||
if (!sessionID || !handledThrough || !api || !api.events || typeof api.events.publish !== 'function') return undefined;
|
||||
|
|
@ -535,9 +561,9 @@
|
|||
el('div', { className: 'journal-main' }, [
|
||||
el('div', { className: 'journal-entry-title', textContent: entry.title }),
|
||||
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' : '') + (entry.sourceTodoId ? ' · linked todo' : '') })
|
||||
el('div', { className: 'journal-meta', textContent: entryMeta(entry) })
|
||||
]),
|
||||
el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' }),
|
||||
el('div', { className: 'journal-minutes', textContent: tr('ui.minutesValue', { minutes: entry.minutes }, entry.minutes + ' min') }),
|
||||
scope.mode === 'workspace' ? el('div', { className: 'journal-row-actions' }, [
|
||||
el('button', { className: 'journal-icon-btn', type: 'button', title: tr('ui.edit', null, 'Edit'), 'aria-label': tr('ui.edit', null, 'Edit'), 'data-journal-action': 'edit', innerHTML: iconSvg('edit'), onClick: function () { showEntryModal(entry); } }),
|
||||
el('button', { className: 'journal-icon-btn danger', type: 'button', title: tr('ui.delete', null, 'Delete'), 'aria-label': tr('ui.delete', null, 'Delete'), 'data-journal-action': 'delete', innerHTML: iconSvg('trash'), onClick: function () { deleteEntry(entry); } })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"manifest.name": "Journal",
|
||||
"manifest.description": "Workspace-scoped journal with user-authored entries and optional Activity links.",
|
||||
"manifest.description": "Deal-scoped journal with user-authored entries and optional Activity links.",
|
||||
"contributions.views.verstak.journal.view.title": "Journal",
|
||||
"contributions.sidebarItems.verstak.journal.sidebar.title": "Journal",
|
||||
"contributions.workspaceItems.verstak.journal.workspace.title": "Journal",
|
||||
|
|
@ -31,5 +31,33 @@
|
|||
"ui.edit": "Edit",
|
||||
"ui.delete": "Delete",
|
||||
"ui.entryCount.one": "{count} entry",
|
||||
"ui.entryCount.many": "{count} entries"
|
||||
"ui.entryCount.many": "{count} entries",
|
||||
"ui.candidate.title": "Possible journal entry",
|
||||
"ui.candidate.deal": "Deal: {deal}",
|
||||
"ui.candidate.time": "Time: {start} – {end}",
|
||||
"ui.candidate.duration": "Estimated duration: {minutes} min",
|
||||
"ui.candidate.activities": "Activities: {count}",
|
||||
"ui.candidate.linkedActivities": "Linked activities",
|
||||
"ui.candidate.duplicate": "A journal entry already references this candidate",
|
||||
"ui.todo.title": "Completed todo",
|
||||
"ui.todo.completed": "Completed: {time}",
|
||||
"ui.todo.duplicate": "A journal entry already references this todo",
|
||||
"ui.activity.captureLink": "Link captured",
|
||||
"ui.activity.captureSelection": "Selection captured",
|
||||
"ui.activity.capturePage": "Page captured",
|
||||
"ui.activity.fileCreated": "File created",
|
||||
"ui.activity.fileUpdated": "File updated",
|
||||
"ui.activity.fileDeleted": "File deleted",
|
||||
"ui.activity.noteSaved": "Note saved",
|
||||
"ui.activity.dealCreated": "Deal created",
|
||||
"ui.activity.dealRenamed": "Deal renamed",
|
||||
"ui.activity.generic": "Activity",
|
||||
"ui.updated": "Entry updated",
|
||||
"ui.added": "Entry added",
|
||||
"ui.minutesValue": "{minutes} min",
|
||||
"ui.meta.billable": "billable",
|
||||
"ui.meta.nonBillable": "non-billable",
|
||||
"ui.meta.activities.one": "{count} linked activity",
|
||||
"ui.meta.activities.many": "{count} linked activities",
|
||||
"ui.meta.todo": "linked todo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"manifest.name": "Журнал",
|
||||
"manifest.description": "Журнал рабочего пространства с ручными записями и необязательными связями с активностью.",
|
||||
"manifest.description": "Журнал Дела с ручными записями и необязательными связями с активностью.",
|
||||
"contributions.views.verstak.journal.view.title": "Журнал",
|
||||
"contributions.sidebarItems.verstak.journal.sidebar.title": "Журнал",
|
||||
"contributions.workspaceItems.verstak.journal.workspace.title": "Журнал",
|
||||
|
|
@ -31,5 +31,33 @@
|
|||
"ui.edit": "Изменить",
|
||||
"ui.delete": "Удалить",
|
||||
"ui.entryCount.one": "{count} запись",
|
||||
"ui.entryCount.many": "Записей: {count}"
|
||||
"ui.entryCount.many": "Записей: {count}",
|
||||
"ui.candidate.title": "Возможная запись журнала",
|
||||
"ui.candidate.deal": "Дело: {deal}",
|
||||
"ui.candidate.time": "Время: {start} – {end}",
|
||||
"ui.candidate.duration": "Расчётная длительность: {minutes} мин",
|
||||
"ui.candidate.activities": "Активностей: {count}",
|
||||
"ui.candidate.linkedActivities": "Связанные активности",
|
||||
"ui.candidate.duplicate": "Запись журнала уже ссылается на этого кандидата",
|
||||
"ui.todo.title": "Выполненная задача",
|
||||
"ui.todo.completed": "Выполнена: {time}",
|
||||
"ui.todo.duplicate": "Запись журнала уже ссылается на эту задачу",
|
||||
"ui.activity.captureLink": "Сохранена ссылка",
|
||||
"ui.activity.captureSelection": "Захвачено выделение",
|
||||
"ui.activity.capturePage": "Сохранена страница",
|
||||
"ui.activity.fileCreated": "Создан файл",
|
||||
"ui.activity.fileUpdated": "Изменён файл",
|
||||
"ui.activity.fileDeleted": "Удалён файл",
|
||||
"ui.activity.noteSaved": "Сохранена заметка",
|
||||
"ui.activity.dealCreated": "Создано Дело",
|
||||
"ui.activity.dealRenamed": "Переименовано Дело",
|
||||
"ui.activity.generic": "Активность",
|
||||
"ui.updated": "Запись обновлена",
|
||||
"ui.added": "Запись добавлена",
|
||||
"ui.minutesValue": "{minutes} мин",
|
||||
"ui.meta.billable": "оплачиваемая",
|
||||
"ui.meta.nonBillable": "неоплачиваемая",
|
||||
"ui.meta.activities.one": "{count} связанная активность",
|
||||
"ui.meta.activities.many": "Связанных активностей: {count}",
|
||||
"ui.meta.todo": "связанная задача"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ const vm = require('vm');
|
|||
const root = path.resolve(__dirname, '..');
|
||||
const manifestPath = path.join(root, 'plugins', 'journal', 'plugin.json');
|
||||
const sourcePath = path.join(root, 'plugins', 'journal', 'frontend', 'src', 'index.js');
|
||||
const russianLocalePath = path.join(root, 'plugins', 'journal', 'locales', 'ru.json');
|
||||
|
||||
if (!fs.existsSync(manifestPath)) throw new Error('journal plugin manifest missing');
|
||||
if (!fs.existsSync(sourcePath)) throw new Error('journal frontend entry missing');
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
const russianLocale = JSON.parse(fs.readFileSync(russianLocalePath, 'utf8'));
|
||||
|
||||
class FakeNode {
|
||||
constructor(tagName) {
|
||||
|
|
@ -131,7 +133,7 @@ function loadComponent(document) {
|
|||
return component;
|
||||
}
|
||||
|
||||
function makeApi(initialSettings = {}) {
|
||||
function makeApi(initialSettings = {}, locale = null) {
|
||||
const settings = { ...initialSettings };
|
||||
const publishedEvents = [];
|
||||
return {
|
||||
|
|
@ -154,6 +156,11 @@ function makeApi(initialSettings = {}) {
|
|||
{ type: 'folder', relativePath: 'Client', name: 'Client' },
|
||||
],
|
||||
},
|
||||
i18n: locale ? {
|
||||
t(key, params, fallback) {
|
||||
return String(locale[key] || fallback || key).replace(/\{(\w+)\}/g, (_match, name) => String((params || {})[name] ?? ''));
|
||||
},
|
||||
} : null,
|
||||
storedEntries(key) {
|
||||
return settings[key] || [];
|
||||
},
|
||||
|
|
@ -245,8 +252,11 @@ function byData(container, attr, value) {
|
|||
toolRequest: { type: 'work-session-candidate', candidate },
|
||||
});
|
||||
if (!candidateView.container.textContent.includes('Review possible journal entry')) throw new Error('candidate review modal was not opened');
|
||||
if (!candidateView.container.textContent.includes('Workspace: Project')) throw new Error('candidate workspace was not shown for review');
|
||||
if (!candidateView.container.textContent.includes('Deal: Project')) throw new Error('candidate Deal was not shown for review');
|
||||
if (!candidateView.container.textContent.includes('Estimated duration: 51 min')) throw new Error('candidate duration was not shown for review');
|
||||
if (candidateView.container.textContent.includes('browser.capture.selection') || candidateView.container.textContent.includes('verstak.browser-inbox') || candidateView.container.textContent.includes('capture-1')) {
|
||||
throw new Error('candidate review exposed technical Activity identifiers');
|
||||
}
|
||||
if (byData(candidateView.container, 'data-journal-input', 'title').value !== '') throw new Error('candidate review must start with an empty title');
|
||||
if (byData(candidateView.container, 'data-journal-input', 'summary').value !== '') throw new Error('candidate review must start with an empty body');
|
||||
if (byData(candidateView.container, 'data-journal-input', 'minutes').value !== '51') throw new Error('candidate review must prefill the factual duration');
|
||||
|
|
@ -273,6 +283,15 @@ function byData(container, attr, value) {
|
|||
throw new Error('journal rows must not navigate to Activity by default');
|
||||
}
|
||||
|
||||
const russianCandidateView = await mountWithApi(makeApi({}, russianLocale), {
|
||||
workspaceNode: { name: 'Project' },
|
||||
workspaceRootPath: 'Project',
|
||||
toolRequest: { type: 'work-session-candidate', candidate },
|
||||
});
|
||||
if (!russianCandidateView.container.textContent.includes('Дело: Project') || !russianCandidateView.container.textContent.includes('Захвачено выделение')) {
|
||||
throw new Error('candidate review was not localized');
|
||||
}
|
||||
|
||||
byData(candidateView.container, 'data-journal-action', 'delete').click();
|
||||
await flush();
|
||||
if (api.storedEntries(projectKey).length !== 1) throw new Error('journal entry was not deleted');
|
||||
|
|
@ -350,6 +369,7 @@ function byData(container, attr, value) {
|
|||
|
||||
component.unmount && component.unmount(container);
|
||||
component.unmount && component.unmount(candidateView.container);
|
||||
component.unmount && component.unmount(russianCandidateView.container);
|
||||
component.unmount && component.unmount(todoView.container);
|
||||
component.unmount && component.unmount(duplicateTodoView.container);
|
||||
component.unmount && component.unmount(globalView.container);
|
||||
|
|
|
|||
Loading…
Reference in New Issue