Compare commits
18
Commits
99172944a1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03f5299209 | ||
|
|
1a201bed6d | ||
|
|
1bcad23b58 | ||
|
|
126d90cd68 | ||
|
|
9904d4a962 | ||
|
|
515dec6a7b | ||
|
|
c9c26faf56 | ||
|
|
484d599a9f | ||
|
|
5ddc63bb8d | ||
|
|
f456287c2e | ||
|
|
c8b8045da1 | ||
|
|
3a1920d104 | ||
|
|
713b95b5a8 | ||
|
|
b449e9c616 | ||
|
|
90ad8e8467 | ||
|
|
aa7956dec4 | ||
|
|
44b39ca051 | ||
|
|
c83c15bced |
@@ -1,13 +1,26 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
# Verstak Official Plugins
|
# Verstak Official Plugins
|
||||||
|
|
||||||
The first-party plugin set for Verstak Desktop: Files, Trash, Notes, editor and
|
### The first-party plugin set — Files, Notes, Editor, Activity, Journal and more.
|
||||||
preview providers, Browser Inbox, Activity, Journal, Todo, Search, Secrets and
|
|
||||||
Templates. Plugin bundles are built into independent directories that the
|
**English** · [Русский](README.ru.md)
|
||||||
desktop host loads from its `plugins/` folder.
|
|
||||||
|
[](https://github.com/mirivlad/verstak-official-plugins/releases)
|
||||||
|

|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
> **Alpha software.** Build this repository together with compatible
|
> **Alpha software.** Build this repository together with compatible
|
||||||
> `verstak-desktop` and `verstak-sdk` checkouts.
|
> `verstak-desktop` and `verstak-sdk` checkouts.
|
||||||
|
|
||||||
|
Verstak Official Plugins is the first-party plugin set for Verstak Desktop:
|
||||||
|
Files, Trash, Notes, Default Editor, File Preview, Browser Inbox, Activity,
|
||||||
|
Journal, Todo, Search, Secrets, Sync, and Templates. Plugin bundles are built
|
||||||
|
into independent directories that the desktop host loads from its `plugins/`
|
||||||
|
folder.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
Requirements: Node.js with npm, Go and Python 3. Keep this repository next to
|
Requirements: Node.js with npm, Go and Python 3. Keep this repository next to
|
||||||
@@ -50,7 +63,7 @@ create a GitHub Release.
|
|||||||
deletion is a separate action.
|
deletion is a separate action.
|
||||||
- Saving a browser link creates a collision-safe `.url` file. Opening a link is
|
- Saving a browser link creates a collision-safe `.url` file. Opening a link is
|
||||||
handled by Verstak, so it does not depend on a Linux `.url` file association.
|
handled by Verstak, so it does not depend on a Linux `.url` file association.
|
||||||
- Activity sessions can be scoped to a durable workspace identity or to an
|
- Activity sessions can be scoped to a durable Deal identity or to an
|
||||||
explicit unassigned scope. Journal creation is always a user action.
|
explicit unassigned scope. Journal creation is always a user action.
|
||||||
- Todo is optional: Overview uses it only when the Todo capability is present.
|
- Todo is optional: Overview uses it only when the Todo capability is present.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# Официальные плагины Верстака
|
||||||
|
|
||||||
|
### Базовый набор плагинов — Файлы, Заметки, Редактор, Активность, Журнал и другие.
|
||||||
|
|
||||||
|
[English](README.md) · **Русский**
|
||||||
|
|
||||||
|
[](https://github.com/mirivlad/verstak-official-plugins/releases)
|
||||||
|

|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> **Alpha-версия.** Собирайте этот репозиторий вместе с совместимыми
|
||||||
|
> версиями `verstak-desktop` и `verstak-sdk`.
|
||||||
|
|
||||||
|
Официальные плагины — базовый набор инструментов Верстака: Файлы, Корзина,
|
||||||
|
Заметки, Редактор, Предпросмотр, Входящие из браузера, Активность, Журнал,
|
||||||
|
Задачи, Поиск, Секреты, Синхронизация и Шаблоны. Плагины собираются в
|
||||||
|
независимые пакеты, которые desktop-приложение загружает из папки `plugins/`.
|
||||||
|
|
||||||
|
## Состав
|
||||||
|
|
||||||
|
| Плагин | Назначение |
|
||||||
|
|--------|------------|
|
||||||
|
| `verstak.files` | Файловый менеджер внутри дела |
|
||||||
|
| `verstak.trash` | Корзина: восстановление и удаление |
|
||||||
|
| `verstak.notes` | Заметки в формате Markdown |
|
||||||
|
| `verstak.default-editor` | Редактор текста и Markdown |
|
||||||
|
| `verstak.file-preview` | Предпросмотр изображений |
|
||||||
|
| `verstak.activity` | История активности и реконструкция работы |
|
||||||
|
| `verstak.journal` | Журнал работ |
|
||||||
|
| `verstak.browser-inbox` | Приём материалов из браузера |
|
||||||
|
| `verstak.search` | Поиск по файлам и заметкам |
|
||||||
|
| `verstak.secrets` | Защищённое хранилище доступов |
|
||||||
|
| `verstak.todo` | Списки задач |
|
||||||
|
| `verstak.sync` | Синхронизация между устройствами |
|
||||||
|
| `verstak.templates` | Шаблоны дел |
|
||||||
|
|
||||||
|
Все плагины используют один и тот же runtime, что и сторонние.
|
||||||
|
|
||||||
|
## Сборка
|
||||||
|
|
||||||
|
Требования: Node.js с npm, Go и Python 3. Репозиторий должен находиться рядом
|
||||||
|
с `verstak-sdk`, чтобы проверка manifest использовала SDK-схему.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm --version
|
||||||
|
go version
|
||||||
|
./scripts/check.sh
|
||||||
|
./scripts/build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Сборка создаёт пакеты в `dist/<plugin-id>/`. Чтобы установить их в соседний
|
||||||
|
desktop-репозиторий:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../verstak-desktop
|
||||||
|
./scripts/install-dev-plugins.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Портативные архивы плагинов
|
||||||
|
|
||||||
|
Сборка архивов под конкретную платформу (Linux):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install gcc-mingw-w64-x86-64
|
||||||
|
./scripts/package-portable.sh v0.1.0-alpha.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Команда создаёт `release/verstak-official-plugins-linux-amd64-<version>.tar.gz`
|
||||||
|
и `release/verstak-official-plugins-windows-amd64-<version>.zip`, а также
|
||||||
|
`SHA256SUMS`. Каждый архив распаковывается прямо в `plugins/` desktop-приложения.
|
||||||
|
Frontend и manifest общие; нативные Go sidecar собираются под целевую ОС.
|
||||||
|
Это локальная операция упаковки, она не создаёт GitHub Release.
|
||||||
|
|
||||||
|
## Публикация GitHub Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/publish-github-release.sh v0.1.0-alpha.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Требуется авторизованный [`gh`](https://cli.github.com/) CLI и чистый
|
||||||
|
актуальный `main`. Создаёт и отправляет аннотированный тег, затем создаёт
|
||||||
|
или обновляет GitHub Release с Linux-архивом, Windows-архивом и `SHA256SUMS`.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Copyright © 2026 Verstak contributors. Распространяется на условиях
|
||||||
|
[GNU AGPLv3 или новее](LICENSE).
|
||||||
+12
-12
@@ -4,22 +4,22 @@ Browser Inbox is one global queue of browser captures. New and changed records a
|
|||||||
stored in `captures:global`; previous `captures` and `captures:workspace:*` keys
|
stored in `captures:global`; previous `captures` and `captures:workspace:*` keys
|
||||||
remain readable for migration compatibility.
|
remain readable for migration compatibility.
|
||||||
|
|
||||||
Each capture contains `workspaceRootPath` and `processed`. An empty
|
Each capture contains a durable `workspaceId`, its current or historical
|
||||||
`workspaceRootPath` means the capture is **Unassigned**. The global view can filter
|
`workspaceRootPath`, and `processed`. An empty `workspaceRootPath` means the
|
||||||
all captures by assignment, workspace, processed state, and text search. A capture
|
capture is **Unassigned**. The global view can filter all captures by assignment,
|
||||||
|
Deal, processed state, and text search. A capture
|
||||||
can be assigned, reassigned, made unassigned, marked processed or unprocessed, and
|
can be assigned, reassigned, made unassigned, marked processed or unprocessed, and
|
||||||
deleted.
|
deleted.
|
||||||
|
|
||||||
The local browser receiver adds the currently active workspace before publishing a
|
The local browser receiver adds the currently active Deal before publishing a
|
||||||
capture event. When it has no active workspace, the capture remains unassigned
|
capture event. When it has no active Deal, the capture remains unassigned
|
||||||
unless an explicit existing domain binding matches it. The frontend never assigns an
|
unless an explicit existing domain binding matches it. The frontend never assigns an
|
||||||
untagged capture merely because a workspace view happens to be open.
|
untagged capture merely because a Deal view happens to be open.
|
||||||
|
|
||||||
Workspace assignment currently uses the top-level vault folder path as the
|
Deal assignment uses the immutable `workspaceId` stored in Deal metadata;
|
||||||
identifier because the core workspace model has no separate immutable ID. In the
|
`workspaceRootPath` is an address and display value. A rename updates the latter,
|
||||||
current model that path is also the displayed workspace name, so a workspace rename
|
while a newly created folder with the old name cannot take over existing captures.
|
||||||
requires a later reassignment of existing captures.
|
|
||||||
|
|
||||||
Workspace Inbox and Overview show only captures whose `workspaceRootPath` exactly
|
Deal Inbox and Overview show only captures whose `workspaceId` matches the selected
|
||||||
matches the selected workspace. Unassigned captures remain visible only in the
|
Deal. Unassigned captures remain visible only in the
|
||||||
global Browser Inbox.
|
global Browser Inbox.
|
||||||
|
|||||||
+14
-16
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
Todos are stored by the official `verstak.todo` plugin under the canonical
|
Todos are stored by the official `verstak.todo` plugin under the canonical
|
||||||
`todos:global` settings key. A todo is global when `workspaceRootPath` is empty;
|
`todos:global` settings key. A todo is global when `workspaceRootPath` is empty;
|
||||||
otherwise that field identifies the top-level workspace folder that owns it.
|
otherwise that field identifies the top-level Deal folder that owns it.
|
||||||
|
|
||||||
The global Todos view aggregates all records and supports workspace, status, text,
|
The global Todos view aggregates all records and supports Deal, status, text,
|
||||||
and due/reminder/updated sorting filters. The workspace Todos view shows only
|
and due/reminder/updated sorting filters. The Deal Todos view shows only records
|
||||||
records whose `workspaceRootPath` exactly matches the current workspace. This
|
whose `workspaceRootPath` exactly matches the current Deal. This keeps unassigned
|
||||||
keeps unassigned and other-workspace records out of a workspace tab and its
|
and other-Deal records out of a Deal tab and its
|
||||||
Overview signals.
|
Overview signals.
|
||||||
|
|
||||||
## Stored fields
|
## Stored fields
|
||||||
@@ -20,15 +20,15 @@ Each record has a stable `id`, `title`, optional `description`, optional
|
|||||||
|
|
||||||
## Reminders
|
## Reminders
|
||||||
|
|
||||||
The plugin stores reminder metadata and renders clear indicators for overdue,
|
The plugin stores reminder metadata, renders clear indicators for overdue,
|
||||||
due-soon, and reminder-due todos. Verstak does not yet have a notification
|
due-soon, and reminder-due todos, and schedules native desktop notifications when
|
||||||
scheduler, so a reminder does not create a native desktop notification or run in
|
the Desktop notification capability is available. Without that capability, the
|
||||||
the background.
|
reminder remains visible in Todos but is not treated as an error.
|
||||||
|
|
||||||
## Completed Todo to Journal
|
## Completed Todo to Journal
|
||||||
|
|
||||||
From a workspace Todo tab, a completed todo exposes **Create Journal Entry**. It
|
From a Deal Todo tab, a completed todo exposes **Create Journal Entry**. It opens
|
||||||
opens the current workspace Journal with a normal, editable form. The form copies
|
the current Deal Journal with a normal, editable form. The form copies
|
||||||
only factual data from the todo: title, description, completion date, and zero
|
only factual data from the todo: title, description, completion date, and zero
|
||||||
minutes. It never generates a summary, duration, or billable status.
|
minutes. It never generates a summary, duration, or billable status.
|
||||||
|
|
||||||
@@ -40,8 +40,6 @@ own record.
|
|||||||
|
|
||||||
## Visibility
|
## Visibility
|
||||||
|
|
||||||
The Plugin Manager can globally enable or disable the Todo plugin. The current
|
The Plugin Manager can globally enable or disable the Todo plugin. A Deal template
|
||||||
workspace host has no per-workspace or template-level contribution filter yet, so
|
controls whether its Todos tab is available; disabling the plugin hides the tab
|
||||||
a globally enabled Todo plugin contributes its tab to every workspace. Template
|
without affecting stored todo records.
|
||||||
visibility will be handled with the workspace/template model rather than by a
|
|
||||||
Todo-specific exception.
|
|
||||||
|
|||||||
@@ -37,19 +37,19 @@
|
|||||||
'activity.session.handled'
|
'activity.session.handled'
|
||||||
];
|
];
|
||||||
var EVENT_LABELS = {
|
var EVENT_LABELS = {
|
||||||
'workspace.selected': 'Workspace selected',
|
'workspace.selected': { key: 'ui.event.workspaceSelected', fallback: 'Deal selected' },
|
||||||
'case.selected': 'Workspace selected',
|
'case.selected': { key: 'ui.event.workspaceSelected', fallback: 'Deal selected' },
|
||||||
'file.opened': 'File opened',
|
'file.opened': { key: 'ui.event.fileOpened', fallback: 'File opened' },
|
||||||
'file.changed': 'File changed',
|
'file.changed': { key: 'ui.event.fileChanged', fallback: 'File changed' },
|
||||||
'note.saved': 'Note edited',
|
'note.saved': { key: 'ui.event.noteEdited', fallback: 'Note edited' },
|
||||||
'action.started': 'Work session detected',
|
'action.started': { key: 'ui.event.workSessionDetected', fallback: 'Work session detected' },
|
||||||
'browser.capture.received': 'Browser capture received',
|
'browser.capture.received': { key: 'ui.event.browserCaptureReceived', fallback: 'Browser capture received' },
|
||||||
'browser.capture.page': 'Page captured',
|
'browser.capture.page': { key: 'ui.event.pageCaptured', fallback: 'Page captured' },
|
||||||
'browser.capture.selection': 'Selection captured',
|
'browser.capture.selection': { key: 'ui.event.selectionCaptured', fallback: 'Selection captured' },
|
||||||
'browser.capture.link': 'Link captured',
|
'browser.capture.link': { key: 'ui.event.linkCaptured', fallback: 'Link captured' },
|
||||||
'browser.capture.file': 'File captured',
|
'browser.capture.file': { key: 'ui.event.fileCaptured', fallback: 'File captured' },
|
||||||
'browser.capture.converted': 'Capture converted',
|
'browser.capture.converted': { key: 'ui.event.captureConverted', fallback: 'Capture converted' },
|
||||||
'browser.activity.domain': 'Browser domain activity'
|
'browser.activity.domain': { key: 'ui.event.browserDomainActivity', fallback: 'Browser domain activity' }
|
||||||
};
|
};
|
||||||
var LOW_VALUE_EVENT_TYPES = {
|
var LOW_VALUE_EVENT_TYPES = {
|
||||||
'workspace.selected': true,
|
'workspace.selected': true,
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
var workspaceRoot = workspaceFromProps(props);
|
var workspaceRoot = workspaceFromProps(props);
|
||||||
var workspaceId = text(props && (props.workspaceId || (props.workspaceNode && props.workspaceNode.workspaceId))).trim();
|
var workspaceId = text(props && (props.workspaceId || (props.workspaceNode && props.workspaceNode.workspaceId))).trim();
|
||||||
if (!workspaceRoot) {
|
if (!workspaceRoot) {
|
||||||
return { mode: 'global', key: GLOBAL_KEY, label: 'All workspaces', workspaceRoot: '', workspaceId: '' };
|
return { mode: 'global', key: GLOBAL_KEY, label: 'All Deals', workspaceRoot: '', workspaceId: '' };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
mode: 'workspace',
|
mode: 'workspace',
|
||||||
@@ -412,25 +412,27 @@
|
|||||||
}).slice(0, MAX_CANDIDATES);
|
}).slice(0, MAX_CANDIDATES);
|
||||||
}
|
}
|
||||||
|
|
||||||
function humanEventType(type) {
|
function humanEventType(type, translate) {
|
||||||
return EVENT_LABELS[text(type).toLowerCase()] || '';
|
var label = EVENT_LABELS[text(type).toLowerCase()];
|
||||||
|
return label ? translate(label.key, null, label.fallback) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function humanEventTitle(activity) {
|
function humanEventTitle(activity, translate) {
|
||||||
var explicit = text(activity && activity.title).trim();
|
var explicit = text(activity && activity.title).trim();
|
||||||
var type = text(activity && activity.type).trim();
|
var type = text(activity && activity.type).trim();
|
||||||
if (explicit && explicit.toLowerCase() !== type.toLowerCase()) return explicit;
|
var label = humanEventType(type, translate);
|
||||||
return humanEventType(type) || text(activity && (activity.summary || activity.activityId)).trim() || 'Activity event';
|
if (explicit && explicit.toLowerCase() !== type.toLowerCase()) return label ? label + ' — ' + explicit : explicit;
|
||||||
|
return label || text(activity && (activity.summary || activity.activityId)).trim() || translate('ui.event.activity', null, 'Activity event');
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventKind(activity) {
|
function eventKind(activity, translate) {
|
||||||
var type = text(activity && activity.type).toLowerCase();
|
var type = text(activity && activity.type).toLowerCase();
|
||||||
if (type.indexOf('browser.capture') === 0) return 'Capture';
|
if (type.indexOf('browser.capture') === 0) return translate('ui.kind.capture', null, 'Capture');
|
||||||
if (type.indexOf('file.') === 0) return 'File';
|
if (type.indexOf('file.') === 0) return translate('ui.kind.file', null, 'File');
|
||||||
if (type.indexOf('note.') === 0) return 'Note';
|
if (type.indexOf('note.') === 0) return translate('ui.kind.note', null, 'Note');
|
||||||
if (type.indexOf('workspace') !== -1 || type.indexOf('case.') === 0) return 'Workspace';
|
if (type.indexOf('workspace') !== -1 || type.indexOf('case.') === 0) return translate('ui.kind.deal', null, 'Deal');
|
||||||
if (type.indexOf('action.') === 0) return 'Work';
|
if (type.indexOf('action.') === 0) return translate('ui.kind.work', null, 'Work');
|
||||||
return 'Activity';
|
return translate('ui.kind.activity', null, 'Activity');
|
||||||
}
|
}
|
||||||
|
|
||||||
function globalEventKeys(settings) {
|
function globalEventKeys(settings) {
|
||||||
@@ -760,14 +762,10 @@
|
|||||||
el('div', { className: 'activity-time', textContent: formatDate(activity.occurredAt) || '-' }),
|
el('div', { className: 'activity-time', textContent: formatDate(activity.occurredAt) || '-' }),
|
||||||
el('div', { className: 'activity-main' }, [
|
el('div', { className: 'activity-main' }, [
|
||||||
el('div', { className: 'activity-row-head' }, [
|
el('div', { className: 'activity-row-head' }, [
|
||||||
el('span', { className: 'activity-type', textContent: eventKind(activity) }),
|
el('span', { className: 'activity-type', textContent: eventKind(activity, tr) }),
|
||||||
el('span', { className: 'activity-title-text', textContent: humanEventTitle(activity) })
|
el('span', { className: 'activity-title-text', textContent: humanEventTitle(activity, tr) })
|
||||||
]),
|
]),
|
||||||
activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null,
|
activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null
|
||||||
el('details', { className: 'activity-details' }, [
|
|
||||||
el('summary', {}, [tr('ui.details', null, 'Details')]),
|
|
||||||
el('div', { className: 'activity-source', textContent: tr('ui.eventSource', { event: activity.type, source: activity.sourcePluginId ? ' · ' + activity.sourcePluginId : '' }, 'Event: ' + activity.type + (activity.sourcePluginId ? ' · Source: ' + activity.sourcePluginId : '')) })
|
|
||||||
])
|
|
||||||
])
|
])
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
@@ -824,19 +822,19 @@
|
|||||||
el('div', {}, [
|
el('div', {}, [
|
||||||
el('div', { className: 'activity-candidate-title', textContent: tr('ui.candidate', null, 'Possible journal entry') }),
|
el('div', { className: 'activity-candidate-title', textContent: tr('ui.candidate', null, 'Possible journal entry') }),
|
||||||
el('div', { className: 'activity-candidate-facts' }, [
|
el('div', { className: 'activity-candidate-facts' }, [
|
||||||
el('div', { textContent: 'Workspace: ' + candidate.workspaceRootPath }),
|
el('div', { textContent: tr('ui.candidateDeal', { deal: candidate.workspaceRootPath }, 'Deal: ' + candidate.workspaceRootPath) }),
|
||||||
el('div', { textContent: 'Time: ' + candidateTimeRange(candidate) }),
|
el('div', { textContent: tr('ui.candidateTime', { time: candidateTimeRange(candidate) }, 'Time: ' + candidateTimeRange(candidate)) }),
|
||||||
el('div', { textContent: 'Estimated duration: ' + candidate.estimatedMinutes + ' min' }),
|
el('div', { textContent: tr('ui.candidateDuration', { minutes: candidate.estimatedMinutes }, 'Estimated duration: ' + candidate.estimatedMinutes + ' min') }),
|
||||||
el('div', { textContent: 'Activities: ' + candidate.activityCount })
|
el('div', { textContent: tr('ui.candidateActivities', { count: candidate.activityCount }, 'Activities: ' + candidate.activityCount) })
|
||||||
]),
|
]),
|
||||||
el('details', { className: 'activity-candidate-activities' }, [
|
el('details', { className: 'activity-candidate-activities' }, [
|
||||||
el('summary', { textContent: 'Included activities (' + candidate.activityCount + ')' })
|
el('summary', { textContent: tr('ui.includedActivities', { count: candidate.activityCount }, 'Included activities (' + candidate.activityCount + ')') })
|
||||||
].concat(candidate.activities.map(function (activity) {
|
].concat(candidate.activities.map(function (activity) {
|
||||||
return el('div', { className: 'activity-candidate-activity', textContent: activity.occurredAt + ' · ' + activity.type + ' · ' + activity.activityId });
|
return el('div', { className: 'activity-candidate-activity', textContent: activity.occurredAt + ' · ' + humanEventTitle(activity, tr) });
|
||||||
})))
|
})))
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'activity-candidate-actions' }, [
|
el('div', { className: 'activity-candidate-actions' }, [
|
||||||
el('div', { className: 'activity-candidate-duration', textContent: candidate.estimatedMinutes + ' min' }),
|
el('div', { className: 'activity-candidate-duration', textContent: tr('ui.minutes', { count: candidate.estimatedMinutes }, candidate.estimatedMinutes + ' min') }),
|
||||||
el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'review', textContent: tr('ui.review', null, 'Review'), onClick: function () { reviewCandidate(candidate); } }),
|
el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'review', textContent: tr('ui.review', null, 'Review'), onClick: function () { reviewCandidate(candidate); } }),
|
||||||
el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'dismiss', textContent: tr('ui.dismiss', null, 'Dismiss'), onClick: function () { dismissCandidate(candidate); } })
|
el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'dismiss', textContent: tr('ui.dismiss', null, 'Dismiss'), onClick: function () { dismissCandidate(candidate); } })
|
||||||
])
|
])
|
||||||
@@ -845,7 +843,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
countEl.textContent = events.length + ' event' + (events.length === 1 ? '' : 's');
|
countEl.textContent = tr(events.length === 1 ? 'ui.eventCount.one' : 'ui.eventCount.many', { count: events.length }, events.length + ' event' + (events.length === 1 ? '' : 's'));
|
||||||
clearBtn.disabled = events.length === 0;
|
clearBtn.disabled = events.length === 0;
|
||||||
statusEl.textContent = statusText;
|
statusEl.textContent = statusText;
|
||||||
statusEl.className = 'activity-status' + (statusClass ? ' ' + statusClass : '');
|
statusEl.className = 'activity-status' + (statusClass ? ' ' + statusClass : '');
|
||||||
@@ -917,7 +915,9 @@
|
|||||||
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
|
||||||
});
|
});
|
||||||
})).then(function () {
|
})).then(function () {
|
||||||
statusText = scope.mode === 'global' ? 'Listening for all activity' : 'Listening for workspace activity';
|
statusText = scope.mode === 'global'
|
||||||
|
? tr('ui.listeningGlobal', null, 'Listening for all activity')
|
||||||
|
: tr('ui.listeningDeal', null, 'Listening for Deal activity');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
reportError('ui.subscriptionsUnavailable', 'Activity updates are unavailable. Please try again.', err);
|
reportError('ui.subscriptionsUnavailable', 'Activity updates are unavailable. Please try again.', err);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Activity",
|
"manifest.name": "Activity",
|
||||||
"manifest.description": "Workspace-scoped activity log for public plugin events.",
|
"manifest.description": "Deal-scoped activity log for public plugin events.",
|
||||||
"contributions.views.verstak.activity.view.title": "Activity",
|
"contributions.views.verstak.activity.view.title": "Activity",
|
||||||
"contributions.sidebarItems.verstak.activity.sidebar.title": "Activity",
|
"contributions.sidebarItems.verstak.activity.sidebar.title": "Activity",
|
||||||
"contributions.workspaceItems.verstak.activity.workspace.title": "Activity",
|
"contributions.workspaceItems.verstak.activity.workspace.title": "Activity",
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
"ui.workspaceTitle": "Activity · {workspace}",
|
"ui.workspaceTitle": "Activity · {workspace}",
|
||||||
"ui.clear": "Clear",
|
"ui.clear": "Clear",
|
||||||
"ui.clearConfirmTitle": "Clear activity?",
|
"ui.clearConfirmTitle": "Clear activity?",
|
||||||
"ui.clearGlobalWarning": "This permanently deletes all recorded activity and journal suggestions in every case.",
|
"ui.clearGlobalWarning": "This permanently deletes all recorded activity and journal suggestions in every Deal.",
|
||||||
"ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other cases remains.",
|
"ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other Deals remains.",
|
||||||
"ui.confirmClear": "Clear activity",
|
"ui.confirmClear": "Clear activity",
|
||||||
"ui.cancel": "Cancel",
|
"ui.cancel": "Cancel",
|
||||||
"ui.saveError": "Could not save activity. Please try again.",
|
"ui.saveError": "Could not save activity. Please try again.",
|
||||||
@@ -29,5 +29,34 @@
|
|||||||
"ui.candidates": "Possible journal entries",
|
"ui.candidates": "Possible journal entries",
|
||||||
"ui.candidate": "Possible journal entry",
|
"ui.candidate": "Possible journal entry",
|
||||||
"ui.review": "Review",
|
"ui.review": "Review",
|
||||||
"ui.dismiss": "Dismiss"
|
"ui.dismiss": "Dismiss",
|
||||||
|
"ui.event.workspaceSelected": "Deal selected",
|
||||||
|
"ui.event.fileOpened": "File opened",
|
||||||
|
"ui.event.fileChanged": "File changed",
|
||||||
|
"ui.event.noteEdited": "Note edited",
|
||||||
|
"ui.event.workSessionDetected": "Work session detected",
|
||||||
|
"ui.event.browserCaptureReceived": "Browser capture received",
|
||||||
|
"ui.event.pageCaptured": "Page captured",
|
||||||
|
"ui.event.selectionCaptured": "Selection captured",
|
||||||
|
"ui.event.linkCaptured": "Link captured",
|
||||||
|
"ui.event.fileCaptured": "File captured",
|
||||||
|
"ui.event.captureConverted": "Capture converted",
|
||||||
|
"ui.event.browserDomainActivity": "Browser domain activity",
|
||||||
|
"ui.event.activity": "Activity event",
|
||||||
|
"ui.kind.capture": "Capture",
|
||||||
|
"ui.kind.file": "File",
|
||||||
|
"ui.kind.note": "Note",
|
||||||
|
"ui.kind.deal": "Deal",
|
||||||
|
"ui.kind.work": "Work",
|
||||||
|
"ui.kind.activity": "Activity",
|
||||||
|
"ui.candidateDeal": "Deal: {deal}",
|
||||||
|
"ui.candidateTime": "Time: {time}",
|
||||||
|
"ui.candidateDuration": "Estimated duration: {minutes} min",
|
||||||
|
"ui.candidateActivities": "Activities: {count}",
|
||||||
|
"ui.includedActivities": "Included activities ({count})",
|
||||||
|
"ui.minutes": "{count} min",
|
||||||
|
"ui.eventCount.one": "{count} event",
|
||||||
|
"ui.eventCount.many": "{count} events",
|
||||||
|
"ui.listeningGlobal": "Listening for all activity",
|
||||||
|
"ui.listeningDeal": "Listening for Deal activity"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Активность",
|
"manifest.name": "Активность",
|
||||||
"manifest.description": "Журнал публичных событий плагинов в рабочем пространстве.",
|
"manifest.description": "Журнал публичных событий плагинов в Деле.",
|
||||||
"contributions.views.verstak.activity.view.title": "Активность",
|
"contributions.views.verstak.activity.view.title": "Активность",
|
||||||
"contributions.sidebarItems.verstak.activity.sidebar.title": "Активность",
|
"contributions.sidebarItems.verstak.activity.sidebar.title": "Активность",
|
||||||
"contributions.workspaceItems.verstak.activity.workspace.title": "Активность",
|
"contributions.workspaceItems.verstak.activity.workspace.title": "Активность",
|
||||||
@@ -29,5 +29,34 @@
|
|||||||
"ui.candidates": "Возможные записи журнала",
|
"ui.candidates": "Возможные записи журнала",
|
||||||
"ui.candidate": "Возможная запись журнала",
|
"ui.candidate": "Возможная запись журнала",
|
||||||
"ui.review": "Проверить",
|
"ui.review": "Проверить",
|
||||||
"ui.dismiss": "Скрыть"
|
"ui.dismiss": "Скрыть",
|
||||||
|
"ui.event.workspaceSelected": "Выбрано Дело",
|
||||||
|
"ui.event.fileOpened": "Открыт файл",
|
||||||
|
"ui.event.fileChanged": "Изменён файл",
|
||||||
|
"ui.event.noteEdited": "Изменена заметка",
|
||||||
|
"ui.event.workSessionDetected": "Обнаружена рабочая сессия",
|
||||||
|
"ui.event.browserCaptureReceived": "Получено сохранённое из браузера",
|
||||||
|
"ui.event.pageCaptured": "Сохранена страница",
|
||||||
|
"ui.event.selectionCaptured": "Сохранено выделение",
|
||||||
|
"ui.event.linkCaptured": "Сохранена ссылка",
|
||||||
|
"ui.event.fileCaptured": "Сохранён файл",
|
||||||
|
"ui.event.captureConverted": "Сохранённое преобразовано",
|
||||||
|
"ui.event.browserDomainActivity": "Активность на домене в браузере",
|
||||||
|
"ui.event.activity": "Событие активности",
|
||||||
|
"ui.kind.capture": "Сохранённое",
|
||||||
|
"ui.kind.file": "Файл",
|
||||||
|
"ui.kind.note": "Заметка",
|
||||||
|
"ui.kind.deal": "Дело",
|
||||||
|
"ui.kind.work": "Работа",
|
||||||
|
"ui.kind.activity": "Активность",
|
||||||
|
"ui.candidateDeal": "Дело: {deal}",
|
||||||
|
"ui.candidateTime": "Время: {time}",
|
||||||
|
"ui.candidateDuration": "Оценка длительности: {minutes} мин.",
|
||||||
|
"ui.candidateActivities": "Активностей: {count}",
|
||||||
|
"ui.includedActivities": "Учтённые активности ({count})",
|
||||||
|
"ui.minutes": "{count} мин.",
|
||||||
|
"ui.eventCount.one": "Событий: {count}",
|
||||||
|
"ui.eventCount.many": "Событий: {count}",
|
||||||
|
"ui.listeningGlobal": "Учёт всей активности включён",
|
||||||
|
"ui.listeningDeal": "Учёт активности Дела включён"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Activity",
|
"name": "Activity",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped activity log for public plugin events.",
|
"description": "Deal-scoped activity log for public plugin events.",
|
||||||
"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": "activity",
|
"icon": "activity",
|
||||||
|
|||||||
@@ -459,7 +459,7 @@
|
|||||||
if (scope.mode === 'global') {
|
if (scope.mode === 'global') {
|
||||||
filtersEl.appendChild(workspaceFilterEl);
|
filtersEl.appendChild(workspaceFilterEl);
|
||||||
} else {
|
} else {
|
||||||
filtersEl.appendChild(el('span', { className: 'browser-inbox-count', textContent: tr('ui.assignedHere', null, 'Assigned to this workspace') }));
|
filtersEl.appendChild(el('span', { className: 'browser-inbox-count', textContent: tr('ui.assignedHere', null, 'Assigned to this Deal') }));
|
||||||
}
|
}
|
||||||
filtersEl.appendChild(searchInput);
|
filtersEl.appendChild(searchInput);
|
||||||
toolbar.appendChild(filtersEl);
|
toolbar.appendChild(filtersEl);
|
||||||
@@ -865,7 +865,7 @@
|
|||||||
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: tr('ui.browser', null, '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, 'Deal') }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || tr('ui.unassigned', null, '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
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
'.de-root{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a}',
|
'.de-root{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a}',
|
||||||
'.de-toolbar,.de-md-toolbar{display:flex;align-items:center;gap:.45rem;padding:.45rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a;flex-wrap:wrap}',
|
'.de-toolbar,.de-md-toolbar{display:flex;align-items:center;gap:.45rem;padding:.45rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a;flex-wrap:wrap}',
|
||||||
'.de-md-toolbar{background:#101028;padding:.38rem .75rem}',
|
'.de-md-toolbar{background:#101028;padding:.38rem .75rem}',
|
||||||
'.de-toolbar-mode{font-size:.75rem;color:#4ecca3;padding:.15rem .5rem;border-radius:3px;background:#1a2a3a}',
|
|
||||||
'.de-toolbar-context{font-size:.75rem;color:#a0a0bb;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
'.de-toolbar-context{font-size:.75rem;color:#a0a0bb;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||||
'.de-toolbar-spacer{flex:1}',
|
'.de-toolbar-spacer{flex:1}',
|
||||||
'.de-toolbar-btn,.de-md-btn{font-size:.75rem;padding:.28rem .58rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}',
|
'.de-toolbar-btn,.de-md-btn{font-size:.75rem;padding:.28rem .58rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}',
|
||||||
@@ -45,7 +44,6 @@
|
|||||||
'.de-preview table{border-collapse:collapse;margin:.8rem 0;max-width:100%;display:block;overflow:auto}.de-preview th,.de-preview td{border:1px solid #333;padding:.35rem .6rem;text-align:left}.de-preview th{background:#1a1a2e}',
|
'.de-preview table{border-collapse:collapse;margin:.8rem 0;max-width:100%;display:block;overflow:auto}.de-preview th,.de-preview td{border:1px solid #333;padding:.35rem .6rem;text-align:left}.de-preview th{background:#1a1a2e}',
|
||||||
'.de-preview img{max-width:100%;height:auto;border-radius:4px}.de-preview .task{margin-right:.4rem}',
|
'.de-preview img{max-width:100%;height:auto;border-radius:4px}.de-preview .task{margin-right:.4rem}',
|
||||||
'.de-notes-badge{font-size:.65rem;padding:.1rem .4rem;border-radius:3px;background:#2a1a3a;color:#b388ff}',
|
'.de-notes-badge{font-size:.65rem;padding:.1rem .4rem;border-radius:3px;background:#2a1a3a;color:#b388ff}',
|
||||||
'.de-notes-info{padding:.45rem .75rem;background:#111126;border-top:1px solid #16213e;font-size:.75rem;color:#8b8ba8;flex-shrink:0}',
|
|
||||||
'.de-loading,.de-error{flex:1;display:flex;align-items:center;justify-content:center;color:#777;padding:2rem}.de-error{color:#e74c3c;flex-direction:column;gap:.5rem}.de-error-msg{font-size:.85rem;color:#aaa;max-width:420px;text-align:center}',
|
'.de-loading,.de-error{flex:1;display:flex;align-items:center;justify-content:center;color:#777;padding:2rem}.de-error{color:#e74c3c;flex-direction:column;gap:.5rem}.de-error-msg{font-size:.85rem;color:#aaa;max-width:420px;text-align:center}',
|
||||||
'@media(max-width:780px){.de-editor-wrap{flex-direction:column}.de-pane+.de-pane{border-left:0;border-top:1px solid #16213e}.de-toolbar-context{max-width:100%}}'
|
'@media(max-width:780px){.de-editor-wrap{flex-direction:column}.de-pane+.de-pane{border-left:0;border-top:1px solid #16213e}.de-toolbar-context{max-width:100%}}'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
@@ -303,9 +301,8 @@
|
|||||||
containerEl.setAttribute('data-resource-path', resourcePath);
|
containerEl.setAttribute('data-resource-path', resourcePath);
|
||||||
containerEl.setAttribute('data-request-mode', requestedMode);
|
containerEl.setAttribute('data-request-mode', requestedMode);
|
||||||
|
|
||||||
var modeLabel = el('span', { className: 'de-toolbar-mode' }, [editorMode]);
|
|
||||||
var contextLabel = el('span', { className: 'de-toolbar-context', title: resourcePath }, [resourcePath || fileName(resourcePath)]);
|
var contextLabel = el('span', { className: 'de-toolbar-context', title: resourcePath }, [resourcePath || fileName(resourcePath)]);
|
||||||
var notesBadge = editorMode === 'notes-markdown' ? el('span', { className: 'de-notes-badge', 'data-notes-badge': '' }, [tr('ui.notesContext', null, 'notes context')]) : null;
|
var notesBadge = editorMode === 'notes-markdown' ? el('span', { className: 'de-notes-badge', 'data-notes-badge': '' }, [tr('ui.note', null, 'Note')]) : null;
|
||||||
var spacer = el('span', { className: 'de-toolbar-spacer' });
|
var spacer = el('span', { className: 'de-toolbar-spacer' });
|
||||||
var editBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'edit' }, [tr('ui.edit', null, 'Edit')]) : null;
|
var editBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'edit' }, [tr('ui.edit', null, 'Edit')]) : null;
|
||||||
var previewBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'preview' }, [tr('ui.preview', null, 'Preview')]) : null;
|
var previewBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'preview' }, [tr('ui.preview', null, 'Preview')]) : null;
|
||||||
@@ -313,7 +310,7 @@
|
|||||||
var reloadBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'reload' }, [tr('ui.reload', null, 'Reload')]);
|
var reloadBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'reload' }, [tr('ui.reload', null, 'Reload')]);
|
||||||
var saveBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'save' }, [tr('ui.save', null, 'Save')]);
|
var saveBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'save' }, [tr('ui.save', null, 'Save')]);
|
||||||
var statusEl = el('span', { className: 'de-status', 'data-save-state': '' });
|
var statusEl = el('span', { className: 'de-status', 'data-save-state': '' });
|
||||||
var toolbarChildren = [modeLabel, contextLabel];
|
var toolbarChildren = [contextLabel];
|
||||||
if (notesBadge) toolbarChildren.push(notesBadge);
|
if (notesBadge) toolbarChildren.push(notesBadge);
|
||||||
toolbarChildren.push(spacer);
|
toolbarChildren.push(spacer);
|
||||||
[editBtn, previewBtn, splitBtn, reloadBtn, saveBtn, statusEl].forEach(function (node) { if (node) toolbarChildren.push(node); });
|
[editBtn, previewBtn, splitBtn, reloadBtn, saveBtn, statusEl].forEach(function (node) { if (node) toolbarChildren.push(node); });
|
||||||
@@ -323,18 +320,18 @@
|
|||||||
if (isMarkdown) {
|
if (isMarkdown) {
|
||||||
mdToolbar = el('div', { className: 'de-md-toolbar' });
|
mdToolbar = el('div', { className: 'de-md-toolbar' });
|
||||||
[
|
[
|
||||||
['heading', 'H', 'Heading'],
|
['heading', 'H', 'ui.md.heading', 'Heading'],
|
||||||
['bold', 'B', 'Bold'],
|
['bold', 'B', 'ui.md.bold', 'Bold'],
|
||||||
['italic', 'I', 'Italic'],
|
['italic', 'I', 'ui.md.italic', 'Italic'],
|
||||||
['link', 'Link', 'Link'],
|
['link', 'Link', 'ui.md.link', 'Link'],
|
||||||
['code', 'Code', 'Inline code'],
|
['code', 'Code', 'ui.md.inlineCode', 'Inline code'],
|
||||||
['code-block', '```', 'Code block'],
|
['code-block', '```', 'ui.md.codeBlock', 'Code block'],
|
||||||
['bullet', '• List', 'Bullet list'],
|
['bullet', '• List', 'ui.md.bulletList', 'Bullet list'],
|
||||||
['numbered', '1. List', 'Numbered list'],
|
['numbered', '1. List', 'ui.md.numberedList', 'Numbered list'],
|
||||||
['quote', 'Quote', 'Quote'],
|
['quote', 'Quote', 'ui.md.quote', 'Quote'],
|
||||||
['task', 'Task', 'Task item']
|
['task', 'Task', 'ui.md.taskItem', 'Task item']
|
||||||
].forEach(function (item) {
|
].forEach(function (item) {
|
||||||
mdToolbar.appendChild(el('button', { className: 'de-md-btn', 'data-md-action': item[0], title: item[2] }, [item[1]]));
|
mdToolbar.appendChild(el('button', { className: 'de-md-btn', type: 'button', 'data-md-action': item[0], title: tr(item[2], null, item[3]), 'aria-label': tr(item[2], null, item[3]) }, [item[1]]));
|
||||||
});
|
});
|
||||||
containerEl.appendChild(mdToolbar);
|
containerEl.appendChild(mdToolbar);
|
||||||
}
|
}
|
||||||
@@ -342,10 +339,6 @@
|
|||||||
var editorWrap = el('div', { className: 'de-editor-wrap' });
|
var editorWrap = el('div', { className: 'de-editor-wrap' });
|
||||||
containerEl.appendChild(editorWrap);
|
containerEl.appendChild(editorWrap);
|
||||||
|
|
||||||
if (editorMode === 'notes-markdown') {
|
|
||||||
containerEl.appendChild(el('div', { className: 'de-notes-info' }, [tr('ui.notesInfo', null, 'Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.')]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLineNumbers() {
|
function updateLineNumbers() {
|
||||||
if (!linesEl || !textarea) return;
|
if (!linesEl || !textarea) return;
|
||||||
var count = textarea.value.split('\n').length;
|
var count = textarea.value.split('\n').length;
|
||||||
@@ -543,12 +536,25 @@
|
|||||||
reloadFromDisk();
|
reloadFromDisk();
|
||||||
var localeUnsubscribe = api.i18n && typeof api.i18n.onDidChangeLocale === 'function'
|
var localeUnsubscribe = api.i18n && typeof api.i18n.onDidChangeLocale === 'function'
|
||||||
? api.i18n.onDidChangeLocale(function () {
|
? api.i18n.onDidChangeLocale(function () {
|
||||||
if (notesBadge) notesBadge.textContent = tr('ui.notesContext', null, 'notes context');
|
if (notesBadge) notesBadge.textContent = tr('ui.note', null, 'Note');
|
||||||
if (editBtn) editBtn.textContent = tr('ui.edit', null, 'Edit');
|
if (editBtn) editBtn.textContent = tr('ui.edit', null, 'Edit');
|
||||||
if (previewBtn) previewBtn.textContent = tr('ui.preview', null, 'Preview');
|
if (previewBtn) previewBtn.textContent = tr('ui.preview', null, 'Preview');
|
||||||
if (splitBtn) splitBtn.textContent = tr('ui.split', null, 'Split');
|
if (splitBtn) splitBtn.textContent = tr('ui.split', null, 'Split');
|
||||||
reloadBtn.textContent = tr('ui.reload', null, 'Reload');
|
reloadBtn.textContent = tr('ui.reload', null, 'Reload');
|
||||||
saveBtn.textContent = tr('ui.save', null, 'Save');
|
saveBtn.textContent = tr('ui.save', null, 'Save');
|
||||||
|
if (mdToolbar) {
|
||||||
|
[
|
||||||
|
['heading', 'ui.md.heading', 'Heading'], ['bold', 'ui.md.bold', 'Bold'], ['italic', 'ui.md.italic', 'Italic'],
|
||||||
|
['link', 'ui.md.link', 'Link'], ['code', 'ui.md.inlineCode', 'Inline code'], ['code-block', 'ui.md.codeBlock', 'Code block'],
|
||||||
|
['bullet', 'ui.md.bulletList', 'Bullet list'], ['numbered', 'ui.md.numberedList', 'Numbered list'], ['quote', 'ui.md.quote', 'Quote'], ['task', 'ui.md.taskItem', 'Task item']
|
||||||
|
].forEach(function (item) {
|
||||||
|
var button = mdToolbar.querySelector('[data-md-action="' + item[0] + '"]');
|
||||||
|
if (!button) return;
|
||||||
|
var label = tr(item[1], null, item[2]);
|
||||||
|
button.setAttribute('title', label);
|
||||||
|
button.setAttribute('aria-label', label);
|
||||||
|
});
|
||||||
|
}
|
||||||
updateStatus();
|
updateStatus();
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"contributions.openProviders.verstak.default-editor.markdown.title": "Default Markdown Editor",
|
"contributions.openProviders.verstak.default-editor.markdown.title": "Default Markdown Editor",
|
||||||
"contributions.openProviders.verstak.default-editor.notes-markdown.title": "Default Notes Markdown Editor",
|
"contributions.openProviders.verstak.default-editor.notes-markdown.title": "Default Notes Markdown Editor",
|
||||||
"ui.notesContext": "notes context",
|
"ui.notesContext": "notes context",
|
||||||
|
"ui.note": "Note",
|
||||||
"ui.edit": "Edit",
|
"ui.edit": "Edit",
|
||||||
"ui.preview": "Preview",
|
"ui.preview": "Preview",
|
||||||
"ui.split": "Split",
|
"ui.split": "Split",
|
||||||
@@ -18,5 +19,15 @@
|
|||||||
"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": "Could not load the file. Please try again."
|
"ui.loadFailed": "Could not load the file. Please try again.",
|
||||||
|
"ui.md.heading": "Heading",
|
||||||
|
"ui.md.bold": "Bold",
|
||||||
|
"ui.md.italic": "Italic",
|
||||||
|
"ui.md.link": "Link",
|
||||||
|
"ui.md.inlineCode": "Inline code",
|
||||||
|
"ui.md.codeBlock": "Code block",
|
||||||
|
"ui.md.bulletList": "Bullet list",
|
||||||
|
"ui.md.numberedList": "Numbered list",
|
||||||
|
"ui.md.quote": "Quote",
|
||||||
|
"ui.md.taskItem": "Task item"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"contributions.openProviders.verstak.default-editor.markdown.title": "Стандартный редактор Markdown",
|
"contributions.openProviders.verstak.default-editor.markdown.title": "Стандартный редактор Markdown",
|
||||||
"contributions.openProviders.verstak.default-editor.notes-markdown.title": "Стандартный редактор Markdown-заметок",
|
"contributions.openProviders.verstak.default-editor.notes-markdown.title": "Стандартный редактор Markdown-заметок",
|
||||||
"ui.notesContext": "контекст заметки",
|
"ui.notesContext": "контекст заметки",
|
||||||
|
"ui.note": "Заметка",
|
||||||
"ui.edit": "Редактировать",
|
"ui.edit": "Редактировать",
|
||||||
"ui.preview": "Просмотр",
|
"ui.preview": "Просмотр",
|
||||||
"ui.split": "Разделить",
|
"ui.split": "Разделить",
|
||||||
@@ -18,5 +19,15 @@
|
|||||||
"ui.saved": "Сохранено",
|
"ui.saved": "Сохранено",
|
||||||
"ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?",
|
"ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?",
|
||||||
"ui.loading": "Загрузка...",
|
"ui.loading": "Загрузка...",
|
||||||
"ui.loadFailed": "Не удалось загрузить файл. Повторите попытку."
|
"ui.loadFailed": "Не удалось загрузить файл. Повторите попытку.",
|
||||||
|
"ui.md.heading": "Заголовок",
|
||||||
|
"ui.md.bold": "Жирный текст",
|
||||||
|
"ui.md.italic": "Курсив",
|
||||||
|
"ui.md.link": "Ссылка",
|
||||||
|
"ui.md.inlineCode": "Встроенный код",
|
||||||
|
"ui.md.codeBlock": "Блок кода",
|
||||||
|
"ui.md.bulletList": "Маркированный список",
|
||||||
|
"ui.md.numberedList": "Нумерованный список",
|
||||||
|
"ui.md.quote": "Цитата",
|
||||||
|
"ui.md.taskItem": "Пункт задачи"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
# План улучшений файлового менеджера Verstak2
|
|
||||||
|
|
||||||
> **Архитектура:** Плагин `verstak.files` в монорепозитории `verstak-official-plugins`.
|
|
||||||
> Код: `plugins/files/frontend/src/index.js` (vanilla JS, VerstakPluginRegister).
|
|
||||||
> API: `api.files.*`, `api.workbench.openResource()`, `api.settings`, `api.events`, `api.commands`.
|
|
||||||
|
|
||||||
## Порядок реализации
|
|
||||||
|
|
||||||
| # | Feature | Приоритет | Зависит от |
|
|
||||||
|---|---------|-----------|------------|
|
|
||||||
| 1 | Контекстное меню (правый клик) | 🔥 High | — |
|
|
||||||
| 2 | Open External / Show in Explorer | ✅ Done | `files.openExternal` |
|
|
||||||
| 3 | Кастомный ConfirmModal | 🔥 High | — |
|
|
||||||
| 4 | Duplicate | 🔥 High | — |
|
|
||||||
| 5 | Cut/Copy/Paste | 🔥 High | — |
|
|
||||||
| 6 | Множественное выделение | 🔥 High | — |
|
|
||||||
| 7 | Drag-and-Drop | 🔥 High | — |
|
|
||||||
| 8 | Клавиатурная навигация (Ctrl+A, Escape, стрелки) | Medium | #6 |
|
|
||||||
| 9 | Богатые иконки (как в v1) | Medium | — |
|
|
||||||
| 10 | История навигации | Medium | — |
|
|
||||||
| 11 | Rename с валидацией | Medium | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 1: Контекстное меню (правый клик)
|
|
||||||
|
|
||||||
**Описание:** При правом клике на файле/папке показывать меню с действиями.
|
|
||||||
|
|
||||||
**Пункты меню:** Open, Open External, Show in Explorer, Rename, Duplicate, Cut, Copy, Delete — с разделителями.
|
|
||||||
|
|
||||||
**Архитектура:**
|
|
||||||
- Событие `contextmenu` на `.files-item`
|
|
||||||
- Позиционирование у курсора (event.clientX/Y)
|
|
||||||
- Кастомный `<div>` с пунктами, закрывается по клику вне или Escape
|
|
||||||
- Класс `.files-context-menu` для стилей
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 2: Open External / Show in Explorer
|
|
||||||
|
|
||||||
**Описание:** Пункты меню для открытия файла во внешнем приложении и показа
|
|
||||||
файла/папки в системном файловом менеджере.
|
|
||||||
|
|
||||||
**Текущий v2 статус:** плагин использует публичные методы
|
|
||||||
`api.files.openExternal(relativePath)` и `api.files.showInFolder(relativePath)`,
|
|
||||||
guarded by `files.openExternal`. Fallback modal с vault-relative path и кнопкой
|
|
||||||
`Copy Path` показывается только если API недоступен или вернул ошибку.
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 3: Кастомный ConfirmModal
|
|
||||||
|
|
||||||
**Описание:** Вместо `window.confirm()` — стилизованный modal с кнопками Отмена/Удалить, опасным стилем (красная кнопка).
|
|
||||||
|
|
||||||
**Архитектура:**
|
|
||||||
- Функция `confirmModal(message, options)` возвращает Promise
|
|
||||||
- Создаёт overlay + modal в DOM
|
|
||||||
- Закрывается по Escape или кнопке Отмена
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 4: Duplicate
|
|
||||||
|
|
||||||
**Описание:** Копирование файла/папки в той же папке с добавлением суффикса (напр. "filename_copy.ext", "filename_2.ext").
|
|
||||||
|
|
||||||
**Архитектура:**
|
|
||||||
- Для файла: `api.files.readText()` + `api.files.writeText()` с новым именем
|
|
||||||
- Для папки: рекурсивное копирование (сложнее, можно defer)
|
|
||||||
- Пункт в контекстном меню: "Duplicate"
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 5: Cut/Copy/Paste
|
|
||||||
|
|
||||||
**Описание:** Буфер обмена внутри файлового менеджера. Cut → перенос. Copy → дублирование. Paste в текущую папку.
|
|
||||||
|
|
||||||
**Архитектура:**
|
|
||||||
- `clipboard: { items: [...], mode: 'cut'|'copy' }`
|
|
||||||
- Клавиши Ctrl+X/C/V
|
|
||||||
- Пункты в контекстном меню
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 6: Множественное выделение
|
|
||||||
|
|
||||||
**Описание:** Ctrl+click (toggle), Shift+click (range), Ctrl+A (select all). Массовые операции (Trash, Copy, Paste).
|
|
||||||
|
|
||||||
**Архитектура:**
|
|
||||||
- `selectedPaths: Set<string>` вместо `selectedPath: string`
|
|
||||||
- Ctrl+click → toggle entry в Set
|
|
||||||
- Shift+click → range select от последнего клика
|
|
||||||
- Действия применяются ко всем выделенным
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 7: Drag-and-Drop
|
|
||||||
|
|
||||||
**Описание:** Перемещение файлов/папок между папками через drag-and-drop.
|
|
||||||
|
|
||||||
**Архитектура:**
|
|
||||||
- HTML5 DnD API: `dragstart`, `dragover`, `drop` на `.files-item`
|
|
||||||
- При drop на папку → `api.files.move()`
|
|
||||||
- Визуальная индикация (подсветка папки при наведении)
|
|
||||||
- Отмена по Escape
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 8: Клавиатурная навигация
|
|
||||||
|
|
||||||
**Описание:** Escape (снять выделение), Ctrl+A (select all), стрелки (навигация по строкам).
|
|
||||||
|
|
||||||
**Зависит от:** Feature 6 (множественное выделение)
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 9: Богатые иконки
|
|
||||||
|
|
||||||
**Описание:** Как в v1 — отдельные SVG иконки для image, video, audio, pdf, document, spreadsheet, presentation, archive, code, text.
|
|
||||||
|
|
||||||
**Архитектура:** Расширить функцию `fileIcon()`.
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 10: История навигации
|
|
||||||
|
|
||||||
**Описание:** Кнопки Back/Forward в тулбаре, как в браузере. Хранение стека посещённых папок.
|
|
||||||
|
|
||||||
**Архитектура:**
|
|
||||||
- `historyStack: string[]` с индексом
|
|
||||||
- back() / forward() / push(path)
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Feature 11: Rename с валидацией
|
|
||||||
|
|
||||||
**Описание:** Проверять имя через API до применения. Показывать ошибку под полем, а не в placeholder.
|
|
||||||
|
|
||||||
**Архитектура:**
|
|
||||||
- Проверка: не пустое, нет `/`, нет null byte, не `.verstak`
|
|
||||||
- API коллизии: `api.files.metadata(newPath)` — если существует, ошибка
|
|
||||||
|
|
||||||
**Файлы:** `plugins/files/frontend/src/index.js`
|
|
||||||
@@ -34,10 +34,10 @@
|
|||||||
'.files-breadcrumb-item:hover{background:var(--vt-color-accent-muted,rgba(78,204,163,.14))}',
|
'.files-breadcrumb-item:hover{background:var(--vt-color-accent-muted,rgba(78,204,163,.14))}',
|
||||||
'.files-breadcrumb-current{color:var(--vt-color-text-primary,#f4f7fb);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
'.files-breadcrumb-current{color:var(--vt-color-text-primary,#f4f7fb);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
||||||
'.files-breadcrumb-sep{color:var(--vt-color-text-muted,#7f8aa3)}',
|
'.files-breadcrumb-sep{color:var(--vt-color-text-muted,#7f8aa3)}',
|
||||||
'.files-filter,.files-sort,.files-create-input,.files-rename-input{font-size:.78rem;padding:.32rem .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);outline:none}',
|
'.files-filter,.files-sort{font-size:.78rem;padding:.32rem .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);outline:none}',
|
||||||
'.files-filter{width:11rem}',
|
'.files-filter{width:11rem}',
|
||||||
'.files-sort{width:9.5rem;appearance:none;background-color:#0f1424;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 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.6rem}.files-sort option{background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb)}',
|
'.files-sort{width:9.5rem;appearance:none;background-color:#0f1424;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 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.6rem}.files-sort option{background:#0f1424;color:var(--vt-color-text-primary,#f4f7fb)}',
|
||||||
'.files-filter:focus,.files-sort:focus,.files-create-input:focus,.files-rename-input:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
'.files-filter:focus,.files-sort:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
||||||
'.files-list{flex:1;overflow:auto;min-height:0}',
|
'.files-list{flex:1;overflow:auto;min-height:0}',
|
||||||
'.files-header,.files-item{display:grid;grid-template-columns:minmax(160px,1fr) 90px 90px 150px 220px;align-items:center;gap:.5rem;padding:.38rem .75rem;border-bottom:1px solid rgba(22,33,62,.55)}',
|
'.files-header,.files-item{display:grid;grid-template-columns:minmax(160px,1fr) 90px 90px 150px 220px;align-items:center;gap:.5rem;padding:.38rem .75rem;border-bottom:1px solid rgba(22,33,62,.55)}',
|
||||||
'.files-header{position:sticky;top:0;background:var(--vt-color-surface-muted,#111629);color:var(--vt-color-text-muted,#7f8aa3);font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;z-index:1}',
|
'.files-header{position:sticky;top:0;background:var(--vt-color-surface-muted,#111629);color:var(--vt-color-text-muted,#7f8aa3);font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;z-index:1}',
|
||||||
@@ -60,10 +60,6 @@
|
|||||||
'.files-empty-btn svg{width:15px;height:15px;display:block;fill:currentColor}',
|
'.files-empty-btn svg{width:15px;height:15px;display:block;fill:currentColor}',
|
||||||
'.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--vt-color-danger,#e94560);gap:.5rem;padding:1rem}',
|
'.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--vt-color-danger,#e94560);gap:.5rem;padding:1rem}',
|
||||||
'.files-error-msg{font-size:.85rem;color:var(--vt-color-text-secondary,#b7c0d4);max-width:420px;text-align:center}',
|
'.files-error-msg{font-size:.85rem;color:var(--vt-color-text-secondary,#b7c0d4);max-width:420px;text-align:center}',
|
||||||
'.files-panel{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-top:1px solid var(--vt-color-border,#202b46);flex-shrink:0;background:var(--vt-color-surface-muted,#111629)}',
|
|
||||||
'.files-field-stack{display:flex;flex:1;min-width:160px;flex-direction:column;gap:.25rem}',
|
|
||||||
'.files-panel-error{display:none;color:#ff9aaa;font-size:.72rem;line-height:1.2}',
|
|
||||||
'.files-create-input,.files-rename-input{flex:1;min-width:160px}',
|
|
||||||
'@media(max-width:760px){.files-header,.files-item{grid-template-columns:minmax(130px,1fr) 70px 0 0 150px}.files-header span:nth-child(3),.files-header span:nth-child(4),.files-item-meta.hide-narrow{display:none}.files-toolbar{align-items:stretch}.files-filter,.files-sort{width:100%}}',
|
'@media(max-width:760px){.files-header,.files-item{grid-template-columns:minmax(130px,1fr) 70px 0 0 150px}.files-header span:nth-child(3),.files-header span:nth-child(4),.files-item-meta.hide-narrow{display:none}.files-toolbar{align-items:stretch}.files-filter,.files-sort{width:100%}}',
|
||||||
'.files-ctx-menu{position:fixed;z-index:9999;min-width:180px;background:var(--vt-color-surface,#15152c);border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);padding:6px 0;box-shadow:var(--vt-elevation-menu,0 14px 32px rgba(0,0,0,.42));font-size:.84rem;color:var(--vt-color-text-primary,#f4f7fb);user-select:none}',
|
'.files-ctx-menu{position:fixed;z-index:9999;min-width:180px;background:var(--vt-color-surface,#15152c);border:1px solid var(--vt-color-border-strong,#2c456a);border-radius:var(--vt-radius-md,6px);padding:6px 0;box-shadow:var(--vt-elevation-menu,0 14px 32px rgba(0,0,0,.42));font-size:.84rem;color:var(--vt-color-text-primary,#f4f7fb);user-select:none}',
|
||||||
'.files-ctx-menu-item{padding:6px 16px;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:.5rem}',
|
'.files-ctx-menu-item{padding:6px 16px;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:.5rem}',
|
||||||
@@ -75,6 +71,12 @@
|
|||||||
'.files-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center}',
|
'.files-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center}',
|
||||||
'.files-modal{width:400px;max-width:90vw;padding:24px;background:#1a1a2e;border:1px solid #333;border-radius:12px;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.5)}',
|
'.files-modal{width:400px;max-width:90vw;padding:24px;background:#1a1a2e;border:1px solid #333;border-radius:12px;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.5)}',
|
||||||
'.files-modal-title{font-size:.95rem;line-height:1.5;margin-bottom:20px;word-wrap:break-word}',
|
'.files-modal-title{font-size:.95rem;line-height:1.5;margin-bottom:20px;word-wrap:break-word}',
|
||||||
|
'.files-modal-form{display:grid;gap:.65rem}',
|
||||||
|
'.files-modal-form .files-modal-title{margin-bottom:0}',
|
||||||
|
'.files-modal-input{width:100%;font:inherit;font-size:.86rem;padding:.52rem .65rem;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);outline:none}',
|
||||||
|
'.files-modal-input:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
||||||
|
'.files-modal-input[aria-invalid="true"]{border-color:var(--vt-color-danger,#e94560)}',
|
||||||
|
'.files-modal-error{display:none;color:#ff9aaa;font-size:.78rem;line-height:1.35}',
|
||||||
'.files-modal-actions{display:flex;justify-content:flex-end;gap:8px}',
|
'.files-modal-actions{display:flex;justify-content:flex-end;gap:8px}',
|
||||||
'.files-modal-btn{font-size:.82rem;padding:.4rem 1rem;border:1px solid #333;border-radius:6px;cursor:pointer;font-family:inherit}',
|
'.files-modal-btn{font-size:.82rem;padding:.4rem 1rem;border:1px solid #333;border-radius:6px;cursor:pointer;font-family:inherit}',
|
||||||
'.files-modal-btn.cancel{background:#2a2a4e;color:#ccc;border-color:#444}',
|
'.files-modal-btn.cancel{background:#2a2a4e;color:#ccc;border-color:#444}',
|
||||||
@@ -222,7 +224,7 @@
|
|||||||
return EXT_MAP[ext] || 'generic';
|
return EXT_MAP[ext] || 'generic';
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileIconLabel(category) {
|
function fileIconLabel(category, translate) {
|
||||||
var labels = {
|
var labels = {
|
||||||
folder: 'Folder',
|
folder: 'Folder',
|
||||||
markdown: 'Markdown file',
|
markdown: 'Markdown file',
|
||||||
@@ -242,7 +244,8 @@
|
|||||||
json: 'JSON file',
|
json: 'JSON file',
|
||||||
generic: 'File'
|
generic: 'File'
|
||||||
};
|
};
|
||||||
return labels[category] || labels.generic;
|
var label = labels[category] || labels.generic;
|
||||||
|
return typeof translate === 'function' ? translate('ui.icon.' + (labels[category] ? category : 'generic'), null, label) : label;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileIcon(entry) {
|
function fileIcon(entry) {
|
||||||
@@ -265,8 +268,8 @@
|
|||||||
return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function typeLabel(entry) {
|
function typeLabel(entry, folderLabel) {
|
||||||
if (entry.type === 'folder') return 'folder';
|
if (entry.type === 'folder') return folderLabel || 'Folder';
|
||||||
return (entry.extension || extension(entry.name) || 'file').toLowerCase();
|
return (entry.extension || extension(entry.name) || 'file').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,15 +323,16 @@
|
|||||||
return Promise.reject(new Error('clipboard unavailable'));
|
return Promise.reject(new Error('clipboard unavailable'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function showExternalFallback(entry, mode) {
|
function showExternalFallback(entry, mode, labels) {
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
labels = labels || {};
|
||||||
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' ? (labels.showInFolder || 'Show in folder') : (labels.openExternal || 'Open externally');
|
||||||
var message = title + ' failed.\nVault-relative path:\n' + pathToShow;
|
var message = (labels.externalFailed || '{action} failed.').replace('{action}', title) + '\n' + (labels.vaultRelativePath || 'Vault-relative path:') + '\n' + pathToShow;
|
||||||
confirmModal(message, { confirmText: 'Copy Path', cancelText: 'Close' }).then(function (copy) {
|
confirmModal(message, { confirmText: labels.copyPath || 'Copy path', cancelText: labels.close || 'Close' }).then(function (copy) {
|
||||||
if (!copy) return;
|
if (!copy) return;
|
||||||
copyTextToClipboard(pathToShow).catch(function (err) {
|
copyTextToClipboard(pathToShow).catch(function (err) {
|
||||||
console.error('[files] copy path failed:', err);
|
console.error('[files] copy path failed:', err);
|
||||||
@@ -346,7 +350,7 @@
|
|||||||
|
|
||||||
var workspaceNode = props && props.workspaceNode;
|
var workspaceNode = props && props.workspaceNode;
|
||||||
var workspaceRoot = cleanPath(props && (props.workspaceRootPath || (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id))) || '');
|
var workspaceRoot = cleanPath(props && (props.workspaceRootPath || (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id))) || '');
|
||||||
var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title || workspaceNode.id)) || 'Workspace';
|
var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title || workspaceNode.id)) || '';
|
||||||
window.__filesHistoryByWorkspace = window.__filesHistoryByWorkspace || {};
|
window.__filesHistoryByWorkspace = window.__filesHistoryByWorkspace || {};
|
||||||
var historyKey = workspaceRoot || workspaceName;
|
var historyKey = workspaceRoot || workspaceName;
|
||||||
var savedHistory = window.__filesHistoryByWorkspace[historyKey] || { stack: [''], index: 0, currentPath: '' };
|
var savedHistory = window.__filesHistoryByWorkspace[historyKey] || { stack: [''], index: 0, currentPath: '' };
|
||||||
@@ -358,6 +362,12 @@
|
|||||||
var sortMode = 'folder-name';
|
var sortMode = 'folder-name';
|
||||||
var createMode = '';
|
var createMode = '';
|
||||||
var renameTarget = null;
|
var renameTarget = null;
|
||||||
|
var createModal = null;
|
||||||
|
var createInput = null;
|
||||||
|
var createError = null;
|
||||||
|
var renameModal = null;
|
||||||
|
var renameInput = null;
|
||||||
|
var renameError = null;
|
||||||
var disposed = false;
|
var disposed = false;
|
||||||
var fileActions = [];
|
var fileActions = [];
|
||||||
var contextMenuEntries = [];
|
var contextMenuEntries = [];
|
||||||
@@ -374,6 +384,10 @@
|
|||||||
return fallback || key;
|
return fallback || key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function displayType(entry) {
|
||||||
|
return typeLabel(entry, tr('ui.folderType', null, 'Folder'));
|
||||||
|
}
|
||||||
|
|
||||||
function reportError(key, fallback, err) {
|
function reportError(key, fallback, err) {
|
||||||
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
||||||
console.warn('[verstak.files] ' + key, err);
|
console.warn('[verstak.files] ' + key, err);
|
||||||
@@ -438,32 +452,6 @@
|
|||||||
var listContainer = el('div', { className: 'files-list', 'data-files-list': '' });
|
var listContainer = el('div', { className: 'files-list', 'data-files-list': '' });
|
||||||
containerEl.appendChild(listContainer);
|
containerEl.appendChild(listContainer);
|
||||||
|
|
||||||
var createPanel = el('div', { className: 'files-panel', style: { display: 'none' } });
|
|
||||||
var createField = el('div', { className: 'files-field-stack' });
|
|
||||||
var createInput = el('input', { className: 'files-create-input', 'data-files-create-input': '' });
|
|
||||||
var createError = el('div', { className: 'files-panel-error', 'data-files-create-error': '', role: 'alert' });
|
|
||||||
var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, [tr('ui.create', null, 'Create')]);
|
|
||||||
var createCancel = el('button', { className: 'files-toolbar-btn' }, [tr('ui.cancel', null, 'Cancel')]);
|
|
||||||
createField.appendChild(createInput);
|
|
||||||
createField.appendChild(createError);
|
|
||||||
createPanel.appendChild(createField);
|
|
||||||
createPanel.appendChild(createConfirm);
|
|
||||||
createPanel.appendChild(createCancel);
|
|
||||||
containerEl.appendChild(createPanel);
|
|
||||||
|
|
||||||
var renamePanel = el('div', { className: 'files-panel', style: { display: 'none' } });
|
|
||||||
var renameField = el('div', { className: 'files-field-stack' });
|
|
||||||
var renameInput = el('input', { className: 'files-rename-input', 'data-files-rename-input': '' });
|
|
||||||
var renameError = el('div', { className: 'files-panel-error', 'data-files-rename-error': '', role: 'alert' });
|
|
||||||
var renameConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-rename-confirm': '' }, [tr('ui.rename', null, 'Rename')]);
|
|
||||||
var renameCancel = el('button', { className: 'files-toolbar-btn' }, [tr('ui.cancel', null, 'Cancel')]);
|
|
||||||
renameField.appendChild(renameInput);
|
|
||||||
renameField.appendChild(renameError);
|
|
||||||
renamePanel.appendChild(renameField);
|
|
||||||
renamePanel.appendChild(renameConfirm);
|
|
||||||
renamePanel.appendChild(renameCancel);
|
|
||||||
containerEl.appendChild(renamePanel);
|
|
||||||
|
|
||||||
function selectedEntry() {
|
function selectedEntry() {
|
||||||
var keys = Object.keys(selectedPaths);
|
var keys = Object.keys(selectedPaths);
|
||||||
if (keys.length === 0) return null;
|
if (keys.length === 0) return null;
|
||||||
@@ -540,7 +528,7 @@
|
|||||||
if (a.type !== 'folder' && b.type === 'folder') return 1;
|
if (a.type !== 'folder' && b.type === 'folder') return 1;
|
||||||
}
|
}
|
||||||
if (sortMode === 'type') {
|
if (sortMode === 'type') {
|
||||||
var typeCmp = typeLabel(a).localeCompare(typeLabel(b));
|
var typeCmp = displayType(a).localeCompare(displayType(b));
|
||||||
if (typeCmp) return typeCmp;
|
if (typeCmp) return typeCmp;
|
||||||
}
|
}
|
||||||
if (sortMode === 'modified-desc') {
|
if (sortMode === 'modified-desc') {
|
||||||
@@ -662,7 +650,7 @@
|
|||||||
|
|
||||||
shown.forEach(function (entry) {
|
shown.forEach(function (entry) {
|
||||||
var iconCategory = fileIconCategory(entry);
|
var iconCategory = fileIconCategory(entry);
|
||||||
var iconLabel = fileIconLabel(iconCategory);
|
var iconLabel = fileIconLabel(iconCategory, tr);
|
||||||
var row = el('div', {
|
var row = el('div', {
|
||||||
className: 'files-item' + (selectedPaths[entry.relativePath] ? ' selected' : ''),
|
className: 'files-item' + (selectedPaths[entry.relativePath] ? ' selected' : ''),
|
||||||
'data-file-name': entry.name,
|
'data-file-name': entry.name,
|
||||||
@@ -695,7 +683,7 @@
|
|||||||
el('span', { className: 'files-item-icon', 'data-file-icon': iconCategory, title: iconLabel, 'aria-label': iconLabel, innerHTML: fileIcon(entry) }),
|
el('span', { className: 'files-item-icon', 'data-file-icon': iconCategory, title: iconLabel, 'aria-label': iconLabel, innerHTML: fileIcon(entry) }),
|
||||||
el('span', { className: 'files-item-name', textContent: entry.name, title: entry.name })
|
el('span', { className: 'files-item-name', textContent: entry.name, title: entry.name })
|
||||||
]),
|
]),
|
||||||
el('span', { className: 'files-item-meta' }, [typeLabel(entry)]),
|
el('span', { className: 'files-item-meta' }, [displayType(entry)]),
|
||||||
el('span', { className: 'files-item-meta hide-narrow' }, [entry.type === 'folder' ? '' : formatSize(entry.size)]),
|
el('span', { className: 'files-item-meta hide-narrow' }, [entry.type === 'folder' ? '' : formatSize(entry.size)]),
|
||||||
el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.modifiedAt)]),
|
el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.modifiedAt)]),
|
||||||
el('div', { className: 'files-row-actions' }, [
|
el('div', { className: 'files-row-actions' }, [
|
||||||
@@ -820,35 +808,131 @@
|
|||||||
}).catch(function (err) { console.error('[files] openResource error:', err); });
|
}).catch(function (err) { console.error('[files] openResource error:', err); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showFileFormModal(mode, details) {
|
||||||
|
var isRename = mode === 'rename';
|
||||||
|
var overlay = el('div', {
|
||||||
|
className: 'files-modal-overlay',
|
||||||
|
role: 'presentation'
|
||||||
|
});
|
||||||
|
overlay.setAttribute('data-files-' + mode + '-modal', '');
|
||||||
|
var input = el('input', {
|
||||||
|
className: 'files-modal-input',
|
||||||
|
type: 'text',
|
||||||
|
value: details.value || '',
|
||||||
|
placeholder: details.placeholder || '',
|
||||||
|
autocomplete: 'off',
|
||||||
|
'aria-invalid': 'false'
|
||||||
|
});
|
||||||
|
input.setAttribute('data-files-' + mode + '-input', '');
|
||||||
|
input.value = details.value || '';
|
||||||
|
var error = el('div', {
|
||||||
|
className: 'files-modal-error',
|
||||||
|
role: 'alert'
|
||||||
|
});
|
||||||
|
error.setAttribute('data-files-' + mode + '-error', '');
|
||||||
|
function closeForm() {
|
||||||
|
if (isRename) cancelRename();
|
||||||
|
else cancelCreate();
|
||||||
|
}
|
||||||
|
function confirmForm() {
|
||||||
|
if (isRename) confirmRename();
|
||||||
|
else confirmCreate();
|
||||||
|
}
|
||||||
|
input.addEventListener('input', function () {
|
||||||
|
if (isRename) setRenameError('');
|
||||||
|
else setCreateError('');
|
||||||
|
});
|
||||||
|
input.addEventListener('keydown', function (event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
if (event.preventDefault) event.preventDefault();
|
||||||
|
confirmForm();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
if (event.preventDefault) event.preventDefault();
|
||||||
|
closeForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var modal = el('div', {
|
||||||
|
className: 'files-modal files-modal-form',
|
||||||
|
role: 'dialog',
|
||||||
|
'aria-modal': 'true',
|
||||||
|
'aria-label': details.title
|
||||||
|
}, [
|
||||||
|
el('div', { className: 'files-modal-title', textContent: details.title }),
|
||||||
|
input,
|
||||||
|
error,
|
||||||
|
el('div', { className: 'files-modal-actions' }, [
|
||||||
|
el('button', {
|
||||||
|
className: 'files-modal-btn cancel',
|
||||||
|
type: 'button',
|
||||||
|
textContent: tr('ui.cancel', null, 'Cancel'),
|
||||||
|
onClick: closeForm
|
||||||
|
}),
|
||||||
|
el('button', {
|
||||||
|
className: 'files-modal-btn confirm',
|
||||||
|
type: 'button',
|
||||||
|
textContent: details.confirmLabel,
|
||||||
|
onClick: confirmForm
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
overlay.appendChild(modal);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
if (isRename) {
|
||||||
|
renameInput = input;
|
||||||
|
renameError = error;
|
||||||
|
} else {
|
||||||
|
createInput = input;
|
||||||
|
createError = error;
|
||||||
|
}
|
||||||
|
input.focus();
|
||||||
|
if (isRename && input.select) input.select();
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
function startCreate(mode) {
|
function startCreate(mode) {
|
||||||
|
cancelCreate();
|
||||||
createMode = mode;
|
createMode = mode;
|
||||||
createInput.value = '';
|
createModal = showFileFormModal('create', {
|
||||||
createInput.placeholder = mode === 'folder' ? 'Folder name' : (mode === 'markdown' ? 'Markdown file name' : 'Text file name');
|
title: mode === 'folder'
|
||||||
setCreateError('');
|
? tr('ui.createFolderTitle', null, 'Create folder')
|
||||||
createPanel.style.display = 'flex';
|
: (mode === 'markdown'
|
||||||
createInput.focus();
|
? tr('ui.createMarkdownTitle', null, 'Create Markdown file')
|
||||||
|
: tr('ui.createTextTitle', null, 'Create text file')),
|
||||||
|
placeholder: mode === 'folder'
|
||||||
|
? tr('ui.folderName', null, 'Folder name')
|
||||||
|
: (mode === 'markdown'
|
||||||
|
? tr('ui.markdownFileName', null, 'Markdown file name')
|
||||||
|
: tr('ui.textFileName', null, 'Text file name')),
|
||||||
|
confirmLabel: tr('ui.create', null, 'Create')
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCreateError(message) {
|
function setCreateError(message) {
|
||||||
|
if (!createError || !createInput) return;
|
||||||
createError.textContent = message || '';
|
createError.textContent = message || '';
|
||||||
createError.style.display = message ? 'block' : 'none';
|
createError.style.display = message ? 'block' : 'none';
|
||||||
createInput.setAttribute('aria-invalid', message ? 'true' : 'false');
|
createInput.setAttribute('aria-invalid', message ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCreateName(name) {
|
function validateCreateName(name) {
|
||||||
if (!name) return 'Name is required';
|
if (!name) return tr('ui.nameRequired', null, 'Enter a name.');
|
||||||
if (/[\\/:*?"<>|\x00-\x1f]/.test(name)) return 'Invalid characters in name';
|
if (/[\\/:*?"<>|\x00-\x1f]/.test(name)) return tr('ui.invalidCharacters', null, 'The name contains invalid characters.');
|
||||||
if (name === '.' || name === '..' || name[0] === ' ' || name[name.length - 1] === ' ' || name[name.length - 1] === '.') return 'Invalid name';
|
if (name === '.' || name === '..' || name[0] === ' ' || name[name.length - 1] === ' ' || name[name.length - 1] === '.') return tr('ui.invalidName', null, 'Enter a valid name.');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelCreate() {
|
function cancelCreate() {
|
||||||
createMode = '';
|
createMode = '';
|
||||||
setCreateError('');
|
if (createModal && typeof createModal.remove === 'function') createModal.remove();
|
||||||
createPanel.style.display = 'none';
|
else if (createModal && createModal.parentNode) createModal.parentNode.removeChild(createModal);
|
||||||
|
createModal = null;
|
||||||
|
createInput = null;
|
||||||
|
createError = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmCreate() {
|
function confirmCreate() {
|
||||||
|
if (!createInput || !createMode) return;
|
||||||
var name = createInput.value.trim();
|
var name = createInput.value.trim();
|
||||||
var validationError = validateCreateName(name);
|
var validationError = validateCreateName(name);
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
@@ -877,39 +961,49 @@
|
|||||||
function beginRename(entry) {
|
function beginRename(entry) {
|
||||||
entry = entry || selectedEntry();
|
entry = entry || selectedEntry();
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
cancelRename();
|
||||||
renameTarget = entry;
|
renameTarget = entry;
|
||||||
renameInput.value = entry.name;
|
renameModal = showFileFormModal('rename', {
|
||||||
setRenameError('');
|
title: tr('ui.renameTitle', { name: entry.name }, 'Rename ' + entry.name),
|
||||||
renamePanel.style.display = 'flex';
|
placeholder: tr('ui.fileName', null, 'File or folder name'),
|
||||||
renameInput.focus();
|
value: entry.name,
|
||||||
renameInput.select();
|
confirmLabel: tr('ui.rename', null, 'Rename')
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelRename() {
|
function cancelRename() {
|
||||||
renameTarget = null;
|
renameTarget = null;
|
||||||
setRenameError('');
|
if (renameModal && typeof renameModal.remove === 'function') renameModal.remove();
|
||||||
renamePanel.style.display = 'none';
|
else if (renameModal && renameModal.parentNode) renameModal.parentNode.removeChild(renameModal);
|
||||||
|
renameModal = null;
|
||||||
|
renameInput = null;
|
||||||
|
renameError = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRenameError(message) {
|
function setRenameError(message) {
|
||||||
|
if (!renameError || !renameInput) return;
|
||||||
renameError.textContent = message || '';
|
renameError.textContent = message || '';
|
||||||
renameError.style.display = message ? 'block' : 'none';
|
renameError.style.display = message ? 'block' : 'none';
|
||||||
renameInput.setAttribute('aria-invalid', message ? 'true' : 'false');
|
renameInput.setAttribute('aria-invalid', message ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmRename() {
|
function confirmRename() {
|
||||||
if (!renameTarget) return;
|
if (!renameTarget || !renameInput) return;
|
||||||
var newName = renameInput.value.trim();
|
var newName = renameInput.value.trim();
|
||||||
if (!newName || newName === renameTarget.name) {
|
if (!newName) {
|
||||||
|
setRenameError(tr('ui.nameRequired', null, 'Enter a name.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newName === renameTarget.name) {
|
||||||
cancelRename();
|
cancelRename();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (/[\\/:*?"<>|\x00-\x1f]/.test(newName)) {
|
if (/[\\/:*?"<>|\x00-\x1f]/.test(newName)) {
|
||||||
setRenameError('Invalid characters in name');
|
setRenameError(tr('ui.invalidCharacters', null, 'The name contains invalid characters.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newName === '.' || newName === '..' || newName[0] === ' ' || newName[newName.length - 1] === ' ' || newName[newName.length - 1] === '.') {
|
if (newName === '.' || newName === '..' || newName[0] === ' ' || newName[newName.length - 1] === ' ' || newName[newName.length - 1] === '.') {
|
||||||
setRenameError('Invalid name');
|
setRenameError(tr('ui.invalidName', null, 'Enter a valid name.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var from = renameTarget.relativePath;
|
var from = renameTarget.relativePath;
|
||||||
@@ -917,17 +1011,17 @@
|
|||||||
var to = targetParent ? targetParent + '/' + newName : newName;
|
var to = targetParent ? targetParent + '/' + newName : newName;
|
||||||
api.files.metadata(to).then(function () {
|
api.files.metadata(to).then(function () {
|
||||||
if (to.toLowerCase() === from.toLowerCase() && to !== from) {
|
if (to.toLowerCase() === from.toLowerCase() && to !== from) {
|
||||||
setRenameError('Name differs only by case');
|
setRenameError(tr('ui.nameDiffersOnlyByCase', null, 'The name differs only by letter case.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRenameError('A file with that name already exists');
|
setRenameError(tr('ui.nameConflict', null, 'An item with that name already exists.'));
|
||||||
}, function () {
|
}, function () {
|
||||||
api.files.move(from, to, { overwrite: false }).then(function () {
|
api.files.move(from, to, { overwrite: false }).then(function () {
|
||||||
cancelRename();
|
cancelRename();
|
||||||
loadEntries();
|
loadEntries();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
if (isConflictError(err)) {
|
if (isConflictError(err)) {
|
||||||
setRenameError('A file with that name already exists');
|
setRenameError(tr('ui.nameConflict', null, 'An item with that name already exists.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRenameError(reportError('ui.renameError', 'Could not rename this item. Please try again.', err));
|
setRenameError(reportError('ui.renameError', 'Could not rename this item. Please try again.', err));
|
||||||
@@ -938,7 +1032,11 @@
|
|||||||
function trashEntry(entry) {
|
function trashEntry(entry) {
|
||||||
var count = selectedCount();
|
var count = selectedCount();
|
||||||
if (entry) {
|
if (entry) {
|
||||||
confirmModal('Move "' + entry.name + '" to trash?', { danger: true }).then(function (ok) {
|
confirmModal(tr('ui.trashConfirm', { name: entry.name }, 'Move "' + entry.name + '" to trash?'), {
|
||||||
|
danger: true,
|
||||||
|
confirmText: tr('ui.trash', null, 'Move to trash'),
|
||||||
|
cancelText: tr('ui.cancel', null, 'Cancel')
|
||||||
|
}).then(function (ok) {
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
api.files.trash(entry.relativePath).then(function () {
|
api.files.trash(entry.relativePath).then(function () {
|
||||||
loadEntries();
|
loadEntries();
|
||||||
@@ -947,7 +1045,11 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (count > 1) {
|
} else if (count > 1) {
|
||||||
confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) {
|
confirmModal(tr('ui.trashManyConfirm', { count: count }, 'Move ' + count + ' items to trash?'), {
|
||||||
|
danger: true,
|
||||||
|
confirmText: tr('ui.trash', null, 'Move to trash'),
|
||||||
|
cancelText: tr('ui.cancel', null, 'Cancel')
|
||||||
|
}).then(function (ok) {
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
var paths = Object.keys(selectedPaths);
|
var paths = Object.keys(selectedPaths);
|
||||||
Promise.allSettled(paths.map(function (p) { return api.files.trash(p); })).then(function () {
|
Promise.allSettled(paths.map(function (p) { return api.files.trash(p); })).then(function () {
|
||||||
@@ -963,14 +1065,6 @@
|
|||||||
|
|
||||||
filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); });
|
filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); });
|
||||||
sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); });
|
sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); });
|
||||||
createConfirm.addEventListener('click', confirmCreate);
|
|
||||||
createCancel.addEventListener('click', cancelCreate);
|
|
||||||
renameConfirm.addEventListener('click', confirmRename);
|
|
||||||
renameCancel.addEventListener('click', cancelRename);
|
|
||||||
createInput.addEventListener('input', function () { setCreateError(''); });
|
|
||||||
createInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmCreate(); if (event.key === 'Escape') cancelCreate(); });
|
|
||||||
renameInput.addEventListener('input', function () { setRenameError(''); });
|
|
||||||
renameInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmRename(); if (event.key === 'Escape') cancelRename(); });
|
|
||||||
/* --- Context menu --- */
|
/* --- Context menu --- */
|
||||||
var ctxMenu = el('div', { className: 'files-ctx-menu', style: { display: 'none' } });
|
var ctxMenu = el('div', { className: 'files-ctx-menu', style: { display: 'none' } });
|
||||||
document.body.appendChild(ctxMenu);
|
document.body.appendChild(ctxMenu);
|
||||||
@@ -1000,15 +1094,23 @@
|
|||||||
|
|
||||||
function openExternalEntry(entry, mode) {
|
function openExternalEntry(entry, mode) {
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
|
var fallbackLabels = {
|
||||||
|
openExternal: tr('ui.openExternal', null, 'Open externally'),
|
||||||
|
showInFolder: tr('ui.showInFolder', null, 'Show in folder'),
|
||||||
|
externalFailed: tr('ui.externalFailed', null, '{action} failed.'),
|
||||||
|
vaultRelativePath: tr('ui.vaultRelativePath', null, 'Vault-relative path:'),
|
||||||
|
copyPath: tr('ui.copyPath', null, 'Copy path'),
|
||||||
|
close: tr('ui.close', null, 'Close')
|
||||||
|
};
|
||||||
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);
|
showExternalFallback(entry, mode, fallbackLabels);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
action(entry.relativePath).catch(function (err) {
|
action(entry.relativePath).catch(function (err) {
|
||||||
console.warn('[verstak.files] external open error:', err);
|
console.warn('[verstak.files] external open error:', err);
|
||||||
showExternalFallback(entry, mode);
|
showExternalFallback(entry, mode, fallbackLabels);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1054,27 +1156,27 @@
|
|||||||
renderList();
|
renderList();
|
||||||
}
|
}
|
||||||
var isFolder = entry.type === 'folder';
|
var isFolder = entry.type === 'folder';
|
||||||
ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }, 'open', 'open'));
|
ctxMenu.appendChild(ctxItem(isFolder ? tr('ui.openFolder', null, 'Open folder') : tr('ui.open', null, 'Open'), '', function () { openEntry(entry); }, 'open', 'open'));
|
||||||
ctxMenu.appendChild(ctxItem('Open External', '', function () { openExternalEntry(entry, 'external'); }, 'open-external', 'external'));
|
ctxMenu.appendChild(ctxItem(tr('ui.openExternal', null, 'Open externally'), '', function () { openExternalEntry(entry, 'external'); }, 'open-external', 'external'));
|
||||||
ctxMenu.appendChild(ctxItem('Show in Explorer', '', function () { openExternalEntry(entry, 'explorer'); }, 'show-in-explorer', 'explorer'));
|
ctxMenu.appendChild(ctxItem(tr('ui.showInFolder', null, 'Show in folder'), '', function () { openExternalEntry(entry, 'explorer'); }, 'show-in-explorer', 'explorer'));
|
||||||
appendContributionMenuItems(entry);
|
appendContributionMenuItems(entry);
|
||||||
ctxMenu.appendChild(ctxSep());
|
ctxMenu.appendChild(ctxSep());
|
||||||
ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename'));
|
ctxMenu.appendChild(ctxItem(tr('ui.rename', null, 'Rename'), '', function () { beginRename(entry); }, 'rename', 'rename'));
|
||||||
if (entry.type !== 'folder') {
|
if (entry.type !== 'folder') {
|
||||||
ctxMenu.appendChild(ctxItem('Duplicate', '', function () { duplicateEntry(entry); }, 'duplicate', 'duplicate'));
|
ctxMenu.appendChild(ctxItem(tr('ui.duplicate', null, 'Duplicate'), '', function () { duplicateEntry(entry); }, 'duplicate', 'duplicate'));
|
||||||
}
|
}
|
||||||
ctxMenu.appendChild(ctxSep());
|
ctxMenu.appendChild(ctxSep());
|
||||||
ctxMenu.appendChild(ctxItem('Cut', '', function () { cutSelection(); }, 'cut', 'cut'));
|
ctxMenu.appendChild(ctxItem(tr('ui.cut', null, 'Cut'), '', function () { cutSelection(); }, 'cut', 'cut'));
|
||||||
ctxMenu.appendChild(ctxItem('Copy', '', function () { copySelection(); }, 'copy', 'copy'));
|
ctxMenu.appendChild(ctxItem(tr('ui.copy', null, 'Copy'), '', function () { copySelection(); }, 'copy', 'copy'));
|
||||||
ctxMenu.appendChild(ctxSep());
|
ctxMenu.appendChild(ctxSep());
|
||||||
ctxMenu.appendChild(ctxItem('Move to Trash', 'danger', function () { trashEntry(); }, 'trash', 'trash'));
|
ctxMenu.appendChild(ctxItem(tr('ui.trash', null, 'Move to trash'), 'danger', function () { trashEntry(); }, 'trash', 'trash'));
|
||||||
} else {
|
} else {
|
||||||
ctxMenu.appendChild(ctxItem('New Folder', '', function () { startCreate('folder'); }, 'new-folder', 'folderAdd'));
|
ctxMenu.appendChild(ctxItem(tr('ui.newFolder', null, 'New folder'), '', function () { startCreate('folder'); }, 'new-folder', 'folderAdd'));
|
||||||
ctxMenu.appendChild(ctxItem('New Markdown', '', function () { startCreate('markdown'); }, 'new-markdown', 'markdownAdd'));
|
ctxMenu.appendChild(ctxItem(tr('ui.newMarkdown', null, 'New Markdown file'), '', function () { startCreate('markdown'); }, 'new-markdown', 'markdownAdd'));
|
||||||
ctxMenu.appendChild(ctxItem('New Text', '', function () { startCreate('text'); }, 'new-text', 'textAdd'));
|
ctxMenu.appendChild(ctxItem(tr('ui.newText', null, 'New text file'), '', function () { startCreate('text'); }, 'new-text', 'textAdd'));
|
||||||
if (window.__filesClipboard && window.__filesClipboard.items && window.__filesClipboard.items.length) {
|
if (window.__filesClipboard && window.__filesClipboard.items && window.__filesClipboard.items.length) {
|
||||||
ctxMenu.appendChild(ctxSep());
|
ctxMenu.appendChild(ctxSep());
|
||||||
ctxMenu.appendChild(ctxItem('Paste', '', function () { pasteEntry(); }, 'paste', 'paste'));
|
ctxMenu.appendChild(ctxItem(tr('ui.paste', null, 'Paste'), '', function () { pasteEntry(); }, 'paste', 'paste'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctxMenu.style.display = 'block';
|
ctxMenu.style.display = 'block';
|
||||||
@@ -1167,7 +1269,7 @@
|
|||||||
var clip = window.__filesClipboard;
|
var clip = window.__filesClipboard;
|
||||||
if (!clip || !clip.items || clip.items.length === 0) return;
|
if (!clip || !clip.items || clip.items.length === 0) return;
|
||||||
if (clip.workspaceRoot && clip.workspaceRoot !== workspaceRoot) {
|
if (clip.workspaceRoot && clip.workspaceRoot !== workspaceRoot) {
|
||||||
window.alert('Clipboard items belong to another workspace.');
|
window.alert(tr('ui.clipboardDifferentDeal', null, 'Clipboard items belong to another Deal.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var destinationDir = scopedPath(currentPath);
|
var destinationDir = scopedPath(currentPath);
|
||||||
@@ -1508,10 +1610,6 @@
|
|||||||
item[0].setAttribute('aria-label', label);
|
item[0].setAttribute('aria-label', label);
|
||||||
});
|
});
|
||||||
filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter current folder'));
|
filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter current folder'));
|
||||||
createConfirm.textContent = tr('ui.create', null, 'Create');
|
|
||||||
createCancel.textContent = tr('ui.cancel', null, 'Cancel');
|
|
||||||
renameConfirm.textContent = tr('ui.rename', null, 'Rename');
|
|
||||||
renameCancel.textContent = tr('ui.cancel', null, 'Cancel');
|
|
||||||
renderList();
|
renderList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1530,6 +1628,8 @@
|
|||||||
|
|
||||||
containerEl.__filesCleanup = function () {
|
containerEl.__filesCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
cancelCreate();
|
||||||
|
cancelRename();
|
||||||
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
|
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
|
||||||
document.removeEventListener('click', onDocClick);
|
document.removeEventListener('click', onDocClick);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Files",
|
"manifest.name": "Files",
|
||||||
"manifest.description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.",
|
"manifest.description": "Deal-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.",
|
||||||
"contributions.workspaceItems.verstak.files.workspace.title": "Files",
|
"contributions.workspaceItems.verstak.files.workspace.title": "Files",
|
||||||
"ui.back": "Back",
|
"ui.back": "Back",
|
||||||
"ui.forward": "Forward",
|
"ui.forward": "Forward",
|
||||||
@@ -10,7 +10,11 @@
|
|||||||
"ui.newMarkdown": "New markdown file",
|
"ui.newMarkdown": "New markdown file",
|
||||||
"ui.newText": "New text file",
|
"ui.newText": "New text file",
|
||||||
"ui.open": "Open",
|
"ui.open": "Open",
|
||||||
|
"ui.openFolder": "Open folder",
|
||||||
|
"ui.openExternal": "Open externally",
|
||||||
|
"ui.showInFolder": "Show in folder",
|
||||||
"ui.rename": "Rename",
|
"ui.rename": "Rename",
|
||||||
|
"ui.duplicate": "Duplicate",
|
||||||
"ui.trash": "Move to trash",
|
"ui.trash": "Move to trash",
|
||||||
"ui.cut": "Cut",
|
"ui.cut": "Cut",
|
||||||
"ui.copy": "Copy",
|
"ui.copy": "Copy",
|
||||||
@@ -22,8 +26,46 @@
|
|||||||
"ui.column.modified": "Modified",
|
"ui.column.modified": "Modified",
|
||||||
"ui.column.size": "Size",
|
"ui.column.size": "Size",
|
||||||
"ui.column.actions": "Actions",
|
"ui.column.actions": "Actions",
|
||||||
|
"ui.folderType": "Folder",
|
||||||
|
"ui.icon.folder": "Folder",
|
||||||
|
"ui.icon.markdown": "Markdown file",
|
||||||
|
"ui.icon.image": "Image file",
|
||||||
|
"ui.icon.video": "Video file",
|
||||||
|
"ui.icon.audio": "Audio file",
|
||||||
|
"ui.icon.archive": "Archive file",
|
||||||
|
"ui.icon.pdf": "PDF file",
|
||||||
|
"ui.icon.text": "Text file",
|
||||||
|
"ui.icon.document": "Document file",
|
||||||
|
"ui.icon.spreadsheet": "Spreadsheet file",
|
||||||
|
"ui.icon.presentation": "Presentation file",
|
||||||
|
"ui.icon.code": "Code file",
|
||||||
|
"ui.icon.database": "Database file",
|
||||||
|
"ui.icon.font": "Font file",
|
||||||
|
"ui.icon.config": "Config file",
|
||||||
|
"ui.icon.json": "JSON file",
|
||||||
|
"ui.icon.generic": "File",
|
||||||
"ui.create": "Create",
|
"ui.create": "Create",
|
||||||
"ui.cancel": "Cancel",
|
"ui.cancel": "Cancel",
|
||||||
|
"ui.createFolderTitle": "Create folder",
|
||||||
|
"ui.createMarkdownTitle": "Create Markdown file",
|
||||||
|
"ui.createTextTitle": "Create text file",
|
||||||
|
"ui.renameTitle": "Rename {name}",
|
||||||
|
"ui.folderName": "Folder name",
|
||||||
|
"ui.markdownFileName": "Markdown file name",
|
||||||
|
"ui.textFileName": "Text file name",
|
||||||
|
"ui.fileName": "File or folder name",
|
||||||
|
"ui.nameRequired": "Enter a name.",
|
||||||
|
"ui.invalidCharacters": "The name contains invalid characters.",
|
||||||
|
"ui.invalidName": "Enter a valid name.",
|
||||||
|
"ui.nameDiffersOnlyByCase": "The name differs only by letter case.",
|
||||||
|
"ui.nameConflict": "An item with that name already exists.",
|
||||||
|
"ui.externalFailed": "{action} failed.",
|
||||||
|
"ui.vaultRelativePath": "Vault-relative path:",
|
||||||
|
"ui.copyPath": "Copy path",
|
||||||
|
"ui.close": "Close",
|
||||||
|
"ui.trashConfirm": "Move \"{name}\" to trash?",
|
||||||
|
"ui.trashManyConfirm": "Move {count} items to trash?",
|
||||||
|
"ui.clipboardDifferentDeal": "Clipboard items belong to another Deal.",
|
||||||
"ui.emptyFolder": "Empty folder",
|
"ui.emptyFolder": "Empty folder",
|
||||||
"ui.noMatches": "No matches",
|
"ui.noMatches": "No matches",
|
||||||
"ui.clearFilter": "Clear filter",
|
"ui.clearFilter": "Clear filter",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Файлы",
|
"manifest.name": "Файлы",
|
||||||
"manifest.description": "Файловый менеджер рабочего пространства с созданием, переименованием, корзиной, фильтрацией и сортировкой.",
|
"manifest.description": "Файловый менеджер Дела с созданием, переименованием, корзиной, фильтрацией и сортировкой.",
|
||||||
"contributions.workspaceItems.verstak.files.workspace.title": "Файлы",
|
"contributions.workspaceItems.verstak.files.workspace.title": "Файлы",
|
||||||
"ui.back": "Назад",
|
"ui.back": "Назад",
|
||||||
"ui.forward": "Вперёд",
|
"ui.forward": "Вперёд",
|
||||||
@@ -10,7 +10,11 @@
|
|||||||
"ui.newMarkdown": "Новый Markdown-файл",
|
"ui.newMarkdown": "Новый Markdown-файл",
|
||||||
"ui.newText": "Новый текстовый файл",
|
"ui.newText": "Новый текстовый файл",
|
||||||
"ui.open": "Открыть",
|
"ui.open": "Открыть",
|
||||||
|
"ui.openFolder": "Открыть папку",
|
||||||
|
"ui.openExternal": "Открыть во внешнем приложении",
|
||||||
|
"ui.showInFolder": "Показать в папке",
|
||||||
"ui.rename": "Переименовать",
|
"ui.rename": "Переименовать",
|
||||||
|
"ui.duplicate": "Создать копию",
|
||||||
"ui.trash": "Переместить в корзину",
|
"ui.trash": "Переместить в корзину",
|
||||||
"ui.cut": "Вырезать",
|
"ui.cut": "Вырезать",
|
||||||
"ui.copy": "Копировать",
|
"ui.copy": "Копировать",
|
||||||
@@ -22,8 +26,46 @@
|
|||||||
"ui.column.modified": "Изменён",
|
"ui.column.modified": "Изменён",
|
||||||
"ui.column.size": "Размер",
|
"ui.column.size": "Размер",
|
||||||
"ui.column.actions": "Действия",
|
"ui.column.actions": "Действия",
|
||||||
|
"ui.folderType": "Папка",
|
||||||
|
"ui.icon.folder": "Папка",
|
||||||
|
"ui.icon.markdown": "Файл Markdown",
|
||||||
|
"ui.icon.image": "Изображение",
|
||||||
|
"ui.icon.video": "Видео",
|
||||||
|
"ui.icon.audio": "Аудиофайл",
|
||||||
|
"ui.icon.archive": "Архив",
|
||||||
|
"ui.icon.pdf": "PDF-файл",
|
||||||
|
"ui.icon.text": "Текстовый файл",
|
||||||
|
"ui.icon.document": "Документ",
|
||||||
|
"ui.icon.spreadsheet": "Таблица",
|
||||||
|
"ui.icon.presentation": "Презентация",
|
||||||
|
"ui.icon.code": "Исходный код",
|
||||||
|
"ui.icon.database": "База данных",
|
||||||
|
"ui.icon.font": "Шрифт",
|
||||||
|
"ui.icon.config": "Файл конфигурации",
|
||||||
|
"ui.icon.json": "JSON-файл",
|
||||||
|
"ui.icon.generic": "Файл",
|
||||||
"ui.create": "Создать",
|
"ui.create": "Создать",
|
||||||
"ui.cancel": "Отмена",
|
"ui.cancel": "Отмена",
|
||||||
|
"ui.createFolderTitle": "Создать папку",
|
||||||
|
"ui.createMarkdownTitle": "Создать Markdown-файл",
|
||||||
|
"ui.createTextTitle": "Создать текстовый файл",
|
||||||
|
"ui.renameTitle": "Переименовать {name}",
|
||||||
|
"ui.folderName": "Название папки",
|
||||||
|
"ui.markdownFileName": "Название Markdown-файла",
|
||||||
|
"ui.textFileName": "Название текстового файла",
|
||||||
|
"ui.fileName": "Название файла или папки",
|
||||||
|
"ui.nameRequired": "Введите название.",
|
||||||
|
"ui.invalidCharacters": "В названии есть недопустимые символы.",
|
||||||
|
"ui.invalidName": "Введите корректное название.",
|
||||||
|
"ui.nameDiffersOnlyByCase": "Название отличается только регистром букв.",
|
||||||
|
"ui.nameConflict": "Элемент с таким названием уже существует.",
|
||||||
|
"ui.externalFailed": "Не удалось выполнить действие «{action}».",
|
||||||
|
"ui.vaultRelativePath": "Путь относительно хранилища:",
|
||||||
|
"ui.copyPath": "Скопировать путь",
|
||||||
|
"ui.close": "Закрыть",
|
||||||
|
"ui.trashConfirm": "Переместить «{name}» в корзину?",
|
||||||
|
"ui.trashManyConfirm": "Переместить в корзину {count} элементов?",
|
||||||
|
"ui.clipboardDifferentDeal": "Элементы буфера обмена принадлежат другому Делу.",
|
||||||
"ui.emptyFolder": "Папка пуста",
|
"ui.emptyFolder": "Папка пуста",
|
||||||
"ui.noMatches": "Совпадений нет",
|
"ui.noMatches": "Совпадений нет",
|
||||||
"ui.clearFilter": "Сбросить фильтр",
|
"ui.clearFilter": "Сбросить фильтр",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Files",
|
"name": "Files",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.",
|
"description": "Deal-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.",
|
||||||
"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": "folder",
|
"icon": "folder",
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
|
|
||||||
function scopeFromProps(props) {
|
function scopeFromProps(props) {
|
||||||
var workspaceRoot = workspaceFromProps(props || {});
|
var workspaceRoot = workspaceFromProps(props || {});
|
||||||
if (!workspaceRoot) return { mode: 'global', key: '', label: 'All workspaces', workspaceRoot: '' };
|
if (!workspaceRoot) return { mode: 'global', key: '', label: 'All Deals', workspaceRoot: '' };
|
||||||
return {
|
return {
|
||||||
mode: 'workspace',
|
mode: 'workspace',
|
||||||
key: WORKLOG_PREFIX + encodeKey(workspaceRoot),
|
key: WORKLOG_PREFIX + encodeKey(workspaceRoot),
|
||||||
@@ -363,6 +363,32 @@
|
|||||||
modalHost.setAttribute('hidden', 'hidden');
|
modalHost.setAttribute('hidden', 'hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function activityLabel(activity) {
|
||||||
|
var type = text(activity && activity.type);
|
||||||
|
var labels = {
|
||||||
|
'browser.capture.link': ['ui.activity.captureLink', 'Link captured'],
|
||||||
|
'browser.capture.selection': ['ui.activity.captureSelection', 'Selection captured'],
|
||||||
|
'browser.capture.page': ['ui.activity.capturePage', 'Page captured'],
|
||||||
|
'file.created': ['ui.activity.fileCreated', 'File created'],
|
||||||
|
'file.updated': ['ui.activity.fileUpdated', 'File updated'],
|
||||||
|
'file.deleted': ['ui.activity.fileDeleted', 'File deleted'],
|
||||||
|
'note.saved': ['ui.activity.noteSaved', 'Note saved'],
|
||||||
|
'workspace.created': ['ui.activity.dealCreated', 'Deal created'],
|
||||||
|
'workspace.renamed': ['ui.activity.dealRenamed', 'Deal renamed']
|
||||||
|
};
|
||||||
|
var label = labels[type] || ['ui.activity.generic', 'Activity'];
|
||||||
|
return tr(label[0], null, label[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryMeta(entry) {
|
||||||
|
var facts = [entry.workspaceRootPath, entry.billable ? tr('ui.meta.billable', null, 'billable') : tr('ui.meta.nonBillable', null, 'non-billable')];
|
||||||
|
if (entry.activityIds.length) {
|
||||||
|
facts.push(tr(entry.activityIds.length === 1 ? 'ui.meta.activities.one' : 'ui.meta.activities.many', { count: entry.activityIds.length }, entry.activityIds.length + ' linked activities'));
|
||||||
|
}
|
||||||
|
if (entry.sourceTodoId) facts.push(tr('ui.meta.todo', null, 'linked todo'));
|
||||||
|
return facts.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
function showEntryModal(existingEntry, candidate, completedTodo) {
|
function showEntryModal(existingEntry, candidate, completedTodo) {
|
||||||
var editing = !!existingEntry;
|
var editing = !!existingEntry;
|
||||||
var reviewingCandidate = !editing && !!candidate;
|
var reviewingCandidate = !editing && !!candidate;
|
||||||
@@ -406,22 +432,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var candidateContext = reviewingCandidate ? el('div', { className: 'journal-candidate-context', 'data-journal-candidate': candidate.candidateId }, [
|
var candidateContext = reviewingCandidate ? el('div', { className: 'journal-candidate-context', 'data-journal-candidate': candidate.candidateId }, [
|
||||||
el('strong', { textContent: 'Possible journal entry' }),
|
el('strong', { textContent: tr('ui.candidate.title', null, 'Possible journal entry') }),
|
||||||
el('div', { textContent: 'Workspace: ' + candidate.workspaceRootPath }),
|
el('div', { textContent: tr('ui.candidate.deal', { deal: candidate.workspaceRootPath }, 'Deal: ' + candidate.workspaceRootPath) }),
|
||||||
el('div', { textContent: 'Time: ' + candidateTime(candidate.startedAt) + ' - ' + candidateTime(candidate.endedAt) }),
|
el('div', { textContent: tr('ui.candidate.time', { start: candidateTime(candidate.startedAt), end: candidateTime(candidate.endedAt) }, 'Time: ' + candidateTime(candidate.startedAt) + ' – ' + candidateTime(candidate.endedAt)) }),
|
||||||
el('div', { textContent: 'Estimated duration: ' + candidate.estimatedMinutes + ' min' }),
|
el('div', { textContent: tr('ui.candidate.duration', { minutes: candidate.estimatedMinutes }, 'Estimated duration: ' + candidate.estimatedMinutes + ' min') }),
|
||||||
el('div', { textContent: 'Activities: ' + candidate.activityCount })
|
el('div', { textContent: tr('ui.candidate.activities', { count: candidate.activityCount }, 'Activities: ' + candidate.activityCount) })
|
||||||
]) : null;
|
]) : null;
|
||||||
var candidateActivities = reviewingCandidate ? el('fieldset', { className: 'journal-candidate-activities' }, [
|
var candidateActivities = reviewingCandidate ? el('fieldset', { className: 'journal-candidate-activities' }, [
|
||||||
el('legend', { textContent: 'Linked activities' })
|
el('legend', { textContent: tr('ui.candidate.linkedActivities', null, 'Linked activities') })
|
||||||
].concat(activityInputs.map(function (item) {
|
].concat(activityInputs.map(function (item) {
|
||||||
var detail = (item.activity.occurredAt ? candidateTime(item.activity.occurredAt) + ' · ' : '') + item.activity.type + ' · ' + item.activity.activityId;
|
var detail = (item.activity.occurredAt ? candidateTime(item.activity.occurredAt) + ' · ' : '') + activityLabel(item.activity);
|
||||||
return el('label', { className: 'journal-candidate-activity' }, [item.input, detail]);
|
return el('label', { className: 'journal-candidate-activity' }, [item.input, detail]);
|
||||||
}))) : null;
|
}))) : null;
|
||||||
var todoContext = reviewingTodo ? el('div', { className: 'journal-candidate-context', 'data-journal-todo': completedTodo.id }, [
|
var todoContext = reviewingTodo ? el('div', { className: 'journal-candidate-context', 'data-journal-todo': completedTodo.id }, [
|
||||||
el('strong', { textContent: 'Completed todo' }),
|
el('strong', { textContent: tr('ui.todo.title', null, 'Completed todo') }),
|
||||||
el('div', { textContent: 'Workspace: ' + completedTodo.workspaceRootPath }),
|
el('div', { textContent: tr('ui.candidate.deal', { deal: completedTodo.workspaceRootPath }, 'Deal: ' + completedTodo.workspaceRootPath) }),
|
||||||
completedTodo.completedAt ? el('div', { textContent: 'Completed: ' + candidateTime(completedTodo.completedAt) }) : null
|
completedTodo.completedAt ? el('div', { textContent: tr('ui.todo.completed', { time: candidateTime(completedTodo.completedAt) }, 'Completed: ' + candidateTime(completedTodo.completedAt)) }) : null
|
||||||
]) : null;
|
]) : null;
|
||||||
|
|
||||||
modalHost.innerHTML = '';
|
modalHost.innerHTML = '';
|
||||||
@@ -467,13 +493,13 @@
|
|||||||
var handledThrough = text(formValue && formValue.handledThrough).trim();
|
var handledThrough = text(formValue && formValue.handledThrough).trim();
|
||||||
var sourceTodoId = text(formValue && formValue.sourceTodoId || (existingEntry && existingEntry.sourceTodoId)).trim();
|
var sourceTodoId = text(formValue && formValue.sourceTodoId || (existingEntry && existingEntry.sourceTodoId)).trim();
|
||||||
if (!existingEntry && sourceCandidateId && entries.some(function (entry) { return entry.sourceCandidateId === sourceCandidateId; })) {
|
if (!existingEntry && sourceCandidateId && entries.some(function (entry) { return entry.sourceCandidateId === sourceCandidateId; })) {
|
||||||
statusText = 'A journal entry already references this candidate';
|
statusText = tr('ui.candidate.duplicate', null, 'A journal entry already references this candidate');
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!existingEntry && sourceTodoId && entries.some(function (entry) { return entry.sourceTodoId === sourceTodoId; })) {
|
if (!existingEntry && sourceTodoId && entries.some(function (entry) { return entry.sourceTodoId === sourceTodoId; })) {
|
||||||
statusText = 'A journal entry already references this todo';
|
statusText = tr('ui.todo.duplicate', null, 'A journal entry already references this todo');
|
||||||
statusClass = 'error';
|
statusClass = 'error';
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
@@ -500,7 +526,7 @@
|
|||||||
entries = sortEntries(entries);
|
entries = sortEntries(entries);
|
||||||
var targetEntries = entries.filter(function (item) { return item.workspaceRootPath === workspaceRoot; });
|
var targetEntries = entries.filter(function (item) { return item.workspaceRootPath === workspaceRoot; });
|
||||||
closeEntryModal();
|
closeEntryModal();
|
||||||
statusText = existingEntry ? 'Entry updated' : 'Entry added';
|
statusText = existingEntry ? tr('ui.updated', null, 'Entry updated') : tr('ui.added', null, 'Entry added');
|
||||||
statusClass = '';
|
statusClass = '';
|
||||||
persist(workspaceRoot, targetEntries).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;
|
||||||
@@ -535,9 +561,9 @@
|
|||||||
el('div', { className: 'journal-main' }, [
|
el('div', { className: 'journal-main' }, [
|
||||||
el('div', { className: 'journal-entry-title', textContent: entry.title }),
|
el('div', { className: 'journal-entry-title', textContent: entry.title }),
|
||||||
entry.summary ? el('div', { className: 'journal-summary', textContent: entry.summary }) : null,
|
entry.summary ? el('div', { className: 'journal-summary', textContent: entry.summary }) : null,
|
||||||
el('div', { className: 'journal-meta', textContent: entry.workspaceRootPath + (entry.billable ? ' · billable' : ' · non-billable') + (entry.activityIds.length ? ' · ' + entry.activityIds.length + ' linked activities' : '') + (entry.sourceTodoId ? ' · linked todo' : '') })
|
el('div', { className: 'journal-meta', textContent: entryMeta(entry) })
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' }),
|
el('div', { className: 'journal-minutes', textContent: tr('ui.minutesValue', { minutes: entry.minutes }, entry.minutes + ' min') }),
|
||||||
scope.mode === 'workspace' ? el('div', { className: 'journal-row-actions' }, [
|
scope.mode === 'workspace' ? el('div', { className: 'journal-row-actions' }, [
|
||||||
el('button', { className: 'journal-icon-btn', type: 'button', title: tr('ui.edit', null, 'Edit'), 'aria-label': tr('ui.edit', null, 'Edit'), 'data-journal-action': 'edit', innerHTML: iconSvg('edit'), onClick: function () { showEntryModal(entry); } }),
|
el('button', { className: 'journal-icon-btn', type: 'button', title: tr('ui.edit', null, 'Edit'), 'aria-label': tr('ui.edit', null, 'Edit'), 'data-journal-action': 'edit', innerHTML: iconSvg('edit'), onClick: function () { showEntryModal(entry); } }),
|
||||||
el('button', { className: 'journal-icon-btn danger', type: 'button', title: tr('ui.delete', null, 'Delete'), 'aria-label': tr('ui.delete', null, 'Delete'), 'data-journal-action': 'delete', innerHTML: iconSvg('trash'), onClick: function () { deleteEntry(entry); } })
|
el('button', { className: 'journal-icon-btn danger', type: 'button', title: tr('ui.delete', null, 'Delete'), 'aria-label': tr('ui.delete', null, 'Delete'), 'data-journal-action': 'delete', innerHTML: iconSvg('trash'), onClick: function () { deleteEntry(entry); } })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Journal",
|
"manifest.name": "Journal",
|
||||||
"manifest.description": "Workspace-scoped journal with user-authored entries and optional Activity links.",
|
"manifest.description": "Deal-scoped journal with user-authored entries and optional Activity links.",
|
||||||
"contributions.views.verstak.journal.view.title": "Journal",
|
"contributions.views.verstak.journal.view.title": "Journal",
|
||||||
"contributions.sidebarItems.verstak.journal.sidebar.title": "Journal",
|
"contributions.sidebarItems.verstak.journal.sidebar.title": "Journal",
|
||||||
"contributions.workspaceItems.verstak.journal.workspace.title": "Journal",
|
"contributions.workspaceItems.verstak.journal.workspace.title": "Journal",
|
||||||
@@ -31,5 +31,33 @@
|
|||||||
"ui.edit": "Edit",
|
"ui.edit": "Edit",
|
||||||
"ui.delete": "Delete",
|
"ui.delete": "Delete",
|
||||||
"ui.entryCount.one": "{count} entry",
|
"ui.entryCount.one": "{count} entry",
|
||||||
"ui.entryCount.many": "{count} entries"
|
"ui.entryCount.many": "{count} entries",
|
||||||
|
"ui.candidate.title": "Possible journal entry",
|
||||||
|
"ui.candidate.deal": "Deal: {deal}",
|
||||||
|
"ui.candidate.time": "Time: {start} – {end}",
|
||||||
|
"ui.candidate.duration": "Estimated duration: {minutes} min",
|
||||||
|
"ui.candidate.activities": "Activities: {count}",
|
||||||
|
"ui.candidate.linkedActivities": "Linked activities",
|
||||||
|
"ui.candidate.duplicate": "A journal entry already references this candidate",
|
||||||
|
"ui.todo.title": "Completed todo",
|
||||||
|
"ui.todo.completed": "Completed: {time}",
|
||||||
|
"ui.todo.duplicate": "A journal entry already references this todo",
|
||||||
|
"ui.activity.captureLink": "Link captured",
|
||||||
|
"ui.activity.captureSelection": "Selection captured",
|
||||||
|
"ui.activity.capturePage": "Page captured",
|
||||||
|
"ui.activity.fileCreated": "File created",
|
||||||
|
"ui.activity.fileUpdated": "File updated",
|
||||||
|
"ui.activity.fileDeleted": "File deleted",
|
||||||
|
"ui.activity.noteSaved": "Note saved",
|
||||||
|
"ui.activity.dealCreated": "Deal created",
|
||||||
|
"ui.activity.dealRenamed": "Deal renamed",
|
||||||
|
"ui.activity.generic": "Activity",
|
||||||
|
"ui.updated": "Entry updated",
|
||||||
|
"ui.added": "Entry added",
|
||||||
|
"ui.minutesValue": "{minutes} min",
|
||||||
|
"ui.meta.billable": "billable",
|
||||||
|
"ui.meta.nonBillable": "non-billable",
|
||||||
|
"ui.meta.activities.one": "{count} linked activity",
|
||||||
|
"ui.meta.activities.many": "{count} linked activities",
|
||||||
|
"ui.meta.todo": "linked todo"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Журнал",
|
"manifest.name": "Журнал",
|
||||||
"manifest.description": "Журнал рабочего пространства с ручными записями и необязательными связями с активностью.",
|
"manifest.description": "Журнал Дела с ручными записями и необязательными связями с активностью.",
|
||||||
"contributions.views.verstak.journal.view.title": "Журнал",
|
"contributions.views.verstak.journal.view.title": "Журнал",
|
||||||
"contributions.sidebarItems.verstak.journal.sidebar.title": "Журнал",
|
"contributions.sidebarItems.verstak.journal.sidebar.title": "Журнал",
|
||||||
"contributions.workspaceItems.verstak.journal.workspace.title": "Журнал",
|
"contributions.workspaceItems.verstak.journal.workspace.title": "Журнал",
|
||||||
@@ -31,5 +31,33 @@
|
|||||||
"ui.edit": "Изменить",
|
"ui.edit": "Изменить",
|
||||||
"ui.delete": "Удалить",
|
"ui.delete": "Удалить",
|
||||||
"ui.entryCount.one": "{count} запись",
|
"ui.entryCount.one": "{count} запись",
|
||||||
"ui.entryCount.many": "Записей: {count}"
|
"ui.entryCount.many": "Записей: {count}",
|
||||||
|
"ui.candidate.title": "Возможная запись журнала",
|
||||||
|
"ui.candidate.deal": "Дело: {deal}",
|
||||||
|
"ui.candidate.time": "Время: {start} – {end}",
|
||||||
|
"ui.candidate.duration": "Расчётная длительность: {minutes} мин",
|
||||||
|
"ui.candidate.activities": "Активностей: {count}",
|
||||||
|
"ui.candidate.linkedActivities": "Связанные активности",
|
||||||
|
"ui.candidate.duplicate": "Запись журнала уже ссылается на этого кандидата",
|
||||||
|
"ui.todo.title": "Выполненная задача",
|
||||||
|
"ui.todo.completed": "Выполнена: {time}",
|
||||||
|
"ui.todo.duplicate": "Запись журнала уже ссылается на эту задачу",
|
||||||
|
"ui.activity.captureLink": "Сохранена ссылка",
|
||||||
|
"ui.activity.captureSelection": "Захвачено выделение",
|
||||||
|
"ui.activity.capturePage": "Сохранена страница",
|
||||||
|
"ui.activity.fileCreated": "Создан файл",
|
||||||
|
"ui.activity.fileUpdated": "Изменён файл",
|
||||||
|
"ui.activity.fileDeleted": "Удалён файл",
|
||||||
|
"ui.activity.noteSaved": "Сохранена заметка",
|
||||||
|
"ui.activity.dealCreated": "Создано Дело",
|
||||||
|
"ui.activity.dealRenamed": "Переименовано Дело",
|
||||||
|
"ui.activity.generic": "Активность",
|
||||||
|
"ui.updated": "Запись обновлена",
|
||||||
|
"ui.added": "Запись добавлена",
|
||||||
|
"ui.minutesValue": "{minutes} мин",
|
||||||
|
"ui.meta.billable": "оплачиваемая",
|
||||||
|
"ui.meta.nonBillable": "неоплачиваемая",
|
||||||
|
"ui.meta.activities.one": "{count} связанная активность",
|
||||||
|
"ui.meta.activities.many": "Связанных активностей: {count}",
|
||||||
|
"ui.meta.todo": "связанная задача"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Journal",
|
"name": "Journal",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped journal with user-authored entries and optional Activity links.",
|
"description": "Deal-scoped journal with user-authored entries and optional Activity links.",
|
||||||
"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": "book-open",
|
"icon": "book-open",
|
||||||
|
|||||||
@@ -43,14 +43,16 @@
|
|||||||
'.notes-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--vt-color-text-muted,#7f8aa3);font-size:.85rem;padding:2rem;gap:.5rem;text-align:center}',
|
'.notes-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--vt-color-text-muted,#7f8aa3);font-size:.85rem;padding:2rem;gap:.5rem;text-align:center}',
|
||||||
'.notes-empty-hint{font-size:.75rem;color:var(--vt-color-text-muted,#7f8aa3)}',
|
'.notes-empty-hint{font-size:.75rem;color:var(--vt-color-text-muted,#7f8aa3)}',
|
||||||
'.notes-error{flex:1;display:flex;align-items:center;justify-content:center;color:var(--vt-color-danger,#e94560);padding:1rem;font-size:.85rem}',
|
'.notes-error{flex:1;display:flex;align-items:center;justify-content:center;color:var(--vt-color-danger,#e94560);padding:1rem;font-size:.85rem}',
|
||||||
'.notes-panel{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-top:1px solid var(--vt-color-border,#202b46);flex-shrink:0;background:var(--vt-color-surface-muted,#111629)}',
|
|
||||||
'.notes-input{flex:1;font-size:.78rem;padding:.32rem .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);outline:none;min-width:120px}',
|
|
||||||
'.notes-input:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
|
||||||
'.notes-title-bar{padding:.4rem .75rem;font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3);background:var(--vt-color-surface-muted,#111629);border-bottom:1px solid var(--vt-color-border,#202b46);text-transform:uppercase;letter-spacing:.04em}',
|
'.notes-title-bar{padding:.4rem .75rem;font-size:.72rem;color:var(--vt-color-text-muted,#7f8aa3);background:var(--vt-color-surface-muted,#111629);border-bottom:1px solid var(--vt-color-border,#202b46);text-transform:uppercase;letter-spacing:.04em}',
|
||||||
'.notes-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center}',
|
'.notes-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center}',
|
||||||
'.notes-modal{width:380px;max-width:90vw;padding:20px;background:#1a1a2e;border:1px solid #333;border-radius:10px;color:#e0e0e0;font-family:inherit;box-shadow:0 12px 40px rgba(0,0,0,.5)}',
|
'.notes-modal{width:380px;max-width:90vw;padding:20px;background:#1a1a2e;border:1px solid #333;border-radius:10px;color:#e0e0e0;font-family:inherit;box-shadow:0 12px 40px rgba(0,0,0,.5)}',
|
||||||
'.notes-modal-title{font-size:.9rem;font-weight:600;margin-bottom:12px}',
|
'.notes-modal-title{font-size:.9rem;font-weight:600;margin-bottom:12px}',
|
||||||
'.notes-modal-msg{font-size:.82rem;color:#aaa;margin-bottom:16px;word-wrap:break-word}',
|
'.notes-modal-msg{font-size:.82rem;color:#aaa;margin-bottom:16px;word-wrap:break-word}',
|
||||||
|
'.notes-modal-form{display:grid;gap:.55rem}',
|
||||||
|
'.notes-modal-input{width:100%;font:inherit;font-size:.84rem;padding:.5rem .6rem;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);outline:none}',
|
||||||
|
'.notes-modal-input:focus{border-color:var(--vt-color-accent,#4ecca3);box-shadow:var(--vt-focus-ring,0 0 0 2px rgba(78,204,163,.34))}',
|
||||||
|
'.notes-modal-input[aria-invalid="true"]{border-color:var(--vt-color-danger,#e94560)}',
|
||||||
|
'.notes-modal-error{display:none;font-size:.78rem;line-height:1.35;color:#ffc6ce}',
|
||||||
'.notes-modal-actions{display:flex;justify-content:flex-end;gap:8px}',
|
'.notes-modal-actions{display:flex;justify-content:flex-end;gap:8px}',
|
||||||
'.notes-modal-btn{font-size:.8rem;padding:.35rem .9rem;border:1px solid #333;border-radius:5px;cursor:pointer;font-family:inherit}',
|
'.notes-modal-btn{font-size:.8rem;padding:.35rem .9rem;border:1px solid #333;border-radius:5px;cursor:pointer;font-family:inherit}',
|
||||||
'.notes-modal-btn.cancel{background:#2a2a4e;color:#ccc}',
|
'.notes-modal-btn.cancel{background:#2a2a4e;color:#ccc}',
|
||||||
@@ -171,7 +173,7 @@
|
|||||||
|
|
||||||
var workspaceNode = props && props.workspaceNode;
|
var workspaceNode = props && props.workspaceNode;
|
||||||
var workspaceRoot = (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id)) || '';
|
var workspaceRoot = (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id)) || '';
|
||||||
var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title)) || 'Workspace';
|
var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title)) || '';
|
||||||
|
|
||||||
var notes = [];
|
var notes = [];
|
||||||
var selectedPath = '';
|
var selectedPath = '';
|
||||||
@@ -287,23 +289,12 @@
|
|||||||
var listContainer = el('div', { className: 'notes-list', 'data-notes-list': '' });
|
var listContainer = el('div', { className: 'notes-list', 'data-notes-list': '' });
|
||||||
containerEl.appendChild(listContainer);
|
containerEl.appendChild(listContainer);
|
||||||
|
|
||||||
var createPanel = el('div', { className: 'notes-panel', style: { display: 'none' } });
|
var createModal = null;
|
||||||
var createInput = el('input', { className: 'notes-input', 'data-notes-create-input': '', placeholder: tr('ui.noteTitle', null, 'Note title') });
|
var createInput = null;
|
||||||
var createConfirm = el('button', { className: 'notes-btn', textContent: tr('ui.create', null, 'Create') });
|
var createError = null;
|
||||||
var createCancel = el('button', { className: 'notes-btn', textContent: tr('ui.cancel', null, 'Cancel') });
|
var renameModal = null;
|
||||||
createPanel.appendChild(createInput);
|
var renameInput = null;
|
||||||
createPanel.appendChild(createConfirm);
|
var renameError = null;
|
||||||
createPanel.appendChild(createCancel);
|
|
||||||
containerEl.appendChild(createPanel);
|
|
||||||
|
|
||||||
var renamePanel = el('div', { className: 'notes-panel', style: { display: 'none' } });
|
|
||||||
var renameInput = el('input', { className: 'notes-input', 'data-notes-rename-input': '', placeholder: tr('ui.newTitle', null, 'New title') });
|
|
||||||
var renameConfirm = el('button', { className: 'notes-btn', textContent: tr('ui.rename', null, 'Rename') });
|
|
||||||
var renameCancel = el('button', { className: 'notes-btn', textContent: tr('ui.cancel', null, 'Cancel') });
|
|
||||||
renamePanel.appendChild(renameInput);
|
|
||||||
renamePanel.appendChild(renameConfirm);
|
|
||||||
renamePanel.appendChild(renameCancel);
|
|
||||||
containerEl.appendChild(renamePanel);
|
|
||||||
|
|
||||||
// ─── Core Functions ─────────────────────────────────────
|
// ─── Core Functions ─────────────────────────────────────
|
||||||
|
|
||||||
@@ -495,28 +486,134 @@
|
|||||||
}).catch(function (err) { console.error('[notes] openResource:', err); });
|
}).catch(function (err) { console.error('[notes] openResource:', err); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function conflictMessage(title, existingPath) {
|
||||||
|
return [
|
||||||
|
tr('ui.conflictMessage', { title: title }, 'A note with the title "' + title + '" already exists.'),
|
||||||
|
existingPath ? tr('ui.existingFile', { path: existingPath }, ' Existing file: ' + existingPath + '.') : '',
|
||||||
|
tr('ui.chooseDifferent', null, ' Please choose a different title.')
|
||||||
|
].join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNoteFormModal(mode, note) {
|
||||||
|
var isRename = mode === 'rename';
|
||||||
|
var modalTitle = isRename
|
||||||
|
? tr('ui.renameTitle', null, 'Rename note')
|
||||||
|
: tr('ui.createTitle', null, 'Create note');
|
||||||
|
var overlay = el('div', {
|
||||||
|
className: 'notes-modal-overlay',
|
||||||
|
role: 'presentation'
|
||||||
|
});
|
||||||
|
overlay.setAttribute('data-notes-' + mode + '-modal', '');
|
||||||
|
var input = el('input', {
|
||||||
|
className: 'notes-modal-input',
|
||||||
|
type: 'text',
|
||||||
|
value: isRename ? (note.title || fileName(note.path)) : '',
|
||||||
|
placeholder: isRename
|
||||||
|
? tr('ui.newTitle', null, 'New title')
|
||||||
|
: tr('ui.noteTitle', null, 'Note title'),
|
||||||
|
autocomplete: 'off',
|
||||||
|
'aria-invalid': 'false'
|
||||||
|
});
|
||||||
|
input.setAttribute('data-notes-' + mode + '-input', '');
|
||||||
|
input.value = isRename ? (note.title || fileName(note.path)) : '';
|
||||||
|
var error = el('div', {
|
||||||
|
className: 'notes-modal-error',
|
||||||
|
role: 'alert'
|
||||||
|
});
|
||||||
|
error.setAttribute('data-notes-' + mode + '-error', '');
|
||||||
|
var confirm = el('button', {
|
||||||
|
className: 'notes-modal-btn confirm',
|
||||||
|
type: 'button',
|
||||||
|
textContent: isRename ? tr('ui.rename', null, 'Rename') : tr('ui.create', null, 'Create'),
|
||||||
|
onClick: function () {
|
||||||
|
if (isRename) confirmRename();
|
||||||
|
else confirmCreate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var cancel = el('button', {
|
||||||
|
className: 'notes-modal-btn cancel',
|
||||||
|
type: 'button',
|
||||||
|
textContent: tr('ui.cancel', null, 'Cancel'),
|
||||||
|
onClick: function () {
|
||||||
|
if (isRename) hideRename();
|
||||||
|
else hideCreate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function handleFormKeydown(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
if (event.preventDefault) event.preventDefault();
|
||||||
|
if (isRename) confirmRename();
|
||||||
|
else confirmCreate();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
if (event.preventDefault) event.preventDefault();
|
||||||
|
if (isRename) hideRename();
|
||||||
|
else hideCreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.addEventListener('keydown', handleFormKeydown);
|
||||||
|
var modal = el('div', {
|
||||||
|
className: 'notes-modal notes-modal-form',
|
||||||
|
role: 'dialog',
|
||||||
|
'aria-modal': 'true',
|
||||||
|
'aria-label': modalTitle
|
||||||
|
}, [
|
||||||
|
el('div', { className: 'notes-modal-title', textContent: modalTitle }),
|
||||||
|
input,
|
||||||
|
error,
|
||||||
|
el('div', { className: 'notes-modal-actions' }, [cancel, confirm])
|
||||||
|
]);
|
||||||
|
overlay.appendChild(modal);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
if (isRename) {
|
||||||
|
renameInput = input;
|
||||||
|
renameError = error;
|
||||||
|
} else {
|
||||||
|
createInput = input;
|
||||||
|
createError = error;
|
||||||
|
}
|
||||||
|
input.focus();
|
||||||
|
if (isRename && input.select) input.select();
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Create ────────────────────────────────────────────
|
// ─── Create ────────────────────────────────────────────
|
||||||
|
|
||||||
function showCreate() {
|
function showCreate() {
|
||||||
createInput.value = '';
|
hideCreate();
|
||||||
createPanel.style.display = 'flex';
|
createModal = showNoteFormModal('create');
|
||||||
createInput.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideCreate() {
|
function hideCreate() {
|
||||||
createPanel.style.display = 'none';
|
if (createModal) createModal.remove();
|
||||||
|
createModal = null;
|
||||||
|
createInput = null;
|
||||||
|
createError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCreateError(message) {
|
||||||
|
if (!createError || !createInput) return;
|
||||||
|
createError.textContent = message || '';
|
||||||
|
createError.style.display = message ? 'block' : 'none';
|
||||||
|
createInput.setAttribute('aria-invalid', message ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmCreate() {
|
function confirmCreate() {
|
||||||
|
if (!createInput) return;
|
||||||
var title = createInput.value.trim();
|
var title = createInput.value.trim();
|
||||||
if (!title) return;
|
if (!title) {
|
||||||
|
setCreateError(tr('ui.titleRequired', null, 'Enter a note title.'));
|
||||||
|
createInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setStatus(tr('ui.creating', null, 'Creating note...'), 'loading');
|
setStatus(tr('ui.creating', null, 'Creating note...'), 'loading');
|
||||||
var parent = notesParent();
|
var parent = notesParent();
|
||||||
createNote(parent, title).then(function (data) {
|
createNote(parent, title).then(function (data) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
data = data || {};
|
data = data || {};
|
||||||
if (data.conflict) {
|
if (data.conflict) {
|
||||||
showConflictModal(title, data.path, createInput);
|
setCreateError(conflictMessage(title, data.path));
|
||||||
|
createInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hideCreate();
|
hideCreate();
|
||||||
@@ -539,42 +636,55 @@
|
|||||||
}).catch(function () {});
|
}).catch(function () {});
|
||||||
}
|
}
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus(userFacingError('ui.createError', 'Could not create the note. Please try again.', err), 'error');
|
setCreateError(userFacingError('ui.createError', 'Could not create the note. Please try again.', err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Rename ─────────────────────────────────────────────
|
// ─── Rename ─────────────────────────────────────────────
|
||||||
|
|
||||||
function beginRename(note) {
|
function beginRename(note) {
|
||||||
|
hideRename();
|
||||||
renameTarget = note;
|
renameTarget = note;
|
||||||
renameInput.value = note.title || fileName(note.path);
|
renameModal = showNoteFormModal('rename', note);
|
||||||
renamePanel.style.display = 'flex';
|
|
||||||
renameInput.focus();
|
|
||||||
renameInput.select();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideRename() {
|
function hideRename() {
|
||||||
renameTarget = null;
|
renameTarget = null;
|
||||||
renamePanel.style.display = 'none';
|
if (renameModal) renameModal.remove();
|
||||||
|
renameModal = null;
|
||||||
|
renameInput = null;
|
||||||
|
renameError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRenameError(message) {
|
||||||
|
if (!renameError || !renameInput) return;
|
||||||
|
renameError.textContent = message || '';
|
||||||
|
renameError.style.display = message ? 'block' : 'none';
|
||||||
|
renameInput.setAttribute('aria-invalid', message ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmRename() {
|
function confirmRename() {
|
||||||
if (!renameTarget) return;
|
if (!renameTarget || !renameInput) return;
|
||||||
var newTitle = renameInput.value.trim();
|
var newTitle = renameInput.value.trim();
|
||||||
if (!newTitle) return;
|
if (!newTitle) {
|
||||||
|
setRenameError(tr('ui.titleRequired', null, 'Enter a note title.'));
|
||||||
|
renameInput.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setStatus(tr('ui.renaming', null, 'Renaming...'), 'loading');
|
setStatus(tr('ui.renaming', null, 'Renaming...'), 'loading');
|
||||||
renameNote(renameTarget.path, newTitle).then(function (data) {
|
renameNote(renameTarget.path, newTitle).then(function (data) {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
data = data || {};
|
data = data || {};
|
||||||
if (data.conflict) {
|
if (data.conflict) {
|
||||||
showConflictModal(newTitle, data.path, renameInput);
|
setRenameError(conflictMessage(newTitle, data.path));
|
||||||
|
renameInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hideRename();
|
hideRename();
|
||||||
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(userFacingError('ui.renameError', 'Could not rename the note. Please try again.', err), 'error');
|
setRenameError(userFacingError('ui.renameError', 'Could not rename the note. Please try again.', err));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,25 +706,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Conflict Modal ─────────────────────────────────────
|
|
||||||
|
|
||||||
function showConflictModal(title, existingPath, focusTarget) {
|
|
||||||
var overlay = el('div', { className: 'notes-modal-overlay' });
|
|
||||||
var modal = el('div', { className: 'notes-modal' }, [
|
|
||||||
el('div', { className: 'notes-modal-title' }, [tr('ui.conflictTitle', null, 'Name Conflict')]),
|
|
||||||
el('div', { className: 'notes-modal-msg' }, [
|
|
||||||
tr('ui.conflictMessage', { title: title }, 'A note with the title "' + title + '" already exists.'),
|
|
||||||
existingPath ? tr('ui.existingFile', { path: existingPath }, ' Existing file: ' + existingPath + '.') : '',
|
|
||||||
tr('ui.chooseDifferent', null, ' Please choose a different title.')
|
|
||||||
].join('')),
|
|
||||||
el('div', { className: 'notes-modal-actions' }, [
|
|
||||||
el('button', { className: 'notes-modal-btn confirm', textContent: 'OK', onClick: function () { overlay.remove(); (focusTarget || createInput).focus(); } })
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
overlay.appendChild(modal);
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showTrashModal(note) {
|
function showTrashModal(note) {
|
||||||
return new Promise(function (resolve) {
|
return new Promise(function (resolve) {
|
||||||
var overlay = el('div', { className: 'notes-modal-overlay' });
|
var overlay = el('div', { className: 'notes-modal-overlay' });
|
||||||
@@ -648,18 +739,6 @@
|
|||||||
sortMode = sortSelect.value || 'title-asc';
|
sortMode = sortSelect.value || 'title-asc';
|
||||||
renderList();
|
renderList();
|
||||||
});
|
});
|
||||||
createConfirm.addEventListener('click', confirmCreate);
|
|
||||||
createCancel.addEventListener('click', hideCreate);
|
|
||||||
renameConfirm.addEventListener('click', confirmRename);
|
|
||||||
renameCancel.addEventListener('click', hideRename);
|
|
||||||
createInput.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Enter') confirmCreate();
|
|
||||||
if (e.key === 'Escape') hideCreate();
|
|
||||||
});
|
|
||||||
renameInput.addEventListener('keydown', function (e) {
|
|
||||||
if (e.key === 'Enter') confirmRename();
|
|
||||||
if (e.key === 'Escape') hideRename();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─── Init ───────────────────────────────────────────────
|
// ─── Init ───────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -672,12 +751,6 @@
|
|||||||
createBtn.innerHTML = iconSvg('add') + ' ' + tr('ui.newNote', null, 'New Note');
|
createBtn.innerHTML = iconSvg('add') + ' ' + tr('ui.newNote', null, 'New Note');
|
||||||
filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter notes'));
|
filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter notes'));
|
||||||
titleBar.textContent = tr('ui.title', { workspace: workspaceName }, 'Notes in ' + workspaceName);
|
titleBar.textContent = tr('ui.title', { workspace: workspaceName }, 'Notes in ' + workspaceName);
|
||||||
createInput.setAttribute('placeholder', tr('ui.noteTitle', null, 'Note title'));
|
|
||||||
renameInput.setAttribute('placeholder', tr('ui.newTitle', null, 'New title'));
|
|
||||||
createConfirm.textContent = tr('ui.create', null, 'Create');
|
|
||||||
createCancel.textContent = tr('ui.cancel', null, 'Cancel');
|
|
||||||
renameConfirm.textContent = tr('ui.rename', null, 'Rename');
|
|
||||||
renameCancel.textContent = tr('ui.cancel', null, 'Cancel');
|
|
||||||
renderList();
|
renderList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -696,6 +769,8 @@
|
|||||||
|
|
||||||
containerEl.__notesCleanup = function () {
|
containerEl.__notesCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
hideCreate();
|
||||||
|
hideRename();
|
||||||
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
|
if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Notes",
|
"manifest.name": "Notes",
|
||||||
"manifest.description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.",
|
"manifest.description": "Deal-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.",
|
||||||
"contributions.workspaceItems.verstak.notes.workspace.title": "Notes",
|
"contributions.workspaceItems.verstak.notes.workspace.title": "Notes",
|
||||||
"ui.newNote": "New Note",
|
"ui.newNote": "New Note",
|
||||||
"ui.filter": "Filter notes",
|
"ui.filter": "Filter notes",
|
||||||
"ui.title": "Notes in {workspace}",
|
"ui.title": "Notes in {workspace}",
|
||||||
"ui.noteTitle": "Note title",
|
"ui.noteTitle": "Note title",
|
||||||
|
"ui.createTitle": "Create note",
|
||||||
|
"ui.renameTitle": "Rename note",
|
||||||
|
"ui.titleRequired": "Enter a note title.",
|
||||||
"ui.create": "Create",
|
"ui.create": "Create",
|
||||||
"ui.cancel": "Cancel",
|
"ui.cancel": "Cancel",
|
||||||
"ui.newTitle": "New title",
|
"ui.newTitle": "New title",
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Заметки",
|
"manifest.name": "Заметки",
|
||||||
"manifest.description": "Менеджер Markdown-заметок рабочего пространства с созданием, переименованием и корзиной.",
|
"manifest.description": "Менеджер Markdown-заметок Дела с созданием, переименованием и корзиной.",
|
||||||
"contributions.workspaceItems.verstak.notes.workspace.title": "Заметки",
|
"contributions.workspaceItems.verstak.notes.workspace.title": "Заметки",
|
||||||
"ui.newNote": "Новая заметка",
|
"ui.newNote": "Новая заметка",
|
||||||
"ui.filter": "Фильтр заметок",
|
"ui.filter": "Фильтр заметок",
|
||||||
"ui.title": "Заметки · {workspace}",
|
"ui.title": "Заметки · {workspace}",
|
||||||
"ui.noteTitle": "Название заметки",
|
"ui.noteTitle": "Название заметки",
|
||||||
|
"ui.createTitle": "Создать заметку",
|
||||||
|
"ui.renameTitle": "Переименовать заметку",
|
||||||
|
"ui.titleRequired": "Введите название заметки.",
|
||||||
"ui.create": "Создать",
|
"ui.create": "Создать",
|
||||||
"ui.cancel": "Отмена",
|
"ui.cancel": "Отмена",
|
||||||
"ui.newTitle": "Новое название",
|
"ui.newTitle": "Новое название",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Notes",
|
"name": "Notes",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.",
|
"description": "Deal-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.",
|
||||||
"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": "edit",
|
"icon": "edit",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Search",
|
"manifest.name": "Search",
|
||||||
"manifest.description": "Workspace-scoped vault text search provider.",
|
"manifest.description": "Deal-scoped vault text search provider.",
|
||||||
"contributions.workspaceItems.verstak.search.workspace.title": "Search",
|
"contributions.workspaceItems.verstak.search.workspace.title": "Search",
|
||||||
"contributions.commands.verstak.search.searchVaultText.title": "Search Vault Text",
|
"contributions.commands.verstak.search.searchVaultText.title": "Search Vault Text",
|
||||||
"contributions.searchProviders.verstak.search.vault-text.label": "Vault Text Search",
|
"contributions.searchProviders.verstak.search.vault-text.label": "Vault Text Search",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Поиск",
|
"manifest.name": "Поиск",
|
||||||
"manifest.description": "Поиск текста в хранилище в пределах рабочего пространства.",
|
"manifest.description": "Поиск текста в хранилище в пределах Дела.",
|
||||||
"contributions.workspaceItems.verstak.search.workspace.title": "Поиск",
|
"contributions.workspaceItems.verstak.search.workspace.title": "Поиск",
|
||||||
"contributions.commands.verstak.search.searchVaultText.title": "Искать текст в хранилище",
|
"contributions.commands.verstak.search.searchVaultText.title": "Искать текст в хранилище",
|
||||||
"contributions.searchProviders.verstak.search.vault-text.label": "Поиск по тексту хранилища",
|
"contributions.searchProviders.verstak.search.vault-text.label": "Поиск по тексту хранилища",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Search",
|
"name": "Search",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Workspace-scoped vault text search provider.",
|
"description": "Deal-scoped vault text search provider.",
|
||||||
"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": "search",
|
"icon": "search",
|
||||||
|
|||||||
@@ -60,6 +60,11 @@
|
|||||||
'.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}',
|
||||||
|
'.secrets-modal-overlay{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.6)}',
|
||||||
|
'.secrets-modal{width:24rem;max-width:90vw;display:grid;gap:.8rem;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:var(--vt-elevation-menu,0 14px 32px rgba(0,0,0,.42))}',
|
||||||
|
'.secrets-modal-title{margin:0;font-size:.96rem}',
|
||||||
|
'.secrets-modal-message{margin:0;color:var(--vt-color-text-secondary,#b7c0d4);font-size:.84rem;line-height:1.45}',
|
||||||
|
'.secrets-modal-actions{display:flex;justify-content:flex-end;gap:.5rem}',
|
||||||
'@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}}'
|
'@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');
|
||||||
|
|
||||||
@@ -97,10 +102,10 @@
|
|||||||
|| (node && (node.rootPath || node.name || node.id)));
|
|| (node && (node.rootPath || node.name || node.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopeLabel(record) {
|
function scopeLabel(record, translate) {
|
||||||
var scope = record && record.scope || {};
|
var scope = record && record.scope || {};
|
||||||
if (scope.kind === ScopeWorkspace) return cleanWorkspace(scope.workspaceRootPath) || 'Deal';
|
if (scope.kind === ScopeWorkspace) return cleanWorkspace(scope.workspaceRootPath) || translate('ui.deal', null, 'Deal');
|
||||||
return 'Global';
|
return translate('ui.global', null, 'Global');
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectedIDFromProps(props) {
|
function selectedIDFromProps(props) {
|
||||||
@@ -111,16 +116,16 @@
|
|||||||
return decodeURIComponent(path.replace(/^\/+/, ''));
|
return decodeURIComponent(path.replace(/^\/+/, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupRecords(records) {
|
function groupRecords(records, translate) {
|
||||||
var groups = {};
|
var groups = {};
|
||||||
records.forEach(function (record) {
|
records.forEach(function (record) {
|
||||||
var label = scopeLabel(record);
|
var label = scopeLabel(record, translate);
|
||||||
groups[label] = groups[label] || [];
|
groups[label] = groups[label] || [];
|
||||||
groups[label].push(record);
|
groups[label].push(record);
|
||||||
});
|
});
|
||||||
return Object.keys(groups).sort(function (a, b) {
|
return Object.keys(groups).sort(function (a, b) {
|
||||||
if (a === 'Global') return -1;
|
if (a === translate('ui.global', null, 'Global')) return -1;
|
||||||
if (b === 'Global') return 1;
|
if (b === translate('ui.global', null, 'Global')) return 1;
|
||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
}).map(function (label) {
|
}).map(function (label) {
|
||||||
groups[label].sort(function (a, b) {
|
groups[label].sort(function (a, b) {
|
||||||
@@ -152,6 +157,8 @@
|
|||||||
var searchQuery = '';
|
var searchQuery = '';
|
||||||
var selectedRecord = null;
|
var selectedRecord = null;
|
||||||
var selectedValue = '';
|
var selectedValue = '';
|
||||||
|
var isValueVisible = false;
|
||||||
|
var deleteModal = null;
|
||||||
var initialized = false;
|
var initialized = false;
|
||||||
var unlocked = false;
|
var unlocked = false;
|
||||||
var statusText = '';
|
var statusText = '';
|
||||||
@@ -190,6 +197,7 @@
|
|||||||
read: 'Could not open this secret. Please try again.',
|
read: 'Could not open this secret. Please try again.',
|
||||||
save: 'Could not save the secret. Please try again.',
|
save: 'Could not save the secret. Please try again.',
|
||||||
delete: 'Could not delete the secret. Please try again.',
|
delete: 'Could not delete the secret. Please try again.',
|
||||||
|
copyValue: 'Could not copy the secret value. Please try again.',
|
||||||
copyLink: 'Could not copy the secret link. 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.'
|
unlock: 'Could not unlock secrets. Check the master password and try again.'
|
||||||
}[action] || 'Could not complete this action. Please try again.');
|
}[action] || 'Could not complete this action. Please try again.');
|
||||||
@@ -358,7 +366,7 @@
|
|||||||
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(visibleRecords).forEach(function (group) {
|
groupRecords(visibleRecords, tr).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;
|
||||||
@@ -382,14 +390,16 @@
|
|||||||
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])
|
el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText])
|
||||||
]);
|
]);
|
||||||
|
var valueIsAvailable = !!selectedValue;
|
||||||
|
var valueDisplay = valueIsAvailable && isValueVisible ? selectedValue : '••••••••••••';
|
||||||
return el('div', { className: 'secrets-card' }, [
|
return el('div', { className: 'secrets-card' }, [
|
||||||
el('h2', {}, [selectedRecord.title || selectedRecord.id]),
|
el('h2', {}, [selectedRecord.title || selectedRecord.id]),
|
||||||
el('table', { className: 'secrets-table' }, [
|
el('table', { className: 'secrets-table' }, [
|
||||||
el('tbody', {}, [
|
el('tbody', {}, [
|
||||||
fieldRow(tr('ui.group', null, 'Group'), scopeLabel(selectedRecord)),
|
fieldRow(tr('ui.group', null, 'Group'), scopeLabel(selectedRecord, tr)),
|
||||||
fieldRow('ID', selectedRecord.id),
|
fieldRow('ID', selectedRecord.id),
|
||||||
fieldRow(tr('ui.username', null, 'Username'), selectedRecord.username || ''),
|
fieldRow(tr('ui.username', null, 'Username'), selectedRecord.username || ''),
|
||||||
fieldRow(tr('ui.password', null, 'Password'), selectedValue ? selectedValue : '••••••••••••', selectedValue ? '' : 'secrets-hidden-value'),
|
fieldRow(tr('ui.password', null, 'Password'), valueDisplay, isValueVisible && valueIsAvailable ? 'secrets-secret-value' : 'secrets-hidden-value'),
|
||||||
fieldRow(tr('ui.updated', null, 'Updated'), selectedRecord.updatedAt || '')
|
fieldRow(tr('ui.updated', null, 'Updated'), selectedRecord.updatedAt || '')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
@@ -400,6 +410,24 @@
|
|||||||
'data-secret-copy-link': selectedRecord.id,
|
'data-secret-copy-link': selectedRecord.id,
|
||||||
onClick: function () { copySecretLink(selectedRecord.id); }
|
onClick: function () { copySecretLink(selectedRecord.id); }
|
||||||
}, [tr('ui.copyLink', null, 'Copy secret link')]),
|
}, [tr('ui.copyLink', null, 'Copy secret link')]),
|
||||||
|
el('button', {
|
||||||
|
className: 'secrets-btn',
|
||||||
|
type: 'button',
|
||||||
|
disabled: !valueIsAvailable,
|
||||||
|
'data-secret-copy-value': selectedRecord.id,
|
||||||
|
onClick: function () { copySecretValue(selectedValue); }
|
||||||
|
}, [tr('ui.copyValue', null, 'Copy value')]),
|
||||||
|
el('button', {
|
||||||
|
className: 'secrets-btn',
|
||||||
|
type: 'button',
|
||||||
|
disabled: !valueIsAvailable,
|
||||||
|
'data-secret-toggle-value': selectedRecord.id,
|
||||||
|
onClick: function () {
|
||||||
|
if (!selectedValue) return;
|
||||||
|
isValueVisible = !isValueVisible;
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}, [isValueVisible ? tr('ui.hideValue', null, 'Hide value') : tr('ui.showValue', null, 'Show value')]),
|
||||||
el('button', {
|
el('button', {
|
||||||
className: 'secrets-btn',
|
className: 'secrets-btn',
|
||||||
type: 'button',
|
type: 'button',
|
||||||
@@ -547,6 +575,7 @@
|
|||||||
selectedID = id;
|
selectedID = id;
|
||||||
selectedRecord = records.find(function (record) { return record.id === id; }) || null;
|
selectedRecord = records.find(function (record) { return record.id === id; }) || null;
|
||||||
selectedValue = '';
|
selectedValue = '';
|
||||||
|
isValueVisible = false;
|
||||||
render();
|
render();
|
||||||
if (!id) return Promise.resolve();
|
if (!id) return Promise.resolve();
|
||||||
return api.secrets.read(id).then(function (record) {
|
return api.secrets.read(id).then(function (record) {
|
||||||
@@ -576,17 +605,71 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deleteSecret(id) {
|
function deleteSecret(id) {
|
||||||
if (!id || !window.confirm(tr('ui.deleteConfirm', null, 'Delete this secret?'))) return;
|
if (!id) return;
|
||||||
|
removeDeleteModal();
|
||||||
|
var title = selectedRecord && selectedRecord.id === id ? selectedRecord.title || selectedRecord.id : id;
|
||||||
|
var overlay = el('div', {
|
||||||
|
className: 'secrets-modal-overlay',
|
||||||
|
'data-secret-delete-modal': '',
|
||||||
|
role: 'presentation'
|
||||||
|
});
|
||||||
|
var cancel = el('button', {
|
||||||
|
className: 'secrets-btn',
|
||||||
|
type: 'button',
|
||||||
|
'data-secret-delete-cancel': '',
|
||||||
|
onClick: removeDeleteModal
|
||||||
|
}, [tr('ui.cancel', null, 'Cancel')]);
|
||||||
|
var confirm = el('button', {
|
||||||
|
className: 'secrets-btn danger',
|
||||||
|
type: 'button',
|
||||||
|
'data-secret-delete-confirm': '',
|
||||||
|
onClick: function () {
|
||||||
|
removeDeleteModal();
|
||||||
|
confirmDeleteSecret(id);
|
||||||
|
}
|
||||||
|
}, [tr('ui.delete', null, 'Delete')]);
|
||||||
|
overlay.appendChild(el('div', {
|
||||||
|
className: 'secrets-modal',
|
||||||
|
role: 'dialog',
|
||||||
|
'aria-modal': 'true',
|
||||||
|
'aria-label': tr('ui.deleteSecretTitle', null, 'Delete secret')
|
||||||
|
}, [
|
||||||
|
el('h2', { className: 'secrets-modal-title' }, [tr('ui.deleteSecretTitle', null, 'Delete secret')]),
|
||||||
|
el('p', { className: 'secrets-modal-message' }, [tr('ui.deleteConfirm', { title: title }, 'Delete "' + title + '"?')]),
|
||||||
|
el('div', { className: 'secrets-modal-actions' }, [cancel, confirm])
|
||||||
|
]));
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
deleteModal = overlay;
|
||||||
|
if (cancel.focus) cancel.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDeleteModal() {
|
||||||
|
if (deleteModal && typeof deleteModal.remove === 'function') deleteModal.remove();
|
||||||
|
else if (deleteModal && deleteModal.parentNode) deleteModal.parentNode.removeChild(deleteModal);
|
||||||
|
deleteModal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDeleteSecret(id) {
|
||||||
api.secrets.delete(id).then(function () {
|
api.secrets.delete(id).then(function () {
|
||||||
selectedID = '';
|
selectedID = '';
|
||||||
selectedRecord = null;
|
selectedRecord = null;
|
||||||
selectedValue = '';
|
selectedValue = '';
|
||||||
|
isValueVisible = false;
|
||||||
return loadRecords();
|
return loadRecords();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setUserFacingError('delete', err);
|
setUserFacingError('delete', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copySecretValue(value) {
|
||||||
|
if (!value) return;
|
||||||
|
writeClipboard(api, value).then(function () {
|
||||||
|
setStatus(tr('ui.valueCopied', null, 'Secret value copied'), false);
|
||||||
|
}).catch(function (err) {
|
||||||
|
setUserFacingError('copyValue', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function copySecretLink(id) {
|
function copySecretLink(id) {
|
||||||
api.secrets.copyLink(id).then(function (link) {
|
api.secrets.copyLink(id).then(function (link) {
|
||||||
return writeClipboard(api, link).then(function () {
|
return writeClipboard(api, link).then(function () {
|
||||||
@@ -614,6 +697,7 @@
|
|||||||
|
|
||||||
containerEl.__secretsCleanup = function () {
|
containerEl.__secretsCleanup = function () {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
removeDeleteModal();
|
||||||
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
if (typeof localeUnsubscribe === 'function') localeUnsubscribe();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Secrets",
|
"manifest.name": "Secrets",
|
||||||
"manifest.description": "Encrypted global and workspace-scoped secret manager.",
|
"manifest.description": "Encrypted global and Deal-scoped secret manager.",
|
||||||
"contributions.views.verstak.secrets.view.title": "Secrets",
|
"contributions.views.verstak.secrets.view.title": "Secrets",
|
||||||
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Secrets",
|
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Secrets",
|
||||||
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
"contributions.openProviders.verstak.secrets.secret.title": "Secrets",
|
||||||
@@ -28,11 +28,14 @@
|
|||||||
"ui.copyLink": "Copy secret link",
|
"ui.copyLink": "Copy secret link",
|
||||||
"ui.edit": "Edit",
|
"ui.edit": "Edit",
|
||||||
"ui.delete": "Delete",
|
"ui.delete": "Delete",
|
||||||
|
"ui.copyValue": "Copy value",
|
||||||
|
"ui.showValue": "Show value",
|
||||||
|
"ui.hideValue": "Hide value",
|
||||||
"ui.fieldTitle": "Title",
|
"ui.fieldTitle": "Title",
|
||||||
"ui.optionalUsername": "optional username",
|
"ui.optionalUsername": "optional username",
|
||||||
"ui.secretValue": "Secret value",
|
"ui.secretValue": "Secret value",
|
||||||
"ui.global": "Global",
|
"ui.global": "Global",
|
||||||
"ui.workspace": "Workspace",
|
"ui.workspace": "Deal",
|
||||||
"ui.editSecret": "Edit secret",
|
"ui.editSecret": "Edit secret",
|
||||||
"ui.newSecret": "New secret",
|
"ui.newSecret": "New secret",
|
||||||
"ui.scope": "Scope",
|
"ui.scope": "Scope",
|
||||||
@@ -46,11 +49,14 @@
|
|||||||
"ui.readError": "Could not open this secret. Please try again.",
|
"ui.readError": "Could not open this secret. Please try again.",
|
||||||
"ui.saveError": "Could not save the 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.deleteError": "Could not delete the secret. Please try again.",
|
||||||
|
"ui.copyValueError": "Could not copy the secret value. Please try again.",
|
||||||
"ui.copyLinkError": "Could not copy the secret link. 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.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",
|
||||||
"ui.deleteConfirm": "Delete this secret?",
|
"ui.deleteSecretTitle": "Delete secret",
|
||||||
"ui.linkCopied": "Secret link copied"
|
"ui.deleteConfirm": "Delete \"{title}\"? This cannot be undone.",
|
||||||
|
"ui.linkCopied": "Secret link copied",
|
||||||
|
"ui.valueCopied": "Secret value copied"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Секреты",
|
"manifest.name": "Секреты",
|
||||||
"manifest.description": "Зашифрованное хранилище общих секретов и секретов рабочих пространств.",
|
"manifest.description": "Зашифрованное хранилище общих секретов и секретов Дел.",
|
||||||
"contributions.views.verstak.secrets.view.title": "Секреты",
|
"contributions.views.verstak.secrets.view.title": "Секреты",
|
||||||
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Секреты",
|
"contributions.sidebarItems.verstak.secrets.sidebar.title": "Секреты",
|
||||||
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
"contributions.openProviders.verstak.secrets.secret.title": "Секреты",
|
||||||
@@ -28,11 +28,14 @@
|
|||||||
"ui.copyLink": "Копировать ссылку на секрет",
|
"ui.copyLink": "Копировать ссылку на секрет",
|
||||||
"ui.edit": "Изменить",
|
"ui.edit": "Изменить",
|
||||||
"ui.delete": "Удалить",
|
"ui.delete": "Удалить",
|
||||||
|
"ui.copyValue": "Копировать значение",
|
||||||
|
"ui.showValue": "Показать значение",
|
||||||
|
"ui.hideValue": "Скрыть значение",
|
||||||
"ui.fieldTitle": "Название",
|
"ui.fieldTitle": "Название",
|
||||||
"ui.optionalUsername": "необязательное имя пользователя",
|
"ui.optionalUsername": "необязательное имя пользователя",
|
||||||
"ui.secretValue": "Значение секрета",
|
"ui.secretValue": "Значение секрета",
|
||||||
"ui.global": "Общий",
|
"ui.global": "Общий",
|
||||||
"ui.workspace": "Рабочее пространство",
|
"ui.workspace": "Дело",
|
||||||
"ui.editSecret": "Изменить секрет",
|
"ui.editSecret": "Изменить секрет",
|
||||||
"ui.newSecret": "Новый секрет",
|
"ui.newSecret": "Новый секрет",
|
||||||
"ui.scope": "Область",
|
"ui.scope": "Область",
|
||||||
@@ -46,11 +49,14 @@
|
|||||||
"ui.readError": "Не удалось открыть секрет. Повторите попытку.",
|
"ui.readError": "Не удалось открыть секрет. Повторите попытку.",
|
||||||
"ui.saveError": "Не удалось сохранить секрет. Повторите попытку.",
|
"ui.saveError": "Не удалось сохранить секрет. Повторите попытку.",
|
||||||
"ui.deleteError": "Не удалось удалить секрет. Повторите попытку.",
|
"ui.deleteError": "Не удалось удалить секрет. Повторите попытку.",
|
||||||
|
"ui.copyValueError": "Не удалось скопировать значение секрета. Повторите попытку.",
|
||||||
"ui.copyLinkError": "Не удалось скопировать ссылку на секрет. Повторите попытку.",
|
"ui.copyLinkError": "Не удалось скопировать ссылку на секрет. Повторите попытку.",
|
||||||
"ui.unlockError": "Не удалось разблокировать секреты. Проверьте мастер-пароль и повторите попытку.",
|
"ui.unlockError": "Не удалось разблокировать секреты. Проверьте мастер-пароль и повторите попытку.",
|
||||||
"ui.value": "Значение",
|
"ui.value": "Значение",
|
||||||
"ui.save": "Сохранить",
|
"ui.save": "Сохранить",
|
||||||
"ui.cancel": "Отмена",
|
"ui.cancel": "Отмена",
|
||||||
"ui.deleteConfirm": "Удалить этот секрет?",
|
"ui.deleteSecretTitle": "Удалить секрет",
|
||||||
"ui.linkCopied": "Ссылка на секрет скопирована"
|
"ui.deleteConfirm": "Удалить «{title}»? Это действие нельзя отменить.",
|
||||||
|
"ui.linkCopied": "Ссылка на секрет скопирована",
|
||||||
|
"ui.valueCopied": "Значение секрета скопировано"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Secrets",
|
"name": "Secrets",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Encrypted global and workspace-scoped secret manager.",
|
"description": "Encrypted global and Deal-scoped secret manager.",
|
||||||
"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": "key-round",
|
"icon": "key-round",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
let connectionOk = null
|
let connectionOk = null
|
||||||
|
|
||||||
let serverUrl = ''
|
let serverUrl = ''
|
||||||
|
let vaultId = ''
|
||||||
let username = ''
|
let username = ''
|
||||||
let password = ''
|
let password = ''
|
||||||
let syncInterval = 5
|
let syncInterval = 5
|
||||||
@@ -30,6 +31,9 @@
|
|||||||
|
|
||||||
function reportError(key, fallback, error) {
|
function reportError(key, fallback, error) {
|
||||||
console.warn('[verstak.sync] operation failed:', error)
|
console.warn('[verstak.sync] operation failed:', error)
|
||||||
|
const code = String(error?.message || error || '').match(/sync-server:([a-z_]+)/)?.[1]
|
||||||
|
const localized = code ? tr(`ui.serverError.${code}`, null, '') : ''
|
||||||
|
if (localized && localized !== `ui.serverError.${code}`) return localized
|
||||||
return tr(key, null, fallback)
|
return tr(key, null, fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@
|
|||||||
const saved = await api.settings.read()
|
const saved = await api.settings.read()
|
||||||
if (saved) {
|
if (saved) {
|
||||||
serverUrl = saved.serverUrl || ''
|
serverUrl = saved.serverUrl || ''
|
||||||
|
vaultId = saved.vaultId || ''
|
||||||
username = saved.username || ''
|
username = saved.username || ''
|
||||||
autoSync = !!saved.autoSync
|
autoSync = !!saved.autoSync
|
||||||
syncInterval = saved.syncInterval || 5
|
syncInterval = saved.syncInterval || 5
|
||||||
@@ -56,6 +61,7 @@
|
|||||||
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.vaultId) vaultId = settings.vaultId
|
||||||
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)
|
if (settings.lastError) console.warn('[verstak.sync] last sync failed:', settings.lastError)
|
||||||
@@ -78,7 +84,7 @@
|
|||||||
resultMsg = ''
|
resultMsg = ''
|
||||||
try {
|
try {
|
||||||
if (api?.settings?.writeAll) {
|
if (api?.settings?.writeAll) {
|
||||||
await api.settings.writeAll({ serverUrl, username, autoSync, syncInterval })
|
await api.settings.writeAll({ serverUrl, vaultId, username, autoSync, syncInterval })
|
||||||
}
|
}
|
||||||
await syncAPI().setInterval(autoSync ? syncInterval : 0)
|
await syncAPI().setInterval(autoSync ? syncInterval : 0)
|
||||||
resultMsg = tr('ui.settingsSaved', null, 'Settings saved.')
|
resultMsg = tr('ui.settingsSaved', null, 'Settings saved.')
|
||||||
@@ -112,7 +118,7 @@
|
|||||||
errorMsg = ''
|
errorMsg = ''
|
||||||
connectionResult = ''
|
connectionResult = ''
|
||||||
try {
|
try {
|
||||||
await syncAPI().configure(serverUrl, username, password)
|
await syncAPI().configure(serverUrl, username, password, vaultId)
|
||||||
connectionResult = tr('ui.connectedSuccessfully', null, 'Connected successfully.')
|
connectionResult = tr('ui.connectedSuccessfully', null, 'Connected successfully.')
|
||||||
connectionOk = true
|
connectionOk = true
|
||||||
username = ''
|
username = ''
|
||||||
@@ -235,6 +241,11 @@
|
|||||||
{tr('ui.lastSyncError', null, 'The last synchronization did not finish. Try again.')}
|
{tr('ui.lastSyncError', null, 'The last synchronization did not finish. Try again.')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if settings && settings.lastWarning && !errorMsg}
|
||||||
|
<div style="padding:0.5rem 0.75rem;margin-bottom:0.75rem;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.3);border-radius:6px;color:#f59e0b;font-size:0.85rem;">
|
||||||
|
{tr('ui.unresolvedFilesWarning', null, 'Some files are not synchronized yet. Resolve the file warning and run sync again.')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div style="background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;">
|
<div style="background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:1rem 1.25rem;margin-bottom:1rem;">
|
||||||
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">{tr('ui.server', null, 'Server')}</h3>
|
<h3 style="margin:0 0 0.75rem;color:#e0e0f0;font-size:0.95rem;">{tr('ui.server', null, 'Server')}</h3>
|
||||||
@@ -244,6 +255,12 @@
|
|||||||
<input id="sync-server-url" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={serverUrl} placeholder="https://example.com" />
|
<input id="sync-server-url" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={serverUrl} placeholder="https://example.com" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:0.75rem;">
|
||||||
|
<label for="sync-vault-id" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.remoteVaultId', null, 'Remote vault ID (optional)')}</label>
|
||||||
|
<input id="sync-vault-id" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={vaultId} />
|
||||||
|
<div style="color:#a0a0b8;font-size:0.78rem;margin-top:0.3rem;">{tr('ui.remoteVaultHint', null, 'Leave empty for this vault. Enter an existing remote vault ID to restore it on this device.')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom:0.75rem;">
|
<div style="margin-bottom:0.75rem;">
|
||||||
<label for="sync-username" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.username', null, 'Username')}</label>
|
<label for="sync-username" style="display:block;color:#a0a0b8;font-size:0.85rem;margin-bottom:0.35rem;">{tr('ui.username', null, 'Username')}</label>
|
||||||
<input id="sync-username" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={username} />
|
<input id="sync-username" type="text" style={INPUT_STYLE} on:focus={e => e.target.style.cssText = INPUT_FOCUS_STYLE} on:blur={e => e.target.style.cssText = INPUT_STYLE} bind:value={username} />
|
||||||
|
|||||||
@@ -24,9 +24,12 @@
|
|||||||
"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": "The last synchronization did not finish. Try again.",
|
"ui.lastSyncError": "The last synchronization did not finish. Try again.",
|
||||||
|
"ui.unresolvedFilesWarning": "Some files are not synchronized yet. Resolve the file warning and run sync again.",
|
||||||
"ui.syncConflictItem": "A synchronization conflict needs attention.",
|
"ui.syncConflictItem": "A synchronization conflict needs attention.",
|
||||||
"ui.server": "Server",
|
"ui.server": "Server",
|
||||||
"ui.serverUrl": "Server URL",
|
"ui.serverUrl": "Server URL",
|
||||||
|
"ui.remoteVaultId": "Remote vault ID (optional)",
|
||||||
|
"ui.remoteVaultHint": "Leave empty for this vault. Enter an existing remote vault ID to restore it on this device.",
|
||||||
"ui.username": "Username",
|
"ui.username": "Username",
|
||||||
"ui.password": "Password",
|
"ui.password": "Password",
|
||||||
"ui.testConnection": "Test Connection",
|
"ui.testConnection": "Test Connection",
|
||||||
@@ -45,4 +48,14 @@
|
|||||||
"ui.status.disabled": "Not configured",
|
"ui.status.disabled": "Not configured",
|
||||||
"ui.status.error": "Error",
|
"ui.status.error": "Error",
|
||||||
"ui.status.revoked": "Revoked"
|
"ui.status.revoked": "Revoked"
|
||||||
|
,"ui.serverError.rate_limited": "Too many attempts. Wait a little and try again."
|
||||||
|
,"ui.serverError.request_too_large": "The synchronization request is too large."
|
||||||
|
,"ui.serverError.payload_too_large": "A file operation is too large to send inline."
|
||||||
|
,"ui.serverError.too_many_operations": "Too many pending changes were sent at once. Try synchronization again."
|
||||||
|
,"ui.serverError.blob_too_large": "This file exceeds the server blob size limit."
|
||||||
|
,"ui.serverError.quota_exceeded": "The server storage quota for this vault has been reached."
|
||||||
|
,"ui.serverError.blob_not_owned": "The file data must be uploaded before it can be synchronized."
|
||||||
|
,"ui.serverError.blob_not_found": "The required file data is unavailable on the server."
|
||||||
|
,"ui.serverError.unauthorized": "This device is no longer authorized. Connect it again."
|
||||||
|
,"ui.serverError.forbidden": "The server denied this synchronization request."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,12 @@
|
|||||||
"ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.",
|
"ui.keyReset": "Ключ синхронизации сброшен. Подключитесь снова, чтобы связать устройство.",
|
||||||
"ui.syncConflicts": "Конфликты синхронизации",
|
"ui.syncConflicts": "Конфликты синхронизации",
|
||||||
"ui.lastSyncError": "Последняя синхронизация не завершилась. Повторите попытку.",
|
"ui.lastSyncError": "Последняя синхронизация не завершилась. Повторите попытку.",
|
||||||
|
"ui.unresolvedFilesWarning": "Некоторые файлы ещё не синхронизированы. Устраните предупреждение по файлу и повторите синхронизацию.",
|
||||||
"ui.syncConflictItem": "Конфликт синхронизации требует внимания.",
|
"ui.syncConflictItem": "Конфликт синхронизации требует внимания.",
|
||||||
"ui.server": "Сервер",
|
"ui.server": "Сервер",
|
||||||
"ui.serverUrl": "URL сервера",
|
"ui.serverUrl": "URL сервера",
|
||||||
|
"ui.remoteVaultId": "ID удалённого хранилища (необязательно)",
|
||||||
|
"ui.remoteVaultHint": "Оставьте пустым для этого хранилища. Укажите ID существующего удалённого хранилища, чтобы восстановить его на этом устройстве.",
|
||||||
"ui.username": "Имя пользователя",
|
"ui.username": "Имя пользователя",
|
||||||
"ui.password": "Пароль",
|
"ui.password": "Пароль",
|
||||||
"ui.testConnection": "Проверить подключение",
|
"ui.testConnection": "Проверить подключение",
|
||||||
@@ -45,4 +48,14 @@
|
|||||||
"ui.status.disabled": "Не настроено",
|
"ui.status.disabled": "Не настроено",
|
||||||
"ui.status.error": "Ошибка",
|
"ui.status.error": "Ошибка",
|
||||||
"ui.status.revoked": "Доступ отозван"
|
"ui.status.revoked": "Доступ отозван"
|
||||||
|
,"ui.serverError.rate_limited": "Слишком много попыток. Подождите и повторите попытку."
|
||||||
|
,"ui.serverError.request_too_large": "Запрос синхронизации слишком велик."
|
||||||
|
,"ui.serverError.payload_too_large": "Операция с файлом слишком велика для отправки в этом виде."
|
||||||
|
,"ui.serverError.too_many_operations": "Одновременно отправлено слишком много изменений. Повторите синхронизацию."
|
||||||
|
,"ui.serverError.blob_too_large": "Размер файла превышает разрешённый сервером лимит."
|
||||||
|
,"ui.serverError.quota_exceeded": "Достигнута квота серверного хранилища для этого vault."
|
||||||
|
,"ui.serverError.blob_not_owned": "Данные файла должны быть загружены до синхронизации операции."
|
||||||
|
,"ui.serverError.blob_not_found": "Необходимые данные файла недоступны на сервере."
|
||||||
|
,"ui.serverError.unauthorized": "Устройство больше не авторизовано. Подключите его заново."
|
||||||
|
,"ui.serverError.forbidden": "Сервер отклонил этот запрос синхронизации."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
var workspaceRoot = workspaceFromProps(props || {});
|
var workspaceRoot = workspaceFromProps(props || {});
|
||||||
return workspaceRoot
|
return workspaceRoot
|
||||||
? { mode: 'workspace', workspaceRoot: workspaceRoot, label: workspaceRoot }
|
? { mode: 'workspace', workspaceRoot: workspaceRoot, label: workspaceRoot }
|
||||||
: { mode: 'global', workspaceRoot: '', label: 'All workspaces' };
|
: { mode: 'global', workspaceRoot: '', label: 'All Deals' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function now() {
|
function now() {
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
function renderWorkspaceFilterOptions() {
|
function renderWorkspaceFilterOptions() {
|
||||||
if (scope.mode !== 'global') return;
|
if (scope.mode !== 'global') return;
|
||||||
workspaceFilterEl.innerHTML = '';
|
workspaceFilterEl.innerHTML = '';
|
||||||
workspaceFilterEl.appendChild(option('', tr('ui.allWorkspaces', null, 'All workspaces')));
|
workspaceFilterEl.appendChild(option('', tr('ui.allWorkspaces', null, 'All Deals')));
|
||||||
workspaceFilterEl.appendChild(option('__unassigned__', tr('ui.unassigned', null, 'Unassigned')));
|
workspaceFilterEl.appendChild(option('__unassigned__', tr('ui.unassigned', null, 'Unassigned')));
|
||||||
workspaceRoots().forEach(function (workspace) {
|
workspaceRoots().forEach(function (workspace) {
|
||||||
workspaceFilterEl.appendChild(option(workspace, workspace));
|
workspaceFilterEl.appendChild(option(workspace, workspace));
|
||||||
@@ -540,8 +540,8 @@
|
|||||||
el('label', { className: 'todo-field' }, [tr('ui.field.reminderDate', null, 'Reminder date'), reminderDateInput]),
|
el('label', { className: 'todo-field' }, [tr('ui.field.reminderDate', null, 'Reminder date'), reminderDateInput]),
|
||||||
el('label', { className: 'todo-field' }, [tr('ui.field.reminderTime', null, 'Reminder time'), reminderTimeInput])
|
el('label', { className: 'todo-field' }, [tr('ui.field.reminderTime', null, 'Reminder time'), reminderTimeInput])
|
||||||
];
|
];
|
||||||
if (workspaceInput) fields.push(el('label', { className: 'todo-field' }, [tr('ui.field.workspace', null, 'Workspace'), workspaceInput]));
|
if (workspaceInput) fields.push(el('label', { className: 'todo-field' }, [tr('ui.field.workspace', null, 'Deal'), workspaceInput]));
|
||||||
else fields.push(el('div', { className: 'todo-field', textContent: tr('ui.workspaceValue', { workspace: scope.workspaceRoot }, 'Workspace: ' + scope.workspaceRoot) }));
|
else fields.push(el('div', { className: 'todo-field', textContent: tr('ui.workspaceValue', { workspace: scope.workspaceRoot }, 'Deal: ' + scope.workspaceRoot) }));
|
||||||
|
|
||||||
modalHost.innerHTML = '';
|
modalHost.innerHTML = '';
|
||||||
if (typeof modalHost.removeAttribute === 'function') modalHost.removeAttribute('hidden');
|
if (typeof modalHost.removeAttribute === 'function') modalHost.removeAttribute('hidden');
|
||||||
@@ -636,7 +636,7 @@
|
|||||||
visible.forEach(function (todo) {
|
visible.forEach(function (todo) {
|
||||||
var actionButtons = [];
|
var actionButtons = [];
|
||||||
if (scope.mode === 'global' && todo.workspaceRootPath) {
|
if (scope.mode === 'global' && todo.workspaceRootPath) {
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'open-workspace', textContent: tr('ui.openWorkspace', null, 'Open workspace'), onClick: function () { openWorkspace(todo); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'open-workspace', textContent: tr('ui.openWorkspace', null, 'Open Deal'), onClick: function () { openWorkspace(todo); } }));
|
||||||
}
|
}
|
||||||
if (todo.status === 'open') {
|
if (todo.status === 'open') {
|
||||||
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'mark-done', textContent: tr('ui.status.done', null, 'Done'), onClick: function () { setTodoStatus(todo, 'done'); } }));
|
actionButtons.push(el('button', { className: 'todo-btn', type: 'button', 'data-todo-action': 'mark-done', textContent: tr('ui.status.done', null, 'Done'), onClick: function () { setTodoStatus(todo, 'done'); } }));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Todos",
|
"manifest.name": "Todos",
|
||||||
"manifest.description": "Global and workspace todo tracking with due and reminder metadata.",
|
"manifest.description": "Global and Deal-scoped todo tracking with due and reminder metadata.",
|
||||||
"contributions.views.verstak.todo.view.title": "Todos",
|
"contributions.views.verstak.todo.view.title": "Todos",
|
||||||
"contributions.sidebarItems.verstak.todo.sidebar.title": "Todos",
|
"contributions.sidebarItems.verstak.todo.sidebar.title": "Todos",
|
||||||
"contributions.workspaceItems.verstak.todo.workspace.title": "Todos",
|
"contributions.workspaceItems.verstak.todo.workspace.title": "Todos",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"ui.sort.updated": "Sort by updated",
|
"ui.sort.updated": "Sort by updated",
|
||||||
"ui.search": "Search todos",
|
"ui.search": "Search todos",
|
||||||
"ui.add": "Add Todo",
|
"ui.add": "Add Todo",
|
||||||
"ui.allWorkspaces": "All workspaces",
|
"ui.allWorkspaces": "All Deals",
|
||||||
"ui.unassigned": "Unassigned",
|
"ui.unassigned": "Unassigned",
|
||||||
"ui.saveError": "Could not save tasks. Please try again.",
|
"ui.saveError": "Could not save tasks. Please try again.",
|
||||||
"ui.loadError": "Could not load tasks. Please try again.",
|
"ui.loadError": "Could not load tasks. Please try again.",
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
"ui.reminderTimePlaceholder": "14:30",
|
"ui.reminderTimePlaceholder": "14:30",
|
||||||
"ui.reminderTimeInvalid": "Enter a valid reminder time (HH:MM)",
|
"ui.reminderTimeInvalid": "Enter a valid reminder time (HH:MM)",
|
||||||
"ui.reminderDateRequired": "Choose a reminder date before setting its time",
|
"ui.reminderDateRequired": "Choose a reminder date before setting its time",
|
||||||
"ui.field.workspace": "Workspace",
|
"ui.field.workspace": "Deal",
|
||||||
"ui.workspaceValue": "Workspace: {workspace}",
|
"ui.workspaceValue": "Deal: {workspace}",
|
||||||
"ui.edit": "Edit Todo",
|
"ui.edit": "Edit Todo",
|
||||||
"ui.cancel": "Cancel",
|
"ui.cancel": "Cancel",
|
||||||
"ui.saveChanges": "Save changes",
|
"ui.saveChanges": "Save changes",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"ui.reminderDateValue": "Reminder date {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 Deal",
|
||||||
"ui.reopen": "Reopen",
|
"ui.reopen": "Reopen",
|
||||||
"ui.createJournal": "Create Journal Entry",
|
"ui.createJournal": "Create Journal Entry",
|
||||||
"ui.editAction": "Edit",
|
"ui.editAction": "Edit",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"manifest.name": "Задачи",
|
"manifest.name": "Задачи",
|
||||||
"manifest.description": "Общие задачи и задачи рабочих пространств со сроками и напоминаниями.",
|
"manifest.description": "Общие задачи и задачи Дел со сроками и напоминаниями.",
|
||||||
"contributions.views.verstak.todo.view.title": "Задачи",
|
"contributions.views.verstak.todo.view.title": "Задачи",
|
||||||
"contributions.sidebarItems.verstak.todo.sidebar.title": "Задачи",
|
"contributions.sidebarItems.verstak.todo.sidebar.title": "Задачи",
|
||||||
"contributions.workspaceItems.verstak.todo.workspace.title": "Задачи",
|
"contributions.workspaceItems.verstak.todo.workspace.title": "Задачи",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"ui.sort.updated": "Сортировать по обновлению",
|
"ui.sort.updated": "Сортировать по обновлению",
|
||||||
"ui.search": "Поиск задач",
|
"ui.search": "Поиск задач",
|
||||||
"ui.add": "Добавить задачу",
|
"ui.add": "Добавить задачу",
|
||||||
"ui.allWorkspaces": "Все рабочие пространства",
|
"ui.allWorkspaces": "Все Дела",
|
||||||
"ui.unassigned": "Не назначено",
|
"ui.unassigned": "Не назначено",
|
||||||
"ui.saveError": "Не удалось сохранить задачи. Повторите попытку.",
|
"ui.saveError": "Не удалось сохранить задачи. Повторите попытку.",
|
||||||
"ui.loadError": "Не удалось загрузить задачи. Повторите попытку.",
|
"ui.loadError": "Не удалось загрузить задачи. Повторите попытку.",
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
"ui.reminderTimePlaceholder": "14:30",
|
"ui.reminderTimePlaceholder": "14:30",
|
||||||
"ui.reminderTimeInvalid": "Введите корректное время напоминания (ЧЧ:ММ)",
|
"ui.reminderTimeInvalid": "Введите корректное время напоминания (ЧЧ:ММ)",
|
||||||
"ui.reminderDateRequired": "Сначала выберите дату напоминания",
|
"ui.reminderDateRequired": "Сначала выберите дату напоминания",
|
||||||
"ui.field.workspace": "Рабочее пространство",
|
"ui.field.workspace": "Дело",
|
||||||
"ui.workspaceValue": "Рабочее пространство: {workspace}",
|
"ui.workspaceValue": "Дело: {workspace}",
|
||||||
"ui.edit": "Изменить задачу",
|
"ui.edit": "Изменить задачу",
|
||||||
"ui.cancel": "Отмена",
|
"ui.cancel": "Отмена",
|
||||||
"ui.saveChanges": "Сохранить изменения",
|
"ui.saveChanges": "Сохранить изменения",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"ui.reminderDateValue": "Дата напоминания: {date}",
|
"ui.reminderDateValue": "Дата напоминания: {date}",
|
||||||
"ui.noMatches": "Нет задач, соответствующих фильтрам.",
|
"ui.noMatches": "Нет задач, соответствующих фильтрам.",
|
||||||
"ui.empty": "Задач пока нет.",
|
"ui.empty": "Задач пока нет.",
|
||||||
"ui.openWorkspace": "Открыть рабочее пространство",
|
"ui.openWorkspace": "Открыть Дело",
|
||||||
"ui.reopen": "Открыть снова",
|
"ui.reopen": "Открыть снова",
|
||||||
"ui.createJournal": "Создать запись журнала",
|
"ui.createJournal": "Создать запись журнала",
|
||||||
"ui.editAction": "Изменить",
|
"ui.editAction": "Изменить",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Todos",
|
"name": "Todos",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"apiVersion": "0.1.0",
|
"apiVersion": "0.1.0",
|
||||||
"description": "Global and workspace todo tracking with due and reminder metadata.",
|
"description": "Global and Deal-scoped todo tracking with due and reminder metadata.",
|
||||||
"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": "list-todo",
|
"icon": "list-todo",
|
||||||
|
|||||||
@@ -217,7 +217,7 @@
|
|||||||
value: state.workspace,
|
value: state.workspace,
|
||||||
'data-trash-filter-workspace': '',
|
'data-trash-filter-workspace': '',
|
||||||
onChange: function (event) { state.workspace = event.target.value; render(); }
|
onChange: function (event) { state.workspace = event.target.value; render(); }
|
||||||
}, [el('option', { value: '' }, [tr('ui.allWorkspaces', null, 'All workspaces')])]);
|
}, [el('option', { value: '' }, [tr('ui.allWorkspaces', null, 'All Deals')])]);
|
||||||
workspaceOptions().forEach(function (workspace) {
|
workspaceOptions().forEach(function (workspace) {
|
||||||
workspaceSelect.appendChild(el('option', { value: workspace }, [workspace]));
|
workspaceSelect.appendChild(el('option', { value: workspace }, [workspace]));
|
||||||
});
|
});
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
var list = el('div', { className: 'trash-list', 'data-trash-list': '' });
|
var list = el('div', { className: 'trash-list', 'data-trash-list': '' });
|
||||||
list.appendChild(el('div', { className: 'trash-header' }, [
|
list.appendChild(el('div', { className: 'trash-header' }, [
|
||||||
el('span', {}, [tr('ui.name', null, 'Name')]),
|
el('span', {}, [tr('ui.name', null, 'Name')]),
|
||||||
el('span', {}, [tr('ui.workspace', null, 'Workspace')]),
|
el('span', {}, [tr('ui.workspace', null, 'Deal')]),
|
||||||
el('span', {}, [tr('ui.originalPath', null, 'Original path')]),
|
el('span', {}, [tr('ui.originalPath', null, 'Original path')]),
|
||||||
el('span', {}, [tr('ui.deleted', null, 'Deleted')]),
|
el('span', {}, [tr('ui.deleted', null, 'Deleted')]),
|
||||||
el('span', {}, [tr('ui.typeSize', null, 'Type / size')]),
|
el('span', {}, [tr('ui.typeSize', null, 'Type / size')]),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"ui.cannotUndo": "This cannot be undone.",
|
"ui.cannotUndo": "This cannot be undone.",
|
||||||
"ui.cancel": "Cancel",
|
"ui.cancel": "Cancel",
|
||||||
"ui.deleting": "Deleting...",
|
"ui.deleting": "Deleting...",
|
||||||
"ui.allWorkspaces": "All workspaces",
|
"ui.allWorkspaces": "All Deals",
|
||||||
"ui.allTypes": "All types",
|
"ui.allTypes": "All types",
|
||||||
"ui.files": "Files",
|
"ui.files": "Files",
|
||||||
"ui.folders": "Folders",
|
"ui.folders": "Folders",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"ui.total": "{count} total",
|
"ui.total": "{count} total",
|
||||||
"ui.filter": "Filter name or path",
|
"ui.filter": "Filter name or path",
|
||||||
"ui.refresh": "Refresh",
|
"ui.refresh": "Refresh",
|
||||||
"ui.workspace": "Workspace",
|
"ui.workspace": "Deal",
|
||||||
"ui.originalPath": "Original path",
|
"ui.originalPath": "Original path",
|
||||||
"ui.deleted": "Deleted",
|
"ui.deleted": "Deleted",
|
||||||
"ui.typeSize": "Type / size",
|
"ui.typeSize": "Type / size",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"ui.cannotUndo": "Это действие нельзя отменить.",
|
"ui.cannotUndo": "Это действие нельзя отменить.",
|
||||||
"ui.cancel": "Отмена",
|
"ui.cancel": "Отмена",
|
||||||
"ui.deleting": "Удаление...",
|
"ui.deleting": "Удаление...",
|
||||||
"ui.allWorkspaces": "Все рабочие пространства",
|
"ui.allWorkspaces": "Все Дела",
|
||||||
"ui.allTypes": "Все типы",
|
"ui.allTypes": "Все типы",
|
||||||
"ui.files": "Файлы",
|
"ui.files": "Файлы",
|
||||||
"ui.folders": "Папки",
|
"ui.folders": "Папки",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"ui.total": "Всего: {count}",
|
"ui.total": "Всего: {count}",
|
||||||
"ui.filter": "Фильтр по имени или пути",
|
"ui.filter": "Фильтр по имени или пути",
|
||||||
"ui.refresh": "Обновить",
|
"ui.refresh": "Обновить",
|
||||||
"ui.workspace": "Рабочее пространство",
|
"ui.workspace": "Дело",
|
||||||
"ui.originalPath": "Исходный путь",
|
"ui.originalPath": "Исходный путь",
|
||||||
"ui.deleted": "Удалён",
|
"ui.deleted": "Удалён",
|
||||||
"ui.typeSize": "Тип / размер",
|
"ui.typeSize": "Тип / размер",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"manifest.name": "Workspace Folders",
|
||||||
|
"manifest.description": "Nested folders with icons, colors and drag-and-drop for the Deal tree.",
|
||||||
|
"common.close": "Close",
|
||||||
|
"common.cancel": "Cancel",
|
||||||
|
"common.save": "Save",
|
||||||
|
"common.create": "Create",
|
||||||
|
"common.select": "Select",
|
||||||
|
"workspaceTree.title": "Deals",
|
||||||
|
"workspaceTree.new": "New Deal",
|
||||||
|
"workspaceTree.nameRequired": "Name is required",
|
||||||
|
"workspaceTree.namePlaceholder": "Deal name",
|
||||||
|
"workspaceTree.template": "Template",
|
||||||
|
"workspaceTree.create": "Create Deal",
|
||||||
|
"workspaceTree.trash": "Move Deal to trash",
|
||||||
|
"pluginCard.name": "Name",
|
||||||
|
"folder.create": "Create Folder",
|
||||||
|
"folder.edit": "Edit Folder",
|
||||||
|
"folder.delete": "Delete Folder",
|
||||||
|
"folder.deleteConfirm": "Delete folder? Deals inside will move to the parent folder.",
|
||||||
|
"folder.namePlaceholder": "Folder name",
|
||||||
|
"folder.nameRequired": "Name is required",
|
||||||
|
"folder.icon": "Icon",
|
||||||
|
"folder.chooseIcon": "Choose...",
|
||||||
|
"folder.color": "Color",
|
||||||
|
"folder.chooseColor": "Choose...",
|
||||||
|
"folder.iconPickerTitle": "Choose Icon",
|
||||||
|
"folder.iconSearch": "Search icons...",
|
||||||
|
"folder.colorPickerTitle": "Choose Color"
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"manifest.name": "Папки дел",
|
||||||
|
"manifest.description": "Вложенные папки с иконками, цветами и drag-and-drop для дерева Дел.",
|
||||||
|
"common.close": "Закрыть",
|
||||||
|
"common.cancel": "Отмена",
|
||||||
|
"common.save": "Сохранить",
|
||||||
|
"common.create": "Создать",
|
||||||
|
"common.select": "Выбрать",
|
||||||
|
"workspaceTree.title": "Дела",
|
||||||
|
"workspaceTree.new": "Новое дело",
|
||||||
|
"workspaceTree.nameRequired": "Имя обязательно",
|
||||||
|
"workspaceTree.namePlaceholder": "Название дела",
|
||||||
|
"workspaceTree.template": "Шаблон",
|
||||||
|
"workspaceTree.create": "Создать дело",
|
||||||
|
"workspaceTree.trash": "Переместить дело в корзину",
|
||||||
|
"pluginCard.name": "Имя",
|
||||||
|
"folder.create": "Создать папку",
|
||||||
|
"folder.edit": "Редактировать папку",
|
||||||
|
"folder.delete": "Удалить папку",
|
||||||
|
"folder.deleteConfirm": "Удалить папку? Дела внутри переместятся в родительскую папку.",
|
||||||
|
"folder.namePlaceholder": "Название папки",
|
||||||
|
"folder.nameRequired": "Имя обязательно",
|
||||||
|
"folder.icon": "Иконка",
|
||||||
|
"folder.chooseIcon": "Выбрать...",
|
||||||
|
"folder.color": "Цвет",
|
||||||
|
"folder.chooseColor": "Выбрать...",
|
||||||
|
"folder.iconPickerTitle": "Выбрать иконку",
|
||||||
|
"folder.iconSearch": "Поиск иконок...",
|
||||||
|
"folder.colorPickerTitle": "Выбрать цвет"
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "verstak.workspace-folders",
|
||||||
|
"name": "Workspace Folders",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"apiVersion": "0.1.0",
|
||||||
|
"description": "Nested folders with icons, colors and drag-and-drop for the Deal tree.",
|
||||||
|
"source": "official",
|
||||||
|
"icon": "folder",
|
||||||
|
"localization": {
|
||||||
|
"defaultLocale": "en",
|
||||||
|
"locales": {
|
||||||
|
"en": "locales/en.json",
|
||||||
|
"ru": "locales/ru.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"provides": ["workspace.folders"],
|
||||||
|
"requires": [
|
||||||
|
"verstak/core/files/v1",
|
||||||
|
"verstak/core/workspace/v1"
|
||||||
|
],
|
||||||
|
"optionalRequires": [],
|
||||||
|
"permissions": [
|
||||||
|
"files.read",
|
||||||
|
"files.write",
|
||||||
|
"ui.register",
|
||||||
|
"events.publish",
|
||||||
|
"events.subscribe",
|
||||||
|
"storage.namespace"
|
||||||
|
],
|
||||||
|
"frontend": {
|
||||||
|
"entry": "frontend/src/index.js",
|
||||||
|
"style": "frontend/src/style.css"
|
||||||
|
},
|
||||||
|
"contributes": {
|
||||||
|
"workspaceTree": {
|
||||||
|
"component": "FolderTreeView"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
## Highlights
|
||||||
|
|
||||||
|
- First public bundle of Verstak's official plugins.
|
||||||
|
- Includes Files, Notes, Journal, Todo, Secrets, Browser Inbox, Activity, Search, Trash, Sync, and supporting editors and previews.
|
||||||
|
- Adds durable browser captures, archive handling, activity sessions, and localized plugin interfaces.
|
||||||
|
|
||||||
|
**Commit history**: https://github.com/mirivlad/verstak-official-plugins/commits/v0.1.0-alpha.1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Todo tasks can schedule native reminders through the Desktop notification capability.
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/mirivlad/verstak-official-plugins/compare/v0.1.0-alpha.1...v0.1.0-alpha.2
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Makes Todo reminder time selection, global Secrets, and global Journal entry creation usable.
|
||||||
|
- Localizes plugin errors, browser materials, activity review, files menus, and other key workflows.
|
||||||
|
- Replaces technical terminology with “Deal” and improves modal forms and secret safety.
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/mirivlad/verstak-official-plugins/compare/v0.1.0-alpha.2...v0.1.0-alpha.3
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Folder rows in the Files plugin now use localized labels instead of the internal `folder` type.
|
||||||
|
- File icons have localized, accessible names in both supported languages.
|
||||||
|
|
||||||
|
Install the archive matching your platform into Verstak's plugins directory, replacing the previous official plugins bundle.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
## Highlights
|
||||||
|
|
||||||
|
- **Workspace Folders plugin.** A new official plugin `verstak.workspace-folders`
|
||||||
|
replaces the sidebar Deal tree with a rich folder tree: icons (1703 Lucide
|
||||||
|
icons), 12 preset colors, drag‑and‑drop between folders, inline folder
|
||||||
|
creation and editing, and a Deal creation modal with parent folder selector.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Unpack the archive for your platform into Verstak's `plugins/` directory,
|
||||||
|
replacing the previous official plugins bundle.
|
||||||
@@ -6,6 +6,7 @@ VERSION="${1:-}"
|
|||||||
REPOSITORY="mirivlad/verstak-official-plugins"
|
REPOSITORY="mirivlad/verstak-official-plugins"
|
||||||
RELEASE_SCRIPT="${VERSTAK_RELEASE_SCRIPT:-$ROOT/scripts/release.sh}"
|
RELEASE_SCRIPT="${VERSTAK_RELEASE_SCRIPT:-$ROOT/scripts/release.sh}"
|
||||||
RELEASE_DIR="${VERSTAK_RELEASE_DIR:-$ROOT/release}"
|
RELEASE_DIR="${VERSTAK_RELEASE_DIR:-$ROOT/release}"
|
||||||
|
RELEASE_NOTES_DIR="${VERSTAK_RELEASE_NOTES_DIR:-$ROOT/release-notes}"
|
||||||
GIT_BIN="${GIT_BIN:-git}"
|
GIT_BIN="${GIT_BIN:-git}"
|
||||||
GH_BIN="${GH_BIN:-gh}"
|
GH_BIN="${GH_BIN:-gh}"
|
||||||
|
|
||||||
@@ -64,7 +65,17 @@ fi
|
|||||||
if "$GH_BIN" release view "$VERSION" --repo "$REPOSITORY" >/dev/null 2>&1; then
|
if "$GH_BIN" release view "$VERSION" --repo "$REPOSITORY" >/dev/null 2>&1; then
|
||||||
"$GH_BIN" release upload "$VERSION" "${ASSETS[@]}" --repo "$REPOSITORY" --clobber
|
"$GH_BIN" release upload "$VERSION" "${ASSETS[@]}" --repo "$REPOSITORY" --clobber
|
||||||
else
|
else
|
||||||
RELEASE_OPTIONS=(--generate-notes --verify-tag)
|
NOTES_FILE="$RELEASE_NOTES_DIR/$VERSION.md"
|
||||||
|
if [[ ! -s "$NOTES_FILE" ]]; then
|
||||||
|
echo "human-readable release notes are required: $NOTES_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_OPTIONS=(--notes-file "$NOTES_FILE" --generate-notes --verify-tag)
|
||||||
|
PREVIOUS_TAG="$("$GIT_BIN" describe --tags --abbrev=0 "${HEAD}^" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$PREVIOUS_TAG" ]]; then
|
||||||
|
RELEASE_OPTIONS+=(--notes-start-tag "$PREVIOUS_TAG")
|
||||||
|
fi
|
||||||
if [[ "$VERSION" == *-* ]]; then
|
if [[ "$VERSION" == *-* ]]; then
|
||||||
RELEASE_OPTIONS+=(--prerelease)
|
RELEASE_OPTIONS+=(--prerelease)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -280,13 +280,16 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w
|
|||||||
}
|
}
|
||||||
if (api.storedEvents(globalKey).length !== 0) throw new Error('workspace activity leaked into global storage');
|
if (api.storedEvents(globalKey).length !== 0) throw new Error('workspace activity leaked into global storage');
|
||||||
if (!container.textContent.includes('Example Article')) throw new Error('browser capture title was not rendered');
|
if (!container.textContent.includes('Example Article')) throw new Error('browser capture title was not rendered');
|
||||||
if (!container.textContent.includes('browser.capture.selection')) throw new Error('event type was not rendered');
|
if (!container.textContent.includes('Selection captured')) throw new Error('browser capture label was not rendered');
|
||||||
if (!container.textContent.includes('browser.capture.converted')) throw new Error('conversion event type was not rendered');
|
if (!container.textContent.includes('Capture converted')) throw new Error('conversion label was not rendered');
|
||||||
|
if (container.textContent.includes('browser.capture.selection') || container.textContent.includes('browser.capture.converted') || container.textContent.includes('verstak.browser-inbox')) {
|
||||||
|
throw new Error('Activity must not expose technical event or plugin identifiers in its UI');
|
||||||
|
}
|
||||||
if (!container.textContent.includes('Possible journal entries')) throw new Error('work session candidate section was not rendered');
|
if (!container.textContent.includes('Possible journal entries')) throw new Error('work session candidate section was not rendered');
|
||||||
if (container.textContent.includes('Project work on 2026-06-27')) throw new Error('candidate must not invent a worklog title');
|
if (container.textContent.includes('Project work on 2026-06-27')) throw new Error('candidate must not invent a worklog title');
|
||||||
const candidateNode = walk(container, (node) => node.getAttribute && node.getAttribute('data-work-session-candidate'));
|
const candidateNode = walk(container, (node) => node.getAttribute && node.getAttribute('data-work-session-candidate'));
|
||||||
if (!candidateNode) throw new Error('work session candidate data attribute was not rendered');
|
if (!candidateNode) throw new Error('work session candidate data attribute was not rendered');
|
||||||
if (!candidateNode.textContent.includes('Workspace: Project')) throw new Error('candidate workspace was not rendered');
|
if (!candidateNode.textContent.includes('Deal: Project')) throw new Error('candidate Deal was not rendered');
|
||||||
if (!candidateNode.textContent.includes('Estimated duration: 20 min')) throw new Error('candidate duration was not rendered');
|
if (!candidateNode.textContent.includes('Estimated duration: 20 min')) throw new Error('candidate duration was not rendered');
|
||||||
if (!candidateNode.textContent.includes('Activities: 3')) throw new Error('candidate activity count was not rendered');
|
if (!candidateNode.textContent.includes('Activities: 3')) throw new Error('candidate activity count was not rendered');
|
||||||
if (!walk(candidateNode, (node) => node.getAttribute && node.getAttribute('data-work-session-action') === 'review')) throw new Error('candidate review action was not rendered');
|
if (!walk(candidateNode, (node) => node.getAttribute && node.getAttribute('data-work-session-action') === 'review')) throw new Error('candidate review action was not rendered');
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ async function flush() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mountEditor(secretProviderEnabled) {
|
async function mountEditor(secretProviderEnabled, translations) {
|
||||||
const document = makeDocument();
|
const document = makeDocument();
|
||||||
const component = loadComponent(document);
|
const component = loadComponent(document);
|
||||||
const opened = [];
|
const opened = [];
|
||||||
@@ -146,6 +146,15 @@ async function mountEditor(secretProviderEnabled) {
|
|||||||
return { status: 'opened', request };
|
return { status: 'opened', request };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
i18n: {
|
||||||
|
t: (key, params, fallback) => {
|
||||||
|
let value = translations && translations[key] ? translations[key] : (fallback || key);
|
||||||
|
Object.entries(params || {}).forEach(([name, replacement]) => {
|
||||||
|
value = value.replace(`{${name}}`, String(replacement));
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
component.mount(container, {
|
component.mount(container, {
|
||||||
@@ -162,6 +171,12 @@ async function mountEditor(secretProviderEnabled) {
|
|||||||
if (disabledPreview.innerHTML.includes('data-secret-id')) throw new Error('secret link rendered without secrets provider');
|
if (disabledPreview.innerHTML.includes('data-secret-id')) throw new Error('secret link rendered without secrets provider');
|
||||||
|
|
||||||
const enabled = await mountEditor(true);
|
const enabled = await mountEditor(true);
|
||||||
|
if (enabled.container.textContent.includes('notes-markdown') || enabled.container.textContent.includes('generic-markdown')) {
|
||||||
|
throw new Error('editor must not expose the technical editor mode in its toolbar');
|
||||||
|
}
|
||||||
|
if (enabled.container.textContent.includes('Notes context active')) {
|
||||||
|
throw new Error('editor must not show an implementation roadmap in the notes UI');
|
||||||
|
}
|
||||||
const preview = walk(enabled.container, (node) => node.className === 'de-preview');
|
const preview = walk(enabled.container, (node) => node.className === 'de-preview');
|
||||||
if (!preview) throw new Error('enabled preview missing');
|
if (!preview) throw new Error('enabled preview missing');
|
||||||
if (!preview.innerHTML.includes('data-secret-id="client-a.db"')) throw new Error('secret link did not render with provider');
|
if (!preview.innerHTML.includes('data-secret-id="client-a.db"')) throw new Error('secret link did not render with provider');
|
||||||
@@ -182,6 +197,12 @@ async function mountEditor(secretProviderEnabled) {
|
|||||||
throw new Error('secret link did not open through workbench');
|
throw new Error('secret link did not open through workbench');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const russian = await mountEditor(true, { 'ui.md.heading': 'Заголовок' });
|
||||||
|
const headingButton = walk(russian.container, (node) => node.getAttribute && node.getAttribute('data-md-action') === 'heading');
|
||||||
|
if (!headingButton || headingButton.getAttribute('title') !== 'Заголовок') {
|
||||||
|
throw new Error('Markdown toolbar titles must use the locale catalog');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('default editor smoke passed');
|
console.log('default editor smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -195,9 +195,11 @@ function loadFilesComponent(document) {
|
|||||||
return { component, clipboard: sandbox.navigator.clipboard };
|
return { component, clipboard: sandbox.navigator.clipboard };
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeApi() {
|
function makeApi(options = {}) {
|
||||||
const externalCalls = [];
|
const externalCalls = [];
|
||||||
const contributionCalls = [];
|
const contributionCalls = [];
|
||||||
|
const created = [];
|
||||||
|
const moved = [];
|
||||||
const eventHandlers = {};
|
const eventHandlers = {};
|
||||||
let restored = false;
|
let restored = false;
|
||||||
let externalVisible = false;
|
let externalVisible = false;
|
||||||
@@ -212,6 +214,8 @@ function makeApi() {
|
|||||||
return {
|
return {
|
||||||
externalCalls,
|
externalCalls,
|
||||||
contributionCalls,
|
contributionCalls,
|
||||||
|
created,
|
||||||
|
moved,
|
||||||
emitFileChanged(payload) {
|
emitFileChanged(payload) {
|
||||||
(eventHandlers['file.changed'] || []).forEach((handler) => handler({
|
(eventHandlers['file.changed'] || []).forEach((handler) => handler({
|
||||||
name: 'file.changed',
|
name: 'file.changed',
|
||||||
@@ -229,6 +233,15 @@ function makeApi() {
|
|||||||
size: 12,
|
size: 12,
|
||||||
modifiedAt: '2026-06-27T00:00:00Z',
|
modifiedAt: '2026-06-27T00:00:00Z',
|
||||||
}];
|
}];
|
||||||
|
if (options.includeFolder) {
|
||||||
|
entries.push({
|
||||||
|
name: 'Archive',
|
||||||
|
relativePath: 'Docs/Archive',
|
||||||
|
type: 'folder',
|
||||||
|
size: 0,
|
||||||
|
modifiedAt: '2026-06-27T00:00:00Z',
|
||||||
|
});
|
||||||
|
}
|
||||||
if (restored) {
|
if (restored) {
|
||||||
entries.push({
|
entries.push({
|
||||||
name: 'deleted.md',
|
name: 'deleted.md',
|
||||||
@@ -253,9 +266,15 @@ function makeApi() {
|
|||||||
},
|
},
|
||||||
metadata: async () => { throw new Error('not-found'); },
|
metadata: async () => { throw new Error('not-found'); },
|
||||||
readText: async () => '# Readme\n',
|
readText: async () => '# Readme\n',
|
||||||
writeText: async () => undefined,
|
writeText: async (relativePath, content, options) => {
|
||||||
createFolder: async () => undefined,
|
created.push({ type: 'file', relativePath, content, options });
|
||||||
move: async () => undefined,
|
},
|
||||||
|
createFolder: async (relativePath) => {
|
||||||
|
created.push({ type: 'folder', relativePath });
|
||||||
|
},
|
||||||
|
move: async (fromRelativePath, toRelativePath) => {
|
||||||
|
moved.push({ fromRelativePath, toRelativePath });
|
||||||
|
},
|
||||||
trash: async () => undefined,
|
trash: async () => undefined,
|
||||||
listTrash: async () => trashEntries.slice(),
|
listTrash: async () => trashEntries.slice(),
|
||||||
restoreTrash: async (trashId) => {
|
restoreTrash: async (trashId) => {
|
||||||
@@ -307,6 +326,15 @@ function makeApi() {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
i18n: {
|
||||||
|
t: (key, params, fallback) => {
|
||||||
|
let value = options.translations && options.translations[key] ? options.translations[key] : (fallback || key);
|
||||||
|
Object.entries(params || {}).forEach(([name, replacement]) => {
|
||||||
|
value = value.replace(`{${name}}`, String(replacement));
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
},
|
||||||
showExternalFile() {
|
showExternalFile() {
|
||||||
externalVisible = true;
|
externalVisible = true;
|
||||||
},
|
},
|
||||||
@@ -328,6 +356,62 @@ async function flush() {
|
|||||||
const row = walk(container, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/readme.md');
|
const row = walk(container, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/readme.md');
|
||||||
if (!row) throw new Error('file row not rendered');
|
if (!row) throw new Error('file row not rendered');
|
||||||
|
|
||||||
|
const newFolder = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-action') === 'new-folder');
|
||||||
|
if (!newFolder) throw new Error('new folder button not found');
|
||||||
|
newFolder.click();
|
||||||
|
const createModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-create-modal') !== undefined);
|
||||||
|
if (!createModal) throw new Error('create file modal not found');
|
||||||
|
const createInput = walk(createModal, (node) => node.getAttribute && node.getAttribute('data-files-create-input') !== undefined);
|
||||||
|
if (!createInput) throw new Error('create file input not found');
|
||||||
|
const createConfirm = walk(createModal, (node) => node.tagName === 'BUTTON' && node.textContent === 'Create');
|
||||||
|
if (!createConfirm) throw new Error('create file confirm button not found');
|
||||||
|
createConfirm.click();
|
||||||
|
const createError = walk(createModal, (node) => node.getAttribute && node.getAttribute('data-files-create-error') !== undefined);
|
||||||
|
if (!createError || !createError.textContent.includes('Enter a name') || createInput.getAttribute('aria-invalid') !== 'true') {
|
||||||
|
throw new Error('create file modal does not show a validation error for an empty name');
|
||||||
|
}
|
||||||
|
createInput.value = 'New folder';
|
||||||
|
createInput.dispatchEvent('keydown', { key: 'Enter' });
|
||||||
|
await flush();
|
||||||
|
if (!api.created.some((entry) => entry.type === 'folder' && entry.relativePath === 'New folder')) {
|
||||||
|
throw new Error(`new folder was not created: ${JSON.stringify(api.created)}`);
|
||||||
|
}
|
||||||
|
if (walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-create-modal') !== undefined)) {
|
||||||
|
throw new Error('create file modal should close after a successful create');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMarkdown = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-action') === 'new-markdown');
|
||||||
|
if (!newMarkdown) throw new Error('new markdown button not found');
|
||||||
|
newMarkdown.click();
|
||||||
|
const markdownModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-create-modal') !== undefined);
|
||||||
|
const markdownInput = walk(markdownModal, (node) => node.getAttribute && node.getAttribute('data-files-create-input') !== undefined);
|
||||||
|
markdownInput.value = 'Readme';
|
||||||
|
markdownInput.dispatchEvent('keydown', { key: 'Enter' });
|
||||||
|
await flush();
|
||||||
|
if (!api.created.some((entry) => entry.type === 'file' && entry.relativePath === 'Readme.md')) {
|
||||||
|
throw new Error(`new markdown file was not created: ${JSON.stringify(api.created)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rename = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-action') === 'row-rename');
|
||||||
|
if (!rename) throw new Error('rename file button not found');
|
||||||
|
rename.click();
|
||||||
|
const renameModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-files-rename-modal') !== undefined);
|
||||||
|
if (!renameModal) throw new Error('rename file modal not found');
|
||||||
|
const renameInput = walk(renameModal, (node) => node.getAttribute && node.getAttribute('data-files-rename-input') !== undefined);
|
||||||
|
if (!renameInput || renameInput.value !== 'readme.md') throw new Error('rename file input should keep the current filename and extension');
|
||||||
|
renameInput.value = '';
|
||||||
|
renameInput.dispatchEvent('keydown', { key: 'Enter' });
|
||||||
|
const renameError = walk(renameModal, (node) => node.getAttribute && node.getAttribute('data-files-rename-error') !== undefined);
|
||||||
|
if (!renameError || !renameError.textContent.includes('Enter a name') || renameInput.getAttribute('aria-invalid') !== 'true') {
|
||||||
|
throw new Error('rename file modal does not show a validation error for an empty name');
|
||||||
|
}
|
||||||
|
renameInput.value = 'renamed.md';
|
||||||
|
renameInput.dispatchEvent('keydown', { key: 'Enter' });
|
||||||
|
await flush();
|
||||||
|
if (!api.moved.some((entry) => entry.fromRelativePath === 'Docs/readme.md' && entry.toRelativePath === 'Docs/renamed.md')) {
|
||||||
|
throw new Error(`rename did not preserve the extension: ${JSON.stringify(api.moved)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const list = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-list') !== undefined);
|
const list = walk(container, (node) => node.getAttribute && node.getAttribute('data-files-list') !== undefined);
|
||||||
if (!list) throw new Error('files list not rendered');
|
if (!list) throw new Error('files list not rendered');
|
||||||
list.dispatchEvent('contextmenu', { target: row, clientX: 20, clientY: 20 });
|
list.dispatchEvent('contextmenu', { target: row, clientX: 20, clientY: 20 });
|
||||||
@@ -378,6 +462,41 @@ async function flush() {
|
|||||||
throw new Error(`external file row not rendered after file.changed: ${container.textContent}`);
|
throw new Error(`external file row not rendered after file.changed: ${container.textContent}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const russianDocument = makeDocument();
|
||||||
|
const { component: russianComponent } = loadFilesComponent(russianDocument);
|
||||||
|
const russianApi = makeApi({
|
||||||
|
includeFolder: true,
|
||||||
|
translations: {
|
||||||
|
'ui.open': 'Открыть',
|
||||||
|
'ui.openExternal': 'Открыть во внешнем приложении',
|
||||||
|
'ui.showInFolder': 'Показать в папке',
|
||||||
|
'ui.rename': 'Переименовать',
|
||||||
|
'ui.duplicate': 'Создать копию',
|
||||||
|
'ui.cut': 'Вырезать',
|
||||||
|
'ui.copy': 'Копировать',
|
||||||
|
'ui.trash': 'Переместить в корзину',
|
||||||
|
'ui.folderType': 'Папка',
|
||||||
|
'ui.icon.markdown': 'Файл Markdown',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const russianContainer = new FakeNode('div');
|
||||||
|
russianComponent.mount(russianContainer, {}, russianApi);
|
||||||
|
await flush();
|
||||||
|
if (!russianContainer.textContent.includes('Папка') || russianContainer.textContent.includes('folder')) {
|
||||||
|
throw new Error(`folder type was not localized: ${russianContainer.textContent}`);
|
||||||
|
}
|
||||||
|
const russianList = walk(russianContainer, (node) => node.getAttribute && node.getAttribute('data-files-list') !== undefined);
|
||||||
|
const russianRow = walk(russianContainer, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/readme.md');
|
||||||
|
const russianIcon = walk(russianRow, (node) => node.getAttribute && node.getAttribute('data-file-icon') === 'markdown');
|
||||||
|
if (!russianIcon || russianIcon.getAttribute('title') !== 'Файл Markdown' || russianIcon.getAttribute('aria-label') !== 'Файл Markdown') {
|
||||||
|
throw new Error(`file icon label was not localized: ${russianIcon && russianIcon.getAttribute('title')}`);
|
||||||
|
}
|
||||||
|
russianList.dispatchEvent('contextmenu', { target: russianRow, clientX: 20, clientY: 20 });
|
||||||
|
const russianMenuText = russianDocument.body.textContent;
|
||||||
|
['Открыть во внешнем приложении', 'Показать в папке', 'Переименовать', 'Создать копию', 'Вырезать', 'Копировать', 'Переместить в корзину'].forEach((label) => {
|
||||||
|
if (!russianMenuText.includes(label)) throw new Error(`context menu label was not localized: ${label}`);
|
||||||
|
});
|
||||||
|
|
||||||
console.log('files frontend smoke passed');
|
console.log('files frontend smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ const vm = require('vm');
|
|||||||
const root = path.resolve(__dirname, '..');
|
const root = path.resolve(__dirname, '..');
|
||||||
const manifestPath = path.join(root, 'plugins', 'journal', 'plugin.json');
|
const manifestPath = path.join(root, 'plugins', 'journal', 'plugin.json');
|
||||||
const sourcePath = path.join(root, 'plugins', 'journal', 'frontend', 'src', 'index.js');
|
const sourcePath = path.join(root, 'plugins', 'journal', 'frontend', 'src', 'index.js');
|
||||||
|
const russianLocalePath = path.join(root, 'plugins', 'journal', 'locales', 'ru.json');
|
||||||
|
|
||||||
if (!fs.existsSync(manifestPath)) throw new Error('journal plugin manifest missing');
|
if (!fs.existsSync(manifestPath)) throw new Error('journal plugin manifest missing');
|
||||||
if (!fs.existsSync(sourcePath)) throw new Error('journal frontend entry missing');
|
if (!fs.existsSync(sourcePath)) throw new Error('journal frontend entry missing');
|
||||||
|
|
||||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||||
|
const russianLocale = JSON.parse(fs.readFileSync(russianLocalePath, 'utf8'));
|
||||||
|
|
||||||
class FakeNode {
|
class FakeNode {
|
||||||
constructor(tagName) {
|
constructor(tagName) {
|
||||||
@@ -131,7 +133,7 @@ function loadComponent(document) {
|
|||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeApi(initialSettings = {}) {
|
function makeApi(initialSettings = {}, locale = null) {
|
||||||
const settings = { ...initialSettings };
|
const settings = { ...initialSettings };
|
||||||
const publishedEvents = [];
|
const publishedEvents = [];
|
||||||
return {
|
return {
|
||||||
@@ -154,6 +156,11 @@ function makeApi(initialSettings = {}) {
|
|||||||
{ type: 'folder', relativePath: 'Client', name: 'Client' },
|
{ type: 'folder', relativePath: 'Client', name: 'Client' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
i18n: locale ? {
|
||||||
|
t(key, params, fallback) {
|
||||||
|
return String(locale[key] || fallback || key).replace(/\{(\w+)\}/g, (_match, name) => String((params || {})[name] ?? ''));
|
||||||
|
},
|
||||||
|
} : null,
|
||||||
storedEntries(key) {
|
storedEntries(key) {
|
||||||
return settings[key] || [];
|
return settings[key] || [];
|
||||||
},
|
},
|
||||||
@@ -245,8 +252,11 @@ function byData(container, attr, value) {
|
|||||||
toolRequest: { type: 'work-session-candidate', candidate },
|
toolRequest: { type: 'work-session-candidate', candidate },
|
||||||
});
|
});
|
||||||
if (!candidateView.container.textContent.includes('Review possible journal entry')) throw new Error('candidate review modal was not opened');
|
if (!candidateView.container.textContent.includes('Review possible journal entry')) throw new Error('candidate review modal was not opened');
|
||||||
if (!candidateView.container.textContent.includes('Workspace: Project')) throw new Error('candidate workspace was not shown for review');
|
if (!candidateView.container.textContent.includes('Deal: Project')) throw new Error('candidate Deal was not shown for review');
|
||||||
if (!candidateView.container.textContent.includes('Estimated duration: 51 min')) throw new Error('candidate duration was not shown for review');
|
if (!candidateView.container.textContent.includes('Estimated duration: 51 min')) throw new Error('candidate duration was not shown for review');
|
||||||
|
if (candidateView.container.textContent.includes('browser.capture.selection') || candidateView.container.textContent.includes('verstak.browser-inbox') || candidateView.container.textContent.includes('capture-1')) {
|
||||||
|
throw new Error('candidate review exposed technical Activity identifiers');
|
||||||
|
}
|
||||||
if (byData(candidateView.container, 'data-journal-input', 'title').value !== '') throw new Error('candidate review must start with an empty title');
|
if (byData(candidateView.container, 'data-journal-input', 'title').value !== '') throw new Error('candidate review must start with an empty title');
|
||||||
if (byData(candidateView.container, 'data-journal-input', 'summary').value !== '') throw new Error('candidate review must start with an empty body');
|
if (byData(candidateView.container, 'data-journal-input', 'summary').value !== '') throw new Error('candidate review must start with an empty body');
|
||||||
if (byData(candidateView.container, 'data-journal-input', 'minutes').value !== '51') throw new Error('candidate review must prefill the factual duration');
|
if (byData(candidateView.container, 'data-journal-input', 'minutes').value !== '51') throw new Error('candidate review must prefill the factual duration');
|
||||||
@@ -273,6 +283,15 @@ function byData(container, attr, value) {
|
|||||||
throw new Error('journal rows must not navigate to Activity by default');
|
throw new Error('journal rows must not navigate to Activity by default');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const russianCandidateView = await mountWithApi(makeApi({}, russianLocale), {
|
||||||
|
workspaceNode: { name: 'Project' },
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
toolRequest: { type: 'work-session-candidate', candidate },
|
||||||
|
});
|
||||||
|
if (!russianCandidateView.container.textContent.includes('Дело: Project') || !russianCandidateView.container.textContent.includes('Захвачено выделение')) {
|
||||||
|
throw new Error('candidate review was not localized');
|
||||||
|
}
|
||||||
|
|
||||||
byData(candidateView.container, 'data-journal-action', 'delete').click();
|
byData(candidateView.container, 'data-journal-action', 'delete').click();
|
||||||
await flush();
|
await flush();
|
||||||
if (api.storedEntries(projectKey).length !== 1) throw new Error('journal entry was not deleted');
|
if (api.storedEntries(projectKey).length !== 1) throw new Error('journal entry was not deleted');
|
||||||
@@ -350,6 +369,7 @@ function byData(container, attr, value) {
|
|||||||
|
|
||||||
component.unmount && component.unmount(container);
|
component.unmount && component.unmount(container);
|
||||||
component.unmount && component.unmount(candidateView.container);
|
component.unmount && component.unmount(candidateView.container);
|
||||||
|
component.unmount && component.unmount(russianCandidateView.container);
|
||||||
component.unmount && component.unmount(todoView.container);
|
component.unmount && component.unmount(todoView.container);
|
||||||
component.unmount && component.unmount(duplicateTodoView.container);
|
component.unmount && component.unmount(duplicateTodoView.container);
|
||||||
component.unmount && component.unmount(globalView.container);
|
component.unmount && component.unmount(globalView.container);
|
||||||
|
|||||||
@@ -264,12 +264,19 @@ async function mountNotes(api) {
|
|||||||
const createButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-action') === 'create');
|
const createButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-action') === 'create');
|
||||||
if (!createButton) throw new Error('create button not found');
|
if (!createButton) throw new Error('create button not found');
|
||||||
createButton.click();
|
createButton.click();
|
||||||
const input = walk(container, (node) => node.getAttribute && node.getAttribute('data-notes-create-input') !== undefined);
|
const createModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-notes-create-modal') !== undefined);
|
||||||
|
if (!createModal) throw new Error('create modal not found');
|
||||||
|
const input = walk(createModal, (node) => node.getAttribute && node.getAttribute('data-notes-create-input') !== undefined);
|
||||||
if (!input) throw new Error('create input not found');
|
if (!input) throw new Error('create input not found');
|
||||||
input.value = 'First Note';
|
const confirm = walk(createModal, (node) => node.tagName === 'BUTTON' && node.textContent === 'Create');
|
||||||
const confirm = walk(container, (node) => node.tagName === 'BUTTON' && node.textContent === 'Create');
|
|
||||||
if (!confirm) throw new Error('create confirm button not found');
|
if (!confirm) throw new Error('create confirm button not found');
|
||||||
confirm.click();
|
confirm.click();
|
||||||
|
const createError = walk(createModal, (node) => node.getAttribute && node.getAttribute('data-notes-create-error') !== undefined);
|
||||||
|
if (!createError || !createError.textContent.includes('Enter a note title') || input.getAttribute('aria-invalid') !== 'true') {
|
||||||
|
throw new Error('create modal does not show a validation error for an empty title');
|
||||||
|
}
|
||||||
|
input.value = 'First Note';
|
||||||
|
input.dispatchEvent('keydown', { key: 'Enter' });
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
const created = createApi.entries.get('Project/Notes/First_Note.md');
|
const created = createApi.entries.get('Project/Notes/First_Note.md');
|
||||||
@@ -319,21 +326,25 @@ async function mountNotes(api) {
|
|||||||
const renameButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-action') === 'rename');
|
const renameButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-action') === 'rename');
|
||||||
if (!renameButton) throw new Error('rename note button not found');
|
if (!renameButton) throw new Error('rename note button not found');
|
||||||
renameButton.click();
|
renameButton.click();
|
||||||
const renameInput = walk(container, (node) => node.getAttribute && node.getAttribute('data-notes-rename-input') !== undefined);
|
const renameModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-notes-rename-modal') !== undefined);
|
||||||
|
if (!renameModal) throw new Error('rename modal not found');
|
||||||
|
const renameInput = walk(renameModal, (node) => node.getAttribute && node.getAttribute('data-notes-rename-input') !== undefined);
|
||||||
if (!renameInput) throw new Error('rename input not found');
|
if (!renameInput) throw new Error('rename input not found');
|
||||||
renameInput.value = 'Second Note';
|
renameInput.value = 'Second Note';
|
||||||
const renameConfirm = walk(container, (node) => node.tagName === 'BUTTON' && node.textContent === 'Rename');
|
renameInput.dispatchEvent('keydown', { key: 'Enter' });
|
||||||
if (!renameConfirm) throw new Error('rename confirm button not found');
|
|
||||||
renameConfirm.click();
|
|
||||||
await flush();
|
await flush();
|
||||||
const conflictModal = walk(document.body, (node) => node.className === 'notes-modal-msg');
|
const renameError = walk(renameModal, (node) => node.getAttribute && node.getAttribute('data-notes-rename-error') !== undefined);
|
||||||
if (!conflictModal || !conflictModal.textContent.includes('Project/Notes/Second_Note.md')) {
|
if (!renameError || !renameError.textContent.includes('Project/Notes/Second_Note.md')) {
|
||||||
throw new Error(`rename conflict modal should include existing path, got ${conflictModal && conflictModal.textContent}`);
|
throw new Error(`rename conflict should stay in the form modal, got ${renameError && renameError.textContent}`);
|
||||||
}
|
}
|
||||||
const conflictOk = walk(document.body, (node) => node.tagName === 'BUTTON' && node.textContent === 'OK');
|
if (walk(document.body, (node) => node.className === 'notes-modal-msg')) {
|
||||||
if (!conflictOk) throw new Error('rename conflict OK button not found');
|
throw new Error('rename conflict opened a second modal instead of reporting in the form');
|
||||||
conflictOk.click();
|
}
|
||||||
|
renameInput.dispatchEvent('keydown', { key: 'Escape' });
|
||||||
await flush();
|
await flush();
|
||||||
|
if (walk(document.body, (node) => node.getAttribute && node.getAttribute('data-notes-rename-modal') !== undefined)) {
|
||||||
|
throw new Error('Escape did not close the rename modal');
|
||||||
|
}
|
||||||
|
|
||||||
const providerAction = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-contribution-action') === 'provider.note.action');
|
const providerAction = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-contribution-action') === 'provider.note.action');
|
||||||
if (!providerAction) throw new Error('provider note action button not found');
|
if (!providerAction) throw new Error('provider note action button not found');
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ class FakeNode {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeChild(node) {
|
||||||
|
this.children = this.children.filter((child) => child !== node);
|
||||||
|
node.parentNode = null;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
if (this.parentNode) this.parentNode.removeChild(this);
|
||||||
|
}
|
||||||
|
|
||||||
setAttribute(name, value) {
|
setAttribute(name, value) {
|
||||||
this.attributes[name] = String(value);
|
this.attributes[name] = String(value);
|
||||||
}
|
}
|
||||||
@@ -235,7 +245,17 @@ async function flush() {
|
|||||||
if (!container.textContent.includes('Group')) throw new Error('secret field table missing Group row');
|
if (!container.textContent.includes('Group')) throw new Error('secret field table missing Group row');
|
||||||
if (!container.textContent.includes('Username')) throw new Error('secret field table missing Username row');
|
if (!container.textContent.includes('Username')) throw new Error('secret field table missing Username row');
|
||||||
if (!container.textContent.includes('Password')) throw new Error('secret field table missing Password row');
|
if (!container.textContent.includes('Password')) throw new Error('secret field table missing Password row');
|
||||||
if (!container.textContent.includes('secret-value')) throw new Error('secret value was not shown in the field table');
|
if (container.textContent.includes('secret-value')) throw new Error('secret value must stay hidden until the user explicitly reveals it');
|
||||||
|
const copyValueButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-copy-value') === 'client-a.db');
|
||||||
|
if (!copyValueButton) throw new Error('copy value button missing');
|
||||||
|
copyValueButton.click();
|
||||||
|
await flush();
|
||||||
|
if (!copied.includes('secret-value')) throw new Error('secret value was not copied');
|
||||||
|
const showValueButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-toggle-value') === 'client-a.db');
|
||||||
|
if (!showValueButton) throw new Error('show value button missing');
|
||||||
|
showValueButton.click();
|
||||||
|
await flush();
|
||||||
|
if (!container.textContent.includes('secret-value')) throw new Error('secret value was not revealed after explicit request');
|
||||||
|
|
||||||
const copyButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-copy-link') === 'client-a.db');
|
const copyButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-secret-copy-link') === 'client-a.db');
|
||||||
if (!copyButton) throw new Error('copy link button missing');
|
if (!copyButton) throw new Error('copy link button missing');
|
||||||
@@ -261,6 +281,20 @@ async function flush() {
|
|||||||
if (!deleteButton) throw new Error('delete button missing');
|
if (!deleteButton) throw new Error('delete button missing');
|
||||||
deleteButton.click();
|
deleteButton.click();
|
||||||
await flush();
|
await flush();
|
||||||
|
if (deleted.includes('client-a.db')) throw new Error('secret delete must wait for confirmation');
|
||||||
|
const deleteConfirmModal = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-secret-delete-modal') === '');
|
||||||
|
if (!deleteConfirmModal) throw new Error('secret delete confirmation modal missing');
|
||||||
|
const deleteCancelButton = walk(deleteConfirmModal, (node) => node.getAttribute && node.getAttribute('data-secret-delete-cancel') === '');
|
||||||
|
if (!deleteCancelButton) throw new Error('secret delete cancel button missing');
|
||||||
|
deleteCancelButton.click();
|
||||||
|
await flush();
|
||||||
|
if (deleted.includes('client-a.db')) throw new Error('secret delete ran after cancelling the confirmation');
|
||||||
|
deleteButton.click();
|
||||||
|
await flush();
|
||||||
|
const deleteConfirmButton = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-secret-delete-confirm') === '');
|
||||||
|
if (!deleteConfirmButton) throw new Error('secret delete confirm button missing');
|
||||||
|
deleteConfirmButton.click();
|
||||||
|
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' });
|
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' });
|
||||||
|
|||||||
@@ -27,5 +27,14 @@ if (!source.includes('conflictDetails')) {
|
|||||||
if (!source.includes('Sync conflicts')) {
|
if (!source.includes('Sync conflicts')) {
|
||||||
throw new Error('SyncSettings must label the conflict details section');
|
throw new Error('SyncSettings must label the conflict details section');
|
||||||
}
|
}
|
||||||
|
if (!source.includes('syncAPI().configure(serverUrl, username, password, vaultId)')) {
|
||||||
|
throw new Error('SyncSettings must pass the optional remote vault ID during pairing');
|
||||||
|
}
|
||||||
|
if (!source.includes('settings.lastWarning')) {
|
||||||
|
throw new Error('SyncSettings must render unresolved scanner warnings');
|
||||||
|
}
|
||||||
|
if (!source.includes("tr('ui.remoteVaultHint'")) {
|
||||||
|
throw new Error('SyncSettings must explain remote vault restoration');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('sync plugin smoke passed');
|
console.log('sync plugin smoke passed');
|
||||||
|
|||||||
@@ -261,6 +261,13 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
|
|||||||
if (!container.textContent.includes('Задачи · Project') || !container.textContent.includes('Просрочено')) {
|
if (!container.textContent.includes('Задачи · Project') || !container.textContent.includes('Просрочено')) {
|
||||||
throw new Error('Todo view did not update to Russian without remounting');
|
throw new Error('Todo view did not update to Russian without remounting');
|
||||||
}
|
}
|
||||||
|
byData(container, 'data-todo-action', 'add').click();
|
||||||
|
if (!container.textContent.includes('Дело: Project')) {
|
||||||
|
throw new Error('Todo editor did not use Deal terminology in Russian');
|
||||||
|
}
|
||||||
|
const cancelModal = walk(container, (node) => node.tagName === 'BUTTON' && node.textContent === 'Отмена' && !node.getAttribute('data-todo-action'));
|
||||||
|
if (!cancelModal) throw new Error('Todo editor cancel action was not rendered in Russian');
|
||||||
|
cancelModal.click();
|
||||||
if ((apiState.settings['todos:global'] || []).length !== 1) throw new Error('locale change lost Todo state');
|
if ((apiState.settings['todos:global'] || []).length !== 1) throw new Error('locale change lost Todo state');
|
||||||
apiState.setLocale('en');
|
apiState.setLocale('en');
|
||||||
|
|
||||||
@@ -322,6 +329,9 @@ async function mountWithApi(apiState, props, emittedEvents = [], document = make
|
|||||||
if (!globalView.container.textContent.includes('Prepare project review updated') || !globalView.container.textContent.includes('Client follow-up')) {
|
if (!globalView.container.textContent.includes('Prepare project review updated') || !globalView.container.textContent.includes('Client follow-up')) {
|
||||||
throw new Error('global Todo view did not aggregate workspace todos');
|
throw new Error('global Todo view did not aggregate workspace todos');
|
||||||
}
|
}
|
||||||
|
if (!globalView.container.textContent.includes('All Deals')) {
|
||||||
|
throw new Error('global Todo filter did not use Deal terminology');
|
||||||
|
}
|
||||||
const workspaceFilter = byData(globalView.container, 'data-todo-filter', 'workspace');
|
const workspaceFilter = byData(globalView.container, 'data-todo-filter', 'workspace');
|
||||||
workspaceFilter.value = 'ClientA';
|
workspaceFilter.value = 'ClientA';
|
||||||
workspaceFilter.dispatchEvent('change');
|
workspaceFilter.dispatchEvent('change');
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ const vm = require('vm');
|
|||||||
const root = path.resolve(__dirname, '..');
|
const root = path.resolve(__dirname, '..');
|
||||||
const sourcePath = path.join(root, 'plugins', 'trash', 'frontend', 'src', 'index.js');
|
const sourcePath = path.join(root, 'plugins', 'trash', 'frontend', 'src', 'index.js');
|
||||||
const manifestPath = path.join(root, 'plugins', 'trash', 'plugin.json');
|
const manifestPath = path.join(root, 'plugins', 'trash', 'plugin.json');
|
||||||
|
const russianLocalePath = path.join(root, 'plugins', 'trash', 'locales', 'ru.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 russianLocale = JSON.parse(fs.readFileSync(russianLocalePath, 'utf8'));
|
||||||
|
|
||||||
class FakeClassList {
|
class FakeClassList {
|
||||||
constructor(node) {
|
constructor(node) {
|
||||||
@@ -159,7 +161,7 @@ function loadTrashComponent(document) {
|
|||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeApi() {
|
function makeApi(locale = null) {
|
||||||
let entries = [
|
let entries = [
|
||||||
{
|
{
|
||||||
trashId: 'old-project-file',
|
trashId: 'old-project-file',
|
||||||
@@ -194,6 +196,11 @@ function makeApi() {
|
|||||||
return {
|
return {
|
||||||
restoreCalls,
|
restoreCalls,
|
||||||
deleteCalls,
|
deleteCalls,
|
||||||
|
i18n: locale ? {
|
||||||
|
t(key, params, fallback) {
|
||||||
|
return String(locale[key] || fallback || key).replace(/\{(\w+)\}/g, (_match, name) => String((params || {})[name] ?? ''));
|
||||||
|
},
|
||||||
|
} : null,
|
||||||
files: {
|
files: {
|
||||||
listTrash: async () => entries.slice(),
|
listTrash: async () => entries.slice(),
|
||||||
restoreTrash: async (trashId, options) => {
|
restoreTrash: async (trashId, options) => {
|
||||||
@@ -237,6 +244,10 @@ function findByData(rootNode, name, value) {
|
|||||||
component.mount(container, {}, api);
|
component.mount(container, {}, api);
|
||||||
await flush();
|
await flush();
|
||||||
|
|
||||||
|
if (!container.textContent.includes('All Deals') || !container.textContent.includes('Deal')) {
|
||||||
|
throw new Error('global Trash did not use Deal terminology');
|
||||||
|
}
|
||||||
|
|
||||||
const latestRow = findByData(container, 'data-trash-row', 'latest-client-report');
|
const latestRow = findByData(container, 'data-trash-row', 'latest-client-report');
|
||||||
if (!latestRow || !latestRow.textContent.includes('ClientA') || !latestRow.textContent.includes('ClientA/Archive/report.pdf')) {
|
if (!latestRow || !latestRow.textContent.includes('ClientA') || !latestRow.textContent.includes('ClientA/Archive/report.pdf')) {
|
||||||
throw new Error(`global Trash must render workspace and original path: ${container.textContent}`);
|
throw new Error(`global Trash must render workspace and original path: ${container.textContent}`);
|
||||||
@@ -298,6 +309,14 @@ function findByData(rootNode, name, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component.unmount(container);
|
component.unmount(container);
|
||||||
|
|
||||||
|
const russianContainer = new FakeNode('div');
|
||||||
|
component.mount(russianContainer, {}, makeApi(russianLocale));
|
||||||
|
await flush();
|
||||||
|
if (!russianContainer.textContent.includes('Все Дела') || !russianContainer.textContent.includes('Дело')) {
|
||||||
|
throw new Error('global Trash did not localize Deal terminology');
|
||||||
|
}
|
||||||
|
component.unmount(russianContainer);
|
||||||
console.log('trash frontend smoke passed');
|
console.log('trash frontend smoke passed');
|
||||||
})().catch((err) => {
|
})().catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ if [[ ! -x "$PUBLISHER" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$WORK/bin" "$WORK/release" "$WORK/state"
|
mkdir -p "$WORK/bin" "$WORK/release" "$WORK/release-notes" "$WORK/state"
|
||||||
LOG="$WORK/log"
|
LOG="$WORK/log"
|
||||||
export LOG
|
export LOG
|
||||||
|
printf '## Highlights\n\nHuman-readable release notes.\n' > "$WORK/release-notes/$VERSION.md"
|
||||||
|
|
||||||
cat > "$WORK/release.sh" <<'SCRIPT'
|
cat > "$WORK/release.sh" <<'SCRIPT'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -49,6 +50,7 @@ case "${1:-}" in
|
|||||||
fi
|
fi
|
||||||
echo test-commit
|
echo test-commit
|
||||||
;;
|
;;
|
||||||
|
describe) echo v0.0.0-previous ;;
|
||||||
tag) touch "$TEST_STATE/tag"; printf 'tag:%s\n' "${3:-}" >> "$LOG" ;;
|
tag) touch "$TEST_STATE/tag"; printf 'tag:%s\n' "${3:-}" >> "$LOG" ;;
|
||||||
push) printf 'push:%s:%s\n' "${2:-}" "${3:-}" >> "$LOG" ;;
|
push) printf 'push:%s:%s\n' "${2:-}" "${3:-}" >> "$LOG" ;;
|
||||||
*) echo "unexpected git invocation: $*" >&2; exit 1 ;;
|
*) echo "unexpected git invocation: $*" >&2; exit 1 ;;
|
||||||
@@ -74,6 +76,7 @@ chmod +x "$WORK/bin/gh"
|
|||||||
run_publisher() {
|
run_publisher() {
|
||||||
VERSTAK_RELEASE_SCRIPT="$WORK/release.sh" \
|
VERSTAK_RELEASE_SCRIPT="$WORK/release.sh" \
|
||||||
VERSTAK_RELEASE_DIR="$WORK/release" \
|
VERSTAK_RELEASE_DIR="$WORK/release" \
|
||||||
|
VERSTAK_RELEASE_NOTES_DIR="$WORK/release-notes" \
|
||||||
GIT_BIN="$WORK/bin/git" \
|
GIT_BIN="$WORK/bin/git" \
|
||||||
GH_BIN="$WORK/bin/gh" \
|
GH_BIN="$WORK/bin/gh" \
|
||||||
EXPECTED_ROOT="$ROOT" \
|
EXPECTED_ROOT="$ROOT" \
|
||||||
@@ -90,6 +93,9 @@ for asset in "${ASSET_NAMES[@]}"; do
|
|||||||
grep -F "$asset" "$LOG" >/dev/null
|
grep -F "$asset" "$LOG" >/dev/null
|
||||||
done
|
done
|
||||||
grep -F "SHA256SUMS" "$LOG" >/dev/null
|
grep -F "SHA256SUMS" "$LOG" >/dev/null
|
||||||
|
grep -F -- "--notes-file $WORK/release-notes/$VERSION.md" "$LOG" >/dev/null
|
||||||
|
grep -F -- "--generate-notes" "$LOG" >/dev/null
|
||||||
|
grep -F -- "--notes-start-tag v0.0.0-previous" "$LOG" >/dev/null
|
||||||
grep -F -- "--prerelease" "$LOG" >/dev/null
|
grep -F -- "--prerelease" "$LOG" >/dev/null
|
||||||
|
|
||||||
run_publisher
|
run_publisher
|
||||||
|
|||||||
Reference in New Issue
Block a user