Compare commits
11 Commits
eb0f468463
...
99172944a1
| Author | SHA1 | Date |
|---|---|---|
|
|
99172944a1 | |
|
|
a1b3c31d0d | |
|
|
1f03226378 | |
|
|
bee365ad22 | |
|
|
dfc53f1a5f | |
|
|
dd75335727 | |
|
|
f23fb6e993 | |
|
|
0cd2c2a6ab | |
|
|
304620952f | |
|
|
112fb4cbec | |
|
|
78a9dd31a3 |
|
|
@ -79,6 +79,7 @@
|
||||||
'.activity-btn.danger{border-color:rgba(233,69,96,.42);color:#ff9a9a}',
|
'.activity-btn.danger{border-color:rgba(233,69,96,.42);color:#ff9a9a}',
|
||||||
'.activity-status{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3);white-space:nowrap}',
|
'.activity-status{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3);white-space:nowrap}',
|
||||||
'.activity-status.error{display:inline-flex;border:1px solid rgba(233,69,96,.45);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-danger-muted,rgba(233,69,96,.14));color:#ffc6ce;padding:.18rem .4rem}',
|
'.activity-status.error{display:inline-flex;border:1px solid rgba(233,69,96,.45);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-danger-muted,rgba(233,69,96,.14));color:#ffc6ce;padding:.18rem .4rem}',
|
||||||
|
'.activity-modal-host[hidden]{display:none}.activity-modal-overlay{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;padding:1rem;background:rgba(0,0,0,.58)}.activity-modal{width:440px;max-width:96vw;display:grid;gap:.75rem;padding:1rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c);box-shadow:0 18px 44px rgba(0,0,0,.38)}.activity-modal-title{font-size:.95rem;font-weight:650}.activity-modal-copy{color:var(--vt-color-text-secondary,#b7c0d4);font-size:.84rem;line-height:1.45}.activity-modal-actions{display:flex;justify-content:flex-end;gap:.5rem}.activity-btn.destructive{background:var(--vt-color-danger,#e94560);border-color:var(--vt-color-danger,#e94560);color:#fff}',
|
||||||
'.activity-candidates{border-bottom:1px solid rgba(32,43,70,.72);background:var(--vt-color-surface-muted,#111629);padding:.65rem .75rem;display:grid;gap:.5rem}',
|
'.activity-candidates{border-bottom:1px solid rgba(32,43,70,.72);background:var(--vt-color-surface-muted,#111629);padding:.65rem .75rem;display:grid;gap:.5rem}',
|
||||||
'.activity-candidates-title{font-size:.76rem;font-weight:600;color:var(--vt-color-text-muted,#7f8aa3);text-transform:uppercase;letter-spacing:.04em}',
|
'.activity-candidates-title{font-size:.76rem;font-weight:600;color:var(--vt-color-text-muted,#7f8aa3);text-transform:uppercase;letter-spacing:.04em}',
|
||||||
'.activity-candidate{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.65rem;align-items:start;padding:.65rem .75rem;border:1px solid rgba(78,204,163,.34);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c)}',
|
'.activity-candidate{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.65rem;align-items:start;padding:.65rem .75rem;border:1px solid rgba(78,204,163,.34);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c)}',
|
||||||
|
|
@ -562,6 +563,14 @@
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
var unsubscribers = [];
|
var unsubscribers = [];
|
||||||
|
|
||||||
|
function reportError(key, fallback, err) {
|
||||||
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
|
console.warn('[verstak.activity] ' + key, err);
|
||||||
|
}
|
||||||
|
statusText = tr(key, null, fallback);
|
||||||
|
statusClass = 'error';
|
||||||
|
}
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'activity-toolbar' });
|
var toolbar = el('div', { className: 'activity-toolbar' });
|
||||||
var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Activity') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Activity · ' + scope.label) });
|
var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Activity') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Activity · ' + scope.label) });
|
||||||
var countEl = el('span', { className: 'activity-count' });
|
var countEl = el('span', { className: 'activity-count' });
|
||||||
|
|
@ -570,18 +579,7 @@
|
||||||
className: 'activity-btn danger',
|
className: 'activity-btn danger',
|
||||||
'data-activity-action': 'clear',
|
'data-activity-action': 'clear',
|
||||||
textContent: tr('ui.clear', null, 'Clear'),
|
textContent: tr('ui.clear', null, 'Clear'),
|
||||||
onClick: function () {
|
onClick: showClearConfirmation
|
||||||
if (scope.mode === 'global') {
|
|
||||||
clearGlobal().then(render);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
events = [];
|
|
||||||
candidateSourceEvents = candidateSourceEvents.filter(function (activity) {
|
|
||||||
return candidateWorkspace(activity) !== scope.workspaceRoot;
|
|
||||||
});
|
|
||||||
updateCandidates();
|
|
||||||
clearWorkspaceRaw(scope.workspaceRoot).then(persist).then(render);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
toolbar.appendChild(titleEl);
|
toolbar.appendChild(titleEl);
|
||||||
toolbar.appendChild(countEl);
|
toolbar.appendChild(countEl);
|
||||||
|
|
@ -594,9 +592,11 @@
|
||||||
'data-activity-section': 'work-session-candidates'
|
'data-activity-section': 'work-session-candidates'
|
||||||
});
|
});
|
||||||
var listEl = el('div', { className: 'activity-list' });
|
var listEl = el('div', { className: 'activity-list' });
|
||||||
|
var modalHost = el('div', { className: 'activity-modal-host', hidden: 'hidden' });
|
||||||
containerEl.appendChild(toolbar);
|
containerEl.appendChild(toolbar);
|
||||||
containerEl.appendChild(candidatesEl);
|
containerEl.appendChild(candidatesEl);
|
||||||
containerEl.appendChild(listEl);
|
containerEl.appendChild(listEl);
|
||||||
|
containerEl.appendChild(modalHost);
|
||||||
|
|
||||||
function candidatesForWorkspace(workspaceRoot) {
|
function candidatesForWorkspace(workspaceRoot) {
|
||||||
return visibleCandidates(candidateSourceEvents, workspaceRoot, sessionRegistry, dismissedByWorkspace, handledSessions);
|
return visibleCandidates(candidateSourceEvents, workspaceRoot, sessionRegistry, dismissedByWorkspace, handledSessions);
|
||||||
|
|
@ -642,8 +642,7 @@
|
||||||
? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; })
|
? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; })
|
||||||
: events;
|
: events;
|
||||||
return api.settings.write(scope.key, storageEvents(toStore)).then(persistSessionRegistry).then(persistCandidateCaches).catch(function (err) {
|
return api.settings.write(scope.key, storageEvents(toStore)).then(persistSessionRegistry).then(persistCandidateCaches).catch(function (err) {
|
||||||
statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save activity: ' + (err && err.message ? err.message : String(err)));
|
reportError('ui.saveError', 'Could not save activity. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -676,11 +675,61 @@
|
||||||
statusText = tr('ui.cleared', null, 'Activity cleared');
|
statusText = tr('ui.cleared', null, 'Activity cleared');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = tr('ui.clearError', { error: err && err.message ? err.message : String(err) }, 'Could not clear activity: ' + (err && err.message ? err.message : String(err)));
|
reportError('ui.clearError', 'Could not clear activity. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeClearConfirmation() {
|
||||||
|
modalHost.innerHTML = '';
|
||||||
|
modalHost.setAttribute('hidden', 'hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCurrentScope() {
|
||||||
|
if (scope.mode === 'global') {
|
||||||
|
return clearGlobal().then(render);
|
||||||
|
}
|
||||||
|
events = [];
|
||||||
|
candidateSourceEvents = candidateSourceEvents.filter(function (activity) {
|
||||||
|
return candidateWorkspace(activity) !== scope.workspaceRoot;
|
||||||
|
});
|
||||||
|
updateCandidates();
|
||||||
|
return clearWorkspaceRaw(scope.workspaceRoot).then(persist).then(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showClearConfirmation() {
|
||||||
|
var scopeMessage = scope.mode === 'global'
|
||||||
|
? tr('ui.clearGlobalWarning', null, 'This permanently deletes all recorded activity and journal suggestions in every case.')
|
||||||
|
: tr('ui.clearWorkspaceWarning', { workspace: scope.label }, 'This permanently deletes recorded activity for ' + scope.label + '. Activity in other cases remains.');
|
||||||
|
var confirmBtn = el('button', {
|
||||||
|
className: 'activity-btn danger destructive',
|
||||||
|
type: 'button',
|
||||||
|
'data-activity-clear-confirm': '',
|
||||||
|
textContent: tr('ui.confirmClear', null, 'Clear activity'),
|
||||||
|
onClick: function () {
|
||||||
|
confirmBtn.disabled = true;
|
||||||
|
clearCurrentScope().then(closeClearConfirmation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
modalHost.innerHTML = '';
|
||||||
|
if (typeof modalHost.removeAttribute === 'function') modalHost.removeAttribute('hidden');
|
||||||
|
else delete modalHost.attributes.hidden;
|
||||||
|
modalHost.appendChild(el('div', {
|
||||||
|
className: 'activity-modal-overlay',
|
||||||
|
onClick: function (event) {
|
||||||
|
if (event.target === event.currentTarget) closeClearConfirmation();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
el('div', { className: 'activity-modal', role: 'dialog', 'aria-modal': 'true', 'data-activity-clear-confirmation': '' }, [
|
||||||
|
el('div', { className: 'activity-modal-title', textContent: tr('ui.clearConfirmTitle', null, 'Clear activity?') }),
|
||||||
|
el('div', { className: 'activity-modal-copy', textContent: scopeMessage }),
|
||||||
|
el('div', { className: 'activity-modal-actions' }, [
|
||||||
|
el('button', { className: 'activity-btn', type: 'button', 'data-activity-clear-cancel': '', textContent: tr('ui.cancel', null, 'Cancel'), onClick: closeClearConfirmation }),
|
||||||
|
confirmBtn
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
function clearWorkspaceRaw(workspaceRoot) {
|
function clearWorkspaceRaw(workspaceRoot) {
|
||||||
if (!api || !api.storage || !api.storage.data || typeof api.storage.data.readNDJSON !== 'function' || typeof api.storage.data.writeNDJSON !== 'function') {
|
if (!api || !api.storage || !api.storage.data || typeof api.storage.data.readNDJSON !== 'function' || typeof api.storage.data.writeNDJSON !== 'function') {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|
@ -754,8 +803,7 @@
|
||||||
statusText = tr('ui.dismissed', null, 'Candidate dismissed');
|
statusText = tr('ui.dismissed', null, 'Candidate dismissed');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not dismiss candidate: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.dismissError', 'Could not dismiss the suggestion. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
}).then(render);
|
}).then(render);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -827,8 +875,7 @@
|
||||||
updateCandidates();
|
updateCandidates();
|
||||||
return persistSessionRegistry().then(persistCandidateCaches);
|
return persistSessionRegistry().then(persistCandidateCaches);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not load activity: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.loadError', 'Could not load activity. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -855,8 +902,7 @@
|
||||||
return api.commands.register(WORKLOG_COMMAND_ID, listWorkSessionCandidates).then(function (unregister) {
|
return api.commands.register(WORKLOG_COMMAND_ID, listWorkSessionCandidates).then(function (unregister) {
|
||||||
if (typeof unregister === 'function') unsubscribers.push(unregister);
|
if (typeof unregister === 'function') unsubscribers.push(unregister);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Activity commands unavailable: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.commandsUnavailable', 'Activity actions are unavailable. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -874,8 +920,7 @@
|
||||||
statusText = scope.mode === 'global' ? 'Listening for all activity' : 'Listening for workspace 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));
|
reportError('ui.subscriptionsUnavailable', 'Activity updates are unavailable. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,18 @@
|
||||||
"ui.title": "Activity",
|
"ui.title": "Activity",
|
||||||
"ui.workspaceTitle": "Activity · {workspace}",
|
"ui.workspaceTitle": "Activity · {workspace}",
|
||||||
"ui.clear": "Clear",
|
"ui.clear": "Clear",
|
||||||
"ui.saveError": "Could not save activity: {error}",
|
"ui.clearConfirmTitle": "Clear activity?",
|
||||||
|
"ui.clearGlobalWarning": "This permanently deletes all recorded activity and journal suggestions in every case.",
|
||||||
|
"ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other cases remains.",
|
||||||
|
"ui.confirmClear": "Clear activity",
|
||||||
|
"ui.cancel": "Cancel",
|
||||||
|
"ui.saveError": "Could not save activity. Please try again.",
|
||||||
"ui.cleared": "Activity cleared",
|
"ui.cleared": "Activity cleared",
|
||||||
"ui.clearError": "Could not clear activity: {error}",
|
"ui.clearError": "Could not clear activity. Please try again.",
|
||||||
|
"ui.dismissError": "Could not dismiss the suggestion. Please try again.",
|
||||||
|
"ui.loadError": "Could not load activity. Please try again.",
|
||||||
|
"ui.commandsUnavailable": "Activity actions are unavailable. Please try again.",
|
||||||
|
"ui.subscriptionsUnavailable": "Activity updates are unavailable. Please try again.",
|
||||||
"ui.empty": "No activity events yet",
|
"ui.empty": "No activity events yet",
|
||||||
"ui.emptyHint": "File changes, browser captures, and conversions will appear here.",
|
"ui.emptyHint": "File changes, browser captures, and conversions will appear here.",
|
||||||
"ui.details": "Details",
|
"ui.details": "Details",
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,18 @@
|
||||||
"ui.title": "Активность",
|
"ui.title": "Активность",
|
||||||
"ui.workspaceTitle": "Активность · {workspace}",
|
"ui.workspaceTitle": "Активность · {workspace}",
|
||||||
"ui.clear": "Очистить",
|
"ui.clear": "Очистить",
|
||||||
"ui.saveError": "Не удалось сохранить активность: {error}",
|
"ui.clearConfirmTitle": "Очистить активность?",
|
||||||
|
"ui.clearGlobalWarning": "Будут безвозвратно удалены вся записанная активность и предложения для журнала во всех Делах.",
|
||||||
|
"ui.clearWorkspaceWarning": "Будет безвозвратно удалена записанная активность Дела «{workspace}». Активность других Дел останется.",
|
||||||
|
"ui.confirmClear": "Очистить активность",
|
||||||
|
"ui.cancel": "Отмена",
|
||||||
|
"ui.saveError": "Не удалось сохранить активность. Повторите попытку.",
|
||||||
"ui.cleared": "Активность очищена",
|
"ui.cleared": "Активность очищена",
|
||||||
"ui.clearError": "Не удалось очистить активность: {error}",
|
"ui.clearError": "Не удалось очистить активность. Повторите попытку.",
|
||||||
|
"ui.dismissError": "Не удалось отклонить предложение. Повторите попытку.",
|
||||||
|
"ui.loadError": "Не удалось загрузить активность. Повторите попытку.",
|
||||||
|
"ui.commandsUnavailable": "Действия с активностью недоступны. Повторите попытку.",
|
||||||
|
"ui.subscriptionsUnavailable": "Обновления активности недоступны. Повторите попытку.",
|
||||||
"ui.empty": "Событий активности пока нет",
|
"ui.empty": "Событий активности пока нет",
|
||||||
"ui.emptyHint": "Здесь появятся изменения файлов, материалы из браузера и преобразования.",
|
"ui.emptyHint": "Здесь появятся изменения файлов, материалы из браузера и преобразования.",
|
||||||
"ui.details": "Подробнее",
|
"ui.details": "Подробнее",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* ===========================================================
|
/* ===========================================================
|
||||||
Browser Inbox Plugin — Verstak v2 Frontend Bundle
|
Browser Plugin — Verstak v2 Frontend Bundle
|
||||||
Contract: window.VerstakPluginRegister(id, { components })
|
Contract: window.VerstakPluginRegister(id, { components })
|
||||||
=========================================================== */
|
=========================================================== */
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@
|
||||||
function scopeFromProps(props) {
|
function scopeFromProps(props) {
|
||||||
var workspaceRoot = workspaceFromProps(props);
|
var workspaceRoot = workspaceFromProps(props);
|
||||||
if (!workspaceRoot) {
|
if (!workspaceRoot) {
|
||||||
return { mode: 'global', key: GLOBAL_KEY, label: 'All workspaces', workspaceRoot: '' };
|
return { mode: 'global', key: GLOBAL_KEY, label: '', workspaceRoot: '' };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
mode: 'workspace',
|
mode: 'workspace',
|
||||||
|
|
@ -161,13 +161,13 @@
|
||||||
return value === 'selection' || value === 'link' || value === 'file' || value === 'page' ? value : 'page';
|
return value === 'selection' || value === 'link' || value === 'file' || value === 'page' ? value : 'page';
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayTitle(capture) {
|
function displayTitle(capture, fallbackTitle) {
|
||||||
if (capture && capture.kind === 'file' && capture.fileName) return capture.fileName;
|
if (capture && capture.kind === 'file' && capture.fileName) return capture.fileName;
|
||||||
return capture.title || capture.url || capture.captureId || 'Untitled capture';
|
return capture.title || capture.url || capture.captureId || fallbackTitle || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteTitle(capture) {
|
function noteTitle(capture, fallbackTitle) {
|
||||||
return text((capture && (capture.title || capture.domain || capture.captureId)) || 'Browser Capture').trim() || 'Browser Capture';
|
return text((capture && (capture.title || capture.domain || capture.captureId)) || fallbackTitle).trim() || text(fallbackTitle).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeNoteFilename(title) {
|
function safeNoteFilename(title) {
|
||||||
|
|
@ -199,8 +199,8 @@
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureToMarkdown(capture) {
|
function captureToMarkdown(capture, fallbackTitle) {
|
||||||
var title = noteTitle(capture);
|
var title = noteTitle(capture, fallbackTitle);
|
||||||
var lines = ['# ' + title, ''];
|
var lines = ['# ' + title, ''];
|
||||||
if (capture && capture.url) lines.push('Source: ' + capture.url);
|
if (capture && capture.url) lines.push('Source: ' + capture.url);
|
||||||
if (capture && capture.capturedAt) lines.push('Captured: ' + capture.capturedAt);
|
if (capture && capture.capturedAt) lines.push('Captured: ' + capture.capturedAt);
|
||||||
|
|
@ -363,7 +363,7 @@
|
||||||
var scope = scopeFromProps(props || {});
|
var scope = scopeFromProps(props || {});
|
||||||
var captures = [];
|
var captures = [];
|
||||||
var selectedId = '';
|
var selectedId = '';
|
||||||
var statusText = 'Connecting to receiver events...';
|
var statusText = '';
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
var unsubscribers = [];
|
var unsubscribers = [];
|
||||||
|
|
@ -376,17 +376,39 @@
|
||||||
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
||||||
return fallback || key;
|
return fallback || key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function localizedItemCount(count) {
|
||||||
|
var locale = api && api.i18n && typeof api.i18n.getLocale === 'function' ? api.i18n.getLocale() : 'en';
|
||||||
|
if (locale === 'ru') {
|
||||||
|
var mod10 = count % 10;
|
||||||
|
var mod100 = count % 100;
|
||||||
|
if (mod10 === 1 && mod100 !== 11) return tr('ui.items.one', { count: count }, count + ' материал');
|
||||||
|
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return tr('ui.items.few', { count: count }, count + ' материала');
|
||||||
|
return tr('ui.items.many', { count: count }, count + ' материалов');
|
||||||
|
}
|
||||||
|
return tr(count === 1 ? 'ui.items.one' : 'ui.items.other', { count: count }, count + (count === 1 ? ' item' : ' items'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportError(key, fallback, err) {
|
||||||
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
|
console.warn('[verstak.browser-inbox] ' + key, err);
|
||||||
|
}
|
||||||
|
statusText = tr(key, null, fallback);
|
||||||
|
statusClass = 'error';
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
statusText = tr('ui.connecting', null, 'Connecting to receiver events...');
|
statusText = tr('ui.connecting', null, 'Connecting to receiver events...');
|
||||||
|
|
||||||
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: scope.mode === 'global' ? tr('ui.title', null, 'Browser Inbox') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser Inbox · ' + scope.label) });
|
var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Browser') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser · ' + 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 filtersEl = el('div', { className: 'browser-inbox-filters' });
|
var filtersEl = el('div', { className: 'browser-inbox-filters' });
|
||||||
var statusFilterEl = el('select', {
|
var statusFilterEl = el('select', {
|
||||||
className: 'browser-inbox-select',
|
className: 'browser-inbox-select',
|
||||||
'data-browser-inbox-filter': 'status',
|
'data-browser-inbox-filter': 'status',
|
||||||
'aria-label': 'Capture status filter',
|
'aria-label': tr('ui.statusFilter', null, 'Material status filter'),
|
||||||
onChange: function (event) {
|
onChange: function (event) {
|
||||||
statusFilter = text(event && event.target && event.target.value) || 'all';
|
statusFilter = text(event && event.target && event.target.value) || 'all';
|
||||||
selectedId = '';
|
selectedId = '';
|
||||||
|
|
@ -402,7 +424,7 @@
|
||||||
var workspaceFilterEl = el('select', {
|
var workspaceFilterEl = el('select', {
|
||||||
className: 'browser-inbox-select',
|
className: 'browser-inbox-select',
|
||||||
'data-browser-inbox-filter': 'workspace',
|
'data-browser-inbox-filter': 'workspace',
|
||||||
'aria-label': 'Workspace filter',
|
'aria-label': tr('ui.workspaceFilter', null, 'Deal filter'),
|
||||||
onChange: function (event) {
|
onChange: function (event) {
|
||||||
workspaceFilter = cleanWorkspace(event && event.target && event.target.value);
|
workspaceFilter = cleanWorkspace(event && event.target && event.target.value);
|
||||||
selectedId = '';
|
selectedId = '';
|
||||||
|
|
@ -414,7 +436,7 @@
|
||||||
type: 'search',
|
type: 'search',
|
||||||
placeholder: tr('ui.search', null, 'Search captures'),
|
placeholder: tr('ui.search', null, 'Search captures'),
|
||||||
'data-browser-inbox-filter': 'search',
|
'data-browser-inbox-filter': 'search',
|
||||||
'aria-label': 'Search captures',
|
'aria-label': tr('ui.search', null, 'Search captures'),
|
||||||
onInput: function (event) {
|
onInput: function (event) {
|
||||||
searchQuery = text(event && event.target && event.target.value).trim().toLowerCase();
|
searchQuery = text(event && event.target && event.target.value).trim().toLowerCase();
|
||||||
selectedId = '';
|
selectedId = '';
|
||||||
|
|
@ -472,7 +494,7 @@
|
||||||
function renderWorkspaceFilterOptions() {
|
function renderWorkspaceFilterOptions() {
|
||||||
if (scope.mode !== 'global') return;
|
if (scope.mode !== 'global') return;
|
||||||
workspaceFilterEl.innerHTML = '';
|
workspaceFilterEl.innerHTML = '';
|
||||||
workspaceFilterEl.appendChild(option('', 'All workspaces'));
|
workspaceFilterEl.appendChild(option('', tr('ui.allDeals', null, 'All Deals')));
|
||||||
workspaceRoots().forEach(function (root) {
|
workspaceRoots().forEach(function (root) {
|
||||||
workspaceFilterEl.appendChild(option(root, root));
|
workspaceFilterEl.appendChild(option(root, root));
|
||||||
});
|
});
|
||||||
|
|
@ -496,9 +518,7 @@
|
||||||
|
|
||||||
function publishMutation(action, payload, verify, verifySettings) {
|
function publishMutation(action, payload, verify, verifySettings) {
|
||||||
if (!api || !api.events || typeof api.events.publish !== 'function') {
|
if (!api || !api.events || typeof api.events.publish !== 'function') {
|
||||||
statusText = 'Could not save inbox: events API unavailable';
|
reportError('ui.saveError', 'Could not update browser materials. Please try again.');
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
return api.events.publish(MUTATION_EVENT, Object.assign({ action: action }, payload || {})).then(function () {
|
return api.events.publish(MUTATION_EVENT, Object.assign({ action: action }, payload || {})).then(function () {
|
||||||
|
|
@ -516,9 +536,7 @@
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.saveError', 'Could not update browser materials. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -545,7 +563,9 @@
|
||||||
var ids = scope.mode === 'global'
|
var ids = scope.mode === 'global'
|
||||||
? captures.map(function (capture) { return capture.captureId; })
|
? captures.map(function (capture) { return capture.captureId; })
|
||||||
: captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).map(function (capture) { return capture.captureId; });
|
: captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).map(function (capture) { return capture.captureId; });
|
||||||
return archiveCaptures(ids, scope.mode === 'global' ? 'Inbox archived' : 'Workspace captures archived');
|
return archiveCaptures(ids, scope.mode === 'global'
|
||||||
|
? tr('ui.inboxArchived', null, 'Inbox archived')
|
||||||
|
: tr('ui.workspaceCapturesArchived', null, 'Deal materials archived'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedCapture() {
|
function selectedCapture() {
|
||||||
|
|
@ -563,10 +583,8 @@
|
||||||
});
|
});
|
||||||
if (existing) return Promise.resolve();
|
if (existing) return Promise.resolve();
|
||||||
selectedId = capture.captureId;
|
selectedId = capture.captureId;
|
||||||
statusText = 'Capture received';
|
statusText = tr('ui.captureReceived', null, 'Material received');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
statusText = 'Could not load received capture from storage';
|
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
render();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
@ -592,14 +610,16 @@
|
||||||
}).then(function (saved) {
|
}).then(function (saved) {
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
if (workspaceRoot && workspaceOptions.indexOf(workspaceRoot) === -1) workspaceOptions.push(workspaceRoot);
|
if (workspaceRoot && workspaceOptions.indexOf(workspaceRoot) === -1) workspaceOptions.push(workspaceRoot);
|
||||||
statusText = workspaceRoot ? 'Capture assigned to ' + workspaceRoot : 'Capture is unassigned';
|
statusText = workspaceRoot
|
||||||
|
? tr('ui.assignedToWorkspace', { workspace: workspaceRoot }, 'Material assigned to ' + workspaceRoot)
|
||||||
|
: tr('ui.captureUnassigned', null, 'Material is unassigned');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function archiveCapture(captureId) {
|
function archiveCapture(captureId) {
|
||||||
return archiveCaptures([captureId], 'Capture archived');
|
return archiveCaptures([captureId], tr('ui.captureArchived', null, 'Material archived'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreCapture(captureId) {
|
function restoreCapture(captureId) {
|
||||||
|
|
@ -609,7 +629,7 @@
|
||||||
});
|
});
|
||||||
}).then(function (saved) {
|
}).then(function (saved) {
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
statusText = 'Capture restored to Inbox';
|
statusText = tr('ui.captureRestored', null, 'Material restored to Inbox');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
@ -621,7 +641,7 @@
|
||||||
}).then(function (saved) {
|
}).then(function (saved) {
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
if (selectedId === captureId) selectedId = '';
|
if (selectedId === captureId) selectedId = '';
|
||||||
statusText = 'Capture permanently deleted';
|
statusText = tr('ui.captureDeleted', null, 'Material permanently deleted');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
@ -637,7 +657,9 @@
|
||||||
});
|
});
|
||||||
}).then(function (saved) {
|
}).then(function (saved) {
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
statusText = processed ? 'Capture marked processed' : 'Capture marked unprocessed';
|
statusText = processed
|
||||||
|
? tr('ui.captureProcessed', null, 'Material marked processed')
|
||||||
|
: tr('ui.captureUnprocessed', null, 'Material marked unprocessed');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
@ -646,17 +668,15 @@
|
||||||
function createNoteFromCapture(capture) {
|
function createNoteFromCapture(capture) {
|
||||||
if (!capture || !capture.workspaceRootPath) return Promise.resolve();
|
if (!capture || !capture.workspaceRootPath) return Promise.resolve();
|
||||||
if (!api || !api.files || typeof api.files.writeText !== 'function') {
|
if (!api || !api.files || typeof api.files.writeText !== 'function') {
|
||||||
statusText = 'Could not create note: files API unavailable';
|
reportError('ui.createNoteError', 'Could not create the note. Please try again.');
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
var title = noteTitle(capture);
|
var title = noteTitle(capture, tr('ui.untitledCapture', null, 'Untitled material'));
|
||||||
var notePath = capture.workspaceRootPath + '/Notes/' + safeNoteFilename(title);
|
var notePath = capture.workspaceRootPath + '/Notes/' + safeNoteFilename(title);
|
||||||
statusText = 'Creating note...';
|
statusText = tr('ui.creatingNote', null, 'Creating note...');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
return api.files.writeText(notePath, captureToMarkdown(capture), {
|
return api.files.writeText(notePath, captureToMarkdown(capture, tr('ui.untitledCapture', null, 'Untitled material')), {
|
||||||
createIfMissing: true,
|
createIfMissing: true,
|
||||||
overwrite: false
|
overwrite: false
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
|
|
@ -673,26 +693,22 @@
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
statusText = 'Created note: ' + notePath;
|
statusText = tr('ui.noteCreated', { path: notePath }, 'Note created: ' + notePath);
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
return archiveCapture(capture.captureId);
|
return archiveCapture(capture.captureId);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not create note: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.createNoteError', 'Could not create the note. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLinkFromCapture(capture) {
|
function createLinkFromCapture(capture) {
|
||||||
if (!capture || !capture.workspaceRootPath || !capture.url) return Promise.resolve();
|
if (!capture || !capture.workspaceRootPath || !capture.url) return Promise.resolve();
|
||||||
if (!api || !api.files || typeof api.files.writeText !== 'function') {
|
if (!api || !api.files || typeof api.files.writeText !== 'function') {
|
||||||
statusText = 'Could not create link: files API unavailable';
|
reportError('ui.createLinkError', 'Could not create the link. Please try again.');
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
var title = noteTitle(capture);
|
var title = noteTitle(capture, tr('ui.untitledCapture', null, 'Untitled material'));
|
||||||
statusText = 'Creating link...';
|
statusText = tr('ui.creatingLink', null, 'Creating link...');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
function writeLink(number) {
|
function writeLink(number) {
|
||||||
|
|
@ -725,36 +741,30 @@
|
||||||
}
|
}
|
||||||
return linkPath;
|
return linkPath;
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
statusText = 'Created link';
|
statusText = tr('ui.linkCreated', null, 'Link created');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
return archiveCapture(capture.captureId);
|
return archiveCapture(capture.captureId);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not create link: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.createLinkError', 'Could not create the link. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCaptureURL(capture) {
|
function openCaptureURL(capture) {
|
||||||
if (!capture || !capture.url || !api || !api.files || typeof api.files.openURL !== 'function') return Promise.resolve();
|
if (!capture || !capture.url || !api || !api.files || typeof api.files.openURL !== 'function') return Promise.resolve();
|
||||||
return api.files.openURL(capture.url).catch(function (err) {
|
return api.files.openURL(capture.url).catch(function (err) {
|
||||||
statusText = 'Could not open link: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.openLinkError', 'Could not open the link. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFileFromCapture(capture) {
|
function createFileFromCapture(capture) {
|
||||||
if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || (!capture.fileText && !capture.fileDataBase64)) return Promise.resolve();
|
if (!capture || !capture.workspaceRootPath || capture.kind !== 'file' || !capture.fileName || (!capture.fileText && !capture.fileDataBase64)) return Promise.resolve();
|
||||||
if (!api || !api.files || (capture.fileDataBase64 ? typeof api.files.writeBytes !== 'function' : typeof api.files.writeText !== 'function')) {
|
if (!api || !api.files || (capture.fileDataBase64 ? typeof api.files.writeBytes !== 'function' : typeof api.files.writeText !== 'function')) {
|
||||||
statusText = 'Could not create file: files API unavailable';
|
reportError('ui.createFileError', 'Could not create the file. Please try again.');
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
var fileName = safeFileFilename(capture.fileName);
|
var fileName = safeFileFilename(capture.fileName);
|
||||||
var filePath = capture.workspaceRootPath + '/Files/' + fileName;
|
var filePath = capture.workspaceRootPath + '/Files/' + fileName;
|
||||||
statusText = 'Creating file...';
|
statusText = tr('ui.creatingFile', null, 'Creating file...');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
var writeOptions = {
|
var writeOptions = {
|
||||||
|
|
@ -781,13 +791,11 @@
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
statusText = 'Created file: ' + filePath;
|
statusText = tr('ui.fileCreated', { path: filePath }, 'File created: ' + filePath);
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
return archiveCapture(capture.captureId);
|
return archiveCapture(capture.captureId);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not create file: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.createFileError', 'Could not create the file. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
render();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -796,8 +804,8 @@
|
||||||
var visible = visibleCaptures();
|
var visible = visibleCaptures();
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
var emptyText = captures.length === 0
|
var emptyText = captures.length === 0
|
||||||
? 'No browser captures yet. Keep this view open, then send a page, selection, or link from the extension.'
|
? tr('ui.empty', null, 'No browser materials yet. Send a page, selection, or link from the extension.')
|
||||||
: 'No captures match the current filters.';
|
: tr('ui.emptyFiltered', null, 'No materials match the current filters.');
|
||||||
listEl.appendChild(el('div', { className: 'browser-inbox-empty', textContent: emptyText }));
|
listEl.appendChild(el('div', { className: 'browser-inbox-empty', textContent: emptyText }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -813,20 +821,22 @@
|
||||||
}, [
|
}, [
|
||||||
el('div', { className: 'browser-inbox-row-head' }, [
|
el('div', { className: 'browser-inbox-row-head' }, [
|
||||||
el('span', { className: 'browser-inbox-kind', textContent: capture.kind }),
|
el('span', { className: 'browser-inbox-kind', textContent: capture.kind }),
|
||||||
el('span', { className: 'browser-inbox-row-title', textContent: displayTitle(capture) })
|
el('span', { className: 'browser-inbox-row-title', textContent: displayTitle(capture, tr('ui.untitledCapture', null, 'Untitled material')) })
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'browser-inbox-row-url', textContent: capture.url || capture.domain || capture.captureId })
|
el('div', { className: 'browser-inbox-row-url', textContent: capture.url || capture.domain || capture.captureId })
|
||||||
]);
|
]);
|
||||||
row.appendChild(el('div', { className: 'browser-inbox-row-meta' }, [
|
row.appendChild(el('div', { className: 'browser-inbox-row-meta' }, [
|
||||||
el('span', {
|
el('span', {
|
||||||
className: 'browser-inbox-badge' + (workspaceRoot ? '' : ' unassigned'),
|
className: 'browser-inbox-badge' + (workspaceRoot ? '' : ' unassigned'),
|
||||||
textContent: workspaceRoot || 'Unassigned'
|
textContent: workspaceRoot || tr('ui.unassigned', null, 'Unassigned')
|
||||||
}),
|
}),
|
||||||
el('span', {
|
el('span', {
|
||||||
className: 'browser-inbox-badge' + (capture.processed ? ' processed' : ''),
|
className: 'browser-inbox-badge' + (capture.processed ? ' processed' : ''),
|
||||||
textContent: capture.processed ? 'Processed' : 'Unprocessed'
|
textContent: capture.processed
|
||||||
|
? tr('ui.processed', null, 'Processed')
|
||||||
|
: tr('ui.unprocessed', null, 'Unprocessed')
|
||||||
}),
|
}),
|
||||||
capture.globalState === 'archived' ? el('span', { className: 'browser-inbox-badge', textContent: 'Archived' }) : null
|
capture.globalState === 'archived' ? el('span', { className: 'browser-inbox-badge', textContent: tr('ui.archive', null, 'Archive') }) : null
|
||||||
]));
|
]));
|
||||||
if (capture.text) {
|
if (capture.text) {
|
||||||
row.appendChild(el('div', { className: 'browser-inbox-row-text', textContent: capture.text }));
|
row.appendChild(el('div', { className: 'browser-inbox-row-text', textContent: capture.text }));
|
||||||
|
|
@ -843,45 +853,47 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedId = capture.captureId;
|
selectedId = capture.captureId;
|
||||||
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: displayTitle(capture) }));
|
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: displayTitle(capture, tr('ui.untitledCapture', null, 'Untitled material')) }));
|
||||||
detailEl.appendChild(el('div', { className: 'browser-inbox-meta' }, [
|
detailEl.appendChild(el('div', { className: 'browser-inbox-meta' }, [
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.kind', null, 'Kind') }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.kind', null, 'Kind') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.kind }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.kind }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'URL' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.url', null, 'URL') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.url || '-' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.url || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.domain', null, 'Domain') }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.domain', null, 'Domain') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.domain || '-' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.domain || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.captured', null, 'Captured') }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.captured', null, 'Captured') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: formatDate(capture.capturedAt) || '-' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: formatDate(capture.capturedAt) || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Browser' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.browser', null, 'Browser') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.browserName || capture.source || '-' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.browserName || capture.source || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.workspace', null, 'Workspace') }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.workspace', null, 'Workspace') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || 'Unassigned' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || tr('ui.unassigned', null, 'Unassigned') }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.status', null, 'Status') }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.status', null, 'Status') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed ? 'Processed' : 'Unprocessed' })
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed
|
||||||
|
? tr('ui.processed', null, 'Processed')
|
||||||
|
: tr('ui.unprocessed', null, 'Unprocessed') })
|
||||||
]));
|
]));
|
||||||
var assignmentSelect = el('select', {
|
var assignmentSelect = el('select', {
|
||||||
className: 'browser-inbox-select',
|
className: 'browser-inbox-select',
|
||||||
'data-browser-inbox-assignment': capture.captureId,
|
'data-browser-inbox-assignment': capture.captureId,
|
||||||
'aria-label': 'Assign capture workspace',
|
'aria-label': tr('ui.assignment', null, 'Assign to Deal'),
|
||||||
onChange: function (event) {
|
onChange: function (event) {
|
||||||
assignWorkspace(capture.captureId, event && event.target && event.target.value);
|
assignWorkspace(capture.captureId, event && event.target && event.target.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
assignmentSelect.appendChild(option('', 'Unassigned'));
|
assignmentSelect.appendChild(option('', tr('ui.unassigned', null, 'Unassigned')));
|
||||||
workspaceRoots().forEach(function (workspaceRoot) {
|
workspaceRoots().forEach(function (workspaceRoot) {
|
||||||
assignmentSelect.appendChild(option(workspaceRoot, workspaceRoot));
|
assignmentSelect.appendChild(option(workspaceRoot, workspaceRoot));
|
||||||
});
|
});
|
||||||
assignmentSelect.value = capture.workspaceRootPath || '';
|
assignmentSelect.value = capture.workspaceRootPath || '';
|
||||||
var assignmentControls = [
|
var assignmentControls = [
|
||||||
el('span', { className: 'browser-inbox-meta-label', textContent: 'Assign workspace' }),
|
el('span', { className: 'browser-inbox-meta-label', textContent: tr('ui.assignment', null, 'Assign to Deal') }),
|
||||||
assignmentSelect
|
assignmentSelect
|
||||||
];
|
];
|
||||||
if (capture.workspaceRootPath) {
|
if (capture.workspaceRootPath) {
|
||||||
assignmentControls.push(el('button', {
|
assignmentControls.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'clear-assignment',
|
'data-browser-inbox-action': 'clear-assignment',
|
||||||
textContent: 'Clear assignment',
|
textContent: tr('ui.clearAssignment', null, 'Clear assignment'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
assignWorkspace(capture.captureId, '');
|
assignWorkspace(capture.captureId, '');
|
||||||
}
|
}
|
||||||
|
|
@ -889,7 +901,7 @@
|
||||||
}
|
}
|
||||||
detailEl.appendChild(el('div', { className: 'browser-inbox-assignment' }, assignmentControls));
|
detailEl.appendChild(el('div', { className: 'browser-inbox-assignment' }, assignmentControls));
|
||||||
if (!capture.workspaceRootPath) {
|
if (!capture.workspaceRootPath) {
|
||||||
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-note', textContent: 'Assign a workspace before creating a note, link, or file.' }));
|
detailEl.appendChild(el('div', { className: 'browser-inbox-detail-note', textContent: tr('ui.assignBeforeCreation', null, 'Assign a Deal before creating a note, link, or file.') }));
|
||||||
}
|
}
|
||||||
if (capture.text) {
|
if (capture.text) {
|
||||||
detailEl.appendChild(el('div', { className: 'browser-inbox-text', textContent: capture.text }));
|
detailEl.appendChild(el('div', { className: 'browser-inbox-text', textContent: capture.text }));
|
||||||
|
|
@ -901,7 +913,9 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'toggle-processed',
|
'data-browser-inbox-action': 'toggle-processed',
|
||||||
textContent: capture.processed ? 'Mark Unprocessed' : 'Mark Processed',
|
textContent: capture.processed
|
||||||
|
? tr('ui.markUnprocessed', null, 'Mark unprocessed')
|
||||||
|
: tr('ui.markProcessed', null, 'Mark processed'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
setProcessed(capture.captureId, !capture.processed);
|
setProcessed(capture.captureId, !capture.processed);
|
||||||
}
|
}
|
||||||
|
|
@ -910,7 +924,7 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'open-link',
|
'data-browser-inbox-action': 'open-link',
|
||||||
textContent: 'Open link',
|
textContent: tr('ui.openLink', null, 'Open link'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
openCaptureURL(capture);
|
openCaptureURL(capture);
|
||||||
}
|
}
|
||||||
|
|
@ -950,7 +964,7 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'restore',
|
'data-browser-inbox-action': 'restore',
|
||||||
textContent: 'Restore to Inbox',
|
textContent: tr('ui.restore', null, 'Restore to Inbox'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
restoreCapture(capture.captureId);
|
restoreCapture(capture.captureId);
|
||||||
}
|
}
|
||||||
|
|
@ -959,7 +973,7 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn',
|
className: 'browser-inbox-btn',
|
||||||
'data-browser-inbox-action': 'archive',
|
'data-browser-inbox-action': 'archive',
|
||||||
textContent: 'Archive',
|
textContent: tr('ui.archive', null, 'Archive'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
archiveCapture(capture.captureId);
|
archiveCapture(capture.captureId);
|
||||||
}
|
}
|
||||||
|
|
@ -968,7 +982,7 @@
|
||||||
actionButtons.push(el('button', {
|
actionButtons.push(el('button', {
|
||||||
className: 'browser-inbox-btn danger',
|
className: 'browser-inbox-btn danger',
|
||||||
'data-browser-inbox-action': 'delete-permanently',
|
'data-browser-inbox-action': 'delete-permanently',
|
||||||
textContent: 'Delete permanently',
|
textContent: tr('ui.deletePermanently', null, 'Delete permanently'),
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
permanentlyDeleteCapture(capture.captureId);
|
permanentlyDeleteCapture(capture.captureId);
|
||||||
}
|
}
|
||||||
|
|
@ -980,8 +994,11 @@
|
||||||
var visibleCount = visibleCaptures().length;
|
var visibleCount = visibleCaptures().length;
|
||||||
var total = captures.length;
|
var total = captures.length;
|
||||||
countEl.textContent = visibleCount === total
|
countEl.textContent = visibleCount === total
|
||||||
? total + ' item' + (total === 1 ? '' : 's')
|
? localizedItemCount(total)
|
||||||
: visibleCount + ' of ' + total + ' items';
|
: tr('ui.items.filtered', {
|
||||||
|
visible: localizedItemCount(visibleCount),
|
||||||
|
total: localizedItemCount(total)
|
||||||
|
}, localizedItemCount(visibleCount) + ' of ' + localizedItemCount(total));
|
||||||
var scopeCount = scope.mode === 'global'
|
var scopeCount = scope.mode === 'global'
|
||||||
? total
|
? total
|
||||||
: captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).length;
|
: captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).length;
|
||||||
|
|
@ -1034,8 +1051,7 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Could not load inbox: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.loadError', 'Could not load browser materials. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1075,13 +1091,13 @@
|
||||||
}).then(function (saved) {
|
}).then(function (saved) {
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
selectedId = received.captureId;
|
selectedId = received.captureId;
|
||||||
statusText = 'Capture received';
|
statusText = tr('ui.captureReceived', null, 'Material received');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
selectedId = received.captureId;
|
selectedId = received.captureId;
|
||||||
statusText = 'Capture received';
|
statusText = tr('ui.captureReceived', null, 'Material received');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
render();
|
render();
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -1090,11 +1106,12 @@
|
||||||
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
||||||
});
|
});
|
||||||
})).then(function () {
|
})).then(function () {
|
||||||
statusText = scope.mode === 'global' ? 'Receiver ready for all workspaces' : 'Receiver ready for workspace';
|
statusText = scope.mode === 'global'
|
||||||
|
? tr('ui.receiverReadyAll', null, 'Receiver ready for all Deals')
|
||||||
|
: tr('ui.receiverReadyWorkspace', null, 'Receiver ready for this Deal');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = 'Receiver unavailable: ' + (err && err.message ? err.message : String(err));
|
reportError('ui.receiverError', 'The browser receiver is unavailable. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1108,8 +1125,11 @@
|
||||||
});
|
});
|
||||||
if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
||||||
api.i18n.onDidChangeLocale(function () {
|
api.i18n.onDidChangeLocale(function () {
|
||||||
titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Browser Inbox') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser Inbox · ' + scope.label);
|
titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Browser') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser · ' + scope.label);
|
||||||
searchInput.setAttribute('placeholder', tr('ui.search', null, 'Search captures'));
|
searchInput.setAttribute('placeholder', tr('ui.search', null, 'Search captures'));
|
||||||
|
statusFilterEl.setAttribute('aria-label', tr('ui.statusFilter', null, 'Material status filter'));
|
||||||
|
workspaceFilterEl.setAttribute('aria-label', tr('ui.workspaceFilter', null, 'Deal filter'));
|
||||||
|
searchInput.setAttribute('aria-label', tr('ui.search', null, 'Search captures'));
|
||||||
clearBtn.textContent = tr('ui.clear', null, 'Clear');
|
clearBtn.textContent = tr('ui.clear', null, 'Clear');
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
|
|
@ -1201,6 +1221,13 @@
|
||||||
statusEl.className = 'browser-inbox-settings-status' + (isError ? ' error' : '');
|
statusEl.className = 'browser-inbox-settings-status' + (isError ? ' error' : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reportError(key, fallback, err) {
|
||||||
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
|
console.warn('[verstak.browser-inbox.settings] ' + key, err);
|
||||||
|
}
|
||||||
|
setStatus(tr(key, null, fallback), true);
|
||||||
|
}
|
||||||
|
|
||||||
function setBusy(busy) {
|
function setBusy(busy) {
|
||||||
copyURLButton.disabled = busy;
|
copyURLButton.disabled = busy;
|
||||||
copyTokenButton.disabled = busy;
|
copyTokenButton.disabled = busy;
|
||||||
|
|
@ -1226,7 +1253,7 @@
|
||||||
applyPairing(pairing);
|
applyPairing(pairing);
|
||||||
setStatus('', false);
|
setStatus('', false);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus(text(err && err.message ? err.message : err), true);
|
reportError('ui.pairingLoadError', 'Could not load browser connection settings. Please try again.', err);
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
});
|
});
|
||||||
|
|
@ -1241,7 +1268,7 @@
|
||||||
navigator.clipboard.writeText(value).then(function () {
|
navigator.clipboard.writeText(value).then(function () {
|
||||||
setStatus(tr('ui.copied', { label: label }, '{label} copied'), false);
|
setStatus(tr('ui.copied', { label: label }, '{label} copied'), false);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus(text(err && err.message ? err.message : err), true);
|
reportError('ui.clipboardError', 'Could not copy to the clipboard. Please try again.', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1261,7 +1288,7 @@
|
||||||
applyPairing(pairing);
|
applyPairing(pairing);
|
||||||
setStatus(tr('ui.tokenRotated', null, 'Token rotated'), false);
|
setStatus(tr('ui.tokenRotated', null, 'Token rotated'), false);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus(text(err && err.message ? err.message : err), true);
|
reportError('ui.tokenRotateError', 'Could not rotate the pairing token. Please try again.', err);
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
{
|
{
|
||||||
"manifest.name": "Browser Inbox",
|
"manifest.name": "Browser",
|
||||||
"manifest.description": "Global browser capture queue with explicit workspace assignment delivered through the local receiver event protocol.",
|
"manifest.description": "Global browser materials with explicit Deal assignment delivered through the local receiver event protocol.",
|
||||||
"contributions.views.verstak.browser-inbox.view.title": "Browser Inbox",
|
"contributions.views.verstak.browser-inbox.view.title": "Browser",
|
||||||
"contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Browser Inbox",
|
"contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Browser",
|
||||||
"contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Browser Inbox",
|
"contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Browser",
|
||||||
"contributions.settingsPanels.verstak.browser-inbox.settings.title": "Browser Inbox",
|
"contributions.settingsPanels.verstak.browser-inbox.settings.title": "Browser",
|
||||||
"ui.connecting": "Connecting to receiver events...",
|
"ui.connecting": "Connecting to receiver events...",
|
||||||
"ui.title": "Browser Inbox",
|
"ui.title": "Browser",
|
||||||
"ui.workspaceTitle": "Browser Inbox · {workspace}",
|
"ui.workspaceTitle": "Browser · {workspace}",
|
||||||
"ui.allCaptures": "All captures",
|
"ui.allCaptures": "All captures",
|
||||||
|
"ui.allDeals": "All Deals",
|
||||||
"ui.unassigned": "Unassigned",
|
"ui.unassigned": "Unassigned",
|
||||||
"ui.unprocessed": "Unprocessed",
|
"ui.unprocessed": "Unprocessed",
|
||||||
"ui.processed": "Processed",
|
"ui.processed": "Processed",
|
||||||
"ui.search": "Search captures",
|
"ui.search": "Search captures",
|
||||||
"ui.clear": "Clear",
|
"ui.clear": "Clear",
|
||||||
"ui.assignedHere": "Assigned to this workspace",
|
"ui.assignedHere": "Assigned to this Deal",
|
||||||
"ui.selectCapture": "Select a capture to inspect it.",
|
"ui.selectCapture": "Select a capture to inspect it.",
|
||||||
"ui.kind": "Kind",
|
"ui.kind": "Kind",
|
||||||
"ui.domain": "Domain",
|
"ui.domain": "Domain",
|
||||||
"ui.captured": "Captured",
|
"ui.captured": "Captured",
|
||||||
"ui.workspace": "Workspace",
|
"ui.workspace": "Deal",
|
||||||
"ui.status": "Status",
|
"ui.status": "Status",
|
||||||
"ui.createNote": "Create Note",
|
"ui.createNote": "Create Note",
|
||||||
"ui.createLink": "Create Link",
|
"ui.createLink": "Create Link",
|
||||||
|
|
@ -35,5 +36,55 @@
|
||||||
"ui.copied": "{label} copied",
|
"ui.copied": "{label} copied",
|
||||||
"ui.rotateConfirm": "Rotate pairing token?",
|
"ui.rotateConfirm": "Rotate pairing token?",
|
||||||
"ui.rotating": "Rotating...",
|
"ui.rotating": "Rotating...",
|
||||||
"ui.tokenRotated": "Token rotated"
|
"ui.tokenRotated": "Token rotated",
|
||||||
|
"ui.saveError": "Could not update browser materials. Please try again.",
|
||||||
|
"ui.createNoteError": "Could not create the note. Please try again.",
|
||||||
|
"ui.createLinkError": "Could not create the link. Please try again.",
|
||||||
|
"ui.openLinkError": "Could not open the link. Please try again.",
|
||||||
|
"ui.createFileError": "Could not create the file. Please try again.",
|
||||||
|
"ui.loadError": "Could not load browser materials. Please try again.",
|
||||||
|
"ui.receiverError": "The browser receiver is unavailable. Please try again.",
|
||||||
|
"ui.pairingLoadError": "Could not load browser connection settings. Please try again.",
|
||||||
|
"ui.clipboardError": "Could not copy to the clipboard. Please try again.",
|
||||||
|
"ui.tokenRotateError": "Could not rotate the pairing token. Please try again.",
|
||||||
|
"ui.statusFilter": "Material status filter",
|
||||||
|
"ui.workspaceFilter": "Deal filter",
|
||||||
|
"ui.assignment": "Assign to Deal",
|
||||||
|
"ui.clearAssignment": "Clear assignment",
|
||||||
|
"ui.empty": "No browser materials yet. Send a page, selection, or link from the extension.",
|
||||||
|
"ui.emptyFiltered": "No materials match the current filters.",
|
||||||
|
"ui.untitledCapture": "Untitled material",
|
||||||
|
"ui.url": "URL",
|
||||||
|
"ui.browser": "Browser",
|
||||||
|
"ui.assignBeforeCreation": "Assign a Deal before creating a note, link, or file.",
|
||||||
|
"ui.markProcessed": "Mark processed",
|
||||||
|
"ui.markUnprocessed": "Mark unprocessed",
|
||||||
|
"ui.openLink": "Open link",
|
||||||
|
"ui.restore": "Restore to Inbox",
|
||||||
|
"ui.archive": "Archive",
|
||||||
|
"ui.deletePermanently": "Delete permanently",
|
||||||
|
"ui.captureReceived": "Material received",
|
||||||
|
"ui.captureLoadError": "Could not load the received material.",
|
||||||
|
"ui.inboxArchived": "Inbox archived",
|
||||||
|
"ui.workspaceCapturesArchived": "Deal materials archived",
|
||||||
|
"ui.assignedToWorkspace": "Material assigned to {workspace}",
|
||||||
|
"ui.captureUnassigned": "Material is unassigned",
|
||||||
|
"ui.captureArchived": "Material archived",
|
||||||
|
"ui.captureRestored": "Material restored to Inbox",
|
||||||
|
"ui.captureDeleted": "Material permanently deleted",
|
||||||
|
"ui.captureProcessed": "Material marked processed",
|
||||||
|
"ui.captureUnprocessed": "Material marked unprocessed",
|
||||||
|
"ui.creatingNote": "Creating note...",
|
||||||
|
"ui.noteCreated": "Note created: {path}",
|
||||||
|
"ui.creatingLink": "Creating link...",
|
||||||
|
"ui.linkCreated": "Link created",
|
||||||
|
"ui.creatingFile": "Creating file...",
|
||||||
|
"ui.fileCreated": "File created: {path}",
|
||||||
|
"ui.receiverReadyAll": "Receiver ready for all Deals",
|
||||||
|
"ui.receiverReadyWorkspace": "Receiver ready for this Deal",
|
||||||
|
"ui.items.one": "{count} item",
|
||||||
|
"ui.items.few": "{count} items",
|
||||||
|
"ui.items.many": "{count} items",
|
||||||
|
"ui.items.other": "{count} items",
|
||||||
|
"ui.items.filtered": "{visible} of {total}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
{
|
{
|
||||||
"manifest.name": "Входящие из браузера",
|
"manifest.name": "Браузер",
|
||||||
"manifest.description": "Общая очередь материалов из браузера с явным назначением рабочего пространства через локальный протокол приёма.",
|
"manifest.description": "Общие материалы из браузера с явным назначением Дела через локальный протокол приёма.",
|
||||||
"contributions.views.verstak.browser-inbox.view.title": "Входящие из браузера",
|
"contributions.views.verstak.browser-inbox.view.title": "Браузер",
|
||||||
"contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Входящие из браузера",
|
"contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Браузер",
|
||||||
"contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Входящие из браузера",
|
"contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Браузер",
|
||||||
"contributions.settingsPanels.verstak.browser-inbox.settings.title": "Входящие из браузера",
|
"contributions.settingsPanels.verstak.browser-inbox.settings.title": "Браузер",
|
||||||
"ui.connecting": "Подключение к событиям приёмника...",
|
"ui.connecting": "Подключение к событиям приёмника...",
|
||||||
"ui.title": "Входящие из браузера",
|
"ui.title": "Браузер",
|
||||||
"ui.workspaceTitle": "Входящие из браузера · {workspace}",
|
"ui.workspaceTitle": "Браузер · {workspace}",
|
||||||
"ui.allCaptures": "Все материалы",
|
"ui.allCaptures": "Все материалы",
|
||||||
|
"ui.allDeals": "Все Дела",
|
||||||
"ui.unassigned": "Не назначено",
|
"ui.unassigned": "Не назначено",
|
||||||
"ui.unprocessed": "Не обработано",
|
"ui.unprocessed": "Не обработано",
|
||||||
"ui.processed": "Обработано",
|
"ui.processed": "Обработано",
|
||||||
"ui.search": "Поиск материалов",
|
"ui.search": "Поиск материалов",
|
||||||
"ui.clear": "Очистить",
|
"ui.clear": "Очистить",
|
||||||
"ui.assignedHere": "Назначено этому рабочему пространству",
|
"ui.assignedHere": "Назначено этому Делу",
|
||||||
"ui.selectCapture": "Выберите материал для просмотра.",
|
"ui.selectCapture": "Выберите материал для просмотра.",
|
||||||
"ui.kind": "Тип",
|
"ui.kind": "Тип",
|
||||||
"ui.domain": "Домен",
|
"ui.domain": "Домен",
|
||||||
"ui.captured": "Получено",
|
"ui.captured": "Получено",
|
||||||
"ui.workspace": "Рабочее пространство",
|
"ui.workspace": "Дело",
|
||||||
"ui.status": "Состояние",
|
"ui.status": "Состояние",
|
||||||
"ui.createNote": "Создать заметку",
|
"ui.createNote": "Создать заметку",
|
||||||
"ui.createLink": "Создать ссылку",
|
"ui.createLink": "Создать ссылку",
|
||||||
|
|
@ -35,5 +36,55 @@
|
||||||
"ui.copied": "{label} скопирован",
|
"ui.copied": "{label} скопирован",
|
||||||
"ui.rotateConfirm": "Сменить токен сопряжения?",
|
"ui.rotateConfirm": "Сменить токен сопряжения?",
|
||||||
"ui.rotating": "Смена токена...",
|
"ui.rotating": "Смена токена...",
|
||||||
"ui.tokenRotated": "Токен изменён"
|
"ui.tokenRotated": "Токен изменён",
|
||||||
|
"ui.saveError": "Не удалось обновить материалы браузера. Повторите попытку.",
|
||||||
|
"ui.createNoteError": "Не удалось создать заметку. Повторите попытку.",
|
||||||
|
"ui.createLinkError": "Не удалось создать ссылку. Повторите попытку.",
|
||||||
|
"ui.openLinkError": "Не удалось открыть ссылку. Повторите попытку.",
|
||||||
|
"ui.createFileError": "Не удалось создать файл. Повторите попытку.",
|
||||||
|
"ui.loadError": "Не удалось загрузить материалы браузера. Повторите попытку.",
|
||||||
|
"ui.receiverError": "Приёмник браузера недоступен. Повторите попытку.",
|
||||||
|
"ui.pairingLoadError": "Не удалось загрузить параметры подключения браузера. Повторите попытку.",
|
||||||
|
"ui.clipboardError": "Не удалось скопировать в буфер обмена. Повторите попытку.",
|
||||||
|
"ui.tokenRotateError": "Не удалось сменить токен сопряжения. Повторите попытку.",
|
||||||
|
"ui.statusFilter": "Фильтр состояния материала",
|
||||||
|
"ui.workspaceFilter": "Фильтр Дела",
|
||||||
|
"ui.assignment": "Назначить Делу",
|
||||||
|
"ui.clearAssignment": "Снять назначение",
|
||||||
|
"ui.empty": "Пока нет материалов из браузера. Отправьте страницу, выделенный текст или ссылку с помощью расширения.",
|
||||||
|
"ui.emptyFiltered": "Нет материалов по текущим фильтрам.",
|
||||||
|
"ui.untitledCapture": "Материал без названия",
|
||||||
|
"ui.url": "URL",
|
||||||
|
"ui.browser": "Браузер",
|
||||||
|
"ui.assignBeforeCreation": "Назначьте Дело перед созданием заметки, ссылки или файла.",
|
||||||
|
"ui.markProcessed": "Отметить обработанным",
|
||||||
|
"ui.markUnprocessed": "Отметить необработанным",
|
||||||
|
"ui.openLink": "Открыть ссылку",
|
||||||
|
"ui.restore": "Вернуть во входящие",
|
||||||
|
"ui.archive": "Архивировать",
|
||||||
|
"ui.deletePermanently": "Удалить навсегда",
|
||||||
|
"ui.captureReceived": "Материал получен",
|
||||||
|
"ui.captureLoadError": "Не удалось загрузить полученный материал.",
|
||||||
|
"ui.inboxArchived": "Входящие архивированы",
|
||||||
|
"ui.workspaceCapturesArchived": "Материалы Дела архивированы",
|
||||||
|
"ui.assignedToWorkspace": "Материал назначен Делу «{workspace}»",
|
||||||
|
"ui.captureUnassigned": "Материал не назначен",
|
||||||
|
"ui.captureArchived": "Материал архивирован",
|
||||||
|
"ui.captureRestored": "Материал возвращён во входящие",
|
||||||
|
"ui.captureDeleted": "Материал удалён навсегда",
|
||||||
|
"ui.captureProcessed": "Материал отмечен обработанным",
|
||||||
|
"ui.captureUnprocessed": "Материал отмечен необработанным",
|
||||||
|
"ui.creatingNote": "Создание заметки...",
|
||||||
|
"ui.noteCreated": "Заметка создана: {path}",
|
||||||
|
"ui.creatingLink": "Создание ссылки...",
|
||||||
|
"ui.linkCreated": "Ссылка создана",
|
||||||
|
"ui.creatingFile": "Создание файла...",
|
||||||
|
"ui.fileCreated": "Файл создан: {path}",
|
||||||
|
"ui.receiverReadyAll": "Приёмник готов для всех Дел",
|
||||||
|
"ui.receiverReadyWorkspace": "Приёмник готов для этого Дела",
|
||||||
|
"ui.items.one": "{count} материал",
|
||||||
|
"ui.items.few": "{count} материала",
|
||||||
|
"ui.items.many": "{count} материалов",
|
||||||
|
"ui.items.other": "{count} материала",
|
||||||
|
"ui.items.filtered": "{visible} из {total}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "verstak.browser-inbox",
|
"id": "verstak.browser-inbox",
|
||||||
"name": "Browser Inbox",
|
"name": "Browser",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Global browser capture queue with explicit workspace assignment delivered through the local receiver event protocol.",
|
"description": "Global browser materials with explicit Deal assignment delivered through the local receiver event protocol.",
|
||||||
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
"localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
|
||||||
"source": "official",
|
"source": "official",
|
||||||
"icon": "inbox",
|
"icon": "inbox",
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
"views": [
|
"views": [
|
||||||
{
|
{
|
||||||
"id": "verstak.browser-inbox.view",
|
"id": "verstak.browser-inbox.view",
|
||||||
"title": "Browser Inbox",
|
"title": "Browser",
|
||||||
"icon": "inbox",
|
"icon": "inbox",
|
||||||
"component": "BrowserInboxView"
|
"component": "BrowserInboxView"
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"sidebarItems": [
|
"sidebarItems": [
|
||||||
{
|
{
|
||||||
"id": "verstak.browser-inbox.sidebar",
|
"id": "verstak.browser-inbox.sidebar",
|
||||||
"title": "Browser Inbox",
|
"title": "Browser",
|
||||||
"icon": "inbox",
|
"icon": "inbox",
|
||||||
"view": "verstak.browser-inbox.view",
|
"view": "verstak.browser-inbox.view",
|
||||||
"position": 30
|
"position": 30
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
"workspaceItems": [
|
"workspaceItems": [
|
||||||
{
|
{
|
||||||
"id": "verstak.browser-inbox.workspace",
|
"id": "verstak.browser-inbox.workspace",
|
||||||
"title": "Browser Inbox",
|
"title": "Browser",
|
||||||
"icon": "inbox",
|
"icon": "inbox",
|
||||||
"component": "BrowserInboxView"
|
"component": "BrowserInboxView"
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
"settingsPanels": [
|
"settingsPanels": [
|
||||||
{
|
{
|
||||||
"id": "verstak.browser-inbox.settings",
|
"id": "verstak.browser-inbox.settings",
|
||||||
"title": "Browser Inbox",
|
"title": "Browser",
|
||||||
"component": "BrowserInboxSettings"
|
"component": "BrowserInboxSettings"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -259,11 +259,14 @@
|
||||||
var start = textarea.selectionStart;
|
var start = textarea.selectionStart;
|
||||||
var end = textarea.selectionEnd;
|
var end = textarea.selectionEnd;
|
||||||
var value = textarea.value;
|
var value = textarea.value;
|
||||||
var selected = value.slice(start, end) || placeholder || '';
|
var lineStart = value.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
|
||||||
|
var lineEnd = value.indexOf('\n', end);
|
||||||
|
if (lineEnd === -1) lineEnd = value.length;
|
||||||
|
var selected = value.slice(lineStart, lineEnd) || placeholder || '';
|
||||||
var replacement = selected.split('\n').map(function (line) { return prefix + line; }).join('\n');
|
var replacement = selected.split('\n').map(function (line) { return prefix + line; }).join('\n');
|
||||||
textarea.value = value.slice(0, start) + replacement + value.slice(end);
|
textarea.value = value.slice(0, lineStart) + replacement + value.slice(lineEnd);
|
||||||
textarea.selectionStart = start;
|
textarea.selectionStart = lineStart;
|
||||||
textarea.selectionEnd = start + replacement.length;
|
textarea.selectionEnd = lineStart + replacement.length;
|
||||||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
}
|
}
|
||||||
|
|
@ -492,10 +495,10 @@
|
||||||
rebuildEditorArea();
|
rebuildEditorArea();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
|
console.warn('[default-editor] load error:', err);
|
||||||
editorWrap.innerHTML = '';
|
editorWrap.innerHTML = '';
|
||||||
editorWrap.appendChild(el('div', { className: 'de-error' }, [
|
editorWrap.appendChild(el('div', { className: 'de-error' }, [
|
||||||
el('div', {}, [tr('ui.loadFailed', null, 'Failed to load file')]),
|
el('div', {}, [tr('ui.loadFailed', null, 'Could not load the file. Please try again.')])
|
||||||
el('div', { className: 'de-error-msg' }, [(err && err.message) ? err.message : String(err)])
|
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,5 @@
|
||||||
"ui.saved": "Saved",
|
"ui.saved": "Saved",
|
||||||
"ui.discardConfirm": "Discard unsaved changes and reload from disk?",
|
"ui.discardConfirm": "Discard unsaved changes and reload from disk?",
|
||||||
"ui.loading": "Loading...",
|
"ui.loading": "Loading...",
|
||||||
"ui.loadFailed": "Failed to load file"
|
"ui.loadFailed": "Could not load the file. Please try again."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,5 @@
|
||||||
"ui.saved": "Сохранено",
|
"ui.saved": "Сохранено",
|
||||||
"ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?",
|
"ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?",
|
||||||
"ui.loading": "Загрузка...",
|
"ui.loading": "Загрузка...",
|
||||||
"ui.loadFailed": "Не удалось загрузить файл"
|
"ui.loadFailed": "Не удалось загрузить файл. Повторите попытку."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,9 @@
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
|
console.warn('[file-preview] preview error:', err);
|
||||||
body.className = 'fp-error';
|
body.className = 'fp-error';
|
||||||
body.textContent = tr('ui.error', { error: err && err.message ? err.message : String(err) }, 'Preview error: ' + (err && err.message ? err.message : String(err)));
|
body.textContent = tr('ui.error', null, 'Could not preview this file. Please try again.');
|
||||||
});
|
});
|
||||||
if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
|
||||||
api.i18n.onDidChangeLocale(function () {
|
api.i18n.onDidChangeLocale(function () {
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,5 @@
|
||||||
"ui.openExternal": "Open External",
|
"ui.openExternal": "Open External",
|
||||||
"ui.preview": "Preview",
|
"ui.preview": "Preview",
|
||||||
"ui.loading": "Loading...",
|
"ui.loading": "Loading...",
|
||||||
"ui.error": "Preview error: {error}"
|
"ui.error": "Could not preview this file. Please try again."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,5 @@
|
||||||
"ui.openExternal": "Открыть во внешнем приложении",
|
"ui.openExternal": "Открыть во внешнем приложении",
|
||||||
"ui.preview": "Просмотр",
|
"ui.preview": "Просмотр",
|
||||||
"ui.loading": "Загрузка...",
|
"ui.loading": "Загрузка...",
|
||||||
"ui.error": "Ошибка просмотра: {error}"
|
"ui.error": "Не удалось открыть предварительный просмотр файла. Повторите попытку."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -320,14 +320,14 @@
|
||||||
return Promise.reject(new Error('clipboard unavailable'));
|
return Promise.reject(new Error('clipboard unavailable'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function showExternalFallback(entry, mode, reason) {
|
function showExternalFallback(entry, mode) {
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
var pathToShow = entry.relativePath;
|
var pathToShow = entry.relativePath;
|
||||||
if (mode === 'explorer' && entry.type !== 'folder') {
|
if (mode === 'explorer' && entry.type !== 'folder') {
|
||||||
pathToShow = parentPath(entry.relativePath) || entry.relativePath;
|
pathToShow = parentPath(entry.relativePath) || entry.relativePath;
|
||||||
}
|
}
|
||||||
var title = mode === 'explorer' ? 'Show in Explorer' : 'Open External';
|
var title = mode === 'explorer' ? 'Show in Explorer' : 'Open External';
|
||||||
var message = title + ' failed.\n' + (reason ? String(reason) + '\n' : '') + 'Vault-relative path:\n' + pathToShow;
|
var message = title + ' failed.\nVault-relative path:\n' + pathToShow;
|
||||||
confirmModal(message, { confirmText: 'Copy Path', cancelText: 'Close' }).then(function (copy) {
|
confirmModal(message, { confirmText: 'Copy Path', cancelText: 'Close' }).then(function (copy) {
|
||||||
if (!copy) return;
|
if (!copy) return;
|
||||||
copyTextToClipboard(pathToShow).catch(function (err) {
|
copyTextToClipboard(pathToShow).catch(function (err) {
|
||||||
|
|
@ -374,6 +374,13 @@
|
||||||
return fallback || key;
|
return fallback || key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reportError(key, fallback, err) {
|
||||||
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
|
console.warn('[verstak.files] ' + key, err);
|
||||||
|
}
|
||||||
|
return tr(key, null, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
function scopedPath(local) {
|
function scopedPath(local) {
|
||||||
local = cleanPath(local);
|
local = cleanPath(local);
|
||||||
return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local;
|
return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local;
|
||||||
|
|
@ -714,10 +721,10 @@
|
||||||
renderList();
|
renderList();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
|
console.warn('[verstak.files] load error:', err);
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
listContainer.appendChild(el('div', { className: 'files-error' }, [
|
listContainer.appendChild(el('div', { className: 'files-error' }, [
|
||||||
el('div', {}, [tr('ui.loadFailed', null, 'Failed to load files')]),
|
el('div', {}, [tr('ui.loadFailed', null, 'Could not load files. Please try again.')])
|
||||||
el('div', { className: 'files-error-msg' }, [(err && err.message) ? err.message : String(err)])
|
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -863,7 +870,7 @@
|
||||||
api.workbench.openResource({ kind: 'vault-file', path: full, mode: 'edit', extension: ext ? '.' + ext : '', context: { sourcePluginId: 'verstak.files', sourceView: 'files' } }).catch(function () {});
|
api.workbench.openResource({ kind: 'vault-file', path: full, mode: 'edit', extension: ext ? '.' + ext : '', context: { sourcePluginId: 'verstak.files', sourceView: 'files' } }).catch(function () {});
|
||||||
}
|
}
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setCreateError('Error: ' + ((err && err.message) ? err.message : String(err)));
|
setCreateError(reportError('ui.createError', 'Could not create this item. Please try again.', err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -923,7 +930,7 @@
|
||||||
setRenameError('A file with that name already exists');
|
setRenameError('A file with that name already exists');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRenameError('Error: ' + ((err && err.message) ? err.message : String(err)));
|
setRenameError(reportError('ui.renameError', 'Could not rename this item. Please try again.', err));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -935,7 +942,9 @@
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
api.files.trash(entry.relativePath).then(function () {
|
api.files.trash(entry.relativePath).then(function () {
|
||||||
loadEntries();
|
loadEntries();
|
||||||
}).catch(function (err) { window.alert((err && err.message) ? err.message : String(err)); });
|
}).catch(function (err) {
|
||||||
|
window.alert(reportError('ui.trashError', 'Could not move this item to trash. Please try again.', err));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else if (count > 1) {
|
} else if (count > 1) {
|
||||||
confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) {
|
confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) {
|
||||||
|
|
@ -994,11 +1003,12 @@
|
||||||
var filesApi = api && api.files;
|
var filesApi = api && api.files;
|
||||||
var action = mode === 'explorer' ? filesApi && filesApi.showInFolder : filesApi && filesApi.openExternal;
|
var action = mode === 'explorer' ? filesApi && filesApi.showInFolder : filesApi && filesApi.openExternal;
|
||||||
if (typeof action !== 'function') {
|
if (typeof action !== 'function') {
|
||||||
showExternalFallback(entry, mode, 'files external-open API is unavailable.');
|
showExternalFallback(entry, mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
action(entry.relativePath).catch(function (err) {
|
action(entry.relativePath).catch(function (err) {
|
||||||
showExternalFallback(entry, mode, err && err.message ? err.message : err);
|
console.warn('[verstak.files] external open error:', err);
|
||||||
|
showExternalFallback(entry, mode);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,8 @@
|
||||||
"ui.noMatches": "No matches",
|
"ui.noMatches": "No matches",
|
||||||
"ui.clearFilter": "Clear filter",
|
"ui.clearFilter": "Clear filter",
|
||||||
"ui.loading": "Loading...",
|
"ui.loading": "Loading...",
|
||||||
"ui.loadFailed": "Failed to load files"
|
"ui.loadFailed": "Could not load files. Please try again.",
|
||||||
|
"ui.createError": "Could not create this item. Please try again.",
|
||||||
|
"ui.renameError": "Could not rename this item. Please try again.",
|
||||||
|
"ui.trashError": "Could not move this item to trash. Please try again."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,8 @@
|
||||||
"ui.noMatches": "Совпадений нет",
|
"ui.noMatches": "Совпадений нет",
|
||||||
"ui.clearFilter": "Сбросить фильтр",
|
"ui.clearFilter": "Сбросить фильтр",
|
||||||
"ui.loading": "Загрузка...",
|
"ui.loading": "Загрузка...",
|
||||||
"ui.loadFailed": "Не удалось загрузить файлы"
|
"ui.loadFailed": "Не удалось загрузить файлы. Повторите попытку.",
|
||||||
|
"ui.createError": "Не удалось создать этот элемент. Повторите попытку.",
|
||||||
|
"ui.renameError": "Не удалось переименовать этот элемент. Повторите попытку.",
|
||||||
|
"ui.trashError": "Не удалось переместить элемент в корзину. Повторите попытку."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
'.journal-status.error{display:inline-flex;border:1px solid rgba(233,69,96,.45);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-danger-muted,rgba(233,69,96,.14));color:#ffc6ce;padding:.18rem .4rem}',
|
'.journal-status.error{display:inline-flex;border:1px solid rgba(233,69,96,.45);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-danger-muted,rgba(233,69,96,.14));color:#ffc6ce;padding:.18rem .4rem}',
|
||||||
'.journal-input{font-size:.8rem;padding:.38rem .5rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);min-width:0;font-family:inherit}',
|
'.journal-input{font-size:.8rem;padding:.38rem .5rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);min-width:0;font-family:inherit}',
|
||||||
'.journal-input.textarea{min-height:7rem;resize:vertical;line-height:1.4}',
|
'.journal-input.textarea{min-height:7rem;resize:vertical;line-height:1.4}',
|
||||||
|
'.journal-input.journal-select{appearance:none;background-color:#0f1424;background-image:linear-gradient(45deg,transparent 50%,var(--vt-color-text-muted,#7f8aa3) 50%),linear-gradient(135deg,var(--vt-color-text-muted,#7f8aa3) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.7rem}.journal-input.journal-select option{background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb)}',
|
||||||
'.journal-input:focus{outline:none;border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
'.journal-input:focus{outline:none;border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
||||||
'.journal-billable{display:flex;align-items:center;gap:.25rem;font-size:.74rem;color:var(--vt-color-text-secondary,#b7c0d4);white-space:nowrap}',
|
'.journal-billable{display:flex;align-items:center;gap:.25rem;font-size:.74rem;color:var(--vt-color-text-secondary,#b7c0d4);white-space:nowrap}',
|
||||||
'.journal-list{flex:1;min-height:0;overflow:auto;background:var(--vt-color-background,#101020)}',
|
'.journal-list{flex:1;min-height:0;overflow:auto;background:var(--vt-color-background,#101020)}',
|
||||||
|
|
@ -275,6 +276,7 @@
|
||||||
|
|
||||||
var scope = scopeFromProps(props || {});
|
var scope = scopeFromProps(props || {});
|
||||||
var entries = [];
|
var entries = [];
|
||||||
|
var workspaceOptions = [];
|
||||||
function tr(key, params, fallback) {
|
function tr(key, params, fallback) {
|
||||||
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback);
|
||||||
return fallback || key;
|
return fallback || key;
|
||||||
|
|
@ -283,6 +285,14 @@
|
||||||
var statusClass = '';
|
var statusClass = '';
|
||||||
var modalHost = el('div', { className: 'journal-modal-host', hidden: 'hidden' });
|
var modalHost = el('div', { className: 'journal-modal-host', hidden: 'hidden' });
|
||||||
|
|
||||||
|
function reportError(key, fallback, err) {
|
||||||
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
|
console.warn('[verstak.journal] ' + key, err);
|
||||||
|
}
|
||||||
|
statusText = tr(key, null, fallback);
|
||||||
|
statusClass = 'error';
|
||||||
|
}
|
||||||
|
|
||||||
var toolbar = el('div', { className: 'journal-toolbar' });
|
var toolbar = el('div', { className: 'journal-toolbar' });
|
||||||
var titleEl = el('span', { className: 'journal-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Journal') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Journal · ' + scope.label) });
|
var titleEl = el('span', { className: 'journal-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Journal') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Journal · ' + scope.label) });
|
||||||
var countEl = el('span', { className: 'journal-count' });
|
var countEl = el('span', { className: 'journal-count' });
|
||||||
|
|
@ -304,12 +314,23 @@
|
||||||
containerEl.appendChild(listEl);
|
containerEl.appendChild(listEl);
|
||||||
containerEl.appendChild(modalHost);
|
containerEl.appendChild(modalHost);
|
||||||
|
|
||||||
function persist() {
|
function persist(workspaceRoot, values) {
|
||||||
if (scope.mode !== 'workspace') return Promise.resolve();
|
|
||||||
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(scope.key, storageEntries(entries)).catch(function (err) {
|
var target = cleanWorkspace(workspaceRoot || scope.workspaceRoot);
|
||||||
statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save journal: ' + (err && err.message ? err.message : String(err)));
|
if (!target) return Promise.resolve();
|
||||||
statusClass = 'error';
|
return api.settings.write(WORKLOG_PREFIX + encodeKey(target), storageEntries(values || entries)).catch(function (err) {
|
||||||
|
reportError('ui.saveError', 'Could not save journal. Please try again.', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWorkspaceOptions() {
|
||||||
|
if (!api || !api.files || typeof api.files.list !== 'function') return Promise.resolve();
|
||||||
|
return api.files.list('').then(function (items) {
|
||||||
|
workspaceOptions = (Array.isArray(items) ? items : []).filter(function (item) {
|
||||||
|
return text(item && item.type).toLowerCase() === 'folder';
|
||||||
|
}).map(function (item) { return cleanWorkspace(item.relativePath || item.name); }).filter(function (value) {
|
||||||
|
return value && value.indexOf('/') === -1;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,8 +346,7 @@
|
||||||
statusText = tr('ui.aggregating', null, 'Aggregating worklogs');
|
statusText = tr('ui.aggregating', null, 'Aggregating worklogs');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load journal: ' + (err && err.message ? err.message : String(err)));
|
reportError('ui.loadError', 'Could not load journal. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return api.settings.read(scope.key).then(function (stored) {
|
return api.settings.read(scope.key).then(function (stored) {
|
||||||
|
|
@ -334,8 +354,7 @@
|
||||||
statusText = tr('ui.ready', null, 'Ready');
|
statusText = tr('ui.ready', null, 'Ready');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load journal: ' + (err && err.message ? err.message : String(err)));
|
reportError('ui.loadError', 'Could not load journal. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,7 +364,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEntryModal(existingEntry, candidate, completedTodo) {
|
function showEntryModal(existingEntry, candidate, completedTodo) {
|
||||||
if (scope.mode !== 'workspace') return;
|
|
||||||
var editing = !!existingEntry;
|
var editing = !!existingEntry;
|
||||||
var reviewingCandidate = !editing && !!candidate;
|
var reviewingCandidate = !editing && !!candidate;
|
||||||
var reviewingTodo = !editing && !!completedTodo;
|
var reviewingTodo = !editing && !!completedTodo;
|
||||||
|
|
@ -356,6 +374,13 @@
|
||||||
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 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 workspaceInput = null;
|
||||||
|
if (scope.mode === 'global') {
|
||||||
|
workspaceInput = el('select', { className: 'journal-input journal-select', 'data-journal-input': 'workspaceRootPath' });
|
||||||
|
workspaceOptions.forEach(function (workspace) {
|
||||||
|
workspaceInput.appendChild(el('option', { value: workspace, textContent: workspace }));
|
||||||
|
});
|
||||||
|
}
|
||||||
var activityInputs = reviewingCandidate ? candidate.activities.map(function (activity) {
|
var activityInputs = reviewingCandidate ? candidate.activities.map(function (activity) {
|
||||||
var input = el('input', { type: 'checkbox', value: activity.activityId, checked: 'checked', 'data-journal-candidate-activity': activity.activityId });
|
var input = el('input', { type: 'checkbox', value: activity.activityId, checked: 'checked', 'data-journal-candidate-activity': activity.activityId });
|
||||||
input.checked = true;
|
input.checked = true;
|
||||||
|
|
@ -369,6 +394,7 @@
|
||||||
summary: summaryInput.value,
|
summary: summaryInput.value,
|
||||||
minutes: minutesInput.value,
|
minutes: minutesInput.value,
|
||||||
billable: billableInput.checked === true,
|
billable: billableInput.checked === true,
|
||||||
|
workspaceRootPath: workspaceInput ? workspaceInput.value : scope.workspaceRoot,
|
||||||
sourceCandidateId: reviewingCandidate ? candidate.candidateId : (existingEntry ? existingEntry.sourceCandidateId : ''),
|
sourceCandidateId: reviewingCandidate ? candidate.candidateId : (existingEntry ? existingEntry.sourceCandidateId : ''),
|
||||||
sessionId: reviewingCandidate ? candidate.sessionId : '',
|
sessionId: reviewingCandidate ? candidate.sessionId : '',
|
||||||
handledThrough: reviewingCandidate ? candidate.handledThrough : '',
|
handledThrough: reviewingCandidate ? candidate.handledThrough : '',
|
||||||
|
|
@ -411,6 +437,7 @@
|
||||||
el('div', { className: 'journal-modal-grid' }, [
|
el('div', { className: 'journal-modal-grid' }, [
|
||||||
el('label', { className: 'journal-field' }, [tr('ui.date', null, 'Date'), dateInput]),
|
el('label', { className: 'journal-field' }, [tr('ui.date', null, 'Date'), dateInput]),
|
||||||
el('label', { className: 'journal-field' }, [tr('ui.minutes', null, 'Minutes'), minutesInput]),
|
el('label', { className: 'journal-field' }, [tr('ui.minutes', null, 'Minutes'), minutesInput]),
|
||||||
|
workspaceInput ? el('label', { className: 'journal-field wide' }, [tr('ui.workspace', null, 'Deal'), workspaceInput]) : null,
|
||||||
el('label', { className: 'journal-field wide' }, [tr('ui.fieldTitle', null, 'Title'), titleInput]),
|
el('label', { className: 'journal-field wide' }, [tr('ui.fieldTitle', null, 'Title'), titleInput]),
|
||||||
el('label', { className: 'journal-field wide' }, [tr('ui.body', null, 'Body'), summaryInput]),
|
el('label', { className: 'journal-field wide' }, [tr('ui.body', null, 'Body'), summaryInput]),
|
||||||
el('label', { className: 'journal-billable' }, [billableInput, tr('ui.billable', null, 'Billable')])
|
el('label', { className: 'journal-billable' }, [billableInput, tr('ui.billable', null, 'Billable')])
|
||||||
|
|
@ -426,7 +453,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOrUpdateEntry(existingEntry, formValue) {
|
function addOrUpdateEntry(existingEntry, formValue) {
|
||||||
if (scope.mode !== 'workspace') return;
|
|
||||||
var title = text(formValue && formValue.title).trim();
|
var title = text(formValue && formValue.title).trim();
|
||||||
if (!title) {
|
if (!title) {
|
||||||
statusText = tr('ui.titleRequired', null, 'Title is required');
|
statusText = tr('ui.titleRequired', null, 'Title is required');
|
||||||
|
|
@ -434,6 +460,8 @@
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var workspaceRoot = cleanWorkspace(formValue && formValue.workspaceRootPath || scope.workspaceRoot);
|
||||||
|
if (!workspaceRoot) return;
|
||||||
var sourceCandidateId = text(formValue && formValue.sourceCandidateId || (existingEntry && existingEntry.sourceCandidateId)).trim();
|
var sourceCandidateId = text(formValue && formValue.sourceCandidateId || (existingEntry && existingEntry.sourceCandidateId)).trim();
|
||||||
var sessionID = text(formValue && formValue.sessionId).trim();
|
var sessionID = text(formValue && formValue.sessionId).trim();
|
||||||
var handledThrough = text(formValue && formValue.handledThrough).trim();
|
var handledThrough = text(formValue && formValue.handledThrough).trim();
|
||||||
|
|
@ -451,8 +479,8 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var entry = normalizeEntry({
|
var entry = normalizeEntry({
|
||||||
entryId: existingEntry ? existingEntry.entryId : entryId(scope.workspaceRoot, formValue.date || today(), title),
|
entryId: existingEntry ? existingEntry.entryId : entryId(workspaceRoot, formValue.date || today(), title),
|
||||||
workspaceRootPath: scope.workspaceRoot,
|
workspaceRootPath: workspaceRoot,
|
||||||
date: formValue.date || today(),
|
date: formValue.date || today(),
|
||||||
title: title,
|
title: title,
|
||||||
summary: formValue.summary,
|
summary: formValue.summary,
|
||||||
|
|
@ -470,10 +498,11 @@
|
||||||
entries = [entry].concat(entries);
|
entries = [entry].concat(entries);
|
||||||
}
|
}
|
||||||
entries = sortEntries(entries);
|
entries = sortEntries(entries);
|
||||||
|
var targetEntries = entries.filter(function (item) { return item.workspaceRootPath === workspaceRoot; });
|
||||||
closeEntryModal();
|
closeEntryModal();
|
||||||
statusText = existingEntry ? 'Entry updated' : 'Entry added';
|
statusText = existingEntry ? 'Entry updated' : 'Entry added';
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
persist().then(function () {
|
persist(workspaceRoot, targetEntries).then(function () {
|
||||||
if (!sessionID || !handledThrough || !api || !api.events || typeof api.events.publish !== 'function') return undefined;
|
if (!sessionID || !handledThrough || !api || !api.events || typeof api.events.publish !== 'function') return undefined;
|
||||||
return api.events.publish('activity.session.handled', {
|
return api.events.publish('activity.session.handled', {
|
||||||
sessionId: sessionID,
|
sessionId: sessionID,
|
||||||
|
|
@ -525,12 +554,12 @@
|
||||||
);
|
);
|
||||||
statusEl.textContent = statusText;
|
statusEl.textContent = statusText;
|
||||||
statusEl.className = 'journal-status' + (statusClass ? ' ' + statusClass : '');
|
statusEl.className = 'journal-status' + (statusClass ? ' ' + statusClass : '');
|
||||||
addBtn.disabled = scope.mode !== 'workspace';
|
addBtn.disabled = false;
|
||||||
renderList();
|
renderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
render();
|
render();
|
||||||
loadStored().then(function () {
|
Promise.all([loadStored(), loadWorkspaceOptions()]).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);
|
var completedTodo = completedTodoFromRequest(props && props.toolRequest, scope.workspaceRoot);
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
"ui.title": "Journal",
|
"ui.title": "Journal",
|
||||||
"ui.workspaceTitle": "Journal · {workspace}",
|
"ui.workspaceTitle": "Journal · {workspace}",
|
||||||
"ui.add": "Add",
|
"ui.add": "Add",
|
||||||
"ui.saveError": "Could not save journal: {error}",
|
"ui.saveError": "Could not save journal. Please try again.",
|
||||||
"ui.aggregating": "Aggregating worklogs",
|
"ui.aggregating": "Aggregating worklogs",
|
||||||
"ui.loadError": "Could not load journal: {error}",
|
"ui.loadError": "Could not load journal. Please try again.",
|
||||||
"ui.ready": "Ready",
|
"ui.ready": "Ready",
|
||||||
"ui.workItem": "Work item",
|
"ui.workItem": "Work item",
|
||||||
"ui.body": "Body",
|
"ui.body": "Body",
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@
|
||||||
"ui.title": "Журнал",
|
"ui.title": "Журнал",
|
||||||
"ui.workspaceTitle": "Журнал · {workspace}",
|
"ui.workspaceTitle": "Журнал · {workspace}",
|
||||||
"ui.add": "Добавить",
|
"ui.add": "Добавить",
|
||||||
"ui.saveError": "Не удалось сохранить журнал: {error}",
|
"ui.saveError": "Не удалось сохранить журнал. Повторите попытку.",
|
||||||
"ui.aggregating": "Сбор записей о работе",
|
"ui.aggregating": "Сбор записей о работе",
|
||||||
"ui.loadError": "Не удалось загрузить журнал: {error}",
|
"ui.loadError": "Не удалось загрузить журнал. Повторите попытку.",
|
||||||
"ui.ready": "Готово",
|
"ui.ready": "Готово",
|
||||||
"ui.workItem": "Выполненная работа",
|
"ui.workItem": "Выполненная работа",
|
||||||
"ui.body": "Описание",
|
"ui.body": "Описание",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"events.publish",
|
"events.publish",
|
||||||
|
"files.read",
|
||||||
"storage.namespace",
|
"storage.namespace",
|
||||||
"ui.register"
|
"ui.register"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userFacingError(key, fallback, err) {
|
||||||
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
|
console.warn('[verstak.notes] ' + key, err);
|
||||||
|
}
|
||||||
|
return tr(key, null, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
function loadNotes() {
|
function loadNotes() {
|
||||||
listContainer.innerHTML = '';
|
listContainer.innerHTML = '';
|
||||||
listContainer.appendChild(el('div', { className: 'notes-empty' }, [tr('ui.loading', null, 'Loading...')]));
|
listContainer.appendChild(el('div', { className: 'notes-empty' }, [tr('ui.loading', null, 'Loading...')]));
|
||||||
|
|
@ -335,7 +342,7 @@
|
||||||
renderList();
|
renderList();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
renderEmpty('Error: ' + (err.message || err));
|
renderEmpty(userFacingError('ui.loadError', 'Could not load notes. Please try again.', err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,8 +386,7 @@
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
if (!disposed) setStatus(action.label + ' complete', 'success');
|
if (!disposed) setStatus(action.label + ' complete', 'success');
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.error('[notes] contribution action failed:', err);
|
if (!disposed) setStatus(userFacingError('ui.actionError', 'Could not complete this note action. Please try again.', err), 'error');
|
||||||
if (!disposed) setStatus('Error: ' + (err && err.message ? err.message : err), 'error');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -533,7 +539,7 @@
|
||||||
}).catch(function () {});
|
}).catch(function () {});
|
||||||
}
|
}
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus('Error: ' + (err.message || err), 'error');
|
setStatus(userFacingError('ui.createError', 'Could not create the note. Please try again.', err), 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -568,7 +574,7 @@
|
||||||
setStatus(tr('ui.renamed', null, 'Note renamed'), 'success');
|
setStatus(tr('ui.renamed', null, 'Note renamed'), 'success');
|
||||||
loadNotes();
|
loadNotes();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus('Error: ' + (err.message || err), 'error');
|
setStatus(userFacingError('ui.renameError', 'Could not rename the note. Please try again.', err), 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -585,7 +591,7 @@
|
||||||
setStatus(tr('ui.trashed', null, 'Note moved to trash'), 'success');
|
setStatus(tr('ui.trashed', null, 'Note moved to trash'), 'success');
|
||||||
loadNotes();
|
loadNotes();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus('Error: ' + (err.message || err), 'error');
|
setStatus(userFacingError('ui.trashError', 'Could not move the note to trash. Please try again.', err), 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,10 @@
|
||||||
"ui.existingFile": " Existing file: {path}.",
|
"ui.existingFile": " Existing file: {path}.",
|
||||||
"ui.chooseDifferent": " Please choose a different title.",
|
"ui.chooseDifferent": " Please choose a different title.",
|
||||||
"ui.trashTitle": "Move Note to Trash",
|
"ui.trashTitle": "Move Note to Trash",
|
||||||
"ui.trashConfirm": "Move \"{title}\" to trash?"
|
"ui.trashConfirm": "Move \"{title}\" to trash?",
|
||||||
|
"ui.loadError": "Could not load notes. Please try again.",
|
||||||
|
"ui.actionError": "Could not complete this note action. Please try again.",
|
||||||
|
"ui.createError": "Could not create the note. Please try again.",
|
||||||
|
"ui.renameError": "Could not rename the note. Please try again.",
|
||||||
|
"ui.trashError": "Could not move the note to trash. Please try again."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,5 +28,10 @@
|
||||||
"ui.existingFile": " Существующий файл: {path}.",
|
"ui.existingFile": " Существующий файл: {path}.",
|
||||||
"ui.chooseDifferent": " Выберите другое название.",
|
"ui.chooseDifferent": " Выберите другое название.",
|
||||||
"ui.trashTitle": "Переместить заметку в корзину",
|
"ui.trashTitle": "Переместить заметку в корзину",
|
||||||
"ui.trashConfirm": "Переместить «{title}» в корзину?"
|
"ui.trashConfirm": "Переместить «{title}» в корзину?",
|
||||||
|
"ui.loadError": "Не удалось загрузить заметки. Повторите попытку.",
|
||||||
|
"ui.actionError": "Не удалось выполнить действие с заметкой. Повторите попытку.",
|
||||||
|
"ui.createError": "Не удалось создать заметку. Повторите попытку.",
|
||||||
|
"ui.renameError": "Не удалось переименовать заметку. Повторите попытку.",
|
||||||
|
"ui.trashError": "Не удалось переместить заметку в корзину. Повторите попытку."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,8 @@
|
||||||
try {
|
try {
|
||||||
providers = await api.contributions.list('searchProviders');
|
providers = await api.contributions.list('searchProviders');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
output.errors.push(err && err.message ? err.message : String(err));
|
console.warn('[verstak.search] list search providers:', err);
|
||||||
|
output.errors.push(true);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
providers = Array.isArray(providers) ? providers : [];
|
providers = Array.isArray(providers) ? providers : [];
|
||||||
|
|
@ -294,7 +295,8 @@
|
||||||
var normalized = normalizeProviderResults(provider, response && response.result);
|
var normalized = normalizeProviderResults(provider, response && response.result);
|
||||||
output.results = output.results.concat(normalized.slice(0, remaining - output.results.length));
|
output.results = output.results.concat(normalized.slice(0, remaining - output.results.length));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
output.errors.push(err && err.message ? err.message : String(err));
|
console.warn('[verstak.search] provider search:', err);
|
||||||
|
output.errors.push(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
|
|
@ -371,7 +373,9 @@
|
||||||
if (typeof alertEl.removeAttribute === 'function') alertEl.removeAttribute('hidden');
|
if (typeof alertEl.removeAttribute === 'function') alertEl.removeAttribute('hidden');
|
||||||
alertEl.appendChild(el('details', {}, [
|
alertEl.appendChild(el('details', {}, [
|
||||||
el('summary', {}, [tr('ui.providersFailed', null, 'Some plugin search providers did not respond')]),
|
el('summary', {}, [tr('ui.providersFailed', null, 'Some plugin search providers did not respond')]),
|
||||||
el('div', {}, [state.providerErrors.join('; ')])
|
el('div', {}, [state.providerErrors.map(function () {
|
||||||
|
return tr('ui.providerUnavailable', null, 'A search provider is unavailable.');
|
||||||
|
}).join(' ')])
|
||||||
]));
|
]));
|
||||||
} else if (typeof alertEl.setAttribute === 'function') {
|
} else if (typeof alertEl.setAttribute === 'function') {
|
||||||
alertEl.setAttribute('hidden', 'hidden');
|
alertEl.setAttribute('hidden', 'hidden');
|
||||||
|
|
@ -460,8 +464,9 @@
|
||||||
state.providerErrors = external.errors;
|
state.providerErrors = external.errors;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (seq !== searchSeq) return;
|
if (seq !== searchSeq) return;
|
||||||
|
console.warn('[verstak.search] search:', err);
|
||||||
state.results = [];
|
state.results = [];
|
||||||
state.error = err && err.message ? err.message : String(err);
|
state.error = tr('ui.searchError', null, 'Could not search the vault. Please try again.');
|
||||||
state.providerErrors = [];
|
state.providerErrors = [];
|
||||||
} finally {
|
} finally {
|
||||||
if (seq !== searchSeq) return;
|
if (seq !== searchSeq) return;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
"ui.searching": "Searching...",
|
"ui.searching": "Searching...",
|
||||||
"ui.search": "Search",
|
"ui.search": "Search",
|
||||||
"ui.providersFailed": "Some plugin search providers did not respond",
|
"ui.providersFailed": "Some plugin search providers did not respond",
|
||||||
|
"ui.providerUnavailable": "A search provider is unavailable.",
|
||||||
|
"ui.searchError": "Could not search the vault. Please try again.",
|
||||||
"ui.noResults": "No results",
|
"ui.noResults": "No results",
|
||||||
"ui.open": "Open",
|
"ui.open": "Open",
|
||||||
"ui.count": "{count} result(s)"
|
"ui.count": "{count} result(s)"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
"ui.searching": "Поиск...",
|
"ui.searching": "Поиск...",
|
||||||
"ui.search": "Искать",
|
"ui.search": "Искать",
|
||||||
"ui.providersFailed": "Некоторые поставщики поиска не ответили",
|
"ui.providersFailed": "Некоторые поставщики поиска не ответили",
|
||||||
|
"ui.providerUnavailable": "Один из источников поиска недоступен.",
|
||||||
|
"ui.searchError": "Не удалось выполнить поиск в хранилище. Повторите попытку.",
|
||||||
"ui.noResults": "Ничего не найдено",
|
"ui.noResults": "Ничего не найдено",
|
||||||
"ui.open": "Открыть",
|
"ui.open": "Открыть",
|
||||||
"ui.count": "Результатов: {count}"
|
"ui.count": "Результатов: {count}"
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
'.secrets-panel{min-height:0;overflow:auto;border-right:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface-muted,#111629)}',
|
'.secrets-panel{min-height:0;overflow:auto;border-right:1px solid var(--vt-color-border,#202b46);background:var(--vt-color-surface-muted,#111629)}',
|
||||||
'.secrets-main{min-width:0;min-height:0;overflow:auto;padding:1rem;background:var(--vt-color-background,#101020)}',
|
'.secrets-main{min-width:0;min-height:0;overflow:auto;padding:1rem;background:var(--vt-color-background,#101020)}',
|
||||||
'.secrets-toolbar{display:flex;align-items:center;gap:.5rem;min-height:2.75rem;padding:.65rem .75rem;border-bottom:1px solid var(--vt-color-border,#202b46)}',
|
'.secrets-toolbar{display:flex;align-items:center;gap:.5rem;min-height:2.75rem;padding:.65rem .75rem;border-bottom:1px solid var(--vt-color-border,#202b46)}',
|
||||||
|
'.secrets-filters{display:grid;grid-template-columns:minmax(0,1fr) minmax(8rem,.8fr);gap:.4rem;padding:.5rem .55rem;border-bottom:1px solid var(--vt-color-border,#202b46)}',
|
||||||
'.secrets-title{font-weight:600;font-size:.88rem}',
|
'.secrets-title{font-weight:600;font-size:.88rem}',
|
||||||
'.secrets-count{color:var(--vt-color-text-muted,#7f8aa3);font-size:.76rem}',
|
'.secrets-count{color:var(--vt-color-text-muted,#7f8aa3);font-size:.76rem}',
|
||||||
'.secrets-spacer{flex:1}',
|
'.secrets-spacer{flex:1}',
|
||||||
|
|
@ -44,10 +45,11 @@
|
||||||
'.secrets-form{display:grid;gap:.65rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);padding:.9rem;background:var(--vt-color-surface,#15152c)}',
|
'.secrets-form{display:grid;gap:.65rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);padding:.9rem;background:var(--vt-color-surface,#15152c)}',
|
||||||
'.secrets-row{display:grid;grid-template-columns:8rem minmax(0,1fr);gap:.65rem;align-items:center}',
|
'.secrets-row{display:grid;grid-template-columns:8rem minmax(0,1fr);gap:.65rem;align-items:center}',
|
||||||
'.secrets-label{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3)}',
|
'.secrets-label{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3)}',
|
||||||
'.secrets-input,.secrets-textarea,.secrets-select{width:100%;box-sizing:border-box;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);font:inherit;font-size:.84rem;padding:.45rem .55rem;outline:none}',
|
'.secrets-input,.secrets-textarea,.secrets-select,.secrets-search{width:100%;box-sizing:border-box;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb);font:inherit;font-size:.84rem;padding:.45rem .55rem;outline:none}',
|
||||||
|
'.secrets-search{font-size:.76rem}',
|
||||||
'.secrets-select{appearance:none;background-color:#0d1117;background-image:linear-gradient(45deg,transparent 50%,#8b949e 50%),linear-gradient(135deg,#8b949e 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:2rem}.secrets-select option{background:#0d1117;color:var(--vt-color-text-primary,#f4f7fb)}',
|
'.secrets-select{appearance:none;background-color:#0d1117;background-image:linear-gradient(45deg,transparent 50%,#8b949e 50%),linear-gradient(135deg,#8b949e 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:2rem}.secrets-select option{background:#0d1117;color:var(--vt-color-text-primary,#f4f7fb)}',
|
||||||
'.secrets-textarea{min-height:6rem;resize:vertical;font-family:ui-monospace,SFMono-Regular,Consolas,monospace}',
|
'.secrets-textarea{min-height:6rem;resize:vertical;font-family:ui-monospace,SFMono-Regular,Consolas,monospace}',
|
||||||
'.secrets-input:focus,.secrets-textarea:focus,.secrets-select:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
'.secrets-input:focus,.secrets-textarea:focus,.secrets-select:focus,.secrets-search:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
||||||
'.secrets-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
|
'.secrets-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
|
||||||
'.secrets-status{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3);min-height:1rem}',
|
'.secrets-status{font-size:.78rem;color:var(--vt-color-text-muted,#7f8aa3);min-height:1rem}',
|
||||||
'.secrets-status.error{color:#ffc6ce}',
|
'.secrets-status.error{color:#ffc6ce}',
|
||||||
|
|
@ -58,7 +60,7 @@
|
||||||
'.secrets-table th{width:9rem;color:var(--vt-color-text-muted,#7f8aa3);font-weight:500;background:var(--vt-color-surface-muted,#111629)}',
|
'.secrets-table th{width:9rem;color:var(--vt-color-text-muted,#7f8aa3);font-weight:500;background:var(--vt-color-surface-muted,#111629)}',
|
||||||
'.secrets-table td{color:var(--vt-color-text-primary,#f4f7fb);overflow-wrap:anywhere}',
|
'.secrets-table td{color:var(--vt-color-text-primary,#f4f7fb);overflow-wrap:anywhere}',
|
||||||
'.secrets-table tr:last-child th,.secrets-table tr:last-child td{border-bottom:0}',
|
'.secrets-table tr:last-child th,.secrets-table tr:last-child td{border-bottom:0}',
|
||||||
'@media(max-width:780px){.secrets-root{grid-template-columns:1fr}.secrets-panel{border-right:0;border-bottom:1px solid #252b36;max-height:45vh}.secrets-row{grid-template-columns:1fr}}'
|
'@media(max-width:780px){.secrets-root{grid-template-columns:1fr}.secrets-panel{border-right:0;border-bottom:1px solid #252b36;max-height:45vh}.secrets-row,.secrets-filters{grid-template-columns:1fr}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
function el(tag, attrs, children) {
|
function el(tag, attrs, children) {
|
||||||
|
|
@ -97,13 +99,14 @@
|
||||||
|
|
||||||
function scopeLabel(record) {
|
function scopeLabel(record) {
|
||||||
var scope = record && record.scope || {};
|
var scope = record && record.scope || {};
|
||||||
if (scope.kind === ScopeWorkspace) return cleanWorkspace(scope.workspaceRootPath) || 'Workspace';
|
if (scope.kind === ScopeWorkspace) return cleanWorkspace(scope.workspaceRootPath) || 'Deal';
|
||||||
return 'Global';
|
return 'Global';
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedIDFromProps(props) {
|
function selectedIDFromProps(props) {
|
||||||
var resource = props && props.resource || {};
|
var resource = props && props.resource || {};
|
||||||
var path = text(resource.path || props && props.secretId);
|
var request = props && props.request || {};
|
||||||
|
var path = text(resource.path || request.path || props && props.secretId);
|
||||||
if (path.indexOf('verstak-secret://') === 0) return decodeURIComponent(path.slice('verstak-secret://'.length));
|
if (path.indexOf('verstak-secret://') === 0) return decodeURIComponent(path.slice('verstak-secret://'.length));
|
||||||
return decodeURIComponent(path.replace(/^\/+/, ''));
|
return decodeURIComponent(path.replace(/^\/+/, ''));
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +147,9 @@
|
||||||
var workspaceRoot = workspaceFromProps(props || {});
|
var workspaceRoot = workspaceFromProps(props || {});
|
||||||
var selectedID = selectedIDFromProps(props || {});
|
var selectedID = selectedIDFromProps(props || {});
|
||||||
var records = [];
|
var records = [];
|
||||||
|
var workspaceOptions = [];
|
||||||
|
var scopeFilter = 'all';
|
||||||
|
var searchQuery = '';
|
||||||
var selectedRecord = null;
|
var selectedRecord = null;
|
||||||
var selectedValue = '';
|
var selectedValue = '';
|
||||||
var initialized = false;
|
var initialized = false;
|
||||||
|
|
@ -161,6 +167,91 @@
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userFacingError(action, err) {
|
||||||
|
var raw = (err && err.message) ? err.message : String(err || '');
|
||||||
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
|
console.warn('[verstak.secrets] ' + action + ' failed', err);
|
||||||
|
}
|
||||||
|
if (action === 'unlock') {
|
||||||
|
var minimumLength = raw.match(/master password must be at least (\d+) characters/i);
|
||||||
|
if (minimumLength) {
|
||||||
|
return tr('ui.masterPasswordMinLength', { count: minimumLength[1] }, 'Master password must be at least ' + minimumLength[1] + ' characters.');
|
||||||
|
}
|
||||||
|
if (/master password is empty/i.test(raw)) {
|
||||||
|
return tr('ui.masterPasswordRequired', null, 'Enter a master password.');
|
||||||
|
}
|
||||||
|
if (/invalid master password/i.test(raw)) {
|
||||||
|
return tr('ui.masterPasswordInvalid', null, 'The master password is incorrect.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tr('ui.' + action + 'Error', null, {
|
||||||
|
status: 'Could not check the secret storage. Please try again.',
|
||||||
|
load: 'Could not load secrets. Please try again.',
|
||||||
|
read: 'Could not open this secret. Please try again.',
|
||||||
|
save: 'Could not save the secret. Please try again.',
|
||||||
|
delete: 'Could not delete the secret. Please try again.',
|
||||||
|
copyLink: 'Could not copy the secret link. Please try again.',
|
||||||
|
unlock: 'Could not unlock secrets. Check the master password and try again.'
|
||||||
|
}[action] || 'Could not complete this action. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUserFacingError(action, err) {
|
||||||
|
setStatus(userFacingError(action, err), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordWorkspaceRoot(record) {
|
||||||
|
var scope = record && record.scope || {};
|
||||||
|
return scope.kind === ScopeWorkspace ? cleanWorkspace(scope.workspaceRootPath) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceRoots() {
|
||||||
|
var seen = {};
|
||||||
|
var values = [];
|
||||||
|
workspaceOptions.concat(records.map(recordWorkspaceRoot)).forEach(function (value) {
|
||||||
|
value = cleanWorkspace(value);
|
||||||
|
if (!value || seen[value]) return;
|
||||||
|
seen[value] = true;
|
||||||
|
values.push(value);
|
||||||
|
});
|
||||||
|
return values.sort(function (a, b) { return a.localeCompare(b); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function filteredRecords() {
|
||||||
|
var query = text(searchQuery).trim().toLowerCase();
|
||||||
|
return records.filter(function (record) {
|
||||||
|
var recordWorkspace = recordWorkspaceRoot(record);
|
||||||
|
if (scopeFilter === ScopeGlobal && recordWorkspace) return false;
|
||||||
|
if (scopeFilter.indexOf(ScopeWorkspace + ':') === 0 && recordWorkspace !== scopeFilter.slice((ScopeWorkspace + ':').length)) return false;
|
||||||
|
if (!query) return true;
|
||||||
|
return [record.title, record.id, record.username, recordWorkspace].some(function (value) {
|
||||||
|
return text(value).toLowerCase().indexOf(query) !== -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHiddenSelection() {
|
||||||
|
if (!selectedRecord) return;
|
||||||
|
var visible = filteredRecords();
|
||||||
|
if (visible.some(function (record) { return record.id === selectedRecord.id; })) return;
|
||||||
|
selectedRecord = null;
|
||||||
|
selectedValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWorkspaceOptions() {
|
||||||
|
if (!api || !api.files || typeof api.files.list !== 'function') return Promise.resolve();
|
||||||
|
return api.files.list('').then(function (items) {
|
||||||
|
workspaceOptions = (Array.isArray(items) ? items : []).filter(function (item) {
|
||||||
|
return text(item && item.type).toLowerCase() === 'folder';
|
||||||
|
}).map(function (item) {
|
||||||
|
return cleanWorkspace(item.relativePath || item.name);
|
||||||
|
}).filter(function (value) {
|
||||||
|
return value && value.indexOf('/') === -1;
|
||||||
|
});
|
||||||
|
}).catch(function () {
|
||||||
|
workspaceOptions = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderLocked() {
|
function renderLocked() {
|
||||||
var passwordInput = el('input', {
|
var passwordInput = el('input', {
|
||||||
className: 'secrets-input',
|
className: 'secrets-input',
|
||||||
|
|
@ -190,7 +281,7 @@
|
||||||
return loadRecords();
|
return loadRecords();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
unlockBtn.disabled = false;
|
unlockBtn.disabled = false;
|
||||||
setStatus((err && err.message) ? err.message : String(err), true);
|
setUserFacingError('unlock', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [tr('ui.unlock', null, 'Unlock')]);
|
}, [tr('ui.unlock', null, 'Unlock')]);
|
||||||
|
|
@ -226,19 +317,48 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
|
var visibleRecords = filteredRecords();
|
||||||
|
var scopeSelect = el('select', {
|
||||||
|
className: 'secrets-select',
|
||||||
|
'data-secret-scope-filter': '',
|
||||||
|
onChange: function (event) {
|
||||||
|
scopeFilter = text(event.target.value) || 'all';
|
||||||
|
clearHiddenSelection();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
el('option', { value: 'all' }, [tr('ui.scopeAll', null, 'All scopes')]),
|
||||||
|
el('option', { value: ScopeGlobal }, [tr('ui.global', null, 'Global')])
|
||||||
|
].concat(workspaceRoots().map(function (workspace) {
|
||||||
|
return el('option', { value: ScopeWorkspace + ':' + workspace }, [workspace]);
|
||||||
|
})));
|
||||||
|
scopeSelect.value = scopeFilter;
|
||||||
|
var searchInput = el('input', {
|
||||||
|
className: 'secrets-search',
|
||||||
|
type: 'search',
|
||||||
|
'data-secret-search': '',
|
||||||
|
placeholder: tr('ui.search', null, 'Search secrets'),
|
||||||
|
value: searchQuery,
|
||||||
|
onInput: function (event) {
|
||||||
|
searchQuery = text(event.target.value);
|
||||||
|
clearHiddenSelection();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
});
|
||||||
var children = [
|
var children = [
|
||||||
el('div', { className: 'secrets-toolbar' }, [
|
el('div', { className: 'secrets-toolbar' }, [
|
||||||
el('span', { className: 'secrets-title' }, [tr('ui.title', null, 'Secrets')]),
|
el('span', { className: 'secrets-title' }, [tr('ui.title', null, 'Secrets')]),
|
||||||
el('span', { className: 'secrets-count' }, [String(records.length)]),
|
el('span', { className: 'secrets-count' }, [String(visibleRecords.length)]),
|
||||||
el('span', { className: 'secrets-spacer' }),
|
el('span', { className: 'secrets-spacer' }),
|
||||||
el('button', { className: 'secrets-btn', type: 'button', onClick: showNewSecret }, [tr('ui.new', null, 'New')])
|
el('button', { className: 'secrets-btn', type: 'button', onClick: showNewSecret }, [tr('ui.new', null, 'New')])
|
||||||
])
|
]),
|
||||||
|
el('div', { className: 'secrets-filters' }, [searchInput, scopeSelect])
|
||||||
];
|
];
|
||||||
if (!records.length) {
|
if (!visibleRecords.length) {
|
||||||
children.push(el('div', { className: 'secrets-empty' }, [tr('ui.empty', null, 'No secrets')]));
|
children.push(el('div', { className: 'secrets-empty' }, [tr('ui.empty', null, 'No secrets')]));
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
groupRecords(records).forEach(function (group) {
|
groupRecords(visibleRecords).forEach(function (group) {
|
||||||
children.push(el('div', { className: 'secrets-group' }, [group.label]));
|
children.push(el('div', { className: 'secrets-group' }, [group.label]));
|
||||||
children.push(el('div', { className: 'secrets-list' }, group.records.map(function (record) {
|
children.push(el('div', { className: 'secrets-list' }, group.records.map(function (record) {
|
||||||
var active = selectedRecord && selectedRecord.id === record.id;
|
var active = selectedRecord && selectedRecord.id === record.id;
|
||||||
|
|
@ -259,7 +379,8 @@
|
||||||
|
|
||||||
function renderSelected() {
|
function renderSelected() {
|
||||||
if (!selectedRecord) return el('div', { className: 'secrets-card' }, [
|
if (!selectedRecord) return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [tr('ui.select', null, 'Select a secret')])
|
el('h2', {}, [tr('ui.select', null, 'Select a secret')]),
|
||||||
|
el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText])
|
||||||
]);
|
]);
|
||||||
return el('div', { className: 'secrets-card' }, [
|
return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [selectedRecord.title || selectedRecord.id]),
|
el('h2', {}, [selectedRecord.title || selectedRecord.id]),
|
||||||
|
|
@ -316,9 +437,26 @@
|
||||||
value.value = isEdit ? selectedValue : '';
|
value.value = isEdit ? selectedValue : '';
|
||||||
var scope = el('select', { className: 'secrets-select' }, [
|
var scope = el('select', { className: 'secrets-select' }, [
|
||||||
el('option', { value: ScopeGlobal }, [tr('ui.global', null, 'Global')]),
|
el('option', { value: ScopeGlobal }, [tr('ui.global', null, 'Global')]),
|
||||||
el('option', { value: ScopeWorkspace }, [workspaceRoot || tr('ui.workspace', null, 'Workspace')])
|
el('option', { value: ScopeWorkspace }, [tr('ui.deal', null, 'Deal')])
|
||||||
]);
|
]);
|
||||||
|
scope.setAttribute('data-secret-scope', '');
|
||||||
|
var existingWorkspace = existing ? recordWorkspaceRoot(existing) : '';
|
||||||
|
var defaultWorkspace = existingWorkspace || workspaceRoot || workspaceRoots()[0] || '';
|
||||||
|
var workspace = el('select', { className: 'secrets-select', 'data-secret-workspace': '' }, [
|
||||||
|
el('option', { value: '' }, [tr('ui.chooseWorkspace', null, 'Choose a Deal')])
|
||||||
|
].concat(workspaceRoots().map(function (item) {
|
||||||
|
return el('option', { value: item }, [item]);
|
||||||
|
})));
|
||||||
|
workspace.value = defaultWorkspace;
|
||||||
|
var workspaceRow = el('div', { className: 'secrets-row' }, [
|
||||||
|
el('label', { className: 'secrets-label' }, [tr('ui.deal', null, 'Deal')]), workspace
|
||||||
|
]);
|
||||||
|
function updateWorkspaceVisibility() {
|
||||||
|
workspaceRow.hidden = scope.value !== ScopeWorkspace;
|
||||||
|
}
|
||||||
|
scope.addEventListener('change', updateWorkspaceVisibility);
|
||||||
scope.value = existing && existing.scope && existing.scope.kind ? existing.scope.kind : (workspaceRoot ? ScopeWorkspace : ScopeGlobal);
|
scope.value = existing && existing.scope && existing.scope.kind ? existing.scope.kind : (workspaceRoot ? ScopeWorkspace : ScopeGlobal);
|
||||||
|
updateWorkspaceVisibility();
|
||||||
return el('div', { className: 'secrets-card' }, [
|
return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [isEdit ? tr('ui.editSecret', null, 'Edit secret') : tr('ui.newSecret', null, 'New secret')]),
|
el('h2', {}, [isEdit ? tr('ui.editSecret', null, 'Edit secret') : tr('ui.newSecret', null, 'New secret')]),
|
||||||
el('div', { className: 'secrets-form' }, [
|
el('div', { className: 'secrets-form' }, [
|
||||||
|
|
@ -326,6 +464,7 @@
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['ID']), id]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['ID']), id]),
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.username', null, 'Username')]), username]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.username', null, 'Username')]), username]),
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.scope', null, 'Scope')]), scope]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.scope', null, 'Scope')]), scope]),
|
||||||
|
workspaceRow,
|
||||||
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.value', null, 'Value')]), value]),
|
el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.value', null, 'Value')]), value]),
|
||||||
el('div', { className: 'secrets-actions' }, [
|
el('div', { className: 'secrets-actions' }, [
|
||||||
el('button', {
|
el('button', {
|
||||||
|
|
@ -334,19 +473,24 @@
|
||||||
'data-secret-save': '',
|
'data-secret-save': '',
|
||||||
onClick: function () {
|
onClick: function () {
|
||||||
var nextID = text(id.value).trim() || text(title.value).trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '.').replace(/^\.+|\.+$/g, '');
|
var nextID = text(id.value).trim() || text(title.value).trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '.').replace(/^\.+|\.+$/g, '');
|
||||||
|
var targetWorkspace = cleanWorkspace(workspace.value);
|
||||||
|
if (scope.value === ScopeWorkspace && !targetWorkspace) {
|
||||||
|
setStatus(tr('ui.workspaceRequired', null, 'Choose a Deal for this secret.'), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
api.secrets.write({
|
api.secrets.write({
|
||||||
id: nextID,
|
id: nextID,
|
||||||
title: text(title.value).trim() || nextID,
|
title: text(title.value).trim() || nextID,
|
||||||
username: text(username.value).trim(),
|
username: text(username.value).trim(),
|
||||||
value: text(value.value),
|
value: text(value.value),
|
||||||
scope: scope.value === ScopeWorkspace ? { kind: ScopeWorkspace, workspaceRootPath: workspaceRoot } : { kind: ScopeGlobal }
|
scope: scope.value === ScopeWorkspace ? { kind: ScopeWorkspace, workspaceRootPath: targetWorkspace } : { kind: ScopeGlobal }
|
||||||
}).then(function (record) {
|
}).then(function (record) {
|
||||||
selectedID = record.id;
|
selectedID = record.id;
|
||||||
selectedRecord = record;
|
selectedRecord = record;
|
||||||
selectedValue = '';
|
selectedValue = '';
|
||||||
return loadRecords();
|
return loadRecords();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus((err && err.message) ? err.message : String(err), true);
|
setUserFacingError('save', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [tr('ui.save', null, 'Save')]),
|
}, [tr('ui.save', null, 'Save')]),
|
||||||
|
|
@ -381,11 +525,20 @@
|
||||||
if (wanted) {
|
if (wanted) {
|
||||||
var found = records.find(function (record) { return record.id === wanted; });
|
var found = records.find(function (record) { return record.id === wanted; });
|
||||||
if (found) return selectRecord(found.id);
|
if (found) return selectRecord(found.id);
|
||||||
|
selectedRecord = null;
|
||||||
|
selectedValue = '';
|
||||||
|
mode = 'selected';
|
||||||
|
statusText = tr('ui.requestedUnavailable', null, 'The requested secret is unavailable.');
|
||||||
|
statusError = true;
|
||||||
|
render();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
selectedRecord = records[0] || null;
|
selectedRecord = records[0] || null;
|
||||||
selectedValue = '';
|
selectedValue = '';
|
||||||
mode = 'selected';
|
mode = 'selected';
|
||||||
render();
|
render();
|
||||||
|
}).catch(function (err) {
|
||||||
|
setUserFacingError('load', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,7 +556,7 @@
|
||||||
render();
|
render();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
setStatus((err && err.message) ? err.message : String(err), true);
|
setUserFacingError('read', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,7 +583,7 @@
|
||||||
selectedValue = '';
|
selectedValue = '';
|
||||||
return loadRecords();
|
return loadRecords();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus((err && err.message) ? err.message : String(err), true);
|
setUserFacingError('delete', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,17 +593,17 @@
|
||||||
setStatus(tr('ui.linkCopied', null, 'Secret link copied'), false);
|
setStatus(tr('ui.linkCopied', null, 'Secret link copied'), false);
|
||||||
});
|
});
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus((err && err.message) ? err.message : String(err), true);
|
setUserFacingError('copyLink', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
api.secrets.status().then(function (status) {
|
api.secrets.status().then(function (status) {
|
||||||
initialized = !!(status && status.initialized);
|
initialized = !!(status && status.initialized);
|
||||||
unlocked = !!(status && status.unlocked);
|
unlocked = !!(status && status.unlocked);
|
||||||
if (unlocked) return loadRecords();
|
if (unlocked) return loadWorkspaceOptions().then(loadRecords);
|
||||||
render();
|
render();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = (err && err.message) ? err.message : String(err);
|
statusText = userFacingError('status', err);
|
||||||
statusError = true;
|
statusError = true;
|
||||||
renderLocked();
|
renderLocked();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
{
|
{
|
||||||
"manifest.name": "Secrets",
|
"manifest.name": "Secrets",
|
||||||
"manifest.description": "Encrypted global and workspace-scoped secret manager.",
|
"manifest.description": "Encrypted global and workspace-scoped secret manager.",
|
||||||
|
"contributions.views.verstak.secrets.view.title": "Secrets",
|
||||||
|
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Secrets",
|
||||||
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
||||||
"contributions.settingsPanels.verstak.secrets.settings.title": "Secrets",
|
"contributions.settingsPanels.verstak.secrets.settings.title": "Secrets",
|
||||||
"contributions.workspaceItems.verstak.secrets.workspace.title": "Secrets",
|
"contributions.workspaceItems.verstak.secrets.workspace.title": "Secrets",
|
||||||
"ui.masterPassword": "Master password",
|
"ui.masterPassword": "Master password",
|
||||||
"ui.repeatPassword": "Repeat master password",
|
"ui.repeatPassword": "Repeat master password",
|
||||||
"ui.passwordMismatch": "Master passwords do not match",
|
"ui.passwordMismatch": "Master passwords do not match",
|
||||||
|
"ui.masterPasswordMinLength": "Master password must be at least {count} characters.",
|
||||||
|
"ui.masterPasswordRequired": "Enter a master password.",
|
||||||
|
"ui.masterPasswordInvalid": "The master password is incorrect.",
|
||||||
"ui.unlock": "Unlock",
|
"ui.unlock": "Unlock",
|
||||||
"ui.createMaster": "Create master password",
|
"ui.createMaster": "Create master password",
|
||||||
"ui.password": "Password",
|
"ui.password": "Password",
|
||||||
|
|
@ -16,6 +21,7 @@
|
||||||
"ui.new": "New",
|
"ui.new": "New",
|
||||||
"ui.empty": "No secrets",
|
"ui.empty": "No secrets",
|
||||||
"ui.select": "Select a secret",
|
"ui.select": "Select a secret",
|
||||||
|
"ui.requestedUnavailable": "The requested secret is unavailable.",
|
||||||
"ui.group": "Group",
|
"ui.group": "Group",
|
||||||
"ui.username": "Username",
|
"ui.username": "Username",
|
||||||
"ui.updated": "Updated",
|
"ui.updated": "Updated",
|
||||||
|
|
@ -30,6 +36,18 @@
|
||||||
"ui.editSecret": "Edit secret",
|
"ui.editSecret": "Edit secret",
|
||||||
"ui.newSecret": "New secret",
|
"ui.newSecret": "New secret",
|
||||||
"ui.scope": "Scope",
|
"ui.scope": "Scope",
|
||||||
|
"ui.deal": "Deal",
|
||||||
|
"ui.scopeAll": "All scopes",
|
||||||
|
"ui.search": "Search secrets",
|
||||||
|
"ui.chooseWorkspace": "Choose a Deal",
|
||||||
|
"ui.workspaceRequired": "Choose a Deal for this secret.",
|
||||||
|
"ui.statusError": "Could not check the secret storage. Please try again.",
|
||||||
|
"ui.loadError": "Could not load secrets. Please try again.",
|
||||||
|
"ui.readError": "Could not open this secret. Please try again.",
|
||||||
|
"ui.saveError": "Could not save the secret. Please try again.",
|
||||||
|
"ui.deleteError": "Could not delete the secret. Please try again.",
|
||||||
|
"ui.copyLinkError": "Could not copy the secret link. Please try again.",
|
||||||
|
"ui.unlockError": "Could not unlock secrets. Check the master password and try again.",
|
||||||
"ui.value": "Value",
|
"ui.value": "Value",
|
||||||
"ui.save": "Save",
|
"ui.save": "Save",
|
||||||
"ui.cancel": "Cancel",
|
"ui.cancel": "Cancel",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
{
|
{
|
||||||
"manifest.name": "Секреты",
|
"manifest.name": "Секреты",
|
||||||
"manifest.description": "Зашифрованное хранилище общих секретов и секретов рабочих пространств.",
|
"manifest.description": "Зашифрованное хранилище общих секретов и секретов рабочих пространств.",
|
||||||
|
"contributions.views.verstak.secrets.view.title": "Секреты",
|
||||||
|
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Секреты",
|
||||||
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
||||||
"contributions.settingsPanels.verstak.secrets.settings.title": "Секреты",
|
"contributions.settingsPanels.verstak.secrets.settings.title": "Секреты",
|
||||||
"contributions.workspaceItems.verstak.secrets.workspace.title": "Секреты",
|
"contributions.workspaceItems.verstak.secrets.workspace.title": "Секреты",
|
||||||
"ui.masterPassword": "Мастер-пароль",
|
"ui.masterPassword": "Мастер-пароль",
|
||||||
"ui.repeatPassword": "Повторите мастер-пароль",
|
"ui.repeatPassword": "Повторите мастер-пароль",
|
||||||
"ui.passwordMismatch": "Мастер-пароли не совпадают",
|
"ui.passwordMismatch": "Мастер-пароли не совпадают",
|
||||||
|
"ui.masterPasswordMinLength": "Мастер-пароль должен содержать не менее {count} символов.",
|
||||||
|
"ui.masterPasswordRequired": "Введите мастер-пароль.",
|
||||||
|
"ui.masterPasswordInvalid": "Мастер-пароль указан неверно.",
|
||||||
"ui.unlock": "Разблокировать",
|
"ui.unlock": "Разблокировать",
|
||||||
"ui.createMaster": "Создать мастер-пароль",
|
"ui.createMaster": "Создать мастер-пароль",
|
||||||
"ui.password": "Пароль",
|
"ui.password": "Пароль",
|
||||||
|
|
@ -16,6 +21,7 @@
|
||||||
"ui.new": "Новый",
|
"ui.new": "Новый",
|
||||||
"ui.empty": "Секретов нет",
|
"ui.empty": "Секретов нет",
|
||||||
"ui.select": "Выберите секрет",
|
"ui.select": "Выберите секрет",
|
||||||
|
"ui.requestedUnavailable": "Запрошенный секрет недоступен.",
|
||||||
"ui.group": "Группа",
|
"ui.group": "Группа",
|
||||||
"ui.username": "Имя пользователя",
|
"ui.username": "Имя пользователя",
|
||||||
"ui.updated": "Обновлён",
|
"ui.updated": "Обновлён",
|
||||||
|
|
@ -30,6 +36,18 @@
|
||||||
"ui.editSecret": "Изменить секрет",
|
"ui.editSecret": "Изменить секрет",
|
||||||
"ui.newSecret": "Новый секрет",
|
"ui.newSecret": "Новый секрет",
|
||||||
"ui.scope": "Область",
|
"ui.scope": "Область",
|
||||||
|
"ui.deal": "Дело",
|
||||||
|
"ui.scopeAll": "Все области",
|
||||||
|
"ui.search": "Поиск секретов",
|
||||||
|
"ui.chooseWorkspace": "Выберите дело",
|
||||||
|
"ui.workspaceRequired": "Выберите дело для этого секрета.",
|
||||||
|
"ui.statusError": "Не удалось проверить хранилище секретов. Повторите попытку.",
|
||||||
|
"ui.loadError": "Не удалось загрузить секреты. Повторите попытку.",
|
||||||
|
"ui.readError": "Не удалось открыть секрет. Повторите попытку.",
|
||||||
|
"ui.saveError": "Не удалось сохранить секрет. Повторите попытку.",
|
||||||
|
"ui.deleteError": "Не удалось удалить секрет. Повторите попытку.",
|
||||||
|
"ui.copyLinkError": "Не удалось скопировать ссылку на секрет. Повторите попытку.",
|
||||||
|
"ui.unlockError": "Не удалось разблокировать секреты. Проверьте мастер-пароль и повторите попытку.",
|
||||||
"ui.value": "Значение",
|
"ui.value": "Значение",
|
||||||
"ui.save": "Сохранить",
|
"ui.save": "Сохранить",
|
||||||
"ui.cancel": "Отмена",
|
"ui.cancel": "Отмена",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"secrets.write-ui"
|
"secrets.write-ui"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
"files.read",
|
||||||
"secrets.read",
|
"secrets.read",
|
||||||
"secrets.write",
|
"secrets.write",
|
||||||
"ui.register"
|
"ui.register"
|
||||||
|
|
@ -22,6 +23,23 @@
|
||||||
"entry": "frontend/src/index.js"
|
"entry": "frontend/src/index.js"
|
||||||
},
|
},
|
||||||
"contributes": {
|
"contributes": {
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"id": "verstak.secrets.view",
|
||||||
|
"title": "Secrets",
|
||||||
|
"icon": "key-round",
|
||||||
|
"component": "SecretsView"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sidebarItems": [
|
||||||
|
{
|
||||||
|
"id": "verstak.secrets.sidebar",
|
||||||
|
"title": "Secrets",
|
||||||
|
"icon": "key-round",
|
||||||
|
"view": "verstak.secrets.view",
|
||||||
|
"position": 45
|
||||||
|
}
|
||||||
|
],
|
||||||
"openProviders": [
|
"openProviders": [
|
||||||
{
|
{
|
||||||
"id": "verstak.secrets.secret",
|
"id": "verstak.secrets.secret",
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,9 @@
|
||||||
const INPUT_STYLE = 'width:100%;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;box-sizing:border-box;height:36px;'
|
const INPUT_STYLE = 'width:100%;background:#0f3460;border:1px solid #1a3a5c;color:#e0e0f0;padding:8px 10px;border-radius:4px;font-size:0.85rem;box-sizing:border-box;height:36px;'
|
||||||
const INPUT_FOCUS_STYLE = INPUT_STYLE + 'outline:none;border-color:#4ecca3;'
|
const INPUT_FOCUS_STYLE = INPUT_STYLE + 'outline:none;border-color:#4ecca3;'
|
||||||
|
|
||||||
function sanitizeError(msg) {
|
function reportError(key, fallback, error) {
|
||||||
if (!msg) return tr('ui.unknownError', null, 'Unknown error')
|
console.warn('[verstak.sync] operation failed:', error)
|
||||||
let s = String(msg).replace(/<[^>]+>/g, '')
|
return tr(key, null, fallback)
|
||||||
if (s.length > 200) s = s.substring(0, 200) + '...'
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncAPI() {
|
function syncAPI() {
|
||||||
|
|
@ -51,15 +49,21 @@
|
||||||
syncInterval = saved.syncInterval || 5
|
syncInterval = saved.syncInterval || 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (error) {
|
||||||
|
console.warn('[verstak.sync] settings load failed:', error)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
settings = await syncAPI().status()
|
settings = await syncAPI().status()
|
||||||
if (settings) {
|
if (settings) {
|
||||||
if (settings.serverUrl) serverUrl = settings.serverUrl
|
if (settings.serverUrl) serverUrl = settings.serverUrl
|
||||||
if (settings.syncInterval != null) syncInterval = settings.syncInterval
|
if (settings.syncInterval != null) syncInterval = settings.syncInterval
|
||||||
if (settings.syncInterval > 0) autoSync = true
|
if (settings.syncInterval > 0) autoSync = true
|
||||||
|
if (settings.lastError) console.warn('[verstak.sync] last sync failed:', settings.lastError)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[verstak.sync] status load failed:', error)
|
||||||
|
settings = null
|
||||||
}
|
}
|
||||||
} catch (_) { settings = null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
load()
|
load()
|
||||||
|
|
@ -80,7 +84,7 @@
|
||||||
resultMsg = tr('ui.settingsSaved', null, 'Settings saved.')
|
resultMsg = tr('ui.settingsSaved', null, 'Settings saved.')
|
||||||
resultKind = ''
|
resultKind = ''
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg = sanitizeError(e.message || e)
|
errorMsg = reportError('ui.saveFailed', 'Could not save sync settings. Please try again.', e)
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +101,7 @@
|
||||||
connectionResult = tr('ui.connectionSuccessful', null, 'Connection successful.')
|
connectionResult = tr('ui.connectionSuccessful', null, 'Connection successful.')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
connectionOk = false
|
connectionOk = false
|
||||||
connectionResult = tr('ui.connectionFailed', { error: sanitizeError(e.message || e) }, 'Connection failed: {error}')
|
connectionResult = reportError('ui.connectionFailed', 'Could not connect. Check the server address and credentials.', e)
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +119,7 @@
|
||||||
password = ''
|
password = ''
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg = sanitizeError(e.message || e)
|
errorMsg = reportError('ui.connectFailed', 'Could not connect this device. Please try again.', e)
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -129,23 +133,8 @@
|
||||||
return parts.join(' · ')
|
return parts.join(' · ')
|
||||||
}
|
}
|
||||||
|
|
||||||
function conflictField(conflict, keys) {
|
function formatSyncConflict() {
|
||||||
for (const key of keys) {
|
return tr('ui.syncConflictItem', null, 'A synchronization conflict needs attention.')
|
||||||
const value = conflict && conflict[key]
|
|
||||||
if (value != null && String(value).trim()) return String(value)
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSyncConflict(conflict) {
|
|
||||||
const entityType = conflictField(conflict, ['entity_type', 'entityType']) || 'item'
|
|
||||||
const entityId = conflictField(conflict, ['entity_id', 'entityId', 'path']) || 'unknown'
|
|
||||||
const opId = conflictField(conflict, ['op_id', 'opId'])
|
|
||||||
const reason = conflictField(conflict, ['reason', 'message'])
|
|
||||||
const parts = [entityType + ': ' + entityId]
|
|
||||||
if (opId) parts.push('op ' + opId)
|
|
||||||
if (reason) parts.push(reason)
|
|
||||||
return parts.join(' · ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSyncNow() {
|
async function runSyncNow() {
|
||||||
|
|
@ -163,7 +152,7 @@
|
||||||
resultKind = warning ? 'warning' : ''
|
resultKind = warning ? 'warning' : ''
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg = sanitizeError(e.message || e)
|
errorMsg = reportError('ui.syncFailed', 'Could not synchronize. Please try again.', e)
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -194,7 +183,7 @@
|
||||||
settings = null
|
settings = null
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg = sanitizeError(e.message || e)
|
errorMsg = reportError('ui.disconnectFailed', 'Could not disconnect from the server. Please try again.', e)
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +198,7 @@
|
||||||
resultKind = ''
|
resultKind = ''
|
||||||
await load()
|
await load()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMsg = sanitizeError(e.message || e)
|
errorMsg = reportError('ui.resetKeyFailed', 'Could not reset the sync key. Please try again.', e)
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
@ -243,7 +232,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if settings && settings.lastError && !errorMsg}
|
{#if settings && settings.lastError && !errorMsg}
|
||||||
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">
|
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:6px;color:#ff6b6b;font-size:0.85rem;">
|
||||||
{tr('ui.lastSyncError', { error: sanitizeError(settings.lastError) }, 'Last sync error: {error}')}
|
{tr('ui.lastSyncError', null, 'The last synchronization did not finish. Try again.')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,17 @@
|
||||||
"ui.title": "Sync",
|
"ui.title": "Sync",
|
||||||
"ui.description": "Synchronize your vault across devices.",
|
"ui.description": "Synchronize your vault across devices.",
|
||||||
"ui.unknownError": "Unknown error",
|
"ui.unknownError": "Unknown error",
|
||||||
"ui.apiUnavailable": "Plugin API sync namespace not available",
|
"ui.apiUnavailable": "Synchronization is unavailable right now.",
|
||||||
"ui.intervalError": "Sync interval must be between 1 and 1440 minutes.",
|
"ui.intervalError": "Sync interval must be between 1 and 1440 minutes.",
|
||||||
"ui.settingsSaved": "Settings saved.",
|
"ui.settingsSaved": "Settings saved.",
|
||||||
"ui.serverRequired": "Server URL is required.",
|
"ui.serverRequired": "Server URL is required.",
|
||||||
"ui.connectionSuccessful": "Connection successful.",
|
"ui.connectionSuccessful": "Connection successful.",
|
||||||
"ui.connectionFailed": "Connection failed: {error}",
|
"ui.connectionFailed": "Could not connect. Check the server address and credentials.",
|
||||||
|
"ui.saveFailed": "Could not save sync settings. Please try again.",
|
||||||
|
"ui.connectFailed": "Could not connect this device. Please try again.",
|
||||||
|
"ui.syncFailed": "Could not synchronize. Please try again.",
|
||||||
|
"ui.disconnectFailed": "Could not disconnect from the server. Please try again.",
|
||||||
|
"ui.resetKeyFailed": "Could not reset the sync key. Please try again.",
|
||||||
"ui.connectedSuccessfully": "Connected successfully.",
|
"ui.connectedSuccessfully": "Connected successfully.",
|
||||||
"ui.conflictsCount": "{count} conflict(s)",
|
"ui.conflictsCount": "{count} conflict(s)",
|
||||||
"ui.errorsCount": "{count} error(s)",
|
"ui.errorsCount": "{count} error(s)",
|
||||||
|
|
@ -18,7 +23,8 @@
|
||||||
"ui.disconnected": "Disconnected from server.",
|
"ui.disconnected": "Disconnected from server.",
|
||||||
"ui.keyReset": "Sync key reset. Connect again to pair this device.",
|
"ui.keyReset": "Sync key reset. Connect again to pair this device.",
|
||||||
"ui.syncConflicts": "Sync conflicts",
|
"ui.syncConflicts": "Sync conflicts",
|
||||||
"ui.lastSyncError": "Last sync error: {error}",
|
"ui.lastSyncError": "The last synchronization did not finish. Try again.",
|
||||||
|
"ui.syncConflictItem": "A synchronization conflict needs attention.",
|
||||||
"ui.server": "Server",
|
"ui.server": "Server",
|
||||||
"ui.serverUrl": "Server URL",
|
"ui.serverUrl": "Server URL",
|
||||||
"ui.username": "Username",
|
"ui.username": "Username",
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,17 @@
|
||||||
"ui.title": "Синхронизация",
|
"ui.title": "Синхронизация",
|
||||||
"ui.description": "Синхронизируйте хранилище между устройствами.",
|
"ui.description": "Синхронизируйте хранилище между устройствами.",
|
||||||
"ui.unknownError": "Неизвестная ошибка",
|
"ui.unknownError": "Неизвестная ошибка",
|
||||||
"ui.apiUnavailable": "API синхронизации плагина недоступен",
|
"ui.apiUnavailable": "Синхронизация сейчас недоступна.",
|
||||||
"ui.intervalError": "Интервал синхронизации должен быть от 1 до 1440 минут.",
|
"ui.intervalError": "Интервал синхронизации должен быть от 1 до 1440 минут.",
|
||||||
"ui.settingsSaved": "Настройки сохранены.",
|
"ui.settingsSaved": "Настройки сохранены.",
|
||||||
"ui.serverRequired": "Укажите URL сервера.",
|
"ui.serverRequired": "Укажите URL сервера.",
|
||||||
"ui.connectionSuccessful": "Подключение успешно.",
|
"ui.connectionSuccessful": "Подключение успешно.",
|
||||||
"ui.connectionFailed": "Не удалось подключиться: {error}",
|
"ui.connectionFailed": "Не удалось подключиться. Проверьте адрес сервера и учётные данные.",
|
||||||
|
"ui.saveFailed": "Не удалось сохранить настройки синхронизации. Повторите попытку.",
|
||||||
|
"ui.connectFailed": "Не удалось подключить это устройство. Повторите попытку.",
|
||||||
|
"ui.syncFailed": "Не удалось синхронизировать данные. Повторите попытку.",
|
||||||
|
"ui.disconnectFailed": "Не удалось отключиться от сервера. Повторите попытку.",
|
||||||
|
"ui.resetKeyFailed": "Не удалось сбросить ключ синхронизации. Повторите попытку.",
|
||||||
"ui.connectedSuccessfully": "Подключение установлено.",
|
"ui.connectedSuccessfully": "Подключение установлено.",
|
||||||
"ui.conflictsCount": "Конфликтов: {count}",
|
"ui.conflictsCount": "Конфликтов: {count}",
|
||||||
"ui.errorsCount": "Ошибок: {count}",
|
"ui.errorsCount": "Ошибок: {count}",
|
||||||
|
|
@ -18,7 +23,8 @@
|
||||||
"ui.disconnected": "Сервер отключён.",
|
"ui.disconnected": "Сервер отключён.",
|
||||||
"ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.",
|
"ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.",
|
||||||
"ui.syncConflicts": "Конфликты синхронизации",
|
"ui.syncConflicts": "Конфликты синхронизации",
|
||||||
"ui.lastSyncError": "Ошибка последней синхронизации: {error}",
|
"ui.lastSyncError": "Последняя синхронизация не завершилась. Повторите попытку.",
|
||||||
|
"ui.syncConflictItem": "Конфликт синхронизации требует внимания.",
|
||||||
"ui.server": "Сервер",
|
"ui.server": "Сервер",
|
||||||
"ui.serverUrl": "URL сервера",
|
"ui.serverUrl": "URL сервера",
|
||||||
"ui.username": "Имя пользователя",
|
"ui.username": "Имя пользователя",
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
'.todo-title{font-size:.86rem;font-weight:600}.todo-count,.todo-status,.todo-scope{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3)}.todo-spacer{flex:1}',
|
'.todo-title{font-size:.86rem;font-weight:600}.todo-count,.todo-status,.todo-scope{font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3)}.todo-spacer{flex:1}',
|
||||||
'.todo-filters{display:flex;align-items:center;gap:.35rem;min-width:0;flex:1;flex-wrap:wrap}',
|
'.todo-filters{display:flex;align-items:center;gap:.35rem;min-width:0;flex:1;flex-wrap:wrap}',
|
||||||
'.todo-input,.todo-select{box-sizing:border-box;min-height:1.9rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);color-scheme:dark;font:inherit;font-size:.78rem;padding:.32rem .45rem}',
|
'.todo-input,.todo-select{box-sizing:border-box;min-height:1.9rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-sm,4px);background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb);color-scheme:dark;font:inherit;font-size:.78rem;padding:.32rem .45rem}',
|
||||||
'.todo-input.search{width:min(15rem,100%)}.todo-input.textarea{min-height:6.5rem;resize:vertical;line-height:1.4}.todo-select{max-width:12rem;appearance:none;background-color:var(--vt-color-surface,#15152c);background-image:linear-gradient(45deg,transparent 50%,var(--vt-color-text-muted,#7f8aa3) 50%),linear-gradient(135deg,var(--vt-color-text-muted,#7f8aa3) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.7rem}.todo-select option{background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb)}',
|
'.todo-input.search{width:min(15rem,100%)}.todo-input.textarea{min-height:6.5rem;resize:vertical;line-height:1.4}.todo-input.todo-time-input{width:100%;font-variant-numeric:tabular-nums}.todo-select{max-width:12rem;appearance:none;background-color:var(--vt-color-surface,#15152c);background-image:linear-gradient(45deg,transparent 50%,var(--vt-color-text-muted,#7f8aa3) 50%),linear-gradient(135deg,var(--vt-color-text-muted,#7f8aa3) 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.7rem}.todo-select option{background:var(--vt-color-surface,#15152c);color:var(--vt-color-text-primary,#f4f7fb)}',
|
||||||
'.todo-btn{min-height:1.9rem;padding:.32rem .62rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);background:var(--vt-color-surface-hover,#1b2440);color:var(--vt-color-text-secondary,#b7c0d4);font-size:.78rem;cursor:pointer}.todo-btn:hover{border-color:var(--vt-color-accent,#4ecca3);color:var(--vt-color-text-primary,#f4f7fb)}.todo-btn.primary{background:var(--vt-color-accent,#4ecca3);border-color:var(--vt-color-accent,#4ecca3);color:#101827}.todo-btn.danger{border-color:rgba(233,69,96,.5);color:#ff9a9a}.todo-btn:disabled{opacity:.45;cursor:default}',
|
'.todo-btn{min-height:1.9rem;padding:.32rem .62rem;border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);background:var(--vt-color-surface-hover,#1b2440);color:var(--vt-color-text-secondary,#b7c0d4);font-size:.78rem;cursor:pointer}.todo-btn:hover{border-color:var(--vt-color-accent,#4ecca3);color:var(--vt-color-text-primary,#f4f7fb)}.todo-btn.primary{background:var(--vt-color-accent,#4ecca3);border-color:var(--vt-color-accent,#4ecca3);color:#101827}.todo-btn.danger{border-color:rgba(233,69,96,.5);color:#ff9a9a}.todo-btn:disabled{opacity:.45;cursor:default}',
|
||||||
'.todo-list{flex:1;min-height:0;overflow:auto;padding:.5rem .75rem .85rem}.todo-empty{height:100%;display:flex;align-items:center;justify-content:center;padding:2rem;text-align:center;color:var(--vt-color-text-muted,#7f8aa3);font-size:.86rem}',
|
'.todo-list{flex:1;min-height:0;overflow:auto;padding:.5rem .75rem .85rem}.todo-empty{height:100%;display:flex;align-items:center;justify-content:center;padding:2rem;text-align:center;color:var(--vt-color-text-muted,#7f8aa3);font-size:.86rem}',
|
||||||
'.todo-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.75rem;align-items:start;margin-top:.5rem;padding:.75rem .85rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c)}.todo-row:hover{background:var(--vt-color-surface-hover,#1b2440)}.todo-row.done .todo-row-title{text-decoration:line-through;color:var(--vt-color-text-muted,#7f8aa3)}',
|
'.todo-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.75rem;align-items:start;margin-top:.5rem;padding:.75rem .85rem;border:1px solid var(--vt-color-border,#202b46);border-radius:var(--vt-radius-lg,8px);background:var(--vt-color-surface,#15152c)}.todo-row:hover{background:var(--vt-color-surface-hover,#1b2440)}.todo-row.done .todo-row-title{text-decoration:line-through;color:var(--vt-color-text-muted,#7f8aa3)}',
|
||||||
|
|
@ -132,10 +132,19 @@
|
||||||
return match ? { date: match[1], time: match[2] } : { date: '', time: '' };
|
return match ? { date: match[1], time: match[2] } : { date: '', time: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanReminderTime(value) {
|
||||||
|
var match = /^(\d{1,2}):(\d{2})$/.exec(text(value).trim());
|
||||||
|
if (!match) return '';
|
||||||
|
var hour = Number(match[1]);
|
||||||
|
var minute = Number(match[2]);
|
||||||
|
if (hour > 23 || minute > 59) return '';
|
||||||
|
return String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
function joinReminderDateTime(date, time) {
|
function joinReminderDateTime(date, time) {
|
||||||
date = cleanDate(date);
|
date = cleanDate(date);
|
||||||
time = text(time).trim();
|
time = cleanReminderTime(time);
|
||||||
if (!date || !/^\d{2}:\d{2}$/.test(time)) return '';
|
if (!date || !time) return '';
|
||||||
return cleanDateTime(date + 'T' + time);
|
return cleanDateTime(date + 'T' + time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +155,8 @@
|
||||||
function normalizeTodo(value) {
|
function normalizeTodo(value) {
|
||||||
value = value || {};
|
value = value || {};
|
||||||
var status = cleanStatus(value.status);
|
var status = cleanStatus(value.status);
|
||||||
|
var reminderAt = cleanDateTime(value.reminderAt || value.reminderDateTime);
|
||||||
|
var reminder = splitReminderDateTime(reminderAt);
|
||||||
var createdAt = text(value.createdAt).trim() || now();
|
var createdAt = text(value.createdAt).trim() || now();
|
||||||
var completedAt = status === 'done' ? (text(value.completedAt).trim() || createdAt) : '';
|
var completedAt = status === 'done' ? (text(value.completedAt).trim() || createdAt) : '';
|
||||||
return {
|
return {
|
||||||
|
|
@ -157,7 +168,8 @@
|
||||||
status: status,
|
status: status,
|
||||||
priority: cleanPriority(value.priority),
|
priority: cleanPriority(value.priority),
|
||||||
dueAt: cleanDate(value.dueAt || value.dueDate),
|
dueAt: cleanDate(value.dueAt || value.dueDate),
|
||||||
reminderAt: cleanDateTime(value.reminderAt || value.reminderDateTime),
|
reminderDate: cleanDate(value.reminderDate) || reminder.date,
|
||||||
|
reminderAt: reminderAt,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
updatedAt: text(value.updatedAt).trim() || createdAt,
|
updatedAt: text(value.updatedAt).trim() || createdAt,
|
||||||
completedAt: completedAt,
|
completedAt: completedAt,
|
||||||
|
|
@ -186,6 +198,7 @@
|
||||||
status: todo.status,
|
status: todo.status,
|
||||||
priority: todo.priority,
|
priority: todo.priority,
|
||||||
dueAt: todo.dueAt,
|
dueAt: todo.dueAt,
|
||||||
|
reminderDate: todo.reminderDate,
|
||||||
reminderAt: todo.reminderAt,
|
reminderAt: todo.reminderAt,
|
||||||
createdAt: todo.createdAt,
|
createdAt: todo.createdAt,
|
||||||
updatedAt: todo.updatedAt,
|
updatedAt: todo.updatedAt,
|
||||||
|
|
@ -389,11 +402,18 @@
|
||||||
}).filter(function (item) { return item !== null; });
|
}).filter(function (item) { return item !== null; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reportError(key, fallback, err) {
|
||||||
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
|
console.warn('[verstak.todo] ' + key, err);
|
||||||
|
}
|
||||||
|
statusText = tr(key, null, fallback);
|
||||||
|
statusClass = 'error';
|
||||||
|
}
|
||||||
|
|
||||||
function syncNotifications() {
|
function syncNotifications() {
|
||||||
if (!api || !api.notifications || typeof api.notifications.replace !== 'function') return Promise.resolve();
|
if (!api || !api.notifications || typeof api.notifications.replace !== 'function') return Promise.resolve();
|
||||||
return api.notifications.replace(notificationRequests()).catch(function (err) {
|
return api.notifications.replace(notificationRequests()).catch(function (err) {
|
||||||
statusText = tr('ui.notificationError', { error: err && err.message ? err.message : String(err) }, 'Could not schedule reminders: ' + (err && err.message ? err.message : String(err)));
|
reportError('ui.notificationError', 'Could not schedule reminders. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,8 +422,7 @@
|
||||||
return api.settings.write(GLOBAL_KEY, storageTodos(sortTodos(todos))).then(function () {
|
return api.settings.write(GLOBAL_KEY, storageTodos(sortTodos(todos))).then(function () {
|
||||||
return syncNotifications();
|
return syncNotifications();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save todos: ' + (err && err.message ? err.message : String(err)));
|
reportError('ui.saveError', 'Could not save tasks. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,8 +432,7 @@
|
||||||
todos = sortTodos(normalizeTodos((settings || {})[GLOBAL_KEY]));
|
todos = sortTodos(normalizeTodos((settings || {})[GLOBAL_KEY]));
|
||||||
return syncNotifications();
|
return syncNotifications();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load todos: ' + (err && err.message ? err.message : String(err)));
|
reportError('ui.loadError', 'Could not load tasks. Please try again.', err);
|
||||||
statusClass = 'error';
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,8 +465,8 @@
|
||||||
priorityInput.value = editing ? existingTodo.priority : 'normal';
|
priorityInput.value = editing ? existingTodo.priority : 'normal';
|
||||||
var dueInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.dueAt : '', 'data-todo-input': 'dueAt' });
|
var dueInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.dueAt : '', 'data-todo-input': 'dueAt' });
|
||||||
var reminder = splitReminderDateTime(editing ? existingTodo.reminderAt : '');
|
var reminder = splitReminderDateTime(editing ? existingTodo.reminderAt : '');
|
||||||
var reminderDateInput = el('input', { className: 'todo-input', type: 'date', value: reminder.date, 'data-todo-input': 'reminderDate' });
|
var reminderDateInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.reminderDate || reminder.date : '', 'data-todo-input': 'reminderDate' });
|
||||||
var reminderTimeInput = el('input', { className: 'todo-input', type: 'time', value: reminder.time, 'data-todo-input': 'reminderTime' });
|
var reminderTimeInput = el('input', { className: 'todo-input todo-time-input', type: 'text', inputmode: 'numeric', maxlength: '5', placeholder: tr('ui.reminderTimePlaceholder', null, '14:30'), value: reminder.time, 'data-todo-input': 'reminderTime', 'aria-label': tr('ui.field.reminderTime', null, 'Reminder time') });
|
||||||
var workspaceInput = null;
|
var workspaceInput = null;
|
||||||
var workspace = editing ? existingTodo.workspaceRootPath : scope.workspaceRoot;
|
var workspace = editing ? existingTodo.workspaceRootPath : scope.workspaceRoot;
|
||||||
if (scope.mode === 'global') {
|
if (scope.mode === 'global') {
|
||||||
|
|
@ -469,6 +487,20 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var workspaceRoot = scope.mode === 'workspace' ? scope.workspaceRoot : cleanWorkspace(workspaceInput && workspaceInput.value);
|
var workspaceRoot = scope.mode === 'workspace' ? scope.workspaceRoot : cleanWorkspace(workspaceInput && workspaceInput.value);
|
||||||
|
var reminderDate = cleanDate(reminderDateInput.value);
|
||||||
|
var reminderTime = cleanReminderTime(reminderTimeInput.value);
|
||||||
|
if (text(reminderTimeInput.value).trim() && !reminderTime) {
|
||||||
|
statusText = tr('ui.reminderTimeInvalid', null, 'Enter a valid reminder time (HH:MM)');
|
||||||
|
statusClass = 'error';
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reminderTime && !reminderDate) {
|
||||||
|
statusText = tr('ui.reminderDateRequired', null, 'Choose a reminder date before setting its time');
|
||||||
|
statusClass = 'error';
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
var timestamp = now();
|
var timestamp = now();
|
||||||
var next = normalizeTodo({
|
var next = normalizeTodo({
|
||||||
id: editing ? existingTodo.id : todoId(workspaceRoot, title),
|
id: editing ? existingTodo.id : todoId(workspaceRoot, title),
|
||||||
|
|
@ -479,7 +511,8 @@
|
||||||
status: editing ? existingTodo.status : 'open',
|
status: editing ? existingTodo.status : 'open',
|
||||||
priority: priorityInput.value,
|
priority: priorityInput.value,
|
||||||
dueAt: dueInput.value,
|
dueAt: dueInput.value,
|
||||||
reminderAt: joinReminderDateTime(reminderDateInput.value, reminderTimeInput.value),
|
reminderDate: reminderDate,
|
||||||
|
reminderAt: joinReminderDateTime(reminderDate, reminderTime),
|
||||||
createdAt: editing ? existingTodo.createdAt : timestamp,
|
createdAt: editing ? existingTodo.createdAt : timestamp,
|
||||||
updatedAt: timestamp,
|
updatedAt: timestamp,
|
||||||
completedAt: editing ? existingTodo.completedAt : '',
|
completedAt: editing ? existingTodo.completedAt : '',
|
||||||
|
|
@ -586,6 +619,7 @@
|
||||||
meta.push(el('span', { className: 'todo-badge', textContent: tr('ui.status.' + todo.status, null, todo.status) }));
|
meta.push(el('span', { className: 'todo-badge', textContent: tr('ui.status.' + todo.status, null, todo.status) }));
|
||||||
if (todo.dueAt) meta.push(el('span', { className: 'todo-badge ' + due, textContent: tr('ui.dueValue', { prefix: due === 'overdue' ? tr('ui.overduePrefix', null, 'Overdue · ') : (due === 'due-soon' ? tr('ui.dueSoonPrefix', null, 'Due soon · ') : ''), date: formatDate(todo.dueAt) }, (due === 'overdue' ? 'Overdue · ' : (due === 'due-soon' ? 'Due soon · ' : '')) + 'Due ' + formatDate(todo.dueAt)) }));
|
if (todo.dueAt) meta.push(el('span', { className: 'todo-badge ' + due, textContent: tr('ui.dueValue', { prefix: due === 'overdue' ? tr('ui.overduePrefix', null, 'Overdue · ') : (due === 'due-soon' ? tr('ui.dueSoonPrefix', null, 'Due soon · ') : ''), date: formatDate(todo.dueAt) }, (due === 'overdue' ? 'Overdue · ' : (due === 'due-soon' ? 'Due soon · ' : '')) + 'Due ' + formatDate(todo.dueAt)) }));
|
||||||
if (todo.reminderAt) meta.push(el('span', { className: 'todo-badge ' + (reminderDue ? 'reminder-due' : ''), textContent: tr(reminderDue ? 'ui.reminderDueValue' : 'ui.reminderValue', { date: formatDate(todo.reminderAt) }, (reminderDue ? 'Reminder due ' : 'Reminder ') + formatDate(todo.reminderAt)) }));
|
if (todo.reminderAt) meta.push(el('span', { className: 'todo-badge ' + (reminderDue ? 'reminder-due' : ''), textContent: tr(reminderDue ? 'ui.reminderDueValue' : 'ui.reminderValue', { date: formatDate(todo.reminderAt) }, (reminderDue ? 'Reminder due ' : 'Reminder ') + formatDate(todo.reminderAt)) }));
|
||||||
|
else if (todo.reminderDate) meta.push(el('span', { className: 'todo-badge', textContent: tr('ui.reminderDateValue', { date: formatDate(todo.reminderDate) }, 'Reminder date ' + formatDate(todo.reminderDate)) }));
|
||||||
return el('div', { className: 'todo-row-meta' }, meta);
|
return el('div', { className: 'todo-row-meta' }, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
"ui.add": "Add Todo",
|
"ui.add": "Add Todo",
|
||||||
"ui.allWorkspaces": "All workspaces",
|
"ui.allWorkspaces": "All workspaces",
|
||||||
"ui.unassigned": "Unassigned",
|
"ui.unassigned": "Unassigned",
|
||||||
"ui.saveError": "Could not save todos: {error}",
|
"ui.saveError": "Could not save tasks. Please try again.",
|
||||||
"ui.loadError": "Could not load todos: {error}",
|
"ui.loadError": "Could not load tasks. Please try again.",
|
||||||
"ui.notificationError": "Could not schedule reminders: {error}",
|
"ui.notificationError": "Could not schedule reminders. Please try again.",
|
||||||
"ui.notificationTitle": "Todo reminder",
|
"ui.notificationTitle": "Todo reminder",
|
||||||
"ui.notificationBody": "{title}",
|
"ui.notificationBody": "{title}",
|
||||||
"ui.titlePlaceholder": "Todo title",
|
"ui.titlePlaceholder": "Todo title",
|
||||||
|
|
@ -37,6 +37,9 @@
|
||||||
"ui.field.reminder": "Reminder",
|
"ui.field.reminder": "Reminder",
|
||||||
"ui.field.reminderDate": "Reminder date",
|
"ui.field.reminderDate": "Reminder date",
|
||||||
"ui.field.reminderTime": "Reminder time",
|
"ui.field.reminderTime": "Reminder time",
|
||||||
|
"ui.reminderTimePlaceholder": "14:30",
|
||||||
|
"ui.reminderTimeInvalid": "Enter a valid reminder time (HH:MM)",
|
||||||
|
"ui.reminderDateRequired": "Choose a reminder date before setting its time",
|
||||||
"ui.field.workspace": "Workspace",
|
"ui.field.workspace": "Workspace",
|
||||||
"ui.workspaceValue": "Workspace: {workspace}",
|
"ui.workspaceValue": "Workspace: {workspace}",
|
||||||
"ui.edit": "Edit Todo",
|
"ui.edit": "Edit Todo",
|
||||||
|
|
@ -52,6 +55,7 @@
|
||||||
"ui.dueValue": "{prefix}Due {date}",
|
"ui.dueValue": "{prefix}Due {date}",
|
||||||
"ui.reminderDueValue": "Reminder due {date}",
|
"ui.reminderDueValue": "Reminder due {date}",
|
||||||
"ui.reminderValue": "Reminder {date}",
|
"ui.reminderValue": "Reminder {date}",
|
||||||
|
"ui.reminderDateValue": "Reminder date {date}",
|
||||||
"ui.noMatches": "No todos match the current filters.",
|
"ui.noMatches": "No todos match the current filters.",
|
||||||
"ui.empty": "No todos yet.",
|
"ui.empty": "No todos yet.",
|
||||||
"ui.openWorkspace": "Open workspace",
|
"ui.openWorkspace": "Open workspace",
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
"ui.add": "Добавить задачу",
|
"ui.add": "Добавить задачу",
|
||||||
"ui.allWorkspaces": "Все рабочие пространства",
|
"ui.allWorkspaces": "Все рабочие пространства",
|
||||||
"ui.unassigned": "Не назначено",
|
"ui.unassigned": "Не назначено",
|
||||||
"ui.saveError": "Не удалось сохранить задачи: {error}",
|
"ui.saveError": "Не удалось сохранить задачи. Повторите попытку.",
|
||||||
"ui.loadError": "Не удалось загрузить задачи: {error}",
|
"ui.loadError": "Не удалось загрузить задачи. Повторите попытку.",
|
||||||
"ui.notificationError": "Не удалось запланировать напоминания: {error}",
|
"ui.notificationError": "Не удалось запланировать напоминания. Повторите попытку.",
|
||||||
"ui.notificationTitle": "Напоминание о задаче",
|
"ui.notificationTitle": "Напоминание о задаче",
|
||||||
"ui.notificationBody": "{title}",
|
"ui.notificationBody": "{title}",
|
||||||
"ui.titlePlaceholder": "Название задачи",
|
"ui.titlePlaceholder": "Название задачи",
|
||||||
|
|
@ -37,6 +37,9 @@
|
||||||
"ui.field.reminder": "Напоминание",
|
"ui.field.reminder": "Напоминание",
|
||||||
"ui.field.reminderDate": "Дата напоминания",
|
"ui.field.reminderDate": "Дата напоминания",
|
||||||
"ui.field.reminderTime": "Время напоминания",
|
"ui.field.reminderTime": "Время напоминания",
|
||||||
|
"ui.reminderTimePlaceholder": "14:30",
|
||||||
|
"ui.reminderTimeInvalid": "Введите корректное время напоминания (ЧЧ:ММ)",
|
||||||
|
"ui.reminderDateRequired": "Сначала выберите дату напоминания",
|
||||||
"ui.field.workspace": "Рабочее пространство",
|
"ui.field.workspace": "Рабочее пространство",
|
||||||
"ui.workspaceValue": "Рабочее пространство: {workspace}",
|
"ui.workspaceValue": "Рабочее пространство: {workspace}",
|
||||||
"ui.edit": "Изменить задачу",
|
"ui.edit": "Изменить задачу",
|
||||||
|
|
@ -52,6 +55,7 @@
|
||||||
"ui.dueValue": "{prefix}Срок: {date}",
|
"ui.dueValue": "{prefix}Срок: {date}",
|
||||||
"ui.reminderDueValue": "Пора напомнить: {date}",
|
"ui.reminderDueValue": "Пора напомнить: {date}",
|
||||||
"ui.reminderValue": "Напоминание: {date}",
|
"ui.reminderValue": "Напоминание: {date}",
|
||||||
|
"ui.reminderDateValue": "Дата напоминания: {date}",
|
||||||
"ui.noMatches": "Нет задач, соответствующих фильтрам.",
|
"ui.noMatches": "Нет задач, соответствующих фильтрам.",
|
||||||
"ui.empty": "Задач пока нет.",
|
"ui.empty": "Задач пока нет.",
|
||||||
"ui.openWorkspace": "Открыть рабочее пространство",
|
"ui.openWorkspace": "Открыть рабочее пространство",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const checks = [
|
||||||
['plugins/files/frontend/src/index.js', '.files-sort'],
|
['plugins/files/frontend/src/index.js', '.files-sort'],
|
||||||
['plugins/notes/frontend/src/index.js', '.notes-sort'],
|
['plugins/notes/frontend/src/index.js', '.notes-sort'],
|
||||||
['plugins/browser-inbox/frontend/src/index.js', '.browser-inbox-select'],
|
['plugins/browser-inbox/frontend/src/index.js', '.browser-inbox-select'],
|
||||||
|
['plugins/journal/frontend/src/index.js', '.journal-input.journal-select'],
|
||||||
['plugins/trash/frontend/src/index.js', '.trash-select'],
|
['plugins/trash/frontend/src/index.js', '.trash-select'],
|
||||||
['plugins/secrets/frontend/src/index.js', '.secrets-select'],
|
['plugins/secrets/frontend/src/index.js', '.secrets-select'],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
function frontendSources(dir) {
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const target = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) return frontendSources(target);
|
||||||
|
return /\.(?:js|svelte)$/.test(entry.name) ? [target] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(path.join(root, 'plugins'))
|
||||||
|
.flatMap((plugin) => frontendSources(path.join(root, 'plugins', plugin, 'frontend', 'src')));
|
||||||
|
|
||||||
|
const renderPatterns = [
|
||||||
|
/statusText\s*=/,
|
||||||
|
/state\.error\s*=/,
|
||||||
|
/errorMsg\s*=/,
|
||||||
|
/setStatus\(/,
|
||||||
|
/setCreateError\(/,
|
||||||
|
/setRenameError\(/,
|
||||||
|
/renderEmpty\(/,
|
||||||
|
/output\.errors\.push\(/,
|
||||||
|
/window\.alert\(/,
|
||||||
|
/showExternalFallback\(/,
|
||||||
|
/de-error-msg/,
|
||||||
|
/files-error-msg/,
|
||||||
|
/body\.textContent\s*=/
|
||||||
|
];
|
||||||
|
|
||||||
|
const violations = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (!/(?:err(?:or)?\.message|String\(err(?:or)?\b)/.test(line)) return;
|
||||||
|
if (!renderPatterns.some((pattern) => pattern.test(line))) return;
|
||||||
|
violations.push(`${path.relative(root, file)}:${index + 1}: technical error text reaches a user-facing UI sink`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length) {
|
||||||
|
console.error('User-facing UI must not render raw backend/plugin errors:');
|
||||||
|
violations.forEach((violation) => console.error(` ${violation}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('user-facing error messages do not expose raw backend details');
|
||||||
|
|
@ -101,6 +101,18 @@ else
|
||||||
echo " ⚠️ node not available — skipping select style validation"
|
echo " ⚠️ node not available — skipping select style validation"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[user-facing errors]"
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
set +e
|
||||||
|
node "$ROOT/scripts/check-user-facing-errors.js"
|
||||||
|
STATUS=$?
|
||||||
|
set -e
|
||||||
|
report "user-facing errors" "$STATUS"
|
||||||
|
else
|
||||||
|
echo " ⚠️ node not available — skipping user-facing error validation"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
# Guard official plugins against bypassing the v2 plugin API for note features.
|
# Guard official plugins against bypassing the v2 plugin API for note features.
|
||||||
echo "[frontend API boundary]"
|
echo "[frontend API boundary]"
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,13 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
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(projectKey).length === 0) throw new Error('clear action removed activity before confirmation');
|
||||||
|
const clearConfirmation = walk(container, (node) => node.getAttribute && node.getAttribute('data-activity-clear-confirmation') === '');
|
||||||
|
if (!clearConfirmation) throw new Error('clear activity confirmation was not rendered');
|
||||||
|
const confirmClear = walk(clearConfirmation, (node) => node.getAttribute && node.getAttribute('data-activity-clear-confirm') === '');
|
||||||
|
if (!confirmClear) throw new Error('clear activity confirmation button was not rendered');
|
||||||
|
confirmClear.click();
|
||||||
|
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 (api.storedEvents('work-session-candidates:workspace:Project').length !== 0) throw new Error('clear action did not remove cached candidates');
|
if (api.storedEvents('work-session-candidates:workspace:Project').length !== 0) throw new Error('clear action did not remove cached candidates');
|
||||||
|
|
||||||
|
|
@ -531,6 +538,13 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
||||||
const rawClear = walk(rawView.container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'clear');
|
const rawClear = walk(rawView.container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'clear');
|
||||||
rawClear.click();
|
rawClear.click();
|
||||||
await flush();
|
await flush();
|
||||||
|
if (rawApi.storedData('activity-events').length === 0) throw new Error('append-only activity was removed before confirmation');
|
||||||
|
const rawClearConfirmation = walk(rawView.container, (node) => node.getAttribute && node.getAttribute('data-activity-clear-confirmation') === '');
|
||||||
|
if (!rawClearConfirmation) throw new Error('append-only activity confirmation was not rendered');
|
||||||
|
const rawConfirmClear = walk(rawClearConfirmation, (node) => node.getAttribute && node.getAttribute('data-activity-clear-confirm') === '');
|
||||||
|
if (!rawConfirmClear) throw new Error('append-only activity confirmation button was not rendered');
|
||||||
|
rawConfirmClear.click();
|
||||||
|
await flush();
|
||||||
if (rawApi.storedData('activity-events').length !== 0) throw new Error('clear activity did not replace append-only data');
|
if (rawApi.storedData('activity-events').length !== 0) throw new Error('clear activity did not replace append-only data');
|
||||||
component.unmount && component.unmount(rawView.container);
|
component.unmount && component.unmount(rawView.container);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ const vm = require('vm');
|
||||||
const root = path.resolve(__dirname, '..');
|
const root = path.resolve(__dirname, '..');
|
||||||
const sourcePath = path.join(root, 'plugins', 'browser-inbox', 'frontend', 'src', 'index.js');
|
const sourcePath = path.join(root, 'plugins', 'browser-inbox', 'frontend', 'src', 'index.js');
|
||||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||||
|
const catalogs = {
|
||||||
|
en: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'browser-inbox', 'locales', 'en.json'), 'utf8')),
|
||||||
|
ru: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'browser-inbox', 'locales', 'ru.json'), 'utf8')),
|
||||||
|
};
|
||||||
|
const technicalErrors = [];
|
||||||
|
|
||||||
class FakeNode {
|
class FakeNode {
|
||||||
constructor(tagName) {
|
constructor(tagName) {
|
||||||
|
|
@ -113,7 +118,15 @@ function makeDocument() {
|
||||||
function loadComponents(document) {
|
function loadComponents(document) {
|
||||||
const registry = {};
|
const registry = {};
|
||||||
const sandbox = {
|
const sandbox = {
|
||||||
console,
|
console: {
|
||||||
|
...console,
|
||||||
|
warn(...args) {
|
||||||
|
technicalErrors.push(args.map((value) => String(value)).join(' '));
|
||||||
|
},
|
||||||
|
error(...args) {
|
||||||
|
technicalErrors.push(args.map((value) => String(value)).join(' '));
|
||||||
|
},
|
||||||
|
},
|
||||||
Date,
|
Date,
|
||||||
document,
|
document,
|
||||||
window: {
|
window: {
|
||||||
|
|
@ -136,7 +149,7 @@ function loadComponent(document) {
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeApi(initialSettings = {}) {
|
function makeApi(initialSettings = {}, locale = 'en') {
|
||||||
const settings = { ...initialSettings };
|
const settings = { ...initialSettings };
|
||||||
const handlers = {};
|
const handlers = {};
|
||||||
const unsubscribed = [];
|
const unsubscribed = [];
|
||||||
|
|
@ -153,6 +166,13 @@ function makeApi(initialSettings = {}) {
|
||||||
receiverToken: 'initial-browser-token',
|
receiverToken: 'initial-browser-token',
|
||||||
};
|
};
|
||||||
let nextWriteError = null;
|
let nextWriteError = null;
|
||||||
|
function translate(key, params, fallback) {
|
||||||
|
let value = catalogs[locale]?.[key] || catalogs.en[key] || fallback || key;
|
||||||
|
Object.entries(params || {}).forEach(([name, replacement]) => {
|
||||||
|
value = value.replace(new RegExp(`\\{${name}\\}`, 'g'), String(replacement));
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
function backendCaptures() {
|
function backendCaptures() {
|
||||||
const keys = ['captures:global', 'captures', ...Object.keys(settings).filter((key) => key.startsWith('captures:workspace:'))];
|
const keys = ['captures:global', 'captures', ...Object.keys(settings).filter((key) => key.startsWith('captures:workspace:'))];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
@ -205,6 +225,11 @@ function makeApi(initialSettings = {}) {
|
||||||
fileByteWrites,
|
fileByteWrites,
|
||||||
openedURLs,
|
openedURLs,
|
||||||
publishedEvents,
|
publishedEvents,
|
||||||
|
i18n: {
|
||||||
|
getLocale: () => locale,
|
||||||
|
t: translate,
|
||||||
|
onDidChangeLocale: () => () => {},
|
||||||
|
},
|
||||||
failNextWrite(message) {
|
failNextWrite(message) {
|
||||||
nextWriteError = new Error(message || 'write failed');
|
nextWriteError = new Error(message || 'write failed');
|
||||||
},
|
},
|
||||||
|
|
@ -298,6 +323,18 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||||
}
|
}
|
||||||
styledView.component.unmount && styledView.component.unmount(styledView.container);
|
styledView.component.unmount && styledView.component.unmount(styledView.container);
|
||||||
|
|
||||||
|
const russianView = await mountWithApi(makeApi({}, 'ru'), {});
|
||||||
|
if (!russianView.container.textContent.includes('Браузер')) {
|
||||||
|
throw new Error('Browser Inbox does not use the localized Browser title');
|
||||||
|
}
|
||||||
|
if (!russianView.container.textContent.includes('Все Дела')) {
|
||||||
|
throw new Error('Browser Inbox does not use the localized Deal filter');
|
||||||
|
}
|
||||||
|
if (!russianView.container.textContent.includes('Пока нет материалов из браузера')) {
|
||||||
|
throw new Error('Browser Inbox empty state is not localized');
|
||||||
|
}
|
||||||
|
russianView.component.unmount && russianView.component.unmount(russianView.container);
|
||||||
|
|
||||||
const api = makeApi();
|
const api = makeApi();
|
||||||
const settingsView = await mountSettingsWithApi(makeApi());
|
const settingsView = await mountSettingsWithApi(makeApi());
|
||||||
const receiverURLInput = walk(settingsView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-pairing-url') === '');
|
const receiverURLInput = walk(settingsView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-pairing-url') === '');
|
||||||
|
|
@ -738,9 +775,12 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||||
if (!failedConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-conflict')) {
|
if (!failedConversionApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-conflict')) {
|
||||||
throw new Error('failed conversion removed capture from queue');
|
throw new Error('failed conversion removed capture from queue');
|
||||||
}
|
}
|
||||||
if (!failedConversionView.container.textContent.includes('Could not create note')) {
|
if (!failedConversionView.container.textContent.includes('Could not create the note. Please try again.')) {
|
||||||
throw new Error('failed conversion did not render an error status');
|
throw new Error('failed conversion did not render an error status');
|
||||||
}
|
}
|
||||||
|
if (failedConversionView.container.textContent.includes('file already exists')) {
|
||||||
|
throw new Error('failed conversion exposed a raw backend error');
|
||||||
|
}
|
||||||
if (failedConversionApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
if (failedConversionApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
||||||
throw new Error('failed conversion published converted event');
|
throw new Error('failed conversion published converted event');
|
||||||
}
|
}
|
||||||
|
|
@ -827,7 +867,7 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||||
if (!failedLinkApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-link-conflict')) {
|
if (!failedLinkApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-link-conflict')) {
|
||||||
throw new Error('failed link conversion removed capture from queue');
|
throw new Error('failed link conversion removed capture from queue');
|
||||||
}
|
}
|
||||||
if (!failedLinkView.container.textContent.includes('Could not create link')) {
|
if (!failedLinkView.container.textContent.includes('Could not create the link. Please try again.')) {
|
||||||
throw new Error('failed link conversion did not render an error status');
|
throw new Error('failed link conversion did not render an error status');
|
||||||
}
|
}
|
||||||
if (failedLinkApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
if (failedLinkApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
||||||
|
|
@ -934,7 +974,7 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||||
if (!failedFileApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-file-conflict')) {
|
if (!failedFileApi.getStoredCaptures(globalKey).some((capture) => capture.captureId === 'convert-file-conflict')) {
|
||||||
throw new Error('failed file conversion removed capture from queue');
|
throw new Error('failed file conversion removed capture from queue');
|
||||||
}
|
}
|
||||||
if (!failedFileView.container.textContent.includes('Could not create file')) {
|
if (!failedFileView.container.textContent.includes('Could not create the file. Please try again.')) {
|
||||||
throw new Error('failed file conversion did not render an error status');
|
throw new Error('failed file conversion did not render an error status');
|
||||||
}
|
}
|
||||||
if (failedFileApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
if (failedFileApi.publishedEvents.some((event) => event.name === 'browser.capture.converted')) {
|
||||||
|
|
@ -942,6 +982,10 @@ async function mountSettingsWithApi(api, document = makeDocument()) {
|
||||||
}
|
}
|
||||||
component.unmount && component.unmount(failedFileView.container);
|
component.unmount && component.unmount(failedFileView.container);
|
||||||
|
|
||||||
|
if (!technicalErrors.some((entry) => entry.includes('file already exists'))) {
|
||||||
|
throw new Error('failed conversion did not retain its technical details in the console log');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('browser inbox plugin smoke passed');
|
console.log('browser inbox plugin smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,12 @@ function makeApi(initialSettings = {}) {
|
||||||
publishedEvents.push({ name, payload });
|
publishedEvents.push({ name, payload });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
files: {
|
||||||
|
list: async () => [
|
||||||
|
{ type: 'folder', relativePath: 'Project', name: 'Project' },
|
||||||
|
{ type: 'folder', relativePath: 'Client', name: 'Client' },
|
||||||
|
],
|
||||||
|
},
|
||||||
storedEntries(key) {
|
storedEntries(key) {
|
||||||
return settings[key] || [];
|
return settings[key] || [];
|
||||||
},
|
},
|
||||||
|
|
@ -325,6 +331,22 @@ function byData(container, attr, value) {
|
||||||
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');
|
||||||
}
|
}
|
||||||
|
const globalAdd = byData(globalView.container, 'data-journal-action', 'add');
|
||||||
|
if (globalAdd.disabled) throw new Error('global Journal Add must be available');
|
||||||
|
globalAdd.click();
|
||||||
|
await flush();
|
||||||
|
const globalWorkspace = byData(globalView.container, 'data-journal-input', 'workspaceRootPath');
|
||||||
|
if (!globalWorkspace || globalWorkspace.tagName !== 'SELECT') throw new Error('global Journal form did not render the Deal selector');
|
||||||
|
globalWorkspace.value = 'Client';
|
||||||
|
byData(globalView.container, 'data-journal-input', 'title').value = 'Prepare client summary';
|
||||||
|
byData(globalView.container, 'data-journal-input', 'minutes').value = '30';
|
||||||
|
byData(globalView.container, 'data-journal-action', 'save-entry').click();
|
||||||
|
await flush();
|
||||||
|
const clientKey = 'worklog:workspace:Client';
|
||||||
|
if (api.storedEntries(clientKey).length !== 1 || api.storedEntries(clientKey)[0].title !== 'Prepare client summary') {
|
||||||
|
throw new Error('global Journal entry was not stored under the selected Deal');
|
||||||
|
}
|
||||||
|
if (!globalView.container.textContent.includes('Prepare client summary')) throw new Error('global Journal did not render the created entry');
|
||||||
|
|
||||||
component.unmount && component.unmount(container);
|
component.unmount && component.unmount(container);
|
||||||
component.unmount && component.unmount(candidateView.container);
|
component.unmount && component.unmount(candidateView.container);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const sourcePath = path.join(root, 'plugins', 'search', 'frontend', 'src', 'inde
|
||||||
const manifestPath = path.join(root, 'plugins', 'search', 'plugin.json');
|
const manifestPath = path.join(root, 'plugins', 'search', 'plugin.json');
|
||||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
const technicalErrors = [];
|
||||||
|
|
||||||
class FakeNode {
|
class FakeNode {
|
||||||
constructor(tagName) {
|
constructor(tagName) {
|
||||||
|
|
@ -100,7 +101,15 @@ function makeDocument() {
|
||||||
function loadComponent(document) {
|
function loadComponent(document) {
|
||||||
const registry = {};
|
const registry = {};
|
||||||
vm.runInNewContext(source, {
|
vm.runInNewContext(source, {
|
||||||
console,
|
console: {
|
||||||
|
...console,
|
||||||
|
warn(...args) {
|
||||||
|
technicalErrors.push(args.map((value) => String(value)).join(' '));
|
||||||
|
},
|
||||||
|
error(...args) {
|
||||||
|
technicalErrors.push(args.map((value) => String(value)).join(' '));
|
||||||
|
},
|
||||||
|
},
|
||||||
document,
|
document,
|
||||||
window: {
|
window: {
|
||||||
VerstakPluginRegister(pluginId, bundle) {
|
VerstakPluginRegister(pluginId, bundle) {
|
||||||
|
|
@ -253,11 +262,13 @@ async function wait(ms) {
|
||||||
if (!container.textContent.includes('Project/Target Assets')) throw new Error('typing should search folder paths');
|
if (!container.textContent.includes('Project/Target Assets')) throw new Error('typing should search folder paths');
|
||||||
if (!container.textContent.includes('Project/External/target.note')) throw new Error('external provider result should be rendered');
|
if (!container.textContent.includes('Project/External/target.note')) throw new Error('external provider result should be rendered');
|
||||||
if (!container.textContent.includes('External Notes')) throw new Error('external provider label should be rendered');
|
if (!container.textContent.includes('External Notes')) throw new Error('external provider label should be rendered');
|
||||||
if (!container.textContent.includes('provider unavailable')) throw new Error('provider failure should be reported without failing search');
|
if (!container.textContent.includes('A search provider is unavailable.')) throw new Error('provider failure should be reported without failing search');
|
||||||
|
if (container.textContent.includes('provider unavailable')) throw new Error('provider failure leaked a raw backend error');
|
||||||
if (!container.textContent.includes('Content match')) throw new Error('content result type was not rendered');
|
if (!container.textContent.includes('Content match')) throw new Error('content result type was not rendered');
|
||||||
if (!container.textContent.includes('Folder name')) throw new Error('folder result type was not rendered');
|
if (!container.textContent.includes('Folder name')) throw new Error('folder result type was not rendered');
|
||||||
if (!pluginData['search-index'] || !Array.isArray(pluginData['search-index'].files)) throw new Error('search index was not written to plugin data storage');
|
if (!pluginData['search-index'] || !Array.isArray(pluginData['search-index'].files)) throw new Error('search index was not written to plugin data storage');
|
||||||
if (providerCalls.some((call) => call.pluginId === 'verstak.search')) throw new Error('search must not call itself as an external provider');
|
if (providerCalls.some((call) => call.pluginId === 'verstak.search')) throw new Error('search must not call itself as an external provider');
|
||||||
|
if (!technicalErrors.some((entry) => entry.includes('provider unavailable'))) throw new Error('provider failure was not retained in the console log');
|
||||||
|
|
||||||
input = queryInput();
|
input = queryInput();
|
||||||
input.value = 'image';
|
input.value = 'image';
|
||||||
|
|
|
||||||
|
|
@ -96,10 +96,18 @@ function makeDocument() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadComponent(document) {
|
function loadComponent(document, errorLog) {
|
||||||
const registry = {};
|
const registry = {};
|
||||||
vm.runInNewContext(source, {
|
vm.runInNewContext(source, {
|
||||||
console,
|
console: {
|
||||||
|
...console,
|
||||||
|
warn(...args) {
|
||||||
|
errorLog.push(args.map((value) => String(value)).join(' '));
|
||||||
|
},
|
||||||
|
error(...args) {
|
||||||
|
errorLog.push(args.map((value) => String(value)).join(' '));
|
||||||
|
},
|
||||||
|
},
|
||||||
document,
|
document,
|
||||||
window: {
|
window: {
|
||||||
VerstakPluginRegister(pluginId, bundle) {
|
VerstakPluginRegister(pluginId, bundle) {
|
||||||
|
|
@ -129,19 +137,24 @@ async function flush() {
|
||||||
if (!manifest.provides.includes('secrets.write-ui')) throw new Error('secrets manifest must provide secrets.write-ui');
|
if (!manifest.provides.includes('secrets.write-ui')) throw new Error('secrets manifest must provide secrets.write-ui');
|
||||||
if (!manifest.permissions.includes('secrets.read')) throw new Error('secrets manifest must request secrets.read');
|
if (!manifest.permissions.includes('secrets.read')) throw new Error('secrets manifest must request secrets.read');
|
||||||
if (!manifest.permissions.includes('secrets.write')) throw new Error('secrets manifest must request secrets.write');
|
if (!manifest.permissions.includes('secrets.write')) throw new Error('secrets manifest must request secrets.write');
|
||||||
|
if (!manifest.permissions.includes('files.read')) throw new Error('secrets manifest must request files.read for the Deal selector');
|
||||||
if (!manifest.permissions.includes('ui.register')) throw new Error('secrets manifest must request ui.register');
|
if (!manifest.permissions.includes('ui.register')) throw new Error('secrets manifest must request ui.register');
|
||||||
if (!(manifest.contributes.openProviders || []).some((item) => (item.supports || []).some((support) => support.kind === 'secret'))) throw new Error('secrets secret open provider missing');
|
if (!(manifest.contributes.openProviders || []).some((item) => (item.supports || []).some((support) => support.kind === 'secret'))) throw new Error('secrets secret open provider missing');
|
||||||
|
if (!(manifest.contributes.views || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets global view missing');
|
||||||
|
if (!(manifest.contributes.sidebarItems || []).some((item) => item.view === 'verstak.secrets.view')) throw new Error('secrets global sidebar item missing');
|
||||||
if (!(manifest.contributes.workspaceItems || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets workspace item missing');
|
if (!(manifest.contributes.workspaceItems || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets workspace item missing');
|
||||||
if (!(manifest.contributes.settingsPanels || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets settings panel missing');
|
if (!(manifest.contributes.settingsPanels || []).some((item) => item.component === 'SecretsView')) throw new Error('secrets settings panel missing');
|
||||||
|
|
||||||
const document = makeDocument();
|
const document = makeDocument();
|
||||||
const component = loadComponent(document);
|
const errorLog = [];
|
||||||
|
const component = loadComponent(document, errorLog);
|
||||||
const records = [
|
const records = [
|
||||||
{ id: 'global.server', title: 'Global Server', username: 'root', scope: { kind: 'global' }, updatedAt: '2026-06-29T00:00:00Z' },
|
{ id: 'global.server', title: 'Global Server', username: 'root', scope: { kind: 'global' }, updatedAt: '2026-06-29T00:00:00Z' },
|
||||||
{ id: 'client-a.db', title: 'Client A DB', username: 'app', scope: { kind: 'workspace', workspaceRootPath: 'ClientA' }, updatedAt: '2026-06-29T00:00:00Z' },
|
{ id: 'client-a.db', title: 'Client A DB', username: 'app', scope: { kind: 'workspace', workspaceRootPath: 'ClientA' }, updatedAt: '2026-06-29T00:00:00Z' },
|
||||||
];
|
];
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let unlocked = false;
|
let unlocked = false;
|
||||||
|
let unlockError = '';
|
||||||
const readCalls = [];
|
const readCalls = [];
|
||||||
const copied = [];
|
const copied = [];
|
||||||
const deleted = [];
|
const deleted = [];
|
||||||
|
|
@ -149,6 +162,7 @@ async function flush() {
|
||||||
secrets: {
|
secrets: {
|
||||||
status: async () => ({ initialized, unlocked }),
|
status: async () => ({ initialized, unlocked }),
|
||||||
unlock: async (password) => {
|
unlock: async (password) => {
|
||||||
|
if (unlockError) throw new Error(unlockError);
|
||||||
if (password !== 'master-password') throw new Error('bad password');
|
if (password !== 'master-password') throw new Error('bad password');
|
||||||
initialized = true;
|
initialized = true;
|
||||||
unlocked = true;
|
unlocked = true;
|
||||||
|
|
@ -177,6 +191,12 @@ async function flush() {
|
||||||
clipboard: {
|
clipboard: {
|
||||||
writeText: async (text) => copied.push(text),
|
writeText: async (text) => copied.push(text),
|
||||||
},
|
},
|
||||||
|
files: {
|
||||||
|
list: async () => [
|
||||||
|
{ type: 'folder', relativePath: 'ClientA' },
|
||||||
|
{ type: 'folder', relativePath: 'ClientB' },
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
|
|
@ -188,6 +208,21 @@ async function flush() {
|
||||||
const confirmInput = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-master-password-confirm') === '');
|
const confirmInput = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-master-password-confirm') === '');
|
||||||
const unlockButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-unlock') === '');
|
const unlockButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-unlock') === '');
|
||||||
if (!passwordInput || !confirmInput || !unlockButton) throw new Error('setup controls missing');
|
if (!passwordInput || !confirmInput || !unlockButton) throw new Error('setup controls missing');
|
||||||
|
unlockError = '[plugin:verstak.secrets] secrets.unlock failed: master password must be at least 8 characters';
|
||||||
|
passwordInput.value = 'short';
|
||||||
|
confirmInput.value = 'short';
|
||||||
|
unlockButton.click();
|
||||||
|
await flush();
|
||||||
|
if (!container.textContent.includes('Master password must be at least 8 characters')) {
|
||||||
|
throw new Error('weak master password error was not explained clearly');
|
||||||
|
}
|
||||||
|
if (container.textContent.includes('[plugin:') || container.textContent.includes('secrets.unlock')) {
|
||||||
|
throw new Error('technical unlock details leaked into the Secrets UI');
|
||||||
|
}
|
||||||
|
if (!errorLog.some((entry) => entry.includes('[plugin:verstak.secrets] secrets.unlock failed'))) {
|
||||||
|
throw new Error('technical unlock details were not retained in the console log');
|
||||||
|
}
|
||||||
|
unlockError = '';
|
||||||
passwordInput.value = 'master-password';
|
passwordInput.value = 'master-password';
|
||||||
confirmInput.value = 'master-password';
|
confirmInput.value = 'master-password';
|
||||||
unlockButton.click();
|
unlockButton.click();
|
||||||
|
|
@ -228,6 +263,53 @@ async function flush() {
|
||||||
await flush();
|
await flush();
|
||||||
if (!deleted.includes('client-a.db')) throw new Error('secret delete was not called');
|
if (!deleted.includes('client-a.db')) throw new Error('secret delete was not called');
|
||||||
|
|
||||||
|
records.push({ id: 'client-a.global-view', title: 'Client A Global View', username: 'app', scope: { kind: 'workspace', workspaceRootPath: 'ClientA' }, updatedAt: '2026-06-29T00:00:00Z' });
|
||||||
|
const globalContainer = document.createElement('div');
|
||||||
|
component.mount(globalContainer, {}, api);
|
||||||
|
await flush();
|
||||||
|
|
||||||
|
if (!globalContainer.textContent.includes('Global Server') || !globalContainer.textContent.includes('Client A Global View')) {
|
||||||
|
throw new Error('global secrets view did not show global and Deal-scoped records');
|
||||||
|
}
|
||||||
|
const scopeFilter = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-scope-filter') === '');
|
||||||
|
if (!scopeFilter) throw new Error('global secrets scope filter missing');
|
||||||
|
scopeFilter.value = 'workspace:ClientA';
|
||||||
|
scopeFilter.dispatchEvent('change');
|
||||||
|
await flush();
|
||||||
|
if (!globalContainer.textContent.includes('Client A Global View') || globalContainer.textContent.includes('Global Server')) {
|
||||||
|
throw new Error('global secrets scope filter did not isolate the selected Deal');
|
||||||
|
}
|
||||||
|
const searchInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-search') === '');
|
||||||
|
if (!searchInput) throw new Error('global secrets search missing');
|
||||||
|
searchInput.value = 'no-match';
|
||||||
|
searchInput.dispatchEvent('input');
|
||||||
|
await flush();
|
||||||
|
if (!globalContainer.textContent.includes('No secrets')) throw new Error('global secrets search did not filter records');
|
||||||
|
|
||||||
|
scopeFilter.value = 'all';
|
||||||
|
scopeFilter.dispatchEvent('change');
|
||||||
|
await flush();
|
||||||
|
const newButton = walk(globalContainer, (node) => node.tagName === 'BUTTON' && node.textContent === 'New');
|
||||||
|
if (!newButton) throw new Error('global secrets new button missing');
|
||||||
|
newButton.click();
|
||||||
|
await flush();
|
||||||
|
const globalTitleInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-title') === '');
|
||||||
|
const globalValueInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-value') === '');
|
||||||
|
const scopeInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-scope') === '');
|
||||||
|
const workspaceInput = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-workspace') === '');
|
||||||
|
const globalSaveButton = walk(globalContainer, (node) => node.getAttribute && node.getAttribute('data-secret-save') === '');
|
||||||
|
if (!globalTitleInput || !globalValueInput || !scopeInput || !workspaceInput || !globalSaveButton) throw new Error('global Deal-scoped secret form controls missing');
|
||||||
|
globalTitleInput.value = 'Client B API';
|
||||||
|
globalValueInput.value = 'client-b-api-value';
|
||||||
|
scopeInput.value = 'workspace';
|
||||||
|
scopeInput.dispatchEvent('change');
|
||||||
|
workspaceInput.value = 'ClientB';
|
||||||
|
globalSaveButton.click();
|
||||||
|
await flush();
|
||||||
|
if (!records.some((record) => record.title === 'Client B API' && record.scope && record.scope.workspaceRootPath === 'ClientB')) {
|
||||||
|
throw new Error('global secrets form did not create a Deal-scoped secret');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('secrets plugin smoke passed');
|
console.log('secrets plugin smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,17 @@ const source = fs.readFileSync(sourcePath, 'utf8');
|
||||||
if (!source.includes('settings.lastError')) {
|
if (!source.includes('settings.lastError')) {
|
||||||
throw new Error('SyncSettings must render persisted settings.lastError');
|
throw new Error('SyncSettings must render persisted settings.lastError');
|
||||||
}
|
}
|
||||||
if (!source.includes('sanitizeError(settings.lastError)')) {
|
if (!source.includes('function reportError')) {
|
||||||
throw new Error('SyncSettings must sanitize persisted sync errors before rendering');
|
throw new Error('SyncSettings must map technical failures to localized action-specific errors');
|
||||||
}
|
}
|
||||||
if (!source.includes('Last sync error')) {
|
if (/sanitizeError\(/.test(source) || /\{ error: sanitizeError/.test(source)) {
|
||||||
|
throw new Error('SyncSettings must not interpolate raw technical errors into user-facing messages');
|
||||||
|
}
|
||||||
|
if (!source.includes("tr('ui.lastSyncError'")) {
|
||||||
throw new Error('SyncSettings must label the persisted sync error');
|
throw new Error('SyncSettings must label the persisted sync error');
|
||||||
}
|
}
|
||||||
if (!source.includes('function formatSyncConflict')) {
|
if (!source.includes("tr('ui.syncConflictItem'")) {
|
||||||
throw new Error('SyncSettings must format individual sync conflicts');
|
throw new Error('SyncSettings must hide technical conflict identifiers behind a user-facing summary');
|
||||||
}
|
}
|
||||||
if (!source.includes('conflictDetails')) {
|
if (!source.includes('conflictDetails')) {
|
||||||
throw new Error('SyncSettings must store sync conflict details after Sync Now');
|
throw new Error('SyncSettings must store sync conflict details after Sync Now');
|
||||||
|
|
|
||||||
|
|
@ -235,9 +235,9 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
|
||||||
const reminderDate = byData(container, 'data-todo-input', 'reminderDate');
|
const reminderDate = byData(container, 'data-todo-input', 'reminderDate');
|
||||||
const reminderTime = byData(container, 'data-todo-input', 'reminderTime');
|
const reminderTime = byData(container, 'data-todo-input', 'reminderTime');
|
||||||
if (!reminderDate || reminderDate.getAttribute('type') !== 'date') throw new Error('Todo reminder date input was not rendered');
|
if (!reminderDate || reminderDate.getAttribute('type') !== 'date') throw new Error('Todo reminder date input was not rendered');
|
||||||
if (!reminderTime || reminderTime.getAttribute('type') !== 'time') throw new Error('Todo reminder time input was not rendered');
|
if (!reminderTime || reminderTime.getAttribute('type') !== 'text') throw new Error('Todo reminder time must be a keyboard-editable text input');
|
||||||
reminderDate.value = '01/02/2000';
|
reminderDate.value = '01/02/2000';
|
||||||
reminderTime.value = '09:00';
|
reminderTime.value = '09:30';
|
||||||
byData(container, 'data-todo-action', 'save').click();
|
byData(container, 'data-todo-action', 'save').click();
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
|
|
@ -246,7 +246,7 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
|
||||||
const createdTodo = storedAfterCreate[0];
|
const createdTodo = storedAfterCreate[0];
|
||||||
if (createdTodo.workspaceRootPath !== 'Project') throw new Error('workspace Todo did not keep the Project root path');
|
if (createdTodo.workspaceRootPath !== 'Project') throw new Error('workspace Todo did not keep the Project root path');
|
||||||
if (createdTodo.status !== 'open' || createdTodo.priority !== 'high') throw new Error('Todo status or priority was not stored');
|
if (createdTodo.status !== 'open' || createdTodo.priority !== 'high') throw new Error('Todo status or priority was not stored');
|
||||||
if (createdTodo.dueAt !== '2000-01-02' || createdTodo.reminderAt !== '2000-01-02T09:00') throw new Error('Todo due/reminder metadata was not stored');
|
if (createdTodo.dueAt !== '2000-01-02' || createdTodo.reminderDate !== '2000-01-02' || createdTodo.reminderAt !== '2000-01-02T09:30') throw new Error('Todo due/reminder metadata was not stored');
|
||||||
if (!container.textContent.includes('Overdue') || !container.textContent.includes('Reminder due')) throw new Error('due/reminder indicators were not rendered');
|
if (!container.textContent.includes('Overdue') || !container.textContent.includes('Reminder due')) throw new Error('due/reminder indicators were not rendered');
|
||||||
const scheduledAfterCreate = apiState.notificationCalls.at(-1) || [];
|
const scheduledAfterCreate = apiState.notificationCalls.at(-1) || [];
|
||||||
if (scheduledAfterCreate.length !== 1
|
if (scheduledAfterCreate.length !== 1
|
||||||
|
|
@ -266,9 +266,20 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
|
||||||
|
|
||||||
byData(container, 'data-todo-action', 'edit').click();
|
byData(container, 'data-todo-action', 'edit').click();
|
||||||
byData(container, 'data-todo-input', 'title').value = 'Prepare project review updated';
|
byData(container, 'data-todo-input', 'title').value = 'Prepare project review updated';
|
||||||
|
byData(container, 'data-todo-input', 'reminderTime').value = '29:30';
|
||||||
|
byData(container, 'data-todo-action', 'save').click();
|
||||||
|
await flush();
|
||||||
|
if (apiState.settings['todos:global'][0].title !== 'Prepare project review' || !container.textContent.includes('Enter a valid reminder time')) {
|
||||||
|
throw new Error('invalid reminder time was saved instead of showing a human-readable error');
|
||||||
|
}
|
||||||
|
byData(container, 'data-todo-input', 'reminderTime').value = '';
|
||||||
byData(container, 'data-todo-action', 'save').click();
|
byData(container, 'data-todo-action', 'save').click();
|
||||||
await flush();
|
await flush();
|
||||||
if (apiState.settings['todos:global'][0].title !== 'Prepare project review updated') throw new Error('Todo edit was not persisted');
|
if (apiState.settings['todos:global'][0].title !== 'Prepare project review updated') throw new Error('Todo edit was not persisted');
|
||||||
|
if (apiState.settings['todos:global'][0].reminderDate !== '2000-01-02' || apiState.settings['todos:global'][0].reminderAt !== '') {
|
||||||
|
throw new Error('clearing reminder time did not preserve its date while cancelling the reminder');
|
||||||
|
}
|
||||||
|
if ((apiState.notificationCalls.at(-1) || []).length !== 0) throw new Error('clearing reminder time did not remove the native notification');
|
||||||
|
|
||||||
byData(container, 'data-todo-action', 'mark-done').click();
|
byData(container, 'data-todo-action', 'mark-done').click();
|
||||||
await flush();
|
await flush();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue