Compare commits
No commits in common. "main" and "v0.1.0-alpha.2" have entirely different histories.
main
...
v0.1.0-alp
32
README.md
|
|
@ -7,7 +7,7 @@
|
||||||
### Keep the context of your work together — locally.
|
### Keep the context of your work together — locally.
|
||||||
|
|
||||||
Files, notes, links, browser captures, activity and work history
|
Files, notes, links, browser captures, activity and work history
|
||||||
in one extensible local-first workbench.
|
in one extensible local-first workspace.
|
||||||
|
|
||||||
**English** · [Русский](README.ru.md)
|
**English** · [Русский](README.ru.md)
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ in one extensible local-first workbench.
|
||||||
|
|
||||||
Verstak is a local-first workbench that keeps the context around your work in one place.
|
Verstak is a local-first workbench that keeps the context around your work in one place.
|
||||||
|
|
||||||
A **Deal** can represent almost anything:
|
A **workspace** can represent almost anything:
|
||||||
|
|
||||||
* a software project;
|
* a software project;
|
||||||
* a client or customer;
|
* a client or customer;
|
||||||
|
|
@ -47,29 +47,29 @@ No account, cloud service or sync server is required for local use.
|
||||||
|
|
||||||
## Verstak in use
|
## Verstak in use
|
||||||
|
|
||||||
| Overview: return to a Deal and see its recent work, captures and items that need attention. | Files: manage ordinary folders and documents inside a Deal. |
|
| Overview: return to a workspace and see its recent work, captures and items that need attention. | Files: manage ordinary folders and documents inside a workspace. |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
| Notes: keep Markdown notes next to the Deal they describe. | Journal: record a completed session and retain its context. |
|
| Notes: keep Markdown notes next to the workspace they describe. | Journal: record a completed session and retain its context. |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
## Main features
|
## Main features
|
||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
| ------------------------ | -------------------------------------------------------------------------------------------- |
|
| ------------------------ | -------------------------------------------------------------------------------------------- |
|
||||||
| **Deals** | Organize files, notes and activity around durable projects, clients and other areas of work. |
|
| **Workspaces** | Organize files, notes and activity around durable projects, clients and other areas of work. |
|
||||||
| **Files** | Browse and manage ordinary files stored inside your vault. |
|
| **Files** | Browse and manage ordinary files stored inside your vault. |
|
||||||
| **Notes** | Create Markdown notes, overview pages and links between related information. |
|
| **Notes** | Create Markdown notes, overview pages and links between related information. |
|
||||||
| **Overview** | Quickly return to recent work and see what may need attention. |
|
| **Overview** | Quickly return to recent work and see what may need attention. |
|
||||||
| **Browser Inbox** | Send pages, links, selections and files from a browser into Verstak. |
|
| **Browser Inbox** | Send pages, links, selections and files from a browser into Verstak. |
|
||||||
| **Activity and Journal** | Reconstruct work sessions and turn selected activity into journal entries. |
|
| **Activity and Journal** | Reconstruct work sessions and turn selected activity into journal entries. |
|
||||||
| **Todo** | Keep optional task lists inside individual Deals and across the whole vault. |
|
| **Todo** | Keep optional task lists inside individual workspaces and across the whole vault. |
|
||||||
| **Search** | Search across notes, files and supported plugin data. |
|
| **Search** | Search across notes, files and supported plugin data. |
|
||||||
| **Trash** | Restore deleted items or remove them permanently from one central location. |
|
| **Trash** | Restore deleted items or remove them permanently from one central location. |
|
||||||
| **Secrets** | Keep credentials and access information connected to the relevant Deal. |
|
| **Secrets** | Keep credentials and access information connected to the relevant workspace. |
|
||||||
| **Templates** | Create repeatable Deal structures for common types of work. |
|
| **Templates** | Create repeatable workspace structures for common types of work. |
|
||||||
| **Plugins** | Add, replace or disable tools without turning Verstak into one rigid monolithic application. |
|
| **Plugins** | Add, replace or disable tools without turning Verstak into one rigid monolithic application. |
|
||||||
| **Optional sync** | Synchronize vaults between devices using a self-hosted Verstak Sync Server. |
|
| **Optional sync** | Synchronize vaults between devices using a self-hosted Verstak Sync Server. |
|
||||||
|
|
||||||
|
|
@ -151,9 +151,9 @@ Documents/
|
||||||
|
|
||||||
Your files and notes remain ordinary files that can be accessed without Verstak.
|
Your files and notes remain ordinary files that can be accessed without Verstak.
|
||||||
|
|
||||||
### 2. Create your first Deal
|
### 2. Create your first workspace
|
||||||
|
|
||||||
Inside the vault, create a Deal for a project, client, server, device or another area of work.
|
Inside the vault, create a workspace for a project, client, server, device or another area of work.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
|
@ -167,7 +167,7 @@ Training course
|
||||||
|
|
||||||
### 3. Add context
|
### 3. Add context
|
||||||
|
|
||||||
Open the Deal and add the information needed to return to it later:
|
Open the workspace and add the information needed to return to it later:
|
||||||
|
|
||||||
* notes and decisions;
|
* notes and decisions;
|
||||||
* documents and source files;
|
* documents and source files;
|
||||||
|
|
@ -245,15 +245,15 @@ for distribution-specific dependencies.
|
||||||
The desktop, SDK and official plugin repositories must be cloned as sibling directories:
|
The desktop, SDK and official plugin repositories must be cloned as sibling directories:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
verstak-source/
|
verstak-workspace/
|
||||||
├── verstak/
|
├── verstak/
|
||||||
├── verstak-sdk/
|
├── verstak-sdk/
|
||||||
└── verstak-official-plugins/
|
└── verstak-official-plugins/
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir verstak-source
|
mkdir verstak-workspace
|
||||||
cd verstak-source
|
cd verstak-workspace
|
||||||
|
|
||||||
git clone https://github.com/mirivlad/verstak.git
|
git clone https://github.com/mirivlad/verstak.git
|
||||||
git clone https://github.com/mirivlad/verstak-sdk.git
|
git clone https://github.com/mirivlad/verstak-sdk.git
|
||||||
|
|
|
||||||
14
README.ru.md
|
|
@ -7,7 +7,7 @@
|
||||||
### Рабочий контекст остаётся рядом — и хранится локально.
|
### Рабочий контекст остаётся рядом — и хранится локально.
|
||||||
|
|
||||||
Файлы, заметки, ссылки, материалы из браузера, активность и история работы
|
Файлы, заметки, ссылки, материалы из браузера, активность и история работы
|
||||||
в одной расширяемой локальной среде.
|
в одном расширяемом рабочем пространстве.
|
||||||
|
|
||||||
[English](README.md) · **Русский**
|
[English](README.md) · **Русский**
|
||||||
|
|
||||||
|
|
@ -49,11 +49,11 @@
|
||||||
|
|
||||||
| Обзор: возвращает к делу и показывает недавнюю работу, входящие и то, что требует внимания. | Файлы: обычные папки и документы прямо внутри дела. |
|
| Обзор: возвращает к делу и показывает недавнюю работу, входящие и то, что требует внимания. | Файлы: обычные папки и документы прямо внутри дела. |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
| Заметки: Markdown-заметки остаются рядом с делом, которое они описывают. | Журнал: фиксирует завершённую работу и её контекст. |
|
| Заметки: Markdown-заметки остаются рядом с делом, которое они описывают. | Журнал: фиксирует завершённую работу и её контекст. |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
## Основные возможности
|
## Основные возможности
|
||||||
|
|
||||||
|
|
@ -124,7 +124,7 @@ APPIMAGE_EXTRACT_AND_RUN=1 ./verstak-linux-x86_64-*.AppImage
|
||||||
|
|
||||||
## Фоновая работа и трей
|
## Фоновая работа и трей
|
||||||
|
|
||||||
Закрытие главного окна не завершает Верстак: приложение остаётся в системном трее. Выберите **«Показать Верстак»**, чтобы вернуть окно, или **«Выйти»**, чтобы полностью завершить приложение. Благодаря этому напоминания Todo продолжают работать, пока окно скрыто.
|
Закрытие главного окна не завершает Верстак: приложение остаётся в системном трее. Выберите **Show Verstak**, чтобы вернуть окно, или **Quit**, чтобы полностью завершить приложение. Благодаря этому напоминания Todo продолжают работать, пока окно скрыто.
|
||||||
|
|
||||||
### Проверка скачанного файла
|
### Проверка скачанного файла
|
||||||
|
|
||||||
|
|
@ -244,15 +244,15 @@ sha256sum -c SHA256SUMS --ignore-missing
|
||||||
Desktop, SDK и официальные плагины должны находиться в соседних каталогах:
|
Desktop, SDK и официальные плагины должны находиться в соседних каталогах:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
verstak-source/
|
verstak-workspace/
|
||||||
├── verstak/
|
├── verstak/
|
||||||
├── verstak-sdk/
|
├── verstak-sdk/
|
||||||
└── verstak-official-plugins/
|
└── verstak-official-plugins/
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir verstak-source
|
mkdir verstak-workspace
|
||||||
cd verstak-source
|
cd verstak-workspace
|
||||||
|
|
||||||
git clone https://github.com/mirivlad/verstak.git
|
git clone https://github.com/mirivlad/verstak.git
|
||||||
git clone https://github.com/mirivlad/verstak-sdk.git
|
git clone https://github.com/mirivlad/verstak-sdk.git
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,7 @@ contributions summary.
|
||||||
Conflicts are rejected unless `options.overwrite` is true.
|
Conflicts are rejected unless `options.overwrite` is true.
|
||||||
- `files.deleteTrash(trashId)` — permanently removes a file/folder already in
|
- `files.deleteTrash(trashId)` — permanently removes a file/folder already in
|
||||||
internal trash. This operation cannot be undone.
|
internal trash. This operation cannot be undone.
|
||||||
- UI policy: the Deal `Files` plugin displays only live files and folders.
|
- UI policy: the workspace `Files` plugin displays only live files and folders.
|
||||||
The global official `verstak.trash` plugin owns listing, restoring, filtering,
|
The global official `verstak.trash` plugin owns listing, restoring, filtering,
|
||||||
and permanently deleting deleted items across the vault.
|
and permanently deleting deleted items across the vault.
|
||||||
- `files.openExternal(relativePath)` — opens a vault-relative file/folder in
|
- `files.openExternal(relativePath)` — opens a vault-relative file/folder in
|
||||||
|
|
@ -635,7 +635,7 @@ bundled runtime. Это реальный runtime contract для cooperative bun
|
||||||
|
|
||||||
1. Unregister contributions всех plugins предыдущего discovery.
|
1. Unregister contributions всех plugins предыдущего discovery.
|
||||||
2. Unregister all non-core capabilities.
|
2. Unregister all non-core capabilities.
|
||||||
3. Re-register core capabilities + vault + Deal manager (если открыт).
|
3. Re-register core capabilities + vault + workspace (если открыт).
|
||||||
4. Re-scan discovery directories и повторить capability resolution.
|
4. Re-scan discovery directories и повторить capability resolution.
|
||||||
5. Register contributions для loaded/degraded plugins (disabled/failed — не регистрируются).
|
5. Register contributions для loaded/degraded plugins (disabled/failed — не регистрируются).
|
||||||
6. Update plugins list.
|
6. Update plugins list.
|
||||||
|
|
@ -838,52 +838,51 @@ Vault plugin state хранится **внутри vault** в `.verstak/plugins.
|
||||||
- `./scripts/smoke-platform.sh` — ✅ (enable/disable/plugins.json)
|
- `./scripts/smoke-platform.sh` — ✅ (enable/disable/plugins.json)
|
||||||
- `./scripts/build.sh` — ✅
|
- `./scripts/build.sh` — ✅
|
||||||
|
|
||||||
## Дело: core capability
|
## Workspace Core Capability
|
||||||
|
|
||||||
Дело — это физическая папка верхнего уровня внутри vault root. Filesystem
|
Workspace — это физическая папка верхнего уровня внутри vault root. Filesystem
|
||||||
является source of truth для списка Дел.
|
является source of truth для списка workspaces.
|
||||||
|
|
||||||
Пример:
|
Пример:
|
||||||
|
|
||||||
```
|
```
|
||||||
<vault>/
|
<vault>/
|
||||||
Deal/
|
Workspace/
|
||||||
Notes/
|
Notes/
|
||||||
Project/
|
Project/
|
||||||
ClientA/
|
ClientA/
|
||||||
.verstak/
|
.verstak/
|
||||||
```
|
```
|
||||||
|
|
||||||
Нет единого `<vault>/Deal/` контейнера для всех Дел. Папка `Deal/` может быть
|
Нет единого `<vault>/Workspace/` контейнера для всех workspaces. Папка
|
||||||
обычным Делом, но `Project/` и `ClientA/` являются такими же Делами на том же
|
`Workspace/` может быть обычным workspace, но `Project/` и `ClientA/` являются
|
||||||
уровне.
|
такими же workspace на том же уровне.
|
||||||
|
|
||||||
### Хранение
|
### Хранение
|
||||||
|
|
||||||
Существование и список Дел хранятся только в filesystem:
|
Workspace existence/list хранится только в filesystem:
|
||||||
|
|
||||||
- `ListWorkspaces()` читает top-level directories из vault root.
|
- `ListWorkspaces()` читает top-level directories из vault root.
|
||||||
- `.verstak`, reserved/internal directories, top-level files и symlinks не
|
- `.verstak`, reserved/internal directories, top-level files и symlinks не
|
||||||
считаются Делами.
|
считаются workspaces.
|
||||||
- `.verstak/workspace*.json` не является source of truth для списка Дел.
|
- `.verstak/workspace*.json` не является source of truth для списка workspaces.
|
||||||
- Нет virtual workspace tree, которое
|
- Нет persisted workspace path mapping и нет virtual workspace tree, которое
|
||||||
мапится на произвольные папки.
|
мапится на произвольные папки.
|
||||||
|
|
||||||
`.verstak` может хранить только metadata, которая не заменяет filesystem:
|
`.verstak` может хранить только metadata, которая не заменяет filesystem:
|
||||||
|
|
||||||
- UI state: выбранное Дело, expanded folders, sort/pin state, preferences.
|
- UI state: selected workspace, expanded folders, sort/pin state, preferences.
|
||||||
- Semantic snapshot: applied template snapshot, enabled feature areas, exact
|
- Semantic snapshot: applied template snapshot, enabled feature areas, exact
|
||||||
`workspaceTools`, and folder conventions.
|
`workspaceTools`, and folder conventions.
|
||||||
|
|
||||||
При создании Дела runtime генерирует UUID и сохраняет его в
|
Template snapshot копируется в metadata при создании workspace. Workspace
|
||||||
`.verstak/workspace.json`. `workspaceId` — постоянная личность Дела;
|
identity при этом остаётся именем top-level folder; `metadata.workspaceName`
|
||||||
`workspaceRootPath` и `workspaceName` — изменяемый адрес и presentation fields.
|
является presentation/snapshot field, not canonical identity. Если сохранённое
|
||||||
Переименование обновляет путь, но не ID. Новая папка с прежним именем получает
|
значение расходится с именем папки, runtime возвращает canonical `workspaceName`
|
||||||
другой ID и не может перехватить старые привязки.
|
равным имени папки без filesystem side effects.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"workspaceId": "1eb7cc69-52e8-4a18-ae2a-50d5229c5b60",
|
|
||||||
"workspaceName": "Project",
|
"workspaceName": "Project",
|
||||||
"createdFromTemplate": {
|
"createdFromTemplate": {
|
||||||
"templateId": "project",
|
"templateId": "project",
|
||||||
|
|
@ -922,25 +921,25 @@ Vault plugin state хранится **внутри vault** в `.verstak/plugins.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Если original template удалён или изменён позже, существующее Дело
|
Если original template удалён или изменён позже, существующий workspace
|
||||||
открывается по сохранённому snapshot и не мутирует автоматически. Template
|
открывается по сохранённому snapshot и не мутирует автоматически. Template
|
||||||
update/migration может быть только явной future feature. Если metadata
|
update/migration может быть только явной future feature. Если metadata
|
||||||
отсутствует, Дело открывается как generic Deal минимум с `files: true`.
|
отсутствует, workspace открывается как generic workspace минимум с `files: true`.
|
||||||
|
|
||||||
### API
|
### API
|
||||||
|
|
||||||
- `ListWorkspaces()` — список top-level папок Дел с их `workspaceId`.
|
- `ListWorkspaces()` — список top-level physical folders.
|
||||||
- `ListWorkspaceTemplates()` — selectable built-in templates с presentation
|
- `ListWorkspaceTemplates()` — selectable built-in templates с presentation
|
||||||
metadata и `workspaceTools`.
|
metadata и `workspaceTools`.
|
||||||
- `CreateWorkspace(name, templateId?)` — создать `<vault>/<name>/`, применить
|
- `CreateWorkspace(name, templateId?)` — создать `<vault>/<name>/`, применить
|
||||||
template один раз, сохранить snapshot metadata.
|
template один раз, сохранить snapshot metadata.
|
||||||
- `RenameWorkspace(oldName, newName)` — физически переименовать top-level папку
|
- `RenameWorkspace(oldName, newName)` — физически переименовать top-level folder
|
||||||
Дела и обновить metadata name, не меняя `workspaceId`.
|
и обновить metadata key/name.
|
||||||
- `TrashWorkspace(name)` — перенести весь top-level folder Дела в internal
|
- `TrashWorkspace(name)` — перенести весь top-level workspace folder в internal
|
||||||
trash policy.
|
trash policy.
|
||||||
- `GetWorkspaceMetadata(name)` — прочитать metadata или вернуть generic fallback.
|
- `GetWorkspaceMetadata(name)` — прочитать metadata или вернуть generic fallback.
|
||||||
- `UpdateWorkspaceMetadata(name, patch)` — обновить metadata без влияния на
|
- `UpdateWorkspaceMetadata(name, patch)` — обновить metadata без влияния на
|
||||||
существование Дела.
|
существование workspace.
|
||||||
|
|
||||||
Deprecated compatibility APIs:
|
Deprecated compatibility APIs:
|
||||||
|
|
||||||
|
|
@ -950,7 +949,7 @@ Deprecated compatibility APIs:
|
||||||
- `ArchiveWorkspaceNode(...)` — wrapper over `TrashWorkspace`.
|
- `ArchiveWorkspaceNode(...)` — wrapper over `TrashWorkspace`.
|
||||||
- `MoveWorkspaceNode(...)` — unsupported; old nested/mapped moves are rejected.
|
- `MoveWorkspaceNode(...)` — unsupported; old nested/mapped moves are rejected.
|
||||||
- `GetCurrentWorkspaceNode()` / `SetCurrentWorkspaceNode(...)` — wrappers over
|
- `GetCurrentWorkspaceNode()` / `SetCurrentWorkspaceNode(...)` — wrappers over
|
||||||
selected top-level Deal UI state.
|
selected top-level workspace UI state.
|
||||||
|
|
||||||
Эти методы существуют только для постепенного frontend/Wails cleanup. Они не
|
Эти методы существуют только для постепенного frontend/Wails cleanup. Они не
|
||||||
должны создавать или сохранять nested workspace tree и не должны восстанавливать
|
должны создавать или сохранять nested workspace tree и не должны восстанавливать
|
||||||
|
|
@ -958,33 +957,32 @@ Deprecated compatibility APIs:
|
||||||
|
|
||||||
### Capability
|
### Capability
|
||||||
|
|
||||||
`verstak/core/workspace/v1` — техническое имя capability; оно регистрируется
|
`verstak/core/workspace/v1` — регистрируется только когда vault открыт и workspace инициализирован.
|
||||||
только когда vault открыт и Deal manager инициализирован.
|
|
||||||
|
|
||||||
### Правила
|
### Правила
|
||||||
|
|
||||||
- Имя Дела — один safe folder name, не path.
|
- Workspace name — один safe folder name, не path.
|
||||||
- Reject: empty, slash, backslash, absolute-looking paths, `..`, null byte,
|
- Reject: empty, slash, backslash, absolute-looking paths, `..`, null byte,
|
||||||
`.verstak`, reserved/internal names, symlink Дел, conflicts.
|
`.verstak`, reserved/internal names, symlink workspaces, conflicts.
|
||||||
- WorkspaceItems получают техническое поле `workspaceRootPath`, равное имени top-level папки
|
- WorkspaceItems получают `workspaceRootPath`, равный имени top-level папки
|
||||||
(`Project`, `ClientA`, etc). Files plugin показывает именно эту папку.
|
(`Project`, `ClientA`, etc). Files plugin показывает именно эту папку.
|
||||||
- Files API остаётся raw vault-relative API: `Project/Notes/example.md`,
|
- Files API остаётся raw vault-relative API: `Project/Notes/example.md`,
|
||||||
`Project/docs/file.md`, `Test/readme.md`.
|
`Project/docs/file.md`, `Test/readme.md`.
|
||||||
- Notes are ordinary Markdown files under `<Deal>/Notes/`; нет
|
- Notes are ordinary Markdown files under `<workspace>/Notes/`; нет
|
||||||
`.verstak/notes`, UUID note storage или второго source of truth для note
|
`.verstak/notes`, UUID note storage или второго source of truth для note
|
||||||
content.
|
content.
|
||||||
|
|
||||||
### Lifecycle Events
|
### Lifecycle Events
|
||||||
|
|
||||||
Runtime publishes lifecycle events Дел после успешных операций:
|
Runtime publishes workspace lifecycle events after successful operations:
|
||||||
|
|
||||||
- `workspace.created` — payload includes `operation: "create"`, `workspaceId`,
|
- `workspace.created` — payload includes `operation: "create"`,
|
||||||
`workspaceRootPath`, `workspaceName`, and optional `templateId`.
|
`workspaceRootPath`, `workspaceName`, and optional `templateId`.
|
||||||
- `workspace.renamed` — payload includes `operation: "rename"`, `workspaceId`,
|
- `workspace.renamed` — payload includes `operation: "rename"`, new
|
||||||
new `workspaceRootPath` / `workspaceName`, and previous path fields.
|
`workspaceRootPath` / `workspaceName`, and previous workspace fields.
|
||||||
- `workspace.trashed` — payload includes `operation: "trash"`, `workspaceId`,
|
- `workspace.trashed` — payload includes `operation: "trash"`,
|
||||||
`workspaceRootPath`, `workspaceName`, `trashId`, `trashPath`, and `deletedAt`.
|
`workspaceRootPath`, `workspaceName`, `trashId`, `trashPath`, and `deletedAt`.
|
||||||
- `workspace.selected` — payload includes `operation: "select"`, `workspaceId`,
|
- `workspace.selected` — payload includes `operation: "select"`,
|
||||||
`workspaceRootPath`, and `workspaceName`.
|
`workspaceRootPath`, and `workspaceName`.
|
||||||
|
|
||||||
Official Activity subscribes to these events and stores them through the normal
|
Official Activity subscribes to these events and stores them through the normal
|
||||||
|
|
@ -992,11 +990,11 @@ activity provider path.
|
||||||
|
|
||||||
### UI
|
### UI
|
||||||
|
|
||||||
Список Дел в sidebar:
|
Workspace list в sidebar:
|
||||||
- Flat list of top-level папок Дел.
|
- Flat list of top-level workspace folders.
|
||||||
- Создать Дело, переименовать Дело, переместить Дело в корзину.
|
- Create workspace, rename workspace, trash workspace.
|
||||||
- Selection is stored as selected Deal name.
|
- Selection is stored as selected workspace name.
|
||||||
- No expand/collapse tree and no case/folder node creation in core.
|
- No expand/collapse workspace tree and no case/folder node creation in core.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
# Deal Templates
|
# Workspace Templates
|
||||||
|
|
||||||
Deal templates choose which dynamic plugin Deal tabs are visible when a new
|
Workspace templates choose which dynamic plugin workspace tabs are visible when a
|
||||||
top-level Deal is created. They do not enable or disable plugins for the
|
new top-level workspace is created. They do not enable or disable plugins for the
|
||||||
whole vault and do not affect global sidebar tools.
|
whole vault and do not affect global sidebar tools.
|
||||||
|
|
||||||
## Built-in templates
|
## Built-in templates
|
||||||
|
|
||||||
| Template | Deal tabs |
|
| Template | Workspace tabs |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| General | Notes, Files, Journal, Activity, Browser Inbox |
|
| General | Notes, Files, Journal, Activity, Browser Inbox |
|
||||||
| Project | Notes, Files, Todos, Journal, Activity, Browser Inbox |
|
| Project | Notes, Files, Todos, Journal, Activity, Browser Inbox |
|
||||||
|
|
@ -14,27 +14,27 @@ whole vault and do not affect global sidebar tools.
|
||||||
| Admin | Notes, Files, Secrets, Todos, Journal |
|
| Admin | Notes, Files, Secrets, Todos, Journal |
|
||||||
| Minimal | Notes, Files |
|
| Minimal | Notes, Files |
|
||||||
|
|
||||||
The create-Deal modal displays the selected template description and its
|
The create-workspace modal displays the selected template description and its
|
||||||
included plugin tabs before the folder is created.
|
included plugin tabs before the folder is created.
|
||||||
|
|
||||||
## Metadata and compatibility
|
## Metadata and compatibility
|
||||||
|
|
||||||
Creation stores a template snapshot in `.verstak/workspaces/` metadata. The
|
Creation stores a template snapshot in `.verstak/workspaces/` metadata. The
|
||||||
snapshot contains the template id, name, version, applied time, and an ordered
|
snapshot contains the template id, name, version, applied time, and an ordered
|
||||||
`workspaceTools` list of plugin IDs. Existing Deal metadata without
|
`workspaceTools` list of plugin IDs. Existing workspace metadata without
|
||||||
`workspaceTools` remains compatible: its Deal continues to show all globally
|
`workspaceTools` remains compatible: its workspace continues to show all globally
|
||||||
enabled Deal plugins rather than unexpectedly hiding tabs.
|
enabled workspace plugins rather than unexpectedly hiding tabs.
|
||||||
|
|
||||||
Templates are applied once. Editing the built-in catalog or creating another
|
Templates are applied once. Editing the built-in catalog or creating another
|
||||||
Deal with a different template never changes an existing Deal snapshot.
|
workspace with a different template never changes an existing workspace snapshot.
|
||||||
There is no template editor or post-creation template switcher yet.
|
There is no template editor or post-creation template switcher yet.
|
||||||
|
|
||||||
## Global tools and unavailable plugins
|
## Global tools and unavailable plugins
|
||||||
|
|
||||||
Template visibility applies only to `workspaceItems` in the selected Deal.
|
Template visibility applies only to `workspaceItems` in the selected workspace.
|
||||||
Global views and sidebar items, such as global Todos, Browser Inbox, and Trash,
|
Global views and sidebar items, such as global Todos, Browser Inbox, and Trash,
|
||||||
remain available according to the normal plugin enablement state.
|
remain available according to the normal plugin enablement state.
|
||||||
|
|
||||||
The Deal host intersects the snapshot with dynamically discovered, globally
|
The workspace host intersects the snapshot with dynamically discovered, globally
|
||||||
enabled plugin contributions. If a template references a plugin that is missing or
|
enabled plugin contributions. If a template references a plugin that is missing or
|
||||||
disabled, that tab is simply absent; the other template tabs remain usable.
|
disabled, that tab is simply absent; the other template tabs remain usable.
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
|
@ -27,38 +27,6 @@ test.describe('Activity workflow', () => {
|
||||||
await expect(activity.locator('[data-activity-action="clear"]')).toBeDisabled();
|
await expect(activity.locator('[data-activity-action="clear"]')).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clearing activity requires destructive confirmation for the current case', async ({ page }) => {
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
await window.go.api.App.WritePluginSettings('verstak.activity', {
|
|
||||||
'events:workspace:Project': [{
|
|
||||||
activityId: 'activity-to-clear',
|
|
||||||
occurredAt: '2026-06-30T08:00:00.000Z',
|
|
||||||
type: 'note.saved',
|
|
||||||
title: 'Keep until confirmed',
|
|
||||||
workspaceRootPath: 'Project',
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
|
||||||
const activity = page.locator('.activity-root');
|
|
||||||
await expect(activity.locator('.activity-count')).toHaveText('1 event');
|
|
||||||
|
|
||||||
await activity.locator('[data-activity-action="clear"]').click();
|
|
||||||
const confirmation = activity.locator('[data-activity-clear-confirmation]');
|
|
||||||
await expect(confirmation).toBeVisible();
|
|
||||||
await expect(confirmation).toContainText(/Project|Дело/);
|
|
||||||
await expect(activity.locator('.activity-count')).toHaveText('1 event');
|
|
||||||
|
|
||||||
await confirmation.locator('[data-activity-clear-cancel]').click();
|
|
||||||
await expect(confirmation).toHaveCount(0);
|
|
||||||
await expect(activity.locator('.activity-count')).toHaveText('1 event');
|
|
||||||
|
|
||||||
await activity.locator('[data-activity-action="clear"]').click();
|
|
||||||
await activity.locator('[data-activity-clear-confirm]').click();
|
|
||||||
await expect(activity.locator('.activity-count')).toHaveText('0 events');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('workspace activity keeps raw events and renders factual work session candidates', async ({ page }) => {
|
test('workspace activity keeps raw events and renders factual work session candidates', async ({ page }) => {
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
await window.go.api.App.WritePluginSettings('verstak.activity', {
|
await window.go.api.App.WritePluginSettings('verstak.activity', {
|
||||||
|
|
@ -106,8 +74,8 @@ test.describe('Activity workflow', () => {
|
||||||
await expect(candidateSection).toContainText('Possible journal entries');
|
await expect(candidateSection).toContainText('Possible journal entries');
|
||||||
const candidate = candidateSection.locator('[data-work-session-candidate]');
|
const candidate = candidateSection.locator('[data-work-session-candidate]');
|
||||||
await expect(candidate).toHaveCount(1);
|
await expect(candidate).toHaveCount(1);
|
||||||
await expect(candidate).toContainText('Deal: Project');
|
await expect(candidate).toContainText('Workspace: Project');
|
||||||
await expect(candidate).toContainText('Estimated duration: 10 min');
|
await expect(candidate).toContainText('Estimated duration: 20 min');
|
||||||
await expect(candidate).toContainText('Activities: 2');
|
await expect(candidate).toContainText('Activities: 2');
|
||||||
await expect(candidate).not.toContainText('Project work on');
|
await expect(candidate).not.toContainText('Project work on');
|
||||||
await expect(candidate.locator('[data-work-session-action="review"]')).toBeVisible();
|
await expect(candidate.locator('[data-work-session-action="review"]')).toBeVisible();
|
||||||
|
|
@ -142,11 +110,11 @@ test.describe('Activity workflow', () => {
|
||||||
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveAttribute('aria-selected', 'true');
|
||||||
const journal = page.locator('.journal-root');
|
const journal = page.locator('.journal-root');
|
||||||
await expect(journal).toBeVisible({ timeout: 10000 });
|
await expect(journal).toBeVisible({ timeout: 10000 });
|
||||||
await expect(journal.locator('[data-journal-candidate]')).toContainText('Deal: Project');
|
await expect(journal.locator('[data-journal-candidate]')).toContainText('Workspace: Project');
|
||||||
await expect(journal.locator('[data-journal-candidate]')).toContainText('Estimated duration: 10 min');
|
await expect(journal.locator('[data-journal-candidate]')).toContainText('Estimated duration: 20 min');
|
||||||
await expect(journal.locator('[data-journal-input="title"]')).toHaveValue('');
|
await expect(journal.locator('[data-journal-input="title"]')).toHaveValue('');
|
||||||
await expect(journal.locator('[data-journal-input="summary"]')).toHaveValue('');
|
await expect(journal.locator('[data-journal-input="summary"]')).toHaveValue('');
|
||||||
await expect(journal.locator('[data-journal-input="minutes"]')).toHaveValue('10');
|
await expect(journal.locator('[data-journal-input="minutes"]')).toHaveValue('20');
|
||||||
|
|
||||||
const activityInputs = journal.locator('[data-journal-candidate-activity]');
|
const activityInputs = journal.locator('[data-journal-candidate-activity]');
|
||||||
await expect(activityInputs).toHaveCount(2);
|
await expect(activityInputs).toHaveCount(2);
|
||||||
|
|
@ -158,7 +126,7 @@ test.describe('Activity workflow', () => {
|
||||||
await journal.locator('[data-journal-action="save-entry"]').click();
|
await journal.locator('[data-journal-action="save-entry"]').click();
|
||||||
|
|
||||||
await expect(journal).toContainText('Review research capture');
|
await expect(journal).toContainText('Review research capture');
|
||||||
await expect(journal).toContainText('10 min');
|
await expect(journal).toContainText('20 min');
|
||||||
const stored = await page.evaluate(async () => {
|
const stored = await page.evaluate(async () => {
|
||||||
const result = await window.go.api.App.ReadPluginSettings('verstak.journal');
|
const result = await window.go.api.App.ReadPluginSettings('verstak.journal');
|
||||||
return Array.isArray(result) ? result[0]['worklog:workspace:Project'] : result['worklog:workspace:Project'];
|
return Array.isArray(result) ? result[0]['worklog:workspace:Project'] : result['worklog:workspace:Project'];
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||||
|
|
||||||
test.describe('Browser workflow', () => {
|
test.describe('Browser Inbox workflow', () => {
|
||||||
let consoleCollector;
|
let consoleCollector;
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
|
@ -16,14 +16,14 @@ test.describe('Browser workflow', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('global inbox explains empty capture flow and exposes assignment filters', async ({ page }) => {
|
test('global inbox explains empty capture flow and exposes assignment filters', async ({ page }) => {
|
||||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser' }).click();
|
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser Inbox' }).click();
|
||||||
|
|
||||||
const inbox = page.locator('.browser-inbox-root');
|
const inbox = page.locator('.browser-inbox-root');
|
||||||
await expect(inbox).toBeVisible({ timeout: 10000 });
|
await expect(inbox).toBeVisible({ timeout: 10000 });
|
||||||
await expect(inbox.locator('.browser-inbox-title')).toHaveText('Browser');
|
await expect(inbox.locator('.browser-inbox-title')).toContainText('Browser Inbox');
|
||||||
await expect(inbox.locator('.browser-inbox-count')).toHaveText('0 items');
|
await expect(inbox.locator('.browser-inbox-count')).toHaveText('0 items');
|
||||||
await expect(inbox.locator('.browser-inbox-empty')).toContainText('No browser materials yet');
|
await expect(inbox.locator('.browser-inbox-empty')).toContainText('No browser captures yet');
|
||||||
await expect(inbox.locator('.browser-inbox-empty')).toContainText('Send a page, selection, or link from the extension');
|
await expect(inbox.locator('.browser-inbox-empty')).toContainText('send a page, selection, link, or file from the browser extension');
|
||||||
await expect(inbox.locator('[data-browser-inbox-filter="status"]')).toBeVisible();
|
await expect(inbox.locator('[data-browser-inbox-filter="status"]')).toBeVisible();
|
||||||
await expect(inbox.locator('[data-browser-inbox-filter="workspace"]')).toBeVisible();
|
await expect(inbox.locator('[data-browser-inbox-filter="workspace"]')).toBeVisible();
|
||||||
await expect(inbox.locator('[data-browser-inbox-action="clear"]')).toBeDisabled();
|
await expect(inbox.locator('[data-browser-inbox-action="clear"]')).toBeDisabled();
|
||||||
|
|
@ -47,7 +47,7 @@ test.describe('Browser workflow', () => {
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await page.getByRole('tab', { name: 'Browser' }).click();
|
await page.getByRole('tab', { name: 'Browser Inbox' }).click();
|
||||||
|
|
||||||
const inbox = page.locator('.browser-inbox-root');
|
const inbox = page.locator('.browser-inbox-root');
|
||||||
await expect(inbox.locator('.browser-inbox-count')).toHaveText('1 item');
|
await expect(inbox.locator('.browser-inbox-count')).toHaveText('1 item');
|
||||||
|
|
@ -94,7 +94,7 @@ test.describe('Browser workflow', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser' }).click();
|
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser Inbox' }).click();
|
||||||
|
|
||||||
const inbox = page.locator('.browser-inbox-root');
|
const inbox = page.locator('.browser-inbox-root');
|
||||||
await expect(inbox.locator('.browser-inbox-count')).toHaveText('3 items');
|
await expect(inbox.locator('.browser-inbox-count')).toHaveText('3 items');
|
||||||
|
|
|
||||||
|
|
@ -124,20 +124,4 @@ test.describe('Command Palette', () => {
|
||||||
await expect(page.locator('.modal[aria-label="Plugin Settings"]')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('.modal[aria-label="Plugin Settings"]')).toBeVisible({ timeout: 10000 });
|
||||||
await expect(page.locator('.modal-header h3')).toContainText('Sync');
|
await expect(page.locator('.modal-header h3')).toContainText('Sync');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keeps technical command failures out of the palette status', async ({ page }) => {
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.go.api.App.PluginSyncNow = () => Promise.resolve('[plugin:verstak.sync] sync.now failed: internal service code 42');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
|
|
||||||
const palette = page.locator('.command-palette');
|
|
||||||
await palette.locator('[data-command-palette-input]').fill('sync now');
|
|
||||||
await page.keyboard.press('Enter');
|
|
||||||
|
|
||||||
const status = page.locator('[data-command-palette-status="error"]');
|
|
||||||
await expect(status).toContainText('Could not run Sync Now. Please try again.');
|
|
||||||
await expect(status).not.toContainText('[plugin:');
|
|
||||||
await expect(status).not.toContainText('sync.now');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -36,69 +36,6 @@ test.describe('F: Default Editor Plugin', () => {
|
||||||
await expect(textarea).toHaveValue('Buy groceries\nWrite tests');
|
await expect(textarea).toHaveValue('Buy groceries\nWrite tests');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('secret link opens its exact secret and closing it restores the note preview', async ({ page }) => {
|
|
||||||
const notePath = 'Notes/Secret Link.md';
|
|
||||||
const noteContent = '# Secret link\n\n[Target secret](verstak-secret://target.secret)';
|
|
||||||
|
|
||||||
await page.evaluate(async ({ notePath, noteContent }) => {
|
|
||||||
const writeError = await window.go.api.App.WriteVaultTextFile(
|
|
||||||
'verstak.platform-test',
|
|
||||||
notePath,
|
|
||||||
noteContent,
|
|
||||||
{ createIfMissing: true, overwrite: true },
|
|
||||||
);
|
|
||||||
if (writeError) throw new Error(writeError);
|
|
||||||
const [result, openError] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
|
||||||
kind: 'vault-file',
|
|
||||||
path: notePath,
|
|
||||||
extension: '.md',
|
|
||||||
context: { sourceView: 'notes', isInsideNotesFolder: true, notesMode: true },
|
|
||||||
});
|
|
||||||
if (openError) throw new Error(openError);
|
|
||||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
|
||||||
}, { notePath, noteContent });
|
|
||||||
|
|
||||||
const note = page.locator('[data-editor-mode="notes-markdown"]');
|
|
||||||
await expect(note).toBeVisible({ timeout: 10000 });
|
|
||||||
await expect(note.locator('[data-preview]')).toContainText('Target secret');
|
|
||||||
await note.locator('.secret-link').click();
|
|
||||||
|
|
||||||
const secrets = page.locator('.secrets-root');
|
|
||||||
await expect(secrets).toBeVisible({ timeout: 10000 });
|
|
||||||
await expect(secrets.locator('.secrets-item.active .secrets-item-title')).toHaveText('Target secret');
|
|
||||||
await expect(secrets.locator('.secrets-card h2')).toHaveText('Target secret');
|
|
||||||
|
|
||||||
await page.locator('.workbench-host .close-btn').click();
|
|
||||||
await expect(note).toBeVisible({ timeout: 10000 });
|
|
||||||
await expect(note.locator('[data-preview]')).toContainText('Target secret');
|
|
||||||
await expect(note.locator('[data-editor-textarea]')).toHaveCount(0);
|
|
||||||
await expect(note.locator('[data-save-state]')).toHaveText('');
|
|
||||||
|
|
||||||
const storedContent = await page.evaluate(async ({ notePath }) => {
|
|
||||||
const [content, readError] = await window.go.api.App.ReadVaultTextFile('verstak.platform-test', notePath);
|
|
||||||
if (readError) throw new Error(readError);
|
|
||||||
return content;
|
|
||||||
}, { notePath });
|
|
||||||
expect(storedContent).toBe(noteContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('unavailable secret does not fall back to the first secret', async ({ page }) => {
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
const [result, openError] = await window.go.api.App.OpenWorkbenchResource('verstak.default-editor', {
|
|
||||||
kind: 'secret',
|
|
||||||
path: 'missing.secret',
|
|
||||||
mode: 'view',
|
|
||||||
});
|
|
||||||
if (openError) throw new Error(openError);
|
|
||||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
|
||||||
});
|
|
||||||
|
|
||||||
const secrets = page.locator('.secrets-root');
|
|
||||||
await expect(secrets).toBeVisible({ timeout: 10000 });
|
|
||||||
await expect(secrets.locator('.secrets-item.active')).toHaveCount(0);
|
|
||||||
await expect(secrets.locator('.secrets-status.error')).toContainText(/unavailable|недоступен/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('editor supports markdown toolbar split save reopen and revert', async ({ page }) => {
|
test('editor supports markdown toolbar split save reopen and revert', async ({ page }) => {
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const err = await window.go.api.App.WriteVaultTextFile(
|
const err = await window.go.api.App.WriteVaultTextFile(
|
||||||
|
|
@ -175,10 +112,10 @@ test.describe('F: Default Editor Plugin', () => {
|
||||||
const workbench = page.locator('.workbench-host');
|
const workbench = page.locator('.workbench-host');
|
||||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||||
const title = workbench.locator('.workbench-title');
|
const title = workbench.locator('.workbench-title');
|
||||||
await expect(title).toHaveText('readme.md');
|
await expect(title).toHaveText('Docs/readme.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('opening a note uses its title rather than its technical path', async ({ page }) => {
|
test('open .md with notes context routes to highest-priority provider', async ({ page }) => {
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
|
||||||
kind: 'vault-file',
|
kind: 'vault-file',
|
||||||
|
|
@ -193,7 +130,7 @@ test.describe('F: Default Editor Plugin', () => {
|
||||||
const workbench = page.locator('.workbench-host');
|
const workbench = page.locator('.workbench-host');
|
||||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||||
const title = workbench.locator('.workbench-title');
|
const title = workbench.locator('.workbench-title');
|
||||||
await expect(title).toHaveText('Overview');
|
await expect(title).toHaveText('Notes/Overview.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('default-editor plugin is listed as loaded in plugin manager', async ({ page }) => {
|
test('default-editor plugin is listed as loaded in plugin manager', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,7 @@ test.describe('G: Files Plugin', () => {
|
||||||
|
|
||||||
const workbench = page.locator('.workbench-host');
|
const workbench = page.locator('.workbench-host');
|
||||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||||
await expect(workbench.locator('.workbench-title')).toHaveText('readme.md');
|
await expect(workbench.locator('.workbench-title')).toHaveText('Docs/readme.md');
|
||||||
|
|
||||||
const preview = page.locator('.de-preview');
|
const preview = page.locator('.de-preview');
|
||||||
await expect(preview).toBeVisible({ timeout: 10000 });
|
await expect(preview).toBeVisible({ timeout: 10000 });
|
||||||
|
|
@ -535,7 +535,7 @@ test.describe('G: Files Plugin', () => {
|
||||||
|
|
||||||
const workbench = page.locator('.workbench-host');
|
const workbench = page.locator('.workbench-host');
|
||||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||||
await expect(workbench.locator('.workbench-title')).toHaveText('Overview');
|
await expect(workbench.locator('.workbench-title')).toHaveText('Notes/Overview.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('files plugin card shows openProviders in contributions', async ({ page }) => {
|
test('files plugin card shows openProviders in contributions', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
|
||||||
|
|
||||||
test('global Journal creates an entry in the selected Deal', async ({ page }) => {
|
|
||||||
const consoleCollector = setupConsoleCollector(page);
|
|
||||||
await resetMockState(page);
|
|
||||||
await page.goto('/');
|
|
||||||
await waitForAppReady(page);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
await window.go.api.App.WritePluginSettings('verstak.journal', {
|
|
||||||
'worklog:workspace:Project': [{ entryId: 'existing-project-entry', workspaceRootPath: 'Project', date: '2026-07-14', title: 'Existing entry', minutes: 5 }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Journal' }).click();
|
|
||||||
const journal = page.locator('.journal-root');
|
|
||||||
await expect(journal.locator('.journal-title')).toHaveText('Journal');
|
|
||||||
await journal.locator('[data-journal-action="add"]').click();
|
|
||||||
await journal.locator('[data-journal-input="workspaceRootPath"]').selectOption('Project');
|
|
||||||
await journal.locator('[data-journal-input="title"]').fill('Prepare project handoff');
|
|
||||||
await journal.locator('[data-journal-input="minutes"]').fill('30');
|
|
||||||
await journal.locator('[data-journal-action="save-entry"]').click();
|
|
||||||
|
|
||||||
await expect.poll(async () => page.evaluate(async () => {
|
|
||||||
const result = await window.go.api.App.ReadPluginSettings('verstak.journal');
|
|
||||||
const settings = Array.isArray(result) ? result[0] : result;
|
|
||||||
return settings['worklog:workspace:Project']?.[0]?.title;
|
|
||||||
})).toBe('Prepare project handoff');
|
|
||||||
await expect(journal).toContainText('Prepare project handoff');
|
|
||||||
|
|
||||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Journal' }).click();
|
|
||||||
await expect(page.locator('.journal-root')).toContainText('Prepare project handoff');
|
|
||||||
consoleCollector.assertNoErrors();
|
|
||||||
});
|
|
||||||
|
|
@ -25,23 +25,9 @@ test.describe('Desktop localization', () => {
|
||||||
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Тест платформы' })).toBeVisible();
|
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Тест платформы' })).toBeVisible();
|
||||||
await expect(page.locator('[data-status-item-id="verstak.platform-test.status"]')).toContainText('Все тесты пройдены');
|
await expect(page.locator('[data-status-item-id="verstak.platform-test.status"]')).toContainText('Все тесты пройдены');
|
||||||
|
|
||||||
const project = page.locator('.wt-node').filter({ hasText: 'Project' });
|
|
||||||
await project.locator('button[title="Переименовать Дело"]').click();
|
|
||||||
await expect(page.locator('button[title="Сохранить новое название"]')).toHaveText('Сохранить');
|
|
||||||
await page.locator('.wt-rename').press('Escape');
|
|
||||||
|
|
||||||
await page.locator('[data-settings-menu-button]').click();
|
await page.locator('[data-settings-menu-button]').click();
|
||||||
await expect(page.locator('[data-settings-action="plugin-manager"]')).toContainText('Менеджер плагинов');
|
await expect(page.locator('[data-settings-action="plugin-manager"]')).toContainText('Менеджер плагинов');
|
||||||
await expect(page.locator('[data-settings-language="ru"]')).toHaveAttribute('aria-checked', 'true');
|
await expect(page.locator('[data-settings-language="ru"]')).toHaveAttribute('aria-checked', 'true');
|
||||||
await page.locator('[data-settings-action="plugin-manager"]').click();
|
|
||||||
const pluginManager = page.locator('.plugin-manager');
|
|
||||||
await expect(pluginManager).toContainText('Зарегистрировано возможностей:');
|
|
||||||
await expect(pluginManager.locator('.registry-section')).toContainText('Реестр возможностей');
|
|
||||||
await expect(pluginManager.locator('.registry-section')).toContainText('Возможность');
|
|
||||||
await expect(pluginManager.locator('.plugin-filters')).toContainText('Фильтры');
|
|
||||||
await expect(pluginManager.locator('[data-plugin-filter-results]')).toContainText('Показано плагинов:');
|
|
||||||
await expect(pluginManager.locator('[data-plugin-filter="status"]')).toContainText('Выключенные');
|
|
||||||
await expect(pluginManager).not.toContainText('Workspace-scoped');
|
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await waitForAppReady(page);
|
await waitForAppReady(page);
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ test.describe('D: Plugin API bridge', () => {
|
||||||
|
|
||||||
const workbench = page.locator('.workbench-host');
|
const workbench = page.locator('.workbench-host');
|
||||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||||
await expect(workbench.locator('.workbench-title')).toHaveText('Overview');
|
await expect(workbench.locator('.workbench-title')).toHaveText('Notes/Overview.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('workbench routes markdown files to default-editor provider', async ({ page }) => {
|
test('workbench routes markdown files to default-editor provider', async ({ page }) => {
|
||||||
|
|
@ -76,7 +76,7 @@ test.describe('D: Plugin API bridge', () => {
|
||||||
const workbench = page.locator('.workbench-host');
|
const workbench = page.locator('.workbench-host');
|
||||||
await expect(workbench).toBeVisible({ timeout: 10000 });
|
await expect(workbench).toBeVisible({ timeout: 10000 });
|
||||||
const title = workbench.locator('.workbench-title');
|
const title = workbench.locator('.workbench-title');
|
||||||
await expect(title).toHaveText('readme.md');
|
await expect(title).toHaveText('Docs/readme.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('workbench shows no-provider fallback when no provider matches', async ({ page }) => {
|
test('workbench shows no-provider fallback when no provider matches', async ({ page }) => {
|
||||||
|
|
@ -91,10 +91,8 @@ test.describe('D: Plugin API bridge', () => {
|
||||||
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
|
||||||
});
|
});
|
||||||
|
|
||||||
const fallback = page.locator('[data-workbench-status="no-provider"]');
|
await expect(page.locator('[data-workbench-status="no-provider"]')).toBeVisible();
|
||||||
await expect(fallback).toBeVisible();
|
await expect(page.locator('[data-workbench-status="no-provider"]')).toContainText('No viewer/editor available');
|
||||||
await expect(fallback).toContainText('No viewer/editor available');
|
|
||||||
await expect(fallback).not.toContainText('generic-text');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sync plugin API routes through mocked Wails bridge', async ({ page }) => {
|
test('sync plugin API routes through mocked Wails bridge', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager, setPluginStatus } from './helpers.js';
|
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
||||||
|
|
||||||
test.describe('E: Plugin Manager layout', () => {
|
test.describe('E: Plugin Manager layout', () => {
|
||||||
let consoleCollector;
|
let consoleCollector;
|
||||||
|
|
@ -88,56 +88,6 @@ test.describe('E: Plugin Manager layout', () => {
|
||||||
await expect(risk).toContainText('elevated permissions');
|
await expect(risk).toContainText('elevated permissions');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('plugin manager filters by state, declared permissions, capabilities, settings and source', async ({ page }) => {
|
|
||||||
await setPluginStatus(page, 'verstak.todo', 'disabled', false);
|
|
||||||
await page.evaluate(() => window.__wailsMock.addSyntheticPlugins(1, 'third-party'));
|
|
||||||
await openPluginManager(page);
|
|
||||||
|
|
||||||
const results = page.locator('[data-plugin-filter-results]');
|
|
||||||
await expect(results).toContainText('Showing');
|
|
||||||
const filterSelectStyle = await page.locator('[data-plugin-filter="status"]').evaluate((node) => {
|
|
||||||
const style = getComputedStyle(node);
|
|
||||||
return { appearance: style.appearance, backgroundImage: style.backgroundImage };
|
|
||||||
});
|
|
||||||
expect(filterSelectStyle.appearance).toBe('none');
|
|
||||||
expect(filterSelectStyle.backgroundImage).not.toBe('none');
|
|
||||||
|
|
||||||
await page.locator('[data-plugin-filter="status"]').selectOption('disabled');
|
|
||||||
await expect(page.locator('.plugin-card')).toHaveCount(1);
|
|
||||||
await expect(page.locator('.plugin-card')).toContainText('Todos');
|
|
||||||
await page.locator('button.reload-btn').click();
|
|
||||||
await expect(page.locator('[data-plugin-filter="status"]')).toHaveValue('disabled');
|
|
||||||
await expect(page.locator('.plugin-card')).toHaveCount(1);
|
|
||||||
|
|
||||||
await page.locator('[data-plugin-filter-reset]').click();
|
|
||||||
await page.locator('[data-plugin-filter-permission="notifications.schedule"]').check();
|
|
||||||
await expect(page.locator('.plugin-card')).toHaveCount(1);
|
|
||||||
await expect(page.locator('.plugin-card')).toContainText('Todos');
|
|
||||||
await page.locator('[data-plugin-filter-permission="secrets.read"]').check();
|
|
||||||
await expect(page.locator('.plugin-card')).toHaveCount(2);
|
|
||||||
await page.locator('[data-plugin-filter-permission="secrets.read"]').uncheck();
|
|
||||||
|
|
||||||
await page.locator('[data-plugin-filter-reset]').click();
|
|
||||||
await page.locator('[data-plugin-filter-capability="verstak/core/notifications/v1"]').check();
|
|
||||||
await expect(page.locator('.plugin-card')).toHaveCount(1);
|
|
||||||
await expect(page.locator('.plugin-card')).toContainText('Todos');
|
|
||||||
await page.locator('[data-plugin-filter-capability="secret-store"]').check();
|
|
||||||
await expect(page.locator('.plugin-card')).toHaveCount(2);
|
|
||||||
await page.locator('[data-plugin-filter-capability="secret-store"]').uncheck();
|
|
||||||
|
|
||||||
await page.locator('[data-plugin-filter-reset]').click();
|
|
||||||
await page.locator('[data-plugin-filter="settings"]').selectOption('with');
|
|
||||||
await expect(page.locator('.plugin-card').filter({ hasText: 'Platform Test' })).toBeVisible();
|
|
||||||
await expect(page.locator('.plugin-card').filter({ hasText: 'Default Editor' })).toHaveCount(0);
|
|
||||||
|
|
||||||
await page.locator('[data-plugin-filter="source"]').selectOption('third-party');
|
|
||||||
await expect(page.locator('.plugin-card')).toHaveCount(0);
|
|
||||||
await expect(page.locator('[data-plugin-filter-empty]')).toBeVisible();
|
|
||||||
|
|
||||||
await page.locator('[data-plugin-filter-reset]').click();
|
|
||||||
await expect(page.locator('.plugin-card')).toHaveCount(13);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('workspace selection keeps exactly one active node', async ({ page }) => {
|
test('workspace selection keeps exactly one active node', async ({ page }) => {
|
||||||
const selected = page.locator('.wt-node.selected .wt-label');
|
const selected = page.locator('.wt-node.selected .wt-label');
|
||||||
await expect(selected).toHaveCount(1);
|
await expect(selected).toHaveCount(1);
|
||||||
|
|
@ -168,15 +118,15 @@ test.describe('E: Plugin Manager layout', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('workspace sidebar creates renames and trashes top-level workspaces', async ({ page }) => {
|
test('workspace sidebar creates renames and trashes top-level workspaces', async ({ page }) => {
|
||||||
await page.locator('button[title="New Deal"]').click();
|
await page.locator('button[title="New workspace"]').click();
|
||||||
const modal = page.locator('[data-workspace-create-modal]');
|
const modal = page.locator('[data-workspace-create-modal]');
|
||||||
await modal.locator('[data-workspace-name]').fill('ClientA');
|
await modal.locator('[data-workspace-name]').fill('ClientA');
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
await modal.getByRole('button', { name: 'Create workspace' }).click();
|
||||||
|
|
||||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toBeVisible();
|
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toBeVisible();
|
||||||
|
|
||||||
const client = page.locator('.wt-node').filter({ hasText: 'ClientA' });
|
const client = page.locator('.wt-node').filter({ hasText: 'ClientA' });
|
||||||
await client.locator('button[title="Rename Deal"]').click();
|
await client.locator('button[title="Rename workspace"]').click();
|
||||||
await page.locator('.wt-rename').fill('ClientB');
|
await page.locator('.wt-rename').fill('ClientB');
|
||||||
await page.locator('button[title="Save rename"]').click();
|
await page.locator('button[title="Save rename"]').click();
|
||||||
|
|
||||||
|
|
@ -184,7 +134,7 @@ test.describe('E: Plugin Manager layout', () => {
|
||||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toHaveCount(0);
|
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toHaveCount(0);
|
||||||
|
|
||||||
const renamed = page.locator('.wt-node').filter({ hasText: 'ClientB' });
|
const renamed = page.locator('.wt-node').filter({ hasText: 'ClientB' });
|
||||||
await renamed.locator('button[title="Move Deal to trash"]').click();
|
await renamed.locator('button[title="Trash workspace"]').click();
|
||||||
|
|
||||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientB' })).toHaveCount(0);
|
await expect(page.locator('.wt-label').filter({ hasText: 'ClientB' })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
|
||||||
|
|
||||||
test('global Secrets lists, filters, and creates Deal-scoped secrets', async ({ page }) => {
|
|
||||||
const consoleCollector = setupConsoleCollector(page);
|
|
||||||
await resetMockState(page);
|
|
||||||
await page.goto('/');
|
|
||||||
await waitForAppReady(page);
|
|
||||||
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
const [record, error] = await window.go.api.App.PluginSecretsWrite('verstak.secrets', {
|
|
||||||
id: 'project.global-secret',
|
|
||||||
title: 'Project API',
|
|
||||||
username: 'project-user',
|
|
||||||
value: 'project-value',
|
|
||||||
scope: { kind: 'workspace', workspaceRootPath: 'Project' },
|
|
||||||
});
|
|
||||||
if (error || !record?.id) throw new Error(error || 'could not create a project secret');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Secrets' }).click();
|
|
||||||
const secrets = page.locator('.secrets-root');
|
|
||||||
await expect(secrets).toBeVisible({ timeout: 10000 });
|
|
||||||
await expect(secrets).toContainText('First secret');
|
|
||||||
await expect(secrets).toContainText('Project API');
|
|
||||||
|
|
||||||
await secrets.locator('[data-secret-scope-filter]').selectOption('workspace:Project');
|
|
||||||
await expect(secrets).toContainText('Project API');
|
|
||||||
await expect(secrets).not.toContainText('First secret');
|
|
||||||
|
|
||||||
await secrets.locator('[data-secret-search]').fill('no matching secret');
|
|
||||||
await expect(secrets).toContainText('No secrets');
|
|
||||||
await secrets.locator('[data-secret-search]').fill('');
|
|
||||||
|
|
||||||
await secrets.locator('[data-secret-scope-filter]').selectOption('all');
|
|
||||||
await secrets.getByRole('button', { name: 'New' }).click();
|
|
||||||
await secrets.locator('[data-secret-title]').fill('Project deployment token');
|
|
||||||
await secrets.locator('[data-secret-value]').fill('deployment-token-value');
|
|
||||||
await secrets.locator('[data-secret-scope]').selectOption('workspace');
|
|
||||||
await secrets.locator('[data-secret-workspace]').selectOption('Project');
|
|
||||||
await secrets.locator('[data-secret-save]').click();
|
|
||||||
|
|
||||||
await expect.poll(async () => page.evaluate(async () => {
|
|
||||||
const [records, error] = await window.go.api.App.PluginSecretsList('verstak.secrets');
|
|
||||||
if (error) throw new Error(error);
|
|
||||||
const record = records.find((item) => item.title === 'Project deployment token');
|
|
||||||
return record && [record.scope?.kind, record.scope?.workspaceRootPath].join('|');
|
|
||||||
})).toBe('workspace|Project');
|
|
||||||
consoleCollector.assertNoErrors();
|
|
||||||
});
|
|
||||||
|
|
@ -30,45 +30,18 @@ test.describe('B: Sidebar opens plugin view by item.view', () => {
|
||||||
test('Sidebar item exists with correct label', async ({ page }) => {
|
test('Sidebar item exists with correct label', async ({ page }) => {
|
||||||
await expect(page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' })).not.toBeVisible();
|
await expect(page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' })).not.toBeVisible();
|
||||||
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Activity' })).toBeVisible();
|
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Activity' })).toBeVisible();
|
||||||
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser' })).toBeVisible();
|
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser Inbox' })).toBeVisible();
|
||||||
|
|
||||||
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
|
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
|
||||||
await expect(sidebarItem).toBeVisible();
|
await expect(sidebarItem).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Global Activity and Browser sidebar items open plugin views', async ({ page }) => {
|
test('Global Activity and Browser Inbox sidebar items open plugin views', async ({ page }) => {
|
||||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Activity' }).click();
|
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Activity' }).click();
|
||||||
await expect(page.locator('.view-container .view-header h2')).toHaveText('Activity', { timeout: 10000 });
|
await expect(page.locator('.view-container .view-header h2')).toHaveText('Activity', { timeout: 10000 });
|
||||||
|
|
||||||
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser' }).click();
|
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser Inbox' }).click();
|
||||||
await expect(page.locator('.view-container .view-header h2')).toHaveText('Browser', { timeout: 10000 });
|
await expect(page.locator('.view-container .view-header h2')).toHaveText('Browser Inbox', { timeout: 10000 });
|
||||||
});
|
|
||||||
|
|
||||||
test('selected global tool remains visibly active through navigation and sidebar reloads', async ({ page }) => {
|
|
||||||
const activity = page.locator('.sidebar .plugin-item').filter({ hasText: 'Activity' });
|
|
||||||
const browserInbox = page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser' });
|
|
||||||
|
|
||||||
await activity.click();
|
|
||||||
await expect(activity).toHaveAttribute('aria-current', 'page');
|
|
||||||
await expect(activity).toHaveClass(/is-active/);
|
|
||||||
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.dispatchEvent(new CustomEvent('verstak:plugins-changed'));
|
|
||||||
});
|
|
||||||
await expect(activity).toHaveAttribute('aria-current', 'page');
|
|
||||||
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.dispatchEvent(new CustomEvent('verstak:open-view', {
|
|
||||||
detail: { viewId: 'verstak.browser-inbox.view', pluginId: 'verstak.browser-inbox' },
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
await expect(page.locator('.view-container .view-header h2')).toHaveText('Browser', { timeout: 10000 });
|
|
||||||
await expect(browserInbox).toHaveAttribute('aria-current', 'page');
|
|
||||||
await expect(browserInbox).toHaveClass(/is-active/);
|
|
||||||
await expect(activity).not.toHaveAttribute('aria-current', 'page');
|
|
||||||
|
|
||||||
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
|
|
||||||
await expect(browserInbox).not.toHaveAttribute('aria-current', 'page');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Click sidebar item opens diagnostics view by view ID, not sidebar ID', async ({ page }) => {
|
test('Click sidebar item opens diagnostics view by view ID, not sidebar ID', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,7 @@ test.describe('Todo plugin workflow', () => {
|
||||||
await todos.locator('[data-todo-input="description"]').fill('Collect factual review notes.');
|
await todos.locator('[data-todo-input="description"]').fill('Collect factual review notes.');
|
||||||
await todos.locator('[data-todo-input="priority"]').selectOption('high');
|
await todos.locator('[data-todo-input="priority"]').selectOption('high');
|
||||||
await todos.locator('[data-todo-input="dueAt"]').fill('2000-01-01');
|
await todos.locator('[data-todo-input="dueAt"]').fill('2000-01-01');
|
||||||
await todos.locator('[data-todo-input="reminderDate"]').fill('2000-01-01');
|
await todos.locator('[data-todo-input="reminderAt"]').fill('2000-01-01T09:00');
|
||||||
const reminderTime = todos.locator('[data-todo-input="reminderTime"]');
|
|
||||||
await expect(reminderTime).toHaveAttribute('type', 'text');
|
|
||||||
await reminderTime.fill('09:30');
|
|
||||||
await todos.locator('[data-todo-action="save"]').click();
|
await todos.locator('[data-todo-action="save"]').click();
|
||||||
|
|
||||||
await expect(todos).toContainText('Overdue');
|
await expect(todos).toContainText('Overdue');
|
||||||
|
|
@ -39,23 +36,13 @@ test.describe('Todo plugin workflow', () => {
|
||||||
const result = await window.go.api.App.ReadPluginSettings('verstak.todo');
|
const result = await window.go.api.App.ReadPluginSettings('verstak.todo');
|
||||||
const settings = Array.isArray(result) ? result[0] : result;
|
const settings = Array.isArray(result) ? result[0] : result;
|
||||||
const todo = settings['todos:global'].find((item) => item.title === 'Prepare project review');
|
const todo = settings['todos:global'].find((item) => item.title === 'Prepare project review');
|
||||||
return todo && [todo.workspaceRootPath, todo.priority, todo.dueAt, todo.reminderDate, todo.reminderAt].join('|');
|
return todo && [todo.workspaceRootPath, todo.priority, todo.dueAt, todo.reminderAt].join('|');
|
||||||
})).toBe('Project|high|2000-01-01|2000-01-01|2000-01-01T09:30');
|
})).toBe('Project|high|2000-01-01|2000-01-01T09:00');
|
||||||
|
|
||||||
await todos.locator('[data-todo-action="edit"]').click();
|
await todos.locator('[data-todo-action="edit"]').click();
|
||||||
await todos.locator('[data-todo-input="title"]').fill('Prepare project review updated');
|
await todos.locator('[data-todo-input="title"]').fill('Prepare project review updated');
|
||||||
await todos.locator('[data-todo-input="reminderTime"]').fill('not-a-time');
|
|
||||||
await todos.locator('[data-todo-action="save"]').click();
|
|
||||||
await expect(todos).toContainText('Enter a valid reminder time');
|
|
||||||
await todos.locator('[data-todo-input="reminderTime"]').fill('');
|
|
||||||
await todos.locator('[data-todo-action="save"]').click();
|
await todos.locator('[data-todo-action="save"]').click();
|
||||||
await expect(todos).toContainText('Prepare project review updated');
|
await expect(todos).toContainText('Prepare project review updated');
|
||||||
await expect.poll(async () => page.evaluate(async () => {
|
|
||||||
const result = await window.go.api.App.ReadPluginSettings('verstak.todo');
|
|
||||||
const settings = Array.isArray(result) ? result[0] : result;
|
|
||||||
const todo = settings['todos:global'].find((item) => item.title === 'Prepare project review updated');
|
|
||||||
return todo && [todo.reminderDate, todo.reminderAt].join('|');
|
|
||||||
})).toBe('2000-01-01|');
|
|
||||||
|
|
||||||
await todos.locator('[data-todo-action="mark-done"]').click();
|
await todos.locator('[data-todo-action="mark-done"]').click();
|
||||||
await expect(todos.locator('[data-todo-action="create-journal-entry"]')).toBeVisible();
|
await expect(todos.locator('[data-todo-action="create-journal-entry"]')).toBeVisible();
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ test.describe('UX follow-up fixes', () => {
|
||||||
await expect(search).toBeVisible();
|
await expect(search).toBeVisible();
|
||||||
await expect(page.locator('.sidebar [data-global-search-input]')).toBeVisible();
|
await expect(page.locator('.sidebar [data-global-search-input]')).toBeVisible();
|
||||||
|
|
||||||
await page.locator('.sidebar .nav-item').filter({ hasText: 'Browser' }).click();
|
await page.locator('.sidebar .nav-item').filter({ hasText: 'Browser Inbox' }).click();
|
||||||
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
||||||
await expect(search).toBeVisible();
|
await expect(search).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
@ -67,7 +67,7 @@ test.describe('UX follow-up fixes', () => {
|
||||||
|
|
||||||
const search = page.locator('[data-global-search-input]');
|
const search = page.locator('[data-global-search-input]');
|
||||||
await search.fill('Research Search Result');
|
await search.fill('Research Search Result');
|
||||||
const result = page.locator('[data-global-search-result-type="Browser"]').filter({ hasText: 'Research Search Result' });
|
const result = page.locator('[data-global-search-result-type="Browser Inbox"]').filter({ hasText: 'Research Search Result' });
|
||||||
await expect(result).toBeVisible({ timeout: 10000 });
|
await expect(result).toBeVisible({ timeout: 10000 });
|
||||||
await result.click();
|
await result.click();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,6 @@ test.describe('UX P0 shell flow', () => {
|
||||||
await expect(page.locator('.plugin-manager')).toHaveCount(0);
|
await expect(page.locator('.plugin-manager')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Deal header does not expose the internal workspace type badge', async ({ page }) => {
|
|
||||||
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
|
||||||
await expect(page.locator('.workspace-type')).toHaveCount(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('status bar plugin contribution failures do not render large error panels', async ({ page }) => {
|
test('status bar plugin contribution failures do not render large error panels', async ({ page }) => {
|
||||||
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
|
||||||
await expect(page.getByText('Plugin View Error')).toHaveCount(0);
|
await expect(page.getByText('Plugin View Error')).toHaveCount(0);
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
await page.getByRole('tab', { name: 'Overview' }).click();
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
await overview.locator('[data-overview-summary="captures"]').click();
|
await overview.locator('[data-overview-summary="captures"]').click();
|
||||||
|
|
||||||
await expect(page.getByRole('tab', { name: 'Browser' })).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Overview' }).click();
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
|
|
@ -70,7 +70,7 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Overview' }).click();
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
await overview.locator('[data-overview-summary="attention"]').click();
|
await overview.locator('[data-overview-summary="attention"]').click();
|
||||||
await expect(page.getByRole('tab', { name: 'Browser' })).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Overview keeps the Notes total factual when the Files plugin is unavailable', async ({ page }) => {
|
test('Overview keeps the Notes total factual when the Files plugin is unavailable', async ({ page }) => {
|
||||||
|
|
@ -80,54 +80,6 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
await expect(page.locator('[data-overview-summary="notes"]')).toContainText('1 total');
|
await expect(page.locator('[data-overview-summary="notes"]')).toContainText('1 total');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Overview hides Browser cards and actions when the current Deal does not include it', async ({ page }) => {
|
|
||||||
await page.locator('button[title="New Deal"]').click();
|
|
||||||
const modal = page.locator('[data-workspace-create-modal]');
|
|
||||||
await modal.locator('[data-workspace-name]').fill('MinimalOverview');
|
|
||||||
await modal.locator('[data-workspace-template]').selectOption('minimal');
|
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
|
||||||
await expect(page.getByRole('tab', { name: 'Browser' })).toHaveCount(0);
|
|
||||||
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
|
|
||||||
'captures:global': [{
|
|
||||||
captureId: 'hidden-browser-capture',
|
|
||||||
capturedAt: '2026-07-14T08:00:00.000Z',
|
|
||||||
kind: 'page',
|
|
||||||
title: 'Inbox material must stay hidden',
|
|
||||||
workspaceRootPath: 'MinimalOverview',
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const overview = page.locator('[data-overview-root]');
|
|
||||||
await overview.locator('[data-overview-action="refresh"]').click();
|
|
||||||
|
|
||||||
await expect(overview.locator('[data-overview-summary="captures"]')).toHaveCount(0);
|
|
||||||
await expect(overview.locator('[data-overview-action="browser-inbox"]')).toHaveCount(0);
|
|
||||||
await expect(overview).not.toContainText('Inbox material must stay hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Overview refreshes when Browser is disabled through plugin state changes', async ({ page }) => {
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
|
|
||||||
'captures:global': [{
|
|
||||||
captureId: 'disabled-browser-capture',
|
|
||||||
capturedAt: '2026-07-14T08:00:00.000Z',
|
|
||||||
kind: 'page',
|
|
||||||
title: 'Disabled inbox material',
|
|
||||||
workspaceRootPath: 'Project',
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
window.__wailsMock.setPluginStatus('verstak.browser-inbox', 'disabled', false);
|
|
||||||
window.dispatchEvent(new CustomEvent('verstak:plugins-changed'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const overview = page.locator('[data-overview-root]');
|
|
||||||
await expect(page.getByRole('tab', { name: 'Browser' })).toHaveCount(0);
|
|
||||||
await expect(overview.locator('[data-overview-summary="captures"]')).toHaveCount(0);
|
|
||||||
await expect(overview.locator('[data-overview-action="browser-inbox"]')).toHaveCount(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Overview prioritizes resume work and filters meaningful recent changes', async ({ page }) => {
|
test('Overview prioritizes resume work and filters meaningful recent changes', async ({ page }) => {
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
|
await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
|
||||||
|
|
@ -248,10 +200,10 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
await expect(overview.locator('[data-overview-summary="attention"]')).toContainText('3');
|
await expect(overview.locator('[data-overview-summary="attention"]')).toContainText('3');
|
||||||
const attention = overview.locator('[data-overview-section="attention"]');
|
const attention = overview.locator('[data-overview-section="attention"]');
|
||||||
await expect(attention).toContainText('Possible journal entry');
|
await expect(attention).toContainText('Possible journal entry');
|
||||||
await expect(attention).toContainText('Deal: Project · 10 min · 2 activities');
|
await expect(attention).toContainText('Workspace: Project · 10 min · 2 activities');
|
||||||
await attention.locator('.overview-attention-row', { hasText: 'Possible journal entry' }).getByRole('button', { name: 'Review candidate' }).click();
|
await attention.locator('.overview-attention-row', { hasText: 'Possible journal entry' }).getByRole('button', { name: 'Review candidate' }).click();
|
||||||
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(page.locator('.journal-root [data-journal-candidate]')).toContainText('Deal: Project');
|
await expect(page.locator('.journal-root [data-journal-candidate]')).toContainText('Workspace: Project');
|
||||||
await page.locator('.journal-modal-actions').getByRole('button', { name: 'Cancel' }).click();
|
await page.locator('.journal-modal-actions').getByRole('button', { name: 'Cancel' }).click();
|
||||||
await page.getByRole('tab', { name: 'Overview' }).click();
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
|
|
||||||
|
|
@ -264,7 +216,7 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
await expect(candidates.nth(3)).toContainText('Changed file "draft.md"');
|
await expect(candidates.nth(3)).toContainText('Changed file "draft.md"');
|
||||||
await candidates.nth(0).click();
|
await candidates.nth(0).click();
|
||||||
|
|
||||||
await expect(page.getByRole('tab', { name: 'Browser' })).toHaveAttribute('aria-selected', 'true');
|
await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
|
||||||
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Overview' }).click();
|
await page.getByRole('tab', { name: 'Overview' }).click();
|
||||||
|
|
@ -295,28 +247,4 @@ test.describe('UX Overview workspace flow', () => {
|
||||||
await expect(recent).toContainText('Added journal entry "Write project summary"');
|
await expect(recent).toContainText('Added journal entry "Write project summary"');
|
||||||
await expect(recent).not.toContainText('Changed file "draft.md"');
|
await expect(recent).not.toContainText('Changed file "draft.md"');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Overview localizes activity labels without exposing internal event names', async ({ page }) => {
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
await window.go.api.App.WritePluginSettings('verstak.activity', {
|
|
||||||
'events:workspace:Project': [{
|
|
||||||
activityId: 'overview-russian-note',
|
|
||||||
occurredAt: '2026-06-30T08:25:00.000Z',
|
|
||||||
type: 'note.saved',
|
|
||||||
title: 'Локализация',
|
|
||||||
summary: 'Project/Notes/Localization.md',
|
|
||||||
workspaceRootPath: 'Project',
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await page.locator('[data-settings-menu-button]').click();
|
|
||||||
await page.locator('[data-settings-language="ru"]').click();
|
|
||||||
|
|
||||||
const overview = page.locator('[data-overview-root]');
|
|
||||||
await overview.locator('[data-overview-action="refresh"]').click();
|
|
||||||
const recent = overview.locator('[data-overview-section="recent"]');
|
|
||||||
await expect(recent).toContainText('Изменена заметка «Локализация»');
|
|
||||||
await expect(recent).not.toContainText('note.saved');
|
|
||||||
await expect(recent).not.toContainText('Edited note');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ test.describe('Workspace templates', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function openCreateModal(page) {
|
async function openCreateModal(page) {
|
||||||
await page.locator('button[title="New Deal"]').click();
|
await page.locator('button[title="New workspace"]').click();
|
||||||
const modal = page.locator('[data-workspace-create-modal]');
|
const modal = page.locator('[data-workspace-create-modal]');
|
||||||
await expect(modal).toBeVisible();
|
await expect(modal).toBeVisible();
|
||||||
return modal;
|
return modal;
|
||||||
|
|
@ -24,24 +24,22 @@ test.describe('Workspace templates', () => {
|
||||||
|
|
||||||
test('creation modal validates names, shows template tools, and persists the selected snapshot', async ({ page }) => {
|
test('creation modal validates names, shows template tools, and persists the selected snapshot', async ({ page }) => {
|
||||||
const modal = await openCreateModal(page);
|
const modal = await openCreateModal(page);
|
||||||
const templateSelect = modal.locator('[data-workspace-template]');
|
await expect(modal.locator('[data-workspace-template]')).toHaveValue('default');
|
||||||
await expect(templateSelect).toHaveValue('default');
|
|
||||||
await expect(templateSelect).toHaveCSS('appearance', 'none');
|
|
||||||
await expect(modal.locator('[data-workspace-template-tools]')).toContainText('Notes');
|
await expect(modal.locator('[data-workspace-template-tools]')).toContainText('Notes');
|
||||||
await expect(modal.locator('[data-workspace-template-tools]')).toContainText('Browser');
|
await expect(modal.locator('[data-workspace-template-tools]')).toContainText('Browser Inbox');
|
||||||
|
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
await modal.getByRole('button', { name: 'Create workspace' }).click();
|
||||||
await expect(modal.locator('[data-workspace-create-error]')).toContainText('Name is required');
|
await expect(modal.locator('[data-workspace-create-error]')).toContainText('Name is required');
|
||||||
|
|
||||||
await modal.locator('[data-workspace-name]').fill('bad/name');
|
await modal.locator('[data-workspace-name]').fill('bad/name');
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
await modal.getByRole('button', { name: 'Create workspace' }).click();
|
||||||
await expect(modal.locator('[data-workspace-create-error]')).toContainText('Could not create the Deal. Please try again.');
|
await expect(modal.locator('[data-workspace-create-error]')).toContainText('invalid-workspace-name');
|
||||||
|
|
||||||
await modal.locator('[data-workspace-name]').fill('ProjectPlan');
|
await modal.locator('[data-workspace-name]').fill('ProjectPlan');
|
||||||
await modal.locator('[data-workspace-template]').selectOption('project');
|
await modal.locator('[data-workspace-template]').selectOption('project');
|
||||||
await expect(modal.locator('[data-workspace-template-description]')).toContainText('Project planning');
|
await expect(modal.locator('[data-workspace-template-description]')).toContainText('Project planning');
|
||||||
await expect(modal.locator('[data-workspace-template-tools]')).toContainText('Todos');
|
await expect(modal.locator('[data-workspace-template-tools]')).toContainText('Todos');
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
await modal.getByRole('button', { name: 'Create workspace' }).click();
|
||||||
|
|
||||||
await expect(page.locator('.wt-label').filter({ hasText: 'ProjectPlan' })).toBeVisible();
|
await expect(page.locator('.wt-label').filter({ hasText: 'ProjectPlan' })).toBeVisible();
|
||||||
await expect.poll(async () => page.evaluate(async () => {
|
await expect.poll(async () => page.evaluate(async () => {
|
||||||
|
|
@ -65,7 +63,7 @@ test.describe('Workspace templates', () => {
|
||||||
const modal = await openCreateModal(page);
|
const modal = await openCreateModal(page);
|
||||||
await modal.locator('[data-workspace-name]').fill('MinimalSpace');
|
await modal.locator('[data-workspace-name]').fill('MinimalSpace');
|
||||||
await modal.locator('[data-workspace-template]').selectOption('minimal');
|
await modal.locator('[data-workspace-template]').selectOption('minimal');
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
await modal.getByRole('button', { name: 'Create workspace' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('tab', { name: 'Overview' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: 'Overview' })).toBeVisible();
|
||||||
await expect(page.getByRole('tab', { name: 'Notes' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: 'Notes' })).toBeVisible();
|
||||||
|
|
@ -74,40 +72,21 @@ test.describe('Workspace templates', () => {
|
||||||
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveCount(0);
|
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveCount(0);
|
||||||
await expect(page.getByRole('tab', { name: 'Secrets' })).toHaveCount(0);
|
await expect(page.getByRole('tab', { name: 'Secrets' })).toHaveCount(0);
|
||||||
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Todos' })).toBeVisible();
|
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Todos' })).toBeVisible();
|
||||||
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser' })).toBeVisible();
|
await expect(page.locator('.sidebar .plugin-item').filter({ hasText: 'Browser Inbox' })).toBeVisible();
|
||||||
});
|
|
||||||
|
|
||||||
test('template explains an unavailable plugin and warns after incomplete creation', async ({ page }) => {
|
|
||||||
await page.evaluate(() => window.__wailsMock.setPluginStatus('verstak.todo', 'disabled', false));
|
|
||||||
const modal = await openCreateModal(page);
|
|
||||||
await modal.locator('[data-workspace-template]').selectOption('project');
|
|
||||||
|
|
||||||
const todo = modal.locator('[data-workspace-template-tool="verstak.todo"]');
|
|
||||||
await expect(todo).toContainText('Todos');
|
|
||||||
await expect(todo).toContainText('Plugin is disabled');
|
|
||||||
await expect(todo).toHaveAttribute('data-template-tool-status', 'unavailable');
|
|
||||||
|
|
||||||
await modal.locator('[data-workspace-name]').fill('ProjectWithWarning');
|
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
|
||||||
|
|
||||||
const warning = page.locator('[data-workspace-template-warning]');
|
|
||||||
await expect(warning).toContainText('ProjectWithWarning');
|
|
||||||
await expect(warning).toContainText('Todos');
|
|
||||||
await expect(warning).toContainText('Plugin is disabled');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Admin shows Secrets when available and missing workspace plugins degrade without breaking tabs', async ({ page }) => {
|
test('Admin shows Secrets when available and missing workspace plugins degrade without breaking tabs', async ({ page }) => {
|
||||||
let modal = await openCreateModal(page);
|
let modal = await openCreateModal(page);
|
||||||
await modal.locator('[data-workspace-name]').fill('AdminSpace');
|
await modal.locator('[data-workspace-name]').fill('AdminSpace');
|
||||||
await modal.locator('[data-workspace-template]').selectOption('admin');
|
await modal.locator('[data-workspace-template]').selectOption('admin');
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
await modal.getByRole('button', { name: 'Create workspace' }).click();
|
||||||
await expect(page.getByRole('tab', { name: 'Secrets' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: 'Secrets' })).toBeVisible();
|
||||||
|
|
||||||
await page.evaluate(() => window.__wailsMock.setPluginStatus('verstak.todo', 'disabled', false));
|
await page.evaluate(() => window.__wailsMock.setPluginStatus('verstak.todo', 'disabled', false));
|
||||||
modal = await openCreateModal(page);
|
modal = await openCreateModal(page);
|
||||||
await modal.locator('[data-workspace-name]').fill('ProjectWithoutTodo');
|
await modal.locator('[data-workspace-name]').fill('ProjectWithoutTodo');
|
||||||
await modal.locator('[data-workspace-template]').selectOption('project');
|
await modal.locator('[data-workspace-template]').selectOption('project');
|
||||||
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
await modal.getByRole('button', { name: 'Create workspace' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('tab', { name: 'Notes' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: 'Notes' })).toBeVisible();
|
||||||
await expect(page.getByRole('tab', { name: 'Files' })).toBeVisible();
|
await expect(page.getByRole('tab', { name: 'Files' })).toBeVisible();
|
||||||
|
|
|
||||||
|
|
@ -395,11 +395,7 @@
|
||||||
<VaultSelection />
|
<VaultSelection />
|
||||||
{:else}
|
{:else}
|
||||||
<main>
|
<main>
|
||||||
<Sidebar
|
<Sidebar showGlobalSearch={currentView !== 'workspace' && currentView !== 'workspace-empty'} />
|
||||||
showGlobalSearch={currentView !== 'workspace' && currentView !== 'workspace-empty'}
|
|
||||||
{activeView}
|
|
||||||
{activeViewPluginId}
|
|
||||||
/>
|
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
|
|
||||||
<section class="content-shell">
|
<section class="content-shell">
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ export default {
|
||||||
'sidebar.error.load': 'Failed to load sidebar',
|
'sidebar.error.load': 'Failed to load sidebar',
|
||||||
'sidebar.error.ui': 'Plugin UI error',
|
'sidebar.error.ui': 'Plugin UI error',
|
||||||
'workspace.overview': 'Overview',
|
'workspace.overview': 'Overview',
|
||||||
'workspace.search': 'Search in this Deal',
|
'workspace.search': 'Workspace search',
|
||||||
'workspace.tools': 'Deal tools',
|
'workspace.tools': 'Workspace tools',
|
||||||
'workspace.tool': 'Deal tool',
|
'workspace.tool': 'Workspace tool',
|
||||||
'workspace.emptyTools': 'No Deal tools available',
|
'workspace.emptyTools': 'No workspace tools available',
|
||||||
'workspace.emptyToolsHint': 'Enable plugins with Deal tools or open Plugin Manager from settings.',
|
'workspace.emptyToolsHint': 'Enable plugins with workspace tools or open Plugin Manager from settings.',
|
||||||
'workspace.select': 'Select a Deal',
|
'workspace.select': 'Select a workspace',
|
||||||
'workspace.selectHint': 'Use the + button in Deals to add your first project.',
|
'workspace.selectHint': 'Use the + button in Workspaces to add your first project.',
|
||||||
'common.details': 'Details',
|
'common.details': 'Details',
|
||||||
'common.plugin': 'Plugin',
|
'common.plugin': 'Plugin',
|
||||||
'common.component': 'Component',
|
'common.component': 'Component',
|
||||||
|
|
@ -35,7 +35,6 @@ export default {
|
||||||
'pluginView.select': 'Select a plugin view from the sidebar',
|
'pluginView.select': 'Select a plugin view from the sidebar',
|
||||||
'pluginView.selectHint': 'Plugin views will appear here',
|
'pluginView.selectHint': 'Plugin views will appear here',
|
||||||
'common.retry': '⟳ Retry',
|
'common.retry': '⟳ Retry',
|
||||||
'common.save': 'Save',
|
|
||||||
'common.source': 'Source',
|
'common.source': 'Source',
|
||||||
'common.status': 'Status',
|
'common.status': 'Status',
|
||||||
'common.capability': 'Capability',
|
'common.capability': 'Capability',
|
||||||
|
|
@ -55,24 +54,6 @@ export default {
|
||||||
'pluginManager.health': 'Plugin Health',
|
'pluginManager.health': 'Plugin Health',
|
||||||
'pluginManager.permissionRisk': 'Permission Risk',
|
'pluginManager.permissionRisk': 'Permission Risk',
|
||||||
'pluginManager.elevatedPermissions': '{count} plugins request elevated permissions',
|
'pluginManager.elevatedPermissions': '{count} plugins request elevated permissions',
|
||||||
'pluginManager.filters': 'Filters',
|
|
||||||
'pluginManager.filterResults': 'Showing {visible} of {total} plugins',
|
|
||||||
'pluginManager.filterReset': 'Reset filters',
|
|
||||||
'pluginManager.filterState': 'State',
|
|
||||||
'pluginManager.filterAll': 'All',
|
|
||||||
'pluginManager.filterEnabled': 'Enabled',
|
|
||||||
'pluginManager.filterDisabled': 'Disabled',
|
|
||||||
'pluginManager.filterFailed': 'Problem',
|
|
||||||
'pluginManager.filterDegraded': 'Degraded',
|
|
||||||
'pluginManager.filterPermissions': 'Permissions',
|
|
||||||
'pluginManager.filterCapabilities': 'Capabilities',
|
|
||||||
'pluginManager.filterSettings': 'Settings',
|
|
||||||
'pluginManager.filterWithSettings': 'With settings',
|
|
||||||
'pluginManager.filterWithoutSettings': 'Without settings',
|
|
||||||
'pluginManager.filterSource': 'Source',
|
|
||||||
'pluginManager.filterOfficial': 'Official',
|
|
||||||
'pluginManager.filterThirdParty': 'Third-party or local',
|
|
||||||
'pluginManager.filterEmpty': 'No plugins match the selected filters.',
|
|
||||||
'pluginManager.none': 'No plugins found',
|
'pluginManager.none': 'No plugins found',
|
||||||
'pluginManager.scannedDirs': 'Plugin directories scanned:',
|
'pluginManager.scannedDirs': 'Plugin directories scanned:',
|
||||||
'pluginManager.userPlugins': 'user plugins',
|
'pluginManager.userPlugins': 'user plugins',
|
||||||
|
|
@ -93,7 +74,7 @@ export default {
|
||||||
'pluginCard.count.sidebar': '{count} sidebar item(s)',
|
'pluginCard.count.sidebar': '{count} sidebar item(s)',
|
||||||
'pluginCard.count.statusbar': '{count} status bar item(s)',
|
'pluginCard.count.statusbar': '{count} status bar item(s)',
|
||||||
'pluginCard.count.openProviders': '{count} openProviders',
|
'pluginCard.count.openProviders': '{count} openProviders',
|
||||||
'pluginCard.count.workspace': '{count} Deal tool(s)',
|
'pluginCard.count.workspace': '{count} workspace item(s)',
|
||||||
'pluginCard.degraded': 'Plugin is usable, but some optional capabilities are unavailable.',
|
'pluginCard.degraded': 'Plugin is usable, but some optional capabilities are unavailable.',
|
||||||
'pluginCard.name': 'Name',
|
'pluginCard.name': 'Name',
|
||||||
'pluginCard.contributions': 'Contributions',
|
'pluginCard.contributions': 'Contributions',
|
||||||
|
|
@ -115,12 +96,12 @@ export default {
|
||||||
'common.loading': 'Loading...',
|
'common.loading': 'Loading...',
|
||||||
'common.browse': 'Browse...',
|
'common.browse': 'Browse...',
|
||||||
'vaultSelection.chooseNew': 'Choose or enter a folder for the new vault.',
|
'vaultSelection.chooseNew': 'Choose or enter a folder for the new vault.',
|
||||||
'vaultSelection.createError': 'Could not create the vault. Please try again.',
|
'vaultSelection.createError': 'Could not create vault: {error}',
|
||||||
'vaultSelection.openError': 'Could not open the vault. Please try again.',
|
'vaultSelection.openError': 'Could not open vault: {error}',
|
||||||
'vaultSelection.chooseExisting': 'Choose or enter an existing vault.',
|
'vaultSelection.chooseExisting': 'Choose or enter an existing vault.',
|
||||||
'vaultSelection.subtitle': 'Choose a vault to start working',
|
'vaultSelection.subtitle': 'Choose a vault to start working',
|
||||||
'vaultSelection.createTitle': 'Create a new vault',
|
'vaultSelection.createTitle': 'Create a new vault',
|
||||||
'vaultSelection.createHint': 'Create a local vault folder for Deals and projects.',
|
'vaultSelection.createHint': 'Create a local vault folder for workspaces and projects.',
|
||||||
'vaultSelection.pathPlaceholder': 'Choose or enter a path...',
|
'vaultSelection.pathPlaceholder': 'Choose or enter a path...',
|
||||||
'vaultSelection.creating': 'Creating...',
|
'vaultSelection.creating': 'Creating...',
|
||||||
'vaultSelection.create': 'Create vault',
|
'vaultSelection.create': 'Create vault',
|
||||||
|
|
@ -131,45 +112,21 @@ export default {
|
||||||
'vaultSelection.recent': 'Recent vaults',
|
'vaultSelection.recent': 'Recent vaults',
|
||||||
'common.cancel': 'Cancel',
|
'common.cancel': 'Cancel',
|
||||||
'common.close': 'Close',
|
'common.close': 'Close',
|
||||||
'workspaceTree.title': 'Deals',
|
'workspaceTree.title': 'Workspaces',
|
||||||
'workspaceTree.new': 'New Deal',
|
'workspaceTree.new': 'New workspace',
|
||||||
'workspaceTree.nameRequired': 'Name is required',
|
'workspaceTree.nameRequired': 'Name is required',
|
||||||
'workspaceTree.chooseTemplate': 'Choose a Deal template',
|
'workspaceTree.chooseTemplate': 'Choose a workspace template',
|
||||||
'workspaceTree.saveRename': 'Save rename',
|
'workspaceTree.saveRename': 'Save rename',
|
||||||
'workspaceTree.rename': 'Rename Deal',
|
'workspaceTree.rename': 'Rename workspace',
|
||||||
'workspaceTree.trash': 'Move Deal to trash',
|
'workspaceTree.trash': 'Trash workspace',
|
||||||
'workspaceTree.create': 'Create Deal',
|
'workspaceTree.create': 'Create workspace',
|
||||||
'workspaceTree.namePlaceholder': 'Deal name',
|
'workspaceTree.namePlaceholder': 'Workspace name',
|
||||||
'workspaceTree.template': 'Template',
|
'workspaceTree.template': 'Template',
|
||||||
'workspaceTree.creating': 'Creating...',
|
'workspaceTree.creating': 'Creating...',
|
||||||
'workspaceTree.templateAvailable': 'Available',
|
|
||||||
'workspaceTree.templateLimited': 'Available with limitations',
|
|
||||||
'workspaceTree.templateMissingPlugin': 'Plugin is not installed',
|
|
||||||
'workspaceTree.templatePluginDisabled': 'Plugin is disabled',
|
|
||||||
'workspaceTree.templateCapabilityUnavailable': 'A required capability is unavailable',
|
|
||||||
'workspaceTree.templateIncompatible': 'Plugin is incompatible with this version of Verstak',
|
|
||||||
'workspaceTree.templateLoadFailed': 'Plugin failed to load',
|
|
||||||
'workspaceTree.templateNotReady': 'Plugin is not ready',
|
|
||||||
'workspaceTree.templateToolTabs': 'Creates: {tabs}',
|
|
||||||
'workspaceTree.templateToolNoTabs': 'No Deal tab provided',
|
|
||||||
'workspaceTree.templateWillBeIncomplete': 'This Deal will be created without the unavailable components.',
|
|
||||||
'workspaceTree.templateIncompleteCreated': '«{name}» was created with unavailable components.',
|
|
||||||
'workspaceTree.templatesError': 'Could not load Deal templates. Please try again.',
|
|
||||||
'workspaceTree.loadError': 'Could not load Deals. Please try again.',
|
|
||||||
'workspaceTree.selectError': 'Could not select this Deal. Please try again.',
|
|
||||||
'workspaceTree.createError': 'Could not create the Deal. Please try again.',
|
|
||||||
'workspaceTree.renameError': 'Could not rename the Deal. Please try again.',
|
|
||||||
'workspaceTree.trashError': 'Could not move the Deal to trash. Please try again.',
|
|
||||||
'pluginManager.loadError': 'Could not load plugins. Please try again.',
|
|
||||||
'pluginManager.reloadError': 'Could not reload plugins. Please try again.',
|
|
||||||
'pluginManager.enableError': 'Could not enable the plugin. Please try again.',
|
|
||||||
'pluginManager.disableError': 'Could not disable the plugin. Please try again.',
|
|
||||||
'pluginManager.settingsLoadError': 'Could not load plugin settings. Please try again.',
|
|
||||||
'pluginManager.settingsUnavailable': 'Plugin settings are unavailable.',
|
|
||||||
'command.openOverview': 'Open Overview',
|
'command.openOverview': 'Open Overview',
|
||||||
'command.openFiles': 'Open Files',
|
'command.openFiles': 'Open Files',
|
||||||
'command.openActivity': 'Open Activity',
|
'command.openActivity': 'Open Activity',
|
||||||
'command.openBrowserInbox': 'Open Browser',
|
'command.openBrowserInbox': 'Open Browser Inbox',
|
||||||
'command.createMarkdown': 'Create Markdown File',
|
'command.createMarkdown': 'Create Markdown File',
|
||||||
'command.createText': 'Create Text File',
|
'command.createText': 'Create Text File',
|
||||||
'command.syncNow': 'Sync Now',
|
'command.syncNow': 'Sync Now',
|
||||||
|
|
@ -177,7 +134,6 @@ export default {
|
||||||
'command.openPluginManager': 'Open Plugin Manager',
|
'command.openPluginManager': 'Open Plugin Manager',
|
||||||
'command.handled': '{title} handled',
|
'command.handled': '{title} handled',
|
||||||
'command.result': '{title} {status}',
|
'command.result': '{title} {status}',
|
||||||
'command.failed': 'Could not run {title}. Please try again.',
|
|
||||||
'command.statusHandled': 'handled',
|
'command.statusHandled': 'handled',
|
||||||
'command.palette': 'Command Palette',
|
'command.palette': 'Command Palette',
|
||||||
'command.run': 'Run command',
|
'command.run': 'Run command',
|
||||||
|
|
@ -186,26 +142,26 @@ export default {
|
||||||
'search.placeholder': 'Search',
|
'search.placeholder': 'Search',
|
||||||
'search.global': 'Global search',
|
'search.global': 'Global search',
|
||||||
'search.noResults': 'No results',
|
'search.noResults': 'No results',
|
||||||
'search.type.workspace': 'Deal',
|
'search.type.workspace': 'Workspace',
|
||||||
'search.type.tool': 'Tool',
|
'search.type.tool': 'Tool',
|
||||||
'search.type.folder': 'Folder',
|
'search.type.folder': 'Folder',
|
||||||
'search.type.file': 'File',
|
'search.type.file': 'File',
|
||||||
'search.type.journal': 'Journal',
|
'search.type.journal': 'Journal',
|
||||||
'search.type.browserInbox': 'Browser',
|
'search.type.browserInbox': 'Browser Inbox',
|
||||||
'search.type.activity': 'Activity',
|
'search.type.activity': 'Activity',
|
||||||
'workbench.noProvider': 'No provider',
|
'workbench.noProvider': 'No provider',
|
||||||
'workbench.noViewer': 'No viewer/editor available',
|
'workbench.noViewer': 'No viewer/editor available',
|
||||||
'workbench.noResource': 'No resource opened',
|
'workbench.noResource': 'No resource opened',
|
||||||
'bundle.noFrontend': 'Plugin has no frontend bundle',
|
'bundle.noFrontend': 'Plugin has no frontend bundle',
|
||||||
'bundle.notFound': 'Plugin not found',
|
'bundle.notFound': 'Plugin not found',
|
||||||
'bundle.loadFailed': 'Could not load the plugin interface. Please try again.',
|
'bundle.loadFailed': 'Failed to load bundle: {error}',
|
||||||
'bundle.emptyContent': 'empty content',
|
'bundle.emptyContent': 'empty content',
|
||||||
'bundle.executionError': 'Could not start the plugin interface. Please try again.',
|
'bundle.executionError': 'Bundle execution error: {error}',
|
||||||
'bundle.registrationMissing': 'Bundle loaded but no VerstakPluginRegister call detected',
|
'bundle.registrationMissing': 'Bundle loaded but no VerstakPluginRegister call detected',
|
||||||
'bundle.componentMissing': 'The requested plugin interface is unavailable.',
|
'bundle.componentMissing': 'Component "{component}" not found in bundle. Available: {available}',
|
||||||
'bundle.mountError': 'Could not open the plugin interface. Please try again.',
|
'bundle.mountError': 'Component mount error: {error}',
|
||||||
'bundle.mountUnavailable': 'Mount container not available',
|
'bundle.mountUnavailable': 'Mount container not available',
|
||||||
'bundle.unexpectedError': 'Could not open the plugin interface. Please try again.',
|
'bundle.unexpectedError': 'Unexpected error: {error}',
|
||||||
'bundle.unknownError': 'Unknown error',
|
'bundle.unknownError': 'Unknown error',
|
||||||
'bundle.frontendEntry': 'Frontend entry',
|
'bundle.frontendEntry': 'Frontend entry',
|
||||||
'bundle.availableComponents': 'Available components',
|
'bundle.availableComponents': 'Available components',
|
||||||
|
|
@ -241,57 +197,18 @@ export default {
|
||||||
'overview.count.journal.many': '{count} journal entries',
|
'overview.count.journal.many': '{count} journal entries',
|
||||||
'overview.count.pending.one': '{count} pending item',
|
'overview.count.pending.one': '{count} pending item',
|
||||||
'overview.count.pending.many': '{count} pending items',
|
'overview.count.pending.many': '{count} pending items',
|
||||||
'overview.loadingContext': 'Loading Deal context...',
|
'overview.loadingContext': 'Loading workspace context...',
|
||||||
'overview.lastActive': 'Last active {time}',
|
'overview.lastActive': 'Last active {time}',
|
||||||
'overview.noRecentActivity': 'No recent Deal activity',
|
'overview.noRecentActivity': 'No recent workspace activity',
|
||||||
'overview.refresh': 'Refresh',
|
'overview.refresh': 'Refresh',
|
||||||
'overview.summary': 'Deal overview summary',
|
'overview.summary': 'Workspace overview summary',
|
||||||
'overview.continue': 'Continue working',
|
'overview.continue': 'Continue working',
|
||||||
'overview.continueHint': 'Pick up the next useful item in this Deal.',
|
'overview.continueHint': 'Pick up the next useful item in this workspace.',
|
||||||
'overview.loadingSignals': 'Loading Deal signals...',
|
'overview.loadingSignals': 'Loading workspace signals...',
|
||||||
'overview.noResume': 'No clear resume point yet',
|
'overview.noResume': 'No clear resume point yet',
|
||||||
'overview.noResumeHint': 'Recent notes, files, captures, and journal entries will appear here.',
|
'overview.noResumeHint': 'Recent notes, files, captures, and journal entries will appear here.',
|
||||||
'overview.recentChanges': 'Recent changes',
|
'overview.recentChanges': 'Recent changes',
|
||||||
'overview.recentChangesHint': 'Latest meaningful activity in this Deal.',
|
'overview.recentChangesHint': 'Latest meaningful activity in this workspace.',
|
||||||
'overview.event.workspaceOpened': 'Opened Deal',
|
|
||||||
'overview.event.activity': 'Deal activity',
|
|
||||||
'overview.event.noteEdited': 'Edited note "{name}"',
|
|
||||||
'overview.event.noteOpened': 'Opened note "{name}"',
|
|
||||||
'overview.event.noteCreated': 'Created note "{name}"',
|
|
||||||
'overview.event.fileOpened': 'Opened file "{name}"',
|
|
||||||
'overview.event.fileChanged': 'Changed file "{name}"',
|
|
||||||
'overview.event.fileCreated': 'Created file "{name}"',
|
|
||||||
'overview.event.fileRemoved': 'Removed file "{name}"',
|
|
||||||
'overview.event.capturePage': 'Captured page "{name}"',
|
|
||||||
'overview.event.captureSelection': 'Captured selection "{name}"',
|
|
||||||
'overview.event.captureLink': 'Captured link "{name}"',
|
|
||||||
'overview.event.captureFile': 'Captured file "{name}"',
|
|
||||||
'overview.event.captureConverted': 'Converted capture "{name}"',
|
|
||||||
'overview.event.capture': 'Captured {kind} "{title}"',
|
|
||||||
'overview.event.journalAdded': 'Added journal entry "{name}"',
|
|
||||||
'overview.event.workSessionDetected': 'Work session detected',
|
|
||||||
'overview.event.todo': 'Todo "{title}"',
|
|
||||||
'overview.event.reviewCapture': 'Review capture "{title}"',
|
|
||||||
'overview.event.continueJournal': 'Continue journal entry "{title}"',
|
|
||||||
'overview.event.possibleJournalEntry': 'Possible journal entry',
|
|
||||||
'overview.captureKind.page': 'page',
|
|
||||||
'overview.captureKind.selection': 'selection',
|
|
||||||
'overview.captureKind.link': 'link',
|
|
||||||
'overview.captureKind.file': 'file',
|
|
||||||
'overview.captureKind.item': 'item',
|
|
||||||
'overview.todo.reminderDue': 'Reminder due',
|
|
||||||
'overview.todo.overdue': 'Overdue',
|
|
||||||
'overview.todo.dueSoon': 'Due soon',
|
|
||||||
'overview.todo.due': 'Due {date}',
|
|
||||||
'overview.candidateMeta': 'Deal: {deal} · {minutes} min · {activities} activities',
|
|
||||||
'overview.minutes': '{count} min',
|
|
||||||
'overview.browserCapture': 'Browser capture',
|
|
||||||
'overview.untitledTodo': 'Untitled todo',
|
|
||||||
'overview.untitledCapture': 'Untitled capture',
|
|
||||||
'overview.untitledJournal': 'Journal entry',
|
|
||||||
'overview.unknownDeal': 'Unknown Deal',
|
|
||||||
'overview.item': 'item',
|
|
||||||
'overview.overviewNote': 'Deal overview note',
|
|
||||||
'overview.recentFilter': 'Recent changes filter',
|
'overview.recentFilter': 'Recent changes filter',
|
||||||
'overview.loadingRecent': 'Loading recent changes...',
|
'overview.loadingRecent': 'Loading recent changes...',
|
||||||
'overview.noChanges': 'No meaningful changes for this filter yet.',
|
'overview.noChanges': 'No meaningful changes for this filter yet.',
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ export default {
|
||||||
'sidebar.error.load': 'Не удалось загрузить боковую панель',
|
'sidebar.error.load': 'Не удалось загрузить боковую панель',
|
||||||
'sidebar.error.ui': 'Ошибка интерфейса плагина',
|
'sidebar.error.ui': 'Ошибка интерфейса плагина',
|
||||||
'workspace.overview': 'Обзор',
|
'workspace.overview': 'Обзор',
|
||||||
'workspace.search': 'Поиск в Деле',
|
'workspace.search': 'Поиск в рабочем пространстве',
|
||||||
'workspace.tools': 'Инструменты Дела',
|
'workspace.tools': 'Инструменты рабочего пространства',
|
||||||
'workspace.tool': 'Инструмент Дела',
|
'workspace.tool': 'Инструмент рабочего пространства',
|
||||||
'workspace.emptyTools': 'Нет доступных инструментов',
|
'workspace.emptyTools': 'Нет доступных инструментов',
|
||||||
'workspace.emptyToolsHint': 'Включите плагины с инструментами Дела или откройте Менеджер плагинов в настройках.',
|
'workspace.emptyToolsHint': 'Включите плагины с инструментами рабочего пространства или откройте Менеджер плагинов в настройках.',
|
||||||
'workspace.select': 'Выберите Дело',
|
'workspace.select': 'Выберите рабочее пространство',
|
||||||
'workspace.selectHint': 'Используйте кнопку + в разделе Дел, чтобы добавить первый проект.',
|
'workspace.selectHint': 'Используйте кнопку + в разделе рабочих пространств, чтобы добавить первый проект.',
|
||||||
'common.details': 'Подробнее',
|
'common.details': 'Подробнее',
|
||||||
'common.plugin': 'Плагин',
|
'common.plugin': 'Плагин',
|
||||||
'common.component': 'Компонент',
|
'common.component': 'Компонент',
|
||||||
|
|
@ -35,10 +35,9 @@ export default {
|
||||||
'pluginView.select': 'Выберите представление плагина на боковой панели',
|
'pluginView.select': 'Выберите представление плагина на боковой панели',
|
||||||
'pluginView.selectHint': 'Представления плагинов появятся здесь',
|
'pluginView.selectHint': 'Представления плагинов появятся здесь',
|
||||||
'common.retry': '⟳ Повторить',
|
'common.retry': '⟳ Повторить',
|
||||||
'common.save': 'Сохранить',
|
|
||||||
'common.source': 'Источник',
|
'common.source': 'Источник',
|
||||||
'common.status': 'Состояние',
|
'common.status': 'Состояние',
|
||||||
'common.capability': 'Возможность',
|
'common.capability': 'Capability',
|
||||||
'common.provider': 'Поставщик',
|
'common.provider': 'Поставщик',
|
||||||
'status.loaded': 'загружено',
|
'status.loaded': 'загружено',
|
||||||
'status.degraded': 'ограничено',
|
'status.degraded': 'ограничено',
|
||||||
|
|
@ -49,30 +48,12 @@ export default {
|
||||||
'pluginManager.reload': '⟳ Обновить',
|
'pluginManager.reload': '⟳ Обновить',
|
||||||
'pluginManager.scanning': 'Сканирование каталогов плагинов...',
|
'pluginManager.scanning': 'Сканирование каталогов плагинов...',
|
||||||
'pluginManager.summary.plugins': 'Обнаружено плагинов: {count}',
|
'pluginManager.summary.plugins': 'Обнаружено плагинов: {count}',
|
||||||
'pluginManager.summary.capabilities': 'Зарегистрировано возможностей: {count}',
|
'pluginManager.summary.capabilities': 'Зарегистрировано capabilities: {count}',
|
||||||
'pluginManager.summary.permissions': 'Известно разрешений: {count}',
|
'pluginManager.summary.permissions': 'Известно разрешений: {count}',
|
||||||
'pluginManager.scanSummary': 'Сводка проверки плагинов',
|
'pluginManager.scanSummary': 'Сводка проверки плагинов',
|
||||||
'pluginManager.health': 'Состояние плагинов',
|
'pluginManager.health': 'Состояние плагинов',
|
||||||
'pluginManager.permissionRisk': 'Риск разрешений',
|
'pluginManager.permissionRisk': 'Риск разрешений',
|
||||||
'pluginManager.elevatedPermissions': 'Плагинов с повышенными разрешениями: {count}',
|
'pluginManager.elevatedPermissions': 'Плагинов с повышенными разрешениями: {count}',
|
||||||
'pluginManager.filters': 'Фильтры',
|
|
||||||
'pluginManager.filterResults': 'Показано плагинов: {visible} из {total}',
|
|
||||||
'pluginManager.filterReset': 'Сбросить фильтры',
|
|
||||||
'pluginManager.filterState': 'Состояние',
|
|
||||||
'pluginManager.filterAll': 'Все',
|
|
||||||
'pluginManager.filterEnabled': 'Включённые',
|
|
||||||
'pluginManager.filterDisabled': 'Выключенные',
|
|
||||||
'pluginManager.filterFailed': 'С проблемами',
|
|
||||||
'pluginManager.filterDegraded': 'Ограниченные',
|
|
||||||
'pluginManager.filterPermissions': 'Разрешения',
|
|
||||||
'pluginManager.filterCapabilities': 'Возможности',
|
|
||||||
'pluginManager.filterSettings': 'Настройки',
|
|
||||||
'pluginManager.filterWithSettings': 'С настройками',
|
|
||||||
'pluginManager.filterWithoutSettings': 'Без настроек',
|
|
||||||
'pluginManager.filterSource': 'Источник',
|
|
||||||
'pluginManager.filterOfficial': 'Официальные',
|
|
||||||
'pluginManager.filterThirdParty': 'Сторонние или локальные',
|
|
||||||
'pluginManager.filterEmpty': 'Нет плагинов, соответствующих выбранным фильтрам.',
|
|
||||||
'pluginManager.none': 'Плагины не найдены',
|
'pluginManager.none': 'Плагины не найдены',
|
||||||
'pluginManager.scannedDirs': 'Проверенные каталоги плагинов:',
|
'pluginManager.scannedDirs': 'Проверенные каталоги плагинов:',
|
||||||
'pluginManager.userPlugins': 'пользовательские плагины',
|
'pluginManager.userPlugins': 'пользовательские плагины',
|
||||||
|
|
@ -81,7 +62,7 @@ export default {
|
||||||
'pluginManager.missingTitle': 'Отсутствующие установленные плагины',
|
'pluginManager.missingTitle': 'Отсутствующие установленные плагины',
|
||||||
'pluginManager.missingHint': 'Эти плагины требуются хранилищу, но их пакеты не установлены на этом устройстве.',
|
'pluginManager.missingHint': 'Эти плагины требуются хранилищу, но их пакеты не установлены на этом устройстве.',
|
||||||
'pluginManager.missingPackage': 'Плагин указан среди желаемых плагинов хранилища, но его пакет не установлен.',
|
'pluginManager.missingPackage': 'Плагин указан среди желаемых плагинов хранилища, но его пакет не установлен.',
|
||||||
'pluginManager.capabilityRegistry': 'Реестр возможностей ({count})',
|
'pluginManager.capabilityRegistry': 'Реестр capabilities ({count})',
|
||||||
'pluginManager.settingsError': 'Ошибка настроек',
|
'pluginManager.settingsError': 'Ошибка настроек',
|
||||||
'pluginManager.pluginSettings': 'Настройки плагина',
|
'pluginManager.pluginSettings': 'Настройки плагина',
|
||||||
'pluginManager.settingsBundleUnavailable': 'Frontend bundle панели настроек недоступен',
|
'pluginManager.settingsBundleUnavailable': 'Frontend bundle панели настроек недоступен',
|
||||||
|
|
@ -93,8 +74,8 @@ export default {
|
||||||
'pluginCard.count.sidebar': 'элементов боковой панели: {count}',
|
'pluginCard.count.sidebar': 'элементов боковой панели: {count}',
|
||||||
'pluginCard.count.statusbar': 'элементов строки состояния: {count}',
|
'pluginCard.count.statusbar': 'элементов строки состояния: {count}',
|
||||||
'pluginCard.count.openProviders': 'обработчиков открытия: {count}',
|
'pluginCard.count.openProviders': 'обработчиков открытия: {count}',
|
||||||
'pluginCard.count.workspace': 'инструментов Дела: {count}',
|
'pluginCard.count.workspace': 'инструментов рабочего пространства: {count}',
|
||||||
'pluginCard.degraded': 'Плагин работает, но некоторые необязательные возможности недоступны.',
|
'pluginCard.degraded': 'Плагин работает, но некоторые необязательные capabilities недоступны.',
|
||||||
'pluginCard.name': 'Название',
|
'pluginCard.name': 'Название',
|
||||||
'pluginCard.contributions': 'Элементы интерфейса',
|
'pluginCard.contributions': 'Элементы интерфейса',
|
||||||
'pluginCard.technicalDetails': 'Технические сведения',
|
'pluginCard.technicalDetails': 'Технические сведения',
|
||||||
|
|
@ -102,9 +83,9 @@ export default {
|
||||||
'pluginCard.root': 'Корневой каталог',
|
'pluginCard.root': 'Корневой каталог',
|
||||||
'pluginCard.provides': 'Предоставляет',
|
'pluginCard.provides': 'Предоставляет',
|
||||||
'pluginCard.requires': 'Требует',
|
'pluginCard.requires': 'Требует',
|
||||||
'pluginCard.missingRequired': 'Отсутствуют обязательные возможности',
|
'pluginCard.missingRequired': 'Отсутствуют обязательные capabilities',
|
||||||
'pluginCard.optionalRequires': 'Необязательные зависимости',
|
'pluginCard.optionalRequires': 'Необязательные зависимости',
|
||||||
'pluginCard.optionalUnavailable': 'Необязательные возможности недоступны — плагин работает в ограниченном режиме',
|
'pluginCard.optionalUnavailable': 'Необязательные capabilities недоступны — плагин работает в ограниченном режиме',
|
||||||
'pluginCard.permissions': 'Разрешения',
|
'pluginCard.permissions': 'Разрешения',
|
||||||
'pluginCard.enabling': '⟳ Включение...',
|
'pluginCard.enabling': '⟳ Включение...',
|
||||||
'pluginCard.enable': '▶ Включить',
|
'pluginCard.enable': '▶ Включить',
|
||||||
|
|
@ -115,12 +96,12 @@ export default {
|
||||||
'common.loading': 'Загрузка...',
|
'common.loading': 'Загрузка...',
|
||||||
'common.browse': 'Обзор...',
|
'common.browse': 'Обзор...',
|
||||||
'vaultSelection.chooseNew': 'Выберите или введите папку для нового хранилища.',
|
'vaultSelection.chooseNew': 'Выберите или введите папку для нового хранилища.',
|
||||||
'vaultSelection.createError': 'Не удалось создать хранилище. Повторите попытку.',
|
'vaultSelection.createError': 'Не удалось создать хранилище: {error}',
|
||||||
'vaultSelection.openError': 'Не удалось открыть хранилище. Повторите попытку.',
|
'vaultSelection.openError': 'Не удалось открыть хранилище: {error}',
|
||||||
'vaultSelection.chooseExisting': 'Выберите или введите существующее хранилище.',
|
'vaultSelection.chooseExisting': 'Выберите или введите существующее хранилище.',
|
||||||
'vaultSelection.subtitle': 'Выберите хранилище, чтобы начать работу',
|
'vaultSelection.subtitle': 'Выберите хранилище, чтобы начать работу',
|
||||||
'vaultSelection.createTitle': 'Создать новое хранилище',
|
'vaultSelection.createTitle': 'Создать новое хранилище',
|
||||||
'vaultSelection.createHint': 'Создайте локальную папку хранилища для Дел и проектов.',
|
'vaultSelection.createHint': 'Создайте локальную папку хранилища для рабочих пространств и проектов.',
|
||||||
'vaultSelection.pathPlaceholder': 'Выберите или введите путь...',
|
'vaultSelection.pathPlaceholder': 'Выберите или введите путь...',
|
||||||
'vaultSelection.creating': 'Создание...',
|
'vaultSelection.creating': 'Создание...',
|
||||||
'vaultSelection.create': 'Создать хранилище',
|
'vaultSelection.create': 'Создать хранилище',
|
||||||
|
|
@ -131,45 +112,21 @@ export default {
|
||||||
'vaultSelection.recent': 'Недавние хранилища',
|
'vaultSelection.recent': 'Недавние хранилища',
|
||||||
'common.cancel': 'Отмена',
|
'common.cancel': 'Отмена',
|
||||||
'common.close': 'Закрыть',
|
'common.close': 'Закрыть',
|
||||||
'workspaceTree.title': 'Дела',
|
'workspaceTree.title': 'Рабочие пространства',
|
||||||
'workspaceTree.new': 'Новое Дело',
|
'workspaceTree.new': 'Новое рабочее пространство',
|
||||||
'workspaceTree.nameRequired': 'Введите название',
|
'workspaceTree.nameRequired': 'Введите название',
|
||||||
'workspaceTree.chooseTemplate': 'Выберите шаблон Дела',
|
'workspaceTree.chooseTemplate': 'Выберите шаблон рабочего пространства',
|
||||||
'workspaceTree.saveRename': 'Сохранить новое название',
|
'workspaceTree.saveRename': 'Сохранить новое название',
|
||||||
'workspaceTree.rename': 'Переименовать Дело',
|
'workspaceTree.rename': 'Переименовать рабочее пространство',
|
||||||
'workspaceTree.trash': 'Переместить Дело в корзину',
|
'workspaceTree.trash': 'Переместить рабочее пространство в корзину',
|
||||||
'workspaceTree.create': 'Создать Дело',
|
'workspaceTree.create': 'Создать рабочее пространство',
|
||||||
'workspaceTree.namePlaceholder': 'Название Дела',
|
'workspaceTree.namePlaceholder': 'Название рабочего пространства',
|
||||||
'workspaceTree.template': 'Шаблон',
|
'workspaceTree.template': 'Шаблон',
|
||||||
'workspaceTree.creating': 'Создание...',
|
'workspaceTree.creating': 'Создание...',
|
||||||
'workspaceTree.templateAvailable': 'Доступен',
|
|
||||||
'workspaceTree.templateLimited': 'Доступен с ограничениями',
|
|
||||||
'workspaceTree.templateMissingPlugin': 'Плагин не установлен',
|
|
||||||
'workspaceTree.templatePluginDisabled': 'Плагин выключен',
|
|
||||||
'workspaceTree.templateCapabilityUnavailable': 'Не хватает обязательной возможности',
|
|
||||||
'workspaceTree.templateIncompatible': 'Плагин несовместим с этой версией Верстака',
|
|
||||||
'workspaceTree.templateLoadFailed': 'Плагин не удалось загрузить',
|
|
||||||
'workspaceTree.templateNotReady': 'Плагин ещё не готов к работе',
|
|
||||||
'workspaceTree.templateToolTabs': 'Создаёт вкладки: {tabs}',
|
|
||||||
'workspaceTree.templateToolNoTabs': 'Не предоставляет вкладку Дела',
|
|
||||||
'workspaceTree.templateWillBeIncomplete': 'Дело будет создано без недоступных компонентов.',
|
|
||||||
'workspaceTree.templateIncompleteCreated': 'Дело «{name}» создано с недоступными компонентами.',
|
|
||||||
'workspaceTree.templatesError': 'Не удалось загрузить шаблоны Дел. Повторите попытку.',
|
|
||||||
'workspaceTree.loadError': 'Не удалось загрузить Дела. Повторите попытку.',
|
|
||||||
'workspaceTree.selectError': 'Не удалось выбрать это Дело. Повторите попытку.',
|
|
||||||
'workspaceTree.createError': 'Не удалось создать Дело. Повторите попытку.',
|
|
||||||
'workspaceTree.renameError': 'Не удалось переименовать Дело. Повторите попытку.',
|
|
||||||
'workspaceTree.trashError': 'Не удалось переместить Дело в корзину. Повторите попытку.',
|
|
||||||
'pluginManager.loadError': 'Не удалось загрузить плагины. Повторите попытку.',
|
|
||||||
'pluginManager.reloadError': 'Не удалось перезагрузить плагины. Повторите попытку.',
|
|
||||||
'pluginManager.enableError': 'Не удалось включить плагин. Повторите попытку.',
|
|
||||||
'pluginManager.disableError': 'Не удалось выключить плагин. Повторите попытку.',
|
|
||||||
'pluginManager.settingsLoadError': 'Не удалось загрузить настройки плагина. Повторите попытку.',
|
|
||||||
'pluginManager.settingsUnavailable': 'Настройки плагина недоступны.',
|
|
||||||
'command.openOverview': 'Открыть обзор',
|
'command.openOverview': 'Открыть обзор',
|
||||||
'command.openFiles': 'Открыть файлы',
|
'command.openFiles': 'Открыть файлы',
|
||||||
'command.openActivity': 'Открыть активность',
|
'command.openActivity': 'Открыть активность',
|
||||||
'command.openBrowserInbox': 'Открыть Браузер',
|
'command.openBrowserInbox': 'Открыть входящие из браузера',
|
||||||
'command.createMarkdown': 'Создать файл Markdown',
|
'command.createMarkdown': 'Создать файл Markdown',
|
||||||
'command.createText': 'Создать текстовый файл',
|
'command.createText': 'Создать текстовый файл',
|
||||||
'command.syncNow': 'Синхронизировать сейчас',
|
'command.syncNow': 'Синхронизировать сейчас',
|
||||||
|
|
@ -177,7 +134,6 @@ export default {
|
||||||
'command.openPluginManager': 'Открыть менеджер плагинов',
|
'command.openPluginManager': 'Открыть менеджер плагинов',
|
||||||
'command.handled': 'Выполнено: {title}',
|
'command.handled': 'Выполнено: {title}',
|
||||||
'command.result': '{title}: {status}',
|
'command.result': '{title}: {status}',
|
||||||
'command.failed': 'Не удалось выполнить команду «{title}». Повторите попытку.',
|
|
||||||
'command.statusHandled': 'выполнено',
|
'command.statusHandled': 'выполнено',
|
||||||
'command.palette': 'Палитра команд',
|
'command.palette': 'Палитра команд',
|
||||||
'command.run': 'Выполнить команду',
|
'command.run': 'Выполнить команду',
|
||||||
|
|
@ -186,26 +142,26 @@ export default {
|
||||||
'search.placeholder': 'Поиск',
|
'search.placeholder': 'Поиск',
|
||||||
'search.global': 'Глобальный поиск',
|
'search.global': 'Глобальный поиск',
|
||||||
'search.noResults': 'Ничего не найдено',
|
'search.noResults': 'Ничего не найдено',
|
||||||
'search.type.workspace': 'Дело',
|
'search.type.workspace': 'Рабочее пространство',
|
||||||
'search.type.tool': 'Инструмент',
|
'search.type.tool': 'Инструмент',
|
||||||
'search.type.folder': 'Папка',
|
'search.type.folder': 'Папка',
|
||||||
'search.type.file': 'Файл',
|
'search.type.file': 'Файл',
|
||||||
'search.type.journal': 'Журнал',
|
'search.type.journal': 'Журнал',
|
||||||
'search.type.browserInbox': 'Браузер',
|
'search.type.browserInbox': 'Входящие из браузера',
|
||||||
'search.type.activity': 'Активность',
|
'search.type.activity': 'Активность',
|
||||||
'workbench.noProvider': 'Нет обработчика',
|
'workbench.noProvider': 'Нет обработчика',
|
||||||
'workbench.noViewer': 'Нет доступного средства просмотра или редактора',
|
'workbench.noViewer': 'Нет доступного средства просмотра или редактора',
|
||||||
'workbench.noResource': 'Ресурс не открыт',
|
'workbench.noResource': 'Ресурс не открыт',
|
||||||
'bundle.noFrontend': 'У плагина нет frontend bundle',
|
'bundle.noFrontend': 'У плагина нет frontend bundle',
|
||||||
'bundle.notFound': 'Плагин не найден',
|
'bundle.notFound': 'Плагин не найден',
|
||||||
'bundle.loadFailed': 'Не удалось загрузить интерфейс плагина. Повторите попытку.',
|
'bundle.loadFailed': 'Не удалось загрузить bundle: {error}',
|
||||||
'bundle.emptyContent': 'пустое содержимое',
|
'bundle.emptyContent': 'пустое содержимое',
|
||||||
'bundle.executionError': 'Не удалось запустить интерфейс плагина. Повторите попытку.',
|
'bundle.executionError': 'Ошибка выполнения bundle: {error}',
|
||||||
'bundle.registrationMissing': 'Bundle загружен, но вызов VerstakPluginRegister не обнаружен',
|
'bundle.registrationMissing': 'Bundle загружен, но вызов VerstakPluginRegister не обнаружен',
|
||||||
'bundle.componentMissing': 'Запрошенный интерфейс плагина недоступен.',
|
'bundle.componentMissing': 'Компонент «{component}» не найден в bundle. Доступны: {available}',
|
||||||
'bundle.mountError': 'Не удалось открыть интерфейс плагина. Повторите попытку.',
|
'bundle.mountError': 'Ошибка подключения компонента: {error}',
|
||||||
'bundle.mountUnavailable': 'Контейнер для подключения недоступен',
|
'bundle.mountUnavailable': 'Контейнер для подключения недоступен',
|
||||||
'bundle.unexpectedError': 'Не удалось открыть интерфейс плагина. Повторите попытку.',
|
'bundle.unexpectedError': 'Непредвиденная ошибка: {error}',
|
||||||
'bundle.unknownError': 'Неизвестная ошибка',
|
'bundle.unknownError': 'Неизвестная ошибка',
|
||||||
'bundle.frontendEntry': 'Точка входа frontend',
|
'bundle.frontendEntry': 'Точка входа frontend',
|
||||||
'bundle.availableComponents': 'Доступные компоненты',
|
'bundle.availableComponents': 'Доступные компоненты',
|
||||||
|
|
@ -241,57 +197,18 @@ export default {
|
||||||
'overview.count.journal.many': 'Записей журнала: {count}',
|
'overview.count.journal.many': 'Записей журнала: {count}',
|
||||||
'overview.count.pending.one': 'Ожидают: {count}',
|
'overview.count.pending.one': 'Ожидают: {count}',
|
||||||
'overview.count.pending.many': 'Ожидают: {count}',
|
'overview.count.pending.many': 'Ожидают: {count}',
|
||||||
'overview.loadingContext': 'Загрузка данных Дела...',
|
'overview.loadingContext': 'Загрузка данных рабочего пространства...',
|
||||||
'overview.lastActive': 'Последняя активность: {time}',
|
'overview.lastActive': 'Последняя активность: {time}',
|
||||||
'overview.noRecentActivity': 'В Деле пока нет недавней активности',
|
'overview.noRecentActivity': 'В рабочем пространстве пока нет недавней активности',
|
||||||
'overview.refresh': 'Обновить',
|
'overview.refresh': 'Обновить',
|
||||||
'overview.summary': 'Сводка Дела',
|
'overview.summary': 'Сводка рабочего пространства',
|
||||||
'overview.continue': 'Продолжить работу',
|
'overview.continue': 'Продолжить работу',
|
||||||
'overview.continueHint': 'Вернитесь к следующему полезному делу в этом Деле.',
|
'overview.continueHint': 'Вернитесь к следующему полезному делу в этом рабочем пространстве.',
|
||||||
'overview.loadingSignals': 'Загрузка данных Дела...',
|
'overview.loadingSignals': 'Загрузка рабочих данных...',
|
||||||
'overview.noResume': 'Пока неясно, с чего продолжить',
|
'overview.noResume': 'Пока неясно, с чего продолжить',
|
||||||
'overview.noResumeHint': 'Здесь появятся недавние заметки, файлы, сохранённое и записи журнала.',
|
'overview.noResumeHint': 'Здесь появятся недавние заметки, файлы, сохранённое и записи журнала.',
|
||||||
'overview.recentChanges': 'Недавние изменения',
|
'overview.recentChanges': 'Недавние изменения',
|
||||||
'overview.recentChangesHint': 'Последняя важная активность в этом Деле.',
|
'overview.recentChangesHint': 'Последняя важная активность в этом рабочем пространстве.',
|
||||||
'overview.event.workspaceOpened': 'Открыто Дело',
|
|
||||||
'overview.event.activity': 'Активность Дела',
|
|
||||||
'overview.event.noteEdited': 'Изменена заметка «{name}»',
|
|
||||||
'overview.event.noteOpened': 'Открыта заметка «{name}»',
|
|
||||||
'overview.event.noteCreated': 'Создана заметка «{name}»',
|
|
||||||
'overview.event.fileOpened': 'Открыт файл «{name}»',
|
|
||||||
'overview.event.fileChanged': 'Изменён файл «{name}»',
|
|
||||||
'overview.event.fileCreated': 'Создан файл «{name}»',
|
|
||||||
'overview.event.fileRemoved': 'Удалён файл «{name}»',
|
|
||||||
'overview.event.capturePage': 'Сохранена страница «{name}»',
|
|
||||||
'overview.event.captureSelection': 'Сохранено выделение «{name}»',
|
|
||||||
'overview.event.captureLink': 'Сохранена ссылка «{name}»',
|
|
||||||
'overview.event.captureFile': 'Сохранён файл «{name}»',
|
|
||||||
'overview.event.captureConverted': 'Сохранённое преобразовано: «{name}»',
|
|
||||||
'overview.event.capture': 'Сохранено: {kind} «{title}»',
|
|
||||||
'overview.event.journalAdded': 'Добавлена запись в журнал «{name}»',
|
|
||||||
'overview.event.workSessionDetected': 'Обнаружена рабочая сессия',
|
|
||||||
'overview.event.todo': 'Задача «{title}»',
|
|
||||||
'overview.event.reviewCapture': 'Разобрать сохранённое «{title}»',
|
|
||||||
'overview.event.continueJournal': 'Продолжить запись журнала «{title}»',
|
|
||||||
'overview.event.possibleJournalEntry': 'Возможная запись в журнал',
|
|
||||||
'overview.captureKind.page': 'страница',
|
|
||||||
'overview.captureKind.selection': 'выделение',
|
|
||||||
'overview.captureKind.link': 'ссылка',
|
|
||||||
'overview.captureKind.file': 'файл',
|
|
||||||
'overview.captureKind.item': 'элемент',
|
|
||||||
'overview.todo.reminderDue': 'Пора напомнить',
|
|
||||||
'overview.todo.overdue': 'Просрочено',
|
|
||||||
'overview.todo.dueSoon': 'Скоро срок',
|
|
||||||
'overview.todo.due': 'Срок: {date}',
|
|
||||||
'overview.candidateMeta': 'Дело: {deal} · {minutes} мин. · активностей: {activities}',
|
|
||||||
'overview.minutes': '{count} мин.',
|
|
||||||
'overview.browserCapture': 'Сохранённое из браузера',
|
|
||||||
'overview.untitledTodo': 'Задача без названия',
|
|
||||||
'overview.untitledCapture': 'Сохранённое без названия',
|
|
||||||
'overview.untitledJournal': 'Запись журнала',
|
|
||||||
'overview.unknownDeal': 'Неизвестное Дело',
|
|
||||||
'overview.item': 'элемент',
|
|
||||||
'overview.overviewNote': 'Обзорная заметка Дела',
|
|
||||||
'overview.recentFilter': 'Фильтр недавних изменений',
|
'overview.recentFilter': 'Фильтр недавних изменений',
|
||||||
'overview.loadingRecent': 'Загрузка недавних изменений...',
|
'overview.loadingRecent': 'Загрузка недавних изменений...',
|
||||||
'overview.noChanges': 'Для этого фильтра пока нет важных изменений.',
|
'overview.noChanges': 'Для этого фильтра пока нет важных изменений.',
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,6 @@
|
||||||
return { value: result, error: '' };
|
return { value: result, error: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportError(key, fallback, details) {
|
|
||||||
console.warn('[PluginBundleHost] ' + key + ':', details);
|
|
||||||
return tr(key, undefined, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAndMount(pId, compId, nextPropsKey) {
|
async function loadAndMount(pId, compId, nextPropsKey) {
|
||||||
// If same plugin+component and already mounted, skip
|
// If same plugin+component and already mounted, skip
|
||||||
if (currentPluginId === pId && currentComponent === compId && currentPropsKey === nextPropsKey && loadState === 'loaded') {
|
if (currentPluginId === pId && currentComponent === compId && currentPropsKey === nextPropsKey && loadState === 'loaded') {
|
||||||
|
|
@ -130,7 +125,7 @@
|
||||||
const content = assetResult.value;
|
const content = assetResult.value;
|
||||||
if (assetResult.error || !content) {
|
if (assetResult.error || !content) {
|
||||||
loadState = 'error';
|
loadState = 'error';
|
||||||
errorText = reportError('bundle.loadFailed', 'Could not load the plugin interface. Please try again.', assetResult.error || tr('bundle.emptyContent'));
|
errorText = tr('bundle.loadFailed', { error: assetResult.error || tr('bundle.emptyContent') });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +136,8 @@
|
||||||
fn();
|
fn();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadState = 'error';
|
loadState = 'error';
|
||||||
errorText = reportError('bundle.executionError', 'Could not start the plugin interface. Please try again.', e);
|
errorText = tr('bundle.executionError', { error: e.message });
|
||||||
|
console.error('[PluginBundleHost] bundle exec error:', e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +154,10 @@
|
||||||
const comp = components[compId];
|
const comp = components[compId];
|
||||||
if (!comp || !comp.mount) {
|
if (!comp || !comp.mount) {
|
||||||
loadState = 'error';
|
loadState = 'error';
|
||||||
errorText = tr('bundle.componentMissing', undefined, 'The requested plugin interface is unavailable.');
|
errorText = tr('bundle.componentMissing', {
|
||||||
|
component: compId,
|
||||||
|
available: Object.keys(components).join(', ') || tr('common.none'),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,7 +177,8 @@
|
||||||
errorText = '';
|
errorText = '';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadState = 'error';
|
loadState = 'error';
|
||||||
errorText = reportError('bundle.mountError', 'Could not open the plugin interface. Please try again.', e);
|
errorText = tr('bundle.mountError', { error: e.message });
|
||||||
|
console.error('[PluginBundleHost] mount error:', e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loadState = 'error';
|
loadState = 'error';
|
||||||
|
|
@ -186,7 +186,8 @@
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadState = 'error';
|
loadState = 'error';
|
||||||
errorText = reportError('bundle.unexpectedError', 'Could not open the plugin interface. Please try again.', e);
|
errorText = tr('bundle.unexpectedError', { error: e.message || e });
|
||||||
|
console.error('[PluginBundleHost] error:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -207,18 +208,17 @@
|
||||||
<div class="host-state error">
|
<div class="host-state error">
|
||||||
<Icon name="warning" size={24} class="error-icon" />
|
<Icon name="warning" size={24} class="error-icon" />
|
||||||
<p class="error-title">{tr('pluginView.error')}</p>
|
<p class="error-title">{tr('pluginView.error')}</p>
|
||||||
<p class="error-message">{errorText || tr('bundle.unknownError')}</p>
|
<div class="error-details">
|
||||||
<details class="error-details">
|
|
||||||
<summary>{tr('common.details')}</summary>
|
|
||||||
<p><strong>{tr('common.plugin')}:</strong> {currentPluginId || tr('common.unknown')}</p>
|
<p><strong>{tr('common.plugin')}:</strong> {currentPluginId || tr('common.unknown')}</p>
|
||||||
<p><strong>{tr('common.component')}:</strong> {currentComponent || tr('common.unknown')}</p>
|
<p><strong>{tr('common.component')}:</strong> {currentComponent || tr('common.unknown')}</p>
|
||||||
|
<p class="error-message">{errorText || tr('bundle.unknownError')}</p>
|
||||||
{#if pluginInfo}
|
{#if pluginInfo}
|
||||||
<p class="error-meta">{tr('bundle.frontendEntry')}: {pluginInfo.entry || tr('common.none')}</p>
|
<p class="error-meta">{tr('bundle.frontendEntry')}: {pluginInfo.entry || tr('common.none')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if getComponentList().length > 0}
|
{#if getComponentList().length > 0}
|
||||||
<p class="error-meta">{tr('bundle.availableComponents')}: {getComponentList().join(', ')}</p>
|
<p class="error-meta">{tr('bundle.availableComponents')}: {getComponentList().join(', ')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</details>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -303,21 +303,11 @@
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 1rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid #0f3460;
|
border: 1px solid #0f3460;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-details[open] {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-details summary {
|
|
||||||
cursor: pointer;
|
|
||||||
color: #e0e0f0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-details p {
|
.error-details p {
|
||||||
margin: 0.3rem 0;
|
margin: 0.3rem 0;
|
||||||
}
|
}
|
||||||
|
|
@ -328,8 +318,12 @@
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: #e94560;
|
color: #e94560;
|
||||||
max-width: 400px;
|
font-family: monospace;
|
||||||
margin: 0 0 0.75rem;
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.5rem !important;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(233, 69, 96, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-meta {
|
.error-meta {
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,6 @@
|
||||||
let reloading = false;
|
let reloading = false;
|
||||||
let toastMessage = '';
|
let toastMessage = '';
|
||||||
let toastType = 'success'; // 'success' | 'error' | 'info'
|
let toastType = 'success'; // 'success' | 'error' | 'info'
|
||||||
let statusFilter = 'all';
|
|
||||||
let permissionFilters = [];
|
|
||||||
let capabilityFilters = [];
|
|
||||||
let settingsFilter = 'all';
|
|
||||||
let sourceFilter = 'all';
|
|
||||||
|
|
||||||
export let activeSettingsPluginId = '';
|
export let activeSettingsPluginId = '';
|
||||||
export let activeSettingsPanelId = '';
|
export let activeSettingsPanelId = '';
|
||||||
|
|
@ -58,12 +53,6 @@
|
||||||
}, 4000);
|
}, 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportError(key, fallback, details) {
|
|
||||||
debug.log('[PluginManager] ' + key + ':', String(details));
|
|
||||||
WriteFrontendLog('PluginManager', key + ': ' + String(details)).catch(() => {});
|
|
||||||
return tr(key, undefined, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyPluginsChanged() {
|
function notifyPluginsChanged() {
|
||||||
window.dispatchEvent(new CustomEvent('verstak:plugins-changed'));
|
window.dispatchEvent(new CustomEvent('verstak:plugins-changed'));
|
||||||
}
|
}
|
||||||
|
|
@ -100,14 +89,14 @@
|
||||||
ReadPluginSettings(pluginId).then(result => {
|
ReadPluginSettings(pluginId).then(result => {
|
||||||
const unpacked = unpackBackendResult(result);
|
const unpacked = unpackBackendResult(result);
|
||||||
if (unpacked.error) {
|
if (unpacked.error) {
|
||||||
settingsError = reportError('pluginManager.settingsLoadError', 'Could not load plugin settings. Please try again.', unpacked.error);
|
settingsError = unpacked.error;
|
||||||
settingsData = {};
|
settingsData = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settingsData = unpacked.value || {};
|
settingsData = unpacked.value || {};
|
||||||
}).catch(() => { settingsData = {}; });
|
}).catch(() => { settingsData = {}; });
|
||||||
} else {
|
} else {
|
||||||
settingsError = tr('pluginManager.settingsUnavailable', undefined, 'Plugin settings are unavailable.');
|
settingsError = `Settings panel not found for plugin "${pluginId}". Check that the plugin is enabled and has settingsPanels in its manifest.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +127,7 @@
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debug.log('[PluginManager] loadAll: GetPlugins ERROR:', String(e));
|
debug.log('[PluginManager] loadAll: GetPlugins ERROR:', String(e));
|
||||||
WriteFrontendLog('PluginManager', 'loadAll: GetPlugins ERROR: ' + String(e));
|
WriteFrontendLog('PluginManager', 'loadAll: GetPlugins ERROR: ' + String(e));
|
||||||
error = reportError('pluginManager.loadError', 'Could not load plugins. Please try again.', e);
|
error = 'GetPlugins: ' + String(e);
|
||||||
loading = false;
|
loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +191,7 @@
|
||||||
resultMsg = `Reloaded ${count} plugin(s). ${summary}`;
|
resultMsg = `Reloaded ${count} plugin(s). ${summary}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debug.log('[PluginManager] reload: ReloadPlugins ERROR:', String(e));
|
debug.log('[PluginManager] reload: ReloadPlugins ERROR:', String(e));
|
||||||
error = reportError('pluginManager.reloadError', 'Could not reload plugins. Please try again.', e);
|
error = 'Reload: ' + String(e);
|
||||||
reloading = false;
|
reloading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +211,7 @@
|
||||||
if (err) {
|
if (err) {
|
||||||
debug.log('[PluginManager] enablePlugin: ERROR:', err);
|
debug.log('[PluginManager] enablePlugin: ERROR:', err);
|
||||||
actionFeedback = { ...actionFeedback, [pluginId]: null };
|
actionFeedback = { ...actionFeedback, [pluginId]: null };
|
||||||
error = reportError('pluginManager.enableError', 'Could not enable the plugin. Please try again.', err);
|
error = 'Enable: ' + err;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug.log('[PluginManager] enablePlugin: success, reloading...');
|
debug.log('[PluginManager] enablePlugin: success, reloading...');
|
||||||
|
|
@ -243,7 +232,7 @@
|
||||||
if (err) {
|
if (err) {
|
||||||
debug.log('[PluginManager] disablePlugin: ERROR:', err);
|
debug.log('[PluginManager] disablePlugin: ERROR:', err);
|
||||||
actionFeedback = { ...actionFeedback, [pluginId]: null };
|
actionFeedback = { ...actionFeedback, [pluginId]: null };
|
||||||
error = reportError('pluginManager.disableError', 'Could not disable the plugin. Please try again.', err);
|
error = 'Disable: ' + err;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debug.log('[PluginManager] disablePlugin: success, reloading...');
|
debug.log('[PluginManager] disablePlugin: success, reloading...');
|
||||||
|
|
@ -261,11 +250,6 @@
|
||||||
$: totalPerms = permissions.length;
|
$: totalPerms = permissions.length;
|
||||||
$: statusSummary = computeStatusSummary(plugins);
|
$: statusSummary = computeStatusSummary(plugins);
|
||||||
$: elevatedPermissionPluginCount = computeElevatedPermissionPluginCount(plugins, permissions);
|
$: elevatedPermissionPluginCount = computeElevatedPermissionPluginCount(plugins, permissions);
|
||||||
$: filterPermissions = collectManifestValues(plugins, 'permissions');
|
|
||||||
$: filterCapabilities = collectCapabilities(plugins);
|
|
||||||
$: hasReliableSourceMetadata = plugins.some((plugin) => sourceGroup(plugin) !== '');
|
|
||||||
$: visiblePlugins = filterPlugins(plugins, statusFilter, permissionFilters, capabilityFilters, settingsFilter, sourceFilter, contributions);
|
|
||||||
$: filtersActive = statusFilter !== 'all' || permissionFilters.length > 0 || capabilityFilters.length > 0 || settingsFilter !== 'all' || sourceFilter !== 'all';
|
|
||||||
|
|
||||||
function computeStatusSummary(pluginRows) {
|
function computeStatusSummary(pluginRows) {
|
||||||
return pluginRows.reduce((summary, plugin) => {
|
return pluginRows.reduce((summary, plugin) => {
|
||||||
|
|
@ -284,69 +268,6 @@
|
||||||
return (pluginRows || []).filter(plugin => (plugin?.manifest?.permissions || []).some(permission => dangerous.has(permission))).length;
|
return (pluginRows || []).filter(plugin => (plugin?.manifest?.permissions || []).some(permission => dangerous.has(permission))).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectManifestValues(pluginRows, key) {
|
|
||||||
return Array.from(new Set((pluginRows || []).flatMap((plugin) => plugin?.manifest?.[key] || [])))
|
|
||||||
.sort((left, right) => left.localeCompare(right));
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectCapabilities(pluginRows) {
|
|
||||||
return Array.from(new Set((pluginRows || []).flatMap((plugin) => [
|
|
||||||
...(plugin?.manifest?.provides || []),
|
|
||||||
...(plugin?.manifest?.requires || []),
|
|
||||||
...(plugin?.manifest?.optionalRequires || []),
|
|
||||||
]))).sort((left, right) => left.localeCompare(right));
|
|
||||||
}
|
|
||||||
|
|
||||||
function pluginStatusGroup(plugin) {
|
|
||||||
const status = plugin?.status || '';
|
|
||||||
if (status === 'failed' || status === 'incompatible' || status === 'missing-required-capability') return 'failed';
|
|
||||||
if (status === 'degraded') return 'degraded';
|
|
||||||
if (status === 'disabled' || plugin?.enabled === false) return 'disabled';
|
|
||||||
return 'enabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
function sourceGroup(plugin) {
|
|
||||||
const source = plugin?.manifest?.source;
|
|
||||||
if (source === 'official') return 'official';
|
|
||||||
if (source === 'local' || source === 'third-party') return 'third-party';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSettings(plugin, activeContributions) {
|
|
||||||
const pluginId = plugin?.manifest?.id;
|
|
||||||
return Boolean(pluginId && (activeContributions.settingsPanels || []).some((panel) => panel.pluginId === pluginId));
|
|
||||||
}
|
|
||||||
|
|
||||||
function includesAny(values, expected) {
|
|
||||||
return expected.length === 0 || expected.some((value) => values.includes(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterPlugins(pluginRows, activeStatusFilter, activePermissionFilters, activeCapabilityFilters, activeSettingsFilter, activeSourceFilter, activeContributions) {
|
|
||||||
return pluginRows.filter((plugin) => matchesFilters(plugin, activeStatusFilter, activePermissionFilters, activeCapabilityFilters, activeSettingsFilter, activeSourceFilter, activeContributions));
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesFilters(plugin, activeStatusFilter, activePermissionFilters, activeCapabilityFilters, activeSettingsFilter, activeSourceFilter, activeContributions) {
|
|
||||||
if (activeStatusFilter !== 'all' && pluginStatusGroup(plugin) !== activeStatusFilter) return false;
|
|
||||||
if (!includesAny(plugin?.manifest?.permissions || [], activePermissionFilters)) return false;
|
|
||||||
const declaredCapabilities = [
|
|
||||||
...(plugin?.manifest?.provides || []),
|
|
||||||
...(plugin?.manifest?.requires || []),
|
|
||||||
...(plugin?.manifest?.optionalRequires || []),
|
|
||||||
];
|
|
||||||
if (!includesAny(declaredCapabilities, activeCapabilityFilters)) return false;
|
|
||||||
if (activeSettingsFilter === 'with' && !hasSettings(plugin, activeContributions)) return false;
|
|
||||||
if (activeSettingsFilter === 'without' && hasSettings(plugin, activeContributions)) return false;
|
|
||||||
return activeSourceFilter === 'all' || sourceGroup(plugin) === activeSourceFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFilters() {
|
|
||||||
statusFilter = 'all';
|
|
||||||
permissionFilters = [];
|
|
||||||
capabilityFilters = [];
|
|
||||||
settingsFilter = 'all';
|
|
||||||
sourceFilter = 'all';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeSettings() {
|
function closeSettings() {
|
||||||
settingsPanel = null;
|
settingsPanel = null;
|
||||||
settingsPluginId = '';
|
settingsPluginId = '';
|
||||||
|
|
@ -418,70 +339,6 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="plugin-filters" aria-label={tr('pluginManager.filters')}>
|
|
||||||
<div class="filter-header">
|
|
||||||
<div>
|
|
||||||
<h3>{tr('pluginManager.filters')}</h3>
|
|
||||||
<p data-plugin-filter-results>{tr('pluginManager.filterResults', { visible: visiblePlugins.length, total: totalPlugins })}</p>
|
|
||||||
</div>
|
|
||||||
<button class="filter-reset" data-plugin-filter-reset type="button" on:click={resetFilters} disabled={!filtersActive}>{tr('pluginManager.filterReset')}</button>
|
|
||||||
</div>
|
|
||||||
<div class="filter-grid">
|
|
||||||
<label class="filter-select-label">
|
|
||||||
<span>{tr('pluginManager.filterState')}</span>
|
|
||||||
<select class="filter-select" data-plugin-filter="status" bind:value={statusFilter}>
|
|
||||||
<option value="all">{tr('pluginManager.filterAll')}</option>
|
|
||||||
<option value="enabled">{tr('pluginManager.filterEnabled')}</option>
|
|
||||||
<option value="disabled">{tr('pluginManager.filterDisabled')}</option>
|
|
||||||
<option value="failed">{tr('pluginManager.filterFailed')}</option>
|
|
||||||
<option value="degraded">{tr('pluginManager.filterDegraded')}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="filter-select-label">
|
|
||||||
<span>{tr('pluginManager.filterSettings')}</span>
|
|
||||||
<select class="filter-select" data-plugin-filter="settings" bind:value={settingsFilter}>
|
|
||||||
<option value="all">{tr('pluginManager.filterAll')}</option>
|
|
||||||
<option value="with">{tr('pluginManager.filterWithSettings')}</option>
|
|
||||||
<option value="without">{tr('pluginManager.filterWithoutSettings')}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{#if hasReliableSourceMetadata}
|
|
||||||
<label class="filter-select-label">
|
|
||||||
<span>{tr('pluginManager.filterSource')}</span>
|
|
||||||
<select class="filter-select" data-plugin-filter="source" bind:value={sourceFilter}>
|
|
||||||
<option value="all">{tr('pluginManager.filterAll')}</option>
|
|
||||||
<option value="official">{tr('pluginManager.filterOfficial')}</option>
|
|
||||||
<option value="third-party">{tr('pluginManager.filterThirdParty')}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if filterPermissions.length > 0}
|
|
||||||
<fieldset class="filter-options">
|
|
||||||
<legend>{tr('pluginManager.filterPermissions')}</legend>
|
|
||||||
<div class="filter-option-list">
|
|
||||||
{#each filterPermissions as permission}
|
|
||||||
<label><input data-plugin-filter-permission={permission} type="checkbox" bind:group={permissionFilters} value={permission} /> <code>{permission}</code></label>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if filterCapabilities.length > 0}
|
|
||||||
<fieldset class="filter-options">
|
|
||||||
<legend>{tr('pluginManager.filterCapabilities')}</legend>
|
|
||||||
<div class="filter-option-list">
|
|
||||||
{#each filterCapabilities as capability}
|
|
||||||
<label><input data-plugin-filter-capability={capability} type="checkbox" bind:group={capabilityFilters} value={capability} /> <code>{capability}</code></label>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{#if plugins.length === 0 && missingInstalled.length === 0}
|
{#if plugins.length === 0 && missingInstalled.length === 0}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
|
|
@ -497,13 +354,9 @@
|
||||||
</ul>
|
</ul>
|
||||||
<p class="hint">{tr('pluginManager.installHint')}</p>
|
<p class="hint">{tr('pluginManager.installHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if visiblePlugins.length === 0}
|
|
||||||
<div class="empty filter-empty" data-plugin-filter-empty>
|
|
||||||
<p>{tr('pluginManager.filterEmpty')}</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="plugin-list">
|
<div class="plugin-list">
|
||||||
{#each visiblePlugins as p}
|
{#each plugins as p}
|
||||||
<PluginCard {p} {capabilities} {permissions} {contributions} {vaultOpen} {actionFeedback} settingsPanels={(contributions.settingsPanels || []).filter(sp => sp.pluginId === p.manifest?.id)} onEnable={enablePlugin} onDisable={disablePlugin} />
|
<PluginCard {p} {capabilities} {permissions} {contributions} {vaultOpen} {actionFeedback} settingsPanels={(contributions.settingsPanels || []).filter(sp => sp.pluginId === p.manifest?.id)} onEnable={enablePlugin} onDisable={disablePlugin} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -718,105 +571,6 @@
|
||||||
color: #f4f7fb;
|
color: #f4f7fb;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
.plugin-filters {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: 1px solid #0f3460;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #121a2c;
|
|
||||||
}
|
|
||||||
.filter-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
.filter-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #e0e0f0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.filter-header p {
|
|
||||||
margin: 0.2rem 0 0;
|
|
||||||
color: #a0a0b8;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
|
||||||
.filter-reset {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 0.35rem 0.65rem;
|
|
||||||
border: 1px solid #533483;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #16213e;
|
|
||||||
color: #e0e0e0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
|
||||||
.filter-reset:hover:not(:disabled) { background: #0f3460; }
|
|
||||||
.filter-reset:disabled { cursor: not-allowed; opacity: 0.5; }
|
|
||||||
.filter-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.65rem;
|
|
||||||
}
|
|
||||||
.filter-select-label {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.28rem;
|
|
||||||
min-width: 10rem;
|
|
||||||
color: #a0a0b8;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.filter-select {
|
|
||||||
min-height: 2rem;
|
|
||||||
padding: 0.3rem 1.9rem 0.3rem 0.55rem;
|
|
||||||
border: 1px solid #0f3460;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #16213e;
|
|
||||||
background-image: linear-gradient(45deg, transparent 50%, #a0a0b8 50%), linear-gradient(135deg, #a0a0b8 50%, transparent 50%);
|
|
||||||
background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%;
|
|
||||||
background-size: 5px 5px, 5px 5px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
color: #e0e0e0;
|
|
||||||
font: inherit;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
.filter-select option { background: #16213e; color: #e0e0e0; }
|
|
||||||
.filter-select:focus { outline: 2px solid #533483; outline-offset: 1px; }
|
|
||||||
.filter-options {
|
|
||||||
min-width: 0;
|
|
||||||
margin: 0.75rem 0 0;
|
|
||||||
padding: 0.55rem 0.65rem 0.65rem;
|
|
||||||
border: 1px solid rgba(15, 52, 96, 0.85);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
.filter-options legend {
|
|
||||||
padding: 0 0.25rem;
|
|
||||||
color: #a0a0b8;
|
|
||||||
font-size: 0.76rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.filter-option-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.4rem 0.75rem;
|
|
||||||
}
|
|
||||||
.filter-option-list label {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.28rem;
|
|
||||||
min-width: 0;
|
|
||||||
color: #c6c6d8;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
.filter-option-list input { accent-color: #6c4fa3; }
|
|
||||||
.filter-option-list code {
|
|
||||||
max-width: min(31rem, 72vw);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.filter-empty { margin-bottom: 1.5rem; }
|
|
||||||
.empty {
|
.empty {
|
||||||
padding: 2rem; text-align: center; color: #a0a0b8;
|
padding: 2rem; text-align: center; color: #a0a0b8;
|
||||||
background: #16213e; border-radius: 8px; border: 1px dashed #0f3460;
|
background: #16213e; border-radius: 8px; border: 1px dashed #0f3460;
|
||||||
|
|
@ -825,6 +579,7 @@
|
||||||
.hint { font-size: 0.85rem; margin-top: 0.5rem; opacity: 0.7; }
|
.hint { font-size: 0.85rem; margin-top: 0.5rem; opacity: 0.7; }
|
||||||
.hint-list { list-style: none; padding: 0; margin: 0.5rem 0; font-size: 0.8rem; opacity: 0.7; }
|
.hint-list { list-style: none; padding: 0; margin: 0.5rem 0; font-size: 0.8rem; opacity: 0.7; }
|
||||||
.hint-list li { margin: 0.25rem 0; }
|
.hint-list li { margin: 0.25rem 0; }
|
||||||
|
.hint code { background: #0f3460; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
||||||
.plugin-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.5rem; min-width: 0; }
|
.plugin-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.5rem; min-width: 0; }
|
||||||
|
|
||||||
.missing-section { margin-bottom: 1.5rem; }
|
.missing-section { margin-bottom: 1.5rem; }
|
||||||
|
|
@ -891,19 +646,6 @@
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-header {
|
|
||||||
align-items: stretch;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-reset {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select-label {
|
|
||||||
flex: 1 1 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
width: min(880px, calc(100vw - 2rem));
|
width: min(880px, calc(100vw - 2rem));
|
||||||
height: min(680px, calc(100vh - 2rem));
|
height: min(680px, calc(100vh - 2rem));
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import * as App from '../../../wailsjs/go/api/App';
|
import * as App from '../../../wailsjs/go/api/App';
|
||||||
import { executePluginCommand } from '../plugin-host/VerstakPluginAPI.js';
|
import { executePluginCommand } from '../plugin-host/VerstakPluginAPI.js';
|
||||||
import { i18n } from '../i18n/index.js';
|
import { i18n } from '../i18n/index.js';
|
||||||
import { debug } from '../log/debug.js';
|
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
let query = '';
|
let query = '';
|
||||||
|
|
@ -195,12 +194,7 @@
|
||||||
closePalette();
|
closePalette();
|
||||||
setStatus('success', tr('command.result', { title: command.title || command.id, status: result.status || tr('command.statusHandled') }));
|
setStatus('success', tr('command.result', { title: command.title || command.id, status: result.status || tr('command.statusHandled') }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const details = err?.message || String(err);
|
setStatus('error', `${command.title || command.id}: ${err?.message || String(err)}`);
|
||||||
debug.log('[CommandPalette] command failed:', details);
|
|
||||||
App.WriteFrontendLog('CommandPalette', `command failed: ${details}`).catch(() => {});
|
|
||||||
setStatus('error', tr('command.failed', {
|
|
||||||
title: command.title || command.id,
|
|
||||||
}, `Could not run ${command.title || command.id}. Please try again.`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@
|
||||||
import { i18n } from '../i18n/index.js';
|
import { i18n } from '../i18n/index.js';
|
||||||
|
|
||||||
export let showGlobalSearch = true;
|
export let showGlobalSearch = true;
|
||||||
export let activeView = null;
|
|
||||||
export let activeViewPluginId = '';
|
|
||||||
|
|
||||||
function flog(msg) {
|
function flog(msg) {
|
||||||
App.WriteFrontendLog('Sidebar', msg);
|
App.WriteFrontendLog('Sidebar', msg);
|
||||||
|
|
@ -28,7 +26,6 @@
|
||||||
})(locale);
|
})(locale);
|
||||||
|
|
||||||
$: vaultOpen = vaultStatus.status === 'open';
|
$: vaultOpen = vaultStatus.status === 'open';
|
||||||
$: activeSidebarKey = activeView ? `${activeViewPluginId}:${activeView}` : '';
|
|
||||||
|
|
||||||
async function loadSidebar() {
|
async function loadSidebar() {
|
||||||
debug.log('[Sidebar] onMount: START');
|
debug.log('[Sidebar] onMount: START');
|
||||||
|
|
@ -110,8 +107,6 @@
|
||||||
{#each sidebarItems as item}
|
{#each sidebarItems as item}
|
||||||
<button
|
<button
|
||||||
class="nav-item plugin-item vt-list-row"
|
class="nav-item plugin-item vt-list-row"
|
||||||
class:is-active={activeSidebarKey === `${item.pluginId || ''}:${item.view || item.id}`}
|
|
||||||
aria-current={activeSidebarKey === `${item.pluginId || ''}:${item.view || item.id}` ? 'page' : undefined}
|
|
||||||
on:click={() => handleSidebarItem(item)}
|
on:click={() => handleSidebarItem(item)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
@ -215,16 +210,6 @@
|
||||||
color: var(--vt-color-text-primary);
|
color: var(--vt-color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.is-active {
|
|
||||||
background: var(--vt-color-surface-selected);
|
|
||||||
color: var(--vt-color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.is-active :global(.nav-icon.icon-plugin) {
|
|
||||||
color: currentColor;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.nav-icon) {
|
:global(.nav-icon) {
|
||||||
width: 0.9rem;
|
width: 0.9rem;
|
||||||
height: 0.9rem;
|
height: 0.9rem;
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@
|
||||||
let keyResources = [];
|
let keyResources = [];
|
||||||
let totalNotes = 0;
|
let totalNotes = 0;
|
||||||
let loadedWorkspaceRoot = '';
|
let loadedWorkspaceRoot = '';
|
||||||
let loadedToolKey = '';
|
|
||||||
let toolProbe = 0;
|
let toolProbe = 0;
|
||||||
let locale = i18n.getLocale();
|
let locale = i18n.getLocale();
|
||||||
let unsubscribeLocale = null;
|
let unsubscribeLocale = null;
|
||||||
|
|
@ -39,25 +38,17 @@
|
||||||
void activeLocale;
|
void activeLocale;
|
||||||
return i18n.t(key, params, fallback);
|
return i18n.t(key, params, fallback);
|
||||||
})(locale);
|
})(locale);
|
||||||
|
$: FILTERS = ['all', 'notes', 'files', 'captures', 'journal'].map((key) => ({
|
||||||
|
key,
|
||||||
|
label: tr(`overview.filter.${key}`),
|
||||||
|
}));
|
||||||
function countText(key, count, params = {}) {
|
function countText(key, count, params = {}) {
|
||||||
return tr(`${key}.${count === 1 ? 'one' : 'many'}`, { count, ...params });
|
return tr(`${key}.${count === 1 ? 'one' : 'many'}`, { count, ...params });
|
||||||
}
|
}
|
||||||
|
|
||||||
$: hasNotes = hasTool('notes', availableTools);
|
$: hasNotes = hasTool('notes', availableTools);
|
||||||
$: hasFiles = hasTool('files', availableTools);
|
|
||||||
$: hasBrowserInbox = hasTool('browser-inbox', availableTools);
|
|
||||||
$: hasActivity = hasTool('activity', availableTools);
|
|
||||||
$: hasJournal = hasTool('journal', availableTools);
|
|
||||||
$: hasTodos = hasTool('todo', availableTools);
|
$: hasTodos = hasTool('todo', availableTools);
|
||||||
$: overviewToolKey = [hasNotes, hasFiles, hasBrowserInbox, hasActivity, hasJournal, hasTodos].join('|');
|
$: recentChanges = buildRecentChanges(activityEvents, captures, journalEntries);
|
||||||
$: FILTERS = ['all']
|
|
||||||
.concat(hasNotes ? ['notes'] : [])
|
|
||||||
.concat(hasFiles ? ['files'] : [])
|
|
||||||
.concat(hasBrowserInbox ? ['captures'] : [])
|
|
||||||
.concat(hasJournal ? ['journal'] : [])
|
|
||||||
.map((key) => ({ key, label: tr(`overview.filter.${key}`) }));
|
|
||||||
$: if (!FILTERS.some(filter => filter.key === activeFilter)) activeFilter = 'all';
|
|
||||||
$: recentChanges = filterAvailableItems(buildRecentChanges(activityEvents, captures, journalEntries), overviewToolKey);
|
|
||||||
$: filteredRecentChanges = activeFilter === 'all'
|
$: filteredRecentChanges = activeFilter === 'all'
|
||||||
? recentChanges
|
? recentChanges
|
||||||
: recentChanges.filter(item => item.category === activeFilter);
|
: recentChanges.filter(item => item.category === activeFilter);
|
||||||
|
|
@ -67,19 +58,18 @@
|
||||||
$: linkedCandidateIds = new Set(journalEntries.map(item => String(item?.sourceCandidateId || '')).filter(Boolean));
|
$: linkedCandidateIds = new Set(journalEntries.map(item => String(item?.sourceCandidateId || '')).filter(Boolean));
|
||||||
$: pendingWorkSessionCandidates = workSessionCandidates.filter(item => !linkedCandidateIds.has(String(item?.candidateId || '')));
|
$: pendingWorkSessionCandidates = workSessionCandidates.filter(item => !linkedCandidateIds.has(String(item?.candidateId || '')));
|
||||||
$: urgentTodos = hasTodos ? todos.filter(item => todoAttentionState(item)) : [];
|
$: urgentTodos = hasTodos ? todos.filter(item => todoAttentionState(item)) : [];
|
||||||
$: needsAttention = filterAvailableItems(buildNeedsAttention(unprocessedCaptures, pendingWorkSessionCandidates, urgentTodos), overviewToolKey);
|
$: needsAttention = buildNeedsAttention(unprocessedCaptures, pendingWorkSessionCandidates, urgentTodos);
|
||||||
$: continueItems = filterAvailableItems(buildContinueItems(activityEvents, unprocessedCaptures, journalEntries, urgentTodos), overviewToolKey);
|
$: continueItems = buildContinueItems(activityEvents, unprocessedCaptures, journalEntries, urgentTodos);
|
||||||
$: hasAttentionTools = hasBrowserInbox || hasTodos || (hasActivity && hasJournal);
|
$: attentionActionKind = needsAttention[0]?.actionKind || 'browser-inbox';
|
||||||
$: attentionActionKind = needsAttention[0]?.actionKind || fallbackAttentionAction();
|
|
||||||
$: lastActive = lastActiveDate([...recentChanges, ...continueItems], captures, journalEntries, todos);
|
$: lastActive = lastActiveDate([...recentChanges, ...continueItems], captures, journalEntries, todos);
|
||||||
$: summaryItems = [
|
$: summaryItems = [
|
||||||
hasNotes ? { key: 'notes', label: tr('overview.notes'), count: totalNotes, detail: countText('overview.count.totalRecent', noteRecentChanges, { total: totalNotes, recent: noteRecentChanges }), actionKind: 'notes', actionLabel: tr('overview.openNotes') } : null,
|
{ key: 'notes', label: tr('overview.notes'), count: totalNotes, detail: countText('overview.count.totalRecent', noteRecentChanges, { total: totalNotes, recent: noteRecentChanges }), actionKind: 'notes', actionLabel: tr('overview.openNotes') },
|
||||||
hasFiles ? { key: 'files', label: tr('overview.files'), count: fileRecentChanges, detail: countText('overview.count.recentChanges', fileRecentChanges), actionKind: 'files', actionLabel: tr('overview.openFiles') } : null,
|
{ key: 'files', label: tr('overview.files'), count: fileRecentChanges, detail: countText('overview.count.recentChanges', fileRecentChanges), actionKind: 'files', actionLabel: tr('overview.openFiles') },
|
||||||
hasBrowserInbox ? { key: 'captures', label: tr('overview.captures'), count: unprocessedCaptures.length, detail: countText('overview.count.captures', unprocessedCaptures.length), actionKind: 'browser-inbox', actionLabel: tr('overview.reviewInbox') } : null,
|
{ key: 'captures', label: tr('overview.captures'), count: unprocessedCaptures.length, detail: countText('overview.count.captures', unprocessedCaptures.length), actionKind: 'browser-inbox', actionLabel: tr('overview.reviewInbox') },
|
||||||
hasActivity ? { key: 'activity', label: tr('overview.activity'), count: activityEvents.length, detail: countText('overview.count.events', activityEvents.length), actionKind: 'activity', actionLabel: tr('overview.viewActivity') } : null,
|
{ key: 'activity', label: tr('overview.activity'), count: activityEvents.length, detail: countText('overview.count.events', activityEvents.length), actionKind: 'activity', actionLabel: tr('overview.viewActivity') },
|
||||||
hasJournal ? { key: 'journal', label: tr('overview.journal'), count: journalEntries.length, detail: countText('overview.count.journal', journalEntries.length), actionKind: 'journal', actionLabel: tr('overview.openJournal') } : null,
|
{ key: 'journal', label: tr('overview.journal'), count: journalEntries.length, detail: countText('overview.count.journal', journalEntries.length), actionKind: 'journal', actionLabel: tr('overview.openJournal') },
|
||||||
hasAttentionTools ? { key: 'attention', label: tr('overview.attention'), count: needsAttention.length, detail: countText('overview.count.pending', needsAttention.length), actionKind: attentionActionKind, actionLabel: tr('overview.reviewPending') } : null,
|
{ key: 'attention', label: tr('overview.attention'), count: needsAttention.length, detail: countText('overview.count.pending', needsAttention.length), actionKind: attentionActionKind, actionLabel: tr('overview.reviewPending') },
|
||||||
].filter(Boolean);
|
];
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
unsubscribeLocale = i18n.subscribe((nextLocale) => locale = nextLocale);
|
unsubscribeLocale = i18n.subscribe((nextLocale) => locale = nextLocale);
|
||||||
|
|
@ -88,7 +78,7 @@
|
||||||
|
|
||||||
onDestroy(() => unsubscribeLocale?.());
|
onDestroy(() => unsubscribeLocale?.());
|
||||||
|
|
||||||
$: if (workspaceRootPath && (workspaceRootPath !== loadedWorkspaceRoot || overviewToolKey !== loadedToolKey)) {
|
$: if (workspaceRootPath && workspaceRootPath !== loadedWorkspaceRoot) {
|
||||||
loadOverview();
|
loadOverview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,29 +89,11 @@
|
||||||
const label = `${tool?.title || ''} ${tool?.id || ''} ${tool?.pluginId || ''}`.toLowerCase();
|
const label = `${tool?.title || ''} ${tool?.id || ''} ${tool?.pluginId || ''}`.toLowerCase();
|
||||||
return label.includes(name);
|
return label.includes(name);
|
||||||
});
|
});
|
||||||
return fromProps;
|
if (fromProps) return true;
|
||||||
}
|
if (typeof document === 'undefined') return false;
|
||||||
|
return Array.from(document.querySelectorAll('.workspace-tabs [role="tab"]')).some(tab => {
|
||||||
function actionIsAvailable(kind) {
|
return String(tab.textContent || '').trim().toLowerCase().includes(name);
|
||||||
if (kind === 'notes') return hasNotes;
|
});
|
||||||
if (kind === 'files') return hasFiles;
|
|
||||||
if (kind === 'browser-inbox') return hasBrowserInbox;
|
|
||||||
if (kind === 'activity') return hasActivity;
|
|
||||||
if (kind === 'journal') return hasJournal;
|
|
||||||
if (kind === 'todo') return hasTodos;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterAvailableItems(items, _toolKey) {
|
|
||||||
void _toolKey;
|
|
||||||
return (items || []).filter(item => actionIsAvailable(item?.actionKind));
|
|
||||||
}
|
|
||||||
|
|
||||||
function fallbackAttentionAction() {
|
|
||||||
if (hasBrowserInbox) return 'browser-inbox';
|
|
||||||
if (hasTodos) return 'todo';
|
|
||||||
if (hasActivity && hasJournal) return 'journal';
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeTuple(response, fallback) {
|
function decodeTuple(response, fallback) {
|
||||||
|
|
@ -195,7 +167,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function todoTitle(item) {
|
function todoTitle(item) {
|
||||||
return String(item?.title || item?.name || tr('overview.untitledTodo')).trim() || tr('overview.untitledTodo');
|
return String(item?.title || item?.name || 'Untitled todo').trim() || 'Untitled todo';
|
||||||
}
|
}
|
||||||
|
|
||||||
function todoAttentionState(item) {
|
function todoAttentionState(item) {
|
||||||
|
|
@ -203,9 +175,9 @@
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const dueAt = todoDateMs(item?.dueAt);
|
const dueAt = todoDateMs(item?.dueAt);
|
||||||
const reminderAt = todoDateMs(item?.reminderAt);
|
const reminderAt = todoDateMs(item?.reminderAt);
|
||||||
if (reminderAt && reminderAt <= now) return tr('overview.todo.reminderDue');
|
if (reminderAt && reminderAt <= now) return 'Reminder due';
|
||||||
if (dueAt && dueAt < now) return tr('overview.todo.overdue');
|
if (dueAt && dueAt < now) return 'Overdue';
|
||||||
if (dueAt && dueAt <= now + 3 * 24 * 60 * 60 * 1000) return tr('overview.todo.dueSoon');
|
if (dueAt && dueAt <= now + 3 * 24 * 60 * 60 * 1000) return 'Due soon';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,12 +210,17 @@
|
||||||
return fileName(path).replace(/\.(md|markdown|txt)$/i, '').replace(/_/g, ' ') || 'Untitled';
|
return fileName(path).replace(/\.(md|markdown|txt)$/i, '').replace(/_/g, ' ') || 'Untitled';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function quoted(value) {
|
||||||
|
const clean = String(value || '').trim();
|
||||||
|
return clean ? `"${clean}"` : '';
|
||||||
|
}
|
||||||
|
|
||||||
function entityName(item) {
|
function entityName(item) {
|
||||||
const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {};
|
const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {};
|
||||||
const path = payload.path || payload.notePath || item?.path || item?.relativePath || looksLikePath(item?.summary);
|
const path = payload.path || payload.notePath || item?.path || item?.relativePath || looksLikePath(item?.summary);
|
||||||
const title = String(item?.title || payload.title || '').trim();
|
const title = String(item?.title || payload.title || '').trim();
|
||||||
if (path && (!title || /^(saved note|file opened|file changed|activity event)$/i.test(title))) return titleFromPath(path);
|
if (path && (!title || /^(saved note|file opened|file changed|activity event)$/i.test(title))) return titleFromPath(path);
|
||||||
return title || titleFromPath(path) || String(item?.summary || item?.activityId || tr('overview.item'));
|
return title || titleFromPath(path) || String(item?.summary || item?.activityId || 'item');
|
||||||
}
|
}
|
||||||
|
|
||||||
function looksLikePath(value) {
|
function looksLikePath(value) {
|
||||||
|
|
@ -253,11 +230,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureTitle(capture) {
|
function captureTitle(capture) {
|
||||||
return capture?.title || capture?.fileName || capture?.url || capture?.captureId || tr('overview.untitledCapture');
|
return capture?.title || capture?.fileName || capture?.url || capture?.captureId || 'Untitled capture';
|
||||||
}
|
}
|
||||||
|
|
||||||
function journalTitle(entry) {
|
function journalTitle(entry) {
|
||||||
return entry?.title || entry?.summary || entry?.date || entry?.entryId || tr('overview.untitledJournal');
|
return entry?.title || entry?.summary || entry?.date || entry?.entryId || 'Journal entry';
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemTimeLabel(item) {
|
function itemTimeLabel(item) {
|
||||||
|
|
@ -307,31 +284,22 @@
|
||||||
function activityTitle(item) {
|
function activityTitle(item) {
|
||||||
const type = String(item?.type || '').toLowerCase();
|
const type = String(item?.type || '').toLowerCase();
|
||||||
const name = entityName(item);
|
const name = entityName(item);
|
||||||
if (type === 'note.saved' || type === 'note.edited') return tr('overview.event.noteEdited', { name });
|
if (type === 'note.saved' || type === 'note.edited') return `Edited note ${quoted(name)}`;
|
||||||
if (type === 'note.opened') return tr('overview.event.noteOpened', { name });
|
if (type === 'note.opened') return `Opened note ${quoted(name)}`;
|
||||||
if (type === 'note.created') return tr('overview.event.noteCreated', { name });
|
if (type === 'note.created') return `Created note ${quoted(name)}`;
|
||||||
if (type === 'file.opened') return tr('overview.event.fileOpened', { name });
|
if (type === 'file.opened') return `Opened file ${quoted(name)}`;
|
||||||
if (type === 'file.changed') return tr('overview.event.fileChanged', { name });
|
if (type === 'file.changed') return `Changed file ${quoted(name)}`;
|
||||||
if (type === 'file.created') return tr('overview.event.fileCreated', { name });
|
if (type === 'file.created') return `Created file ${quoted(name)}`;
|
||||||
if (type === 'file.deleted' || type === 'file.trashed') return tr('overview.event.fileRemoved', { name });
|
if (type === 'file.deleted' || type === 'file.trashed') return `Removed file ${quoted(name)}`;
|
||||||
if (type === 'browser.capture.page') return tr('overview.event.capturePage', { name });
|
if (type === 'browser.capture.page') return `Captured page ${quoted(name)}`;
|
||||||
if (type === 'browser.capture.selection') return tr('overview.event.captureSelection', { name });
|
if (type === 'browser.capture.selection') return `Captured selection ${quoted(name)}`;
|
||||||
if (type === 'browser.capture.link') return tr('overview.event.captureLink', { name });
|
if (type === 'browser.capture.link') return `Captured link ${quoted(name)}`;
|
||||||
if (type === 'browser.capture.file') return tr('overview.event.captureFile', { name });
|
if (type === 'browser.capture.file') return `Captured file ${quoted(name)}`;
|
||||||
if (type === 'browser.capture.converted') return tr('overview.event.captureConverted', { name });
|
if (type === 'browser.capture.converted') return `Converted capture ${quoted(name)}`;
|
||||||
if (type === 'journal.entry.added' || type === 'worklog.entry.added') return tr('overview.event.journalAdded', { name });
|
if (type === 'journal.entry.added' || type === 'worklog.entry.added') return `Added journal entry ${quoted(name)}`;
|
||||||
if (type === 'action.started') return tr('overview.event.workSessionDetected');
|
if (type === 'action.started') return 'Work session detected';
|
||||||
if (type === 'workspace.opened') return tr('overview.event.workspaceOpened');
|
if (type === 'workspace.opened') return 'Workspace opened';
|
||||||
return item?.title || item?.summary || tr('overview.event.activity');
|
return item?.title || item?.summary || 'Workspace activity';
|
||||||
}
|
|
||||||
|
|
||||||
function captureKindLabel(capture) {
|
|
||||||
const kind = String(capture?.kind || '').toLowerCase();
|
|
||||||
if (kind === 'page') return tr('overview.captureKind.page');
|
|
||||||
if (kind === 'selection') return tr('overview.captureKind.selection');
|
|
||||||
if (kind === 'link') return tr('overview.captureKind.link');
|
|
||||||
if (kind === 'file') return tr('overview.captureKind.file');
|
|
||||||
return tr('overview.captureKind.item');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionForCategory(category) {
|
function actionForCategory(category) {
|
||||||
|
|
@ -352,7 +320,7 @@
|
||||||
id: item.activityId || `${item.type}:${timeValue(item)}`,
|
id: item.activityId || `${item.type}:${timeValue(item)}`,
|
||||||
category,
|
category,
|
||||||
title: activityTitle(item),
|
title: activityTitle(item),
|
||||||
meta: itemTimeLabel(item),
|
meta: `${itemTimeLabel(item)}${item.sourcePluginId ? ' · ' + item.sourcePluginId.replace('verstak.', '') : ''}`,
|
||||||
time: timeValue(item),
|
time: timeValue(item),
|
||||||
absolute: absoluteTime(timeValue(item)),
|
absolute: absoluteTime(timeValue(item)),
|
||||||
actionKind: action.kind,
|
actionKind: action.kind,
|
||||||
|
|
@ -362,8 +330,8 @@
|
||||||
const captureItems = captureRows.map(item => ({
|
const captureItems = captureRows.map(item => ({
|
||||||
id: item.captureId || `capture:${timeValue(item)}`,
|
id: item.captureId || `capture:${timeValue(item)}`,
|
||||||
category: 'captures',
|
category: 'captures',
|
||||||
title: tr('overview.event.capture', { kind: captureKindLabel(item), title: captureTitle(item) }),
|
title: `Captured ${item.kind || 'item'} ${quoted(captureTitle(item))}`,
|
||||||
meta: `${itemTimeLabel(item)} · ${item.domain || item.url || tr('overview.browserCapture')}`,
|
meta: `${itemTimeLabel(item)} · ${item.domain || item.url || 'Browser capture'}`,
|
||||||
time: timeValue(item),
|
time: timeValue(item),
|
||||||
absolute: absoluteTime(timeValue(item)),
|
absolute: absoluteTime(timeValue(item)),
|
||||||
actionKind: 'browser-inbox',
|
actionKind: 'browser-inbox',
|
||||||
|
|
@ -372,8 +340,8 @@
|
||||||
const journalItems = journalRows.map(item => ({
|
const journalItems = journalRows.map(item => ({
|
||||||
id: item.entryId || `journal:${timeValue(item)}`,
|
id: item.entryId || `journal:${timeValue(item)}`,
|
||||||
category: 'journal',
|
category: 'journal',
|
||||||
title: tr('overview.event.journalAdded', { name: journalTitle(item) }),
|
title: `Added journal entry ${quoted(journalTitle(item))}`,
|
||||||
meta: `${itemTimeLabel(item)}${item.minutes ? ' · ' + tr('overview.minutes', { count: item.minutes }) : ''}`,
|
meta: `${itemTimeLabel(item)}${item.minutes ? ' · ' + item.minutes + ' min' : ''}`,
|
||||||
time: timeValue(item),
|
time: timeValue(item),
|
||||||
absolute: absoluteTime(timeValue(item)),
|
absolute: absoluteTime(timeValue(item)),
|
||||||
actionKind: 'journal',
|
actionKind: 'journal',
|
||||||
|
|
@ -413,8 +381,8 @@
|
||||||
const todoCandidates = [...todoRows].sort((a, b) => todoDateMs(todoTimeValue(a)) - todoDateMs(todoTimeValue(b))).map(item => ({
|
const todoCandidates = [...todoRows].sort((a, b) => todoDateMs(todoTimeValue(a)) - todoDateMs(todoTimeValue(b))).map(item => ({
|
||||||
id: item.id || `todo:${todoTitle(item)}`,
|
id: item.id || `todo:${todoTitle(item)}`,
|
||||||
category: 'todo',
|
category: 'todo',
|
||||||
title: tr('overview.event.todo', { title: todoTitle(item) }),
|
title: `Todo ${quoted(todoTitle(item))}`,
|
||||||
meta: `${todoAttentionState(item)}${item.dueAt ? ` · ${tr('overview.todo.due', { date: item.dueAt })}` : ''}`,
|
meta: `${todoAttentionState(item)}${item.dueAt ? ` · Due ${item.dueAt}` : ''}`,
|
||||||
time: todoTimeValue(item),
|
time: todoTimeValue(item),
|
||||||
absolute: absoluteTime(todoTimeValue(item)),
|
absolute: absoluteTime(todoTimeValue(item)),
|
||||||
actionKind: 'todo',
|
actionKind: 'todo',
|
||||||
|
|
@ -423,8 +391,8 @@
|
||||||
const captureCandidates = sortByTime(captureRows).map(item => ({
|
const captureCandidates = sortByTime(captureRows).map(item => ({
|
||||||
id: item.captureId || `capture:${timeValue(item)}`,
|
id: item.captureId || `capture:${timeValue(item)}`,
|
||||||
category: 'captures',
|
category: 'captures',
|
||||||
title: tr('overview.event.reviewCapture', { title: captureTitle(item) }),
|
title: `Review capture ${quoted(captureTitle(item))}`,
|
||||||
meta: `${itemTimeLabel(item)} · ${item.domain || captureKindLabel(item) || tr('overview.browserCapture')}`,
|
meta: `${itemTimeLabel(item)} · ${item.domain || item.kind || 'Browser capture'}`,
|
||||||
time: timeValue(item),
|
time: timeValue(item),
|
||||||
absolute: absoluteTime(timeValue(item)),
|
absolute: absoluteTime(timeValue(item)),
|
||||||
actionKind: 'browser-inbox',
|
actionKind: 'browser-inbox',
|
||||||
|
|
@ -439,7 +407,7 @@
|
||||||
const journalCandidates = sortByTime(journalRows).map(item => ({
|
const journalCandidates = sortByTime(journalRows).map(item => ({
|
||||||
id: item.entryId || `journal:${timeValue(item)}`,
|
id: item.entryId || `journal:${timeValue(item)}`,
|
||||||
category: 'journal',
|
category: 'journal',
|
||||||
title: tr('overview.event.continueJournal', { title: journalTitle(item) }),
|
title: `Continue journal entry ${quoted(journalTitle(item))}`,
|
||||||
meta: itemTimeLabel(item),
|
meta: itemTimeLabel(item),
|
||||||
time: timeValue(item),
|
time: timeValue(item),
|
||||||
absolute: absoluteTime(timeValue(item)),
|
absolute: absoluteTime(timeValue(item)),
|
||||||
|
|
@ -453,18 +421,14 @@
|
||||||
const captureItems = sortByTime(captureRows).slice(0, 4).map(item => ({
|
const captureItems = sortByTime(captureRows).slice(0, 4).map(item => ({
|
||||||
id: item.captureId || `capture:${timeValue(item)}`,
|
id: item.captureId || `capture:${timeValue(item)}`,
|
||||||
title: captureTitle(item),
|
title: captureTitle(item),
|
||||||
meta: `${captureKindLabel(item)} · ${itemTimeLabel(item)}`,
|
meta: `${item.kind || 'capture'} · ${itemTimeLabel(item)}`,
|
||||||
actionKind: 'browser-inbox',
|
actionKind: 'browser-inbox',
|
||||||
actionLabel: tr('overview.reviewInbox'),
|
actionLabel: tr('overview.reviewInbox'),
|
||||||
}));
|
}));
|
||||||
const candidateItems = sortByTime(candidates).slice(0, 4).map(item => ({
|
const candidateItems = sortByTime(candidates).slice(0, 4).map(item => ({
|
||||||
id: item.candidateId || `work-session:${timeValue(item)}`,
|
id: item.candidateId || `work-session:${timeValue(item)}`,
|
||||||
title: tr('overview.event.possibleJournalEntry'),
|
title: 'Possible journal entry',
|
||||||
meta: tr('overview.candidateMeta', {
|
meta: `Workspace: ${item.workspaceRootPath || workspaceRootPath || 'Unknown'} · ${item.estimatedMinutes || 0} min · ${item.activityCount || (item.activityIds || []).length || 0} activities`,
|
||||||
deal: item.workspaceRootPath || workspaceRootPath || tr('overview.unknownDeal'),
|
|
||||||
minutes: item.estimatedMinutes || 0,
|
|
||||||
activities: item.activityCount || (item.activityIds || []).length || 0,
|
|
||||||
}),
|
|
||||||
actionKind: 'journal',
|
actionKind: 'journal',
|
||||||
actionLabel: tr('overview.reviewCandidate'),
|
actionLabel: tr('overview.reviewCandidate'),
|
||||||
toolRequest: { type: 'work-session-candidate', candidate: item },
|
toolRequest: { type: 'work-session-candidate', candidate: item },
|
||||||
|
|
@ -472,7 +436,7 @@
|
||||||
const todoItems = [...todoRows].sort((a, b) => todoDateMs(todoTimeValue(a)) - todoDateMs(todoTimeValue(b))).slice(0, 4).map(item => ({
|
const todoItems = [...todoRows].sort((a, b) => todoDateMs(todoTimeValue(a)) - todoDateMs(todoTimeValue(b))).slice(0, 4).map(item => ({
|
||||||
id: item.id || `todo:${todoTitle(item)}`,
|
id: item.id || `todo:${todoTitle(item)}`,
|
||||||
title: todoTitle(item),
|
title: todoTitle(item),
|
||||||
meta: `${todoAttentionState(item)}${item.dueAt ? ` · ${tr('overview.todo.due', { date: item.dueAt })}` : ''}`,
|
meta: `${todoAttentionState(item)}${item.dueAt ? ` · Due ${item.dueAt}` : ''}`,
|
||||||
actionKind: 'todo',
|
actionKind: 'todo',
|
||||||
actionLabel: tr('overview.openTodos'),
|
actionLabel: tr('overview.openTodos'),
|
||||||
}));
|
}));
|
||||||
|
|
@ -513,12 +477,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkspaceResources(toolState) {
|
async function loadWorkspaceResources() {
|
||||||
const workspace = String(workspaceRootPath || '').trim();
|
const workspace = String(workspaceRootPath || '').trim();
|
||||||
if (!workspace) return { keyResources: [], totalNotes: 0 };
|
if (!workspace) return { keyResources: [], totalNotes: 0 };
|
||||||
const [rootEntries, notesEntries] = await Promise.all([
|
const [rootEntries, notesEntries] = await Promise.all([
|
||||||
toolState.files ? listFiles('verstak.files', workspace) : Promise.resolve([]),
|
listFiles('verstak.files', workspace),
|
||||||
toolState.notes ? listFiles('verstak.notes', `${workspace}/Notes`) : Promise.resolve([]),
|
listFiles('verstak.notes', `${workspace}/Notes`),
|
||||||
]);
|
]);
|
||||||
const noteFiles = notesEntries.filter(item => item?.type === 'file');
|
const noteFiles = notesEntries.filter(item => item?.type === 'file');
|
||||||
const overview = [...notesEntries, ...rootEntries].find(item => /(^|\/)overview\.md$/i.test(String(item.relativePath || item.name || '')));
|
const overview = [...notesEntries, ...rootEntries].find(item => /(^|\/)overview\.md$/i.test(String(item.relativePath || item.name || '')));
|
||||||
|
|
@ -527,54 +491,44 @@
|
||||||
keyResources: overview ? [{
|
keyResources: overview ? [{
|
||||||
id: overview.relativePath || overview.name,
|
id: overview.relativePath || overview.name,
|
||||||
title: overview.name || fileName(overview.relativePath) || 'Overview.md',
|
title: overview.name || fileName(overview.relativePath) || 'Overview.md',
|
||||||
meta: overview.relativePath || tr('overview.overviewNote'),
|
meta: overview.relativePath || 'Workspace overview note',
|
||||||
actionKind: toolState.notes ? 'notes' : 'files',
|
actionKind: hasNotes ? 'notes' : 'files',
|
||||||
actionLabel: toolState.notes ? tr('overview.openNotes') : tr('overview.openFiles'),
|
actionLabel: hasNotes ? tr('overview.openNotes') : tr('overview.openFiles'),
|
||||||
}] : [],
|
}] : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadOverview() {
|
async function loadOverview() {
|
||||||
const workspaceAtStart = String(workspaceRootPath || '').trim();
|
const workspaceAtStart = String(workspaceRootPath || '').trim();
|
||||||
const toolKeyAtStart = overviewToolKey;
|
|
||||||
const toolState = {
|
|
||||||
notes: hasNotes,
|
|
||||||
files: hasFiles,
|
|
||||||
browserInbox: hasBrowserInbox,
|
|
||||||
activity: hasActivity,
|
|
||||||
journal: hasJournal,
|
|
||||||
todos: hasTodos,
|
|
||||||
};
|
|
||||||
loadedWorkspaceRoot = workspaceAtStart;
|
loadedWorkspaceRoot = workspaceAtStart;
|
||||||
loadedToolKey = toolKeyAtStart;
|
|
||||||
loading = true;
|
loading = true;
|
||||||
const [browserSettings, activitySettings, activityRecords, journalSettings, todoSettings, resources] = await Promise.all([
|
const [browserSettings, activitySettings, activityRecords, journalSettings, todoSettings, resources] = await Promise.all([
|
||||||
toolState.browserInbox ? readPluginSettings('verstak.browser-inbox') : Promise.resolve({}),
|
readPluginSettings('verstak.browser-inbox'),
|
||||||
toolState.activity ? readPluginSettings('verstak.activity') : Promise.resolve({}),
|
readPluginSettings('verstak.activity'),
|
||||||
toolState.activity && App.ReadPluginDataNDJSON
|
App.ReadPluginDataNDJSON
|
||||||
? App.ReadPluginDataNDJSON('verstak.activity', 'activity-events')
|
? App.ReadPluginDataNDJSON('verstak.activity', 'activity-events')
|
||||||
.then(value => decodeTuple(value, []))
|
.then(value => decodeTuple(value, []))
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
toolState.journal ? readPluginSettings('verstak.journal') : Promise.resolve({}),
|
readPluginSettings('verstak.journal'),
|
||||||
toolState.todos ? readPluginSettings('verstak.todo') : Promise.resolve({}),
|
readPluginSettings('verstak.todo'),
|
||||||
loadWorkspaceResources(toolState),
|
loadWorkspaceResources(),
|
||||||
]);
|
]);
|
||||||
if (workspaceAtStart !== String(workspaceRootPath || '').trim() || toolKeyAtStart !== overviewToolKey) return;
|
if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
|
||||||
|
|
||||||
captures = toolState.browserInbox ? browserCaptureRowsForWorkspace(browserSettings) : [];
|
captures = browserCaptureRowsForWorkspace(browserSettings);
|
||||||
activityEvents = toolState.activity ? normalizeRows(activityRecords).filter(item => {
|
activityEvents = normalizeRows(activityRecords).filter(item => {
|
||||||
const tagged = String(item.workspaceRootPath || item.workspaceName || item.workspaceNodeId || '').trim();
|
const tagged = String(item.workspaceRootPath || item.workspaceName || item.workspaceNodeId || '').trim();
|
||||||
return !tagged || tagged === workspaceAtStart;
|
return !tagged || tagged === workspaceAtStart;
|
||||||
}) : [];
|
});
|
||||||
journalEntries = toolState.journal ? rowsFor(journalSettings, [
|
journalEntries = rowsFor(journalSettings, [
|
||||||
workspaceKey('worklog:workspace:'),
|
workspaceKey('worklog:workspace:'),
|
||||||
'worklog',
|
'worklog',
|
||||||
]) : [];
|
]);
|
||||||
todos = toolState.todos ? todoRowsForWorkspace(todoSettings) : [];
|
todos = todoRowsForWorkspace(todoSettings);
|
||||||
workSessionCandidates = toolState.activity && toolState.journal ? rowsFor(activitySettings, [
|
workSessionCandidates = rowsFor(activitySettings, [
|
||||||
workspaceKey('work-session-candidates:workspace:'),
|
workspaceKey('work-session-candidates:workspace:'),
|
||||||
]) : [];
|
]);
|
||||||
keyResources = resources.keyResources;
|
keyResources = resources.keyResources;
|
||||||
totalNotes = resources.totalNotes;
|
totalNotes = resources.totalNotes;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
@ -703,7 +657,7 @@
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<aside class="overview-side">
|
<aside class="overview-side">
|
||||||
{#if hasAttentionTools && (needsAttention.length || !loading)}
|
{#if needsAttention.length || !loading}
|
||||||
<section class="today-panel overview-panel" data-overview-section="attention">
|
<section class="today-panel overview-panel" data-overview-section="attention">
|
||||||
<div class="today-panel-head overview-panel-head">
|
<div class="today-panel-head overview-panel-head">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,6 @@
|
||||||
void activeLocale;
|
void activeLocale;
|
||||||
return i18n.t(key, params, fallback);
|
return i18n.t(key, params, fallback);
|
||||||
})(locale);
|
})(locale);
|
||||||
|
|
||||||
function reportError(key, fallback, details) {
|
|
||||||
console.warn('[VaultSelection] operation failed:', details);
|
|
||||||
return tr(key, undefined, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(unsubscribeLocale);
|
onDestroy(unsubscribeLocale);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
|
@ -60,13 +54,13 @@
|
||||||
try {
|
try {
|
||||||
const createErr = await App.CreateVault(newVaultPath.trim());
|
const createErr = await App.CreateVault(newVaultPath.trim());
|
||||||
if (createErr) {
|
if (createErr) {
|
||||||
error = reportError('vaultSelection.createError', 'Could not create the vault. Please try again.', createErr);
|
error = tr('vaultSelection.createError', { error: createErr });
|
||||||
creating = false;
|
creating = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const openErr = await App.OpenVault(newVaultPath.trim());
|
const openErr = await App.OpenVault(newVaultPath.trim());
|
||||||
if (openErr) {
|
if (openErr) {
|
||||||
error = reportError('vaultSelection.openError', 'Could not open the vault. Please try again.', openErr);
|
error = tr('vaultSelection.openError', { error: openErr });
|
||||||
creating = false;
|
creating = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +70,7 @@
|
||||||
}
|
}
|
||||||
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = reportError('vaultSelection.createError', 'Could not create the vault. Please try again.', e);
|
error = String(e);
|
||||||
creating = false;
|
creating = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +85,7 @@
|
||||||
try {
|
try {
|
||||||
const openErr = await App.OpenVault(openVaultPath.trim());
|
const openErr = await App.OpenVault(openVaultPath.trim());
|
||||||
if (openErr) {
|
if (openErr) {
|
||||||
error = reportError('vaultSelection.openError', 'Could not open the vault. Please try again.', openErr);
|
error = tr('vaultSelection.openError', { error: openErr });
|
||||||
opening = false;
|
opening = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +95,7 @@
|
||||||
}
|
}
|
||||||
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = reportError('vaultSelection.openError', 'Could not open the vault. Please try again.', e);
|
error = String(e);
|
||||||
opening = false;
|
opening = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +106,7 @@
|
||||||
try {
|
try {
|
||||||
const openErr = await App.OpenVault(path);
|
const openErr = await App.OpenVault(path);
|
||||||
if (openErr) {
|
if (openErr) {
|
||||||
error = reportError('vaultSelection.openError', 'Could not open the vault. Please try again.', openErr);
|
error = tr('vaultSelection.openError', { error: openErr });
|
||||||
opening = false;
|
opening = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +116,7 @@
|
||||||
}
|
}
|
||||||
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
window.dispatchEvent(new CustomEvent('verstak:vault-opened'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = reportError('vaultSelection.openError', 'Could not open the vault. Please try again.', e);
|
error = String(e);
|
||||||
opening = false;
|
opening = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@
|
||||||
$: requestContext = openedResource?.request?.context?.notesMode || openedResource?.request?.context?.isInsideNotesFolder
|
$: requestContext = openedResource?.request?.context?.notesMode || openedResource?.request?.context?.isInsideNotesFolder
|
||||||
? 'notes-markdown'
|
? 'notes-markdown'
|
||||||
: ((openedResource?.request?.extension === '.md' || openedResource?.request?.extension === '.markdown') ? 'generic-markdown' : 'generic-text');
|
: ((openedResource?.request?.extension === '.md' || openedResource?.request?.extension === '.markdown') ? 'generic-markdown' : 'generic-text');
|
||||||
$: resourceTitle = resourceDisplayTitle(resourcePath, requestContext);
|
|
||||||
$: componentProps = openedResource || {};
|
$: componentProps = openedResource || {};
|
||||||
$: mountKey = [
|
$: mountKey = [
|
||||||
providerPluginId,
|
providerPluginId,
|
||||||
|
|
@ -32,11 +31,6 @@
|
||||||
requestContext,
|
requestContext,
|
||||||
].join(':');
|
].join(':');
|
||||||
|
|
||||||
function resourceDisplayTitle(path, context) {
|
|
||||||
const fileName = String(path || '').split('/').filter(Boolean).pop() || String(path || '');
|
|
||||||
return context === 'notes-markdown' ? fileName.replace(/\.(md|markdown)$/i, '') : fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeWorkbench() {
|
function closeWorkbench() {
|
||||||
window.dispatchEvent(new CustomEvent('verstak:close-workbench', { cancelable: true }));
|
window.dispatchEvent(new CustomEvent('verstak:close-workbench', { cancelable: true }));
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +41,7 @@
|
||||||
<div class="workbench-host vt-page">
|
<div class="workbench-host vt-page">
|
||||||
{#if openedResource?.status === 'no-provider'}
|
{#if openedResource?.status === 'no-provider'}
|
||||||
<div class="workbench-header vt-page-header">
|
<div class="workbench-header vt-page-header">
|
||||||
<span class="workbench-title vt-page-title">{resourceTitle}</span>
|
<span class="workbench-title vt-page-title">{resourcePath}</span>
|
||||||
<span class="workbench-provider vt-badge">{tr('workbench.noProvider')}</span>
|
<span class="workbench-provider vt-badge">{tr('workbench.noProvider')}</span>
|
||||||
<button class="close-btn btn-ghost btn-icon" type="button" title={tr('common.close')} aria-label={tr('common.close')} on:click={closeWorkbench}>
|
<button class="close-btn btn-ghost btn-icon" type="button" title={tr('common.close')} aria-label={tr('common.close')} on:click={closeWorkbench}>
|
||||||
<Icon name="x" size={18} />
|
<Icon name="x" size={18} />
|
||||||
|
|
@ -55,13 +49,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="workbench-empty no-provider vt-empty-state" data-workbench-status="no-provider">
|
<div class="workbench-empty no-provider vt-empty-state" data-workbench-status="no-provider">
|
||||||
<p class="vt-empty-title">{tr('workbench.noViewer')}</p>
|
<p class="vt-empty-title">{tr('workbench.noViewer')}</p>
|
||||||
{#if debug.isEnabled()}
|
<p class="workbench-meta">{requestMode} · {requestContext}</p>
|
||||||
<p class="workbench-meta">{requestMode} · {requestContext}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:else if openedResource}
|
{:else if openedResource}
|
||||||
<div class="workbench-header vt-page-header">
|
<div class="workbench-header vt-page-header">
|
||||||
<span class="workbench-title vt-page-title">{resourceTitle}</span>
|
<span class="workbench-title vt-page-title">{resourcePath}</span>
|
||||||
{#if debug.isEnabled() && providerId}
|
{#if debug.isEnabled() && providerId}
|
||||||
<span class="workbench-provider vt-badge accent" data-debug-provider>{providerId}</span>
|
<span class="workbench-provider vt-badge accent" data-debug-provider>{providerId}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
$: workspaceRootPath = selectedWorkspace?.rootPath || selectedWorkspace?.name || selectedWorkspace?.id || '';
|
$: workspaceRootPath = selectedWorkspace?.rootPath || selectedWorkspace?.name || selectedWorkspace?.id || '';
|
||||||
$: workspaceId = selectedWorkspace?.workspaceId || '';
|
$: workspaceId = selectedWorkspace?.workspaceId || '';
|
||||||
$: workspaceTitle = selectedWorkspace?.title || selectedWorkspace?.name || selectedWorkspace?.id || selectedWorkspaceName;
|
$: workspaceTitle = selectedWorkspace?.title || selectedWorkspace?.name || selectedWorkspace?.id || selectedWorkspaceName;
|
||||||
|
$: workspaceType = selectedWorkspace?.type || 'workspace';
|
||||||
$: if (workspaceRootPath !== metadataWorkspaceRoot) {
|
$: if (workspaceRootPath !== metadataWorkspaceRoot) {
|
||||||
metadataWorkspaceRoot = workspaceRootPath;
|
metadataWorkspaceRoot = workspaceRootPath;
|
||||||
workspaceMetadata = null;
|
workspaceMetadata = null;
|
||||||
|
|
@ -79,13 +80,11 @@
|
||||||
if (changed && selectedWorkspaceName) loadTools();
|
if (changed && selectedWorkspaceName) loadTools();
|
||||||
});
|
});
|
||||||
window.addEventListener('verstak:workspace-open-tool', handleWorkspaceOpenTool);
|
window.addEventListener('verstak:workspace-open-tool', handleWorkspaceOpenTool);
|
||||||
window.addEventListener('verstak:plugins-changed', handlePluginsChanged);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (unsubscribeLocale) unsubscribeLocale();
|
if (unsubscribeLocale) unsubscribeLocale();
|
||||||
window.removeEventListener('verstak:workspace-open-tool', handleWorkspaceOpenTool);
|
window.removeEventListener('verstak:workspace-open-tool', handleWorkspaceOpenTool);
|
||||||
window.removeEventListener('verstak:plugins-changed', handlePluginsChanged);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function toolKey(tool) {
|
function toolKey(tool) {
|
||||||
|
|
@ -171,10 +170,6 @@
|
||||||
requestWorkspaceTool(event?.detail?.kind, event?.detail?.toolRequest || null);
|
requestWorkspaceTool(event?.detail?.kind, event?.detail?.toolRequest || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePluginsChanged() {
|
|
||||||
if (selectedWorkspaceName) loadTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTools() {
|
async function loadTools() {
|
||||||
try {
|
try {
|
||||||
toolsLoaded = false;
|
toolsLoaded = false;
|
||||||
|
|
@ -206,6 +201,7 @@
|
||||||
<div class="workspace-header vt-page-header">
|
<div class="workspace-header vt-page-header">
|
||||||
<div class="workspace-title-group">
|
<div class="workspace-title-group">
|
||||||
<span class="workspace-title vt-page-title">{workspaceTitle}</span>
|
<span class="workspace-title vt-page-title">{workspaceTitle}</span>
|
||||||
|
<span class="workspace-type vt-badge accent">{workspaceType}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workspace-search" aria-label={tr('workspace.search')}>
|
<div class="workspace-search" aria-label={tr('workspace.search')}>
|
||||||
<GlobalSearch />
|
<GlobalSearch />
|
||||||
|
|
@ -292,6 +288,14 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-type {
|
||||||
|
color: var(--vt-color-accent);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: var(--vt-radius-sm);
|
||||||
|
background: var(--vt-color-accent-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-search {
|
.workspace-search {
|
||||||
width: min(27rem, 46vw);
|
width: min(27rem, 46vw);
|
||||||
min-width: 16rem;
|
min-width: 16rem;
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,8 @@
|
||||||
let newWorkspaceName = '';
|
let newWorkspaceName = '';
|
||||||
let workspaceTemplates = [];
|
let workspaceTemplates = [];
|
||||||
let templatePluginNames = {};
|
let templatePluginNames = {};
|
||||||
let templatePlugins = {};
|
|
||||||
let templateCapabilities = new Set();
|
|
||||||
let selectedTemplateId = 'default';
|
let selectedTemplateId = 'default';
|
||||||
let createError = '';
|
let createError = '';
|
||||||
let templateWarning = null;
|
|
||||||
let templatesLoading = false;
|
let templatesLoading = false;
|
||||||
let creating = false;
|
let creating = false;
|
||||||
let renamingId = '';
|
let renamingId = '';
|
||||||
|
|
@ -35,11 +32,6 @@
|
||||||
return i18n.t(key, params, fallback);
|
return i18n.t(key, params, fallback);
|
||||||
})(locale);
|
})(locale);
|
||||||
|
|
||||||
function reportError(key, fallback, details) {
|
|
||||||
console.warn('[WorkspaceTree] operation failed:', details);
|
|
||||||
return tr(key, undefined, fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
unsubscribeLocale = i18n.subscribe((nextLocale) => {
|
unsubscribeLocale = i18n.subscribe((nextLocale) => {
|
||||||
const changed = locale !== nextLocale;
|
const changed = locale !== nextLocale;
|
||||||
|
|
@ -69,71 +61,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selectedTemplate = workspaceTemplates.find(template => template.id === selectedTemplateId) || workspaceTemplates[0] || null;
|
$: selectedTemplate = workspaceTemplates.find(template => template.id === selectedTemplateId) || workspaceTemplates[0] || null;
|
||||||
$: selectedTemplateTools = (selectedTemplate?.workspaceTools || []).map((pluginId) => (
|
|
||||||
templateToolState(pluginId, templatePlugins, templateCapabilities, templatePluginNames, tr)
|
|
||||||
));
|
|
||||||
$: selectedTemplateIssues = selectedTemplateTools.filter(tool => tool.status !== 'available');
|
|
||||||
|
|
||||||
function toolLabel(pluginId, names = templatePluginNames) {
|
function toolLabel(pluginId) {
|
||||||
return names[pluginId] || String(pluginId || '').replace(/^verstak\./, '');
|
return templatePluginNames[pluginId] || String(pluginId || '').replace(/^verstak\./, '');
|
||||||
}
|
|
||||||
|
|
||||||
function templateToolState(pluginId, plugins, capabilities, names, translate) {
|
|
||||||
const plugin = plugins[pluginId];
|
|
||||||
if (!plugin) {
|
|
||||||
return {
|
|
||||||
pluginId,
|
|
||||||
name: toolLabel(pluginId, names),
|
|
||||||
tabs: [],
|
|
||||||
status: 'unavailable',
|
|
||||||
reason: translate('workspaceTree.templateMissingPlugin'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifest = plugin.manifest || {};
|
|
||||||
const tabs = Array.isArray(manifest.contributes?.workspaceItems)
|
|
||||||
? manifest.contributes.workspaceItems.map(item => item?.title || item?.id).filter(Boolean)
|
|
||||||
: [];
|
|
||||||
const pluginStatus = String(plugin.status || '').toLowerCase();
|
|
||||||
const missingCapability = Array.isArray(manifest.requires)
|
|
||||||
&& manifest.requires.some(capabilityId => !capabilities.has(capabilityId));
|
|
||||||
let status = 'available';
|
|
||||||
let reason = translate('workspaceTree.templateAvailable');
|
|
||||||
|
|
||||||
if (!plugin.enabled || pluginStatus === 'disabled') {
|
|
||||||
status = 'unavailable';
|
|
||||||
reason = translate('workspaceTree.templatePluginDisabled');
|
|
||||||
} else if (pluginStatus === 'missing-required-capability' || missingCapability) {
|
|
||||||
status = 'unavailable';
|
|
||||||
reason = translate('workspaceTree.templateCapabilityUnavailable');
|
|
||||||
} else if (pluginStatus === 'incompatible') {
|
|
||||||
status = 'unavailable';
|
|
||||||
reason = translate('workspaceTree.templateIncompatible');
|
|
||||||
} else if (pluginStatus === 'failed') {
|
|
||||||
status = 'unavailable';
|
|
||||||
reason = translate('workspaceTree.templateLoadFailed');
|
|
||||||
} else if (pluginStatus === 'degraded') {
|
|
||||||
status = 'limited';
|
|
||||||
reason = translate('workspaceTree.templateLimited');
|
|
||||||
} else if (pluginStatus !== 'loaded') {
|
|
||||||
status = 'unavailable';
|
|
||||||
reason = translate('workspaceTree.templateNotReady');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { pluginId, name: manifest.name || toolLabel(pluginId, names), tabs, status, reason };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWorkspaceTemplates() {
|
async function loadWorkspaceTemplates() {
|
||||||
templatesLoading = true;
|
templatesLoading = true;
|
||||||
try {
|
try {
|
||||||
const [templates, plugins, capabilities] = await Promise.all([
|
const [templates, plugins] = await Promise.all([
|
||||||
App.ListWorkspaceTemplates ? App.ListWorkspaceTemplates() : [],
|
App.ListWorkspaceTemplates ? App.ListWorkspaceTemplates() : [],
|
||||||
App.GetPlugins ? App.GetPlugins() : [],
|
App.GetPlugins ? App.GetPlugins() : [],
|
||||||
App.GetCapabilities ? App.GetCapabilities() : [],
|
|
||||||
]);
|
]);
|
||||||
const [list, err] = resultOrError(templates, []);
|
const [list, err] = resultOrError(templates, []);
|
||||||
if (err) {
|
if (err) {
|
||||||
createError = reportError('workspaceTree.templatesError', 'Could not load Deal templates. Please try again.', err);
|
createError = err;
|
||||||
workspaceTemplates = [];
|
workspaceTemplates = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -141,25 +83,17 @@
|
||||||
await Promise.all((Array.isArray(plugins) ? plugins : []).map((plugin) => (
|
await Promise.all((Array.isArray(plugins) ? plugins : []).map((plugin) => (
|
||||||
i18n.loadPlugin(plugin.manifest?.id, plugin.manifest?.localization).catch(() => {})
|
i18n.loadPlugin(plugin.manifest?.id, plugin.manifest?.localization).catch(() => {})
|
||||||
)));
|
)));
|
||||||
const localizedPlugins = (Array.isArray(plugins) ? plugins : []).map((plugin) => i18n.localizePlugin(plugin));
|
templatePluginNames = (Array.isArray(plugins) ? plugins : []).map((plugin) => i18n.localizePlugin(plugin)).reduce((names, plugin) => {
|
||||||
templatePluginNames = localizedPlugins.reduce((names, plugin) => {
|
|
||||||
const id = plugin?.manifest?.id;
|
const id = plugin?.manifest?.id;
|
||||||
const name = plugin?.manifest?.name;
|
const name = plugin?.manifest?.name;
|
||||||
if (id && name) names[id] = name;
|
if (id && name) names[id] = name;
|
||||||
return names;
|
return names;
|
||||||
}, {});
|
}, {});
|
||||||
templatePlugins = localizedPlugins.reduce((result, plugin) => {
|
|
||||||
const id = plugin?.manifest?.id;
|
|
||||||
if (id) result[id] = plugin;
|
|
||||||
return result;
|
|
||||||
}, {});
|
|
||||||
const [capabilityList] = resultOrError(capabilities, []);
|
|
||||||
templateCapabilities = new Set((Array.isArray(capabilityList) ? capabilityList : []).map(capability => capability?.name).filter(Boolean));
|
|
||||||
if (!workspaceTemplates.some(template => template.id === selectedTemplateId)) {
|
if (!workspaceTemplates.some(template => template.id === selectedTemplateId)) {
|
||||||
selectedTemplateId = workspaceTemplates[0]?.id || '';
|
selectedTemplateId = workspaceTemplates[0]?.id || '';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
createError = reportError('workspaceTree.templatesError', 'Could not load Deal templates. Please try again.', error);
|
createError = String(error);
|
||||||
workspaceTemplates = [];
|
workspaceTemplates = [];
|
||||||
} finally {
|
} finally {
|
||||||
templatesLoading = false;
|
templatesLoading = false;
|
||||||
|
|
@ -193,7 +127,7 @@
|
||||||
try {
|
try {
|
||||||
const [list, err] = resultOrError(await App.ListWorkspaces(), []);
|
const [list, err] = resultOrError(await App.ListWorkspaces(), []);
|
||||||
if (err) {
|
if (err) {
|
||||||
localError = reportError('workspaceTree.loadError', 'Could not load Deals. Please try again.', err);
|
localError = err;
|
||||||
workspaces = [];
|
workspaces = [];
|
||||||
} else {
|
} else {
|
||||||
workspaces = list || [];
|
workspaces = list || [];
|
||||||
|
|
@ -214,7 +148,7 @@
|
||||||
activeWorkspaceId.set(currentWorkspaceId);
|
activeWorkspaceId.set(currentWorkspaceId);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
localError = reportError('workspaceTree.loadError', 'Could not load Deals. Please try again.', e);
|
localError = String(e);
|
||||||
}
|
}
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +157,7 @@
|
||||||
const id = wsName(workspace);
|
const id = wsName(workspace);
|
||||||
const err = await App.SetCurrentWorkspace(id);
|
const err = await App.SetCurrentWorkspace(id);
|
||||||
if (err) {
|
if (err) {
|
||||||
localError = reportError('workspaceTree.selectError', 'Could not select this Deal. Please try again.', err);
|
localError = err;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentWorkspaceId = id;
|
currentWorkspaceId = id;
|
||||||
|
|
@ -243,16 +177,11 @@
|
||||||
createError = tr('workspaceTree.chooseTemplate');
|
createError = tr('workspaceTree.chooseTemplate');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const creationIssues = selectedTemplateIssues.map(tool => ({
|
|
||||||
pluginId: tool.pluginId,
|
|
||||||
name: tool.name,
|
|
||||||
reason: tool.reason,
|
|
||||||
}));
|
|
||||||
creating = true;
|
creating = true;
|
||||||
createError = '';
|
createError = '';
|
||||||
const [, err] = resultOrError(await App.CreateWorkspace(name, selectedTemplate.id), null);
|
const [, err] = resultOrError(await App.CreateWorkspace(name, selectedTemplate.id), null);
|
||||||
if (err) {
|
if (err) {
|
||||||
createError = reportError('workspaceTree.createError', 'Could not create the Deal. Please try again.', err);
|
createError = err;
|
||||||
creating = false;
|
creating = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -262,14 +191,13 @@
|
||||||
await loadWorkspaces();
|
await loadWorkspaces();
|
||||||
const created = workspaces.find((ws) => wsName(ws) === name);
|
const created = workspaces.find((ws) => wsName(ws) === name);
|
||||||
if (created) await selectWorkspace(created);
|
if (created) await selectWorkspace(created);
|
||||||
templateWarning = creationIssues.length > 0 ? { workspaceName: name, issues: creationIssues } : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openCreateDialog() {
|
function openCreateDialog() {
|
||||||
showCreate = true;
|
showCreate = true;
|
||||||
newWorkspaceName = '';
|
newWorkspaceName = '';
|
||||||
createError = '';
|
createError = '';
|
||||||
await loadWorkspaceTemplates();
|
if (!workspaceTemplates.length && !templatesLoading) loadWorkspaceTemplates();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeCreateDialog() {
|
function closeCreateDialog() {
|
||||||
|
|
@ -279,10 +207,6 @@
|
||||||
createError = '';
|
createError = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissTemplateWarning() {
|
|
||||||
templateWarning = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startRename(workspace) {
|
function startRename(workspace) {
|
||||||
renamingId = wsName(workspace);
|
renamingId = wsName(workspace);
|
||||||
renameValue = renamingId;
|
renameValue = renamingId;
|
||||||
|
|
@ -304,7 +228,7 @@
|
||||||
busyId = oldName;
|
busyId = oldName;
|
||||||
const err = await App.RenameWorkspace(oldName, newName);
|
const err = await App.RenameWorkspace(oldName, newName);
|
||||||
if (err) {
|
if (err) {
|
||||||
localError = reportError('workspaceTree.renameError', 'Could not rename the Deal. Please try again.', err);
|
localError = err;
|
||||||
busyId = '';
|
busyId = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +246,7 @@
|
||||||
busyId = name;
|
busyId = name;
|
||||||
const [, err] = resultOrError(await App.TrashWorkspace(name), null);
|
const [, err] = resultOrError(await App.TrashWorkspace(name), null);
|
||||||
if (err) {
|
if (err) {
|
||||||
localError = reportError('workspaceTree.trashError', 'Could not move the Deal to trash. Please try again.', err);
|
localError = err;
|
||||||
busyId = '';
|
busyId = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -364,7 +288,7 @@
|
||||||
if (e.key === 'Escape') cancelRename();
|
if (e.key === 'Escape') cancelRename();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button class="wt-btn wt-btn-small wt-always" on:click={() => commitRename(workspace)} title={tr('workspaceTree.saveRename')} type="button" disabled={busyId === id}>{tr('common.save')}</button>
|
<button class="wt-btn wt-btn-small wt-always" on:click={() => commitRename(workspace)} title={tr('workspaceTree.saveRename')} type="button" disabled={busyId === id}>OK</button>
|
||||||
<button class="wt-btn wt-btn-small wt-always" on:click={cancelRename} title={tr('common.cancel')} type="button" disabled={busyId === id}>{tr('common.cancel')}</button>
|
<button class="wt-btn wt-btn-small wt-always" on:click={cancelRename} title={tr('common.cancel')} type="button" disabled={busyId === id}>{tr('common.cancel')}</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button class="wt-label" on:click={() => selectWorkspace(workspace)} type="button">{id}</button>
|
<button class="wt-label" on:click={() => selectWorkspace(workspace)} type="button">{id}</button>
|
||||||
|
|
@ -380,18 +304,6 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if templateWarning}
|
|
||||||
<section class="workspace-template-warning" data-workspace-template-warning role="alert">
|
|
||||||
<div>
|
|
||||||
<strong>{tr('workspaceTree.templateIncompleteCreated', { name: templateWarning.workspaceName })}</strong>
|
|
||||||
{#each templateWarning.issues as issue (issue.pluginId)}
|
|
||||||
<div class="workspace-template-warning-item">{issue.name}: {issue.reason}</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<button class="wt-btn" type="button" on:click={dismissTemplateWarning} title={tr('common.close')}>{tr('common.close')}</button>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if showCreate}
|
{#if showCreate}
|
||||||
<div class="workspace-create-overlay" data-workspace-create-modal role="dialog" aria-modal="true" aria-label={tr('workspaceTree.create')}>
|
<div class="workspace-create-overlay" data-workspace-create-modal role="dialog" aria-modal="true" aria-label={tr('workspaceTree.create')}>
|
||||||
<div class="workspace-create-modal">
|
<div class="workspace-create-modal">
|
||||||
|
|
@ -417,23 +329,10 @@
|
||||||
<div class="workspace-template-summary">
|
<div class="workspace-template-summary">
|
||||||
<p data-workspace-template-description>{selectedTemplate.description}</p>
|
<p data-workspace-template-description>{selectedTemplate.description}</p>
|
||||||
<div class="workspace-template-tools" data-workspace-template-tools>
|
<div class="workspace-template-tools" data-workspace-template-tools>
|
||||||
{#each selectedTemplateTools as tool (tool.pluginId)}
|
{#each selectedTemplate.workspaceTools || [] as pluginId (pluginId)}
|
||||||
<div
|
<span>{toolLabel(pluginId)}</span>
|
||||||
class="workspace-template-tool"
|
|
||||||
class:limited={tool.status === 'limited'}
|
|
||||||
class:unavailable={tool.status === 'unavailable'}
|
|
||||||
data-workspace-template-tool={tool.pluginId}
|
|
||||||
data-template-tool-status={tool.status}
|
|
||||||
>
|
|
||||||
<span class="workspace-template-tool-name">{tool.name}</span>
|
|
||||||
<span class="workspace-template-tool-tabs">{tool.tabs.length ? tr('workspaceTree.templateToolTabs', { tabs: tool.tabs.join(', ') }) : tr('workspaceTree.templateToolNoTabs')}</span>
|
|
||||||
<span class="workspace-template-tool-reason">{tool.reason}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if selectedTemplateIssues.length > 0}
|
|
||||||
<p class="workspace-template-incomplete" data-workspace-template-incomplete>{tr('workspaceTree.templateWillBeIncomplete')}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if createError}
|
{#if createError}
|
||||||
|
|
@ -479,25 +378,11 @@
|
||||||
.workspace-create-header h2 { margin: 0; font-size: 1rem; }
|
.workspace-create-header h2 { margin: 0; font-size: 1rem; }
|
||||||
.workspace-create-field { display: grid; gap: 0.35rem; color: var(--vt-color-text-muted); font-size: 0.75rem; }
|
.workspace-create-field { display: grid; gap: 0.35rem; color: var(--vt-color-text-muted); font-size: 0.75rem; }
|
||||||
.workspace-create-field input, .workspace-create-field select { width: 100%; min-height: 2rem; box-sizing: border-box; border: 1px solid var(--vt-color-border-strong); border-radius: var(--vt-radius-sm); background: #0f1424; color: var(--vt-color-text-primary); padding: 0.35rem 0.5rem; font: inherit; font-size: 0.84rem; }
|
.workspace-create-field input, .workspace-create-field select { width: 100%; min-height: 2rem; box-sizing: border-box; border: 1px solid var(--vt-color-border-strong); border-radius: var(--vt-radius-sm); background: #0f1424; color: var(--vt-color-text-primary); padding: 0.35rem 0.5rem; font: inherit; font-size: 0.84rem; }
|
||||||
.workspace-create-field select { appearance: none; background-color: #0f1424; background-image: linear-gradient(45deg, transparent 50%, var(--vt-color-text-muted) 50%), linear-gradient(135deg, var(--vt-color-text-muted) 50%, transparent 50%); background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; padding-right: 1.7rem; }
|
|
||||||
.workspace-create-field select option { background: #0f1424; color: var(--vt-color-text-primary); }
|
|
||||||
.workspace-create-field input:focus, .workspace-create-field select:focus { outline: none; border-color: var(--vt-color-accent); box-shadow: var(--vt-focus-ring); }
|
.workspace-create-field input:focus, .workspace-create-field select:focus { outline: none; border-color: var(--vt-color-accent); box-shadow: var(--vt-focus-ring); }
|
||||||
.workspace-template-summary { display: grid; gap: 0.55rem; padding: 0.75rem; border: 1px solid var(--vt-color-border); border-radius: var(--vt-radius-md); background: var(--vt-color-surface-muted); }
|
.workspace-template-summary { display: grid; gap: 0.55rem; padding: 0.75rem; border: 1px solid var(--vt-color-border); border-radius: var(--vt-radius-md); background: var(--vt-color-surface-muted); }
|
||||||
.workspace-template-summary p { margin: 0; color: var(--vt-color-text-secondary); font-size: 0.8rem; line-height: 1.45; }
|
.workspace-template-summary p { margin: 0; color: var(--vt-color-text-secondary); font-size: 0.8rem; line-height: 1.45; }
|
||||||
.workspace-template-tools { display: grid; gap: 0.4rem; }
|
.workspace-template-tools { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
||||||
.workspace-template-tool { display: grid; grid-template-columns: minmax(7rem, 1fr) minmax(0, 1.4fr) minmax(0, 1.2fr); gap: 0.5rem; align-items: center; padding: 0.45rem 0.55rem; border: 1px solid var(--vt-color-border-strong); border-radius: var(--vt-radius-sm); color: var(--vt-color-text-secondary); font-size: 0.72rem; }
|
.workspace-template-tools span { min-height: 1.35rem; display: inline-flex; align-items: center; padding: 0 0.35rem; border: 1px solid var(--vt-color-border-strong); border-radius: var(--vt-radius-sm); color: var(--vt-color-text-secondary); font-size: 0.72rem; }
|
||||||
.workspace-template-tool-name { color: var(--vt-color-text-primary); font-weight: 600; }
|
|
||||||
.workspace-template-tool-tabs { color: var(--vt-color-text-muted); }
|
|
||||||
.workspace-template-tool-reason { color: var(--vt-color-accent); }
|
|
||||||
.workspace-template-tool.limited { border-color: rgba(220, 164, 57, 0.58); background: rgba(220, 164, 57, 0.08); }
|
|
||||||
.workspace-template-tool.limited .workspace-template-tool-reason { color: #e8bc63; }
|
|
||||||
.workspace-template-tool.unavailable { border-color: rgba(233, 69, 96, 0.58); background: var(--vt-color-danger-muted); }
|
|
||||||
.workspace-template-tool.unavailable .workspace-template-tool-reason { color: var(--vt-color-danger); }
|
|
||||||
.workspace-template-incomplete { color: var(--vt-color-danger) !important; }
|
|
||||||
.workspace-template-warning { display: flex; gap: 0.5rem; align-items: flex-start; justify-content: space-between; margin: 0.25rem 0.6rem 0.5rem; padding: 0.6rem; border: 1px solid rgba(233, 69, 96, 0.58); border-radius: var(--vt-radius-md); background: var(--vt-color-danger-muted); color: var(--vt-color-text-secondary); font-size: 0.73rem; line-height: 1.4; }
|
|
||||||
.workspace-template-warning strong { color: var(--vt-color-danger); }
|
|
||||||
.workspace-template-warning-item { margin-top: 0.2rem; }
|
|
||||||
@media (max-width: 700px) { .workspace-template-tool { grid-template-columns: 1fr; gap: 0.18rem; } }
|
|
||||||
.workspace-create-error { margin: 0; color: var(--vt-color-danger); font-size: 0.78rem; line-height: 1.4; }
|
.workspace-create-error { margin: 0; color: var(--vt-color-danger); font-size: 0.78rem; line-height: 1.4; }
|
||||||
.workspace-create-actions { display: flex; gap: 0.4rem; justify-content: flex-end; }
|
.workspace-create-actions { display: flex; gap: 0.4rem; justify-content: flex-end; }
|
||||||
.wt-btn-primary { background: var(--vt-color-accent); color: #101827; border: none; padding: 0.3rem 0.6rem; border-radius: var(--vt-radius-sm); cursor: pointer; font-size: 0.75rem; font-weight: 600; }
|
.wt-btn-primary { background: var(--vt-color-accent); color: #101827; border: none; padding: 0.3rem 0.6rem; border-radius: var(--vt-radius-sm); cursor: pointer; font-size: 0.75rem; font-weight: 600; }
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,6 @@
|
||||||
* Каждый метод возвращает Promise с данными, совместимыми с Wails-контрактом.
|
* Каждый метод возвращает Promise с данными, совместимыми с Wails-контрактом.
|
||||||
* Состояние мутабельно — тесты могут менять его между сценариями.
|
* Состояние мутабельно — тесты могут менять его между сценариями.
|
||||||
*/
|
*/
|
||||||
import defaultEditorSource from '../../../../../verstak-official-plugins/plugins/default-editor/frontend/src/index.js?raw';
|
|
||||||
import secretsSource from '../../../../../verstak-official-plugins/plugins/secrets/frontend/src/index.js?raw';
|
|
||||||
import activitySource from '../../../../../verstak-official-plugins/plugins/activity/frontend/src/index.js?raw';
|
|
||||||
import todoSource from '../../../../../verstak-official-plugins/plugins/todo/frontend/src/index.js?raw';
|
|
||||||
import journalSource from '../../../../../verstak-official-plugins/plugins/journal/frontend/src/index.js?raw';
|
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
if (window.__wailsMockReady) return;
|
if (window.__wailsMockReady) return;
|
||||||
|
|
||||||
|
|
@ -150,7 +144,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Notes',
|
name: 'Notes',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Deal-scoped notes manager.',
|
description: 'Workspace-scoped notes manager.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'edit',
|
icon: 'edit',
|
||||||
provides: ['verstak/notes/v1'],
|
provides: ['verstak/notes/v1'],
|
||||||
|
|
@ -197,7 +191,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Activity',
|
name: 'Activity',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Deal-scoped activity log for public plugin events.',
|
description: 'Workspace-scoped activity log for public plugin events.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'activity',
|
icon: 'activity',
|
||||||
provides: ['activity.log', 'activity.provider', 'activity.reconstruction'],
|
provides: ['activity.log', 'activity.provider', 'activity.reconstruction'],
|
||||||
|
|
@ -221,11 +215,11 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Journal',
|
name: 'Journal',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Deal-scoped journal with user-authored entries and optional Activity links.',
|
description: 'Workspace-scoped journal with user-authored entries and optional Activity links.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'book-open',
|
icon: 'book-open',
|
||||||
provides: ['worklog', 'journal', 'report.worklog'],
|
provides: ['worklog', 'journal', 'report.worklog'],
|
||||||
permissions: ['events.publish', 'files.read', 'storage.namespace', 'ui.register'],
|
permissions: ['storage.namespace', 'ui.register'],
|
||||||
frontend: { entry: 'frontend/dist/index.js' },
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
contributes: {
|
contributes: {
|
||||||
views: [{ id: 'verstak.journal.view', title: 'Journal', icon: 'book-open', component: 'JournalView' }],
|
views: [{ id: 'verstak.journal.view', title: 'Journal', icon: 'book-open', component: 'JournalView' }],
|
||||||
|
|
@ -242,19 +236,19 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
manifest: {
|
manifest: {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
id: 'verstak.browser-inbox',
|
id: 'verstak.browser-inbox',
|
||||||
name: 'Browser',
|
name: 'Browser Inbox',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Global browser materials with explicit Deal assignment.',
|
description: 'Global browser capture queue with explicit workspace assignment.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'inbox',
|
icon: 'inbox',
|
||||||
provides: ['browser.inbox'],
|
provides: ['browser.inbox'],
|
||||||
permissions: ['events.subscribe', 'files.read', 'storage.namespace', 'ui.register'],
|
permissions: ['events.subscribe', 'files.read', 'storage.namespace', 'ui.register'],
|
||||||
frontend: { entry: 'frontend/dist/index.js' },
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
contributes: {
|
contributes: {
|
||||||
views: [{ id: 'verstak.browser-inbox.view', title: 'Browser', icon: 'inbox', component: 'BrowserInboxView' }],
|
views: [{ id: 'verstak.browser-inbox.view', title: 'Browser Inbox', icon: 'inbox', component: 'BrowserInboxView' }],
|
||||||
sidebarItems: [{ id: 'verstak.browser-inbox.sidebar', title: 'Browser', icon: 'inbox', view: 'verstak.browser-inbox.view', position: 30 }],
|
sidebarItems: [{ id: 'verstak.browser-inbox.sidebar', title: 'Browser Inbox', icon: 'inbox', view: 'verstak.browser-inbox.view', position: 30 }],
|
||||||
workspaceItems: [{ id: 'verstak.browser-inbox.workspace', title: 'Browser', icon: 'inbox', component: 'BrowserInboxView' }]
|
workspaceItems: [{ id: 'verstak.browser-inbox.workspace', title: 'Browser Inbox', icon: 'inbox', component: 'BrowserInboxView' }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rootPath: '/tmp/verstak-test/plugins/browser-inbox',
|
rootPath: '/tmp/verstak-test/plugins/browser-inbox',
|
||||||
|
|
@ -271,7 +265,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Search',
|
name: 'Search',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Deal-scoped vault text search provider.',
|
description: 'Workspace-scoped vault text search provider.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
provides: ['verstak/search/v1', 'search.provider'],
|
provides: ['verstak/search/v1', 'search.provider'],
|
||||||
|
|
@ -297,7 +291,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
'verstak.sync': 'Синхронизация',
|
'verstak.sync': 'Синхронизация',
|
||||||
'verstak.activity': 'Активность',
|
'verstak.activity': 'Активность',
|
||||||
'verstak.journal': 'Журнал',
|
'verstak.journal': 'Журнал',
|
||||||
'verstak.browser-inbox': 'Браузер',
|
'verstak.browser-inbox': 'Входящие из браузера',
|
||||||
'verstak.search': 'Поиск',
|
'verstak.search': 'Поиск',
|
||||||
'verstak.trash': 'Корзина',
|
'verstak.trash': 'Корзина',
|
||||||
'verstak.todo': 'Задачи',
|
'verstak.todo': 'Задачи',
|
||||||
|
|
@ -365,9 +359,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
var pluginSettings = {
|
var pluginSettings = {
|
||||||
'verstak.platform-test': { savedText: 'initial value' }
|
'verstak.platform-test': { savedText: 'initial value' }
|
||||||
};
|
};
|
||||||
var pluginNotifications = {};
|
|
||||||
var pluginData = {};
|
var pluginData = {};
|
||||||
var secretRecords = makeDefaultSecretRecords();
|
|
||||||
var vaultFiles = makeDefaultVaultFiles();
|
var vaultFiles = makeDefaultVaultFiles();
|
||||||
var externalOpens = [];
|
var externalOpens = [];
|
||||||
var trashEntries = [];
|
var trashEntries = [];
|
||||||
|
|
@ -412,33 +404,12 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
return JSON.parse(JSON.stringify(value));
|
return JSON.parse(JSON.stringify(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDefaultSecretRecords() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'first.secret',
|
|
||||||
title: 'First secret',
|
|
||||||
username: 'first-user',
|
|
||||||
value: 'first-value',
|
|
||||||
scope: { kind: 'global' },
|
|
||||||
updatedAt: '2026-07-14T00:00:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'target.secret',
|
|
||||||
title: 'Target secret',
|
|
||||||
username: 'target-user',
|
|
||||||
value: 'target-value',
|
|
||||||
scope: { kind: 'global' },
|
|
||||||
updatedAt: '2026-07-14T00:00:00Z'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function builtInWorkspaceTemplates() {
|
function builtInWorkspaceTemplates() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: 'General',
|
name: 'General',
|
||||||
description: 'Everyday Deal with notes, files, journal, activity, and browser captures.',
|
description: 'Everyday workspace with notes, files, journal, activity, and browser captures.',
|
||||||
version: 2,
|
version: 2,
|
||||||
workspaceTools: ['verstak.notes', 'verstak.files', 'verstak.journal', 'verstak.activity', 'verstak.browser-inbox'],
|
workspaceTools: ['verstak.notes', 'verstak.files', 'verstak.journal', 'verstak.activity', 'verstak.browser-inbox'],
|
||||||
folders: ['Notes', 'Files'],
|
folders: ['Notes', 'Files'],
|
||||||
|
|
@ -456,7 +427,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
{
|
{
|
||||||
id: 'writing',
|
id: 'writing',
|
||||||
name: 'Writing',
|
name: 'Writing',
|
||||||
description: 'Focused notes, files, and journal Deal for documentation and writing.',
|
description: 'Focused notes, files, and journal workspace for documentation and writing.',
|
||||||
version: 1,
|
version: 1,
|
||||||
workspaceTools: ['verstak.notes', 'verstak.files', 'verstak.journal'],
|
workspaceTools: ['verstak.notes', 'verstak.files', 'verstak.journal'],
|
||||||
folders: ['Notes', 'Files'],
|
folders: ['Notes', 'Files'],
|
||||||
|
|
@ -465,7 +436,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
{
|
{
|
||||||
id: 'admin',
|
id: 'admin',
|
||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
description: 'Infrastructure Deal with secrets, todos, and journal.',
|
description: 'Infrastructure workspace with secrets, todos, and journal.',
|
||||||
version: 1,
|
version: 1,
|
||||||
workspaceTools: ['verstak.notes', 'verstak.files', 'verstak.secrets', 'verstak.todo', 'verstak.journal'],
|
workspaceTools: ['verstak.notes', 'verstak.files', 'verstak.secrets', 'verstak.todo', 'verstak.journal'],
|
||||||
folders: ['Notes', 'Files', 'Secrets'],
|
folders: ['Notes', 'Files', 'Secrets'],
|
||||||
|
|
@ -474,7 +445,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
{
|
{
|
||||||
id: 'minimal',
|
id: 'minimal',
|
||||||
name: 'Minimal',
|
name: 'Minimal',
|
||||||
description: 'Only notes and files for a lightweight Deal.',
|
description: 'Only notes and files for a lightweight workspace.',
|
||||||
version: 1,
|
version: 1,
|
||||||
workspaceTools: ['verstak.notes', 'verstak.files'],
|
workspaceTools: ['verstak.notes', 'verstak.files'],
|
||||||
folders: ['Notes', 'Files'],
|
folders: ['Notes', 'Files'],
|
||||||
|
|
@ -570,12 +541,11 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Todos',
|
name: 'Todos',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Global and Deal-scoped todo tracking with due and reminder metadata.',
|
description: 'Global and workspace todo tracking with due and reminder metadata.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'list-todo',
|
icon: 'list-todo',
|
||||||
provides: ['todo.list', 'todo.workspace'],
|
provides: ['todo.list', 'todo.workspace'],
|
||||||
requires: ['verstak/core/notifications/v1'],
|
permissions: ['files.read', 'storage.namespace', 'ui.register'],
|
||||||
permissions: ['files.read', 'storage.namespace', 'ui.register', 'notifications.schedule'],
|
|
||||||
frontend: { entry: 'frontend/dist/index.js' },
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
contributes: {
|
contributes: {
|
||||||
views: [{ id: 'verstak.todo.view', title: 'Todos', icon: 'list-todo', component: 'TodoView' }],
|
views: [{ id: 'verstak.todo.view', title: 'Todos', icon: 'list-todo', component: 'TodoView' }],
|
||||||
|
|
@ -598,22 +568,13 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Secrets',
|
name: 'Secrets',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Encrypted global and Deal-scoped secret manager.',
|
description: 'Encrypted global and workspace-scoped secret manager.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'key-round',
|
icon: 'key-round',
|
||||||
provides: ['secret-store', 'secrets.read-ui', 'secrets.write-ui'],
|
provides: ['secret-store', 'secrets.read-ui', 'secrets.write-ui'],
|
||||||
permissions: ['files.read', 'secrets.read', 'secrets.write', 'ui.register'],
|
permissions: ['secrets.read', 'secrets.write', 'ui.register'],
|
||||||
frontend: { entry: 'frontend/src/index.js' },
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
contributes: {
|
contributes: {
|
||||||
views: [{ id: 'verstak.secrets.view', title: 'Secrets', icon: 'key-round', component: 'SecretsView' }],
|
|
||||||
sidebarItems: [{ id: 'verstak.secrets.sidebar', title: 'Secrets', icon: 'key-round', view: 'verstak.secrets.view', position: 45 }],
|
|
||||||
openProviders: [{
|
|
||||||
id: 'verstak.secrets.secret',
|
|
||||||
title: 'Secrets',
|
|
||||||
priority: 100,
|
|
||||||
component: 'SecretsView',
|
|
||||||
supports: [{ kind: 'secret', modes: ['view'] }]
|
|
||||||
}],
|
|
||||||
workspaceItems: [{ id: 'verstak.secrets.workspace', title: 'Secrets', icon: 'key-round', component: 'SecretsView' }]
|
workspaceItems: [{ id: 'verstak.secrets.workspace', title: 'Secrets', icon: 'key-round', component: 'SecretsView' }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -717,7 +678,6 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
caps.push({ name: 'verstak/core/files/v1', description: 'Files API', pluginId: 'verstak-desktop', status: 'stable' });
|
caps.push({ name: 'verstak/core/files/v1', description: 'Files API', pluginId: 'verstak-desktop', status: 'stable' });
|
||||||
caps.push({ name: 'verstak/core/workbench/v1', description: 'Workbench routing', pluginId: 'verstak-desktop', status: 'stable' });
|
caps.push({ name: 'verstak/core/workbench/v1', description: 'Workbench routing', pluginId: 'verstak-desktop', status: 'stable' });
|
||||||
caps.push({ name: 'verstak/core/sync/v1', description: 'Sync API', pluginId: 'verstak-desktop', status: 'stable' });
|
caps.push({ name: 'verstak/core/sync/v1', description: 'Sync API', pluginId: 'verstak-desktop', status: 'stable' });
|
||||||
caps.push({ name: 'verstak/core/notifications/v1', description: 'Native notifications', pluginId: 'verstak-desktop', status: 'stable' });
|
|
||||||
for (var id in pluginStates) {
|
for (var id in pluginStates) {
|
||||||
var s = pluginStates[id];
|
var s = pluginStates[id];
|
||||||
if (s.status === 'loaded' && s.enabled && s.manifest && s.manifest.provides) {
|
if (s.status === 'loaded' && s.enabled && s.manifest && s.manifest.provides) {
|
||||||
|
|
@ -2456,7 +2416,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
}
|
}
|
||||||
|
|
||||||
function title(capture) {
|
function title(capture) {
|
||||||
return capture.title || capture.fileName || capture.url || capture.captureId || 'Untitled material';
|
return capture.title || capture.fileName || capture.url || capture.captureId || 'Untitled capture';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBrowserInbox(containerEl, props, api) {
|
function renderBrowserInbox(containerEl, props, api) {
|
||||||
|
|
@ -2473,7 +2433,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
containerEl.innerHTML = '';
|
containerEl.innerHTML = '';
|
||||||
var root = el('div', { className: 'browser-inbox-root', 'data-plugin-id': PLUGIN_ID });
|
var root = el('div', { className: 'browser-inbox-root', 'data-plugin-id': PLUGIN_ID });
|
||||||
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
|
var toolbar = el('div', { className: 'browser-inbox-toolbar' });
|
||||||
var titleEl = el('span', { className: 'browser-inbox-title', textContent: rootPath ? 'Browser · ' + rootPath : 'Browser' });
|
var titleEl = el('span', { className: 'browser-inbox-title', textContent: rootPath ? 'Browser Inbox · ' + rootPath : 'Browser Inbox' });
|
||||||
var countEl = el('span', { className: 'browser-inbox-count' });
|
var countEl = el('span', { className: 'browser-inbox-count' });
|
||||||
var statusEl = el('span', { className: 'browser-inbox-status' });
|
var statusEl = el('span', { className: 'browser-inbox-status' });
|
||||||
var filtersEl = el('div', { className: 'browser-inbox-filters' });
|
var filtersEl = el('div', { className: 'browser-inbox-filters' });
|
||||||
|
|
@ -2557,7 +2517,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
function renderWorkspaceFilterOptions() {
|
function renderWorkspaceFilterOptions() {
|
||||||
if (rootPath) return;
|
if (rootPath) return;
|
||||||
workspaceFilterEl.innerHTML = '';
|
workspaceFilterEl.innerHTML = '';
|
||||||
workspaceFilterEl.appendChild(option('', 'All Deals'));
|
workspaceFilterEl.appendChild(option('', 'All workspaces'));
|
||||||
workspaceRoots().forEach(function (workspace) {
|
workspaceRoots().forEach(function (workspace) {
|
||||||
workspaceFilterEl.appendChild(option(workspace, workspace));
|
workspaceFilterEl.appendChild(option(workspace, workspace));
|
||||||
});
|
});
|
||||||
|
|
@ -2630,7 +2590,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
}) : captures).map(function (capture) { return capture.captureId; });
|
}) : captures).map(function (capture) { return capture.captureId; });
|
||||||
captures = captures.filter(function (capture) { return ids.indexOf(capture.captureId) === -1; });
|
captures = captures.filter(function (capture) { return ids.indexOf(capture.captureId) === -1; });
|
||||||
selectedId = '';
|
selectedId = '';
|
||||||
statusText = rootPath ? 'Deal materials cleared' : 'Inbox cleared';
|
statusText = rootPath ? 'Workspace captures cleared' : 'Inbox cleared';
|
||||||
return persist();
|
return persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2672,7 +2632,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
listEl.appendChild(el('div', {
|
listEl.appendChild(el('div', {
|
||||||
className: 'browser-inbox-empty',
|
className: 'browser-inbox-empty',
|
||||||
textContent: captures.length === 0
|
textContent: captures.length === 0
|
||||||
? 'No browser materials yet. Send a page, selection, or link from the extension.'
|
? 'No browser captures yet. Keep this view open, then send a page, selection, link, or file from the browser extension.'
|
||||||
: 'No captures match the current filters.'
|
: 'No captures match the current filters.'
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
|
|
@ -2717,7 +2677,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.domain || '-' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.domain || '-' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Browser' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: '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: 'Deal' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: 'Workspace' }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || 'Unassigned' }),
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || 'Unassigned' }),
|
||||||
el('div', { className: 'browser-inbox-meta-label', textContent: 'Status' }),
|
el('div', { className: 'browser-inbox-meta-label', textContent: 'Status' }),
|
||||||
el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed ? 'Processed' : 'Unprocessed' })
|
el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed ? 'Processed' : 'Unprocessed' })
|
||||||
|
|
@ -3368,43 +3328,6 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
GetPluginLocalization: function (pluginId, locale) {
|
GetPluginLocalization: function (pluginId, locale) {
|
||||||
return Promise.resolve([mockPluginCatalog(pluginId, locale), '']);
|
return Promise.resolve([mockPluginCatalog(pluginId, locale), '']);
|
||||||
},
|
},
|
||||||
PluginSecretsStatus: function () {
|
|
||||||
return Promise.resolve([{ initialized: true, unlocked: true }, '']);
|
|
||||||
},
|
|
||||||
PluginSecretsUnlock: function () {
|
|
||||||
return Promise.resolve('');
|
|
||||||
},
|
|
||||||
PluginSecretsList: function () {
|
|
||||||
return Promise.resolve([secretRecords.map(function (record) {
|
|
||||||
var listed = cloneJson(record);
|
|
||||||
delete listed.value;
|
|
||||||
return listed;
|
|
||||||
}), '']);
|
|
||||||
},
|
|
||||||
PluginSecretsRead: function (_pluginId, secretID) {
|
|
||||||
var record = secretRecords.find(function (item) { return item.id === secretID; });
|
|
||||||
if (!record) return Promise.resolve([{}, 'not-found: secret ' + secretID]);
|
|
||||||
return Promise.resolve([cloneJson(record), '']);
|
|
||||||
},
|
|
||||||
PluginSecretsWrite: function (_pluginId, nextRecord) {
|
|
||||||
var record = Object.assign({}, nextRecord || {});
|
|
||||||
if (!record.id) return Promise.resolve([{}, 'secret id is required']);
|
|
||||||
record.scope = record.scope || { kind: 'global' };
|
|
||||||
record.updatedAt = new Date().toISOString();
|
|
||||||
var index = secretRecords.findIndex(function (item) { return item.id === record.id; });
|
|
||||||
if (index === -1) secretRecords.push(record);
|
|
||||||
else secretRecords[index] = record;
|
|
||||||
return Promise.resolve([cloneJson(record), '']);
|
|
||||||
},
|
|
||||||
PluginSecretsDelete: function (_pluginId, secretID) {
|
|
||||||
secretRecords = secretRecords.filter(function (record) { return record.id !== secretID; });
|
|
||||||
return Promise.resolve('');
|
|
||||||
},
|
|
||||||
PluginSecretsCopyLink: function (_pluginId, secretID) {
|
|
||||||
var record = secretRecords.find(function (item) { return item.id === secretID; });
|
|
||||||
if (!record) return Promise.resolve(['', 'not-found: secret ' + secretID]);
|
|
||||||
return Promise.resolve(['[' + (record.title || record.id) + '](verstak-secret://' + encodeURIComponent(record.id) + ')', '']);
|
|
||||||
},
|
|
||||||
ReadPluginSettings: function (pluginId) {
|
ReadPluginSettings: function (pluginId) {
|
||||||
return Promise.resolve([Object.assign({}, pluginSettings[pluginId] || {}), '']);
|
return Promise.resolve([Object.assign({}, pluginSettings[pluginId] || {}), '']);
|
||||||
},
|
},
|
||||||
|
|
@ -3412,22 +3335,8 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
pluginSettings[pluginId] = Object.assign({}, settings || {});
|
pluginSettings[pluginId] = Object.assign({}, settings || {});
|
||||||
return Promise.resolve('');
|
return Promise.resolve('');
|
||||||
},
|
},
|
||||||
ReadPluginSetting: function (pluginId, key) {
|
ReadPluginSetting: function () { return Promise.resolve(null); },
|
||||||
return Promise.resolve([pluginSettings[pluginId] && pluginSettings[pluginId][key], '']);
|
WritePluginSetting: function () { return Promise.resolve(null); },
|
||||||
},
|
|
||||||
WritePluginSetting: function (pluginId, key, value) {
|
|
||||||
pluginSettings[pluginId] = pluginSettings[pluginId] || {};
|
|
||||||
pluginSettings[pluginId][key] = value;
|
|
||||||
return Promise.resolve('');
|
|
||||||
},
|
|
||||||
ReplacePluginNotifications: function (pluginId, items) {
|
|
||||||
pluginNotifications[pluginId] = Array.isArray(items) ? items.slice() : [];
|
|
||||||
return Promise.resolve('');
|
|
||||||
},
|
|
||||||
ClearPluginNotifications: function (pluginId) {
|
|
||||||
delete pluginNotifications[pluginId];
|
|
||||||
return Promise.resolve('');
|
|
||||||
},
|
|
||||||
ReadPluginDataJSON: function (pluginId, name) {
|
ReadPluginDataJSON: function (pluginId, name) {
|
||||||
var data = (pluginData[pluginId] && pluginData[pluginId][name]) || {};
|
var data = (pluginData[pluginId] && pluginData[pluginId][name]) || {};
|
||||||
return Promise.resolve([Object.assign({}, data), '']);
|
return Promise.resolve([Object.assign({}, data), '']);
|
||||||
|
|
@ -3546,8 +3455,8 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
if (pluginId === 'verstak.platform-test' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.platform-test' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(platformTestBundle());
|
return Promise.resolve(platformTestBundle());
|
||||||
}
|
}
|
||||||
if (pluginId === 'verstak.default-editor') {
|
if (pluginId === 'verstak.default-editor' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(defaultEditorSource);
|
return Promise.resolve(defaultEditorBundle());
|
||||||
}
|
}
|
||||||
if (pluginId === 'verstak.files' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.files' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(filesPluginBundle());
|
return Promise.resolve(filesPluginBundle());
|
||||||
|
|
@ -3558,20 +3467,20 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
if (pluginId === 'verstak.notes' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.notes' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(simplePluginBundle('verstak.notes', 'NotesView', 'notes-root', 'Notes'));
|
return Promise.resolve(simplePluginBundle('verstak.notes', 'NotesView', 'notes-root', 'Notes'));
|
||||||
}
|
}
|
||||||
if (pluginId === 'verstak.activity') {
|
if (pluginId === 'verstak.activity' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(activitySource);
|
return Promise.resolve(activityBundle());
|
||||||
}
|
}
|
||||||
if (pluginId === 'verstak.journal' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.journal' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(journalSource);
|
return Promise.resolve(journalBundle());
|
||||||
}
|
}
|
||||||
if (pluginId === 'verstak.browser-inbox' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.browser-inbox' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(browserInboxBundle());
|
return Promise.resolve(browserInboxBundle());
|
||||||
}
|
}
|
||||||
if (pluginId === 'verstak.todo' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.todo' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(todoSource);
|
return Promise.resolve(todoBundle());
|
||||||
}
|
}
|
||||||
if (pluginId === 'verstak.secrets') {
|
if (pluginId === 'verstak.secrets' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(secretsSource);
|
return Promise.resolve(simplePluginBundle('verstak.secrets', 'SecretsView', 'secrets-root', 'Secrets'));
|
||||||
}
|
}
|
||||||
if (pluginId === 'verstak.search' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.search' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(searchPluginBundle());
|
return Promise.resolve(searchPluginBundle());
|
||||||
|
|
@ -4104,7 +4013,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Notes',
|
name: 'Notes',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Deal-scoped notes manager.',
|
description: 'Workspace-scoped notes manager.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'edit',
|
icon: 'edit',
|
||||||
provides: ['verstak/notes/v1'],
|
provides: ['verstak/notes/v1'],
|
||||||
|
|
@ -4151,7 +4060,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Activity',
|
name: 'Activity',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Deal-scoped activity log for public plugin events.',
|
description: 'Workspace-scoped activity log for public plugin events.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'activity',
|
icon: 'activity',
|
||||||
provides: ['activity.log', 'activity.provider', 'activity.reconstruction'],
|
provides: ['activity.log', 'activity.provider', 'activity.reconstruction'],
|
||||||
|
|
@ -4175,11 +4084,11 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Journal',
|
name: 'Journal',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Deal-scoped journal with user-authored entries and optional Activity links.',
|
description: 'Workspace-scoped journal with user-authored entries and optional Activity links.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'book-open',
|
icon: 'book-open',
|
||||||
provides: ['worklog', 'journal', 'report.worklog'],
|
provides: ['worklog', 'journal', 'report.worklog'],
|
||||||
permissions: ['events.publish', 'files.read', 'storage.namespace', 'ui.register'],
|
permissions: ['storage.namespace', 'ui.register'],
|
||||||
frontend: { entry: 'frontend/dist/index.js' },
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
contributes: {
|
contributes: {
|
||||||
views: [{ id: 'verstak.journal.view', title: 'Journal', icon: 'book-open', component: 'JournalView' }],
|
views: [{ id: 'verstak.journal.view', title: 'Journal', icon: 'book-open', component: 'JournalView' }],
|
||||||
|
|
@ -4196,19 +4105,19 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
manifest: {
|
manifest: {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
id: 'verstak.browser-inbox',
|
id: 'verstak.browser-inbox',
|
||||||
name: 'Browser',
|
name: 'Browser Inbox',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Global browser materials with explicit Deal assignment.',
|
description: 'Global browser capture queue with explicit workspace assignment.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'inbox',
|
icon: 'inbox',
|
||||||
provides: ['browser.inbox'],
|
provides: ['browser.inbox'],
|
||||||
permissions: ['events.subscribe', 'files.read', 'storage.namespace', 'ui.register'],
|
permissions: ['events.subscribe', 'files.read', 'storage.namespace', 'ui.register'],
|
||||||
frontend: { entry: 'frontend/dist/index.js' },
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
contributes: {
|
contributes: {
|
||||||
views: [{ id: 'verstak.browser-inbox.view', title: 'Browser', icon: 'inbox', component: 'BrowserInboxView' }],
|
views: [{ id: 'verstak.browser-inbox.view', title: 'Browser Inbox', icon: 'inbox', component: 'BrowserInboxView' }],
|
||||||
sidebarItems: [{ id: 'verstak.browser-inbox.sidebar', title: 'Browser', icon: 'inbox', view: 'verstak.browser-inbox.view', position: 30 }],
|
sidebarItems: [{ id: 'verstak.browser-inbox.sidebar', title: 'Browser Inbox', icon: 'inbox', view: 'verstak.browser-inbox.view', position: 30 }],
|
||||||
workspaceItems: [{ id: 'verstak.browser-inbox.workspace', title: 'Browser', icon: 'inbox', component: 'BrowserInboxView' }]
|
workspaceItems: [{ id: 'verstak.browser-inbox.workspace', title: 'Browser Inbox', icon: 'inbox', component: 'BrowserInboxView' }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rootPath: '/tmp/verstak-test/plugins/browser-inbox',
|
rootPath: '/tmp/verstak-test/plugins/browser-inbox',
|
||||||
|
|
@ -4225,7 +4134,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
name: 'Search',
|
name: 'Search',
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Deal-scoped vault text search provider.',
|
description: 'Workspace-scoped vault text search provider.',
|
||||||
source: 'official',
|
source: 'official',
|
||||||
icon: 'search',
|
icon: 'search',
|
||||||
provides: ['verstak/search/v1', 'search.provider'],
|
provides: ['verstak/search/v1', 'search.provider'],
|
||||||
|
|
@ -4255,8 +4164,6 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
workbenchPreferences = {};
|
workbenchPreferences = {};
|
||||||
openedResources = [];
|
openedResources = [];
|
||||||
pluginSettings = { 'verstak.platform-test': { savedText: 'initial value' } };
|
pluginSettings = { 'verstak.platform-test': { savedText: 'initial value' } };
|
||||||
pluginNotifications = {};
|
|
||||||
secretRecords = makeDefaultSecretRecords();
|
|
||||||
vaultFiles = makeDefaultVaultFiles();
|
vaultFiles = makeDefaultVaultFiles();
|
||||||
externalOpens = [];
|
externalOpens = [];
|
||||||
trashEntries = [];
|
trashEntries = [];
|
||||||
|
|
@ -4276,9 +4183,8 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
getPluginState: function (pluginId) {
|
getPluginState: function (pluginId) {
|
||||||
return pluginStates[pluginId] ? Object.assign({}, pluginStates[pluginId]) : null;
|
return pluginStates[pluginId] ? Object.assign({}, pluginStates[pluginId]) : null;
|
||||||
},
|
},
|
||||||
addSyntheticPlugins: function (count, source) {
|
addSyntheticPlugins: function (count) {
|
||||||
var total = Number(count || 0);
|
var total = Number(count || 0);
|
||||||
var pluginSource = source === 'official' || source === 'local' || source === 'third-party' ? source : 'third-party';
|
|
||||||
for (var i = 1; i <= total; i++) {
|
for (var i = 1; i <= total; i++) {
|
||||||
var id = 'verstak.synthetic-layout-' + String(i).padStart(2, '0');
|
var id = 'verstak.synthetic-layout-' + String(i).padStart(2, '0');
|
||||||
pluginStates[id] = {
|
pluginStates[id] = {
|
||||||
|
|
@ -4291,7 +4197,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
version: '0.0.' + i,
|
version: '0.0.' + i,
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Synthetic plugin used by frontend layout tests.',
|
description: 'Synthetic plugin used by frontend layout tests.',
|
||||||
source: pluginSource,
|
source: 'test',
|
||||||
provides: ['verstak/synthetic-layout-' + i + '/v1'],
|
provides: ['verstak/synthetic-layout-' + i + '/v1'],
|
||||||
requires: [],
|
requires: [],
|
||||||
optionalRequires: [],
|
optionalRequires: [],
|
||||||
|
|
@ -4311,7 +4217,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
vaultPluginState.enabledPlugins.push(id);
|
vaultPluginState.enabledPlugins.push(id);
|
||||||
}
|
}
|
||||||
if (!vaultPluginState.desiredPlugins.some(function (p) { return p.id === id; })) {
|
if (!vaultPluginState.desiredPlugins.some(function (p) { return p.id === id; })) {
|
||||||
vaultPluginState.desiredPlugins.push({ id: id, version: '0.0.' + i, source: pluginSource });
|
vaultPluginState.desiredPlugins.push({ id: id, version: '0.0.' + i, source: 'test' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
||||||
const files = [
|
|
||||||
'src/lib/shell/CommandPalette.svelte',
|
|
||||||
'src/lib/plugin-manager/PluginManager.svelte',
|
|
||||||
'src/lib/shell/VaultSelection.svelte',
|
|
||||||
'src/lib/shell/WorkspaceTree.svelte',
|
|
||||||
'src/lib/plugin-host/PluginBundleHost.svelte',
|
|
||||||
];
|
|
||||||
const patterns = [
|
|
||||||
/(?:^|\n)\s*(?:error|localError|createError)\s*=\s*(?:err|String\()/,
|
|
||||||
/(?:^|\n)\s*error\s*=\s*['"].*['"]\s*\+\s*(?:err|String\()/,
|
|
||||||
/tr\([^\n]*\{\s*error\s*:/,
|
|
||||||
];
|
|
||||||
const violations = [];
|
|
||||||
|
|
||||||
for (const relativePath of files) {
|
|
||||||
const source = fs.readFileSync(path.join(root, relativePath), 'utf8');
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
if (pattern.test(source)) violations.push(relativePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (violations.length) {
|
|
||||||
throw new Error(`Technical backend errors reach desktop UI in: ${[...new Set(violations)].join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bundleHost = fs.readFileSync(path.join(root, 'src/lib/plugin-host/PluginBundleHost.svelte'), 'utf8');
|
|
||||||
if (!/<p class="error-message">\{errorText \|\| tr\('bundle\.unknownError'\)\}<\/p>\s*<details class="error-details">/.test(bundleHost)) {
|
|
||||||
throw new Error('Plugin bundle errors must keep the user-facing message outside expandable technical details');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('desktop UI does not interpolate raw backend errors');
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { readFile } from 'node:fs/promises';
|
|
||||||
|
|
||||||
const source = await readFile(new URL('../src/lib/shell/WorkspaceTree.svelte', import.meta.url), 'utf8');
|
|
||||||
|
|
||||||
assert.match(source, /\.workspace-create-field select\s*\{[^}]*appearance:\s*none/, 'workspace template select must hide the native arrow');
|
|
||||||
assert.match(source, /\.workspace-create-field select option\s*\{[^}]*background/, 'workspace template options must use the application surface');
|
|
||||||
|
|
||||||
console.log('workspace select style contract passed');
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import en from '../src/lib/i18n/catalogs/en.js';
|
|
||||||
import ru from '../src/lib/i18n/catalogs/ru.js';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
const forbidden = [
|
|
||||||
['English catalog', /\bworkspace(?:s)?\b/i],
|
|
||||||
['Russian catalog', /рабоч(?:ее|ие|ем|его|их)\s+пространств(?:о|а|е)?/i],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [name, pattern] of forbidden) {
|
|
||||||
const violations = Object.entries(name === 'English catalog' ? en : ru)
|
|
||||||
.filter(([, value]) => pattern.test(value));
|
|
||||||
if (violations.length) {
|
|
||||||
throw new Error(`${name} still exposes workspace terminology: ${violations.map(([key]) => key).join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
||||||
const overview = fs.readFileSync(path.join(root, 'src/lib/shell/TodaySurface.svelte'), 'utf8');
|
|
||||||
for (const phrase of ['Workspace opened', 'Workspace activity', 'Workspace overview note']) {
|
|
||||||
if (overview.includes(phrase)) {
|
|
||||||
throw new Error(`Overview exposes workspace terminology: ${phrase}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('desktop user terminology uses Deal/Дело');
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { resolve } from 'node:path';
|
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
|
@ -14,9 +13,6 @@ export default defineConfig(({ mode }) => {
|
||||||
server: {
|
server: {
|
||||||
port: isTest ? 5174 : 5173,
|
port: isTest ? 5174 : 5173,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
fs: {
|
|
||||||
allow: [resolve(__dirname, '..', '..')],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
include: [
|
||||||
|
|
|
||||||
|
|
@ -1064,23 +1064,30 @@ func (a *App) ReloadPlugins() (int, string) {
|
||||||
// Unregister all non-core capabilities
|
// Unregister all non-core capabilities
|
||||||
a.capRegistry.UnregisterAll()
|
a.capRegistry.UnregisterAll()
|
||||||
|
|
||||||
// Re-register the same core capabilities as initial startup. Keeping this
|
// Re-register core capabilities
|
||||||
// list in the capability package prevents a plugin reload from dropping a
|
coreCaps := []string{
|
||||||
// required capability such as native notifications.
|
"verstak/core/plugin-manager/v1",
|
||||||
if err := a.capRegistry.Register(capability.CorePluginID, capability.CorePlatformCapabilities()); err != nil {
|
"verstak/core/capability-registry/v1",
|
||||||
|
"verstak/core/contribution-registry/v1",
|
||||||
|
"verstak/core/permissions/v1",
|
||||||
|
"verstak/core/events/v1",
|
||||||
|
"verstak/core/files/v1",
|
||||||
|
"verstak/core/workbench/v1",
|
||||||
|
}
|
||||||
|
if err := a.capRegistry.Register("verstak-desktop", coreCaps); err != nil {
|
||||||
log.Printf("[api] ReloadPlugins: failed to re-register core capabilities: %v", err)
|
log.Printf("[api] ReloadPlugins: failed to re-register core capabilities: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-register vault capability if vault is open
|
// Re-register vault capability if vault is open
|
||||||
if a.vault != nil && a.vault.GetVaultStatus() == vault.StatusOpen {
|
if a.vault != nil && a.vault.GetVaultStatus() == vault.StatusOpen {
|
||||||
if err := a.capRegistry.Register(capability.CorePluginID, []string{"verstak/core/vault/v1"}); err != nil {
|
if err := a.capRegistry.Register("verstak-desktop", []string{"verstak/core/vault/v1"}); err != nil {
|
||||||
log.Printf("[api] ReloadPlugins: failed to re-register vault capability: %v", err)
|
log.Printf("[api] ReloadPlugins: failed to re-register vault capability: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-register workspace capability if workspace is initialized
|
// Re-register workspace capability if workspace is initialized
|
||||||
if a.workspace != nil && a.workspace.IsInitialized() {
|
if a.workspace != nil && a.workspace.IsInitialized() {
|
||||||
if err := a.capRegistry.Register(capability.CorePluginID, []string{"verstak/core/workspace/v1"}); err != nil {
|
if err := a.capRegistry.Register("verstak-desktop", []string{"verstak/core/workspace/v1"}); err != nil {
|
||||||
log.Printf("[api] ReloadPlugins: failed to re-register workspace capability: %v", err)
|
log.Printf("[api] ReloadPlugins: failed to re-register workspace capability: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,19 +225,6 @@ func TestReplaceAndClearPluginNotificationsStayWithinPluginNamespace(t *testing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReloadPluginsRestoresNotificationCoreCapability(t *testing.T) {
|
|
||||||
app := &App{
|
|
||||||
capRegistry: capability.NewRegistry(),
|
|
||||||
contribRegistry: contribution.NewRegistry(),
|
|
||||||
}
|
|
||||||
|
|
||||||
app.ReloadPlugins()
|
|
||||||
|
|
||||||
if !app.capRegistry.Has("verstak/core/notifications/v1") {
|
|
||||||
t.Fatal("ReloadPlugins did not restore verstak/core/notifications/v1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDomReadyInitializesNotificationsBeforeScheduler(t *testing.T) {
|
func TestDomReadyInitializesNotificationsBeforeScheduler(t *testing.T) {
|
||||||
oldInitialize := initializeNativeNotifications
|
oldInitialize := initializeNativeNotifications
|
||||||
defer func() { initializeNativeNotifications = oldInitialize }()
|
defer func() { initializeNativeNotifications = oldInitialize }()
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,9 @@ type WindowState struct {
|
||||||
|
|
||||||
// Manager provides thread-safe access to app settings.
|
// Manager provides thread-safe access to app settings.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
config *Config
|
config *Config
|
||||||
configPath string
|
configPath string
|
||||||
onLanguageChanged func(string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfigPath returns the default path for app settings.
|
// DefaultConfigPath returns the default path for app settings.
|
||||||
|
|
@ -231,32 +230,13 @@ func (m *Manager) UpdateLanguage(language string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
if m.config == nil {
|
if m.config == nil {
|
||||||
m.config = defaultConfig()
|
m.config = defaultConfig()
|
||||||
}
|
}
|
||||||
m.config.Language = language
|
m.config.Language = language
|
||||||
m.config.LastOpenedAt = time.Now().UTC().Format(time.RFC3339)
|
m.config.LastOpenedAt = time.Now().UTC().Format(time.RFC3339)
|
||||||
err := m.saveLocked()
|
return m.saveLocked()
|
||||||
handler := m.onLanguageChanged
|
|
||||||
m.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if handler != nil {
|
|
||||||
handler(language)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLanguageChangedHandler registers the shell callback invoked after a
|
|
||||||
// language preference has been durably updated.
|
|
||||||
func (m *Manager) SetLanguageChangedHandler(handler func(string)) {
|
|
||||||
if m == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
m.mu.Lock()
|
|
||||||
m.onLanguageChanged = handler
|
|
||||||
m.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSync replaces sync settings without changing unrelated app settings.
|
// UpdateSync replaces sync settings without changing unrelated app settings.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package appsettings
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -75,34 +74,6 @@ func TestUpdateLanguagePersistsAndPreservesUnrelatedSettings(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateLanguageNotifiesTheDesktopShellAfterPersisting(t *testing.T) {
|
|
||||||
path := filepath.Join(t.TempDir(), "config.json")
|
|
||||||
manager := NewManager(path)
|
|
||||||
if err := manager.Load(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var received []string
|
|
||||||
manager.SetLanguageChangedHandler(func(language string) {
|
|
||||||
received = append(received, language)
|
|
||||||
})
|
|
||||||
if err := manager.UpdateLanguage(LanguageRussian); err != nil {
|
|
||||||
t.Fatalf("UpdateLanguage: %v", err)
|
|
||||||
}
|
|
||||||
if got := manager.Get().Language; got != LanguageRussian {
|
|
||||||
t.Fatalf("persisted language = %q, want %q", got, LanguageRussian)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(received, []string{LanguageRussian}) {
|
|
||||||
t.Fatalf("language notifications = %#v, want [%q]", received, LanguageRussian)
|
|
||||||
}
|
|
||||||
if err := manager.UpdateLanguage("de"); err == nil {
|
|
||||||
t.Fatal("UpdateLanguage(de) succeeded, want validation failure")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(received, []string{LanguageRussian}) {
|
|
||||||
t.Fatalf("language notification fired after rejected update: %#v", received)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLoad_CorruptConfig(t *testing.T) {
|
func TestLoad_CorruptConfig(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
path := filepath.Join(dir, "config.json")
|
path := filepath.Join(dir, "config.json")
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package capability
|
|
||||||
|
|
||||||
const CorePluginID = "verstak-desktop"
|
|
||||||
|
|
||||||
var platformCapabilities = []string{
|
|
||||||
"verstak/core/plugin-manager/v1",
|
|
||||||
"verstak/core/capability-registry/v1",
|
|
||||||
"verstak/core/contribution-registry/v1",
|
|
||||||
"verstak/core/permissions/v1",
|
|
||||||
"verstak/core/events/v1",
|
|
||||||
"verstak/core/files/v1",
|
|
||||||
"verstak/core/workbench/v1",
|
|
||||||
"verstak/core/notifications/v1",
|
|
||||||
}
|
|
||||||
|
|
||||||
// CorePlatformCapabilities returns a copy of the capabilities registered by
|
|
||||||
// the desktop before dynamic plugins are resolved.
|
|
||||||
func CorePlatformCapabilities() []string {
|
|
||||||
return append([]string(nil), platformCapabilities...)
|
|
||||||
}
|
|
||||||
|
|
@ -3,16 +3,25 @@ package plugin
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/verstak/verstak-desktop/internal/core/capability"
|
"github.com/verstak/verstak-desktop/internal/core/capability"
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerCoreCapabilities registers the desktop's core capabilities on a registry.
|
// coreCapabilities lists the 5 core capabilities that the platform registers
|
||||||
|
// before any plugins are loaded.
|
||||||
|
var coreCapabilities = []string{
|
||||||
|
"verstak/core/plugin-manager/v1",
|
||||||
|
"verstak/core/capability-registry/v1",
|
||||||
|
"verstak/core/contribution-registry/v1",
|
||||||
|
"verstak/core/permissions/v1",
|
||||||
|
"verstak/core/events/v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerCoreCapabilities registers the 5 core capabilities on a registry.
|
||||||
func registerCoreCapabilities(t *testing.T, reg *capability.Registry) {
|
func registerCoreCapabilities(t *testing.T, reg *capability.Registry) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if err := reg.Register(capability.CorePluginID, capability.CorePlatformCapabilities()); err != nil {
|
if err := reg.Register("verstak-core", coreCapabilities); err != nil {
|
||||||
t.Fatalf("failed to register core capabilities: %v", err)
|
t.Fatalf("failed to register core capabilities: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,43 +76,6 @@ func TestLifecycle_CoreCapabilitiesRegisteredBeforePlugins(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundledOfficialPluginRequirementsResolve(t *testing.T) {
|
|
||||||
plugins, errs := DiscoverPlugins([]string{bundledPluginDir(t)})
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("discover bundled plugins: %v", errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
reg := capability.NewRegistry()
|
|
||||||
registerCoreCapabilities(t, reg)
|
|
||||||
ResolveLifecycle(plugins, reg, nil)
|
|
||||||
|
|
||||||
foundTodo := false
|
|
||||||
for _, loaded := range plugins {
|
|
||||||
if loaded.Manifest.ID == "verstak.todo" {
|
|
||||||
foundTodo = true
|
|
||||||
}
|
|
||||||
if loaded.Status == StatusMissingRequiredCapability {
|
|
||||||
t.Fatalf("bundled plugin %q is missing required capabilities: %s", loaded.Manifest.ID, loaded.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !foundTodo {
|
|
||||||
t.Fatal("bundled Todo manifest was not discovered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func bundledPluginDir(t *testing.T) string {
|
|
||||||
t.Helper()
|
|
||||||
if configured := os.Getenv("VERSTAK_RELEASE_PLUGIN_DIR"); configured != "" {
|
|
||||||
return configured
|
|
||||||
}
|
|
||||||
_, sourceFile, _, ok := runtime.Caller(0)
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("could not resolve lifecycle test path")
|
|
||||||
}
|
|
||||||
return filepath.Join(filepath.Dir(sourceFile), "../../../plugins")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLifecycle_MissingRequiredCapability verifies that when the required
|
// TestLifecycle_MissingRequiredCapability verifies that when the required
|
||||||
// capability is NOT registered, CheckRequired reports it as missing and the
|
// capability is NOT registered, CheckRequired reports it as missing and the
|
||||||
// plugin status should be set to StatusMissingRequiredCapability.
|
// plugin status should be set to StatusMissingRequiredCapability.
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
// Package tray owns the native tray menu wiring for the desktop shell.
|
// Package tray owns the native tray menu wiring for the desktop shell.
|
||||||
package tray
|
package tray
|
||||||
|
|
||||||
import (
|
import "sync"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MenuItem exposes click events from a native tray menu item.
|
// MenuItem exposes click events from a native tray menu item.
|
||||||
type MenuItem interface {
|
type MenuItem interface {
|
||||||
Clicked() <-chan struct{}
|
Clicked() <-chan struct{}
|
||||||
SetTitle(title string)
|
|
||||||
SetTooltip(tooltip string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend is the platform tray implementation.
|
// Backend is the platform tray implementation.
|
||||||
|
|
@ -28,63 +23,11 @@ type Actions struct {
|
||||||
Quit func()
|
Quit func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels contains the user-visible text for one tray menu locale.
|
|
||||||
type Labels struct {
|
|
||||||
ShowTitle string
|
|
||||||
ShowTooltip string
|
|
||||||
QuitTitle string
|
|
||||||
QuitTooltip string
|
|
||||||
}
|
|
||||||
|
|
||||||
func englishLabels() Labels {
|
|
||||||
return Labels{
|
|
||||||
ShowTitle: "Show Verstak",
|
|
||||||
ShowTooltip: "Show the Verstak window",
|
|
||||||
QuitTitle: "Quit",
|
|
||||||
QuitTooltip: "Quit Verstak",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func russianLabels() Labels {
|
|
||||||
return Labels{
|
|
||||||
ShowTitle: "Показать Верстак",
|
|
||||||
ShowTooltip: "Показать окно Верстака",
|
|
||||||
QuitTitle: "Выйти",
|
|
||||||
QuitTooltip: "Завершить Верстак",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LabelsForPreference resolves the saved language preference for native tray UI.
|
|
||||||
// System locale values are intentionally passed in by the shell so this package
|
|
||||||
// stays independent of process environment and is deterministic in tests.
|
|
||||||
func LabelsForPreference(preference string, systemLocales ...string) Labels {
|
|
||||||
if strings.EqualFold(strings.TrimSpace(preference), "ru") {
|
|
||||||
return russianLabels()
|
|
||||||
}
|
|
||||||
if strings.EqualFold(strings.TrimSpace(preference), "system") {
|
|
||||||
for _, locale := range systemLocales {
|
|
||||||
locale = strings.ToLower(strings.TrimSpace(locale))
|
|
||||||
if locale == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(locale, "ru") {
|
|
||||||
return russianLabels()
|
|
||||||
}
|
|
||||||
return englishLabels()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return englishLabels()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controller initializes one native tray and routes its menu actions.
|
// Controller initializes one native tray and routes its menu actions.
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
backend Backend
|
backend Backend
|
||||||
icon []byte
|
icon []byte
|
||||||
start sync.Once
|
start sync.Once
|
||||||
mu sync.RWMutex
|
|
||||||
labels Labels
|
|
||||||
show MenuItem
|
|
||||||
quit MenuItem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a tray controller for one application process.
|
// New creates a tray controller for one application process.
|
||||||
|
|
@ -92,22 +35,9 @@ func New(backend Backend, icon []byte) *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
backend: backend,
|
backend: backend,
|
||||||
icon: append([]byte(nil), icon...),
|
icon: append([]byte(nil), icon...),
|
||||||
labels: englishLabels(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLabels updates the current and future native tray menu labels.
|
|
||||||
func (c *Controller) SetLabels(labels Labels) {
|
|
||||||
if c == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.mu.Lock()
|
|
||||||
c.labels = labels
|
|
||||||
show, quit := c.show, c.quit
|
|
||||||
c.mu.Unlock()
|
|
||||||
applyLabels(show, quit, labels)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start registers the native tray without taking over the Wails event loop.
|
// Start registers the native tray without taking over the Wails event loop.
|
||||||
func (c *Controller) Start(actions Actions) {
|
func (c *Controller) Start(actions Actions) {
|
||||||
if c == nil || c.backend == nil {
|
if c == nil || c.backend == nil {
|
||||||
|
|
@ -117,16 +47,8 @@ func (c *Controller) Start(actions Actions) {
|
||||||
c.backend.Register(func() {
|
c.backend.Register(func() {
|
||||||
c.backend.SetIcon(c.icon)
|
c.backend.SetIcon(c.icon)
|
||||||
c.backend.SetTooltip("Verstak")
|
c.backend.SetTooltip("Verstak")
|
||||||
c.mu.RLock()
|
show := c.backend.AddMenuItem("Show Verstak", "Show the Verstak window")
|
||||||
labels := c.labels
|
quit := c.backend.AddMenuItem("Quit", "Quit Verstak")
|
||||||
c.mu.RUnlock()
|
|
||||||
show := c.backend.AddMenuItem(labels.ShowTitle, labels.ShowTooltip)
|
|
||||||
quit := c.backend.AddMenuItem(labels.QuitTitle, labels.QuitTooltip)
|
|
||||||
c.mu.Lock()
|
|
||||||
c.show, c.quit = show, quit
|
|
||||||
labels = c.labels
|
|
||||||
c.mu.Unlock()
|
|
||||||
applyLabels(show, quit, labels)
|
|
||||||
if actions.Show != nil && show != nil {
|
if actions.Show != nil && show != nil {
|
||||||
go routeClicks(show.Clicked(), actions.Show)
|
go routeClicks(show.Clicked(), actions.Show)
|
||||||
}
|
}
|
||||||
|
|
@ -137,17 +59,6 @@ func (c *Controller) Start(actions Actions) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyLabels(show, quit MenuItem, labels Labels) {
|
|
||||||
if show != nil {
|
|
||||||
show.SetTitle(labels.ShowTitle)
|
|
||||||
show.SetTooltip(labels.ShowTooltip)
|
|
||||||
}
|
|
||||||
if quit != nil {
|
|
||||||
quit.SetTitle(labels.QuitTitle)
|
|
||||||
quit.SetTooltip(labels.QuitTooltip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop releases the native tray after Wails has begun application shutdown.
|
// Stop releases the native tray after Wails has begun application shutdown.
|
||||||
func (c *Controller) Stop() {
|
func (c *Controller) Stop() {
|
||||||
if c == nil || c.backend == nil {
|
if c == nil || c.backend == nil {
|
||||||
|
|
|
||||||
|
|
@ -7,22 +7,12 @@ import (
|
||||||
|
|
||||||
type fakeMenuItem struct {
|
type fakeMenuItem struct {
|
||||||
clicked chan struct{}
|
clicked chan struct{}
|
||||||
title string
|
|
||||||
tooltip string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fakeMenuItem) Clicked() <-chan struct{} {
|
func (i *fakeMenuItem) Clicked() <-chan struct{} {
|
||||||
return i.clicked
|
return i.clicked
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fakeMenuItem) SetTitle(title string) {
|
|
||||||
i.title = title
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *fakeMenuItem) SetTooltip(tooltip string) {
|
|
||||||
i.tooltip = tooltip
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeBackend struct {
|
type fakeBackend struct {
|
||||||
icon []byte
|
icon []byte
|
||||||
tooltip string
|
tooltip string
|
||||||
|
|
@ -45,7 +35,7 @@ func (b *fakeBackend) SetTooltip(tooltip string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *fakeBackend) AddMenuItem(title, _ string) MenuItem {
|
func (b *fakeBackend) AddMenuItem(title, _ string) MenuItem {
|
||||||
item := &fakeMenuItem{clicked: make(chan struct{}, 1), title: title}
|
item := &fakeMenuItem{clicked: make(chan struct{}, 1)}
|
||||||
b.items[title] = item
|
b.items[title] = item
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
@ -99,38 +89,3 @@ func TestControllerStopsNativeTrayBackend(t *testing.T) {
|
||||||
t.Fatalf("backend quit calls = %d, want 1", backend.quitCalls)
|
t.Fatalf("backend quit calls = %d, want 1", backend.quitCalls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestControllerUsesAndUpdatesLocalizedLabels(t *testing.T) {
|
|
||||||
backend := &fakeBackend{items: make(map[string]*fakeMenuItem)}
|
|
||||||
controller := New(backend, []byte{1})
|
|
||||||
controller.SetLabels(LabelsForPreference("ru"))
|
|
||||||
controller.Start(Actions{})
|
|
||||||
|
|
||||||
show := backend.items["Показать Верстак"]
|
|
||||||
quit := backend.items["Выйти"]
|
|
||||||
if show == nil || quit == nil {
|
|
||||||
t.Fatalf("Russian tray menu = %#v", backend.items)
|
|
||||||
}
|
|
||||||
if show.tooltip != "Показать окно Верстака" || quit.tooltip != "Завершить Верстак" {
|
|
||||||
t.Fatalf("Russian tray tooltips = show:%q quit:%q", show.tooltip, quit.tooltip)
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.SetLabels(LabelsForPreference("en"))
|
|
||||||
if show.title != "Show Verstak" || quit.title != "Quit" {
|
|
||||||
t.Fatalf("English tray menu after update = show:%q quit:%q", show.title, quit.title)
|
|
||||||
}
|
|
||||||
if show.tooltip != "Show the Verstak window" || quit.tooltip != "Quit Verstak" {
|
|
||||||
t.Fatalf("English tray tooltips after update = show:%q quit:%q", show.tooltip, quit.tooltip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLabelsForSystemRussianLocale(t *testing.T) {
|
|
||||||
labels := LabelsForPreference("system", "", "ru_RU.UTF-8", "en_US.UTF-8")
|
|
||||||
if labels.ShowTitle != "Показать Верстак" || labels.QuitTitle != "Выйти" {
|
|
||||||
t.Fatalf("system Russian labels = %#v", labels)
|
|
||||||
}
|
|
||||||
labels = LabelsForPreference("system", "C.UTF-8", "", "ru_RU.UTF-8")
|
|
||||||
if labels.ShowTitle != "Show Verstak" || labels.QuitTitle != "Quit" {
|
|
||||||
t.Fatalf("higher-priority system locale must win, got %#v", labels)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,3 @@ func (item systrayMenuItem) Clicked() <-chan struct{} {
|
||||||
}
|
}
|
||||||
return item.item.ClickedCh
|
return item.item.ClickedCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (item systrayMenuItem) SetTitle(title string) {
|
|
||||||
if item.item != nil {
|
|
||||||
item.item.SetTitle(title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (item systrayMenuItem) SetTooltip(tooltip string) {
|
|
||||||
if item.item != nil {
|
|
||||||
item.item.SetTooltip(tooltip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
20
main.go
|
|
@ -86,8 +86,17 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Register Core Capabilities ─────────────────────────
|
// ─── Register Core Capabilities ─────────────────────────
|
||||||
corePluginID := capability.CorePluginID
|
corePluginID := "verstak-desktop"
|
||||||
coreCaps := capability.CorePlatformCapabilities()
|
coreCaps := []string{
|
||||||
|
"verstak/core/plugin-manager/v1",
|
||||||
|
"verstak/core/capability-registry/v1",
|
||||||
|
"verstak/core/contribution-registry/v1",
|
||||||
|
"verstak/core/permissions/v1",
|
||||||
|
"verstak/core/events/v1",
|
||||||
|
"verstak/core/files/v1",
|
||||||
|
"verstak/core/workbench/v1",
|
||||||
|
"verstak/core/notifications/v1",
|
||||||
|
}
|
||||||
if err := capRegistry.Register(corePluginID, coreCaps); err != nil {
|
if err := capRegistry.Register(corePluginID, coreCaps); err != nil {
|
||||||
log.Fatalf("[main] failed to register core capabilities: %v", err)
|
log.Fatalf("[main] failed to register core capabilities: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -230,13 +239,6 @@ func main() {
|
||||||
app = api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService, filesService, appSettingsMgr, pluginStateMgr, workspaceMgr, syncService, browserReceiver, debugEnabled)
|
app = api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService, filesService, appSettingsMgr, pluginStateMgr, workspaceMgr, syncService, browserReceiver, debugEnabled)
|
||||||
app.SetNotificationService(notifications.New(vaultService, api.NewNativeNotificationSender(), time.Now))
|
app.SetNotificationService(notifications.New(vaultService, api.NewNativeNotificationSender(), time.Now))
|
||||||
trayController := tray.New(tray.NewNativeBackend(), tray.DefaultIcon())
|
trayController := tray.New(tray.NewNativeBackend(), tray.DefaultIcon())
|
||||||
trayLabels := func(language string) tray.Labels {
|
|
||||||
return tray.LabelsForPreference(language, os.Getenv("LC_ALL"), os.Getenv("LC_MESSAGES"), os.Getenv("LANG"))
|
|
||||||
}
|
|
||||||
trayController.SetLabels(trayLabels(cfg.Language))
|
|
||||||
appSettingsMgr.SetLanguageChangedHandler(func(language string) {
|
|
||||||
trayController.SetLabels(trayLabels(language))
|
|
||||||
})
|
|
||||||
if browserReceiver != nil {
|
if browserReceiver != nil {
|
||||||
browserReceiverServer, err := browserreceiver.Start(browserreceiver.DefaultAddr, browserReceiver)
|
browserReceiverServer, err := browserreceiver.Start(browserreceiver.DefaultAddr, browserReceiver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
if command -v update-desktop-database >/dev/null 2>&1; then
|
|
||||||
update-desktop-database -q /usr/share/applications || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
|
||||||
gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
@ -10,4 +10,4 @@ export GST_PLUGIN_SYSTEM_PATH_1_0="$HERE/usr/lib/gstreamer-1.0"
|
||||||
export WEBKIT_EXEC_PATH="$HERE/usr/lib/webkit2gtk-4.1"
|
export WEBKIT_EXEC_PATH="$HERE/usr/lib/webkit2gtk-4.1"
|
||||||
|
|
||||||
cd "$HERE/usr/bin"
|
cd "$HERE/usr/bin"
|
||||||
exec "$HERE/usr/bin/verstak" "$@"
|
exec "$HERE/usr/bin/verstak-desktop" "$@"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
HERE="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
||||||
exec "$HERE/verstak-desktop" "$@"
|
|
||||||
|
|
@ -2,10 +2,8 @@
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=Verstak
|
Name=Verstak
|
||||||
Comment=Local-first workspace
|
Comment=Local-first workspace
|
||||||
Exec=verstak %U
|
Exec=verstak-desktop %U
|
||||||
Icon=verstak
|
Icon=verstak
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Office;
|
Categories=Office;Utility;
|
||||||
StartupNotify=true
|
StartupNotify=true
|
||||||
X-Verstak-Desktop-Entry=true
|
|
||||||
X-AppImage-Name=Verstak
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ fi
|
||||||
echo "=== verstak desktop Linux amd64 bundle ==="
|
echo "=== verstak desktop Linux amd64 bundle ==="
|
||||||
(cd "$OFFICIAL_PLUGINS" && ./scripts/build.sh)
|
(cd "$OFFICIAL_PLUGINS" && ./scripts/build.sh)
|
||||||
"$ROOT/scripts/install-dev-plugins.sh"
|
"$ROOT/scripts/install-dev-plugins.sh"
|
||||||
(cd "$ROOT" && go test ./internal/core/plugin -run TestBundledOfficialPluginRequirementsResolve -count=1)
|
|
||||||
"$ROOT/scripts/build.sh"
|
"$ROOT/scripts/build.sh"
|
||||||
|
|
||||||
BINARY="$ROOT/build/bin/verstak-desktop"
|
BINARY="$ROOT/build/bin/verstak-desktop"
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ if [[ ! -d "$WINDOWS_PLUGIN_DIST" ]]; then
|
||||||
echo "Windows plugin packages were not produced: $WINDOWS_PLUGIN_DIST" >&2
|
echo "Windows plugin packages were not produced: $WINDOWS_PLUGIN_DIST" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
VERSTAK_RELEASE_PLUGIN_DIR="$WINDOWS_PLUGIN_DIST" go test ./internal/core/plugin -run TestBundledOfficialPluginRequirementsResolve -count=1
|
|
||||||
|
|
||||||
# Wails' -compiler option selects a Go binary, not a C compiler. Cross-CGO
|
# Wails' -compiler option selects a Go binary, not a C compiler. Cross-CGO
|
||||||
# therefore has to be supplied through the standard Go environment instead.
|
# therefore has to be supplied through the standard Go environment instead.
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/lib" "$APPDIR/usr/share/applications" \
|
||||||
"$APPDIR/usr/share/icons/hicolor/scalable/apps"
|
"$APPDIR/usr/share/icons/hicolor/scalable/apps"
|
||||||
install -m 755 "$ROOT/packaging/linux/AppRun" "$APPDIR/AppRun"
|
install -m 755 "$ROOT/packaging/linux/AppRun" "$APPDIR/AppRun"
|
||||||
install -m 755 "$BUNDLE/verstak-desktop" "$APPDIR/usr/bin/verstak-desktop"
|
install -m 755 "$BUNDLE/verstak-desktop" "$APPDIR/usr/bin/verstak-desktop"
|
||||||
install -m 755 "$ROOT/packaging/linux/verstak" "$APPDIR/usr/bin/verstak"
|
|
||||||
install -m 644 "$ROOT/packaging/linux/verstak.desktop" "$APPDIR/verstak.desktop"
|
install -m 644 "$ROOT/packaging/linux/verstak.desktop" "$APPDIR/verstak.desktop"
|
||||||
install -m 644 "$ROOT/packaging/linux/verstak.desktop" "$APPDIR/usr/share/applications/verstak.desktop"
|
install -m 644 "$ROOT/packaging/linux/verstak.desktop" "$APPDIR/usr/share/applications/verstak.desktop"
|
||||||
install -m 644 "$ROOT/packaging/linux/verstak.svg" "$APPDIR/verstak.svg"
|
install -m 644 "$ROOT/packaging/linux/verstak.svg" "$APPDIR/verstak.svg"
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ mkdir -p "$STAGING/DEBIAN" "$STAGING/opt/verstak" "$STAGING/usr/bin" \
|
||||||
"$STAGING/usr/share/doc/verstak"
|
"$STAGING/usr/share/doc/verstak"
|
||||||
|
|
||||||
sed "s/@VERSION@/$PACKAGE_VERSION/g" "$ROOT/packaging/deb/control" > "$STAGING/DEBIAN/control"
|
sed "s/@VERSION@/$PACKAGE_VERSION/g" "$ROOT/packaging/deb/control" > "$STAGING/DEBIAN/control"
|
||||||
install -m 755 "$ROOT/packaging/deb/postinst" "$STAGING/DEBIAN/postinst"
|
|
||||||
install -m 755 "$ROOT/packaging/deb/verstak" "$STAGING/usr/bin/verstak"
|
install -m 755 "$ROOT/packaging/deb/verstak" "$STAGING/usr/bin/verstak"
|
||||||
install -m 755 "$BUNDLE/verstak-desktop" "$STAGING/opt/verstak/verstak-desktop"
|
install -m 755 "$BUNDLE/verstak-desktop" "$STAGING/opt/verstak/verstak-desktop"
|
||||||
install -m 644 "$BUNDLE/README.md" "$BUNDLE/LICENSE" "$STAGING/usr/share/doc/verstak/"
|
install -m 644 "$BUNDLE/README.md" "$BUNDLE/LICENSE" "$STAGING/usr/share/doc/verstak/"
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,6 @@ done
|
||||||
grep -Fq 'dpkg-deb' "$ROOT/scripts/package-deb.sh"
|
grep -Fq 'dpkg-deb' "$ROOT/scripts/package-deb.sh"
|
||||||
grep -Fq -- '--build' "$ROOT/scripts/package-deb.sh"
|
grep -Fq -- '--build' "$ROOT/scripts/package-deb.sh"
|
||||||
grep -Fq 'packaging/deb/control' "$ROOT/scripts/package-deb.sh"
|
grep -Fq 'packaging/deb/control' "$ROOT/scripts/package-deb.sh"
|
||||||
grep -Fq 'packaging/deb/postinst' "$ROOT/scripts/package-deb.sh"
|
|
||||||
test -x "$ROOT/packaging/deb/postinst"
|
|
||||||
sh -n "$ROOT/packaging/deb/postinst"
|
|
||||||
grep -Fxq 'Exec=verstak %U' "$ROOT/packaging/linux/verstak.desktop"
|
|
||||||
grep -Fxq 'X-Verstak-Desktop-Entry=true' "$ROOT/packaging/linux/verstak.desktop"
|
|
||||||
grep -Fxq 'Categories=Office;' "$ROOT/packaging/linux/verstak.desktop"
|
|
||||||
grep -Fq 'update-desktop-database' "$ROOT/packaging/deb/postinst"
|
|
||||||
grep -Fq 'gtk-update-icon-cache' "$ROOT/packaging/deb/postinst"
|
|
||||||
grep -Fq 'packaging/linux/verstak' "$ROOT/scripts/package-appimage.sh"
|
|
||||||
grep -Fq 'usr/bin/verstak' "$ROOT/packaging/linux/AppRun"
|
|
||||||
grep -Fq 'libwebkit2gtk-4.1-0' "$ROOT/packaging/deb/control"
|
grep -Fq 'libwebkit2gtk-4.1-0' "$ROOT/packaging/deb/control"
|
||||||
grep -Fq 'libayatana-appindicator3-1' "$ROOT/packaging/deb/control"
|
grep -Fq 'libayatana-appindicator3-1' "$ROOT/packaging/deb/control"
|
||||||
grep -Fq 'appimagetool' "$ROOT/scripts/package-appimage.sh"
|
grep -Fq 'appimagetool' "$ROOT/scripts/package-appimage.sh"
|
||||||
|
|
@ -51,7 +41,5 @@ grep -Fq 'package-appimage.sh' "$ROOT/scripts/release.sh"
|
||||||
grep -Fq 'package-windows-portable.sh' "$ROOT/scripts/release.sh"
|
grep -Fq 'package-windows-portable.sh' "$ROOT/scripts/release.sh"
|
||||||
git -C "$ROOT" check-ignore -q verstak-desktop-res.syso
|
git -C "$ROOT" check-ignore -q verstak-desktop-res.syso
|
||||||
grep -Fq 'chmod -R a+rX' "$ROOT/scripts/build-linux-bundle.sh"
|
grep -Fq 'chmod -R a+rX' "$ROOT/scripts/build-linux-bundle.sh"
|
||||||
grep -Fq 'TestBundledOfficialPluginRequirementsResolve' "$ROOT/scripts/build-linux-bundle.sh"
|
|
||||||
grep -Fq 'TestBundledOfficialPluginRequirementsResolve' "$ROOT/scripts/build-windows.sh"
|
|
||||||
|
|
||||||
echo "desktop package script contracts passed"
|
echo "desktop package script contracts passed"
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,6 @@ DEBUG_MODE_STATUS=0
|
||||||
(cd "$ROOT" && node --experimental-vm-modules frontend/tests/debug-mode-test.mjs) || DEBUG_MODE_STATUS=$?
|
(cd "$ROOT" && node --experimental-vm-modules frontend/tests/debug-mode-test.mjs) || DEBUG_MODE_STATUS=$?
|
||||||
report "session-only debug mode" "$DEBUG_MODE_STATUS"
|
report "session-only debug mode" "$DEBUG_MODE_STATUS"
|
||||||
|
|
||||||
SELECT_STYLE_STATUS=0
|
|
||||||
(cd "$ROOT" && node frontend/tests/select-styles-test.mjs) || SELECT_STYLE_STATUS=$?
|
|
||||||
report "workspace select styles" "$SELECT_STYLE_STATUS"
|
|
||||||
|
|
||||||
# ── Frontend tests ──
|
# ── Frontend tests ──
|
||||||
echo "[frontend]"
|
echo "[frontend]"
|
||||||
if ensure_npm_deps "$ROOT/frontend"; then
|
if ensure_npm_deps "$ROOT/frontend"; then
|
||||||
|
|
|
||||||