diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 46c5e03c..6733fd20 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,45 +1,45 @@ -{ - "permissions": { - "allow": [ - "Bash(xargs grep:*)", - "Bash(dotnet build:*)", - "Bash(dotnet run:*)", - "Bash(dotnet format:*)", - "Bash(dotnet new:*)", - "Bash(dotnet tool:*)", - "Bash(dotnet csharpier:*)", - "Bash(dotnet fsi:*)", - "Bash(grep -r MaterialIconKind. /home/erickazevedo/Downloads/files/src/lude=*.cs)", - "Bash(python3 -c \":*)", - "Bash(grep -n \"InvalidateWires\\\\|Dispatcher\" /home/erickazevedo/Downloads/files/src/ndow.axaml.cs /home/erickazevedo/Downloads/files/src/DBWeaver.U - "WebFetch(domain:docs.avaloniaui.net)", - "Bash(git fsck:*)", - "Bash(find .git/objects -type f -empty -delete)", - "Bash(git update-ref:*)", - "Bash(git fetch:*)", - "Bash(git add:*)", - "Bash(python3:*)", - "Bash(git commit:*)", - "Bash(dotnet test:*)", - "Bash(xargs wc:*)", - "Bash(find /c/Users/azeve/Documents/VisualSqlArchtect/tests -type f -name *.cs)", - "Bash(grep -rn \"\\\\b[0-9]{3,}\\\\b\" /c/Users/azeve/Documents/VisualSqlArchtect/src --include=\"*.cs\")", - "Bash(grep -v \"///\")", - "Bash(rm \"c:/Users/azeve/Documents/VisualSqlArchtect/src/ls/BezierWireLayer.cs\")", - "Bash(rm \"c:/Users/azeve/Documents/VisualSqlArchtect/src/ls/PinDragInteraction.cs\")", - "Bash(grep -v \"private\\\\|//\")", - "Bash(grep -v \"//\")", - "Bash(find \"c:\\\\Users\\\\azeve\\\\Documents\\\\VisualSqlArchtect/src/ls/InfiniteCanvas\" -name \"*.cs\" -exec wc -l {} \\\\;)", - "Bash(wc -l /home/erickazevedo/Documentos/VisualSqlArchtect/src//*.cs)", - "Bash(grep -E \"\\\\.cs$\")", - "Bash(grep -n \"class NodeDefinitionRegistry\\\\|static class NodeDefinitionRegistry\" /home/erickazevedo/Documentos/VisualSqlArchtect/src/s)", - "Read(//tmp/**)", - "Bash(sqlite3 --version)", - "Bash(/home/erickazevedo/.claude/plugins/cache/claude-plugins-official/superpowers/5.0.7/skills/brainstorming/scripts/start-server.sh --project-dir /home/erickazevedo/Documentos/VisualSqlArchtect)", - "Bash(git worktree:*)", - "Bash(git -C /home/erickazevedo/Documentos/VisualSqlArchtect/.worktrees/ui-design-system-overhaul commit -m ':*)", - "Bash(git stash:*)", - "Bash(grep -v \"^$\")" - ] - } -} +{ + "permissions": { + "allow": [ + "Bash(xargs grep:*)", + "Bash(dotnet build:*)", + "Bash(dotnet run:*)", + "Bash(dotnet format:*)", + "Bash(dotnet new:*)", + "Bash(dotnet tool:*)", + "Bash(dotnet csharpier:*)", + "Bash(dotnet fsi:*)", + "Bash(grep -r MaterialIconKind. /home/erickazevedo/Downloads/files/src/lude=*.cs)", + "Bash(python3 -c \":*)", + "Bash(grep -n \"InvalidateWires\\\\|Dispatcher\" /home/erickazevedo/Downloads/files/src/ndow.axaml.cs /home/erickazevedo/Downloads/files/src/AkkornStudio.U + "WebFetch(domain:docs.avaloniaui.net)", + "Bash(git fsck:*)", + "Bash(find .git/objects -type f -empty -delete)", + "Bash(git update-ref:*)", + "Bash(git fetch:*)", + "Bash(git add:*)", + "Bash(python3:*)", + "Bash(git commit:*)", + "Bash(dotnet test:*)", + "Bash(xargs wc:*)", + "Bash(find /c/Users/azeve/Documents/VisualSqlArchtect/tests -type f -name *.cs)", + "Bash(grep -rn \"\\\\b[0-9]{3,}\\\\b\" /c/Users/azeve/Documents/VisualSqlArchtect/src --include=\"*.cs\")", + "Bash(grep -v \"///\")", + "Bash(rm \"c:/Users/azeve/Documents/VisualSqlArchtect/src/ls/BezierWireLayer.cs\")", + "Bash(rm \"c:/Users/azeve/Documents/VisualSqlArchtect/src/ls/PinDragInteraction.cs\")", + "Bash(grep -v \"private\\\\|//\")", + "Bash(grep -v \"//\")", + "Bash(find \"c:\\\\Users\\\\azeve\\\\Documents\\\\VisualSqlArchtect/src/ls/InfiniteCanvas\" -name \"*.cs\" -exec wc -l {} \\\\;)", + "Bash(wc -l /home/erickazevedo/Documentos/VisualSqlArchtect/src//*.cs)", + "Bash(grep -E \"\\\\.cs$\")", + "Bash(grep -n \"class NodeDefinitionRegistry\\\\|static class NodeDefinitionRegistry\" /home/erickazevedo/Documentos/VisualSqlArchtect/src/s)", + "Read(//tmp/**)", + "Bash(sqlite3 --version)", + "Bash(/home/erickazevedo/.claude/plugins/cache/claude-plugins-official/superpowers/5.0.7/skills/brainstorming/scripts/start-server.sh --project-dir /home/erickazevedo/Documentos/VisualSqlArchtect)", + "Bash(git worktree:*)", + "Bash(git -C /home/erickazevedo/Documentos/VisualSqlArchtect/.worktrees/ui-design-system-overhaul commit -m ':*)", + "Bash(git stash:*)", + "Bash(grep -v \"^$\")" + ] + } +} diff --git a/.gitignore b/.gitignore index 1ec7cd89..a01e7159 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ claude/settings.local.json archive/* .codex *.log -.superpowers/* -.worktrees/ -dbweaver_app/* -*.md +.superpowers/* +.worktrees/ +dbweaver_app/* +*.md +node_modules/ diff --git a/README.md b/README.md index 7f5a6b2e..0b783ca7 100644 --- a/README.md +++ b/README.md @@ -1,362 +1,245 @@ -
- -# DBWeaver - -**Crie consultas SQL visualmente - sem precisar digitar.** - -Um designer SQL baseado em nós e canvas infinito que compila para SQL real e parametrizado em SQL Server, PostgreSQL, MySQL e SQLite. - -[![CI](https://github.com/TheyCallMeErick/VisualSqlArchtect/actions/workflows/ci.yml/badge.svg)](https://github.com/TheyCallMeErick/VisualSqlArchtect/actions/workflows/ci.yml) -[![Release](https://github.com/TheyCallMeErick/VisualSqlArchtect/actions/workflows/release.yml/badge.svg)](https://github.com/TheyCallMeErick/VisualSqlArchtect/releases) -[![.NET](https://img.shields.io/badge/.NET-9.0-512BD4)](https://dotnet.microsoft.com) -[![Avalonia](https://img.shields.io/badge/Avalonia-11.1-8B5CF6)](https://avaloniaui.net) - -
- ---- - - - - -**Idioma / Language:** 🇧🇷 [Português (PT-BR)](#pt-br) · 🇺🇸 [English](#english) - -**Status / Backlog:** [PROJECT_BACKLOG.md](PROJECT_BACKLOG.md) - -## O que é? - -O DBWeaver permite arrastar nós para o canvas e conectá-los para montar consultas SQL. Cada conexão vira um JOIN, cada nó de filtro vira uma cláusula WHERE, e o resultado aparece instantaneamente como SQL ao vivo abaixo do canvas - sem digitação, sem erros de sintaxe e sem dor de cabeça com dialetos. - -Diagrama visual do fluxo (query por nós): veja [Diagramas compartilhados](#shared-diagrams). - ---- - -## Funcionalidades - -### Canvas -- **Pan e zoom infinitos** - pan com botão do meio, zoom com scroll e atalhos de teclado -- **Nós por arrastar e soltar** - paleta de busca com fuzzy find e fluxo keyboard-first -- **Conexões por fios** - curvas Bézier com validação em tempo real e pinos com checagem de tipo -- **Multi-seleção e alinhamento** - selecione, mova, exclua e alinhe grupos de nós -- **Auto-layout** - um clique para organizar um grafo bagunçado em uma árvore limpa -- **Desfazer / refazer** - pilha de comandos granular, Ctrl+Z / Ctrl+Y -- **Salvar / carregar sessões** - persistência do canvas em JSON e do workspace (query + DDL) - -### Geração de SQL -- **Pré-visualização SQL em tempo real** - cada edição atualiza a barra de SQL instantaneamente -- **Multi-dialeto** - SQL Server, PostgreSQL, MySQL e SQLite -- **Preview seguro** - `ExecutePreviewAsync` sempre faz rollback; nunca altera dados -- **Plano EXPLAIN** - um clique para visualizar o plano de execução retornado pelo servidor -- **Importador SQL** - cole SQL existente e importe de volta como grafo de nós - -### Biblioteca de nós - -| Categoria | Nós | -|---|---| -| **Fonte de dados** | Tabela, Raw SQL | -| **Comparação** | =, ≠, >, ≥, <, ≤, BETWEEN, LIKE, IS NULL | -| **Portas lógicas** | AND, OR, NOT | -| **Agregações** | SUM, COUNT, AVG, MIN, MAX, COUNT DISTINCT | -| **Matemática** | +, −, ×, ÷, ROUND, ABS, MOD, POWER | -| **String** | UPPER, LOWER, TRIM, LENGTH, CONCAT, REPLACE, SUBSTRING, REGEX | -| **Condicionais** | CASE WHEN, IIF / COALESCE | -| **JSON** | JSON Extract, JSON Value, JSON Array Length | -| **Tipos** | CAST / CONVERT | -| **Modificadores de resultado** | ORDER BY, LIMIT / TOP, DISTINCT, GROUP BY, HAVING | - -### Sistema de tipos de pino - -Cada pino tem **forma + cor** que identificam o que ele carrega. Forma indica a família semântica; cor indica o tipo específico. - -| Família | Forma | Tipos | Cor | -|---|---|---|---| -| Escalar | Círculo `●` | Text, Integer, Decimal, Boolean, DateTime, Json | Azul · Verde · Âmbar · Ciano · Índigo | -| Referência de coluna | Losango `◆` | ColumnRef | Laranja | -| Lista de colunas | Losango vazado `◇` | ColumnSet | Ouro | -| Conjunto de linhas | Losango achatado `⬥` | RowSet | Rosa | -| SQL bruto | Círculo tracejado `○` | Expression | Cinza | - -Pinos incompatíveis ficam apagados durante o drag. Vermelho é reservado para erros de validação estática (pino obrigatório sem conexão). - -Referência completa: [docs/PIN_TYPES_REFERENCE.md](docs/PIN_TYPES_REFERENCE.md) - ---- - -### Integração com banco de dados -- **Gerenciador de conexões** - salve e nomeie múltiplas conexões, com teste em um clique -- **Fluxo de conexão com confirmação** - ao conectar, você pode manter o canvas atual ou limpá-lo antes de carregar o novo contexto de banco -- **Explorador de schema** - navegue por schemas, tabelas e colunas em árvore lateral -- **Carregamento automático de tabelas** - ao conectar com sucesso, os metadados são carregados e as tabelas ficam disponíveis imediatamente no menu de busca -- **Detecção automática de joins** - detecta relacionamentos FK e convenções de nome (`orders.customer_id -> customers.id`) e sugere o join correto -- **Biblioteca de templates de consulta** - salve e carregue snippets reutilizáveis de grafos - -### Ferramentas para desenvolvimento -- **Painel de diagnóstico** - memória em tempo real, FPS e estado da conexão -- **Overlay de benchmark** - mede tempo de renderização e compilação -- **Suíte automatizada de testes** - compiladores de nós, emissão por dialeto, heurísticas de metadados e segurança de ciclo de vida/UX - -### Atualizações recentes -- **Hardening do preview SQL** - o SQL de preview evita placeholders bound no caminho de execução que exige SQL literal-safe -- **Sincronização segura de provider** - o provider do Live SQL é sincronizado com o provider da conexão ativa para evitar erros entre dialetos -- **Consistência de UX em modais** - diálogos de overlay podem ser fechados por clique no backdrop e com `Esc` - ---- - -## Download - -Baixe o binário self-contained mais recente em [Releases](https://github.com/TheyCallMeErick/VisualSqlArchtect/releases) - sem necessidade de instalar .NET. - -| Plataforma | Binário | -|---|---| -| Windows x64 | `xe` | -| Linux x64 | `` | -| macOS x64 | `| - ---- - -## Compilar do código-fonte - -**Pré-requisitos:** [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) - -```bash -git clone https://github.com/TheyCallMeErick/VisualSqlArchtect.git -cd VisualSqlArchtect - -# Executar a aplicação -dotnet run --project src/xecutar a suíte de testes -dotnet test files.sln -``` - ---- - -## Arquitetura - -Diagrama de arquitetura: veja [Diagramas compartilhados](#shared-diagrams). - ---- - -## Estrutura do projeto - -Diagrama da estrutura do projeto: veja [Diagramas compartilhados](#shared-diagrams). - ---- - -## Contribuição - -1. Faça um fork do repositório -2. Crie uma branch de feature a partir de `main` -3. Execute `dotnet test files.sln` - todos os testes devem passar -4. Abra um pull request - -O pipeline de CI roda em todo PR; o pipeline de release publica binários automaticamente quando uma tag `v*` é enviada. - ---- - -## English - -### What is it? - -DBWeaver is a node-based SQL designer with an infinite canvas. You connect nodes to compose queries visually: connections become JOINs, filter nodes become WHERE clauses, and the generated SQL is shown live below the canvas. - -### Features - -#### Canvas -- **Infinite pan & zoom** - middle-mouse pan, scroll-wheel zoom, keyboard shortcuts -- **Drag-and-drop nodes** - searchable palette with fuzzy find and keyboard-first workflow -- **Wired connections** - bezier curves with live validation and type-checked pins -- **Multi-select & align** - move, delete and align node groups -- **Auto-layout** - one-click graph organization -- **Undo / redo** - granular command stack (`Ctrl+Z` / `Ctrl+Y`) -- **Save / load sessions** - JSON canvas persistence and workspace persistence (query + DDL) - -#### SQL Generation -- **Real-time SQL preview** - updates instantly as the graph changes -- **Multi-dialect** - SQL Server, PostgreSQL, MySQL and SQLite -- **Safe previews** - `ExecutePreviewAsync` always rolls back (no data mutations) -- **EXPLAIN plan** - visualize execution plans returned by the database -- **SQL importer** - paste SQL and rebuild it as a node graph - -#### Pin type system - -Every pin has a **shape + color** that communicates what it carries. Shape encodes the semantic family; color encodes the specific type. - -| Family | Shape | Types | Color | -|---|---|---|---| -| Scalar | Circle `●` | Text, Integer, Decimal, Boolean, DateTime, Json | Blue · Green · Amber · Cyan · Indigo | -| Column reference | Diamond `◆` | ColumnRef | Orange | -| Column list | Hollow diamond `◇` | ColumnSet | Gold | -| Row set | Flat diamond `⬥` | RowSet | Pink | -| Raw SQL | Dashed circle `○` | Expression | Gray | - -Incompatible pins fade out during drag. Red is reserved for static validation errors (required pin with no connection). - -Full reference: [docs/PIN_TYPES_REFERENCE.md](docs/PIN_TYPES_REFERENCE.md) - ---- - -#### Database Integration -- **Connection manager** - store and test multiple connections quickly -- **Connect confirmation flow** - choose whether to keep or clear the current canvas when connecting -- **Schema explorer** - browse schemas, tables and columns -- **Automatic table loading** - metadata loads on connect and tables become immediately available in search -- **Auto-join detection** - infers relationships from FK metadata and naming conventions -- **Query template library** - save reusable graph snippets - -#### Developer Tooling -- **Diagnostics panel** - memory, FPS and connection state -- **Benchmark overlay** - render/compile timing insights -- **Automated tests** - coverage for node compilation, dialect emission, metadata heuristics and lifecycle/UX safety - -#### Recent updates -- **Preview SQL hardening** - avoids bound placeholders on literal-safe preview execution paths -- **Provider synchronization** - Live SQL provider now follows the active connection provider to prevent cross-dialect issues -- **Modal UX consistency** - overlay dialogs close via backdrop click and `Esc` - -### Download - -Get the latest self-contained binaries from [Releases](https://github.com/TheyCallMeErick/VisualSqlArchtect/releases). - -| Platform | Binary | -|---|---| -| Windows x64 | `xe` | -| Linux x64 | `` | -| macOS x64 | `| - -### Build from source - -**Prerequisites:** [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) - -```bash -git clone https://github.com/TheyCallMeErick/VisualSqlArchtect.git -cd VisualSqlArchtect - -# Run the app -dotnet run --project src/un tests -dotnet test files.sln -``` - -### Architecture - -The architecture and project-structure diagrams are shared for both languages below: [Shared Diagrams](#shared-diagrams). - -### Contributing - -1. Fork the repository -2. Create a feature branch from `main` -3. Run `dotnet test files.sln` (all tests must pass) -4. Open a pull request - -CI runs on every PR, and the release pipeline publishes binaries automatically for `v*` tags. - ---- - - - -## Diagramas compartilhados / Shared diagrams - -### Fluxo visual de query / Visual query flow - -```mermaid -flowchart TD - O[orders] - C[customers] - W[where status active] - S[select id name total] - L["Live SQL Bar -SELECT o.id c.name o.total -FROM orders o -LEFT JOIN customers c -WHERE status active"] - - O --> S - C --> S - W --> S - S --> L -``` - -### Arquitetura / Architecture - -```mermaid -flowchart TB - UI[UI Avalonia Canvas] - NC[NodeControl] - BWL[BezierWireLayer] - SC[SidebarControl] - LSB[LiveSqlBar] - - NG[NodeGraph] - NGC[NodeGraphCompiler] - NCF[NodeCompilerFactory] - DSC[DataSourceCompiler] - CC[ComparisonCompiler] - LGC[LogicGateCompiler] - AC[AggregateCompiler] - JC[JsonCompiler] - CNG[CompiledNodeGraph] - - QBS[QueryBuilderService SqlKata] - ISD[SqlDialectRegistry] - ISF[SqlFunctionRegistry] - CQ[CompiledQuery] - - DBO[DbOrchestrator] - OPS[Preview Schema Explain] - DBS[SQL Server PostgreSQL MySQL SQLite] - - UI --> NC - UI --> BWL - UI --> SC - UI --> LSB - UI --> NG - - NG --> NGC - NGC --> NCF - NCF --> DSC - NCF --> CC - NCF --> LGC - NCF --> AC - NCF --> JC - DSC --> CNG - CC --> CNG - LGC --> CNG - AC --> CNG - JC --> CNG - - CNG --> QBS - ISD --> QBS - ISF --> QBS - QBS --> CQ - - CQ --> DBO - OPS --> DBO - DBO --> DBS -``` - -### Estrutura do projeto / Project structure - -```mermaid -flowchart TD - ROOT[VisualSqlArchtect] - - ROOT --> SRC[src] - ROOT --> TESTS[tests] - - SRC --> CORE[ SRC --> UI[ CORE --> NODES[Nodes] - CORE --> EXPR[Expressions] - CORE --> META[Metadata] - CORE --> PROV[Providers] - CORE --> QE[QueryEngine] - CORE --> REG[Registry] - - UI --> CONTROLS[Controls] - UI --> VMS[ViewModels] - UI --> SERVICES[Services] - UI --> SERIAL[Serialization] - - TESTS --> TESTPROJ[`` - ---- - -
-Construído com Avalonia UI · .NET 9 · SqlKata -Built with Avalonia UI · .NET 9 · SqlKata -
+
+ +
+ +# AkkornStudio + +### Monte consultas SQL arrastando nós — veja o SQL ao vivo enquanto você cria. + +
+ +[![CI](https://github.com/TheyCallMeErick/VisualSqlArchtect/actions/workflows/ci.yml/badge.svg)](https://github.com/TheyCallMeErick/VisualSqlArchtect/actions/workflows/ci.yml) +[![Release](https://github.com/TheyCallMeErick/VisualSqlArchtect/actions/workflows/release.yml/badge.svg)](https://github.com/TheyCallMeErick/VisualSqlArchtect/releases) +[![.NET 9](https://img.shields.io/badge/.NET-9.0-512BD4)](https://dotnet.microsoft.com) +[![Avalonia UI](https://img.shields.io/badge/Avalonia-11.1-8B5CF6)](https://avaloniaui.net) +[![License: MIT](https://img.shields.io/badge/License-MIT-22c55e)](LICENSE) + +
+ +[**Download**](#-download) · [**Como usar**](#-como-usar) · [**Features**](#-features) · [**Build**](#-compilar-do-código-fonte) + +
+ +
+ +--- + +## O que é? + +O **AkkornStudio** é um designer SQL visual baseado em nós e canvas infinito. Você conecta blocos — tabelas, filtros, funções — e o SQL vai sendo gerado em tempo real, sem precisar digitar uma linha. + +``` +┌──────────┐ ┌─────────────┐ ┌──────────────┐ +│ orders │──▶│ JOIN ON │──▶│ SELECT │──▶ SQL ao vivo +│ (tabela) │ │ customer_id│ │ id, nome, │ +└──────────┘ └─────────────┘ │ total │ + └──────────────┘ +┌──────────┐ ▲ +│customers │─────────┘ +│ (tabela) │ +└──────────┘ +``` + +O resultado aparece instantaneamente na **barra de SQL ao vivo** abaixo do canvas. Clique em executar para rodar direto no banco. Pronto. + +--- + +## ✨ Features + +### 🎨 Canvas infinito + +Um ambiente de trabalho sem limites onde você monta consultas visualmente. + +| | | +|---|---| +| **Pan & zoom** | Scroll para zoom · botão do meio para pan · `Space` para modo mão | +| **Arrastar e soltar** | Paleta de nós com busca fuzzy · fluxo keyboard-first | +| **Fios Bézier** | Curvas suaves com validação de tipo em tempo real | +| **Seleção múltipla** | Rubber-band selection · mover, alinhar e deletar grupos | +| **Auto-layout** | Um clique para organizar o grafo automaticamente | +| **Guias de alinhamento** | Snap em 6 pontos com guias visuais | +| **Fios flexíveis** | Estilos Bézier, reto e ortogonal · edição de breakpoints | +| **Desfazer / refazer** | Pilha de comandos granular `Ctrl+Z` / `Ctrl+Y` | +| **Salvar sessões** | Persistência completa do canvas em JSON | + +--- + +### 🧩 Biblioteca de nós + +Mais de 48 nós organizados por função: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoriaNós
Fonte de dadosTabela, Raw SQL, Alias
Comparação=, ≠, >, ≥, <, ≤, BETWEEN, LIKE, IS NULL, IS NOT NULL
LógicaAND, OR, NOT
AgregaçõesSUM, COUNT, COUNT DISTINCT, AVG, MIN, MAX
Matemática+, −, ×, ÷, ROUND, ABS, CEIL, FLOOR
StringUPPER, LOWER, TRIM, LENGTH, CONCAT, REPLACE, SUBSTRING, REGEX Match/Replace/Extract
CondicionaisNULL Fill, Empty Fill, Value Map (CASE WHEN), CAST, Scalar From Column
JSONJSON Extract, JSON Array Length
ResultadoORDER BY, LIMIT / TOP, DISTINCT, GROUP BY, HAVING, ColumnSet Merge
LiteraisNúmero, String, Data/DateTime, Booleano
ExportaçãoJSON, CSV, Excel
+ +--- + +### ⚡ SQL ao vivo + +- **Pré-visualização instantânea** — cada nó conectado atualiza o SQL na hora +- **Multi-dialeto** — SQL Server, PostgreSQL, MySQL e SQLite +- **Execução segura** — preview sempre faz rollback; nunca altera dados +- **Plano EXPLAIN** — visualize o plano de execução com um clique +- **Importar SQL** — cole uma query existente e ela vira um grafo de nós automaticamente + +--- + +### 🏗️ Modo DDL + +Construa e modifique sua estrutura de banco de dados visualmente: + +- `CREATE TABLE`, `CREATE VIEW`, `CREATE INDEX`, `CREATE SEQUENCE` +- `ALTER TABLE` — adicionar, remover, renomear e alterar colunas +- Gerenciamento de constraints — PK, FK, UNIQUE, CHECK, DEFAULT +- Definições de tipo customizadas +- Compilação e validação completas antes de executar + +--- + +### 🔬 Schema Analysis + +Conecte ao banco e receba um relatório automático da qualidade do seu schema: + +| Regra | O que detecta | +|---|---| +| `MISSING_FK` | Relacionamentos sem foreign key declarada | +| `FK_CATALOG_INCONSISTENT` | FKs com inconsistências de integridade referencial | +| `NAMING_CONVENTION_VIOLATION` | Violações de convenção (snake_case, camelCase, PascalCase) | +| `LOW_SEMANTIC_NAME` | Colunas e tabelas com nomes pouco descritivos | +| `MISSING_REQUIRED_COMMENT` | Objetos sem documentação obrigatória | +| `NF1_HINT_MULTI_VALUED` | Indícios de violação da 1ª Forma Normal | +| `NF2_HINT_PARTIAL_DEPENDENCY` | Indícios de violação da 2ª Forma Normal | +| `NF3_HINT_TRANSITIVE_DEPENDENCY` | Indícios de violação da 3ª Forma Normal | + +Cada issue vem com sugestão de SQL para corrigir. + +--- + +### 🔌 Conexão com o banco + +- **Gerenciador de conexões** — salve múltiplas conexões com nome +- **Explorador de schema** — navegue por schemas, tabelas e colunas +- **Auto-join** — detecta FKs e convenções de nome e sugere o join correto +- **Templates** — salve e reutilize grafos de consulta + +--- + +### ⌨️ Atalhos de teclado + +| Atalho | Ação | +|---|---| +| `Space` | Modo pan (segurar) | +| `F` | Centralizar seleção | +| `Shift+F` | Fit da seleção na tela | +| `Arrow Keys` | Mover nós (1px) | +| `Shift+Arrow` | Mover nós (10px) | +| `Ctrl+Z / Ctrl+Y` | Desfazer / refazer | +| `Ctrl+Shift+X` | Bypass do nó selecionado | +| `Alt+Q / Alt+E` | Selecionar upstream / downstream | +| `Ctrl+Click (fio)` | Deletar fio | +| `Delete / Backspace` | Deletar seleção | +| `Esc` | Deselecionar tudo | + +--- + +## 📥 Download + +Baixe o binário self-contained em [**Releases**](https://github.com/TheyCallMeErick/VisualSqlArchtect/releases) — sem necessidade de instalar o .NET. + +| Plataforma | Arquivo | +|---|---| +| Windows x64 | `AkkornStudio-win-x64.exe` | +| Linux x64 | `AkkornStudio-linux-x64` | +| macOS x64 | `AkkornStudio-osx-x64` | + +--- + +## 🛠️ Compilar do código-fonte + +**Pré-requisito:** [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) + +```bash +git clone https://github.com/TheyCallMeErick/VisualSqlArchtect.git +cd VisualSqlArchtect + +# Executar o app +dotnet run --project src/AkkornStudio.UI + +# Rodar os testes +dotnet test files.sln +``` + +--- + +## 🤝 Contribuição + +1. Fork do repositório +2. Branch a partir de `main` +3. `dotnet test files.sln` — todos os testes devem passar +4. Abra um pull request + +O pipeline de CI roda em todo PR. O pipeline de release publica binários automaticamente para tags `v*`. + +--- + +## 💡 Como usar + +``` +1. Abra o app e conecte ao seu banco de dados +2. Arraste uma tabela do explorador de schema para o canvas +3. Adicione nós de filtro, agregação ou função da paleta +4. Conecte os pinos — o SQL aparece na barra abaixo +5. Clique em Executar para rodar (preview é sempre seguro) +``` + +--- + +
+ +Construído com **Avalonia UI** · **.NET 9** · **SqlKata** + +
diff --git a/STRUCTURE.md b/STRUCTURE.md index 7c7840e2..5f629219 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -1,132 +1,132 @@ -# Estrutura do Projeto DBWeaver - -## Organização da Solução - -``` -. -├── files.sln # Arquivo de solução principal -├── README.md # Documentação do projeto -├── STRUCTURE.md # Este arquivo -│ -├── src/ # Código-fonte principal -│ ├── Projeto Core (DBWeaver.c─ Core/ # Orquestradores principais -│ │ │ ├── BaseDbOrchestrator.cs -│ │ │ └── IDbOrchestrator.cs -│ │ │ -│ │ ├── Metadata/ # Serviços de metadados -│ │ │ ├── MetadataService.cs -│ │ │ ├── DbMetadata.cs -│ │ │ ├── IDatabaseInspector.cs -│ │ │ ├── AutoJoinDetector.cs -│ │ │ └── Inspectors/ # Adaptadores para cada banco -│ │ │ ├── MySqlInspector.cs -│ │ │ ├── PostgresInspector.cs -│ │ │ └── SqlServerInspector.cs -│ │ │ -│ │ ├── Nodes/ # Modelo de nós do grafo -│ │ │ ├── NodeDefinition.cs -│ │ │ ├── NodeGraph.cs -│ │ │ ├── NodeGraphCompiler.cs -│ │ │ └── ISqlExpression.cs -│ │ │ -│ │ ├── Providers/ # Orquestradores específicos por BD -│ │ │ ├── MySqlOrchestrator.cs -│ │ │ ├── PostgresOrchestrator.cs -│ │ │ └── SqlServerOrchestrator.cs -│ │ │ -│ │ ├── QueryEngine/ # Serviços de construção de queries -│ │ │ ├── QueryBuilderService.cs -│ │ │ └── QueryGeneratorService.cs -│ │ │ -│ │ ├── Registry/ # Registro de funções SQL -│ │ │ └── SqlFunctionRegistry.cs -│ │ │ -│ │ ├── ServiceRegistration.cs # Configuração de injeção dependência -│ │ └── │ -│ └── Projeto UI (DBWeaver.U ├── App.axaml # Arquivo XAML principal -│ ├── App.axaml.cs # Code-behind da aplicação -│ ├── MainWindow.axaml # Janela principal -│ ├── MainWindow.axaml.cs -│ │ -│ ├── Assets/ # Recursos -│ │ └── Themes/ # Temas de cores -│ │ ├── AppStyles.axaml -│ │ └── DesignTokens.axaml -│ │ -│ ├── Controls/ # Controles Avalonia customizados -│ │ ├── NodeControl.axaml -│ │ ├── NodeControl.axaml.cs -│ │ ├── InfiniteCanvas.cs -│ │ ├── PropertyPanelControl.axaml -│ │ ├── PropertyPanelControl.axaml.cs -│ │ ├── LiveSqlBar.axaml -│ │ ├── LiveSqlBar.axaml.cs -│ │ ├── AutoJoinOverlay.axaml -│ │ ├── AutoJoinOverlay.axaml.cs -│ │ ├── SearchMenuControl.axaml -│ │ ├── SearchMenuControl.axaml.cs -│ │ ├── BezierWireLayer.cs -│ │ └── PinDragInteraction.cs -│ │ -│ ├── ViewModels/ # ViewModels MVVM -│ │ ├── CanvasViewModel.cs -│ │ ├── LiveSqlBarViewModel.cs -│ │ ├── PropertyPanelViewModel.cs -│ │ ├── AutoJoinOverlayViewModel.cs -│ │ └── UndoRedoStack.cs -│ │ -│ ├── Serialization/ # Serviços de serialização -│ │ └── CanvasSerializer.cs -│ │ -│ ├── DataPreviewPanel.axaml -│ └── -│ -└── tests/ # Projetos de testes - └── Projeto de Testes (DBWeaver.T ├── ArchitectureTests.cs - ├── AtomicNodeTests.cs - ├── MetadataTests.cs - └── roj -``` - -## Namespaces - -### Core (- `nterfaces e classes base -- ` - Serviços de metadados -- `Inspectors` - Inspetores específicos por BD -- `Modelo de nós -- `` - Implementações de orquestradores -- `ne` - Serviços de queries -- ` - Registro de funções - -### UI () -- `igo principal da aplicação -- `ls` - Controles customizados -- `dels` - ViewModels -- `ization` - Serviços de serialização - -### Testes (roj) -- `Testes unitários - -## Dependências do Projeto - -### DBWeaver (Core) -- ✅ Independente de UI -- Depende de: SqlKata, database drivers - -### DBWeaver.UI -- Depende de: Core + Avalonia - -### DBWeaver.Tests -- Depende de: Core + xUnit - -## Compilação - -```bash -# Build completo -dotnet build - -# Build apenas do Core -dotnet build src/cpenas da UI -dotnet build src/er.Ud e executa testes -dotnet test -``` +# Estrutura do Projeto AkkornStudio + +## Organização da Solução + +``` +. +├── files.sln # Arquivo de solução principal +├── README.md # Documentação do projeto +├── STRUCTURE.md # Este arquivo +│ +├── src/ # Código-fonte principal +│ ├── Projeto Core (AkkornStudio.c─ Core/ # Orquestradores principais +│ │ │ ├── BaseDbOrchestrator.cs +│ │ │ └── IDbOrchestrator.cs +│ │ │ +│ │ ├── Metadata/ # Serviços de metadados +│ │ │ ├── MetadataService.cs +│ │ │ ├── DbMetadata.cs +│ │ │ ├── IDatabaseInspector.cs +│ │ │ ├── AutoJoinDetector.cs +│ │ │ └── Inspectors/ # Adaptadores para cada banco +│ │ │ ├── MySqlInspector.cs +│ │ │ ├── PostgresInspector.cs +│ │ │ └── SqlServerInspector.cs +│ │ │ +│ │ ├── Nodes/ # Modelo de nós do grafo +│ │ │ ├── NodeDefinition.cs +│ │ │ ├── NodeGraph.cs +│ │ │ ├── NodeGraphCompiler.cs +│ │ │ └── ISqlExpression.cs +│ │ │ +│ │ ├── Providers/ # Orquestradores específicos por BD +│ │ │ ├── MySqlOrchestrator.cs +│ │ │ ├── PostgresOrchestrator.cs +│ │ │ └── SqlServerOrchestrator.cs +│ │ │ +│ │ ├── QueryEngine/ # Serviços de construção de queries +│ │ │ ├── QueryBuilderService.cs +│ │ │ └── QueryGeneratorService.cs +│ │ │ +│ │ ├── Registry/ # Registro de funções SQL +│ │ │ └── SqlFunctionRegistry.cs +│ │ │ +│ │ ├── ServiceRegistration.cs # Configuração de injeção dependência +│ │ └── │ +│ └── Projeto UI (AkkornStudio.U ├── App.axaml # Arquivo XAML principal +│ ├── App.axaml.cs # Code-behind da aplicação +│ ├── MainWindow.axaml # Janela principal +│ ├── MainWindow.axaml.cs +│ │ +│ ├── Assets/ # Recursos +│ │ └── Themes/ # Temas de cores +│ │ ├── AppStyles.axaml +│ │ └── DesignTokens.axaml +│ │ +│ ├── Controls/ # Controles Avalonia customizados +│ │ ├── NodeControl.axaml +│ │ ├── NodeControl.axaml.cs +│ │ ├── InfiniteCanvas.cs +│ │ ├── PropertyPanelControl.axaml +│ │ ├── PropertyPanelControl.axaml.cs +│ │ ├── LiveSqlBar.axaml +│ │ ├── LiveSqlBar.axaml.cs +│ │ ├── AutoJoinOverlay.axaml +│ │ ├── AutoJoinOverlay.axaml.cs +│ │ ├── SearchMenuControl.axaml +│ │ ├── SearchMenuControl.axaml.cs +│ │ ├── BezierWireLayer.cs +│ │ └── PinDragInteraction.cs +│ │ +│ ├── ViewModels/ # ViewModels MVVM +│ │ ├── CanvasViewModel.cs +│ │ ├── LiveSqlBarViewModel.cs +│ │ ├── PropertyPanelViewModel.cs +│ │ ├── AutoJoinOverlayViewModel.cs +│ │ └── UndoRedoStack.cs +│ │ +│ ├── Serialization/ # Serviços de serialização +│ │ └── CanvasSerializer.cs +│ │ +│ ├── DataPreviewPanel.axaml +│ └── +│ +└── tests/ # Projetos de testes + └── Projeto de Testes (AkkornStudio.T ├── ArchitectureTests.cs + ├── AtomicNodeTests.cs + ├── MetadataTests.cs + └── roj +``` + +## Namespaces + +### Core (- `nterfaces e classes base +- ` - Serviços de metadados +- `Inspectors` - Inspetores específicos por BD +- `Modelo de nós +- `` - Implementações de orquestradores +- `ne` - Serviços de queries +- ` - Registro de funções + +### UI () +- `igo principal da aplicação +- `ls` - Controles customizados +- `dels` - ViewModels +- `ization` - Serviços de serialização + +### Testes (roj) +- `Testes unitários + +## Dependências do Projeto + +### AkkornStudio (Core) +- ✅ Independente de UI +- Depende de: SqlKata, database drivers + +### AkkornStudio.UI +- Depende de: Core + Avalonia + +### AkkornStudio.Tests +- Depende de: Core + xUnit + +## Compilação + +```bash +# Build completo +dotnet build + +# Build apenas do Core +dotnet build src/cpenas da UI +dotnet build src/er.Ud e executa testes +dotnet test +``` diff --git a/docs/CODE_CONVENTIONS.md b/docs/CODE_CONVENTIONS.md index 1b3b8702..d5a0beb9 100644 --- a/docs/CODE_CONVENTIONS.md +++ b/docs/CODE_CONVENTIONS.md @@ -1,39 +1,39 @@ -# Convenções de Código — 2026-04-01 - -## Objetivo - -Padronizar estilo mínimo em arquivos novos e arquivos tocados por refactor, reduzindo inconsistência e warnings evitáveis. - -## Regras - -1. Strings vazias/brancas -- Preferir `string.IsNullOrWhiteSpace` para entradas de usuário, texto de UI e SQL livre. -- Usar `string.IsNullOrEmpty` apenas quando espaço em branco for valor válido. - -2. Inicializadores de coleção -- Em código moderno (C# 12), preferir `[]` quando o tipo já estiver explícito no lado esquerdo. -- Em APIs públicas com legibilidade sensível, manter inicializador clássico se melhorar clareza. - -3. Nullable reference types -- Tratar warnings de nullability como dívida técnica ativa em arquivos tocados. -- Evitar `!` sem comentário/justificativa. -- Em parâmetros opcionais, preferir anotação explícita (`string?`, `ILogger?`). - -4. Logging -- Evitar `Debug.WriteLine` em fluxos de produção. -- Preferir `ILogger` com mensagens estruturadas (`{Token}`). -- Nível recomendado: - - `LogDebug`: detalhe técnico de execução - - `LogInformation`: milestones funcionais - - `LogWarning`: comportamento degradado sem falha total - - `LogError`: falha com exceção - -5. XML docs em API pública -- Toda classe/interface/enum pública nova deve ter ``. -- Para membros públicos principais, adicionar `` e `` quando útil. - -## Aplicação incremental - -- Não reformatar o projeto inteiro de uma vez. -- Aplicar essas regras em cada PR nos arquivos modificados. -- Revisores devem recusar novos `Debug.WriteLine` em caminhos críticos. +# Convenções de Código — 2026-04-01 + +## Objetivo + +Padronizar estilo mínimo em arquivos novos e arquivos tocados por refactor, reduzindo inconsistência e warnings evitáveis. + +## Regras + +1. Strings vazias/brancas +- Preferir `string.IsNullOrWhiteSpace` para entradas de usuário, texto de UI e SQL livre. +- Usar `string.IsNullOrEmpty` apenas quando espaço em branco for valor válido. + +2. Inicializadores de coleção +- Em código moderno (C# 12), preferir `[]` quando o tipo já estiver explícito no lado esquerdo. +- Em APIs públicas com legibilidade sensível, manter inicializador clássico se melhorar clareza. + +3. Nullable reference types +- Tratar warnings de nullability como dívida técnica ativa em arquivos tocados. +- Evitar `!` sem comentário/justificativa. +- Em parâmetros opcionais, preferir anotação explícita (`string?`, `ILogger?`). + +4. Logging +- Evitar `Debug.WriteLine` em fluxos de produção. +- Preferir `ILogger` com mensagens estruturadas (`{Token}`). +- Nível recomendado: + - `LogDebug`: detalhe técnico de execução + - `LogInformation`: milestones funcionais + - `LogWarning`: comportamento degradado sem falha total + - `LogError`: falha com exceção + +5. XML docs em API pública +- Toda classe/interface/enum pública nova deve ter ``. +- Para membros públicos principais, adicionar `` e `` quando útil. + +## Aplicação incremental + +- Não reformatar o projeto inteiro de uma vez. +- Aplicar essas regras em cada PR nos arquivos modificados. +- Revisores devem recusar novos `Debug.WriteLine` em caminhos críticos. diff --git a/docs/EXCEPTION_HANDLING_STRATEGY.md b/docs/EXCEPTION_HANDLING_STRATEGY.md index 28822aa2..4df6c887 100644 --- a/docs/EXCEPTION_HANDLING_STRATEGY.md +++ b/docs/EXCEPTION_HANDLING_STRATEGY.md @@ -1,47 +1,47 @@ -# Estratégia de Tratamento de Exceções — 2026-04-01 -Escopo: domínio, infraestrutura e UI - -## Objetivo - -Padronizar como falhas são propagadas, logadas e exibidas ao usuário para evitar combinações contraditórias entre módulos. - -## Contrato por camada - -1. Domínio/Core (`ações de validação e configuração inválida devem lançar exceção. -- Operações de execução tolerante (ex.: preview/teste de conexão) podem retornar `Result` com `Success=false` quando a falha for operacional esperada. -- Nunca engolir exceções sem transformar em sinal explícito (`Result` ou evento de warning). - -2. Infraestrutura/Serviços (`es`) -- Serviços de execução devem usar `ILogger` para logs estruturados. -- Exceções inesperadas devem ser logadas em `LogError` e repropagadas quando o chamador tiver contexto para decisão de UX. -- Em persistência local de melhor-esforço, emitir warning observável (evento/callback) em vez de falha silenciosa. - -3. UI/ViewModels -- A UI é o limite de apresentação: capturar exceções e converter para mensagem amigável (`DataPreview.ShowError` ou status equivalente). -- Não expor stack trace ao usuário final. - -## Regras práticas - -- `ArgumentException` / `InvalidOperationException`: para uso incorreto de API e pré-condições. -- `OperationCanceledException`: não tratar como erro; registrar em nível `Information` e encerrar fluxo. -- `Exception` genérica: somente no boundary de serviço/UI, sempre com log estruturado. - -## Logging - -- `LogDebug`: detalhe técnico de execução. -- `LogInformation`: marco funcional e cancelamento esperado. -- `LogWarning`: degradação controlada, fallback, dados corrompidos recuperáveis. -- `LogError`: falha inesperada com exceção. - -## Padrões de retorno recomendados - -- Core read-model e probes: `Result` (`Success`, `ErrorMessage`, métricas). -- Serviços de execução síncronos/assíncronos críticos: lançar após log para o boundary de UI. -- Persistência local best-effort: retornar fallback + `WarningRaised`. - -## Checklist para PR - -- Existe um boundary claro de captura de exceção? -- Há log estruturado no nível correto? -- Há consistência com o contrato da camada? -- Existe teste cobrindo o caminho de falha? +# Estratégia de Tratamento de Exceções — 2026-04-01 +Escopo: domínio, infraestrutura e UI + +## Objetivo + +Padronizar como falhas são propagadas, logadas e exibidas ao usuário para evitar combinações contraditórias entre módulos. + +## Contrato por camada + +1. Domínio/Core (`ações de validação e configuração inválida devem lançar exceção. +- Operações de execução tolerante (ex.: preview/teste de conexão) podem retornar `Result` com `Success=false` quando a falha for operacional esperada. +- Nunca engolir exceções sem transformar em sinal explícito (`Result` ou evento de warning). + +2. Infraestrutura/Serviços (`es`) +- Serviços de execução devem usar `ILogger` para logs estruturados. +- Exceções inesperadas devem ser logadas em `LogError` e repropagadas quando o chamador tiver contexto para decisão de UX. +- Em persistência local de melhor-esforço, emitir warning observável (evento/callback) em vez de falha silenciosa. + +3. UI/ViewModels +- A UI é o limite de apresentação: capturar exceções e converter para mensagem amigável (`DataPreview.ShowError` ou status equivalente). +- Não expor stack trace ao usuário final. + +## Regras práticas + +- `ArgumentException` / `InvalidOperationException`: para uso incorreto de API e pré-condições. +- `OperationCanceledException`: não tratar como erro; registrar em nível `Information` e encerrar fluxo. +- `Exception` genérica: somente no boundary de serviço/UI, sempre com log estruturado. + +## Logging + +- `LogDebug`: detalhe técnico de execução. +- `LogInformation`: marco funcional e cancelamento esperado. +- `LogWarning`: degradação controlada, fallback, dados corrompidos recuperáveis. +- `LogError`: falha inesperada com exceção. + +## Padrões de retorno recomendados + +- Core read-model e probes: `Result` (`Success`, `ErrorMessage`, métricas). +- Serviços de execução síncronos/assíncronos críticos: lançar após log para o boundary de UI. +- Persistência local best-effort: retornar fallback + `WarningRaised`. + +## Checklist para PR + +- Existe um boundary claro de captura de exceção? +- Há log estruturado no nível correto? +- Há consistência com o contrato da camada? +- Existe teste cobrindo o caminho de falha? diff --git a/docs/INDEX.md b/docs/INDEX.md index 223471ea..efda4aab 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,93 +1,93 @@ -# 📚 Documentação — DbWeaver - -Este é o índice principal da documentação ativa do projeto DbWeaver. - -## 🎯 Organização Atual - -A documentação ativa está dividida em dois níveis: - -1. `/docs` concentra referências base e documentação transversal; -2. `/docs/next` concentra especificações ativas de arquitetura, canvas e evolução estrutural. - ---- - -## 📘 Documentação Base em `/docs` - -### 🛠️ Engenharia - -- **[CODE_CONVENTIONS.md](CODE_CONVENTIONS.md)** — Convenções de código, naming e estrutura geral -- **[EXCEPTION_HANDLING_STRATEGY.md](EXCEPTION_HANDLING_STRATEGY.md)** — Estratégia de tratamento de erros e falhas -- **[SEARCH_FILTERING_GUIDELINE.md](SEARCH_FILTERING_GUIDELINE.md)** — Padrão de busca textual centralizada (fuzzy vs estrito) para UI -- **[LIST_RENDERING_PERFORMANCE_GUIDELINE.md](LIST_RENDERING_PERFORMANCE_GUIDELINE.md)** — Regras normativas para listas/listagens performáticas (virtualização, cache, debounce e invalidação) -- **[MODAL_LAYOUT_GUIDELINE.md](MODAL_LAYOUT_GUIDELINE.md)** — Guideline normativa para estrutura de modais (header + body rolável + footer fixo) -- **[MODAL_ADOPTION_MAP.md](MODAL_ADOPTION_MAP.md)** — Inventário de modais e status de adoção da guideline -- **[SQL_EDITOR_UX_IMPROVEMENT_IMPLEMENTATION_SPRINT_PLAN.md](SQL_EDITOR_UX_IMPROVEMENT_IMPLEMENTATION_SPRINT_PLAN.md)** — Plano de execução e checklist concluído do review de UX do SQL Editor -- **[SQL_TO_NODE_INTERMEDIATE_LAYER_SPEC.md](SQL_TO_NODE_INTERMEDIATE_LAYER_SPEC.md)** — Especificação básica da camada intermediária SQL → IR → Nodes - -### 🧱 DDL Schema Structure Analysis - -- **[spec_ddl_schema_structure/SPEC_DDL_SCHEMA_STRUCTURE_ANALYSIS_INFERABLE_FINAL.md](spec_ddl_schema_structure/SPEC_DDL_SCHEMA_STRUCTURE_ANALYSIS_INFERABLE_FINAL.md)** — Especificação normativa executável do modo inferível -- **[spec_ddl_schema_structure/DDL_SCHEMA_STRUCTURE_ANALYSIS_IMPLEMENTATION_BREAKDOWN.md](spec_ddl_schema_structure/DDL_SCHEMA_STRUCTURE_ANALYSIS_IMPLEMENTATION_BREAKDOWN.md)** — Breakdown de implementação por fases e tarefas -- **[spec_ddl_schema_structure/DDL_SCHEMA_STRUCTURE_ANALYSIS_ACCEPTANCE_CHECKLIST.md](spec_ddl_schema_structure/DDL_SCHEMA_STRUCTURE_ANALYSIS_ACCEPTANCE_CHECKLIST.md)** — Checklist final de aceite com evidências -- **[spec_ddl_schema_structure/2026-04-13-ddl-schema-analysis-release-note.md](spec_ddl_schema_structure/2026-04-13-ddl-schema-analysis-release-note.md)** — Nota de release da entrega consolidada - -### 🎨 Canvas, Pins e Tema - -- **[PIN_TYPES_REFERENCE.md](PIN_TYPES_REFERENCE.md)** — Referência visual dos tipos de pino, formas, cores e semântica estrutural -- **[THEME_JSON_SCHEMA.md](THEME_JSON_SCHEMA.md)** — Schema JSON para customização de temas -- **[THEME_VISUAL_VALIDATION_CHECKLIST.md](THEME_VISUAL_VALIDATION_CHECKLIST.md)** — Checklist de validação visual do tema - ---- - -## 🧭 Especificações Ativas em `/docs/next` - -### 🏗️ Arquitetura e Workspace - -- **[DOCUMENT_ORIENTED_WORKSPACE_ARCHITECTURE_SPEC.md](next/DOCUMENT_ORIENTED_WORKSPACE_ARCHITECTURE_SPEC.md)** — Especificação formal da arquitetura orientada a documento para Query, DDL e SQL Editor - -### 🧩 Nodes, Pins e Canvas - -- **[NODES_GENERAL_SURVEY.md](next/NODES_GENERAL_SURVEY.md)** — Especificação normativa do modelo graph-first de nodes, contratos semânticos, UX operacional e overhaul das famílias problemáticas -- **[PIN_OBJECT_MODEL_MIGRATION_SPEC.md](next/PIN_OBJECT_MODEL_MIGRATION_SPEC.md)** — Especificação formal da migração do domínio de pins, compatibilidade, contracts e testes -- **[WIRE_SYSTEM_OVERHAUL_SPEC.md](next/WIRE_SYSTEM_OVERHAUL_SPEC.md)** — Overhaul do sistema de wires, seleção, tooltip, roteamento e affordances do canvas - -### ⌨️ Comandos e Atalhos - -- **[SHORTCUT_REGISTRY_CUSTOMIZATION_SPEC.md](next/SHORTCUT_REGISTRY_CUSTOMIZATION_SPEC.md)** — Especificação do registry central de atalhos, customização e contexts de comando -- **[SHORTCUT_REGISTRY_IMPLEMENTATION_BACKLOG.md](next/SHORTCUT_REGISTRY_IMPLEMENTATION_BACKLOG.md)** — Backlog executável da implementação do sistema de atalhos - -### 🌐 Expansão de Paradigma - -- **[NOSQL_EXPANSION_ROADMAP.md](next/NOSQL_EXPANSION_ROADMAP.md)** — Roadmap de expansão para múltiplos paradigmas e pipeline documental - -### 📑 Índice Prioritário das Specs - -- **[next/index.md](next/index.md)** — Priorização, dependências e ordem recomendada de execução das specs em `/docs/next` - ---- - -## 📦 Histórico e Material de Apoio - -- **[../archive/](../archive/)** — Planejamentos, análises, roadmaps e documentação histórica do projeto -- **[superpowers/](superpowers/)** — Material exploratório, planos e estudos complementares - ---- - -## 🚀 Quick Links - -- **Começar:** Veja [../README.md](../README.md) -- **Testes:** Veja [../tests/TESTS.md](../tests/TESTS.md) - ---- - -## 📝 Regras para Atualização do Índice - -1. Documentação transversal e referências estáveis devem permanecer em `/docs`. -2. Especificações ativas de arquitetura, canvas e evolução estrutural devem ficar em `/docs/next`. -3. Materiais históricos, análises concluídas e backlog antigo devem permanecer em `/archive`. -4. Este `INDEX.md` deve refletir apenas arquivos que existam de fato no repositório. -5. Sempre que uma nova spec ativa for adicionada em `/docs/next`, ela deve aparecer também aqui. - ---- - -*Última atualização: 15 de abril de 2026* +# 📚 Documentação — AkkornStudio + +Este é o índice principal da documentação ativa do projeto AkkornStudio. + +## 🎯 Organização Atual + +A documentação ativa está dividida em dois níveis: + +1. `/docs` concentra referências base e documentação transversal; +2. `/docs/next` concentra especificações ativas de arquitetura, canvas e evolução estrutural. + +--- + +## 📘 Documentação Base em `/docs` + +### 🛠️ Engenharia + +- **[CODE_CONVENTIONS.md](CODE_CONVENTIONS.md)** — Convenções de código, naming e estrutura geral +- **[EXCEPTION_HANDLING_STRATEGY.md](EXCEPTION_HANDLING_STRATEGY.md)** — Estratégia de tratamento de erros e falhas +- **[SEARCH_FILTERING_GUIDELINE.md](SEARCH_FILTERING_GUIDELINE.md)** — Padrão de busca textual centralizada (fuzzy vs estrito) para UI +- **[LIST_RENDERING_PERFORMANCE_GUIDELINE.md](LIST_RENDERING_PERFORMANCE_GUIDELINE.md)** — Regras normativas para listas/listagens performáticas (virtualização, cache, debounce e invalidação) +- **[MODAL_LAYOUT_GUIDELINE.md](MODAL_LAYOUT_GUIDELINE.md)** — Guideline normativa para estrutura de modais (header + body rolável + footer fixo) +- **[MODAL_ADOPTION_MAP.md](MODAL_ADOPTION_MAP.md)** — Inventário de modais e status de adoção da guideline +- **[SQL_EDITOR_UX_IMPROVEMENT_IMPLEMENTATION_SPRINT_PLAN.md](SQL_EDITOR_UX_IMPROVEMENT_IMPLEMENTATION_SPRINT_PLAN.md)** — Plano de execução e checklist concluído do review de UX do SQL Editor +- **[SQL_TO_NODE_INTERMEDIATE_LAYER_SPEC.md](SQL_TO_NODE_INTERMEDIATE_LAYER_SPEC.md)** — Especificação básica da camada intermediária SQL → IR → Nodes + +### 🧱 DDL Schema Structure Analysis + +- **[spec_ddl_schema_structure/SPEC_DDL_SCHEMA_STRUCTURE_ANALYSIS_INFERABLE_FINAL.md](spec_ddl_schema_structure/SPEC_DDL_SCHEMA_STRUCTURE_ANALYSIS_INFERABLE_FINAL.md)** — Especificação normativa executável do modo inferível +- **[spec_ddl_schema_structure/DDL_SCHEMA_STRUCTURE_ANALYSIS_IMPLEMENTATION_BREAKDOWN.md](spec_ddl_schema_structure/DDL_SCHEMA_STRUCTURE_ANALYSIS_IMPLEMENTATION_BREAKDOWN.md)** — Breakdown de implementação por fases e tarefas +- **[spec_ddl_schema_structure/DDL_SCHEMA_STRUCTURE_ANALYSIS_ACCEPTANCE_CHECKLIST.md](spec_ddl_schema_structure/DDL_SCHEMA_STRUCTURE_ANALYSIS_ACCEPTANCE_CHECKLIST.md)** — Checklist final de aceite com evidências +- **[spec_ddl_schema_structure/2026-04-13-ddl-schema-analysis-release-note.md](spec_ddl_schema_structure/2026-04-13-ddl-schema-analysis-release-note.md)** — Nota de release da entrega consolidada + +### 🎨 Canvas, Pins e Tema + +- **[PIN_TYPES_REFERENCE.md](PIN_TYPES_REFERENCE.md)** — Referência visual dos tipos de pino, formas, cores e semântica estrutural +- **[THEME_JSON_SCHEMA.md](THEME_JSON_SCHEMA.md)** — Schema JSON para customização de temas +- **[THEME_VISUAL_VALIDATION_CHECKLIST.md](THEME_VISUAL_VALIDATION_CHECKLIST.md)** — Checklist de validação visual do tema + +--- + +## 🧭 Especificações Ativas em `/docs/next` + +### 🏗️ Arquitetura e Workspace + +- **[DOCUMENT_ORIENTED_WORKSPACE_ARCHITECTURE_SPEC.md](next/DOCUMENT_ORIENTED_WORKSPACE_ARCHITECTURE_SPEC.md)** — Especificação formal da arquitetura orientada a documento para Query, DDL e SQL Editor + +### 🧩 Nodes, Pins e Canvas + +- **[NODES_GENERAL_SURVEY.md](next/NODES_GENERAL_SURVEY.md)** — Especificação normativa do modelo graph-first de nodes, contratos semânticos, UX operacional e overhaul das famílias problemáticas +- **[PIN_OBJECT_MODEL_MIGRATION_SPEC.md](next/PIN_OBJECT_MODEL_MIGRATION_SPEC.md)** — Especificação formal da migração do domínio de pins, compatibilidade, contracts e testes +- **[WIRE_SYSTEM_OVERHAUL_SPEC.md](next/WIRE_SYSTEM_OVERHAUL_SPEC.md)** — Overhaul do sistema de wires, seleção, tooltip, roteamento e affordances do canvas + +### ⌨️ Comandos e Atalhos + +- **[SHORTCUT_REGISTRY_CUSTOMIZATION_SPEC.md](next/SHORTCUT_REGISTRY_CUSTOMIZATION_SPEC.md)** — Especificação do registry central de atalhos, customização e contexts de comando +- **[SHORTCUT_REGISTRY_IMPLEMENTATION_BACKLOG.md](next/SHORTCUT_REGISTRY_IMPLEMENTATION_BACKLOG.md)** — Backlog executável da implementação do sistema de atalhos + +### 🌐 Expansão de Paradigma + +- **[NOSQL_EXPANSION_ROADMAP.md](next/NOSQL_EXPANSION_ROADMAP.md)** — Roadmap de expansão para múltiplos paradigmas e pipeline documental + +### 📑 Índice Prioritário das Specs + +- **[next/index.md](next/index.md)** — Priorização, dependências e ordem recomendada de execução das specs em `/docs/next` + +--- + +## 📦 Histórico e Material de Apoio + +- **[../archive/](../archive/)** — Planejamentos, análises, roadmaps e documentação histórica do projeto +- **[superpowers/](superpowers/)** — Material exploratório, planos e estudos complementares + +--- + +## 🚀 Quick Links + +- **Começar:** Veja [../README.md](../README.md) +- **Testes:** Veja [../tests/TESTS.md](../tests/TESTS.md) + +--- + +## 📝 Regras para Atualização do Índice + +1. Documentação transversal e referências estáveis devem permanecer em `/docs`. +2. Especificações ativas de arquitetura, canvas e evolução estrutural devem ficar em `/docs/next`. +3. Materiais históricos, análises concluídas e backlog antigo devem permanecer em `/archive`. +4. Este `INDEX.md` deve refletir apenas arquivos que existam de fato no repositório. +5. Sempre que uma nova spec ativa for adicionada em `/docs/next`, ela deve aparecer também aqui. + +--- + +*Última atualização: 15 de abril de 2026* diff --git a/docs/THEME_JSON_SCHEMA.md b/docs/THEME_JSON_SCHEMA.md index c81ef6eb..394dbc41 100644 --- a/docs/THEME_JSON_SCHEMA.md +++ b/docs/THEME_JSON_SCHEMA.md @@ -1,87 +1,87 @@ -# Theme JSON Schema (Fase 5) - -Arquivo padrao: `src/DBWeaver.UI/Assets/Themes/user-theme.json` - -Objetivo: -- Permitir customizacao de tokens macro sem editar AXAML. -- Manter fallback seguro para valores invalidos. - -## Estrutura - -```json -{ - "meta": { - "name": "Studio Dark", - "version": "1.0" - }, - "colors": { - "bg0": "#090B14", - "bg1": "#0F1220", - "bg2": "#151A2C", - "bg3": "#1C2338", - "bg4": "#252F49", - "textPrimary": "#E7ECFF", - "textSecondary": "#AEB9D9", - "textMuted": "#7F8AAE", - "textDisabled": "#66708F", - "textInverse": "#0B0F1D", - "textAccent": "#8FA7FF", - "btnPrimaryBg": "#2563EB", - "btnPrimaryFg": "#F8FAFC", - "btnWarningBg": "#7C2D12", - "btnWarningFg": "#FED7AA" - }, - "typography": { - "uiFont": "Manrope,Sora,Segoe UI,Arial,sans-serif", - "nodeFont": "Space Grotesk,Manrope,Segoe UI,Arial,sans-serif", - "monoFont": "JetBrainsMono Nerd Font,JetBrains Mono,Cascadia Code,Consolas,monospace", - "displaySize": 24, - "headingSize": 18, - "titleSize": 15, - "nodeTitleSize": 14, - "labelSize": 13, - "bodySize": 12, - "captionSize": 11, - "monoBodySize": 12, - "monoSmallSize": 11 - } -} -``` - -## Mapeamento de chaves - -- `colors.bg0` -> `Bg0`, `Bg0Brush` -- `colors.bg1` -> `Bg1`, `Bg1Brush` -- `colors.bg2` -> `Bg2`, `Bg2Brush` -- `colors.bg3` -> `Bg3`, `Bg3Brush` -- `colors.bg4` -> `Bg4`, `Bg4Brush` -- `colors.textPrimary` -> `TextPrimary`, `TextPrimaryBrush` -- `colors.textSecondary` -> `TextSecondary`, `TextSecondaryBrush` -- `colors.textMuted` -> `TextMuted`, `TextMutedBrush` -- `colors.textDisabled` -> `TextDisabled`, `TextDisabledBrush` -- `colors.textInverse` -> `TextInverse`, `TextInverseBrush` -- `colors.textAccent` -> `TextAccent`, `TextAccentBrush` -- `colors.btnPrimaryBg` -> `BtnPrimaryBg`, `BtnPrimaryBgBrush` -- `colors.btnPrimaryFg` -> `BtnPrimaryFg`, `BtnPrimaryFgBrush` -- `colors.btnWarningBg` -> `BtnWarningBg`, `BtnWarningBgBrush` -- `colors.btnWarningFg` -> `BtnWarningFg`, `BtnWarningFgBrush` -- `typography.uiFont` -> `UIFont` -- `typography.nodeFont` -> `NodeFont` -- `typography.monoFont` -> `MonoFont` -- `typography.displaySize` -> `FontSizeDisplay` -- `typography.headingSize` -> `FontSizeHeading` -- `typography.titleSize` -> `FontSizeTitle` -- `typography.nodeTitleSize` -> `FontSizeNodeTitle` -- `typography.labelSize` -> `FontSizeLabel` -- `typography.bodySize` -> `FontSizeBody` -- `typography.captionSize` -> `FontSizeCaption` -- `typography.monoBodySize` -> `FontSizeMonoBody` -- `typography.monoSmallSize` -> `FontSizeMonoSmall` - -## Regras de seguranca - -- Arquivo ausente: aplica tema padrao (sem erro). -- JSON invalido: fallback para tema padrao. -- Chave invalida (cor/tamanho): ignorada individualmente com warning. -- Tokens sem valor valido permanecem no padrao. -- Escopo atual: tokens de shell, tipografia global e hierarchy de texto. +# Theme JSON Schema (Fase 5) + +Arquivo padrao: `src/AkkornStudio.UI/Assets/Themes/user-theme.json` + +Objetivo: +- Permitir customizacao de tokens macro sem editar AXAML. +- Manter fallback seguro para valores invalidos. + +## Estrutura + +```json +{ + "meta": { + "name": "Studio Dark", + "version": "1.0" + }, + "colors": { + "bg0": "#090B14", + "bg1": "#0F1220", + "bg2": "#151A2C", + "bg3": "#1C2338", + "bg4": "#252F49", + "textPrimary": "#E7ECFF", + "textSecondary": "#AEB9D9", + "textMuted": "#7F8AAE", + "textDisabled": "#66708F", + "textInverse": "#0B0F1D", + "textAccent": "#8FA7FF", + "btnPrimaryBg": "#2563EB", + "btnPrimaryFg": "#F8FAFC", + "btnWarningBg": "#7C2D12", + "btnWarningFg": "#FED7AA" + }, + "typography": { + "uiFont": "Manrope,Sora,Segoe UI,Arial,sans-serif", + "nodeFont": "Space Grotesk,Manrope,Segoe UI,Arial,sans-serif", + "monoFont": "JetBrainsMono Nerd Font,JetBrains Mono,Cascadia Code,Consolas,monospace", + "displaySize": 24, + "headingSize": 18, + "titleSize": 15, + "nodeTitleSize": 14, + "labelSize": 13, + "bodySize": 12, + "captionSize": 11, + "monoBodySize": 12, + "monoSmallSize": 11 + } +} +``` + +## Mapeamento de chaves + +- `colors.bg0` -> `Bg0`, `Bg0Brush` +- `colors.bg1` -> `Bg1`, `Bg1Brush` +- `colors.bg2` -> `Bg2`, `Bg2Brush` +- `colors.bg3` -> `Bg3`, `Bg3Brush` +- `colors.bg4` -> `Bg4`, `Bg4Brush` +- `colors.textPrimary` -> `TextPrimary`, `TextPrimaryBrush` +- `colors.textSecondary` -> `TextSecondary`, `TextSecondaryBrush` +- `colors.textMuted` -> `TextMuted`, `TextMutedBrush` +- `colors.textDisabled` -> `TextDisabled`, `TextDisabledBrush` +- `colors.textInverse` -> `TextInverse`, `TextInverseBrush` +- `colors.textAccent` -> `TextAccent`, `TextAccentBrush` +- `colors.btnPrimaryBg` -> `BtnPrimaryBg`, `BtnPrimaryBgBrush` +- `colors.btnPrimaryFg` -> `BtnPrimaryFg`, `BtnPrimaryFgBrush` +- `colors.btnWarningBg` -> `BtnWarningBg`, `BtnWarningBgBrush` +- `colors.btnWarningFg` -> `BtnWarningFg`, `BtnWarningFgBrush` +- `typography.uiFont` -> `UIFont` +- `typography.nodeFont` -> `NodeFont` +- `typography.monoFont` -> `MonoFont` +- `typography.displaySize` -> `FontSizeDisplay` +- `typography.headingSize` -> `FontSizeHeading` +- `typography.titleSize` -> `FontSizeTitle` +- `typography.nodeTitleSize` -> `FontSizeNodeTitle` +- `typography.labelSize` -> `FontSizeLabel` +- `typography.bodySize` -> `FontSizeBody` +- `typography.captionSize` -> `FontSizeCaption` +- `typography.monoBodySize` -> `FontSizeMonoBody` +- `typography.monoSmallSize` -> `FontSizeMonoSmall` + +## Regras de seguranca + +- Arquivo ausente: aplica tema padrao (sem erro). +- JSON invalido: fallback para tema padrao. +- Chave invalida (cor/tamanho): ignorada individualmente com warning. +- Tokens sem valor valido permanecem no padrao. +- Escopo atual: tokens de shell, tipografia global e hierarchy de texto. diff --git a/files.sln b/files.sln index 89d9e0f0..bcf302cc 100644 --- a/files.sln +++ b/files.sln @@ -1,41 +1,41 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.2.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBWeaver", "src/DBWeaver/DBWeaver.csproj", "{EC6D5DCC-55C8-E541-BE7C-72475C926523}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBWeaver.UI", "src/DBWeaver.UI/DBWeaver.UI.csproj", "{91EC5804-A179-27A6-1C39-B4C629B2AD06}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBWeaver.Canvas", "src/DBWeaver.Canvas/DBWeaver.Canvas.csproj", "{FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{890483B4-9539-40F5-93DA-57728571BDC0}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EC6D5DCC-55C8-E541-BE7C-72475C926523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC6D5DCC-55C8-E541-BE7C-72475C926523}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC6D5DCC-55C8-E541-BE7C-72475C926523}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC6D5DCC-55C8-E541-BE7C-72475C926523}.Release|Any CPU.Build.0 = Release|Any CPU - {91EC5804-A179-27A6-1C39-B4C629B2AD06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {91EC5804-A179-27A6-1C39-B4C629B2AD06}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91EC5804-A179-27A6-1C39-B4C629B2AD06}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91EC5804-A179-27A6-1C39-B4C629B2AD06}.Release|Any CPU.Build.0 = Release|Any CPU - {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C1E38B04-3BCD-4392-A686-DD464894A34C} - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA} = {890483B4-9539-40F5-93DA-57728571BDC0} - EndGlobalSection -EndGlobal +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkkornStudio", "src/AkkornStudio/AkkornStudio.csproj", "{EC6D5DCC-55C8-E541-BE7C-72475C926523}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkkornStudio.UI", "src/AkkornStudio.UI/AkkornStudio.UI.csproj", "{91EC5804-A179-27A6-1C39-B4C629B2AD06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkkornStudio.Canvas", "src/AkkornStudio.Canvas/AkkornStudio.Canvas.csproj", "{FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{890483B4-9539-40F5-93DA-57728571BDC0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EC6D5DCC-55C8-E541-BE7C-72475C926523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC6D5DCC-55C8-E541-BE7C-72475C926523}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC6D5DCC-55C8-E541-BE7C-72475C926523}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC6D5DCC-55C8-E541-BE7C-72475C926523}.Release|Any CPU.Build.0 = Release|Any CPU + {91EC5804-A179-27A6-1C39-B4C629B2AD06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91EC5804-A179-27A6-1C39-B4C629B2AD06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91EC5804-A179-27A6-1C39-B4C629B2AD06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91EC5804-A179-27A6-1C39-B4C629B2AD06}.Release|Any CPU.Build.0 = Release|Any CPU + {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C1E38B04-3BCD-4392-A686-DD464894A34C} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FD5E9E45-6012-4E74-A39B-3AC1DC9564EA} = {890483B4-9539-40F5-93DA-57728571BDC0} + EndGlobalSection +EndGlobal diff --git a/src/DBWeaver.Canvas/.gitignore b/src/AkkornStudio.Canvas/.gitignore similarity index 100% rename from src/DBWeaver.Canvas/.gitignore rename to src/AkkornStudio.Canvas/.gitignore diff --git a/src/AkkornStudio.Canvas/AkkornStudio.Canvas.csproj b/src/AkkornStudio.Canvas/AkkornStudio.Canvas.csproj new file mode 100644 index 00000000..25776aaa --- /dev/null +++ b/src/AkkornStudio.Canvas/AkkornStudio.Canvas.csproj @@ -0,0 +1,6 @@ + + + AkkornStudio.CanvasKit + AkkornStudio.Canvas + + diff --git a/src/DBWeaver.Canvas/CanvasAutoJoinSemantics.cs b/src/AkkornStudio.Canvas/CanvasAutoJoinSemantics.cs similarity index 95% rename from src/DBWeaver.Canvas/CanvasAutoJoinSemantics.cs rename to src/AkkornStudio.Canvas/CanvasAutoJoinSemantics.cs index 1b783df2..06fc7255 100644 --- a/src/DBWeaver.Canvas/CanvasAutoJoinSemantics.cs +++ b/src/AkkornStudio.Canvas/CanvasAutoJoinSemantics.cs @@ -1,100 +1,100 @@ -namespace DBWeaver.CanvasKit; - -public static class CanvasAutoJoinSemantics -{ - public static bool TrySplitJoinClauseOnEquality( - string? clause, - out string left, - out string right) - { - left = string.Empty; - right = string.Empty; - - if (string.IsNullOrWhiteSpace(clause)) - return false; - - string text = clause.Trim(); - int eq = text.IndexOf('='); - if (eq <= 0 || eq >= text.Length - 1) - return false; - - // Accept only a single plain equality operator (no >=, <=, !=, ==, chained expressions). - if (text.Contains(">=", StringComparison.Ordinal) - || text.Contains("<=", StringComparison.Ordinal) - || text.Contains("!=", StringComparison.Ordinal) - || text.Contains("==", StringComparison.Ordinal)) - { - return false; - } - - if (text.IndexOf('=', eq + 1) >= 0) - return false; - - left = text[..eq].Trim(); - right = text[(eq + 1)..].Trim(); - - return left.Length > 0 && right.Length > 0; - } - - public static string BuildSuggestionPairKey( - string existingTable, - string newTable, - string leftColumn, - string rightColumn) - { - string[] pair = [existingTable ?? string.Empty, newTable ?? string.Empty]; - Array.Sort(pair, StringComparer.OrdinalIgnoreCase); - return $"{pair[0]}|{pair[1]}|{leftColumn}|{rightColumn}"; - } - - public static bool TryParseQualifiedColumn( - string expression, - out string? source, - out string column) - { - source = null; - column = string.Empty; - - if (string.IsNullOrWhiteSpace(expression)) - return false; - - string normalized = expression.Trim(); - int lastDot = normalized.LastIndexOf('.'); - if (lastDot <= 0 || lastDot >= normalized.Length - 1) - return false; - - source = normalized[..lastDot].Trim(); - column = normalized[(lastDot + 1)..].Trim(); - - if (column.Length == 0) - return false; - - source = source.Trim('"').Trim('`').Trim('[', ']'); - column = column.Trim('"').Trim('`').Trim('[', ']'); - - return source.Length > 0 && column.Length > 0; - } - - public static bool MatchesSource( - string fullName, - string shortName, - string? alias, - string sourceRef) - { - if (string.IsNullOrWhiteSpace(sourceRef)) - return false; - - if (fullName.Equals(sourceRef, StringComparison.OrdinalIgnoreCase)) - return true; - - if (shortName.Equals(sourceRef, StringComparison.OrdinalIgnoreCase)) - return true; - - if (!string.IsNullOrWhiteSpace(alias) - && alias.Equals(sourceRef, StringComparison.OrdinalIgnoreCase)) - return true; - - string fullShort = fullName.Split('.').Last(); - return fullShort.Equals(sourceRef, StringComparison.OrdinalIgnoreCase); - } -} +namespace AkkornStudio.CanvasKit; + +public static class CanvasAutoJoinSemantics +{ + public static bool TrySplitJoinClauseOnEquality( + string? clause, + out string left, + out string right) + { + left = string.Empty; + right = string.Empty; + + if (string.IsNullOrWhiteSpace(clause)) + return false; + + string text = clause.Trim(); + int eq = text.IndexOf('='); + if (eq <= 0 || eq >= text.Length - 1) + return false; + + // Accept only a single plain equality operator (no >=, <=, !=, ==, chained expressions). + if (text.Contains(">=", StringComparison.Ordinal) + || text.Contains("<=", StringComparison.Ordinal) + || text.Contains("!=", StringComparison.Ordinal) + || text.Contains("==", StringComparison.Ordinal)) + { + return false; + } + + if (text.IndexOf('=', eq + 1) >= 0) + return false; + + left = text[..eq].Trim(); + right = text[(eq + 1)..].Trim(); + + return left.Length > 0 && right.Length > 0; + } + + public static string BuildSuggestionPairKey( + string existingTable, + string newTable, + string leftColumn, + string rightColumn) + { + string[] pair = [existingTable ?? string.Empty, newTable ?? string.Empty]; + Array.Sort(pair, StringComparer.OrdinalIgnoreCase); + return $"{pair[0]}|{pair[1]}|{leftColumn}|{rightColumn}"; + } + + public static bool TryParseQualifiedColumn( + string expression, + out string? source, + out string column) + { + source = null; + column = string.Empty; + + if (string.IsNullOrWhiteSpace(expression)) + return false; + + string normalized = expression.Trim(); + int lastDot = normalized.LastIndexOf('.'); + if (lastDot <= 0 || lastDot >= normalized.Length - 1) + return false; + + source = normalized[..lastDot].Trim(); + column = normalized[(lastDot + 1)..].Trim(); + + if (column.Length == 0) + return false; + + source = source.Trim('"').Trim('`').Trim('[', ']'); + column = column.Trim('"').Trim('`').Trim('[', ']'); + + return source.Length > 0 && column.Length > 0; + } + + public static bool MatchesSource( + string fullName, + string shortName, + string? alias, + string sourceRef) + { + if (string.IsNullOrWhiteSpace(sourceRef)) + return false; + + if (fullName.Equals(sourceRef, StringComparison.OrdinalIgnoreCase)) + return true; + + if (shortName.Equals(sourceRef, StringComparison.OrdinalIgnoreCase)) + return true; + + if (!string.IsNullOrWhiteSpace(alias) + && alias.Equals(sourceRef, StringComparison.OrdinalIgnoreCase)) + return true; + + string fullShort = fullName.Split('.').Last(); + return fullShort.Equals(sourceRef, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/DBWeaver.Canvas/CanvasLayerOrdering.cs b/src/AkkornStudio.Canvas/CanvasLayerOrdering.cs similarity index 95% rename from src/DBWeaver.Canvas/CanvasLayerOrdering.cs rename to src/AkkornStudio.Canvas/CanvasLayerOrdering.cs index c1a7cdd9..df9ad473 100644 --- a/src/DBWeaver.Canvas/CanvasLayerOrdering.cs +++ b/src/AkkornStudio.Canvas/CanvasLayerOrdering.cs @@ -1,74 +1,74 @@ -namespace DBWeaver.CanvasKit; - -public interface ICanvasLayerNode -{ - bool IsSelected { get; } - int ZOrder { get; } -} - -public static class CanvasLayerOrdering -{ - public static List OrderByZ(IEnumerable nodes) - where TNode : ICanvasLayerNode => - [.. nodes.OrderBy(n => n.ZOrder)]; - - public static List BringToFront(IEnumerable nodes) - where TNode : ICanvasLayerNode - { - List ordered = OrderByZ(nodes); - HashSet selected = ordered.Where(n => n.IsSelected).ToHashSet(); - var back = ordered.Where(n => !selected.Contains(n)).ToList(); - var front = ordered.Where(n => selected.Contains(n)).ToList(); - return [.. back, .. front]; - } - - public static List SendToBack(IEnumerable nodes) - where TNode : ICanvasLayerNode - { - List ordered = OrderByZ(nodes); - HashSet selected = ordered.Where(n => n.IsSelected).ToHashSet(); - var back = ordered.Where(n => selected.Contains(n)).ToList(); - var front = ordered.Where(n => !selected.Contains(n)).ToList(); - return [.. back, .. front]; - } - - public static List BringForward(IEnumerable nodes) - where TNode : ICanvasLayerNode - { - List ordered = OrderByZ(nodes); - for (int i = ordered.Count - 2; i >= 0; i--) - { - if (!ordered[i].IsSelected) - continue; - if (ordered[i + 1].IsSelected) - continue; - (ordered[i], ordered[i + 1]) = (ordered[i + 1], ordered[i]); - } - return ordered; - } - - public static List SendBackward(IEnumerable nodes) - where TNode : ICanvasLayerNode - { - List ordered = OrderByZ(nodes); - for (int i = 1; i < ordered.Count; i++) - { - if (!ordered[i].IsSelected) - continue; - if (ordered[i - 1].IsSelected) - continue; - (ordered[i - 1], ordered[i]) = (ordered[i], ordered[i - 1]); - } - return ordered; - } - - public static Dictionary BuildNormalizedMap(IEnumerable ordered) - where TNode : ICanvasLayerNode - { - Dictionary map = []; - int z = 0; - foreach (TNode node in ordered) - map[node] = z++; - return map; - } -} +namespace AkkornStudio.CanvasKit; + +public interface ICanvasLayerNode +{ + bool IsSelected { get; } + int ZOrder { get; } +} + +public static class CanvasLayerOrdering +{ + public static List OrderByZ(IEnumerable nodes) + where TNode : ICanvasLayerNode => + [.. nodes.OrderBy(n => n.ZOrder)]; + + public static List BringToFront(IEnumerable nodes) + where TNode : ICanvasLayerNode + { + List ordered = OrderByZ(nodes); + HashSet selected = ordered.Where(n => n.IsSelected).ToHashSet(); + var back = ordered.Where(n => !selected.Contains(n)).ToList(); + var front = ordered.Where(n => selected.Contains(n)).ToList(); + return [.. back, .. front]; + } + + public static List SendToBack(IEnumerable nodes) + where TNode : ICanvasLayerNode + { + List ordered = OrderByZ(nodes); + HashSet selected = ordered.Where(n => n.IsSelected).ToHashSet(); + var back = ordered.Where(n => selected.Contains(n)).ToList(); + var front = ordered.Where(n => !selected.Contains(n)).ToList(); + return [.. back, .. front]; + } + + public static List BringForward(IEnumerable nodes) + where TNode : ICanvasLayerNode + { + List ordered = OrderByZ(nodes); + for (int i = ordered.Count - 2; i >= 0; i--) + { + if (!ordered[i].IsSelected) + continue; + if (ordered[i + 1].IsSelected) + continue; + (ordered[i], ordered[i + 1]) = (ordered[i + 1], ordered[i]); + } + return ordered; + } + + public static List SendBackward(IEnumerable nodes) + where TNode : ICanvasLayerNode + { + List ordered = OrderByZ(nodes); + for (int i = 1; i < ordered.Count; i++) + { + if (!ordered[i].IsSelected) + continue; + if (ordered[i - 1].IsSelected) + continue; + (ordered[i - 1], ordered[i]) = (ordered[i], ordered[i - 1]); + } + return ordered; + } + + public static Dictionary BuildNormalizedMap(IEnumerable ordered) + where TNode : ICanvasLayerNode + { + Dictionary map = []; + int z = 0; + foreach (TNode node in ordered) + map[node] = z++; + return map; + } +} diff --git a/src/DBWeaver.Canvas/CanvasSubEditorStateMachine.cs b/src/AkkornStudio.Canvas/CanvasSubEditorStateMachine.cs similarity index 94% rename from src/DBWeaver.Canvas/CanvasSubEditorStateMachine.cs rename to src/AkkornStudio.Canvas/CanvasSubEditorStateMachine.cs index e1f27a73..b3c21eee 100644 --- a/src/DBWeaver.Canvas/CanvasSubEditorStateMachine.cs +++ b/src/AkkornStudio.Canvas/CanvasSubEditorStateMachine.cs @@ -1,30 +1,30 @@ -namespace DBWeaver.CanvasKit; - -public enum CanvasSubEditorMode -{ - None, - Cte, - View, -} - -public sealed record CanvasSubEditorSessionState(CanvasSubEditorMode Mode, string DisplayName) -{ - public static readonly CanvasSubEditorSessionState Empty = new(CanvasSubEditorMode.None, string.Empty); - - public bool IsActive => Mode != CanvasSubEditorMode.None; - public bool IsViewEditor => Mode == CanvasSubEditorMode.View; -} - -public static class CanvasSubEditorStateMachine -{ - public static CanvasSubEditorSessionState EnterCte(string displayName) => - new(CanvasSubEditorMode.Cte, NormalizeDisplayName(displayName)); - - public static CanvasSubEditorSessionState EnterView(string displayName) => - new(CanvasSubEditorMode.View, NormalizeDisplayName(displayName)); - - public static CanvasSubEditorSessionState Exit() => CanvasSubEditorSessionState.Empty; - - private static string NormalizeDisplayName(string displayName) => - string.IsNullOrWhiteSpace(displayName) ? string.Empty : displayName.Trim(); -} +namespace AkkornStudio.CanvasKit; + +public enum CanvasSubEditorMode +{ + None, + Cte, + View, +} + +public sealed record CanvasSubEditorSessionState(CanvasSubEditorMode Mode, string DisplayName) +{ + public static readonly CanvasSubEditorSessionState Empty = new(CanvasSubEditorMode.None, string.Empty); + + public bool IsActive => Mode != CanvasSubEditorMode.None; + public bool IsViewEditor => Mode == CanvasSubEditorMode.View; +} + +public static class CanvasSubEditorStateMachine +{ + public static CanvasSubEditorSessionState EnterCte(string displayName) => + new(CanvasSubEditorMode.Cte, NormalizeDisplayName(displayName)); + + public static CanvasSubEditorSessionState EnterView(string displayName) => + new(CanvasSubEditorMode.View, NormalizeDisplayName(displayName)); + + public static CanvasSubEditorSessionState Exit() => CanvasSubEditorSessionState.Empty; + + private static string NormalizeDisplayName(string displayName) => + string.IsNullOrWhiteSpace(displayName) ? string.Empty : displayName.Trim(); +} diff --git a/src/DBWeaver.Canvas/CanvasTableHighlightEngine.cs b/src/AkkornStudio.Canvas/CanvasTableHighlightEngine.cs similarity index 95% rename from src/DBWeaver.Canvas/CanvasTableHighlightEngine.cs rename to src/AkkornStudio.Canvas/CanvasTableHighlightEngine.cs index a560f6d4..cd6f1669 100644 --- a/src/DBWeaver.Canvas/CanvasTableHighlightEngine.cs +++ b/src/AkkornStudio.Canvas/CanvasTableHighlightEngine.cs @@ -1,88 +1,88 @@ -namespace DBWeaver.CanvasKit; - -public interface ICanvasTableNode -{ - bool IsTableSource { get; } - bool IsHighlighted { get; set; } - string? FullName { get; } - string? Title { get; } - string? Alias { get; } -} - -public static class CanvasTableHighlightEngine -{ - public static void ApplyHighlight(IEnumerable nodes, string? highlightedTableName) - { - ArgumentNullException.ThrowIfNull(nodes); - - string? normalizedFull = NormalizeTableReference(highlightedTableName); - string? normalizedShort = normalizedFull is null ? null : normalizedFull.Split('.').Last(); - - foreach (ICanvasTableNode node in nodes) - { - if (!node.IsTableSource) - { - node.IsHighlighted = false; - continue; - } - - node.IsHighlighted = normalizedShort is not null - && MatchesHighlightedTable(node, normalizedFull!, normalizedShort); - } - } - - public static bool MatchesHighlightedTable( - ICanvasTableNode node, - string normalizedFull, - string normalizedShort - ) - { - ArgumentNullException.ThrowIfNull(node); - ArgumentException.ThrowIfNullOrWhiteSpace(normalizedFull); - ArgumentException.ThrowIfNullOrWhiteSpace(normalizedShort); - - string? nodeFull = NormalizeTableReference(node.FullName); - if (nodeFull is not null && nodeFull.Equals(normalizedFull, StringComparison.OrdinalIgnoreCase)) - return true; - - string? nodeTitle = NormalizeTableReference(node.Title); - if (nodeTitle is not null - && nodeTitle.Split('.').Last().Equals(normalizedShort, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - string? nodeAlias = NormalizeTableReference(node.Alias); - if (nodeAlias is not null - && nodeAlias.Split('.').Last().Equals(normalizedShort, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - return false; - } - - public static string? NormalizeTableReference(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return null; - - string raw = value.Trim(); - if (raw.Length == 0) - return null; - - string firstToken = raw.Split( - ' ', - StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0]; - string[] parts = firstToken.Split( - '.', - StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (parts.Length == 0) - return null; - - for (int i = 0; i < parts.Length; i++) - parts[i] = parts[i].Trim('"').Trim('`').Trim('[', ']'); - - return string.Join('.', parts); - } -} +namespace AkkornStudio.CanvasKit; + +public interface ICanvasTableNode +{ + bool IsTableSource { get; } + bool IsHighlighted { get; set; } + string? FullName { get; } + string? Title { get; } + string? Alias { get; } +} + +public static class CanvasTableHighlightEngine +{ + public static void ApplyHighlight(IEnumerable nodes, string? highlightedTableName) + { + ArgumentNullException.ThrowIfNull(nodes); + + string? normalizedFull = NormalizeTableReference(highlightedTableName); + string? normalizedShort = normalizedFull is null ? null : normalizedFull.Split('.').Last(); + + foreach (ICanvasTableNode node in nodes) + { + if (!node.IsTableSource) + { + node.IsHighlighted = false; + continue; + } + + node.IsHighlighted = normalizedShort is not null + && MatchesHighlightedTable(node, normalizedFull!, normalizedShort); + } + } + + public static bool MatchesHighlightedTable( + ICanvasTableNode node, + string normalizedFull, + string normalizedShort + ) + { + ArgumentNullException.ThrowIfNull(node); + ArgumentException.ThrowIfNullOrWhiteSpace(normalizedFull); + ArgumentException.ThrowIfNullOrWhiteSpace(normalizedShort); + + string? nodeFull = NormalizeTableReference(node.FullName); + if (nodeFull is not null && nodeFull.Equals(normalizedFull, StringComparison.OrdinalIgnoreCase)) + return true; + + string? nodeTitle = NormalizeTableReference(node.Title); + if (nodeTitle is not null + && nodeTitle.Split('.').Last().Equals(normalizedShort, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + string? nodeAlias = NormalizeTableReference(node.Alias); + if (nodeAlias is not null + && nodeAlias.Split('.').Last().Equals(normalizedShort, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + public static string? NormalizeTableReference(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + string raw = value.Trim(); + if (raw.Length == 0) + return null; + + string firstToken = raw.Split( + ' ', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)[0]; + string[] parts = firstToken.Split( + '.', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length == 0) + return null; + + for (int i = 0; i < parts.Length; i++) + parts[i] = parts[i].Trim('"').Trim('`').Trim('[', ']'); + + return string.Join('.', parts); + } +} diff --git a/src/DBWeaver.Canvas/CanvasViewportMath.cs b/src/AkkornStudio.Canvas/CanvasViewportMath.cs similarity index 95% rename from src/DBWeaver.Canvas/CanvasViewportMath.cs rename to src/AkkornStudio.Canvas/CanvasViewportMath.cs index fc235e38..9b5d5064 100644 --- a/src/DBWeaver.Canvas/CanvasViewportMath.cs +++ b/src/AkkornStudio.Canvas/CanvasViewportMath.cs @@ -1,74 +1,74 @@ -namespace DBWeaver.CanvasKit; - -public readonly record struct CanvasViewportPoint(double X, double Y); - -public readonly record struct CanvasViewportSize(double Width, double Height); - -public readonly record struct CanvasViewportNodeFrame(double X, double Y, double Width, double Height); - -public readonly record struct CanvasSelectionBounds(double MinX, double MinY, double MaxX, double MaxY) -{ - public double Width => Math.Max(1, MaxX - MinX); - public double Height => Math.Max(1, MaxY - MinY); - public CanvasViewportPoint Center => new((MinX + MaxX) / 2.0, (MinY + MaxY) / 2.0); -} - -public static class CanvasViewportMath -{ - public static bool TryGetSelectionBounds( - IEnumerable selected, - out CanvasSelectionBounds bounds) - { - bounds = default; - - List nodes = selected.ToList(); - if (nodes.Count == 0) - return false; - - double minX = double.MaxValue; - double minY = double.MaxValue; - double maxX = double.MinValue; - double maxY = double.MinValue; - - foreach (CanvasViewportNodeFrame n in nodes) - { - minX = Math.Min(minX, n.X); - minY = Math.Min(minY, n.Y); - maxX = Math.Max(maxX, n.X + Math.Max(1, n.Width)); - maxY = Math.Max(maxY, n.Y + Math.Max(1, n.Height)); - } - - bounds = new CanvasSelectionBounds(minX, minY, maxX, maxY); - return true; - } - - public static CanvasViewportPoint ComputeCenterPan( - CanvasSelectionBounds bounds, - CanvasViewportSize viewport, - double zoom) - { - CanvasViewportPoint center = bounds.Center; - return new CanvasViewportPoint( - viewport.Width / 2.0 - center.X * zoom, - viewport.Height / 2.0 - center.Y * zoom - ); - } - - public static (double Zoom, CanvasViewportPoint Pan) ComputeFit( - CanvasSelectionBounds bounds, - CanvasViewportSize viewport, - double padding, - double minZoom = 0.15, - double maxZoom = 4.0) - { - double contentW = Math.Max(1, bounds.Width + padding * 2); - double contentH = Math.Max(1, bounds.Height + padding * 2); - - double zoomX = viewport.Width / contentW; - double zoomY = viewport.Height / contentH; - double zoom = Math.Clamp(Math.Min(zoomX, zoomY), minZoom, maxZoom); - - CanvasViewportPoint pan = ComputeCenterPan(bounds, viewport, zoom); - return (zoom, pan); - } -} +namespace AkkornStudio.CanvasKit; + +public readonly record struct CanvasViewportPoint(double X, double Y); + +public readonly record struct CanvasViewportSize(double Width, double Height); + +public readonly record struct CanvasViewportNodeFrame(double X, double Y, double Width, double Height); + +public readonly record struct CanvasSelectionBounds(double MinX, double MinY, double MaxX, double MaxY) +{ + public double Width => Math.Max(1, MaxX - MinX); + public double Height => Math.Max(1, MaxY - MinY); + public CanvasViewportPoint Center => new((MinX + MaxX) / 2.0, (MinY + MaxY) / 2.0); +} + +public static class CanvasViewportMath +{ + public static bool TryGetSelectionBounds( + IEnumerable selected, + out CanvasSelectionBounds bounds) + { + bounds = default; + + List nodes = selected.ToList(); + if (nodes.Count == 0) + return false; + + double minX = double.MaxValue; + double minY = double.MaxValue; + double maxX = double.MinValue; + double maxY = double.MinValue; + + foreach (CanvasViewportNodeFrame n in nodes) + { + minX = Math.Min(minX, n.X); + minY = Math.Min(minY, n.Y); + maxX = Math.Max(maxX, n.X + Math.Max(1, n.Width)); + maxY = Math.Max(maxY, n.Y + Math.Max(1, n.Height)); + } + + bounds = new CanvasSelectionBounds(minX, minY, maxX, maxY); + return true; + } + + public static CanvasViewportPoint ComputeCenterPan( + CanvasSelectionBounds bounds, + CanvasViewportSize viewport, + double zoom) + { + CanvasViewportPoint center = bounds.Center; + return new CanvasViewportPoint( + viewport.Width / 2.0 - center.X * zoom, + viewport.Height / 2.0 - center.Y * zoom + ); + } + + public static (double Zoom, CanvasViewportPoint Pan) ComputeFit( + CanvasSelectionBounds bounds, + CanvasViewportSize viewport, + double padding, + double minZoom = 0.15, + double maxZoom = 4.0) + { + double contentW = Math.Max(1, bounds.Width + padding * 2); + double contentH = Math.Max(1, bounds.Height + padding * 2); + + double zoomX = viewport.Width / contentW; + double zoomY = viewport.Height / contentH; + double zoom = Math.Clamp(Math.Min(zoomX, zoomY), minZoom, maxZoom); + + CanvasViewportPoint pan = ComputeCenterPan(bounds, viewport, zoom); + return (zoom, pan); + } +} diff --git a/src/DBWeaver.Canvas/CanvasWireGeometry.cs b/src/AkkornStudio.Canvas/CanvasWireGeometry.cs similarity index 90% rename from src/DBWeaver.Canvas/CanvasWireGeometry.cs rename to src/AkkornStudio.Canvas/CanvasWireGeometry.cs index 066fe8b8..34f234bd 100644 --- a/src/DBWeaver.Canvas/CanvasWireGeometry.cs +++ b/src/AkkornStudio.Canvas/CanvasWireGeometry.cs @@ -1,17 +1,17 @@ -namespace DBWeaver.CanvasKit; - -using System.Globalization; - -public static class CanvasWireGeometry -{ - public static string BuildBezierPath(double fromX, double fromY, double toX, double toY) - { - double dx = Math.Abs(toX - fromX); - double offset = Math.Max(60, dx * 0.5); - - return string.Create( - CultureInfo.InvariantCulture, - $"M {fromX:F1},{fromY:F1} C {fromX + offset:F1},{fromY:F1} {toX - offset:F1},{toY:F1} {toX:F1},{toY:F1}" - ); - } -} +namespace AkkornStudio.CanvasKit; + +using System.Globalization; + +public static class CanvasWireGeometry +{ + public static string BuildBezierPath(double fromX, double fromY, double toX, double toY) + { + double dx = Math.Abs(toX - fromX); + double offset = Math.Max(60, dx * 0.5); + + return string.Create( + CultureInfo.InvariantCulture, + $"M {fromX:F1},{fromY:F1} C {fromX + offset:F1},{fromY:F1} {toX - offset:F1},{toY:F1} {toX:F1},{toY:F1}" + ); + } +} diff --git a/src/DBWeaver.Canvas/CanvasWireStylePolicy.cs b/src/AkkornStudio.Canvas/CanvasWireStylePolicy.cs similarity index 88% rename from src/DBWeaver.Canvas/CanvasWireStylePolicy.cs rename to src/AkkornStudio.Canvas/CanvasWireStylePolicy.cs index 480a6e61..04b3f942 100644 --- a/src/DBWeaver.Canvas/CanvasWireStylePolicy.cs +++ b/src/AkkornStudio.Canvas/CanvasWireStylePolicy.cs @@ -1,20 +1,20 @@ -namespace DBWeaver.CanvasKit; - -public enum CanvasWireDashKind -{ - Solid, - ShortDash, - MediumDash, - LongDash, - WideDash, - Dotted, -} - -public static class CanvasWireStylePolicy -{ - public static double ResolveThickness( - double baseThickness, - bool isHighlighted, - double highlightBoost = 0.7 - ) => isHighlighted ? baseThickness + highlightBoost : baseThickness; -} +namespace AkkornStudio.CanvasKit; + +public enum CanvasWireDashKind +{ + Solid, + ShortDash, + MediumDash, + LongDash, + WideDash, + Dotted, +} + +public static class CanvasWireStylePolicy +{ + public static double ResolveThickness( + double baseThickness, + bool isHighlighted, + double highlightBoost = 0.7 + ) => isHighlighted ? baseThickness + highlightBoost : baseThickness; +} diff --git a/src/DBWeaver.UI/.gitignore b/src/AkkornStudio.UI/.gitignore similarity index 100% rename from src/DBWeaver.UI/.gitignore rename to src/AkkornStudio.UI/.gitignore diff --git a/src/DBWeaver.UI/DBWeaver.UI.csproj b/src/AkkornStudio.UI/AkkornStudio.UI.csproj similarity index 88% rename from src/DBWeaver.UI/DBWeaver.UI.csproj rename to src/AkkornStudio.UI/AkkornStudio.UI.csproj index bf02a673..7ac34b02 100644 --- a/src/DBWeaver.UI/DBWeaver.UI.csproj +++ b/src/AkkornStudio.UI/AkkornStudio.UI.csproj @@ -1,69 +1,71 @@ - - - WinExe - DBWeaver.UI - DBWeaver.UI - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - PreserveNewest - - - PreserveNewest - PreserveNewest - - - - - + + + WinExe + AkkornStudio.UI + AkkornStudio.UI + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + PreserveNewest + + + PreserveNewest + PreserveNewest + + + + + diff --git a/src/DBWeaver.UI/DBWeaver.UI.sln b/src/AkkornStudio.UI/AkkornStudio.UI.sln similarity index 87% rename from src/DBWeaver.UI/DBWeaver.UI.sln rename to src/AkkornStudio.UI/AkkornStudio.UI.sln index 071af49f..f908a951 100644 --- a/src/DBWeaver.UI/DBWeaver.UI.sln +++ b/src/AkkornStudio.UI/AkkornStudio.UI.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.2.0 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBWeaver.UI", "DBWeaver.UI.csproj", "{F8B2CBCC-CEEE-903A-A800-7E1ACB0C4291}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AkkornStudio.UI", "AkkornStudio.UI.csproj", "{F8B2CBCC-CEEE-903A-A800-7E1ACB0C4291}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/DBWeaver.UI/App.axaml b/src/AkkornStudio.UI/App.axaml similarity index 89% rename from src/DBWeaver.UI/App.axaml rename to src/AkkornStudio.UI/App.axaml index f1494caf..bfdab645 100644 --- a/src/DBWeaver.UI/App.axaml +++ b/src/AkkornStudio.UI/App.axaml @@ -1,17 +1,17 @@ - - - - - - - - + + + + + + + + @@ -53,22 +53,22 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/App.axaml.cs b/src/AkkornStudio.UI/App.axaml.cs similarity index 91% rename from src/DBWeaver.UI/App.axaml.cs rename to src/AkkornStudio.UI/App.axaml.cs index 402e8920..2417446c 100644 --- a/src/DBWeaver.UI/App.axaml.cs +++ b/src/AkkornStudio.UI/App.axaml.cs @@ -1,140 +1,140 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; -using Avalonia.Styling; -using DBWeaver.UI.Services.ConnectionManager; -using DBWeaver.UI.Services.ConnectionManager.Contracts; -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.Services.Modal; -using DBWeaver.UI.Services.Settings; -using DBWeaver.UI.Services.Theming; -using DBWeaver.UI.ViewModels.Validation.Conventions; -using DBWeaver.UI.ViewModels.Validation.Conventions.Implementations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace DBWeaver.UI; - -public partial class App : Application -{ - private static readonly ILogger _logger = NullLogger.Instance; - private IServiceProvider? _services; - - public override void Initialize() => AvaloniaXamlLoader.Load(this); - - public override void OnFrameworkInitializationCompleted() - { - ApplySavedThemeVariant(); - ApplyUserThemeIfPresent(); - - _services = BuildServices(); - - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = _services.GetRequiredService(); - - base.OnFrameworkInitializationCompleted(); - } - - private static IServiceProvider BuildServices() - { - var services = new ServiceCollection(); - - services.AddSingleton(_ => NullLoggerFactory.Instance); - services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - services.AddDBWeaver(); - services.AddSingleton(_ => LocalizationService.Instance); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => - new FireAndForgetSafetyExecutor(sp.GetRequiredService>())); - services.AddSingleton(sp => - new ConnectionHealthLifecycleCoordinator(sp.GetRequiredService())); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(_ => GlobalModalManager.Instance); - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - - return services.BuildServiceProvider(); - } - - private static void ApplySavedThemeVariant() - { - if (Application.Current is null) - return; - - AppSettings settings = AppSettingsStore.Load(); - Application.Current.RequestedThemeVariant = - settings.ThemeVariant.Equals("Light", StringComparison.OrdinalIgnoreCase) - ? ThemeVariant.Light - : ThemeVariant.Dark; - } - - private static void ApplyUserThemeIfPresent() - { - string path = ThemeLoader.GetDefaultThemePath(); - ThemeLoadResult load = ThemeLoader.LoadFromPath(path); - if (load.Status == ThemeLoadStatus.NotFound) - return; - - if (load.Status != ThemeLoadStatus.Loaded || load.Config is null) - { - _logger.LogWarning("Theme fallback: {Status} - {Message}", load.Status, load.Message); - return; - } - - ThemeValidationResult validation = ThemeValidator.Validate(load.Config); - foreach (string error in validation.Errors) - _logger.LogError("Theme validation error: {Error}", error); - foreach (string warning in validation.Warnings) - _logger.LogWarning("Theme validation warning: {Warning}", warning); - - if (!validation.IsValid) - { - _logger.LogWarning("Theme fallback: invalid configuration"); - return; - } - - ThemeTokenMapResult mapped = ThemeTokenMapper.Map(load.Config); - foreach (string warning in mapped.Warnings) - _logger.LogWarning("Theme mapping warning: {Warning}", warning); - - int applied = ThemeRuntimeApplier.ApplyToCurrentApplication(mapped.TokenOverrides); - _logger.LogInformation("Theme loaded: applied {AppliedCount} token override(s) from {Path}", applied, path); - } -} - -// ── Program entry point ─────────────────────────────────────────────────────── - -internal static class Program -{ - [STAThread] - public static void Main(string[] args) => - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - - public static AppBuilder BuildAvaloniaApp() => - AppBuilder - .Configure() - .UsePlatformDetect() - .LogToTrace(); -} +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; +using AkkornStudio.UI.Services.ConnectionManager; +using AkkornStudio.UI.Services.ConnectionManager.Contracts; +using AkkornStudio.UI.Services.Localization; +using AkkornStudio.UI.Services.Modal; +using AkkornStudio.UI.Services.Settings; +using AkkornStudio.UI.Services.Theming; +using AkkornStudio.UI.ViewModels.Validation.Conventions; +using AkkornStudio.UI.ViewModels.Validation.Conventions.Implementations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AkkornStudio.UI; + +public partial class App : Application +{ + private static readonly ILogger _logger = NullLogger.Instance; + private IServiceProvider? _services; + + public override void Initialize() => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() + { + ApplySavedThemeVariant(); + ApplyUserThemeIfPresent(); + + _services = BuildServices(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.MainWindow = _services.GetRequiredService(); + + base.OnFrameworkInitializationCompleted(); + } + + private static IServiceProvider BuildServices() + { + var services = new ServiceCollection(); + + services.AddSingleton(_ => NullLoggerFactory.Instance); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddAkkornStudio(); + services.AddSingleton(_ => LocalizationService.Instance); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => + new FireAndForgetSafetyExecutor(sp.GetRequiredService>())); + services.AddSingleton(sp => + new ConnectionHealthLifecycleCoordinator(sp.GetRequiredService())); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(_ => GlobalModalManager.Instance); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + + return services.BuildServiceProvider(); + } + + private static void ApplySavedThemeVariant() + { + if (Application.Current is null) + return; + + AppSettings settings = AppSettingsStore.Load(); + Application.Current.RequestedThemeVariant = + settings.ThemeVariant.Equals("Light", StringComparison.OrdinalIgnoreCase) + ? ThemeVariant.Light + : ThemeVariant.Dark; + } + + private static void ApplyUserThemeIfPresent() + { + string path = ThemeLoader.GetDefaultThemePath(); + ThemeLoadResult load = ThemeLoader.LoadFromPath(path); + if (load.Status == ThemeLoadStatus.NotFound) + return; + + if (load.Status != ThemeLoadStatus.Loaded || load.Config is null) + { + _logger.LogWarning("Theme fallback: {Status} - {Message}", load.Status, load.Message); + return; + } + + ThemeValidationResult validation = ThemeValidator.Validate(load.Config); + foreach (string error in validation.Errors) + _logger.LogError("Theme validation error: {Error}", error); + foreach (string warning in validation.Warnings) + _logger.LogWarning("Theme validation warning: {Warning}", warning); + + if (!validation.IsValid) + { + _logger.LogWarning("Theme fallback: invalid configuration"); + return; + } + + ThemeTokenMapResult mapped = ThemeTokenMapper.Map(load.Config); + foreach (string warning in mapped.Warnings) + _logger.LogWarning("Theme mapping warning: {Warning}", warning); + + int applied = ThemeRuntimeApplier.ApplyToCurrentApplication(mapped.TokenOverrides); + _logger.LogInformation("Theme loaded: applied {AppliedCount} token override(s) from {Path}", applied, path); + } +} + +// ── Program entry point ─────────────────────────────────────────────────────── + +internal static class Program +{ + [STAThread] + public static void Main(string[] args) => + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder + .Configure() + .UsePlatformDetect() + .LogToTrace(); +} diff --git a/src/DBWeaver.UI/AppConstants.cs b/src/AkkornStudio.UI/AppConstants.cs similarity index 93% rename from src/DBWeaver.UI/AppConstants.cs rename to src/AkkornStudio.UI/AppConstants.cs index eaa90752..802a8d2e 100644 --- a/src/DBWeaver.UI/AppConstants.cs +++ b/src/AkkornStudio.UI/AppConstants.cs @@ -1,58 +1,58 @@ -namespace DBWeaver.UI; - -/// -/// Application-wide constants and default values. -/// Centralises configuration that was previously spread across multiple view models and services. -/// -public static class AppConstants -{ - // ── App ────────────────────────────────────────────────────────────────── - - /// Internal app name used in storage paths. - public const string AppName = "DBWeaver"; - - /// Display name shown in window titles and UI labels. - public const string AppDisplayName = "DBWeaver"; - - /// Base directory under AppData for local app persistence. - public static string AppDataDirectory => Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - AppName - ); - - /// Application version embedded in saved canvas files. - public const string AppVersion = "1.0.0"; - - // ── Connection defaults ─────────────────────────────────────────────────── - - /// Default hostname shown in new and reset connection profiles. - public const string DefaultHost = "localhost"; - - /// Seconds between background connection health-check pings. - public const int HealthCheckIntervalSeconds = 60; - - // ── SQL import ──────────────────────────────────────────────────────────── - - /// Maximum characters accepted in the SQL import text box. - public const int DefaultMaxSqlInputLength = 50_000; - - /// Default timeout for a single SQL import operation. - public static readonly TimeSpan DefaultImportTimeout = TimeSpan.FromSeconds(10); - - /// - /// Milliseconds to yield to the UI thread before starting heavy SQL import work, - /// allowing the "Importing…" indicator to render before the CPU-bound parse begins. - /// - public const int DefaultImportStartDelayMs = 80; - - // ── UI timing ───────────────────────────────────────────────────────────── - - /// - /// Milliseconds to wait after the last query-graph change before triggering a - /// live SQL preview refresh (debounce window). - /// - public const int PreviewDebounceMs = 500; - - /// Milliseconds debounce before re-running canvas validation. - public const int ValidationDebounceMs = 200; -} +namespace AkkornStudio.UI; + +/// +/// Application-wide constants and default values. +/// Centralises configuration that was previously spread across multiple view models and services. +/// +public static class AppConstants +{ + // ── App ────────────────────────────────────────────────────────────────── + + /// Internal app name used in storage paths. + public const string AppName = "AkkornStudio"; + + /// Display name shown in window titles and UI labels. + public const string AppDisplayName = "AkkornStudio"; + + /// Base directory under AppData for local app persistence. + public static string AppDataDirectory => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + AppName + ); + + /// Application version embedded in saved canvas files. + public const string AppVersion = "1.0.0"; + + // ── Connection defaults ─────────────────────────────────────────────────── + + /// Default hostname shown in new and reset connection profiles. + public const string DefaultHost = "localhost"; + + /// Seconds between background connection health-check pings. + public const int HealthCheckIntervalSeconds = 60; + + // ── SQL import ──────────────────────────────────────────────────────────── + + /// Maximum characters accepted in the SQL import text box. + public const int DefaultMaxSqlInputLength = 50_000; + + /// Default timeout for a single SQL import operation. + public static readonly TimeSpan DefaultImportTimeout = TimeSpan.FromSeconds(10); + + /// + /// Milliseconds to yield to the UI thread before starting heavy SQL import work, + /// allowing the "Importing…" indicator to render before the CPU-bound parse begins. + /// + public const int DefaultImportStartDelayMs = 80; + + // ── UI timing ───────────────────────────────────────────────────────────── + + /// + /// Milliseconds to wait after the last query-graph change before triggering a + /// live SQL preview refresh (debounce window). + /// + public const int PreviewDebounceMs = 500; + + /// Milliseconds debounce before re-running canvas validation. + public const int ValidationDebounceMs = 200; +} diff --git a/src/DBWeaver.UI/Assets/Localization/en-US.json b/src/AkkornStudio.UI/Assets/Localization/en-US.json similarity index 98% rename from src/DBWeaver.UI/Assets/Localization/en-US.json rename to src/AkkornStudio.UI/Assets/Localization/en-US.json index 7e2041cf..11318a0e 100644 --- a/src/DBWeaver.UI/Assets/Localization/en-US.json +++ b/src/AkkornStudio.UI/Assets/Localization/en-US.json @@ -1,1151 +1,1151 @@ -{ - "main.brand": "DBWeaver", - "main.tab.query1": "Query 1", - "main.new": "New", - "main.open": "Open", - "main.save": "Save", - "main.history": "History", - "main.layout": "Layout", - "main.preview": "Preview", - "main.undo": "Undo", - "main.redo": "Redo", - "main.cleanupOrphans": "Cleanup orphan nodes", - "main.autoFixAliasNaming": "Auto-fix alias naming", - "main.autoLayoutCanvas": "Auto layout canvas", - "main.toggleDataPreview": "Toggle data preview", - "main.language": "Language", - "main.restore.prompt": "Previous session found — restore the last canvas?", - "main.restore.button": "Restore session", - "main.cteEditor.editingPrefix": "Editing CTE: ", - "main.cteEditor.backToCanvas": "Back to Canvas", - "main.cteEditor.exitA11y": "Exit CTE editor", - "main.viewEditor.editingPrefix": "DDL > View: ", - "main.viewEditor.backToCanvas": "Back to DDL", - "main.viewEditor.exitA11y": "Exit view editor", - "connection.title": "Connection Manager", - "connection.subtitle": "Configure, test, and activate connections without leaving your flow", - "connection.none": "No connection", - "connection.active": "ACTIVE", - "connection.health.online": "Online", - "connection.health.degraded": "Degraded", - "connection.health.offline": "Offline", - "connection.tooltip.none": "No active connection — click to manage", - "connection.ping": "Ping", - "connection.saved": "SAVED CONNECTIONS", - "connection.new": "New Connection", - "connection.selectOrCreate": "Select a connection or create a new one", - "connection.name": "Connection Name", - "connection.provider": "Provider", - "connection.host": "Host", - "connection.port": "Port", - "connection.database": "Database", - "connection.sqlitePath": "SQLite Path", - "connection.sqliteBrowse": "Browse", - "connection.sqliteCreate": "Create", - "connection.username": "Username", - "connection.password": "Password", - "connection.timeout": "Timeout (seconds)", - "connection.test": "Test", - "connection.save": "Save", - "connection.connect": "Connect", - "connection.action.testConnection": "Test connection", - "connection.action.saveConnection": "Save connection", - "connection.action.connectConnection": "Connect connection", - "connection.status.connecting": "Connecting...", - "connection.status.connected": "Connected", - "connection.status.testing": "Testing...", - "connection.status.failedPrefix": "Connection failed", - "connection.status.metadataUnavailable": "Connection failed: metadata not available.", - "connection.status.highLatency": "high latency", - "connection.watermark.name": "My Production DB", - "connection.watermark.host": "localhost", - "connection.watermark.port": "5432", - "connection.watermark.database": "database_name", - "connection.watermark.username": "user", - "connection.watermark.password": "••••••••", - "connection.watermark.timeout": "30", - "main.connectingDb": "Connecting to database...", - "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", - "status.nodesSeparator": " nodes · ", - "status.connectionsSuffix": " connections", - "status.undo": "Undo: ", - "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", - "connection.disconnect": "Disconnect", - "connection.action.disconnectConnection": "Disconnect connection", - "connectionTab.active": "ACTIVE CONNECTION", - "connectionTab.none": "No active connection", - "connectionTab.saved": "SAVED CONNECTIONS", - "connectionTab.new": "+ New Connection", - "schema.database": "DATABASE", - "schema.search": "Search tables, columns...", - "schema.loading": "Searching tables, columns...", - "schema.noConnection": "No Connection", - "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", - "schema.emptyNoTables": "No tables found", - "fileHistory.title": "Save/Load Version History", - "fileHistory.reload": "Reload", - "fileHistory.restoreSelected": "Restore selected", - "fileHistory.empty": "No local versions yet", - "fileHistory.emptyHint": "Save this file to generate version history.", - "preview.title": "Data Preview", - "preview.subtitle": "Review data and diagnostics before continuing", - "preview.run": "Run", - "preview.cancel": "Cancel", - "preview.tab.preview": "Preview", - "preview.tab.sql": "SQL", - "preview.close": "Close preview", - "preview.running": "Running preview query… ", - "preview.clickCancel": "Click Cancel to stop", - "preview.cancelled": "Query cancelled", - "preview.runAgain": "Press Run to execute again", - "preview.failed": "Preview execution failed", - "preview.technical": "TECHNICAL DETAILS", - "preview.noData": "No data yet", - "preview.f3Hint": "Press F3 or Space to run the current query", - "sqlImporter.title": "Import SQL to Graph", - "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", - "sqlImporter.sqlStatement": "SQL STATEMENT", - "sqlImporter.supported": "Supported: ", - "sqlImporter.import": "Import", - "sqlImporter.report": "CONVERSION REPORT", - "sqlEditor.mutation.dialogTitle": "Mutation confirmation", - "sqlEditor.mutation.dialogSubtitle": "Review impact before confirming execution", - "search.empty": "No nodes found", - "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", - "search.shortcut": "⇧A", - "search.spawn": "Spawn", - "commandPalette.empty": "No commands match your search", - "commandPalette.search": "Command search", - "commandPalette.shortcut": "CTRL+SHIFT+P", - "commandPalette.execute": "Execute", - "context.editCte": "Edit Selected CTE", - "context.editViewSubcanvas": "Edit View Subcanvas", - "explain.title": "Explain Plan", - "explain.sql": "SQL", - "explain.option.analyze": "Analyze", - "explain.option.buffers": "Buffers", - "explain.badge.simulated": "SIMULATED", - "explain.timing.planning": "Planning:", - "explain.timing.execution": "Execution:", - "explain.section.snapshotComparison": "Snapshot comparison", - "explain.section.indexRecommendations": "Index recommendations", - "explain.section.history": "History", - "explain.detail.estimated": "Estimated", - "explain.detail.actual": "Actual", - "explain.detail.error": "Error", - "explain.detail.time": "Time", - "explain.detail.loops": "Loops", - "explain.rerun": "Re-run", - "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", - "explain.running": "Running EXPLAIN…", - "explain.failed": "⚠ EXPLAIN failed", - "explain.noPlan": "No plan yet", - "explain.rerunHint": "Press Re-run to execute EXPLAIN", - "explain.header.operation": "OPERATION", - "explain.header.cost": "COST", - "explain.header.rows": "ROWS", - "explain.header.err": "ERR", - "explain.header.alert": "ALERT", - "explain.snapshot.labelA": "A", - "explain.snapshot.labelB": "B", - "explain.mode.list": "List", - "explain.mode.tree": "Tree", - "explain.action.snapshot": "Save snapshot", - "explain.action.copyJson": "Copy JSON", - "explain.action.copyText": "Copy text", - "explain.action.saveJson": "Save .json", - "explain.action.openDalibo": "Open in Dalibo", - "explain.legend.seqscan": "SEQ SCAN — full table read, no index", - "explain.legend.sort": "SORT — in-memory sort", - "explain.legend.hash": "HASH — hash join", - "explain.escClose": "Esc to close", - "flowVersion.title": "Flow Version History", - "flowVersion.subtitle": "Create checkpoints, compare versions and restore", - "flowVersion.watermark": "Checkpoint label (optional)…", - "flowVersion.saveCheckpoint": "Save Checkpoint", - "flowVersion.compareMode": "Compare Mode", - "flowVersion.selectBase": "Select BASE version (from):", - "flowVersion.clickAny": "Then click any version in the list below to compare.", - "flowVersion.noCheckpoints": "No checkpoints yet", - "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", - "flowVersion.restore": "Restore", - "flowVersion.diffResults": "Diff Results", - "benchmark.title": "Query Benchmark", - "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", - "benchmark.sql": "SQL being benchmarked", - "benchmark.runLabel": "Run label", - "benchmark.runLabelWatermark": "Run 1", - "benchmark.iterations": "Iterations (1–100)", - "benchmark.warmup": "Warm-up passes (0–10)", - "benchmark.interval": "Interval between runs (ms)", - "benchmark.run": "Run Benchmark", - "benchmark.cancel": "Cancel", - "benchmark.clearHistory": "Clear History", - "benchmark.latest": "LATEST RESULT", - "benchmark.avg": "AVG", - "benchmark.median": "MEDIAN", - "benchmark.min": "MIN", - "benchmark.max": "MAX", - "benchmark.iterationsAt": " iterations · run at ", - "benchmark.history": "HISTORY", - "benchmark.header.label": "Label", - "benchmark.header.avg": "Avg", - "benchmark.header.median": "Median", - "benchmark.header.min": "Min", - "benchmark.header.max": "Max", - "benchmark.itersSuffix": " iters", - "diagnostics.title": "App Diagnostics", - "diagnostics.run": "Run", - "diagnostics.running": "Running checks…", - "diagnostics.ok": "OK", - "diagnostics.warning": "Warning", - "diagnostics.error": "Error", - "diagnostics.tooltip.rerun": "Re-run all checks", - "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", - "diagnostics.tooltip.close": "Close (Esc)", - "autoJoin.title": "Auto-Join Suggestions", - "autoJoin.titleForTable": "Auto-Join suggestions for {0}", - "autoJoin.acceptAll": "Accept All", - "autoJoin.accept": "Accept", - "autoJoin.skip": "Skip", - "autoJoin.allHandled": "All suggestions handled", - "autoJoin.joinKeyword": "JOIN", - "autoJoin.confidence.fkConstraint": "FK Constraint", - "autoJoin.confidence.fkReverse": "FK (Reverse)", - "autoJoin.confidence.namingMatch": "Naming Match", - "autoJoin.confidence.weakMatch": "Weak Match", - "autoJoin.runSelected": "Auto-Join Selected", - "autoJoin.noSimilarityTitle": "No automatic join found", - "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", - "autoJoin.appliedTitle": "Auto-join applied", - "autoJoin.manual.title": "Create Manual Join", - "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", - "autoJoin.manual.leftColumn": "Left column", - "autoJoin.manual.rightColumn": "Right column", - "autoJoin.manual.joinType": "Join type", - "autoJoin.manual.operator": "Operator", - "autoJoin.manual.confirm": "Create join", - "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", - "autoJoin.manualJoinCreatedTitle": "Manual join created", - "autoJoin.manualJoinFailedTitle": "Manual join could not be created", - "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", - "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", - "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", - "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", - "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", - "property.outputAlias": "OUTPUT ALIAS", - "property.sourceAlias": "SOURCE ALIAS", - "property.aliasWatermark": "e.g. MyColumn (optional)", - "property.parameters": "PARAMETERS", - "property.enabled": "Enabled", - "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", - "property.dateWatermark": "YYYY-MM-DD or leave empty", - "property.apply": "Apply", - "property.inputPins": "INPUT PINS", - "property.outputPins": "OUTPUT PINS", - "property.sqlTrace": "SQL TRACE", - "property.live": "live", - "node.numericValue": "Numeric Value", - "node.stringValue": "String Value", - "node.enterText": "Enter text", - "node.datetimeValue": "DateTime Value", - "node.valueLabel": "Value:", - "node.noInputs": "No inputs", - "node.loadingSample": "Loading sample…", - "node.previewFailed": "⚠ Preview failed", - "node.sampleRowsHint": "5 sample rows · demo data", - "sidebar.tab.nodes": "Nodes", - "sidebar.tab.connection": "Connection", - "sidebar.tab.schema": "Schema", - "sidebar.tab.diagnostics": "Diagnostics", - "sidebar.addNode": "+ Add Node (⇧A)", - "sidebar.previewF3": "Preview (F3)", - "nodesList.search": "Search nodes...", - "search.watermark": "Search nodes… (Esc to close)", - "search.snippets": "★ SNIPPETS", - "commandPalette.watermark": "Run a command… (Esc to close)", - "tooltip.newCanvas": "New canvas (Ctrl+N)", - "tooltip.openCanvas": "Open canvas (Ctrl+O)", - "tooltip.saveCanvas": "Save canvas (Ctrl+S)", - "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", - "tooltip.zoomOut": "Zoom out (Ctrl+-)", - "tooltip.zoomIn": "Zoom in (Ctrl++)", - "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", - "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", - "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", - "tooltip.dataPreview": "Data preview (F3)", - "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", - "tooltip.appDiagnostics": "App Diagnostics (self-check)", - "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", - "tooltip.cancelRunningQuery": "Cancel the running query", - "tooltip.closeEsc": "Close (Esc)", - "tooltip.recheckConnectionHealth": "Re-check connection health", - "tooltip.deleteConnection": "Delete connection", - "tooltip.testConnection": "Test connection", - "tooltip.saveConnection": "Save connection", - "tooltip.activateConnection": "Activate this connection", - "tooltip.toggleDataSamplePreview": "Toggle data sample preview", - "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", - "tooltip.copySql": "Copy SQL to clipboard", - "tooltip.formatSql": "Format SQL", - "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", - "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", - "tooltip.switchToQueryMode": "Switch to Query canvas", - "tooltip.switchToDdlMode": "Switch to DDL canvas", - "tooltip.switchToSqlMode": "Switch to SQL editor", - "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", - "tooltip.pins.inputs": "Inputs", - "tooltip.pins.outputs": "Outputs", - "tooltip.pins.none": "None", - "tooltip.tableColumns": "Columns", - "tooltip.tableColumns.none": "No detailed columns", - "window.minimize": "Minimize window", - "window.maximizeRestore": "Maximize/restore window", - "window.close": "Close window", - "menu.newDiagram": "New diagram", - "menu.openFile": "Open file", - "menu.save": "Save", - "menu.fileHistory": "File history", - "menu.shortcuts": "Keyboard shortcuts", - "menu.settings": "Settings", - "menu.group.project": "PROJECT", - "menu.group.currentMode": "CURRENT MODE", - "menu.group.tools": "TOOLS", - "menu.reason.ddlOnly": "Available only in DDL mode.", - "menu.importSqlQuery": "Import SQL to Query", - "menu.importDdlSchema": "Import DDL schema", - "menu.viewDdlSql": "View DDL SQL", - "menu.executeDdl": "Execute DDL", - "menu.backToStart": "Back to start", - "toast.ddlExecuteFailed": "Failed to execute DDL.", - "toast.ddlOpenFailed": "Failed to open DDL SQL.", - "toast.ddlImportFailed": "Failed to import schema into DDL.", - "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", - "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", - "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", - "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", - "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", - "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", - "toast.ddlTableImported": "Table imported into the DDL canvas.", - "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", - "toast.ddlNoActiveConnection": "No active connection to execute DDL.", - "toast.ddlExecutedSuccess": "DDL executed successfully.", - "toast.ddlExecutedWithIssues": "DDL executed with issues.", - "toast.switchToDdl": "Switch to DDL mode to generate SQL.", - "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", - "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", - "toast.previewOpenFailed": "Failed to open preview.", - "tab.switchFailed": "Failed to switch tab: {0}", - "settings.status.darkApplied": "Dark theme applied.", - "settings.status.lightApplied": "Light theme applied.", - "settings.status.snapUpdated": "Snap updated: {0}.", - "settings.status.languageToggled": "Language toggled.", - "settings.status.languageSelected": "Language selected: {0}.", - "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", - "settings.section.appearance.title": "Themes", - "settings.section.languageRegion.title": "Language & Region", - "settings.section.dateTime.title": "Date & Time", - "settings.section.keyboard.title": "Keyboard Shortcuts", - "settings.section.privacy.title": "Privacy", - "settings.section.notification.title": "Notification", - "settings.section.accessibility.title": "Accessibility", - "settings.section.default.title": "Settings", - "settings.section.appearance.subtitle": "Choose your style or customize your theme", - "settings.section.languageRegion.subtitle": "Manage language and regional formatting", - "settings.section.keyboard.subtitle": "Customize keyboard shortcuts used by command palette and canvas execution.", - "settings.section.wip.subtitle": "Work in progress.", - "settings.section.default.subtitle": "Application settings", - "settings.general": "General", - "settings.nav.appearance": "Appearance", - "settings.theme.light": "Light Mode", - "settings.theme.dark": "Dark Mode", - "settings.theme.system": "System Preferences", - "settings.gridSnap.title": "Grid Snap", - "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", - "settings.language.subtitle": "Toggle between PT-BR and EN-US.", - "settings.language.toggle": "Toggle language", - "settings.language.option.ptBR": "Portuguese (Brazil)", - "settings.language.option.enUS": "English (United States)", - "settings.language.option.esES": "Spanish (Spain)", - "settings.language.option.ruRU": "Russian", - "settings.language.option.jaJP": "Japanese", - "settings.language.option.zhTW": "Traditional Chinese", - "settings.themeJson.title": "Theme JSON", - "settings.themeJson.subtitle": "Paste your theme JSON, apply and persist instantly.", - "settings.themeJson.apply": "Apply JSON", - "settings.themeJson.restoreDefault": "Restore default theme", - "mode.query": "Query", - "mode.ddl": "DDL", - "mode.sql": "SQL", - "sidebar.left.close": "Close left sidebar", - "sidebar.left.open": "Reopen left sidebar", - "sidebar.right.close": "Close right sidebar", - "sidebar.right.open": "Reopen right sidebar", - "connection.completedTitle": "Connection completed", - "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", - "connection.close": "Close connection manager", - "connection.refreshHealth": "Refresh connection health", - "common.details": "Details", - "common.cancel": "Cancel", - "common.keep": "Keep", - "common.clear": "Clear", - "zoom.out": "Zoom out", - "zoom.in": "Zoom in", - "zoom.fit": "Fit zoom to screen", - "zoom.level": "Zoom level", - "settings.theme.mode": "Theme mode", - "diagnostics.category.canvas": "Canvas Integrity", - "diagnostics.category.output": "Output & Execution", - "diagnostics.category.session": "Session & Safety", - "diagnostics.category.notice": "Runtime Notices", - "diagnostics.summary.ok": "All systems OK", - "diagnostics.summary.warningCount": "{0} warning(s) detected", - "diagnostics.summary.errorCount": "{0} error(s) detected", - "diagnostics.canvasMigration": "Canvas Migration", - "diagnostics.recommendation.resaveFile": "Re-save the file to update it to the latest schema version.", - "diagnostics.canvasState.name": "Canvas State", - "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", - "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", - "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", - "diagnostics.validation.name": "Validation Errors", - "diagnostics.validation.recommendation": "Fix highlighted nodes before running output preview", - "diagnostics.validation.errorWithWarnings": "{0} error(s) and {1} warning(s) in the graph", - "diagnostics.validation.warningOnly": "{0} warning(s) in the graph", - "diagnostics.validation.none": "No validation issues", - "diagnostics.orphan.name": "Orphan Nodes", - "diagnostics.orphan.recommendation": "Use the orphan cleanup action to remove unused nodes", - "diagnostics.orphan.count": "{0} node(s) not connected to any output", - "diagnostics.orphan.none": "No orphan nodes detected", - "diagnostics.naming.name": "Naming Conventions", - "diagnostics.naming.recommendation": "Use auto-fix alias naming when conformance is below 100%", - "diagnostics.naming.conformance": "Naming conformance: {0}%", - "diagnostics.naming.ok": "All aliases follow naming conventions (100%)", - "diagnostics.queryCompilation.name": "Live SQL Compilation", - "diagnostics.queryCompilation.recommendation": "Review SQL diagnostics in output when errors/warnings are reported.", - "diagnostics.queryCompilation.errorFallback": "Live SQL compilation reported errors.", - "diagnostics.queryCompilation.warningCounts": "{0} diagnostic item(s), {1} guardrail warning(s).", - "diagnostics.queryCompilation.ok": "Live SQL compiled without diagnostics.", - "diagnostics.previewSafety.name": "Preview Safety", - "diagnostics.previewSafety.recommendation": "Preview executes read-only statements only.", - "diagnostics.previewSafety.blocked": "Current SQL is mutating and blocked by Safe Preview mode.", - "diagnostics.previewSafety.ok": "Preview safety checks passed.", - "diagnostics.previewExecution.name": "Preview Execution", - "diagnostics.previewExecution.recommendation": "Run preview and inspect diagnostics for execution/runtime errors.", - "diagnostics.previewExecution.failed": "Preview execution failed.", - "diagnostics.previewExecution.cancelled": "Preview execution was cancelled.", - "diagnostics.previewExecution.done": "{0} row(s) in {1}ms.", - "diagnostics.previewExecution.none": "No preview execution issues detected.", - "diagnostics.ddlCompilation.name": "DDL Compilation", - "diagnostics.ddlCompilation.recommendation": "Fix DDL compile diagnostics before execution.", - "diagnostics.ddlCompilation.failed": "DDL compilation failed.", - "diagnostics.ddlCompilation.warningCount": "{0} warning(s) reported by DDL compiler.", - "diagnostics.ddlCompilation.ok": "DDL compilation succeeded.", - "diagnostics.ddlOutput.name": "DDL Output", - "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", - "diagnostics.ddlOutput.none": "No DDL statements generated yet.", - "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", - "diagnostics.undo.name": "Undo History", - "diagnostics.undo.recommendation": "History is in-memory only; save your canvas regularly.", - "diagnostics.undo.saved": "Canvas is saved (no unsaved changes).", - "diagnostics.undo.unsavedDeep": "Unsaved changes with {0} undo steps.", - "diagnostics.undo.unsaved": "Unsaved changes - {0} undo step(s) available.", - "diagnostics.report.title": "DBWeaver - Diagnostic Report", - "diagnostics.report.generated": "Generated", - "diagnostics.report.overall": "Overall", - "diagnostics.report.details": "Details", - "diagnostics.report.recommendation": "Recommendation", - "diagnostics.report.lastCheck": "Last Check", - "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", - "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", - "preview.providerLabel": "Provider", - "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", - "preview.schemaAnalysis.run": "Run Analysis", - "preview.schemaAnalysis.cancel": "Cancel", - "preview.schemaAnalysis.issues": "Issues", - "preview.schemaAnalysis.clearFilters": "Clear Filters", - "preview.schemaAnalysis.clearBlacklist": "Clear Blacklist", - "preview.schemaAnalysis.severity": "Severity", - "preview.schemaAnalysis.severity.info": "Info", - "preview.schemaAnalysis.severity.warning": "Warning", - "preview.schemaAnalysis.severity.critical": "Critical", - "preview.schemaAnalysis.rule": "Rule", - "preview.schemaAnalysis.rule.fkCatalogInconsistent": "FK catalog inconsistent", - "preview.schemaAnalysis.rule.missingFk": "Missing FK", - "preview.schemaAnalysis.rule.namingConventionViolation": "Naming convention violation", - "preview.schemaAnalysis.rule.lowSemanticName": "Low semantic name", - "preview.schemaAnalysis.rule.missingRequiredComment": "Missing required comment", - "preview.schemaAnalysis.rule.nf1HintMultiValued": "1NF hint: multi-valued", - "preview.schemaAnalysis.rule.nf2HintPartialDependency": "2NF hint: partial dependency", - "preview.schemaAnalysis.rule.nf3HintTransitiveDependency": "3NF hint: transitive dependency", - "preview.schemaAnalysis.minConfidence": "Min Confidence", - "preview.schemaAnalysis.tableFilter": "Table Filter", - "preview.schemaAnalysis.tableFilterWatermark": "schema.table", - "preview.schemaAnalysis.ignore": "Execution Filters", - "preview.schemaAnalysis.ignoreViews": "Ignore views and materialized views", - "preview.schemaAnalysis.blacklist": "Table blacklist", - "preview.schemaAnalysis.blacklistAdd": "Add", - "preview.schemaAnalysis.blacklistRemove": "Remove selected", - "preview.schemaAnalysis.ignoreTable.placeholder": "schema.table", - "preview.schemaAnalysis.details": "Details", - "preview.schemaAnalysis.evidence": "Evidence", - "preview.schemaAnalysis.suggestions": "Suggestions", - "preview.schemaAnalysis.ruleDiagnostics": "Rule Diagnostics", - "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", - "preview.schemaAnalysis.copySql": "Copy SQL", - "preview.schemaAnalysis.applyToCanvas": "Apply to Canvas", - "preview.schemaAnalysis.summary.issues": "Issues:", - "preview.schemaAnalysis.summary.rawPrefix": "(raw:", - "preview.schemaAnalysis.summary.critical": "| Critical:", - "preview.schemaAnalysis.summary.warning": "| Warning:", - "preview.schemaAnalysis.summary.info": "| Info:", - "preview.schemaAnalysis.state.metadataUnavailable": "Metadata unavailable for structural analysis.", - "preview.schemaAnalysis.state.cancelled": "Analysis cancelled by the user.", - "preview.schemaAnalysis.state.partialTimeout": "Analysis finished partially due to timeout.", - "preview.schemaAnalysis.state.failed": "Structural analysis failed.", - "preview.schemaAnalysis.state.empty": "No inferable structural issue was detected.", - "preview.schemaAnalysis.state.noFilterMatch": "No issue matches the selected filters.", - "preview.schemaAnalysis.state.noIssueSelected": "No issue selected.", - "preview.schemaAnalysis.state.noSqlCandidate": "No SQL candidate available.", - "preview.schemaAnalysis.actionBlockedTooltip": "Action unavailable for the current risk level or capability.", - "common.navigate": "Navigate", - "common.close": "Close", - "common.esc": "Esc", - "common.ms": "ms", - "common.zero": "0", - "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", - "nodesList.empty": "No nodes found", - "nodesList.emptyHint": "Adjust the search term to explore available types", - "schema.emptyFiltered": "No objects found for the current filter", - "start.lastSnapshot": "Last snapshot", - "app.brandBadge": "VS", - "property.tab.properties": "Properties", - "property.tab.projectSettings": "Project Settings", - "property.nodeType": "NODE TYPE", - "property.selectNodeHint": "Select a node to edit its properties.", - "property.namingConventions": "Naming Conventions", - "property.aliasConvention": "Alias convention", - "property.enforceAliasNaming": "Enforce alias naming", - "property.warnReservedSql": "Warn on reserved SQL keywords", - "property.maxAliasLength": "Max alias length", - "property.maxAliasLengthDefault": "64", - "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", - "start.tips": "Tips", - "start.tips.quick": "Quick tips", - "start.tips.item1": "1. Click New Diagram to start from scratch.", - "start.tips.item2": "2. Use templates to speed up prototyping.", - "start.tips.item3": "3. Open saved connections to load real tables.", - "start.tips.shortcut": "Shortcut: CTRL+SHIFT+P opens the command palette.", - "start.workspace": "WORKSPACE", - "start.resumeTitle": "Continue where you left off", - "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", - "start.chip.quickFlow": "Quick flow", - "start.chip.templates": "Templates", - "start.chip.connections": "Connections", - "start.savedConnectionsTitle": "Saved Connections", - "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", - "start.noConnectionsTitle": "No connections configured yet", - "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", - "start.newConnection": "+ New Connection", - "start.recentProjectsTitle": "Recent Projects", - "start.searchRecent": "Search recent project...", - "start.quickActions": "Quick actions", - "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", - "start.noRecentTitle": "No recent projects yet", - "start.noRecentSubtitle": "Use the quick actions card above to get started.", - "start.exploreTemplates": "Explore templates", - "start.templatesFavoritesHint": "Favorites on top", - "start.favoriteTemplate": "Favorite template", - "node.columnSetPreview": "ColumnSet preview", - "node.view": "VIEW", - "node.tableDefinition": "Table Definition", - "node.join": "JOIN", - "node.window.addPartition": "Add PARTITION BY slot", - "node.window.removePartition": "Remove PARTITION BY slot", - "node.window.addOrder": "Add ORDER BY slot", - "node.window.removeOrder": "Remove ORDER BY slot", - "sql.keyword.select": "SELECT", - "sql.keyword.from": "FROM", - "sql.keyword.join": "JOIN", - "sql.keyword.where": "WHERE", - "sql.keyword.limit": "LIMIT", - "sqlImporter.close": "Close SQL importer", - "sqlImporter.report.imported": "Imported", - "sqlImporter.report.partial": "Partial", - "sqlImporter.report.skipped": "Skipped", - "sqlImporter.confirmClearCanvas": "SQL import will clear the current canvas. Do you want to continue?", - "sqlImporter.confirmProceed": "Continue import", - "benchmark.close": "Close benchmark", - "benchmark.p95": "P95", - "benchmark.n": "N", - "liveSql.safePreview": "SAFE PREVIEW MODE", - "liveSql.title": "LIVE SQL", - "liveSql.blocked": "BLOCKED", - "liveSql.copy": "Copy", - "liveSql.format": "Format", - "liveSql.benchmark": "Benchmark", - "liveSql.explain": "Explain", - "liveSql.actionsHint": "Performance tools", - "ddl.dialog.title": "Execute DDL", - "ddl.dialog.execute": "Execute", - "ddl.dialog.cancel": "Cancel", - "ddl.dialog.close": "Close", - "ddl.dialog.stopOnError": "Stop on first failure", - "ddl.dialog.confirmDestructive": "I confirm execution of destructive statements (DROP TABLE)", - "ddl.dialog.reviewBeforeRun": "Review the DDL script before confirming.", - "ddl.dialog.confirmQuestion": "Confirm DDL execution on the connected database?", - "ddl.dialog.irreversibleWarning": "This action can change the schema irreversibly.", - "ddl.dialog.mustConfirmDestructive": "Confirm destructive execution to continue.", - "ddl.dialog.executing": "Executing...", - "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", - "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", - "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", - "ddl.execute.result.failed": "Failed to execute DDL.", - "ddl.execute.result.cancelled": "Execution cancelled by the user.", - "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", - "context.deleteSingle": "Delete {0}", - "context.deleteMultiple": "Delete {0} nodes", - "context.bringForward": "Bring Forward (Ctrl+PgUp)", - "context.sendBackward": "Send Backward (Ctrl+PgDown)", - "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", - "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", - "context.normalizeLayers": "Normalize Layers", - "context.deleteWire": "Delete wire", - "context.addNode": "Add Node (Shift+A)", - "context.undoWithDescription": "Undo {0}", - "context.redo": "Redo", - "shortcuts.windowTitle": "Keyboard Shortcuts", - "shortcuts.headerTitle": "DBWeaver - Shortcuts", - "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", - "shortcuts.filterWatermark": "Filter shortcuts by key or action...", - "shortcuts.resultCount": "{0} shortcuts", - "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", - "shortcuts.noneFound": "No shortcuts found.", - "shortcuts.section.fileGeneral": "File and General", - "shortcuts.section.editing": "Editing", - "shortcuts.section.canvasNavigation": "Canvas and Navigation", - "shortcuts.section.zoomPanPrecision": "Zoom, pan and precision", - "shortcuts.section.previewInspection": "Preview and Inspection", - "shortcuts.key.deleteOrBackspace": "Del or Backspace", - "shortcuts.key.middleDrag": "Middle mouse + drag", - "shortcuts.key.rightDrag": "Right mouse + drag", - "shortcuts.key.spaceDrag": "Space + drag", - "shortcuts.key.altLeftDrag": "Alt + left drag", - "shortcuts.key.arrows": "Arrows", - "shortcuts.key.shiftArrows": "Shift + Arrows", - "shortcuts.action.openShortcutScreen": "Open this shortcuts screen", - "shortcuts.action.newCanvas": "New canvas", - "shortcuts.action.openFile": "Open file", - "shortcuts.action.save": "Save", - "shortcuts.action.saveAs": "Save as", - "shortcuts.action.commandPalette": "Command Palette", - "shortcuts.action.undo": "Undo", - "shortcuts.action.redo": "Redo", - "shortcuts.action.selectAll": "Select all", - "shortcuts.action.deleteSelection": "Delete selection", - "shortcuts.action.closeOverlayCancel": "Close overlays / cancel actions", - "shortcuts.action.openNodeSearch": "Open node search", - "shortcuts.action.resetViewport": "Reset viewport", - "shortcuts.action.centerSelection": "Center selection", - "shortcuts.action.fitSelection": "Fit selection", - "shortcuts.action.autoLayout": "Auto Layout", - "shortcuts.action.toggleSnapToGrid": "Toggle Snap to Grid", - "shortcuts.action.bringForward": "Bring Forward", - "shortcuts.action.sendBackward": "Send Backward", - "shortcuts.action.bringToFront": "Bring to Front", - "shortcuts.action.sendToBack": "Send to Back", - "shortcuts.action.zoomInOut": "Zoom in / out", - "shortcuts.action.pan": "Pan", - "shortcuts.action.temporaryPan": "Temporary pan", - "shortcuts.action.alternatePan": "Alternate pan", - "shortcuts.action.fineNudge": "Fine nudge selection", - "shortcuts.action.fastNudge": "Fast nudge", - "shortcuts.action.togglePreview": "Toggle data preview", - "shortcuts.action.explainPlan": "Explain plan", - "shortcuts.action.runPreview": "Run preview", - "shortcuts.action.connectionManager": "Connection manager", - "shortcuts.action.flowVersionHistory": "Flow version history", - "shortcuts.resetAll": "Reset all", - "shortcuts.customized": "Customized", - "shortcuts.default": "Default", - "shortcuts.apply": "Apply", - "shortcuts.reset": "Reset", - "shortcuts.status.resetAllSuccess": "All shortcuts reset to defaults.", - "shortcuts.status.updated": "Shortcut updated.", - "shortcuts.status.reset": "Shortcut reset to default.", - "shortcuts.status.updateFailed": "Unable to update shortcut.", - "toast.severity.success": "Success", - "toast.severity.warning": "Warning", - "toast.severity.error": "Error", - "toast.details.success": "Success Details", - "toast.details.warning": "Warning Details", - "toast.details.error": "Error Details", - "diagnostics.area.cteEditor": "CTE Editor", - "diagnostics.area.viewEditor": "View Editor", - "diagnostics.area.subEditor": "Sub-editor", - "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", - "diagnostics.recommendation.reloadFileIfNeeded": "Reload the file if needed.", - "diagnostics.viewEditor.exitFailed": "Could not exit: {0}", - "diagnostics.viewEditor.canvasIncomplete": "the canvas is incomplete.", - "diagnostics.viewEditor.exitRecommendation": "Connect a valid ResultOutput or use the discard command.", - "diagnostics.viewEditor.restoreParentFailed": "Failed to restore the parent canvas. The subgraph was discarded.", - "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", - "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", - "diagnostics.canvasMigration.openWarning": "Open: {0}", - "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", - "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", - "diagnostics.recommendation.resaveLatestSchema": "Review diagnostics and re-save the canvas to persist the latest schema.", - "diagnostics.recommendation.saveMigratedSchema": "Review diagnostics and save the canvas to persist the migrated schema.", - "file.saveDialog.title": "Save Canvas", - "file.saveDialog.suggestedName": "Query1", - "file.save.success": "Canvas saved successfully.", - "file.save.failedWithReason": "Save failed: {0}", - "file.openDialog.title": "Open Canvas", - "file.openDialog.canvasType": "SQL Architect Canvas", - "file.open.failedWithReason": "Open failed: {0}", - "file.open.success": "Canvas opened successfully.", - "file.open.successWithWarnings": "Canvas opened with warnings.", - "session.restore.failedWithReason": "Restore failed: {0}", - "session.restore.successWithWarnings": "Session restored with warnings.", - "session.restore.success": "Session restored successfully.", - "export.documentation.dialogTitle": "Export Flow Documentation", - "export.documentation.success": "Documentation exported successfully.", - "export.documentation.failed": "Documentation export failed.", - "export.failed.pathPermissionsHint": "Check file path and permissions.", - "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", - "export.dialogTitleByExtension": "Export as {0}", - "export.success": "Export completed successfully.", - "export.failed": "Export failed.", - "fileHistory.currentFile.none": "No file selected", - "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", - "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", - "fileHistory.status.countAvailable": "{0} local version(s) available.", - "fileHistory.restore.failedWithReason": "Restore failed: {0}", - "fileHistory.restore.successFrom": "Restored version from {0}.", - "preview.status.cancelled": "Cancelled", - "preview.status.error": "Error", - "preview.status.ready": "Ready", - "preview.runningWithMs": "Running... {0}ms", - "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", - "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", - "explain.errorWithReason": "Explain plan error: {0}", - "explain.noSql": "No SQL to explain. Build a query on the canvas first.", - "ddl.compilationFailed": "Compilation failed", - "ddl.compileErrorWithReason": "DDL compile error: {0}", - "command.undo.name": "Undo", - "command.undo.description": "Undo last action", - "command.redo.name": "Redo", - "command.redo.description": "Redo last undone action", - "command.addNode.name": "Add Node", - "command.addNode.description": "Open node search menu to add a node", - "command.bringForward.name": "Bring Forward", - "command.bringForward.description": "Move selected nodes one layer forward", - "command.sendBackward.name": "Send Backward", - "command.sendBackward.description": "Move selected nodes one layer backward", - "command.bringToFront.name": "Bring to Front", - "command.bringToFront.description": "Move selected nodes to top layer", - "command.sendToBack.name": "Send to Back", - "command.sendToBack.description": "Move selected nodes to bottom layer", - "command.normalizeLayers.name": "Normalize Layers", - "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", - "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", - "main.orphanSuffix": "Orphan(s)", - "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", - "main.namingPrefix": "Naming", - "fileHistory.compressedLabel": "Compressed:", - "schema.itemsSuffix": "item(s)", - "property.panel.title": "Properties", - "property.panel.multiSelected": "{0} nodes selected", - "sqlImporter.status.pasteSelect": "Paste a SELECT statement above, then click Import.", - "sqlImporter.status.inputTooLarge": "SQL input is too large ({0:N0} chars). Limit is {1:N0}. Split the query or increase the import limit.", - "sqlImporter.status.parsing": "Parsing SQL...", - "sqlImporter.status.done": "Done - {0} imported, {1} partial, {2} skipped.", - "sqlImporter.status.cancelledByUser": "Import cancelled by user.", - "sqlImporter.status.timeout": "Import timed out after {0:0.#}s. Try a smaller query or increase timeout.", - "sqlImporter.status.parseError": "Parse error: {0}", - "sqlImporter.status.clearConfirmationRequired": "SQL import will clear the current canvas. Confirm to continue.", - "sqlImporter.status.clearConfirmationCancelled": "Import cancelled. The current canvas was kept.", - "diagnostics.area.undoRedoTransaction": "Undo/Redo Transaction", - "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", - "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", - "node.preview.noCatalog": "No catalog available", - "connection.error.searchMenuNotInitialized": "search menu not initialized", - "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", - "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", - "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", - "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", - "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", - "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", - "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", - "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", - "diagnostics.area.connection": "Connection", - "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", - "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", - "undoRedo.transaction.unnamed": "unnamed transaction", - "benchmark.runLabelDefault": "Run 1", - "benchmark.runLabelPattern": "Run {0}", - "benchmark.status.failedWithReason": "Benchmark failed: {0}", - "benchmark.status.noSql": "No SQL to benchmark - build a query first.", - "benchmark.status.warmupProgress": "Warm-up {0}/{1}...", - "benchmark.status.iterationProgress": "Iteration {0}/{1}...", - "benchmark.status.done": "Done - {0}", - "benchmark.status.cancelled": "Benchmark cancelled.", - "app.windowTitle": "DBWeaver", - "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", - "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", - "sqlImporter.error.selectFromNotFound": "Could not find SELECT ... FROM in the query.", - "sqlImporter.error.fromClauseParseFailed": "Could not parse FROM clause.", - "sqlImporter.error.syntaxUnterminatedString": "Syntax error at line {0}, column {1}: unterminated string literal.", - "sqlImporter.error.missingClosingParenthesis": "missing closing ')'", - "sqlImporter.error.unexpectedClosingParenthesis": "unexpected ')'", - "sqlImporter.error.syntaxAtLineColumn": "Syntax error at line {0}, column {1}: {2}.", - "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", - "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", - "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", - "errorDiagnostics.connection.label": "Connection failed", - "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", - "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", - "errorDiagnostics.authorization.label": "Authorization error", - "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", - "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", - "errorDiagnostics.timeout.label": "Query timeout", - "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", - "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", - "errorDiagnostics.schema.label": "Schema error", - "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", - "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", - "errorDiagnostics.syntax.label": "SQL syntax error", - "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", - "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", - "errorDiagnostics.compatibility.label": "Compatibility error", - "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", - "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", - "errorDiagnostics.unknown.label": "Unexpected error", - "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", - "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", - "error.mainWindow.invalidDataContext": "MainWindow DataContext must be a ShellViewModel.", - "error.mainWindow.canvasNotInitialized": "CanvasViewModel was not initialized.", - "error.mainWindow.ddlPreviewUnavailable": "DDL preview is unavailable for the current canvas.", - "themeJson.editor.template": "{\n \"meta\": { \"name\": \"Custom Theme\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", - "themeJson.error.pasteBeforeApply": "Paste a theme JSON before applying.", - "themeJson.error.invalidJson": "Invalid JSON: {0}", - "themeJson.error.emptyPayload": "Invalid JSON: empty payload.", - "themeJson.error.invalidTheme": "Invalid theme: {0}", - "themeJson.error.appliedButSaveFailed": "Theme applied, but failed to persist: {0}", - "themeJson.success.appliedAndSaved": "JSON theme applied and saved.", - "themeJson.success.customRemoved": "Custom theme removed. Restart the app to fully return to the default theme.", - "themeJson.error.restoreDefaultFailed": "Failed to restore default theme: {0}", - "themeValidator.error.configNull": "Theme config is null.", - "themeValidator.warning.noSections": "Theme has no colors or typography sections; nothing to apply.", - "themeValidator.warning.invalidColor": "{0} has invalid color '{1}'. This key will be ignored.", - "themeValidator.warning.sizeOutOfRange": "{0}={1} is out of range (8..48). This key will be ignored.", - "queryExecutor.error.openConnectionMethodNotFound": "Cannot find OpenConnectionAsync method on orchestrator", - "queryExecutor.error.openConnectionInvokeFailed": "Failed to invoke OpenConnectionAsync", - "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}': view SELECT cannot be reconstructed visually - edit it manually in the subcanvas.", - "ddlImporter.error.tableNotFoundInMetadata": "Table '{0}' was not found in current metadata.", - "main.window.untitled": "Untitled", - "main.subEditor.noSeedProvided": "No {0} sub-editor seed was provided.", - "main.layerOrder.bringToFront": "Bring to front", - "main.layerOrder.sendToBack": "Send to back", - "main.layerOrder.bringForward": "Bring forward", - "main.layerOrder.sendBackward": "Send backward", - "main.layerOrder.normalizeLayers": "Normalize layers", - "export.fileType.html": "HTML Files", - "export.fileType.json": "JSON Files", - "export.fileType.csv": "CSV Files", - "export.fileType.excel": "Excel Files", - "commandPalette.templatePrefix": "Template: {0}", - "themeLoader.status.notFoundWithPath": "Theme file not found: {0}", - "themeLoader.status.deserializedNull": "Theme JSON deserialized to null.", - "themeLoader.status.loaded": "Theme JSON loaded successfully.", - "credential.error.ciphertextTooShort": "Ciphertext blob is too short.", - "credential.error.dpapiWindowsOnly": "DPAPI is only available on Windows.", - "credential.warning.loadVaultFailed": "Failed to load credential vault '{0}': {1}", - "credential.warning.persistVaultFailed": "Failed to persist credential vault '{0}': {1}", - "snippetStore.warning.loadFailed": "Failed to load snippets from '{0}': {1}", - "snippetStore.warning.saveFailed": "Failed to save snippets: {0}", - "flowVersionStore.warning.loadFailed": "Failed to load flow versions from '{0}': {1}", - "flowVersionStore.warning.saveFailed": "Failed to save flow versions: {0}", - "queryExecutor.error.queryEmpty": "Query cannot be empty", - "queryExecutor.error.providerNotSupported": "Provider {0} is not supported", - "queryExecutor.error.singleStatementOnly": "Preview accepts a single SQL statement only.", - "queryExecutor.error.queryEmptyWithPeriod": "Query cannot be empty.", - "queryExecutor.error.readOnlyOnly": "Preview mode only supports read-only SQL statements.", - "queryExecutor.error.namedParametersNotSupported": "Preview mode does not support bound parameters in execution SQL. Inline safe literals or run the query outside preview.", - "queryExecutor.error.positionalParametersNotSupported": "Preview mode does not support positional parameter placeholders ('?' or '$1').", - "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "Align selected nodes to the bottom edge", - "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "Align selected nodes to the leftmost edge", - "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "Align selected nodes to the rightmost edge", - "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "Align selected nodes to the topmost edge", - "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "Apply CTE sub-canvas edits and return to the parent canvas", - "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "Arrange nodes into logical columns automatically", - "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "Centre selected nodes on a horizontal axis", - "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "Centre selected nodes on a vertical axis", - "commandPalette.description.clear_canvas_and_start_fresh": "Clear canvas and start fresh", - "commandPalette.description.clear_node_selection": "Clear node selection", - "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "Convert aliases to the convention configured in project settings", - "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "Create checkpoints, compare versions side-by-side and restore a previous canvas state", - "commandPalette.description.delete_the_selected_nodes": "Delete the selected nodes", - "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "Discard current sub-editor edits and return to the parent canvas", - "commandPalette.description.execute_the_current_query_in_preview": "Execute the current query in preview", - "commandPalette.description.fit_all_nodes_into_the_visible_area": "Fit all nodes into the visible area", - "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "Generate CSV file from the first CSV Export node", - "commandPalette.description.generate_html_file_from_the_first_html_export_node": "Generate HTML file from the first HTML Export node", - "commandPalette.description.generate_json_file_from_the_first_json_export_node": "Generate JSON file from the first JSON Export node", - "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "Generate XLSX workbook from the first Excel Export node", - "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "Inspect the query execution plan — see scan types, join strategies, and cost estimates", - "commandPalette.description.load_a_vsaq_canvas_file": "Load a .vsaq canvas file", - "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "Measure avg / median / p95 latency of the current SQL over N iterations", - "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "Open isolated sub-canvas editor for the selected CTE Definition node", - "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "Open local file version history created on each save and restore previous saved snapshots", - "commandPalette.description.open_output_preview_modal_for_the_active_mode": "Open output preview modal for the active mode", - "commandPalette.description.open_shortcut_reference_screen": "Open shortcut reference screen", - "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "Open the connection manager to add, edit or switch database connections", - "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "Paste a SELECT statement and generate nodes automatically — FROM, JOIN, WHERE, LIMIT are supported", - "commandPalette.description.remove_all_nodes_not_connected_to_output": "Remove all nodes not connected to output", - "commandPalette.description.reset_zoom_and_pan_to_default": "Reset zoom and pan to default", - "commandPalette.description.save_canvas_to_a_new_file": "Save canvas to a new file", - "commandPalette.description.save_current_canvas": "Save current canvas", - "commandPalette.description.save_markdown_documentation_of_the_current_flow": "Save Markdown documentation of the current flow", - "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "Save the selected nodes as a reusable snippet — insert it later via the node search menu (⇧A)", - "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "Scan all table-source nodes on the canvas for possible join relationships based on FK conventions and naming patterns", - "commandPalette.description.select_all_nodes_on_canvas": "Select all nodes on canvas", - "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "Snap node positions to 16px grid (Ctrl+G)", - "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "Spread selected nodes with equal horizontal spacing", - "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "Spread selected nodes with equal vertical spacing", - "commandPalette.description.zoom_into_the_canvas": "Zoom into the canvas", - "commandPalette.description.zoom_out_of_the_canvas": "Zoom out of the canvas", - "commandPalette.name.align_bottom": "Align Bottom", - "commandPalette.name.align_left": "Align Left", - "commandPalette.name.align_right": "Align Right", - "commandPalette.name.align_top": "Align Top", - "commandPalette.name.analyze_all_joins": "Analyze All Joins", - "commandPalette.name.auto_fix_naming": "Auto-Fix Naming", - "commandPalette.name.auto_layout": "Auto Layout", - "commandPalette.name.center_horizontally": "Center Horizontally", - "commandPalette.name.center_vertically": "Center Vertically", - "commandPalette.name.cleanup_orphans": "Cleanup Orphans", - "commandPalette.name.delete_selected": "Delete Selected", - "commandPalette.name.deselect_all": "Deselect All", - "commandPalette.name.discard_and_exit_editor": "Discard and Exit Editor", - "commandPalette.name.distribute_horizontally": "Distribute Horizontally", - "commandPalette.name.distribute_vertically": "Distribute Vertically", - "commandPalette.name.edit_selected_cte": "Edit Selected CTE", - "commandPalette.name.exit_cte_editor": "Exit CTE Editor", - "commandPalette.name.explain_plan": "Explain Plan", - "commandPalette.name.export_csv": "Export CSV", - "commandPalette.name.export_documentation": "Export Documentation", - "commandPalette.name.export_excel": "Export Excel", - "commandPalette.name.export_html": "Export HTML", - "commandPalette.name.export_json": "Export JSON", - "commandPalette.name.file_save_load_history": "File Save/Load History", - "commandPalette.name.fit_to_screen": "Fit to Screen", - "commandPalette.name.flow_version_history": "Flow Version History", - "commandPalette.name.import_sql_to_graph": "Import SQL to Graph", - "commandPalette.name.keyboard_shortcuts": "Keyboard Shortcuts", - "commandPalette.name.manage_connections": "Manage Connections", - "commandPalette.name.new_canvas": "New Canvas", - "commandPalette.name.open_file": "Open File", - "commandPalette.name.reset_viewport": "Reset Viewport", - "commandPalette.name.run_preview": "Run Preview", - "commandPalette.name.run_query_benchmark": "Run Query Benchmark", - "commandPalette.name.save": "Save", - "commandPalette.name.save_as": "Save As", - "commandPalette.name.save_selection_as_snippet": "Save Selection as Snippet", - "commandPalette.name.select_all": "Select All", - "commandPalette.name.toggle_preview": "Toggle Preview", - "commandPalette.name.toggle_snap_to_grid": "Toggle Snap to Grid", - "commandPalette.name.zoom_in": "Zoom In", - "commandPalette.name.zoom_out": "Zoom Out", - "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", - "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", - "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", - "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", - "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", - "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", - "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", - "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", - "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", - "commandPalette.tags.clear_selection": "clear selection", - "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", - "commandPalette.tags.create_insert_search_transform": "create insert search transform", - "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", - "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", - "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", - "commandPalette.tags.data_results_table_panel": "data results table panel", - "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", - "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", - "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", - "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", - "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", - "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", - "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", - "commandPalette.tags.export_json_file_output_save": "export json file output save", - "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", - "commandPalette.tags.export_persist_copy": "export persist copy", - "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", - "commandPalette.tags.forward_history": "forward history", - "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", - "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", - "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", - "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", - "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", - "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", - "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", - "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", - "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", - "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", - "commandPalette.tags.load_import_vsaq": "load import vsaq", - "commandPalette.tags.magnify_enlarge": "magnify enlarge", - "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", - "commandPalette.tags.persist_write_disk": "persist write disk", - "commandPalette.tags.remove_erase_nodes": "remove erase nodes", - "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", - "commandPalette.tags.reset_clear_blank": "reset clear blank", - "commandPalette.tags.revert_back_history": "revert back history", - "commandPalette.tags.shrink_reduce": "shrink reduce", - "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", - "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", - "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", - "sqlEditor.diffPreview.title": "Transactional Diff Preview", - "sqlEditor.mutation.confirmExecute": "Confirm Execute", - "sqlEditor.tab.closeAnyway": "Close Anyway", - "sqlEditor.tab.keepTab": "Keep Tab", - "sqlEditor.status.ready": "Ready.", - "sqlEditor.telemetry.none": "No execution telemetry yet.", - "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", - "sqlEditor.telemetry.errors.none": "No aggregated errors.", - "sqlEditor.diff.none": "No transactional diff preview available.", - "sqlEditor.mutation.estimate.none": "No mutation estimate available.", - "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", - "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", - "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", - "sqlEditor.tab.noPendingClose": "No tab close pending.", - "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", - "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", - "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", - "sqlEditor.message.empty": "Execute a statement to see messages.", - "sqlEditor.message.success": "Execution completed successfully.", - "sqlEditor.result.summary.empty": "Rows: - Time: -", - "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", - "sqlEditor.file.save.canceled": "Save canceled.", - "sqlEditor.file.save.noPath": "No target path selected.", - "sqlEditor.file.save.success": "SQL file saved.", - "sqlEditor.file.save.failed": "Save failed.", - "sqlEditor.file.open.failed": "Open failed.", - "sqlEditor.file.open.notFound": "Selected SQL file was not found.", - "sqlEditor.file.open.success": "SQL file opened.", - "sqlEditor.status.executing": "Executing SQL...", - "sqlEditor.status.executingScript": "Executing SQL script...", - "sqlEditor.status.executingStep": "Executing {0}/{1}...", - "sqlEditor.status.canceling": "Canceling execution...", - "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", - "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", - "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", - "sqlEditor.status.success": "Execution succeeded.", - "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", - "sqlEditor.status.canceled": "Execution canceled.", - "sqlEditor.status.failed": "Execution failed.", - "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", - "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", - "sqlEditor.result.tabTitle": "Result {0}", - "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", - "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", - "sqlEditor.tab.closed": "Tab closed.", - "sqlEditor.tab.closeCanceled": "Tab close canceled.", - "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", - "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", - "sqlEditor.error.noConnection": "No active database connection for SQL execution.", - "sqlEditor.error.executionCanceled": "SQL execution was canceled.", - "sqlEditor.tab.scriptTitle": "Script {0}", - "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", - "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", - "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", - "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", - "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", - "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", - "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", - "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", - "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", - "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", - "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", - "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", - "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", - "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", - "sqlEditor.results.title": "Results", - "sqlEditor.results.filterWatermark": "Filter results", - "sqlEditor.results.rows.countZero": "0 rows", - "sqlEditor.results.rows.countSingle": "{0} rows", - "sqlEditor.results.rows.countFiltered": "{0} of {1} rows", - "sqlEditor.results.context.copyCell": "Copy Cell", - "sqlEditor.results.context.copyRow": "Copy Row", - "sqlEditor.results.context.hideColumn": "Hide Column", - "sqlEditor.results.context.showAllColumns": "Show All Columns", - "sqlEditor.sidebar.messages": "MESSAGES", - "sqlEditor.sidebar.history": "HISTORY", - "sqlEditor.sidebar.connection.none": "No connection", - "sqlEditor.sidebar.connection.connectHint": "Connect to load metadata and execute queries.", - "sqlEditor.sidebar.connection.connect": "Connect", - "sqlEditor.sidebar.schema.searchWatermark": "Search table or column", - "sqlEditor.sidebar.schema.reloadTooltip": "Reload metadata", - "sqlEditor.sidebar.schema.connectHint": "Connect to view the full schema.", - "sqlEditor.history.searchWatermark": "Search history", - "sqlEditor.history.navigationHint": "Arrow keys navigate history, Enter runs selected, Esc clears the search.", - "sqlEditor.history.noSearchResults": "No items found for this search.", - "sqlEditor.history.use": "Use", - "sqlEditor.history.copy": "Copy", - "sqlEditor.history.run": "Run", - "sqlEditor.history.copiedFromHistory": "SQL copied from history.", - "sqlEditor.toast.scriptSuccessTitle": "Script executed.", - "sqlEditor.toast.scriptSuccessDetail": "{0} statement(s) executed successfully.", - "sqlEditor.toast.scriptWarningTitle": "Script executed with failures.", - "sqlEditor.toast.scriptWarningDetail": "{0} of {1} statement(s) failed.", - "sqlEditor.toast.resultErrorTitle": "Failed to execute statement.", - "sqlEditor.toast.resultSuccessTitle": "Execution completed successfully.", - "sqlEditor.export.action": "Export Report", - "sqlEditor.openSql.pickerTitle": "Open SQL File", - "sqlEditor.saveSql.fileType": "SQL Files", - "sqlEditor.saveSql.pickerTitle": "Save SQL File", - "sqlEditor.export.pickerTitle": "Export SQL Data", - "sqlEditor.export.status.noResultTitle": "No execution result available for export.", - "sqlEditor.export.status.noResultDetail": "Execute a query first.", - "sqlEditor.export.status.successTitle": "Report exported.", - "sqlEditor.export.status.failedTitle": "Failed to export report.", - "sqlEditor.export.fileType.html": "HTML File", - "sqlEditor.export.fileType.json": "JSON File", - "sqlEditor.export.fileType.csv": "CSV File", - "sqlEditor.export.fileType.xlsx": "Excel Workbook", - "sqlEditor.export.defaultFileBase": "report", - "sqlEditor.export.defaultTitle": "SQL Report", - "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", - "sqlEditor.export.type.html.title": "HTML full-feature report", - "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", - "sqlEditor.export.type.json.title": "JSON execution contract", - "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", - "sqlEditor.export.type.csv.title": "CSV data export", - "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", - "sqlEditor.export.type.xlsx.title": "Excel workbook export", - "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", - "sqlEditor.export.dialog.windowTitle": "Export SQL Data", - "sqlEditor.export.dialog.title": "Export SQL Data", - "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", - "sqlEditor.export.dialog.confirm": "Export", - "sqlEditor.export.dialog.fileNameWatermark": "report.html", - "sqlEditor.export.dialog.titleWatermark": "SQL Report", - "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", - "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", - "sqlEditor.export.dialog.section.fileName": "FILE NAME", - "sqlEditor.export.dialog.section.reportTitle": "TITLE", - "sqlEditor.export.dialog.section.description": "DESCRIPTION", - "sqlEditor.export.dialog.section.options": "OPTIONS", - "sqlEditor.export.option.includeSchema": "Include output schema", - "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", - "sqlEditor.export.option.includeMetadata": "Include optional metadata", - "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", - "sqlEditor.export.badge.offline": "OFFLINE READY", - "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", - "sqlEditor.export.badge.dataOnly": "DATA ONLY" -} +{ + "main.brand": "AkkornStudio", + "main.tab.query1": "Query 1", + "main.new": "New", + "main.open": "Open", + "main.save": "Save", + "main.history": "History", + "main.layout": "Layout", + "main.preview": "Preview", + "main.undo": "Undo", + "main.redo": "Redo", + "main.cleanupOrphans": "Cleanup orphan nodes", + "main.autoFixAliasNaming": "Auto-fix alias naming", + "main.autoLayoutCanvas": "Auto layout canvas", + "main.toggleDataPreview": "Toggle data preview", + "main.language": "Language", + "main.restore.prompt": "Previous session found — restore the last canvas?", + "main.restore.button": "Restore session", + "main.cteEditor.editingPrefix": "Editing CTE: ", + "main.cteEditor.backToCanvas": "Back to Canvas", + "main.cteEditor.exitA11y": "Exit CTE editor", + "main.viewEditor.editingPrefix": "DDL > View: ", + "main.viewEditor.backToCanvas": "Back to DDL", + "main.viewEditor.exitA11y": "Exit view editor", + "connection.title": "Connection Manager", + "connection.subtitle": "Configure, test, and activate connections without leaving your flow", + "connection.none": "No connection", + "connection.active": "ACTIVE", + "connection.health.online": "Online", + "connection.health.degraded": "Degraded", + "connection.health.offline": "Offline", + "connection.tooltip.none": "No active connection — click to manage", + "connection.ping": "Ping", + "connection.saved": "SAVED CONNECTIONS", + "connection.new": "New Connection", + "connection.selectOrCreate": "Select a connection or create a new one", + "connection.name": "Connection Name", + "connection.provider": "Provider", + "connection.host": "Host", + "connection.port": "Port", + "connection.database": "Database", + "connection.sqlitePath": "SQLite Path", + "connection.sqliteBrowse": "Browse", + "connection.sqliteCreate": "Create", + "connection.username": "Username", + "connection.password": "Password", + "connection.timeout": "Timeout (seconds)", + "connection.test": "Test", + "connection.save": "Save", + "connection.connect": "Connect", + "connection.action.testConnection": "Test connection", + "connection.action.saveConnection": "Save connection", + "connection.action.connectConnection": "Connect connection", + "connection.status.connecting": "Connecting...", + "connection.status.connected": "Connected", + "connection.status.testing": "Testing...", + "connection.status.failedPrefix": "Connection failed", + "connection.status.metadataUnavailable": "Connection failed: metadata not available.", + "connection.status.highLatency": "high latency", + "connection.watermark.name": "My Production DB", + "connection.watermark.host": "localhost", + "connection.watermark.port": "5432", + "connection.watermark.database": "database_name", + "connection.watermark.username": "user", + "connection.watermark.password": "••••••••", + "connection.watermark.timeout": "30", + "main.connectingDb": "Connecting to database...", + "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", + "status.nodesSeparator": " nodes · ", + "status.connectionsSuffix": " connections", + "status.undo": "Undo: ", + "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", + "connection.disconnect": "Disconnect", + "connection.action.disconnectConnection": "Disconnect connection", + "connectionTab.active": "ACTIVE CONNECTION", + "connectionTab.none": "No active connection", + "connectionTab.saved": "SAVED CONNECTIONS", + "connectionTab.new": "+ New Connection", + "schema.database": "DATABASE", + "schema.search": "Search tables, columns...", + "schema.loading": "Searching tables, columns...", + "schema.noConnection": "No Connection", + "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", + "schema.emptyNoTables": "No tables found", + "fileHistory.title": "Save/Load Version History", + "fileHistory.reload": "Reload", + "fileHistory.restoreSelected": "Restore selected", + "fileHistory.empty": "No local versions yet", + "fileHistory.emptyHint": "Save this file to generate version history.", + "preview.title": "Data Preview", + "preview.subtitle": "Review data and diagnostics before continuing", + "preview.run": "Run", + "preview.cancel": "Cancel", + "preview.tab.preview": "Preview", + "preview.tab.sql": "SQL", + "preview.close": "Close preview", + "preview.running": "Running preview query… ", + "preview.clickCancel": "Click Cancel to stop", + "preview.cancelled": "Query cancelled", + "preview.runAgain": "Press Run to execute again", + "preview.failed": "Preview execution failed", + "preview.technical": "TECHNICAL DETAILS", + "preview.noData": "No data yet", + "preview.f3Hint": "Press F3 or Space to run the current query", + "sqlImporter.title": "Import SQL to Graph", + "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", + "sqlImporter.sqlStatement": "SQL STATEMENT", + "sqlImporter.supported": "Supported: ", + "sqlImporter.import": "Import", + "sqlImporter.report": "CONVERSION REPORT", + "sqlEditor.mutation.dialogTitle": "Mutation confirmation", + "sqlEditor.mutation.dialogSubtitle": "Review impact before confirming execution", + "search.empty": "No nodes found", + "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", + "search.shortcut": "⇧A", + "search.spawn": "Spawn", + "commandPalette.empty": "No commands match your search", + "commandPalette.search": "Command search", + "commandPalette.shortcut": "CTRL+SHIFT+P", + "commandPalette.execute": "Execute", + "context.editCte": "Edit Selected CTE", + "context.editViewSubcanvas": "Edit View Subcanvas", + "explain.title": "Explain Plan", + "explain.sql": "SQL", + "explain.option.analyze": "Analyze", + "explain.option.buffers": "Buffers", + "explain.badge.simulated": "SIMULATED", + "explain.timing.planning": "Planning:", + "explain.timing.execution": "Execution:", + "explain.section.snapshotComparison": "Snapshot comparison", + "explain.section.indexRecommendations": "Index recommendations", + "explain.section.history": "History", + "explain.detail.estimated": "Estimated", + "explain.detail.actual": "Actual", + "explain.detail.error": "Error", + "explain.detail.time": "Time", + "explain.detail.loops": "Loops", + "explain.rerun": "Re-run", + "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", + "explain.running": "Running EXPLAIN…", + "explain.failed": "⚠ EXPLAIN failed", + "explain.noPlan": "No plan yet", + "explain.rerunHint": "Press Re-run to execute EXPLAIN", + "explain.header.operation": "OPERATION", + "explain.header.cost": "COST", + "explain.header.rows": "ROWS", + "explain.header.err": "ERR", + "explain.header.alert": "ALERT", + "explain.snapshot.labelA": "A", + "explain.snapshot.labelB": "B", + "explain.mode.list": "List", + "explain.mode.tree": "Tree", + "explain.action.snapshot": "Save snapshot", + "explain.action.copyJson": "Copy JSON", + "explain.action.copyText": "Copy text", + "explain.action.saveJson": "Save .json", + "explain.action.openDalibo": "Open in Dalibo", + "explain.legend.seqscan": "SEQ SCAN — full table read, no index", + "explain.legend.sort": "SORT — in-memory sort", + "explain.legend.hash": "HASH — hash join", + "explain.escClose": "Esc to close", + "flowVersion.title": "Flow Version History", + "flowVersion.subtitle": "Create checkpoints, compare versions and restore", + "flowVersion.watermark": "Checkpoint label (optional)…", + "flowVersion.saveCheckpoint": "Save Checkpoint", + "flowVersion.compareMode": "Compare Mode", + "flowVersion.selectBase": "Select BASE version (from):", + "flowVersion.clickAny": "Then click any version in the list below to compare.", + "flowVersion.noCheckpoints": "No checkpoints yet", + "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", + "flowVersion.restore": "Restore", + "flowVersion.diffResults": "Diff Results", + "benchmark.title": "Query Benchmark", + "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", + "benchmark.sql": "SQL being benchmarked", + "benchmark.runLabel": "Run label", + "benchmark.runLabelWatermark": "Run 1", + "benchmark.iterations": "Iterations (1–100)", + "benchmark.warmup": "Warm-up passes (0–10)", + "benchmark.interval": "Interval between runs (ms)", + "benchmark.run": "Run Benchmark", + "benchmark.cancel": "Cancel", + "benchmark.clearHistory": "Clear History", + "benchmark.latest": "LATEST RESULT", + "benchmark.avg": "AVG", + "benchmark.median": "MEDIAN", + "benchmark.min": "MIN", + "benchmark.max": "MAX", + "benchmark.iterationsAt": " iterations · run at ", + "benchmark.history": "HISTORY", + "benchmark.header.label": "Label", + "benchmark.header.avg": "Avg", + "benchmark.header.median": "Median", + "benchmark.header.min": "Min", + "benchmark.header.max": "Max", + "benchmark.itersSuffix": " iters", + "diagnostics.title": "App Diagnostics", + "diagnostics.run": "Run", + "diagnostics.running": "Running checks…", + "diagnostics.ok": "OK", + "diagnostics.warning": "Warning", + "diagnostics.error": "Error", + "diagnostics.tooltip.rerun": "Re-run all checks", + "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", + "diagnostics.tooltip.close": "Close (Esc)", + "autoJoin.title": "Auto-Join Suggestions", + "autoJoin.titleForTable": "Auto-Join suggestions for {0}", + "autoJoin.acceptAll": "Accept All", + "autoJoin.accept": "Accept", + "autoJoin.skip": "Skip", + "autoJoin.allHandled": "All suggestions handled", + "autoJoin.joinKeyword": "JOIN", + "autoJoin.confidence.fkConstraint": "FK Constraint", + "autoJoin.confidence.fkReverse": "FK (Reverse)", + "autoJoin.confidence.namingMatch": "Naming Match", + "autoJoin.confidence.weakMatch": "Weak Match", + "autoJoin.runSelected": "Auto-Join Selected", + "autoJoin.noSimilarityTitle": "No automatic join found", + "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", + "autoJoin.appliedTitle": "Auto-join applied", + "autoJoin.manual.title": "Create Manual Join", + "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", + "autoJoin.manual.leftColumn": "Left column", + "autoJoin.manual.rightColumn": "Right column", + "autoJoin.manual.joinType": "Join type", + "autoJoin.manual.operator": "Operator", + "autoJoin.manual.confirm": "Create join", + "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", + "autoJoin.manualJoinCreatedTitle": "Manual join created", + "autoJoin.manualJoinFailedTitle": "Manual join could not be created", + "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", + "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", + "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", + "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", + "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", + "property.outputAlias": "OUTPUT ALIAS", + "property.sourceAlias": "SOURCE ALIAS", + "property.aliasWatermark": "e.g. MyColumn (optional)", + "property.parameters": "PARAMETERS", + "property.enabled": "Enabled", + "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", + "property.dateWatermark": "YYYY-MM-DD or leave empty", + "property.apply": "Apply", + "property.inputPins": "INPUT PINS", + "property.outputPins": "OUTPUT PINS", + "property.sqlTrace": "SQL TRACE", + "property.live": "live", + "node.numericValue": "Numeric Value", + "node.stringValue": "String Value", + "node.enterText": "Enter text", + "node.datetimeValue": "DateTime Value", + "node.valueLabel": "Value:", + "node.noInputs": "No inputs", + "node.loadingSample": "Loading sample…", + "node.previewFailed": "⚠ Preview failed", + "node.sampleRowsHint": "5 sample rows · demo data", + "sidebar.tab.nodes": "Nodes", + "sidebar.tab.connection": "Connection", + "sidebar.tab.schema": "Schema", + "sidebar.tab.diagnostics": "Diagnostics", + "sidebar.addNode": "+ Add Node (⇧A)", + "sidebar.previewF3": "Preview (F3)", + "nodesList.search": "Search nodes...", + "search.watermark": "Search nodes… (Esc to close)", + "search.snippets": "★ SNIPPETS", + "commandPalette.watermark": "Run a command… (Esc to close)", + "tooltip.newCanvas": "New canvas (Ctrl+N)", + "tooltip.openCanvas": "Open canvas (Ctrl+O)", + "tooltip.saveCanvas": "Save canvas (Ctrl+S)", + "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", + "tooltip.zoomOut": "Zoom out (Ctrl+-)", + "tooltip.zoomIn": "Zoom in (Ctrl++)", + "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", + "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", + "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", + "tooltip.dataPreview": "Data preview (F3)", + "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", + "tooltip.appDiagnostics": "App Diagnostics (self-check)", + "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", + "tooltip.cancelRunningQuery": "Cancel the running query", + "tooltip.closeEsc": "Close (Esc)", + "tooltip.recheckConnectionHealth": "Re-check connection health", + "tooltip.deleteConnection": "Delete connection", + "tooltip.testConnection": "Test connection", + "tooltip.saveConnection": "Save connection", + "tooltip.activateConnection": "Activate this connection", + "tooltip.toggleDataSamplePreview": "Toggle data sample preview", + "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", + "tooltip.copySql": "Copy SQL to clipboard", + "tooltip.formatSql": "Format SQL", + "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", + "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", + "tooltip.switchToQueryMode": "Switch to Query canvas", + "tooltip.switchToDdlMode": "Switch to DDL canvas", + "tooltip.switchToSqlMode": "Switch to SQL editor", + "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", + "tooltip.pins.inputs": "Inputs", + "tooltip.pins.outputs": "Outputs", + "tooltip.pins.none": "None", + "tooltip.tableColumns": "Columns", + "tooltip.tableColumns.none": "No detailed columns", + "window.minimize": "Minimize window", + "window.maximizeRestore": "Maximize/restore window", + "window.close": "Close window", + "menu.newDiagram": "New diagram", + "menu.openFile": "Open file", + "menu.save": "Save", + "menu.fileHistory": "File history", + "menu.shortcuts": "Keyboard shortcuts", + "menu.settings": "Settings", + "menu.group.project": "PROJECT", + "menu.group.currentMode": "CURRENT MODE", + "menu.group.tools": "TOOLS", + "menu.reason.ddlOnly": "Available only in DDL mode.", + "menu.importSqlQuery": "Import SQL to Query", + "menu.importDdlSchema": "Import DDL schema", + "menu.viewDdlSql": "View DDL SQL", + "menu.executeDdl": "Execute DDL", + "menu.backToStart": "Back to start", + "toast.ddlExecuteFailed": "Failed to execute DDL.", + "toast.ddlOpenFailed": "Failed to open DDL SQL.", + "toast.ddlImportFailed": "Failed to import schema into DDL.", + "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", + "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", + "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", + "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", + "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", + "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", + "toast.ddlTableImported": "Table imported into the DDL canvas.", + "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", + "toast.ddlNoActiveConnection": "No active connection to execute DDL.", + "toast.ddlExecutedSuccess": "DDL executed successfully.", + "toast.ddlExecutedWithIssues": "DDL executed with issues.", + "toast.switchToDdl": "Switch to DDL mode to generate SQL.", + "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", + "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", + "toast.previewOpenFailed": "Failed to open preview.", + "tab.switchFailed": "Failed to switch tab: {0}", + "settings.status.darkApplied": "Dark theme applied.", + "settings.status.lightApplied": "Light theme applied.", + "settings.status.snapUpdated": "Snap updated: {0}.", + "settings.status.languageToggled": "Language toggled.", + "settings.status.languageSelected": "Language selected: {0}.", + "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", + "settings.section.appearance.title": "Themes", + "settings.section.languageRegion.title": "Language & Region", + "settings.section.dateTime.title": "Date & Time", + "settings.section.keyboard.title": "Keyboard Shortcuts", + "settings.section.privacy.title": "Privacy", + "settings.section.notification.title": "Notification", + "settings.section.accessibility.title": "Accessibility", + "settings.section.default.title": "Settings", + "settings.section.appearance.subtitle": "Choose your style or customize your theme", + "settings.section.languageRegion.subtitle": "Manage language and regional formatting", + "settings.section.keyboard.subtitle": "Customize keyboard shortcuts used by command palette and canvas execution.", + "settings.section.wip.subtitle": "Work in progress.", + "settings.section.default.subtitle": "Application settings", + "settings.general": "General", + "settings.nav.appearance": "Appearance", + "settings.theme.light": "Light Mode", + "settings.theme.dark": "Dark Mode", + "settings.theme.system": "System Preferences", + "settings.gridSnap.title": "Grid Snap", + "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", + "settings.language.subtitle": "Toggle between PT-BR and EN-US.", + "settings.language.toggle": "Toggle language", + "settings.language.option.ptBR": "Portuguese (Brazil)", + "settings.language.option.enUS": "English (United States)", + "settings.language.option.esES": "Spanish (Spain)", + "settings.language.option.ruRU": "Russian", + "settings.language.option.jaJP": "Japanese", + "settings.language.option.zhTW": "Traditional Chinese", + "settings.themeJson.title": "Theme JSON", + "settings.themeJson.subtitle": "Paste your theme JSON, apply and persist instantly.", + "settings.themeJson.apply": "Apply JSON", + "settings.themeJson.restoreDefault": "Restore default theme", + "mode.query": "Query", + "mode.ddl": "DDL", + "mode.sql": "SQL", + "sidebar.left.close": "Close left sidebar", + "sidebar.left.open": "Reopen left sidebar", + "sidebar.right.close": "Close right sidebar", + "sidebar.right.open": "Reopen right sidebar", + "connection.completedTitle": "Connection completed", + "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", + "connection.close": "Close connection manager", + "connection.refreshHealth": "Refresh connection health", + "common.details": "Details", + "common.cancel": "Cancel", + "common.keep": "Keep", + "common.clear": "Clear", + "zoom.out": "Zoom out", + "zoom.in": "Zoom in", + "zoom.fit": "Fit zoom to screen", + "zoom.level": "Zoom level", + "settings.theme.mode": "Theme mode", + "diagnostics.category.canvas": "Canvas Integrity", + "diagnostics.category.output": "Output & Execution", + "diagnostics.category.session": "Session & Safety", + "diagnostics.category.notice": "Runtime Notices", + "diagnostics.summary.ok": "All systems OK", + "diagnostics.summary.warningCount": "{0} warning(s) detected", + "diagnostics.summary.errorCount": "{0} error(s) detected", + "diagnostics.canvasMigration": "Canvas Migration", + "diagnostics.recommendation.resaveFile": "Re-save the file to update it to the latest schema version.", + "diagnostics.canvasState.name": "Canvas State", + "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", + "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", + "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", + "diagnostics.validation.name": "Validation Errors", + "diagnostics.validation.recommendation": "Fix highlighted nodes before running output preview", + "diagnostics.validation.errorWithWarnings": "{0} error(s) and {1} warning(s) in the graph", + "diagnostics.validation.warningOnly": "{0} warning(s) in the graph", + "diagnostics.validation.none": "No validation issues", + "diagnostics.orphan.name": "Orphan Nodes", + "diagnostics.orphan.recommendation": "Use the orphan cleanup action to remove unused nodes", + "diagnostics.orphan.count": "{0} node(s) not connected to any output", + "diagnostics.orphan.none": "No orphan nodes detected", + "diagnostics.naming.name": "Naming Conventions", + "diagnostics.naming.recommendation": "Use auto-fix alias naming when conformance is below 100%", + "diagnostics.naming.conformance": "Naming conformance: {0}%", + "diagnostics.naming.ok": "All aliases follow naming conventions (100%)", + "diagnostics.queryCompilation.name": "Live SQL Compilation", + "diagnostics.queryCompilation.recommendation": "Review SQL diagnostics in output when errors/warnings are reported.", + "diagnostics.queryCompilation.errorFallback": "Live SQL compilation reported errors.", + "diagnostics.queryCompilation.warningCounts": "{0} diagnostic item(s), {1} guardrail warning(s).", + "diagnostics.queryCompilation.ok": "Live SQL compiled without diagnostics.", + "diagnostics.previewSafety.name": "Preview Safety", + "diagnostics.previewSafety.recommendation": "Preview executes read-only statements only.", + "diagnostics.previewSafety.blocked": "Current SQL is mutating and blocked by Safe Preview mode.", + "diagnostics.previewSafety.ok": "Preview safety checks passed.", + "diagnostics.previewExecution.name": "Preview Execution", + "diagnostics.previewExecution.recommendation": "Run preview and inspect diagnostics for execution/runtime errors.", + "diagnostics.previewExecution.failed": "Preview execution failed.", + "diagnostics.previewExecution.cancelled": "Preview execution was cancelled.", + "diagnostics.previewExecution.done": "{0} row(s) in {1}ms.", + "diagnostics.previewExecution.none": "No preview execution issues detected.", + "diagnostics.ddlCompilation.name": "DDL Compilation", + "diagnostics.ddlCompilation.recommendation": "Fix DDL compile diagnostics before execution.", + "diagnostics.ddlCompilation.failed": "DDL compilation failed.", + "diagnostics.ddlCompilation.warningCount": "{0} warning(s) reported by DDL compiler.", + "diagnostics.ddlCompilation.ok": "DDL compilation succeeded.", + "diagnostics.ddlOutput.name": "DDL Output", + "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", + "diagnostics.ddlOutput.none": "No DDL statements generated yet.", + "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", + "diagnostics.undo.name": "Undo History", + "diagnostics.undo.recommendation": "History is in-memory only; save your canvas regularly.", + "diagnostics.undo.saved": "Canvas is saved (no unsaved changes).", + "diagnostics.undo.unsavedDeep": "Unsaved changes with {0} undo steps.", + "diagnostics.undo.unsaved": "Unsaved changes - {0} undo step(s) available.", + "diagnostics.report.title": "AkkornStudio - Diagnostic Report", + "diagnostics.report.generated": "Generated", + "diagnostics.report.overall": "Overall", + "diagnostics.report.details": "Details", + "diagnostics.report.recommendation": "Recommendation", + "diagnostics.report.lastCheck": "Last Check", + "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", + "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", + "preview.providerLabel": "Provider", + "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", + "preview.schemaAnalysis.run": "Run Analysis", + "preview.schemaAnalysis.cancel": "Cancel", + "preview.schemaAnalysis.issues": "Issues", + "preview.schemaAnalysis.clearFilters": "Clear Filters", + "preview.schemaAnalysis.clearBlacklist": "Clear Blacklist", + "preview.schemaAnalysis.severity": "Severity", + "preview.schemaAnalysis.severity.info": "Info", + "preview.schemaAnalysis.severity.warning": "Warning", + "preview.schemaAnalysis.severity.critical": "Critical", + "preview.schemaAnalysis.rule": "Rule", + "preview.schemaAnalysis.rule.fkCatalogInconsistent": "FK catalog inconsistent", + "preview.schemaAnalysis.rule.missingFk": "Missing FK", + "preview.schemaAnalysis.rule.namingConventionViolation": "Naming convention violation", + "preview.schemaAnalysis.rule.lowSemanticName": "Low semantic name", + "preview.schemaAnalysis.rule.missingRequiredComment": "Missing required comment", + "preview.schemaAnalysis.rule.nf1HintMultiValued": "1NF hint: multi-valued", + "preview.schemaAnalysis.rule.nf2HintPartialDependency": "2NF hint: partial dependency", + "preview.schemaAnalysis.rule.nf3HintTransitiveDependency": "3NF hint: transitive dependency", + "preview.schemaAnalysis.minConfidence": "Min Confidence", + "preview.schemaAnalysis.tableFilter": "Table Filter", + "preview.schemaAnalysis.tableFilterWatermark": "schema.table", + "preview.schemaAnalysis.ignore": "Execution Filters", + "preview.schemaAnalysis.ignoreViews": "Ignore views and materialized views", + "preview.schemaAnalysis.blacklist": "Table blacklist", + "preview.schemaAnalysis.blacklistAdd": "Add", + "preview.schemaAnalysis.blacklistRemove": "Remove selected", + "preview.schemaAnalysis.ignoreTable.placeholder": "schema.table", + "preview.schemaAnalysis.details": "Details", + "preview.schemaAnalysis.evidence": "Evidence", + "preview.schemaAnalysis.suggestions": "Suggestions", + "preview.schemaAnalysis.ruleDiagnostics": "Rule Diagnostics", + "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", + "preview.schemaAnalysis.copySql": "Copy SQL", + "preview.schemaAnalysis.applyToCanvas": "Apply to Canvas", + "preview.schemaAnalysis.summary.issues": "Issues:", + "preview.schemaAnalysis.summary.rawPrefix": "(raw:", + "preview.schemaAnalysis.summary.critical": "| Critical:", + "preview.schemaAnalysis.summary.warning": "| Warning:", + "preview.schemaAnalysis.summary.info": "| Info:", + "preview.schemaAnalysis.state.metadataUnavailable": "Metadata unavailable for structural analysis.", + "preview.schemaAnalysis.state.cancelled": "Analysis cancelled by the user.", + "preview.schemaAnalysis.state.partialTimeout": "Analysis finished partially due to timeout.", + "preview.schemaAnalysis.state.failed": "Structural analysis failed.", + "preview.schemaAnalysis.state.empty": "No inferable structural issue was detected.", + "preview.schemaAnalysis.state.noFilterMatch": "No issue matches the selected filters.", + "preview.schemaAnalysis.state.noIssueSelected": "No issue selected.", + "preview.schemaAnalysis.state.noSqlCandidate": "No SQL candidate available.", + "preview.schemaAnalysis.actionBlockedTooltip": "Action unavailable for the current risk level or capability.", + "common.navigate": "Navigate", + "common.close": "Close", + "common.esc": "Esc", + "common.ms": "ms", + "common.zero": "0", + "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", + "nodesList.empty": "No nodes found", + "nodesList.emptyHint": "Adjust the search term to explore available types", + "schema.emptyFiltered": "No objects found for the current filter", + "start.lastSnapshot": "Last snapshot", + "app.brandBadge": "VS", + "property.tab.properties": "Properties", + "property.tab.projectSettings": "Project Settings", + "property.nodeType": "NODE TYPE", + "property.selectNodeHint": "Select a node to edit its properties.", + "property.namingConventions": "Naming Conventions", + "property.aliasConvention": "Alias convention", + "property.enforceAliasNaming": "Enforce alias naming", + "property.warnReservedSql": "Warn on reserved SQL keywords", + "property.maxAliasLength": "Max alias length", + "property.maxAliasLengthDefault": "64", + "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", + "start.tips": "Tips", + "start.tips.quick": "Quick tips", + "start.tips.item1": "1. Click New Diagram to start from scratch.", + "start.tips.item2": "2. Use templates to speed up prototyping.", + "start.tips.item3": "3. Open saved connections to load real tables.", + "start.tips.shortcut": "Shortcut: CTRL+SHIFT+P opens the command palette.", + "start.workspace": "WORKSPACE", + "start.resumeTitle": "Continue where you left off", + "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", + "start.chip.quickFlow": "Quick flow", + "start.chip.templates": "Templates", + "start.chip.connections": "Connections", + "start.savedConnectionsTitle": "Saved Connections", + "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", + "start.noConnectionsTitle": "No connections configured yet", + "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", + "start.newConnection": "+ New Connection", + "start.recentProjectsTitle": "Recent Projects", + "start.searchRecent": "Search recent project...", + "start.quickActions": "Quick actions", + "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", + "start.noRecentTitle": "No recent projects yet", + "start.noRecentSubtitle": "Use the quick actions card above to get started.", + "start.exploreTemplates": "Explore templates", + "start.templatesFavoritesHint": "Favorites on top", + "start.favoriteTemplate": "Favorite template", + "node.columnSetPreview": "ColumnSet preview", + "node.view": "VIEW", + "node.tableDefinition": "Table Definition", + "node.join": "JOIN", + "node.window.addPartition": "Add PARTITION BY slot", + "node.window.removePartition": "Remove PARTITION BY slot", + "node.window.addOrder": "Add ORDER BY slot", + "node.window.removeOrder": "Remove ORDER BY slot", + "sql.keyword.select": "SELECT", + "sql.keyword.from": "FROM", + "sql.keyword.join": "JOIN", + "sql.keyword.where": "WHERE", + "sql.keyword.limit": "LIMIT", + "sqlImporter.close": "Close SQL importer", + "sqlImporter.report.imported": "Imported", + "sqlImporter.report.partial": "Partial", + "sqlImporter.report.skipped": "Skipped", + "sqlImporter.confirmClearCanvas": "SQL import will clear the current canvas. Do you want to continue?", + "sqlImporter.confirmProceed": "Continue import", + "benchmark.close": "Close benchmark", + "benchmark.p95": "P95", + "benchmark.n": "N", + "liveSql.safePreview": "SAFE PREVIEW MODE", + "liveSql.title": "LIVE SQL", + "liveSql.blocked": "BLOCKED", + "liveSql.copy": "Copy", + "liveSql.format": "Format", + "liveSql.benchmark": "Benchmark", + "liveSql.explain": "Explain", + "liveSql.actionsHint": "Performance tools", + "ddl.dialog.title": "Execute DDL", + "ddl.dialog.execute": "Execute", + "ddl.dialog.cancel": "Cancel", + "ddl.dialog.close": "Close", + "ddl.dialog.stopOnError": "Stop on first failure", + "ddl.dialog.confirmDestructive": "I confirm execution of destructive statements (DROP TABLE)", + "ddl.dialog.reviewBeforeRun": "Review the DDL script before confirming.", + "ddl.dialog.confirmQuestion": "Confirm DDL execution on the connected database?", + "ddl.dialog.irreversibleWarning": "This action can change the schema irreversibly.", + "ddl.dialog.mustConfirmDestructive": "Confirm destructive execution to continue.", + "ddl.dialog.executing": "Executing...", + "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", + "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", + "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", + "ddl.execute.result.failed": "Failed to execute DDL.", + "ddl.execute.result.cancelled": "Execution cancelled by the user.", + "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", + "context.deleteSingle": "Delete {0}", + "context.deleteMultiple": "Delete {0} nodes", + "context.bringForward": "Bring Forward (Ctrl+PgUp)", + "context.sendBackward": "Send Backward (Ctrl+PgDown)", + "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", + "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", + "context.normalizeLayers": "Normalize Layers", + "context.deleteWire": "Delete wire", + "context.addNode": "Add Node (Shift+A)", + "context.undoWithDescription": "Undo {0}", + "context.redo": "Redo", + "shortcuts.windowTitle": "Keyboard Shortcuts", + "shortcuts.headerTitle": "AkkornStudio - Shortcuts", + "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", + "shortcuts.filterWatermark": "Filter shortcuts by key or action...", + "shortcuts.resultCount": "{0} shortcuts", + "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", + "shortcuts.noneFound": "No shortcuts found.", + "shortcuts.section.fileGeneral": "File and General", + "shortcuts.section.editing": "Editing", + "shortcuts.section.canvasNavigation": "Canvas and Navigation", + "shortcuts.section.zoomPanPrecision": "Zoom, pan and precision", + "shortcuts.section.previewInspection": "Preview and Inspection", + "shortcuts.key.deleteOrBackspace": "Del or Backspace", + "shortcuts.key.middleDrag": "Middle mouse + drag", + "shortcuts.key.rightDrag": "Right mouse + drag", + "shortcuts.key.spaceDrag": "Space + drag", + "shortcuts.key.altLeftDrag": "Alt + left drag", + "shortcuts.key.arrows": "Arrows", + "shortcuts.key.shiftArrows": "Shift + Arrows", + "shortcuts.action.openShortcutScreen": "Open this shortcuts screen", + "shortcuts.action.newCanvas": "New canvas", + "shortcuts.action.openFile": "Open file", + "shortcuts.action.save": "Save", + "shortcuts.action.saveAs": "Save as", + "shortcuts.action.commandPalette": "Command Palette", + "shortcuts.action.undo": "Undo", + "shortcuts.action.redo": "Redo", + "shortcuts.action.selectAll": "Select all", + "shortcuts.action.deleteSelection": "Delete selection", + "shortcuts.action.closeOverlayCancel": "Close overlays / cancel actions", + "shortcuts.action.openNodeSearch": "Open node search", + "shortcuts.action.resetViewport": "Reset viewport", + "shortcuts.action.centerSelection": "Center selection", + "shortcuts.action.fitSelection": "Fit selection", + "shortcuts.action.autoLayout": "Auto Layout", + "shortcuts.action.toggleSnapToGrid": "Toggle Snap to Grid", + "shortcuts.action.bringForward": "Bring Forward", + "shortcuts.action.sendBackward": "Send Backward", + "shortcuts.action.bringToFront": "Bring to Front", + "shortcuts.action.sendToBack": "Send to Back", + "shortcuts.action.zoomInOut": "Zoom in / out", + "shortcuts.action.pan": "Pan", + "shortcuts.action.temporaryPan": "Temporary pan", + "shortcuts.action.alternatePan": "Alternate pan", + "shortcuts.action.fineNudge": "Fine nudge selection", + "shortcuts.action.fastNudge": "Fast nudge", + "shortcuts.action.togglePreview": "Toggle data preview", + "shortcuts.action.explainPlan": "Explain plan", + "shortcuts.action.runPreview": "Run preview", + "shortcuts.action.connectionManager": "Connection manager", + "shortcuts.action.flowVersionHistory": "Flow version history", + "shortcuts.resetAll": "Reset all", + "shortcuts.customized": "Customized", + "shortcuts.default": "Default", + "shortcuts.apply": "Apply", + "shortcuts.reset": "Reset", + "shortcuts.status.resetAllSuccess": "All shortcuts reset to defaults.", + "shortcuts.status.updated": "Shortcut updated.", + "shortcuts.status.reset": "Shortcut reset to default.", + "shortcuts.status.updateFailed": "Unable to update shortcut.", + "toast.severity.success": "Success", + "toast.severity.warning": "Warning", + "toast.severity.error": "Error", + "toast.details.success": "Success Details", + "toast.details.warning": "Warning Details", + "toast.details.error": "Error Details", + "diagnostics.area.cteEditor": "CTE Editor", + "diagnostics.area.viewEditor": "View Editor", + "diagnostics.area.subEditor": "Sub-editor", + "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", + "diagnostics.recommendation.reloadFileIfNeeded": "Reload the file if needed.", + "diagnostics.viewEditor.exitFailed": "Could not exit: {0}", + "diagnostics.viewEditor.canvasIncomplete": "the canvas is incomplete.", + "diagnostics.viewEditor.exitRecommendation": "Connect a valid ResultOutput or use the discard command.", + "diagnostics.viewEditor.restoreParentFailed": "Failed to restore the parent canvas. The subgraph was discarded.", + "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", + "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", + "diagnostics.canvasMigration.openWarning": "Open: {0}", + "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", + "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", + "diagnostics.recommendation.resaveLatestSchema": "Review diagnostics and re-save the canvas to persist the latest schema.", + "diagnostics.recommendation.saveMigratedSchema": "Review diagnostics and save the canvas to persist the migrated schema.", + "file.saveDialog.title": "Save Canvas", + "file.saveDialog.suggestedName": "Query1", + "file.save.success": "Canvas saved successfully.", + "file.save.failedWithReason": "Save failed: {0}", + "file.openDialog.title": "Open Canvas", + "file.openDialog.canvasType": "SQL Architect Canvas", + "file.open.failedWithReason": "Open failed: {0}", + "file.open.success": "Canvas opened successfully.", + "file.open.successWithWarnings": "Canvas opened with warnings.", + "session.restore.failedWithReason": "Restore failed: {0}", + "session.restore.successWithWarnings": "Session restored with warnings.", + "session.restore.success": "Session restored successfully.", + "export.documentation.dialogTitle": "Export Flow Documentation", + "export.documentation.success": "Documentation exported successfully.", + "export.documentation.failed": "Documentation export failed.", + "export.failed.pathPermissionsHint": "Check file path and permissions.", + "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", + "export.dialogTitleByExtension": "Export as {0}", + "export.success": "Export completed successfully.", + "export.failed": "Export failed.", + "fileHistory.currentFile.none": "No file selected", + "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", + "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", + "fileHistory.status.countAvailable": "{0} local version(s) available.", + "fileHistory.restore.failedWithReason": "Restore failed: {0}", + "fileHistory.restore.successFrom": "Restored version from {0}.", + "preview.status.cancelled": "Cancelled", + "preview.status.error": "Error", + "preview.status.ready": "Ready", + "preview.runningWithMs": "Running... {0}ms", + "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", + "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", + "explain.errorWithReason": "Explain plan error: {0}", + "explain.noSql": "No SQL to explain. Build a query on the canvas first.", + "ddl.compilationFailed": "Compilation failed", + "ddl.compileErrorWithReason": "DDL compile error: {0}", + "command.undo.name": "Undo", + "command.undo.description": "Undo last action", + "command.redo.name": "Redo", + "command.redo.description": "Redo last undone action", + "command.addNode.name": "Add Node", + "command.addNode.description": "Open node search menu to add a node", + "command.bringForward.name": "Bring Forward", + "command.bringForward.description": "Move selected nodes one layer forward", + "command.sendBackward.name": "Send Backward", + "command.sendBackward.description": "Move selected nodes one layer backward", + "command.bringToFront.name": "Bring to Front", + "command.bringToFront.description": "Move selected nodes to top layer", + "command.sendToBack.name": "Send to Back", + "command.sendToBack.description": "Move selected nodes to bottom layer", + "command.normalizeLayers.name": "Normalize Layers", + "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", + "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", + "main.orphanSuffix": "Orphan(s)", + "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", + "main.namingPrefix": "Naming", + "fileHistory.compressedLabel": "Compressed:", + "schema.itemsSuffix": "item(s)", + "property.panel.title": "Properties", + "property.panel.multiSelected": "{0} nodes selected", + "sqlImporter.status.pasteSelect": "Paste a SELECT statement above, then click Import.", + "sqlImporter.status.inputTooLarge": "SQL input is too large ({0:N0} chars). Limit is {1:N0}. Split the query or increase the import limit.", + "sqlImporter.status.parsing": "Parsing SQL...", + "sqlImporter.status.done": "Done - {0} imported, {1} partial, {2} skipped.", + "sqlImporter.status.cancelledByUser": "Import cancelled by user.", + "sqlImporter.status.timeout": "Import timed out after {0:0.#}s. Try a smaller query or increase timeout.", + "sqlImporter.status.parseError": "Parse error: {0}", + "sqlImporter.status.clearConfirmationRequired": "SQL import will clear the current canvas. Confirm to continue.", + "sqlImporter.status.clearConfirmationCancelled": "Import cancelled. The current canvas was kept.", + "diagnostics.area.undoRedoTransaction": "Undo/Redo Transaction", + "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", + "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", + "node.preview.noCatalog": "No catalog available", + "connection.error.searchMenuNotInitialized": "search menu not initialized", + "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", + "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", + "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", + "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", + "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", + "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", + "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", + "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", + "diagnostics.area.connection": "Connection", + "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", + "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", + "undoRedo.transaction.unnamed": "unnamed transaction", + "benchmark.runLabelDefault": "Run 1", + "benchmark.runLabelPattern": "Run {0}", + "benchmark.status.failedWithReason": "Benchmark failed: {0}", + "benchmark.status.noSql": "No SQL to benchmark - build a query first.", + "benchmark.status.warmupProgress": "Warm-up {0}/{1}...", + "benchmark.status.iterationProgress": "Iteration {0}/{1}...", + "benchmark.status.done": "Done - {0}", + "benchmark.status.cancelled": "Benchmark cancelled.", + "app.windowTitle": "AkkornStudio", + "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", + "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", + "sqlImporter.error.selectFromNotFound": "Could not find SELECT ... FROM in the query.", + "sqlImporter.error.fromClauseParseFailed": "Could not parse FROM clause.", + "sqlImporter.error.syntaxUnterminatedString": "Syntax error at line {0}, column {1}: unterminated string literal.", + "sqlImporter.error.missingClosingParenthesis": "missing closing ')'", + "sqlImporter.error.unexpectedClosingParenthesis": "unexpected ')'", + "sqlImporter.error.syntaxAtLineColumn": "Syntax error at line {0}, column {1}: {2}.", + "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", + "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", + "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", + "errorDiagnostics.connection.label": "Connection failed", + "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", + "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", + "errorDiagnostics.authorization.label": "Authorization error", + "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", + "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", + "errorDiagnostics.timeout.label": "Query timeout", + "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", + "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", + "errorDiagnostics.schema.label": "Schema error", + "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", + "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", + "errorDiagnostics.syntax.label": "SQL syntax error", + "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", + "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", + "errorDiagnostics.compatibility.label": "Compatibility error", + "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", + "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", + "errorDiagnostics.unknown.label": "Unexpected error", + "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", + "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", + "error.mainWindow.invalidDataContext": "MainWindow DataContext must be a ShellViewModel.", + "error.mainWindow.canvasNotInitialized": "CanvasViewModel was not initialized.", + "error.mainWindow.ddlPreviewUnavailable": "DDL preview is unavailable for the current canvas.", + "themeJson.editor.template": "{\n \"meta\": { \"name\": \"Custom Theme\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", + "themeJson.error.pasteBeforeApply": "Paste a theme JSON before applying.", + "themeJson.error.invalidJson": "Invalid JSON: {0}", + "themeJson.error.emptyPayload": "Invalid JSON: empty payload.", + "themeJson.error.invalidTheme": "Invalid theme: {0}", + "themeJson.error.appliedButSaveFailed": "Theme applied, but failed to persist: {0}", + "themeJson.success.appliedAndSaved": "JSON theme applied and saved.", + "themeJson.success.customRemoved": "Custom theme removed. Restart the app to fully return to the default theme.", + "themeJson.error.restoreDefaultFailed": "Failed to restore default theme: {0}", + "themeValidator.error.configNull": "Theme config is null.", + "themeValidator.warning.noSections": "Theme has no colors or typography sections; nothing to apply.", + "themeValidator.warning.invalidColor": "{0} has invalid color '{1}'. This key will be ignored.", + "themeValidator.warning.sizeOutOfRange": "{0}={1} is out of range (8..48). This key will be ignored.", + "queryExecutor.error.openConnectionMethodNotFound": "Cannot find OpenConnectionAsync method on orchestrator", + "queryExecutor.error.openConnectionInvokeFailed": "Failed to invoke OpenConnectionAsync", + "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}': view SELECT cannot be reconstructed visually - edit it manually in the subcanvas.", + "ddlImporter.error.tableNotFoundInMetadata": "Table '{0}' was not found in current metadata.", + "main.window.untitled": "Untitled", + "main.subEditor.noSeedProvided": "No {0} sub-editor seed was provided.", + "main.layerOrder.bringToFront": "Bring to front", + "main.layerOrder.sendToBack": "Send to back", + "main.layerOrder.bringForward": "Bring forward", + "main.layerOrder.sendBackward": "Send backward", + "main.layerOrder.normalizeLayers": "Normalize layers", + "export.fileType.html": "HTML Files", + "export.fileType.json": "JSON Files", + "export.fileType.csv": "CSV Files", + "export.fileType.excel": "Excel Files", + "commandPalette.templatePrefix": "Template: {0}", + "themeLoader.status.notFoundWithPath": "Theme file not found: {0}", + "themeLoader.status.deserializedNull": "Theme JSON deserialized to null.", + "themeLoader.status.loaded": "Theme JSON loaded successfully.", + "credential.error.ciphertextTooShort": "Ciphertext blob is too short.", + "credential.error.dpapiWindowsOnly": "DPAPI is only available on Windows.", + "credential.warning.loadVaultFailed": "Failed to load credential vault '{0}': {1}", + "credential.warning.persistVaultFailed": "Failed to persist credential vault '{0}': {1}", + "snippetStore.warning.loadFailed": "Failed to load snippets from '{0}': {1}", + "snippetStore.warning.saveFailed": "Failed to save snippets: {0}", + "flowVersionStore.warning.loadFailed": "Failed to load flow versions from '{0}': {1}", + "flowVersionStore.warning.saveFailed": "Failed to save flow versions: {0}", + "queryExecutor.error.queryEmpty": "Query cannot be empty", + "queryExecutor.error.providerNotSupported": "Provider {0} is not supported", + "queryExecutor.error.singleStatementOnly": "Preview accepts a single SQL statement only.", + "queryExecutor.error.queryEmptyWithPeriod": "Query cannot be empty.", + "queryExecutor.error.readOnlyOnly": "Preview mode only supports read-only SQL statements.", + "queryExecutor.error.namedParametersNotSupported": "Preview mode does not support bound parameters in execution SQL. Inline safe literals or run the query outside preview.", + "queryExecutor.error.positionalParametersNotSupported": "Preview mode does not support positional parameter placeholders ('?' or '$1').", + "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "Align selected nodes to the bottom edge", + "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "Align selected nodes to the leftmost edge", + "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "Align selected nodes to the rightmost edge", + "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "Align selected nodes to the topmost edge", + "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "Apply CTE sub-canvas edits and return to the parent canvas", + "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "Arrange nodes into logical columns automatically", + "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "Centre selected nodes on a horizontal axis", + "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "Centre selected nodes on a vertical axis", + "commandPalette.description.clear_canvas_and_start_fresh": "Clear canvas and start fresh", + "commandPalette.description.clear_node_selection": "Clear node selection", + "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "Convert aliases to the convention configured in project settings", + "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "Create checkpoints, compare versions side-by-side and restore a previous canvas state", + "commandPalette.description.delete_the_selected_nodes": "Delete the selected nodes", + "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "Discard current sub-editor edits and return to the parent canvas", + "commandPalette.description.execute_the_current_query_in_preview": "Execute the current query in preview", + "commandPalette.description.fit_all_nodes_into_the_visible_area": "Fit all nodes into the visible area", + "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "Generate CSV file from the first CSV Export node", + "commandPalette.description.generate_html_file_from_the_first_html_export_node": "Generate HTML file from the first HTML Export node", + "commandPalette.description.generate_json_file_from_the_first_json_export_node": "Generate JSON file from the first JSON Export node", + "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "Generate XLSX workbook from the first Excel Export node", + "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "Inspect the query execution plan — see scan types, join strategies, and cost estimates", + "commandPalette.description.load_a_vsaq_canvas_file": "Load a .vsaq canvas file", + "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "Measure avg / median / p95 latency of the current SQL over N iterations", + "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "Open isolated sub-canvas editor for the selected CTE Definition node", + "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "Open local file version history created on each save and restore previous saved snapshots", + "commandPalette.description.open_output_preview_modal_for_the_active_mode": "Open output preview modal for the active mode", + "commandPalette.description.open_shortcut_reference_screen": "Open shortcut reference screen", + "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "Open the connection manager to add, edit or switch database connections", + "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "Paste a SELECT statement and generate nodes automatically — FROM, JOIN, WHERE, LIMIT are supported", + "commandPalette.description.remove_all_nodes_not_connected_to_output": "Remove all nodes not connected to output", + "commandPalette.description.reset_zoom_and_pan_to_default": "Reset zoom and pan to default", + "commandPalette.description.save_canvas_to_a_new_file": "Save canvas to a new file", + "commandPalette.description.save_current_canvas": "Save current canvas", + "commandPalette.description.save_markdown_documentation_of_the_current_flow": "Save Markdown documentation of the current flow", + "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "Save the selected nodes as a reusable snippet — insert it later via the node search menu (⇧A)", + "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "Scan all table-source nodes on the canvas for possible join relationships based on FK conventions and naming patterns", + "commandPalette.description.select_all_nodes_on_canvas": "Select all nodes on canvas", + "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "Snap node positions to 16px grid (Ctrl+G)", + "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "Spread selected nodes with equal horizontal spacing", + "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "Spread selected nodes with equal vertical spacing", + "commandPalette.description.zoom_into_the_canvas": "Zoom into the canvas", + "commandPalette.description.zoom_out_of_the_canvas": "Zoom out of the canvas", + "commandPalette.name.align_bottom": "Align Bottom", + "commandPalette.name.align_left": "Align Left", + "commandPalette.name.align_right": "Align Right", + "commandPalette.name.align_top": "Align Top", + "commandPalette.name.analyze_all_joins": "Analyze All Joins", + "commandPalette.name.auto_fix_naming": "Auto-Fix Naming", + "commandPalette.name.auto_layout": "Auto Layout", + "commandPalette.name.center_horizontally": "Center Horizontally", + "commandPalette.name.center_vertically": "Center Vertically", + "commandPalette.name.cleanup_orphans": "Cleanup Orphans", + "commandPalette.name.delete_selected": "Delete Selected", + "commandPalette.name.deselect_all": "Deselect All", + "commandPalette.name.discard_and_exit_editor": "Discard and Exit Editor", + "commandPalette.name.distribute_horizontally": "Distribute Horizontally", + "commandPalette.name.distribute_vertically": "Distribute Vertically", + "commandPalette.name.edit_selected_cte": "Edit Selected CTE", + "commandPalette.name.exit_cte_editor": "Exit CTE Editor", + "commandPalette.name.explain_plan": "Explain Plan", + "commandPalette.name.export_csv": "Export CSV", + "commandPalette.name.export_documentation": "Export Documentation", + "commandPalette.name.export_excel": "Export Excel", + "commandPalette.name.export_html": "Export HTML", + "commandPalette.name.export_json": "Export JSON", + "commandPalette.name.file_save_load_history": "File Save/Load History", + "commandPalette.name.fit_to_screen": "Fit to Screen", + "commandPalette.name.flow_version_history": "Flow Version History", + "commandPalette.name.import_sql_to_graph": "Import SQL to Graph", + "commandPalette.name.keyboard_shortcuts": "Keyboard Shortcuts", + "commandPalette.name.manage_connections": "Manage Connections", + "commandPalette.name.new_canvas": "New Canvas", + "commandPalette.name.open_file": "Open File", + "commandPalette.name.reset_viewport": "Reset Viewport", + "commandPalette.name.run_preview": "Run Preview", + "commandPalette.name.run_query_benchmark": "Run Query Benchmark", + "commandPalette.name.save": "Save", + "commandPalette.name.save_as": "Save As", + "commandPalette.name.save_selection_as_snippet": "Save Selection as Snippet", + "commandPalette.name.select_all": "Select All", + "commandPalette.name.toggle_preview": "Toggle Preview", + "commandPalette.name.toggle_snap_to_grid": "Toggle Snap to Grid", + "commandPalette.name.zoom_in": "Zoom In", + "commandPalette.name.zoom_out": "Zoom Out", + "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", + "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", + "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", + "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", + "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", + "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", + "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", + "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", + "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", + "commandPalette.tags.clear_selection": "clear selection", + "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", + "commandPalette.tags.create_insert_search_transform": "create insert search transform", + "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", + "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", + "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", + "commandPalette.tags.data_results_table_panel": "data results table panel", + "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", + "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", + "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", + "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", + "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", + "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", + "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", + "commandPalette.tags.export_json_file_output_save": "export json file output save", + "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", + "commandPalette.tags.export_persist_copy": "export persist copy", + "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", + "commandPalette.tags.forward_history": "forward history", + "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", + "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", + "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", + "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", + "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", + "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", + "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", + "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", + "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", + "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", + "commandPalette.tags.load_import_vsaq": "load import vsaq", + "commandPalette.tags.magnify_enlarge": "magnify enlarge", + "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", + "commandPalette.tags.persist_write_disk": "persist write disk", + "commandPalette.tags.remove_erase_nodes": "remove erase nodes", + "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", + "commandPalette.tags.reset_clear_blank": "reset clear blank", + "commandPalette.tags.revert_back_history": "revert back history", + "commandPalette.tags.shrink_reduce": "shrink reduce", + "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", + "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", + "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", + "sqlEditor.diffPreview.title": "Transactional Diff Preview", + "sqlEditor.mutation.confirmExecute": "Confirm Execute", + "sqlEditor.tab.closeAnyway": "Close Anyway", + "sqlEditor.tab.keepTab": "Keep Tab", + "sqlEditor.status.ready": "Ready.", + "sqlEditor.telemetry.none": "No execution telemetry yet.", + "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", + "sqlEditor.telemetry.errors.none": "No aggregated errors.", + "sqlEditor.diff.none": "No transactional diff preview available.", + "sqlEditor.mutation.estimate.none": "No mutation estimate available.", + "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", + "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", + "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", + "sqlEditor.tab.noPendingClose": "No tab close pending.", + "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", + "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", + "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", + "sqlEditor.message.empty": "Execute a statement to see messages.", + "sqlEditor.message.success": "Execution completed successfully.", + "sqlEditor.result.summary.empty": "Rows: - Time: -", + "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", + "sqlEditor.file.save.canceled": "Save canceled.", + "sqlEditor.file.save.noPath": "No target path selected.", + "sqlEditor.file.save.success": "SQL file saved.", + "sqlEditor.file.save.failed": "Save failed.", + "sqlEditor.file.open.failed": "Open failed.", + "sqlEditor.file.open.notFound": "Selected SQL file was not found.", + "sqlEditor.file.open.success": "SQL file opened.", + "sqlEditor.status.executing": "Executing SQL...", + "sqlEditor.status.executingScript": "Executing SQL script...", + "sqlEditor.status.executingStep": "Executing {0}/{1}...", + "sqlEditor.status.canceling": "Canceling execution...", + "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", + "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", + "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", + "sqlEditor.status.success": "Execution succeeded.", + "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", + "sqlEditor.status.canceled": "Execution canceled.", + "sqlEditor.status.failed": "Execution failed.", + "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", + "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", + "sqlEditor.result.tabTitle": "Result {0}", + "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", + "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", + "sqlEditor.tab.closed": "Tab closed.", + "sqlEditor.tab.closeCanceled": "Tab close canceled.", + "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", + "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", + "sqlEditor.error.noConnection": "No active database connection for SQL execution.", + "sqlEditor.error.executionCanceled": "SQL execution was canceled.", + "sqlEditor.tab.scriptTitle": "Script {0}", + "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", + "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", + "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", + "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", + "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", + "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", + "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", + "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", + "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", + "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", + "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", + "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", + "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", + "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", + "sqlEditor.results.title": "Results", + "sqlEditor.results.filterWatermark": "Filter results", + "sqlEditor.results.rows.countZero": "0 rows", + "sqlEditor.results.rows.countSingle": "{0} rows", + "sqlEditor.results.rows.countFiltered": "{0} of {1} rows", + "sqlEditor.results.context.copyCell": "Copy Cell", + "sqlEditor.results.context.copyRow": "Copy Row", + "sqlEditor.results.context.hideColumn": "Hide Column", + "sqlEditor.results.context.showAllColumns": "Show All Columns", + "sqlEditor.sidebar.messages": "MESSAGES", + "sqlEditor.sidebar.history": "HISTORY", + "sqlEditor.sidebar.connection.none": "No connection", + "sqlEditor.sidebar.connection.connectHint": "Connect to load metadata and execute queries.", + "sqlEditor.sidebar.connection.connect": "Connect", + "sqlEditor.sidebar.schema.searchWatermark": "Search table or column", + "sqlEditor.sidebar.schema.reloadTooltip": "Reload metadata", + "sqlEditor.sidebar.schema.connectHint": "Connect to view the full schema.", + "sqlEditor.history.searchWatermark": "Search history", + "sqlEditor.history.navigationHint": "Arrow keys navigate history, Enter runs selected, Esc clears the search.", + "sqlEditor.history.noSearchResults": "No items found for this search.", + "sqlEditor.history.use": "Use", + "sqlEditor.history.copy": "Copy", + "sqlEditor.history.run": "Run", + "sqlEditor.history.copiedFromHistory": "SQL copied from history.", + "sqlEditor.toast.scriptSuccessTitle": "Script executed.", + "sqlEditor.toast.scriptSuccessDetail": "{0} statement(s) executed successfully.", + "sqlEditor.toast.scriptWarningTitle": "Script executed with failures.", + "sqlEditor.toast.scriptWarningDetail": "{0} of {1} statement(s) failed.", + "sqlEditor.toast.resultErrorTitle": "Failed to execute statement.", + "sqlEditor.toast.resultSuccessTitle": "Execution completed successfully.", + "sqlEditor.export.action": "Export Report", + "sqlEditor.openSql.pickerTitle": "Open SQL File", + "sqlEditor.saveSql.fileType": "SQL Files", + "sqlEditor.saveSql.pickerTitle": "Save SQL File", + "sqlEditor.export.pickerTitle": "Export SQL Data", + "sqlEditor.export.status.noResultTitle": "No execution result available for export.", + "sqlEditor.export.status.noResultDetail": "Execute a query first.", + "sqlEditor.export.status.successTitle": "Report exported.", + "sqlEditor.export.status.failedTitle": "Failed to export report.", + "sqlEditor.export.fileType.html": "HTML File", + "sqlEditor.export.fileType.json": "JSON File", + "sqlEditor.export.fileType.csv": "CSV File", + "sqlEditor.export.fileType.xlsx": "Excel Workbook", + "sqlEditor.export.defaultFileBase": "report", + "sqlEditor.export.defaultTitle": "SQL Report", + "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", + "sqlEditor.export.type.html.title": "HTML full-feature report", + "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", + "sqlEditor.export.type.json.title": "JSON execution contract", + "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", + "sqlEditor.export.type.csv.title": "CSV data export", + "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", + "sqlEditor.export.type.xlsx.title": "Excel workbook export", + "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", + "sqlEditor.export.dialog.windowTitle": "Export SQL Data", + "sqlEditor.export.dialog.title": "Export SQL Data", + "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", + "sqlEditor.export.dialog.confirm": "Export", + "sqlEditor.export.dialog.fileNameWatermark": "report.html", + "sqlEditor.export.dialog.titleWatermark": "SQL Report", + "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", + "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", + "sqlEditor.export.dialog.section.fileName": "FILE NAME", + "sqlEditor.export.dialog.section.reportTitle": "TITLE", + "sqlEditor.export.dialog.section.description": "DESCRIPTION", + "sqlEditor.export.dialog.section.options": "OPTIONS", + "sqlEditor.export.option.includeSchema": "Include output schema", + "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", + "sqlEditor.export.option.includeMetadata": "Include optional metadata", + "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", + "sqlEditor.export.badge.offline": "OFFLINE READY", + "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", + "sqlEditor.export.badge.dataOnly": "DATA ONLY" +} diff --git a/src/DBWeaver.UI/Assets/Localization/es-ES.json b/src/AkkornStudio.UI/Assets/Localization/es-ES.json similarity index 98% rename from src/DBWeaver.UI/Assets/Localization/es-ES.json rename to src/AkkornStudio.UI/Assets/Localization/es-ES.json index ec12deee..420e29ee 100644 --- a/src/DBWeaver.UI/Assets/Localization/es-ES.json +++ b/src/AkkornStudio.UI/Assets/Localization/es-ES.json @@ -1,1105 +1,1105 @@ -{ - "main.brand": "DBWeaver", - "main.tab.query1": "Query 1", - "main.new": "New", - "main.open": "Open", - "main.save": "Save", - "main.history": "History", - "main.layout": "Layout", - "main.preview": "Preview", - "main.undo": "Undo", - "main.redo": "Redo", - "main.cleanupOrphans": "Cleanup orphan nodes", - "main.autoFixAliasNaming": "Auto-fix alias naming", - "main.autoLayoutCanvas": "Auto layout canvas", - "main.toggleDataPreview": "Toggle data preview", - "main.language": "Language", - "main.restore.prompt": "Previous session found — restore the last canvas?", - "main.restore.button": "Restore session", - "main.cteEditor.editingPrefix": "Editing CTE: ", - "main.cteEditor.backToCanvas": "Back to Canvas", - "main.cteEditor.exitA11y": "Exit CTE editor", - "main.viewEditor.editingPrefix": "DDL > View: ", - "main.viewEditor.backToCanvas": "Back to DDL", - "main.viewEditor.exitA11y": "Exit view editor", - "connection.title": "Connection Manager", - "connection.subtitle": "Configure, pruebe y active conexiones sin salir del flujo", - "connection.none": "No connection", - "connection.active": "ACTIVE", - "connection.health.online": "Online", - "connection.health.degraded": "Degraded", - "connection.health.offline": "Offline", - "connection.tooltip.none": "No active connection — click to manage", - "connection.ping": "Ping", - "connection.saved": "SAVED CONNECTIONS", - "connection.new": "New Connection", - "connection.selectOrCreate": "Select a connection or create a new one", - "connection.name": "Connection Name", - "connection.provider": "Provider", - "connection.host": "Host", - "connection.port": "Port", - "connection.database": "Database", - "connection.sqlitePath": "SQLite Path", - "connection.sqliteBrowse": "Browse", - "connection.sqliteCreate": "Create", - "connection.username": "Username", - "connection.password": "Password", - "connection.timeout": "Timeout (seconds)", - "connection.test": "Test", - "connection.save": "Save", - "connection.connect": "Connect", - "connection.action.testConnection": "Test connection", - "connection.action.saveConnection": "Save connection", - "connection.action.connectConnection": "Connect connection", - "connection.status.connecting": "Conectando...", - "connection.status.connected": "Conectado", - "connection.status.testing": "Probando...", - "connection.status.failedPrefix": "Falló la conexión", - "connection.status.metadataUnavailable": "Falló la conexión: metadatos no disponibles.", - "connection.status.highLatency": "alta latencia", - "connection.watermark.name": "Mi BD de Producción", - "connection.watermark.host": "localhost", - "connection.watermark.port": "5432", - "connection.watermark.database": "database_name", - "connection.watermark.username": "user", - "connection.watermark.password": "••••••••", - "connection.watermark.timeout": "30", - "main.connectingDb": "Connecting to database...", - "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", - "status.nodesSeparator": " nodes · ", - "status.connectionsSuffix": " connections", - "status.undo": "Undo: ", - "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", - "connection.disconnect": "Disconnect", - "connection.action.disconnectConnection": "Disconnect connection", - "connectionTab.active": "ACTIVE CONNECTION", - "connectionTab.none": "No active connection", - "connectionTab.saved": "SAVED CONNECTIONS", - "connectionTab.new": "+ New Connection", - "schema.database": "DATABASE", - "schema.search": "Search tables, columns...", - "schema.loading": "Searching tables, columns...", - "schema.noConnection": "No Connection", - "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", - "schema.emptyNoTables": "No tables found", - "fileHistory.title": "Save/Load Version History", - "fileHistory.reload": "Reload", - "fileHistory.restoreSelected": "Restore selected", - "fileHistory.empty": "No local versions yet", - "fileHistory.emptyHint": "Save this file to generate version history.", - "preview.title": "Data Preview", - "preview.subtitle": "Review data and diagnostics before continuing", - "preview.run": "Run", - "preview.cancel": "Cancel", - "preview.tab.preview": "Preview", - "preview.tab.sql": "SQL", - "preview.close": "Close preview", - "preview.running": "Running preview query… ", - "preview.clickCancel": "Click Cancel to stop", - "preview.cancelled": "Query cancelled", - "preview.runAgain": "Press Run to execute again", - "preview.failed": "Preview execution failed", - "preview.technical": "TECHNICAL DETAILS", - "preview.noData": "No data yet", - "preview.f3Hint": "Press F3 or Space to run the current query", - "sqlImporter.title": "Import SQL to Graph", - "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", - "sqlImporter.sqlStatement": "SQL STATEMENT", - "sqlImporter.supported": "Supported: ", - "sqlImporter.import": "Import", - "sqlImporter.report": "CONVERSION REPORT", - "sqlEditor.mutation.dialogTitle": "Confirmacion de cambio", - "sqlEditor.mutation.dialogSubtitle": "Revisa el impacto antes de confirmar la ejecucion", - "search.empty": "No nodes found", - "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", - "search.shortcut": "⇧A", - "search.spawn": "Spawn", - "commandPalette.empty": "Ningún comando coincide con tu búsqueda", - "commandPalette.search": "Buscar comando", - "commandPalette.shortcut": "CTRL+SHIFT+P", - "commandPalette.execute": "Ejecutar", - "context.editCte": "Edit Selected CTE", - "context.editViewSubcanvas": "Edit View Subcanvas", - "explain.title": "Explain Plan", - "explain.sql": "SQL", - "explain.option.analyze": "Analizar", - "explain.option.buffers": "Buffers", - "explain.badge.simulated": "SIMULADO", - "explain.timing.planning": "Planificacion:", - "explain.timing.execution": "Ejecucion:", - "explain.section.snapshotComparison": "Comparacion de instantaneas", - "explain.section.indexRecommendations": "Recomendaciones de indices", - "explain.section.history": "Historial", - "explain.detail.estimated": "Estimadas", - "explain.detail.actual": "Actuales", - "explain.detail.error": "Error", - "explain.detail.time": "Tiempo", - "explain.detail.loops": "Bucles", - "explain.rerun": "Re-run", - "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", - "explain.running": "Running EXPLAIN…", - "explain.failed": "⚠ EXPLAIN failed", - "explain.noPlan": "No plan yet", - "explain.rerunHint": "Press Re-run to execute EXPLAIN", - "explain.header.operation": "OPERACIÓN", - "explain.header.cost": "COSTO", - "explain.header.rows": "FILAS", - "explain.header.alert": "ALERTA", - "explain.mode.list": "Lista", - "explain.mode.tree": "Arbol", - "explain.action.snapshot": "Guardar snapshot", - "explain.action.copyJson": "Copiar JSON", - "explain.action.copyText": "Copiar texto", - "explain.action.saveJson": "Guardar .json", - "explain.action.openDalibo": "Abrir en Dalibo", - "explain.legend.seqscan": "SEQ SCAN — full table read, no index", - "explain.legend.sort": "SORT — in-memory sort", - "explain.legend.hash": "HASH — hash join", - "explain.escClose": "Esc to close", - "flowVersion.title": "Flow Version History", - "flowVersion.subtitle": "Create checkpoints, compare versions and restore", - "flowVersion.watermark": "Checkpoint label (optional)…", - "flowVersion.saveCheckpoint": "Save Checkpoint", - "flowVersion.compareMode": "Compare Mode", - "flowVersion.selectBase": "Select BASE version (from):", - "flowVersion.clickAny": "Then click any version in the list below to compare.", - "flowVersion.noCheckpoints": "No checkpoints yet", - "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", - "flowVersion.restore": "Restore", - "flowVersion.diffResults": "Diff Results", - "benchmark.title": "Query Benchmark", - "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", - "benchmark.sql": "SQL being benchmarked", - "benchmark.runLabel": "Run label", - "benchmark.runLabelWatermark": "Run 1", - "benchmark.iterations": "Iterations (1–100)", - "benchmark.warmup": "Warm-up passes (0–10)", - "benchmark.interval": "Interval between runs (ms)", - "benchmark.run": "Run Benchmark", - "benchmark.cancel": "Cancel", - "benchmark.clearHistory": "Clear History", - "benchmark.latest": "LATEST RESULT", - "benchmark.avg": "AVG", - "benchmark.median": "MEDIAN", - "benchmark.min": "MIN", - "benchmark.max": "MAX", - "benchmark.iterationsAt": " iterations · run at ", - "benchmark.history": "HISTORY", - "benchmark.header.label": "Etiqueta", - "benchmark.header.avg": "Prom", - "benchmark.header.median": "Mediana", - "benchmark.header.min": "Mín", - "benchmark.header.max": "Máx", - "benchmark.itersSuffix": " iters", - "diagnostics.title": "App Diagnostics", - "diagnostics.run": "Run", - "diagnostics.running": "Running checks…", - "diagnostics.ok": "OK", - "diagnostics.warning": "Warning", - "diagnostics.error": "Error", - "diagnostics.tooltip.rerun": "Re-run all checks", - "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", - "diagnostics.tooltip.close": "Close (Esc)", - "autoJoin.title": "Auto-Join Suggestions", - "autoJoin.titleForTable": "Auto-Join suggestions for {0}", - "autoJoin.acceptAll": "Accept All", - "autoJoin.accept": "Accept", - "autoJoin.skip": "Skip", - "autoJoin.allHandled": "All suggestions handled", - "autoJoin.joinKeyword": "JOIN", - "autoJoin.confidence.fkConstraint": "FK Constraint", - "autoJoin.confidence.fkReverse": "FK (Reverse)", - "autoJoin.confidence.namingMatch": "Naming Match", - "autoJoin.confidence.weakMatch": "Weak Match", - "autoJoin.runSelected": "Auto-Join Selected", - "autoJoin.noSimilarityTitle": "No automatic join found", - "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", - "autoJoin.appliedTitle": "Auto-join applied", - "autoJoin.manual.title": "Create Manual Join", - "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", - "autoJoin.manual.leftColumn": "Left column", - "autoJoin.manual.rightColumn": "Right column", - "autoJoin.manual.joinType": "Join type", - "autoJoin.manual.operator": "Operator", - "autoJoin.manual.confirm": "Create join", - "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", - "autoJoin.manualJoinCreatedTitle": "Manual join created", - "autoJoin.manualJoinFailedTitle": "Manual join could not be created", - "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", - "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", - "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", - "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", - "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", - "property.outputAlias": "OUTPUT ALIAS", - "property.sourceAlias": "SOURCE ALIAS", - "property.aliasWatermark": "e.g. MyColumn (optional)", - "property.parameters": "PARAMETERS", - "property.enabled": "Enabled", - "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", - "property.dateWatermark": "YYYY-MM-DD or leave empty", - "property.apply": "Apply", - "property.inputPins": "INPUT PINS", - "property.outputPins": "OUTPUT PINS", - "property.sqlTrace": "SQL TRACE", - "property.live": "live", - "node.numericValue": "Numeric Value", - "node.stringValue": "String Value", - "node.enterText": "Enter text", - "node.datetimeValue": "DateTime Value", - "node.valueLabel": "Value:", - "node.noInputs": "No inputs", - "node.loadingSample": "Loading sample…", - "node.previewFailed": "⚠ Preview failed", - "node.sampleRowsHint": "5 sample rows · demo data", - "sidebar.tab.nodes": "Nodos", - "sidebar.tab.connection": "Conexión", - "sidebar.tab.schema": "Esquema", - "sidebar.tab.diagnostics": "Diagnóstico", - "sidebar.addNode": "+ Add Node (⇧A)", - "sidebar.previewF3": "Preview (F3)", - "nodesList.search": "Search nodes...", - "search.watermark": "Search nodes… (Esc to close)", - "search.snippets": "★ SNIPPETS", - "commandPalette.watermark": "Ejecuta un comando… (Esc para cerrar)", - "tooltip.newCanvas": "New canvas (Ctrl+N)", - "tooltip.openCanvas": "Open canvas (Ctrl+O)", - "tooltip.saveCanvas": "Save canvas (Ctrl+S)", - "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", - "tooltip.zoomOut": "Zoom out (Ctrl+-)", - "tooltip.zoomIn": "Zoom in (Ctrl++)", - "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", - "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", - "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", - "tooltip.dataPreview": "Data preview (F3)", - "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", - "tooltip.appDiagnostics": "App Diagnostics (self-check)", - "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", - "tooltip.cancelRunningQuery": "Cancel the running query", - "tooltip.closeEsc": "Close (Esc)", - "tooltip.recheckConnectionHealth": "Re-check connection health", - "tooltip.deleteConnection": "Delete connection", - "tooltip.testConnection": "Test connection", - "tooltip.saveConnection": "Save connection", - "tooltip.activateConnection": "Activate this connection", - "tooltip.toggleDataSamplePreview": "Toggle data sample preview", - "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", - "tooltip.copySql": "Copy SQL to clipboard", - "tooltip.formatSql": "Format SQL", - "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", - "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", - "tooltip.switchToQueryMode": "Switch to Query canvas", - "tooltip.switchToDdlMode": "Switch to DDL canvas", - "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", - "tooltip.pins.inputs": "Inputs", - "tooltip.pins.outputs": "Outputs", - "tooltip.pins.none": "None", - "tooltip.tableColumns": "Columns", - "tooltip.tableColumns.none": "No detailed columns", - "window.minimize": "Minimize window", - "window.maximizeRestore": "Maximize/restore window", - "window.close": "Close window", - "menu.newDiagram": "New diagram", - "menu.openFile": "Open file", - "menu.save": "Save", - "menu.fileHistory": "File history", - "menu.shortcuts": "Keyboard shortcuts", - "menu.settings": "Settings", - "menu.importDdlSchema": "Import DDL schema", - "menu.viewDdlSql": "View DDL SQL", - "menu.executeDdl": "Execute DDL", - "menu.backToStart": "Back to start", - "toast.ddlExecuteFailed": "Failed to execute DDL.", - "toast.ddlOpenFailed": "Failed to open DDL SQL.", - "toast.ddlImportFailed": "Failed to import schema into DDL.", - "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", - "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", - "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", - "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", - "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", - "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", - "toast.ddlTableImported": "Table imported into the DDL canvas.", - "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", - "toast.ddlNoActiveConnection": "No active connection to execute DDL.", - "toast.ddlExecutedSuccess": "DDL executed successfully.", - "toast.ddlExecutedWithIssues": "DDL executed with issues.", - "toast.switchToDdl": "Switch to DDL mode to generate SQL.", - "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", - "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", - "toast.previewOpenFailed": "Failed to open preview.", - "tab.switchFailed": "Failed to switch tab: {0}", - "settings.status.darkApplied": "Dark theme applied.", - "settings.status.lightApplied": "Light theme applied.", - "settings.status.snapUpdated": "Snap updated: {0}.", - "settings.status.languageToggled": "Language toggled.", - "settings.status.languageSelected": "Language selected: {0}.", - "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", - "settings.section.appearance.title": "Themes", - "settings.section.languageRegion.title": "Idioma y región", - "settings.section.dateTime.title": "Fecha y hora", - "settings.section.keyboard.title": "Atajos de teclado", - "settings.section.privacy.title": "Privacidad", - "settings.section.notification.title": "Notificaciones", - "settings.section.accessibility.title": "Accesibilidad", - "settings.section.default.title": "Configuración", - "settings.section.appearance.subtitle": "Elige tu estilo o personaliza tu tema", - "settings.section.languageRegion.subtitle": "Administra idioma y formato regional", - "settings.section.keyboard.subtitle": "Personaliza los atajos de teclado usados por la command palette y la ejecución del canvas.", - "settings.section.wip.subtitle": "En progreso.", - "settings.section.default.subtitle": "Configuración de la aplicación", - "settings.general": "General", - "settings.nav.appearance": "Appearance", - "settings.theme.light": "Modo claro", - "settings.theme.dark": "Modo oscuro", - "settings.theme.system": "Preferencias del sistema", - "settings.gridSnap.title": "Grid Snap", - "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", - "settings.language.subtitle": "Seleccione el idioma de la aplicación.", - "settings.language.toggle": "Cambiar idioma", - "settings.language.option.ptBR": "Portugués (Brasil)", - "settings.language.option.enUS": "Inglés (Estados Unidos)", - "settings.language.option.esES": "Español (España)", - "settings.language.option.ruRU": "Ruso", - "settings.language.option.jaJP": "Japonés", - "settings.language.option.zhTW": "Chino tradicional", - "settings.themeJson.title": "JSON del tema", - "settings.themeJson.subtitle": "Pega tu JSON de tema, aplícalo y persístelo al instante.", - "settings.themeJson.apply": "Aplicar JSON", - "settings.themeJson.restoreDefault": "Restaurar tema predeterminado", - "mode.query": "Query", - "mode.ddl": "DDL", - "sidebar.left.close": "Close left sidebar", - "sidebar.left.open": "Reopen left sidebar", - "sidebar.right.close": "Close right sidebar", - "sidebar.right.open": "Reopen right sidebar", - "connection.completedTitle": "Connection completed", - "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", - "connection.close": "Close connection manager", - "connection.refreshHealth": "Refresh connection health", - "common.details": "Details", - "common.cancel": "Cancel", - "common.keep": "Keep", - "common.clear": "Clear", - "zoom.out": "Zoom out", - "zoom.in": "Zoom in", - "zoom.fit": "Fit zoom to screen", - "zoom.level": "Zoom level", - "settings.theme.mode": "Modo de tema", - "diagnostics.category.canvas": "Canvas Integrity", - "diagnostics.category.output": "Output & Execution", - "diagnostics.category.session": "Session & Safety", - "diagnostics.category.notice": "Runtime Notices", - "diagnostics.summary.ok": "All systems OK", - "diagnostics.summary.warningCount": "{0} warning(s) detected", - "diagnostics.summary.errorCount": "{0} error(s) detected", - "diagnostics.canvasMigration": "Canvas Migration", - "diagnostics.recommendation.resaveFile": "Vuelve a guardar el archivo para actualizarlo a la última versión del esquema.", - "diagnostics.canvasState.name": "Canvas State", - "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", - "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", - "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", - "diagnostics.validation.name": "Errores de validación", - "diagnostics.validation.recommendation": "Corrige los nodos resaltados antes de ejecutar la vista previa de salida", - "diagnostics.validation.errorWithWarnings": "{0} error(es) y {1} advertencia(s) en el grafo", - "diagnostics.validation.warningOnly": "{0} advertencia(s) en el grafo", - "diagnostics.validation.none": "Sin problemas de validación", - "diagnostics.orphan.name": "Nodos huérfanos", - "diagnostics.orphan.recommendation": "Usa la acción de limpieza de huérfanos para remover nodos no usados", - "diagnostics.orphan.count": "{0} nodo(s) no conectado(s) a ninguna salida", - "diagnostics.orphan.none": "No se detectaron nodos huérfanos", - "diagnostics.naming.name": "Convenciones de nombres", - "diagnostics.naming.recommendation": "Usa autocorrección de alias cuando la conformidad sea menor a 100%", - "diagnostics.naming.conformance": "Conformidad de nombres: {0}%", - "diagnostics.naming.ok": "Todos los alias siguen las convenciones de nombres (100%)", - "diagnostics.queryCompilation.name": "Compilación SQL en vivo", - "diagnostics.queryCompilation.recommendation": "Revisa el diagnóstico SQL en la salida cuando se reporten errores/advertencias.", - "diagnostics.queryCompilation.errorFallback": "La compilación SQL en vivo reportó errores.", - "diagnostics.queryCompilation.warningCounts": "{0} elemento(s) de diagnóstico, {1} advertencia(s) de guardrail.", - "diagnostics.queryCompilation.ok": "SQL en vivo compilado sin diagnósticos.", - "diagnostics.previewSafety.name": "Seguridad de vista previa", - "diagnostics.previewSafety.recommendation": "La vista previa solo ejecuta sentencias de solo lectura.", - "diagnostics.previewSafety.blocked": "El SQL actual modifica datos y está bloqueado por el modo de vista previa segura.", - "diagnostics.previewSafety.ok": "Las verificaciones de seguridad de vista previa pasaron.", - "diagnostics.previewExecution.name": "Ejecución de vista previa", - "diagnostics.previewExecution.recommendation": "Ejecuta la vista previa e inspecciona diagnósticos por errores de ejecución/runtime.", - "diagnostics.previewExecution.failed": "La ejecución de vista previa falló.", - "diagnostics.previewExecution.cancelled": "La ejecución de vista previa fue cancelada.", - "diagnostics.previewExecution.done": "{0} fila(s) en {1}ms.", - "diagnostics.previewExecution.none": "No se detectaron problemas en la ejecución de vista previa.", - "diagnostics.ddlCompilation.name": "Compilación DDL", - "diagnostics.ddlCompilation.recommendation": "Corrige los diagnósticos de compilación DDL antes de ejecutar.", - "diagnostics.ddlCompilation.failed": "La compilación DDL falló.", - "diagnostics.ddlCompilation.warningCount": "{0} advertencia(s) reportada(s) por el compilador DDL.", - "diagnostics.ddlCompilation.ok": "Compilación DDL exitosa.", - "diagnostics.ddlOutput.name": "DDL Output", - "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", - "diagnostics.ddlOutput.none": "No DDL statements generated yet.", - "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", - "diagnostics.undo.name": "Historial de deshacer", - "diagnostics.undo.recommendation": "El historial solo está en memoria; guarda tu canvas regularmente.", - "diagnostics.undo.saved": "Canvas guardado (sin cambios sin guardar).", - "diagnostics.undo.unsavedDeep": "Cambios sin guardar con {0} pasos de deshacer.", - "diagnostics.undo.unsaved": "Cambios sin guardar - {0} paso(s) de deshacer disponibles.", - "diagnostics.report.title": "DBWeaver - Informe de diagnóstico", - "diagnostics.report.generated": "Generado", - "diagnostics.report.overall": "General", - "diagnostics.report.details": "Detalles", - "diagnostics.report.recommendation": "Recomendación", - "diagnostics.report.lastCheck": "Última verificación", - "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", - "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", - "preview.providerLabel": "Provider", - "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", - "preview.schemaAnalysis.run": "Run Analysis", - "preview.schemaAnalysis.cancel": "Cancel", - "preview.schemaAnalysis.issues": "Issues", - "preview.schemaAnalysis.clearFilters": "Clear Filters", - "preview.schemaAnalysis.clearBlacklist": "Clear Blacklist", - "preview.schemaAnalysis.severity": "Severity", - "preview.schemaAnalysis.severity.info": "Info", - "preview.schemaAnalysis.severity.warning": "Warning", - "preview.schemaAnalysis.severity.critical": "Critical", - "preview.schemaAnalysis.rule": "Rule", - "preview.schemaAnalysis.rule.fkCatalogInconsistent": "FK catalog inconsistent", - "preview.schemaAnalysis.rule.missingFk": "Missing FK", - "preview.schemaAnalysis.rule.namingConventionViolation": "Naming convention violation", - "preview.schemaAnalysis.rule.lowSemanticName": "Low semantic name", - "preview.schemaAnalysis.rule.missingRequiredComment": "Missing required comment", - "preview.schemaAnalysis.rule.nf1HintMultiValued": "1NF hint: multi-valued", - "preview.schemaAnalysis.rule.nf2HintPartialDependency": "2NF hint: partial dependency", - "preview.schemaAnalysis.rule.nf3HintTransitiveDependency": "3NF hint: transitive dependency", - "preview.schemaAnalysis.minConfidence": "Min Confidence", - "preview.schemaAnalysis.tableFilter": "Table Filter", - "preview.schemaAnalysis.tableFilterWatermark": "schema.table", - "preview.schemaAnalysis.ignore": "Execution Filters", - "preview.schemaAnalysis.ignoreViews": "Ignore views and materialized views", - "preview.schemaAnalysis.blacklist": "Table blacklist", - "preview.schemaAnalysis.blacklistAdd": "Add", - "preview.schemaAnalysis.blacklistRemove": "Remove selected", - "preview.schemaAnalysis.ignoreTable.placeholder": "schema.table", - "preview.schemaAnalysis.details": "Details", - "preview.schemaAnalysis.evidence": "Evidence", - "preview.schemaAnalysis.suggestions": "Suggestions", - "preview.schemaAnalysis.ruleDiagnostics": "Rule Diagnostics", - "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", - "preview.schemaAnalysis.copySql": "Copy SQL", - "preview.schemaAnalysis.applyToCanvas": "Apply to Canvas", - "preview.schemaAnalysis.summary.issues": "Issues:", - "preview.schemaAnalysis.summary.rawPrefix": "(raw:", - "preview.schemaAnalysis.summary.critical": "| Critical:", - "preview.schemaAnalysis.summary.warning": "| Warning:", - "preview.schemaAnalysis.summary.info": "| Info:", - "preview.schemaAnalysis.state.metadataUnavailable": "Metadata unavailable for structural analysis.", - "preview.schemaAnalysis.state.cancelled": "Analysis cancelled by the user.", - "preview.schemaAnalysis.state.partialTimeout": "Analysis finished partially due to timeout.", - "preview.schemaAnalysis.state.failed": "Structural analysis failed.", - "preview.schemaAnalysis.state.empty": "No inferable structural issue was detected.", - "preview.schemaAnalysis.state.noFilterMatch": "No issue matches the selected filters.", - "preview.schemaAnalysis.state.noIssueSelected": "No issue selected.", - "preview.schemaAnalysis.state.noSqlCandidate": "No SQL candidate available.", - "preview.schemaAnalysis.actionBlockedTooltip": "Action unavailable for the current risk level or capability.", - "common.navigate": "Navigate", - "common.close": "Close", - "common.esc": "Esc", - "common.ms": "ms", - "common.zero": "0", - "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", - "nodesList.empty": "No nodes found", - "nodesList.emptyHint": "Adjust the search term to explore available types", - "schema.emptyFiltered": "No objects found for the current filter", - "start.lastSnapshot": "Last snapshot", - "app.brandBadge": "VS", - "property.tab.properties": "Properties", - "property.tab.projectSettings": "Project Settings", - "property.nodeType": "NODE TYPE", - "property.selectNodeHint": "Select a node to edit its properties.", - "property.namingConventions": "Naming Conventions", - "property.aliasConvention": "Alias convention", - "property.enforceAliasNaming": "Enforce alias naming", - "property.warnReservedSql": "Warn on reserved SQL keywords", - "property.maxAliasLength": "Max alias length", - "property.maxAliasLengthDefault": "64", - "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", - "start.tips": "Tips", - "start.tips.quick": "Consejos rápidos", - "start.tips.item1": "1. Haz clic en Nuevo diagrama para empezar desde cero.", - "start.tips.item2": "2. Usa plantillas para acelerar el prototipado.", - "start.tips.item3": "3. Abre conexiones guardadas para cargar tablas reales.", - "start.tips.shortcut": "Atajo: CTRL+SHIFT+P abre la paleta de comandos.", - "start.workspace": "WORKSPACE", - "start.resumeTitle": "Continue where you left off", - "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", - "start.chip.quickFlow": "Quick flow", - "start.chip.templates": "Templates", - "start.chip.connections": "Connections", - "start.savedConnectionsTitle": "Saved Connections", - "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", - "start.noConnectionsTitle": "No connections configured yet", - "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", - "start.newConnection": "+ New Connection", - "start.recentProjectsTitle": "Recent Projects", - "start.searchRecent": "Search recent project...", - "start.quickActions": "Quick actions", - "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", - "start.noRecentTitle": "No recent projects yet", - "start.noRecentSubtitle": "Use the quick actions card above to get started.", - "start.exploreTemplates": "Explore templates", - "start.templatesFavoritesHint": "Favorites on top", - "start.favoriteTemplate": "Favorite template", - "node.columnSetPreview": "ColumnSet preview", - "node.view": "VIEW", - "node.tableDefinition": "Table Definition", - "node.join": "JOIN", - "node.window.addPartition": "Agregar ranura PARTITION BY", - "node.window.removePartition": "Remover ranura PARTITION BY", - "node.window.addOrder": "Agregar ranura ORDER BY", - "node.window.removeOrder": "Remover ranura ORDER BY", - "sql.keyword.select": "SELECT", - "sql.keyword.from": "FROM", - "sql.keyword.join": "JOIN", - "sql.keyword.where": "WHERE", - "sql.keyword.limit": "LIMIT", - "sqlImporter.close": "Close SQL importer", - "sqlImporter.report.imported": "Importado", - "sqlImporter.report.partial": "Parcial", - "sqlImporter.report.skipped": "Omitido", - "benchmark.close": "Close benchmark", - "benchmark.p95": "P95", - "benchmark.n": "N", - "liveSql.safePreview": "SAFE PREVIEW MODE", - "liveSql.title": "LIVE SQL", - "liveSql.blocked": "BLOCKED", - "liveSql.copy": "Copy", - "liveSql.format": "Format", - "liveSql.benchmark": "Benchmark", - "liveSql.explain": "Explain", - "liveSql.actionsHint": "Performance tools", - "ddl.dialog.title": "Ejecutar DDL", - "ddl.dialog.execute": "Ejecutar", - "ddl.dialog.cancel": "Cancelar", - "ddl.dialog.close": "Cerrar", - "ddl.dialog.stopOnError": "Detener en el primer fallo", - "ddl.dialog.confirmDestructive": "Confirmo la ejecución de sentencias destructivas (DROP TABLE)", - "ddl.dialog.reviewBeforeRun": "Revisa el script DDL antes de confirmar.", - "ddl.dialog.confirmQuestion": "¿Confirmar ejecución DDL en la base conectada?", - "ddl.dialog.irreversibleWarning": "Esta acción puede cambiar el esquema de forma irreversible.", - "ddl.dialog.mustConfirmDestructive": "Confirma la ejecución destructiva para continuar.", - "ddl.dialog.executing": "Ejecutando...", - "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", - "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", - "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", - "ddl.execute.result.failed": "Failed to execute DDL.", - "ddl.execute.result.cancelled": "Execution cancelled by the user.", - "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", - "context.deleteSingle": "Delete {0}", - "context.deleteMultiple": "Delete {0} nodes", - "context.bringForward": "Bring Forward (Ctrl+PgUp)", - "context.sendBackward": "Send Backward (Ctrl+PgDown)", - "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", - "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", - "context.normalizeLayers": "Normalize Layers", - "context.deleteWire": "Delete wire", - "context.addNode": "Add Node (Shift+A)", - "context.undoWithDescription": "Undo {0}", - "context.redo": "Redo", - "shortcuts.windowTitle": "Keyboard Shortcuts", - "shortcuts.headerTitle": "DBWeaver - Shortcuts", - "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", - "shortcuts.filterWatermark": "Filter shortcuts by key or action...", - "shortcuts.resultCount": "{0} shortcuts", - "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", - "shortcuts.noneFound": "No shortcuts found.", - "shortcuts.section.fileGeneral": "Archivo y general", - "shortcuts.section.editing": "Edición", - "shortcuts.section.canvasNavigation": "Canvas y navegación", - "shortcuts.section.zoomPanPrecision": "Zoom, paneo y precisión", - "shortcuts.section.previewInspection": "Vista previa e inspección", - "shortcuts.key.deleteOrBackspace": "Del o Backspace", - "shortcuts.key.middleDrag": "Botón medio + arrastrar", - "shortcuts.key.rightDrag": "Botón derecho + arrastrar", - "shortcuts.key.spaceDrag": "Espacio + arrastrar", - "shortcuts.key.altLeftDrag": "Alt + arrastre izquierdo", - "shortcuts.key.arrows": "Flechas", - "shortcuts.key.shiftArrows": "Shift + Flechas", - "shortcuts.action.openShortcutScreen": "Abrir esta pantalla de atajos", - "shortcuts.action.newCanvas": "Nuevo canvas", - "shortcuts.action.openFile": "Abrir archivo", - "shortcuts.action.save": "Guardar", - "shortcuts.action.saveAs": "Guardar como", - "shortcuts.action.commandPalette": "Paleta de comandos", - "shortcuts.action.undo": "Deshacer", - "shortcuts.action.redo": "Rehacer", - "shortcuts.action.selectAll": "Seleccionar todo", - "shortcuts.action.deleteSelection": "Eliminar selección", - "shortcuts.action.closeOverlayCancel": "Cerrar overlays / cancelar acciones", - "shortcuts.action.openNodeSearch": "Abrir búsqueda de nodos", - "shortcuts.action.resetViewport": "Restablecer viewport", - "shortcuts.action.centerSelection": "Centrar selección", - "shortcuts.action.fitSelection": "Ajustar selección", - "shortcuts.action.autoLayout": "Diseño automático", - "shortcuts.action.toggleSnapToGrid": "Alternar ajuste a cuadrícula", - "shortcuts.action.bringForward": "Traer adelante", - "shortcuts.action.sendBackward": "Enviar atrás", - "shortcuts.action.bringToFront": "Traer al frente", - "shortcuts.action.sendToBack": "Enviar al fondo", - "shortcuts.action.zoomInOut": "Acercar / alejar", - "shortcuts.action.pan": "Panear", - "shortcuts.action.temporaryPan": "Paneo temporal", - "shortcuts.action.alternatePan": "Paneo alternativo", - "shortcuts.action.fineNudge": "Movimiento fino de selección", - "shortcuts.action.fastNudge": "Movimiento rápido", - "shortcuts.action.togglePreview": "Alternar vista previa de datos", - "shortcuts.action.explainPlan": "Plan de ejecución", - "shortcuts.action.runPreview": "Ejecutar vista previa", - "shortcuts.action.connectionManager": "Administrador de conexiones", - "shortcuts.action.flowVersionHistory": "Historial de versiones del flujo", - "shortcuts.resetAll": "Restablecer todo", - "shortcuts.customized": "Personalizado", - "shortcuts.default": "Predeterminado", - "shortcuts.apply": "Aplicar", - "shortcuts.reset": "Restablecer", - "shortcuts.status.resetAllSuccess": "Todos los atajos se restablecieron a los valores predeterminados.", - "shortcuts.status.updated": "Atajo actualizado.", - "shortcuts.status.reset": "Atajo restablecido al valor predeterminado.", - "shortcuts.status.updateFailed": "No se pudo actualizar el atajo.", - "toast.severity.success": "Success", - "toast.severity.warning": "Warning", - "toast.severity.error": "Error", - "toast.details.success": "Success Details", - "toast.details.warning": "Warning Details", - "toast.details.error": "Error Details", - "diagnostics.area.cteEditor": "Editor CTE", - "diagnostics.area.viewEditor": "Editor de vista", - "diagnostics.area.subEditor": "Subeditor", - "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", - "diagnostics.recommendation.reloadFileIfNeeded": "Recarga el archivo si es necesario.", - "diagnostics.viewEditor.exitFailed": "No se pudo salir: {0}", - "diagnostics.viewEditor.canvasIncomplete": "el canvas está incompleto.", - "diagnostics.viewEditor.exitRecommendation": "Conecta un ResultOutput válido o usa el comando descartar.", - "diagnostics.viewEditor.restoreParentFailed": "Falló la restauración del canvas padre. El subgrafo fue descartado.", - "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", - "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", - "diagnostics.canvasMigration.openWarning": "Open: {0}", - "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", - "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", - "diagnostics.recommendation.resaveLatestSchema": "Revisa diagnósticos y vuelve a guardar el canvas para persistir el esquema más reciente.", - "diagnostics.recommendation.saveMigratedSchema": "Revisa diagnósticos y guarda el canvas para persistir el esquema migrado.", - "file.saveDialog.title": "Save Canvas", - "file.saveDialog.suggestedName": "Query1", - "file.save.success": "Canvas saved successfully.", - "file.save.failedWithReason": "Save failed: {0}", - "file.openDialog.title": "Open Canvas", - "file.open.failedWithReason": "Open failed: {0}", - "file.open.success": "Canvas opened successfully.", - "file.open.successWithWarnings": "Canvas opened with warnings.", - "session.restore.failedWithReason": "Restore failed: {0}", - "session.restore.successWithWarnings": "Session restored with warnings.", - "session.restore.success": "Session restored successfully.", - "export.documentation.dialogTitle": "Export Flow Documentation", - "export.documentation.success": "Documentation exported successfully.", - "export.documentation.failed": "Documentation export failed.", - "export.failed.pathPermissionsHint": "Check file path and permissions.", - "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", - "export.dialogTitleByExtension": "Export as {0}", - "export.success": "Export completed successfully.", - "export.failed": "Export failed.", - "fileHistory.currentFile.none": "No file selected", - "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", - "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", - "fileHistory.status.countAvailable": "{0} local version(s) available.", - "fileHistory.restore.failedWithReason": "Restore failed: {0}", - "fileHistory.restore.successFrom": "Restored version from {0}.", - "preview.status.cancelled": "Cancelled", - "preview.status.error": "Error", - "preview.status.ready": "Ready", - "preview.runningWithMs": "Running... {0}ms", - "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", - "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", - "explain.errorWithReason": "Explain plan error: {0}", - "explain.noSql": "No SQL to explain. Build a query on the canvas first.", - "ddl.compilationFailed": "Compilation failed", - "ddl.compileErrorWithReason": "DDL compile error: {0}", - "command.undo.name": "Undo", - "command.undo.description": "Undo last action", - "command.redo.name": "Redo", - "command.redo.description": "Redo last undone action", - "command.addNode.name": "Add Node", - "command.addNode.description": "Open node search menu to add a node", - "command.bringForward.name": "Bring Forward", - "command.bringForward.description": "Move selected nodes one layer forward", - "command.sendBackward.name": "Send Backward", - "command.sendBackward.description": "Move selected nodes one layer backward", - "command.bringToFront.name": "Bring to Front", - "command.bringToFront.description": "Move selected nodes to top layer", - "command.sendToBack.name": "Send to Back", - "command.sendToBack.description": "Move selected nodes to bottom layer", - "command.normalizeLayers.name": "Normalize Layers", - "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", - "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", - "main.orphanSuffix": "Orphan(s)", - "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", - "main.namingPrefix": "Naming", - "fileHistory.compressedLabel": "Compressed:", - "schema.itemsSuffix": "item(s)", - "property.panel.title": "Properties", - "property.panel.multiSelected": "{0} nodes selected", - "sqlImporter.status.pasteSelect": "Pega una sentencia SELECT arriba y luego haz clic en Importar.", - "sqlImporter.status.inputTooLarge": "La entrada SQL es demasiado grande ({0:N0} caracteres). El límite es {1:N0}. Divide la consulta o aumenta el límite de importación.", - "sqlImporter.status.parsing": "Analizando SQL...", - "sqlImporter.status.done": "Listo - {0} importado(s), {1} parcial(es), {2} omitido(s).", - "sqlImporter.status.cancelledByUser": "Importación cancelada por el usuario.", - "sqlImporter.status.timeout": "La importación agotó el tiempo tras {0:0.#}s. Prueba una consulta más pequeña o aumenta el timeout.", - "sqlImporter.status.parseError": "Error de parseo: {0}", - "diagnostics.area.undoRedoTransaction": "Transacción Deshacer/Rehacer", - "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", - "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", - "node.preview.noCatalog": "No catalog available", - "connection.error.searchMenuNotInitialized": "el menú de búsqueda no está inicializado", - "connection.error.timeoutReachability": "Tiempo de conexión agotado - verifica que el servidor sea accesible y aumenta el timeout si es necesario.", - "connection.error.authenticationFailedForProvider": "Falló la autenticación - verifica usuario y contraseña para {0}.", - "connection.error.databaseNotFoundForProvider": "Base de datos no encontrada - confirma que el nombre exista en {0}.", - "connection.error.hostNotFound": "Host no encontrado - verifica dirección del servidor y resolución DNS.", - "connection.error.portRefused": "Conexión al puerto rechazada - verifica el puerto y que el servidor/firewall permitan acceso.", - "connection.error.sslTls": "Error SSL/TLS - verifica configuración SSL del servidor o desactiva SSL para conexiones locales.", - "connection.error.timeoutOverloaded": "Tiempo de conexión agotado - el servidor puede estar sobrecargado o inaccesible. Intenta aumentar el timeout.", - "connection.error.insufficientPrivileges": "Privilegios insuficientes - el usuario puede no tener permiso para conectarse a esta base.", - "diagnostics.area.connection": "Conexión", - "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", - "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", - "undoRedo.transaction.unnamed": "unnamed transaction", - "benchmark.runLabelDefault": "Run 1", - "benchmark.runLabelPattern": "Run {0}", - "benchmark.status.failedWithReason": "Benchmark falló: {0}", - "benchmark.status.noSql": "No hay SQL para benchmark - construye una consulta primero.", - "benchmark.status.warmupProgress": "Calentamiento {0}/{1}...", - "benchmark.status.iterationProgress": "Iteración {0}/{1}...", - "benchmark.status.done": "Listo - {0}", - "benchmark.status.cancelled": "Benchmark cancelado.", - "app.windowTitle": "DBWeaver", - "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", - "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", - "sqlImporter.error.selectFromNotFound": "No se pudo encontrar SELECT ... FROM en la consulta.", - "sqlImporter.error.fromClauseParseFailed": "No se pudo parsear la cláusula FROM.", - "sqlImporter.error.syntaxUnterminatedString": "Error de sintaxis en línea {0}, columna {1}: literal de cadena sin cerrar.", - "sqlImporter.error.missingClosingParenthesis": "falta ')' de cierre", - "sqlImporter.error.unexpectedClosingParenthesis": "')' inesperado", - "sqlImporter.error.syntaxAtLineColumn": "Error de sintaxis en línea {0}, columna {1}: {2}.", - "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", - "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", - "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", - "errorDiagnostics.connection.label": "Connection failed", - "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", - "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", - "errorDiagnostics.authorization.label": "Authorization error", - "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", - "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", - "errorDiagnostics.timeout.label": "Query timeout", - "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", - "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", - "errorDiagnostics.schema.label": "Schema error", - "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", - "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", - "errorDiagnostics.syntax.label": "SQL syntax error", - "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", - "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", - "errorDiagnostics.compatibility.label": "Compatibility error", - "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", - "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", - "errorDiagnostics.unknown.label": "Unexpected error", - "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", - "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", - "error.mainWindow.invalidDataContext": "El DataContext de MainWindow debe ser un ShellViewModel.", - "error.mainWindow.canvasNotInitialized": "CanvasViewModel no fue inicializado.", - "error.mainWindow.ddlPreviewUnavailable": "La vista previa DDL no está disponible para el canvas actual.", - "themeJson.editor.template": "{\n \"meta\": { \"name\": \"Tema personalizado\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", - "themeJson.error.pasteBeforeApply": "Pega un JSON de tema antes de aplicar.", - "themeJson.error.invalidJson": "JSON inválido: {0}", - "themeJson.error.emptyPayload": "JSON inválido: payload vacío.", - "themeJson.error.invalidTheme": "Tema inválido: {0}", - "themeJson.error.appliedButSaveFailed": "Tema aplicado, pero no se pudo guardar: {0}", - "themeJson.success.appliedAndSaved": "Tema JSON aplicado y guardado.", - "themeJson.success.customRemoved": "Tema personalizado eliminado. Reinicia la app para volver completamente al tema predeterminado.", - "themeJson.error.restoreDefaultFailed": "Error al restaurar el tema predeterminado: {0}", - "themeValidator.error.configNull": "La configuración del tema es nula.", - "themeValidator.warning.noSections": "El tema no tiene secciones de colores o tipografía; no hay nada para aplicar.", - "themeValidator.warning.invalidColor": "{0} tiene un color inválido '{1}'. Esta clave será ignorada.", - "themeValidator.warning.sizeOutOfRange": "{0}={1} está fuera del rango (8..48). Esta clave será ignorada.", - "queryExecutor.error.openConnectionMethodNotFound": "No se puede encontrar el método OpenConnectionAsync en el orquestador", - "queryExecutor.error.openConnectionInvokeFailed": "Error al invocar OpenConnectionAsync", - "ddlImporter.warning.viewSelectNotReconstructable": "Vista '{0}': el SELECT de la vista no puede reconstruirse visualmente; edítalo manualmente en el subcanvas.", - "ddlImporter.error.tableNotFoundInMetadata": "La tabla '{0}' no se encontró en los metadatos actuales.", - "main.window.untitled": "Sin título", - "main.subEditor.noSeedProvided": "No se proporcionó seed de subeditor para {0}.", - "main.layerOrder.bringToFront": "Traer al frente", - "main.layerOrder.sendToBack": "Enviar atrás", - "main.layerOrder.bringForward": "Adelantar capa", - "main.layerOrder.sendBackward": "Retroceder capa", - "main.layerOrder.normalizeLayers": "Normalizar capas", - "export.fileType.html": "Archivos HTML", - "export.fileType.json": "Archivos JSON", - "export.fileType.csv": "Archivos CSV", - "export.fileType.excel": "Archivos Excel", - "commandPalette.templatePrefix": "Plantilla: {0}", - "themeLoader.status.notFoundWithPath": "Archivo de tema no encontrado: {0}", - "themeLoader.status.deserializedNull": "El JSON del tema se deserializó como null.", - "themeLoader.status.loaded": "JSON del tema cargado correctamente.", - "credential.error.ciphertextTooShort": "El bloque de texto cifrado es demasiado corto.", - "credential.error.dpapiWindowsOnly": "DPAPI solo está disponible en Windows.", - "credential.warning.loadVaultFailed": "No se pudo cargar el almacén de credenciales {0}: {1}", - "credential.warning.persistVaultFailed": "No se pudo guardar el almacén de credenciales {0}: {1}", - "snippetStore.warning.loadFailed": "No se pudieron cargar snippets desde {0}: {1}", - "snippetStore.warning.saveFailed": "No se pudieron guardar snippets: {0}", - "flowVersionStore.warning.loadFailed": "No se pudieron cargar versiones de flujo desde {0}: {1}", - "flowVersionStore.warning.saveFailed": "No se pudieron guardar versiones de flujo: {0}", - "queryExecutor.error.queryEmpty": "La consulta no puede estar vacía", - "queryExecutor.error.providerNotSupported": "El proveedor {0} no es compatible", - "queryExecutor.error.singleStatementOnly": "La vista previa solo acepta una única sentencia SQL.", - "queryExecutor.error.queryEmptyWithPeriod": "La consulta no puede estar vacía.", - "queryExecutor.error.readOnlyOnly": "El modo de vista previa solo admite sentencias SQL de solo lectura.", - "queryExecutor.error.namedParametersNotSupported": "El modo de vista previa no admite parámetros vinculados en SQL de ejecución. Usa literales seguros inline o ejecuta la consulta fuera de la vista previa.", - "queryExecutor.error.positionalParametersNotSupported": "El modo de vista previa no admite placeholders posicionales (? o ).", - "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "Alinear nodos seleccionados al borde inferior", - "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "Alinear nodos seleccionados al borde izquierdo", - "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "Alinear nodos seleccionados al borde derecho", - "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "Alinear nodos seleccionados al borde superior", - "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "Aplicar cambios del subcanvas CTE y volver al canvas padre", - "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "Organizar nodos automáticamente en columnas lógicas", - "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "Centrar nodos seleccionados en el eje horizontal", - "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "Centrar nodos seleccionados en el eje vertical", - "commandPalette.description.clear_canvas_and_start_fresh": "Limpiar canvas y comenzar desde cero", - "commandPalette.description.clear_node_selection": "Limpiar selección de nodos", - "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "Convertir alias a la convención configurada en ajustes del proyecto", - "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "Crear checkpoints, comparar versiones lado a lado y restaurar un estado anterior del canvas", - "commandPalette.description.delete_the_selected_nodes": "Eliminar nodos seleccionados", - "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "Descartar cambios del subeditor actual y volver al canvas padre", - "commandPalette.description.execute_the_current_query_in_preview": "Ejecutar la consulta actual en vista previa", - "commandPalette.description.fit_all_nodes_into_the_visible_area": "Ajustar todos los nodos al área visible", - "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "Generar archivo CSV desde el primer nodo de exportación CSV", - "commandPalette.description.generate_html_file_from_the_first_html_export_node": "Generar archivo HTML desde el primer nodo de exportación HTML", - "commandPalette.description.generate_json_file_from_the_first_json_export_node": "Generar archivo JSON desde el primer nodo de exportación JSON", - "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "Generar libro XLSX desde el primer nodo de exportación Excel", - "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "Inspeccionar plan de ejecución: tipos de scan, estrategias de join y estimaciones de costo", - "commandPalette.description.load_a_vsaq_canvas_file": "Cargar archivo de canvas .vsaq", - "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "Medir latencia promedio/mediana/p95 del SQL actual en N iteraciones", - "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "Abrir editor de subcanvas aislado para el nodo de definición CTE seleccionado", - "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "Abrir historial local de versiones creado en cada guardado y restaurar snapshots anteriores", - "commandPalette.description.open_output_preview_modal_for_the_active_mode": "Abrir modal de vista previa de salida para el modo activo", - "commandPalette.description.open_shortcut_reference_screen": "Abrir pantalla de referencia de atajos", - "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "Abrir administrador de conexiones para agregar, editar o cambiar conexiones de base de datos", - "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "Pegar SELECT y generar nodos automáticamente: FROM, JOIN, WHERE y LIMIT soportados", - "commandPalette.description.remove_all_nodes_not_connected_to_output": "Eliminar todos los nodos no conectados a la salida", - "commandPalette.description.reset_zoom_and_pan_to_default": "Restablecer zoom y desplazamiento a valores predeterminados", - "commandPalette.description.save_canvas_to_a_new_file": "Guardar canvas en un archivo nuevo", - "commandPalette.description.save_current_canvas": "Guardar canvas actual", - "commandPalette.description.save_markdown_documentation_of_the_current_flow": "Guardar documentación Markdown del flujo actual", - "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "Guardar nodos seleccionados como snippet reutilizable e insertarlo luego desde el buscador de nodos (⇧A)", - "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "Escanear todos los nodos de tabla del canvas para posibles joins según convenciones FK y patrones de nombres", - "commandPalette.description.select_all_nodes_on_canvas": "Seleccionar todos los nodos del canvas", - "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "Ajustar posiciones de nodos a cuadrícula de 16px (Ctrl+G)", - "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "Distribuir nodos seleccionados con espaciado horizontal igual", - "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "Distribuir nodos seleccionados con espaciado vertical igual", - "commandPalette.description.zoom_into_the_canvas": "Acercar en el canvas", - "commandPalette.description.zoom_out_of_the_canvas": "Alejar en el canvas", - "commandPalette.name.align_bottom": "Alinear Abajo", - "commandPalette.name.align_left": "Alinear Izquierda", - "commandPalette.name.align_right": "Alinear Derecha", - "commandPalette.name.align_top": "Alinear Arriba", - "commandPalette.name.analyze_all_joins": "Analizar Todos los Joins", - "commandPalette.name.auto_fix_naming": "Auto-Corregir Nombres", - "commandPalette.name.auto_layout": "Diseño automático", - "commandPalette.name.center_horizontally": "Centrar Horizontalmente", - "commandPalette.name.center_vertically": "Centrar Verticalmente", - "commandPalette.name.cleanup_orphans": "Limpiar Huérfanos", - "commandPalette.name.delete_selected": "Eliminar Seleccionados", - "commandPalette.name.deselect_all": "Deseleccionar Todo", - "commandPalette.name.discard_and_exit_editor": "Descartar y Salir del Editor", - "commandPalette.name.distribute_horizontally": "Distribuir Horizontalmente", - "commandPalette.name.distribute_vertically": "Distribuir Verticalmente", - "commandPalette.name.edit_selected_cte": "Editar CTE Seleccionado", - "commandPalette.name.exit_cte_editor": "Salir del Editor CTE", - "commandPalette.name.explain_plan": "Plan de Ejecución", - "commandPalette.name.export_csv": "Exportar CSV", - "commandPalette.name.export_documentation": "Exportar Documentación", - "commandPalette.name.export_excel": "Exportar Excel", - "commandPalette.name.export_html": "Exportar HTML", - "commandPalette.name.export_json": "Exportar JSON", - "commandPalette.name.file_save_load_history": "Historial de Guardado/Carga", - "commandPalette.name.fit_to_screen": "Ajustar a Pantalla", - "commandPalette.name.flow_version_history": "Historial de Versiones del Flujo", - "commandPalette.name.import_sql_to_graph": "Importar SQL a Grafo", - "commandPalette.name.keyboard_shortcuts": "Atajos de Teclado", - "commandPalette.name.manage_connections": "Administrar Conexiones", - "commandPalette.name.new_canvas": "Nuevo Canvas", - "commandPalette.name.open_file": "Abrir Archivo", - "commandPalette.name.reset_viewport": "Restablecer Vista", - "commandPalette.name.run_preview": "Ejecutar Vista Previa", - "commandPalette.name.run_query_benchmark": "Ejecutar Benchmark de Consulta", - "commandPalette.name.save": "Guardar", - "commandPalette.name.save_as": "Guardar Como", - "commandPalette.name.save_selection_as_snippet": "Guardar Selección como Snippet", - "commandPalette.name.select_all": "Seleccionar Todo", - "commandPalette.name.toggle_preview": "Alternar Vista Previa", - "commandPalette.name.toggle_snap_to_grid": "Alternar Ajuste a Cuadrícula", - "commandPalette.name.zoom_in": "Acercar", - "commandPalette.name.zoom_out": "Alejar", - "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 por ciento restaurar zoom paneo viewport", - "commandPalette.tags.align_bottom_edge_selection_nodes": "alinear borde inferior selección nodos", - "commandPalette.tags.align_center_middle_horizontal_nodes": "alinear centro medio horizontal nodos", - "commandPalette.tags.align_center_middle_vertical_nodes": "alinear centro medio vertical nodos", - "commandPalette.tags.align_left_edge_selection_nodes": "alinear borde izquierdo selección nodos", - "commandPalette.tags.align_right_edge_selection_nodes": "alinear borde derecho selección nodos", - "commandPalette.tags.align_top_edge_selection_nodes": "alinear borde superior selección nodos", - "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout vista restablecer zoom", - "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark rendimiento latencia tiempos perfil medir velocidad", - "commandPalette.tags.clear_selection": "limpiar selección", - "commandPalette.tags.connection_database_server_host_provider_switch": "conexión base datos servidor host proveedor cambiar", - "commandPalette.tags.create_insert_search_transform": "crear insertar buscar transformar", - "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas salir aplicar volver", - "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte vista subcanvas descartar salir forzar", - "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursivo editor subgrafo subcanvas aislar", - "commandPalette.tags.data_results_table_panel": "datos resultados tabla panel", - "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribuir espacio igual horizontal nodos", - "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribuir espacio igual vertical nodos", - "commandPalette.tags.execute_run_sql_query_results": "ejecutar correr sql consulta resultados", - "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan ejecución costo scan índice join rendimiento", - "commandPalette.tags.export_csv_file_tabular_output_save": "exportar csv archivo salida tabular guardar", - "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "exportar excel xlsx archivo salida tabular hoja cálculo guardar", - "commandPalette.tags.export_html_file_output_report_save": "exportar html archivo salida informe guardar", - "commandPalette.tags.export_json_file_output_save": "exportar json archivo salida guardar", - "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "exportar markdown doc documentación flujo guardar md", - "commandPalette.tags.export_persist_copy": "exportar persistir copia", - "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "archivo historial guardar cargar backup versiones restaurar local", - "commandPalette.tags.forward_history": "adelante historial", - "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "ayuda atajos hotkeys teclado referencia", - "commandPalette.tags.highlight_mark_all_nodes": "resaltar marcar todos nodos", - "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "importar sql pegar convertir grafo ingeniería inversa consulta", - "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analizar sugerir detectar foreign key relaciones heurística", - "commandPalette.tags.layer_z_order_back_selected_nodes": "capa orden z atrás nodos seleccionados", - "commandPalette.tags.layer_z_order_backward_selected_nodes": "capa orden z enviar atrás nodos seleccionados", - "commandPalette.tags.layer_z_order_forward_selected_nodes": "capa orden z traer adelante nodos seleccionados", - "commandPalette.tags.layer_z_order_front_selected_nodes": "capa orden z al frente nodos seleccionados", - "commandPalette.tags.layer_z_order_normalize_compact": "capa orden z normalizar compactar", - "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout organizar columnas auto legibilidad", - "commandPalette.tags.load_import_vsaq": "cargar importar vsaq", - "commandPalette.tags.magnify_enlarge": "aumentar ampliar", - "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "huérfano no usado desconectado limpiar eliminar nodos", - "commandPalette.tags.persist_write_disk": "persistir escribir disco", - "commandPalette.tags.remove_erase_nodes": "remover borrar nodos", - "commandPalette.tags.rename_alias_fix_naming_convention": "renombrar alias corregir convención nombres", - "commandPalette.tags.reset_clear_blank": "restablecer limpiar en blanco", - "commandPalette.tags.revert_back_history": "revertir volver historial", - "commandPalette.tags.shrink_reduce": "encoger reducir", - "commandPalette.tags.snap_grid_align_precision_position": "snap cuadrícula alinear precisión posición", - "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet guardar selección reutilizar plantilla favorito marcador", - "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "versión historial checkpoint diff restaurar snapshot comparar deshacer flujo", - "sqlEditor.diffPreview.title": "Transactional Diff Preview", - "sqlEditor.mutation.confirmExecute": "Confirm Execute", - "sqlEditor.tab.closeAnyway": "Close Anyway", - "sqlEditor.tab.keepTab": "Keep Tab", - "sqlEditor.status.ready": "Ready.", - "sqlEditor.telemetry.none": "No execution telemetry yet.", - "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", - "sqlEditor.telemetry.errors.none": "No aggregated errors.", - "sqlEditor.diff.none": "No transactional diff preview available.", - "sqlEditor.mutation.estimate.none": "No mutation estimate available.", - "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", - "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", - "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", - "sqlEditor.tab.noPendingClose": "No tab close pending.", - "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", - "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", - "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", - "sqlEditor.message.empty": "Execute a statement to see messages.", - "sqlEditor.message.success": "Execution completed successfully.", - "sqlEditor.result.summary.empty": "Rows: - Time: -", - "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", - "sqlEditor.file.save.canceled": "Save canceled.", - "sqlEditor.file.save.noPath": "No target path selected.", - "sqlEditor.file.save.success": "SQL file saved.", - "sqlEditor.file.save.failed": "Save failed.", - "sqlEditor.file.open.failed": "Open failed.", - "sqlEditor.file.open.notFound": "Selected SQL file was not found.", - "sqlEditor.file.open.success": "SQL file opened.", - "sqlEditor.status.executing": "Executing SQL...", - "sqlEditor.status.executingScript": "Executing SQL script...", - "sqlEditor.status.executingStep": "Executing {0}/{1}...", - "sqlEditor.status.canceling": "Canceling execution...", - "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", - "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", - "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", - "sqlEditor.status.success": "Execution succeeded.", - "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", - "sqlEditor.status.canceled": "Execution canceled.", - "sqlEditor.status.failed": "Execution failed.", - "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", - "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", - "sqlEditor.result.tabTitle": "Result {0}", - "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", - "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", - "sqlEditor.tab.closed": "Tab closed.", - "sqlEditor.tab.closeCanceled": "Tab close canceled.", - "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", - "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", - "sqlEditor.error.noConnection": "No active database connection for SQL execution.", - "sqlEditor.error.executionCanceled": "SQL execution was canceled.", - "sqlEditor.tab.scriptTitle": "Script {0}", - "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", - "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", - "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", - "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", - "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", - "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", - "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", - "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", - "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", - "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", - "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", - "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", - "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", - "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", - "sqlEditor.results.title": "Results", - "sqlEditor.saveSql.fileType": "SQL Files", - "sqlEditor.saveSql.pickerTitle": "Save SQL File", - "sqlEditor.export.pickerTitle": "Export SQL Data", - "sqlEditor.export.status.noResultTitle": "No execution result available for export.", - "sqlEditor.export.status.noResultDetail": "Execute a query first.", - "sqlEditor.export.status.successTitle": "Report exported.", - "sqlEditor.export.status.failedTitle": "Failed to export report.", - "sqlEditor.export.fileType.html": "HTML File", - "sqlEditor.export.fileType.json": "JSON File", - "sqlEditor.export.fileType.csv": "CSV File", - "sqlEditor.export.fileType.xlsx": "Excel Workbook", - "sqlEditor.export.defaultFileBase": "report", - "sqlEditor.export.defaultTitle": "SQL Report", - "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", - "sqlEditor.export.type.html.title": "HTML full-feature report", - "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", - "sqlEditor.export.type.json.title": "JSON execution contract", - "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", - "sqlEditor.export.type.csv.title": "CSV data export", - "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", - "sqlEditor.export.type.xlsx.title": "Excel workbook export", - "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", - "sqlEditor.export.dialog.windowTitle": "Export SQL Data", - "sqlEditor.export.dialog.title": "Export SQL Data", - "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", - "sqlEditor.export.dialog.confirm": "Export", - "sqlEditor.export.dialog.fileNameWatermark": "report.html", - "sqlEditor.export.dialog.titleWatermark": "SQL Report", - "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", - "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", - "sqlEditor.export.dialog.section.fileName": "FILE NAME", - "sqlEditor.export.dialog.section.reportTitle": "TITLE", - "sqlEditor.export.dialog.section.description": "DESCRIPTION", - "sqlEditor.export.dialog.section.options": "OPTIONS", - "sqlEditor.export.option.includeSchema": "Include output schema", - "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", - "sqlEditor.export.option.includeMetadata": "Include optional metadata", - "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", - "sqlEditor.export.badge.offline": "OFFLINE READY", - "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", - "sqlEditor.export.badge.dataOnly": "DATA ONLY" -} +{ + "main.brand": "AkkornStudio", + "main.tab.query1": "Query 1", + "main.new": "New", + "main.open": "Open", + "main.save": "Save", + "main.history": "History", + "main.layout": "Layout", + "main.preview": "Preview", + "main.undo": "Undo", + "main.redo": "Redo", + "main.cleanupOrphans": "Cleanup orphan nodes", + "main.autoFixAliasNaming": "Auto-fix alias naming", + "main.autoLayoutCanvas": "Auto layout canvas", + "main.toggleDataPreview": "Toggle data preview", + "main.language": "Language", + "main.restore.prompt": "Previous session found — restore the last canvas?", + "main.restore.button": "Restore session", + "main.cteEditor.editingPrefix": "Editing CTE: ", + "main.cteEditor.backToCanvas": "Back to Canvas", + "main.cteEditor.exitA11y": "Exit CTE editor", + "main.viewEditor.editingPrefix": "DDL > View: ", + "main.viewEditor.backToCanvas": "Back to DDL", + "main.viewEditor.exitA11y": "Exit view editor", + "connection.title": "Connection Manager", + "connection.subtitle": "Configure, pruebe y active conexiones sin salir del flujo", + "connection.none": "No connection", + "connection.active": "ACTIVE", + "connection.health.online": "Online", + "connection.health.degraded": "Degraded", + "connection.health.offline": "Offline", + "connection.tooltip.none": "No active connection — click to manage", + "connection.ping": "Ping", + "connection.saved": "SAVED CONNECTIONS", + "connection.new": "New Connection", + "connection.selectOrCreate": "Select a connection or create a new one", + "connection.name": "Connection Name", + "connection.provider": "Provider", + "connection.host": "Host", + "connection.port": "Port", + "connection.database": "Database", + "connection.sqlitePath": "SQLite Path", + "connection.sqliteBrowse": "Browse", + "connection.sqliteCreate": "Create", + "connection.username": "Username", + "connection.password": "Password", + "connection.timeout": "Timeout (seconds)", + "connection.test": "Test", + "connection.save": "Save", + "connection.connect": "Connect", + "connection.action.testConnection": "Test connection", + "connection.action.saveConnection": "Save connection", + "connection.action.connectConnection": "Connect connection", + "connection.status.connecting": "Conectando...", + "connection.status.connected": "Conectado", + "connection.status.testing": "Probando...", + "connection.status.failedPrefix": "Falló la conexión", + "connection.status.metadataUnavailable": "Falló la conexión: metadatos no disponibles.", + "connection.status.highLatency": "alta latencia", + "connection.watermark.name": "Mi BD de Producción", + "connection.watermark.host": "localhost", + "connection.watermark.port": "5432", + "connection.watermark.database": "database_name", + "connection.watermark.username": "user", + "connection.watermark.password": "••••••••", + "connection.watermark.timeout": "30", + "main.connectingDb": "Connecting to database...", + "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", + "status.nodesSeparator": " nodes · ", + "status.connectionsSuffix": " connections", + "status.undo": "Undo: ", + "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", + "connection.disconnect": "Disconnect", + "connection.action.disconnectConnection": "Disconnect connection", + "connectionTab.active": "ACTIVE CONNECTION", + "connectionTab.none": "No active connection", + "connectionTab.saved": "SAVED CONNECTIONS", + "connectionTab.new": "+ New Connection", + "schema.database": "DATABASE", + "schema.search": "Search tables, columns...", + "schema.loading": "Searching tables, columns...", + "schema.noConnection": "No Connection", + "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", + "schema.emptyNoTables": "No tables found", + "fileHistory.title": "Save/Load Version History", + "fileHistory.reload": "Reload", + "fileHistory.restoreSelected": "Restore selected", + "fileHistory.empty": "No local versions yet", + "fileHistory.emptyHint": "Save this file to generate version history.", + "preview.title": "Data Preview", + "preview.subtitle": "Review data and diagnostics before continuing", + "preview.run": "Run", + "preview.cancel": "Cancel", + "preview.tab.preview": "Preview", + "preview.tab.sql": "SQL", + "preview.close": "Close preview", + "preview.running": "Running preview query… ", + "preview.clickCancel": "Click Cancel to stop", + "preview.cancelled": "Query cancelled", + "preview.runAgain": "Press Run to execute again", + "preview.failed": "Preview execution failed", + "preview.technical": "TECHNICAL DETAILS", + "preview.noData": "No data yet", + "preview.f3Hint": "Press F3 or Space to run the current query", + "sqlImporter.title": "Import SQL to Graph", + "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", + "sqlImporter.sqlStatement": "SQL STATEMENT", + "sqlImporter.supported": "Supported: ", + "sqlImporter.import": "Import", + "sqlImporter.report": "CONVERSION REPORT", + "sqlEditor.mutation.dialogTitle": "Confirmacion de cambio", + "sqlEditor.mutation.dialogSubtitle": "Revisa el impacto antes de confirmar la ejecucion", + "search.empty": "No nodes found", + "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", + "search.shortcut": "⇧A", + "search.spawn": "Spawn", + "commandPalette.empty": "Ningún comando coincide con tu búsqueda", + "commandPalette.search": "Buscar comando", + "commandPalette.shortcut": "CTRL+SHIFT+P", + "commandPalette.execute": "Ejecutar", + "context.editCte": "Edit Selected CTE", + "context.editViewSubcanvas": "Edit View Subcanvas", + "explain.title": "Explain Plan", + "explain.sql": "SQL", + "explain.option.analyze": "Analizar", + "explain.option.buffers": "Buffers", + "explain.badge.simulated": "SIMULADO", + "explain.timing.planning": "Planificacion:", + "explain.timing.execution": "Ejecucion:", + "explain.section.snapshotComparison": "Comparacion de instantaneas", + "explain.section.indexRecommendations": "Recomendaciones de indices", + "explain.section.history": "Historial", + "explain.detail.estimated": "Estimadas", + "explain.detail.actual": "Actuales", + "explain.detail.error": "Error", + "explain.detail.time": "Tiempo", + "explain.detail.loops": "Bucles", + "explain.rerun": "Re-run", + "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", + "explain.running": "Running EXPLAIN…", + "explain.failed": "⚠ EXPLAIN failed", + "explain.noPlan": "No plan yet", + "explain.rerunHint": "Press Re-run to execute EXPLAIN", + "explain.header.operation": "OPERACIÓN", + "explain.header.cost": "COSTO", + "explain.header.rows": "FILAS", + "explain.header.alert": "ALERTA", + "explain.mode.list": "Lista", + "explain.mode.tree": "Arbol", + "explain.action.snapshot": "Guardar snapshot", + "explain.action.copyJson": "Copiar JSON", + "explain.action.copyText": "Copiar texto", + "explain.action.saveJson": "Guardar .json", + "explain.action.openDalibo": "Abrir en Dalibo", + "explain.legend.seqscan": "SEQ SCAN — full table read, no index", + "explain.legend.sort": "SORT — in-memory sort", + "explain.legend.hash": "HASH — hash join", + "explain.escClose": "Esc to close", + "flowVersion.title": "Flow Version History", + "flowVersion.subtitle": "Create checkpoints, compare versions and restore", + "flowVersion.watermark": "Checkpoint label (optional)…", + "flowVersion.saveCheckpoint": "Save Checkpoint", + "flowVersion.compareMode": "Compare Mode", + "flowVersion.selectBase": "Select BASE version (from):", + "flowVersion.clickAny": "Then click any version in the list below to compare.", + "flowVersion.noCheckpoints": "No checkpoints yet", + "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", + "flowVersion.restore": "Restore", + "flowVersion.diffResults": "Diff Results", + "benchmark.title": "Query Benchmark", + "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", + "benchmark.sql": "SQL being benchmarked", + "benchmark.runLabel": "Run label", + "benchmark.runLabelWatermark": "Run 1", + "benchmark.iterations": "Iterations (1–100)", + "benchmark.warmup": "Warm-up passes (0–10)", + "benchmark.interval": "Interval between runs (ms)", + "benchmark.run": "Run Benchmark", + "benchmark.cancel": "Cancel", + "benchmark.clearHistory": "Clear History", + "benchmark.latest": "LATEST RESULT", + "benchmark.avg": "AVG", + "benchmark.median": "MEDIAN", + "benchmark.min": "MIN", + "benchmark.max": "MAX", + "benchmark.iterationsAt": " iterations · run at ", + "benchmark.history": "HISTORY", + "benchmark.header.label": "Etiqueta", + "benchmark.header.avg": "Prom", + "benchmark.header.median": "Mediana", + "benchmark.header.min": "Mín", + "benchmark.header.max": "Máx", + "benchmark.itersSuffix": " iters", + "diagnostics.title": "App Diagnostics", + "diagnostics.run": "Run", + "diagnostics.running": "Running checks…", + "diagnostics.ok": "OK", + "diagnostics.warning": "Warning", + "diagnostics.error": "Error", + "diagnostics.tooltip.rerun": "Re-run all checks", + "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", + "diagnostics.tooltip.close": "Close (Esc)", + "autoJoin.title": "Auto-Join Suggestions", + "autoJoin.titleForTable": "Auto-Join suggestions for {0}", + "autoJoin.acceptAll": "Accept All", + "autoJoin.accept": "Accept", + "autoJoin.skip": "Skip", + "autoJoin.allHandled": "All suggestions handled", + "autoJoin.joinKeyword": "JOIN", + "autoJoin.confidence.fkConstraint": "FK Constraint", + "autoJoin.confidence.fkReverse": "FK (Reverse)", + "autoJoin.confidence.namingMatch": "Naming Match", + "autoJoin.confidence.weakMatch": "Weak Match", + "autoJoin.runSelected": "Auto-Join Selected", + "autoJoin.noSimilarityTitle": "No automatic join found", + "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", + "autoJoin.appliedTitle": "Auto-join applied", + "autoJoin.manual.title": "Create Manual Join", + "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", + "autoJoin.manual.leftColumn": "Left column", + "autoJoin.manual.rightColumn": "Right column", + "autoJoin.manual.joinType": "Join type", + "autoJoin.manual.operator": "Operator", + "autoJoin.manual.confirm": "Create join", + "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", + "autoJoin.manualJoinCreatedTitle": "Manual join created", + "autoJoin.manualJoinFailedTitle": "Manual join could not be created", + "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", + "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", + "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", + "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", + "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", + "property.outputAlias": "OUTPUT ALIAS", + "property.sourceAlias": "SOURCE ALIAS", + "property.aliasWatermark": "e.g. MyColumn (optional)", + "property.parameters": "PARAMETERS", + "property.enabled": "Enabled", + "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", + "property.dateWatermark": "YYYY-MM-DD or leave empty", + "property.apply": "Apply", + "property.inputPins": "INPUT PINS", + "property.outputPins": "OUTPUT PINS", + "property.sqlTrace": "SQL TRACE", + "property.live": "live", + "node.numericValue": "Numeric Value", + "node.stringValue": "String Value", + "node.enterText": "Enter text", + "node.datetimeValue": "DateTime Value", + "node.valueLabel": "Value:", + "node.noInputs": "No inputs", + "node.loadingSample": "Loading sample…", + "node.previewFailed": "⚠ Preview failed", + "node.sampleRowsHint": "5 sample rows · demo data", + "sidebar.tab.nodes": "Nodos", + "sidebar.tab.connection": "Conexión", + "sidebar.tab.schema": "Esquema", + "sidebar.tab.diagnostics": "Diagnóstico", + "sidebar.addNode": "+ Add Node (⇧A)", + "sidebar.previewF3": "Preview (F3)", + "nodesList.search": "Search nodes...", + "search.watermark": "Search nodes… (Esc to close)", + "search.snippets": "★ SNIPPETS", + "commandPalette.watermark": "Ejecuta un comando… (Esc para cerrar)", + "tooltip.newCanvas": "New canvas (Ctrl+N)", + "tooltip.openCanvas": "Open canvas (Ctrl+O)", + "tooltip.saveCanvas": "Save canvas (Ctrl+S)", + "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", + "tooltip.zoomOut": "Zoom out (Ctrl+-)", + "tooltip.zoomIn": "Zoom in (Ctrl++)", + "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", + "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", + "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", + "tooltip.dataPreview": "Data preview (F3)", + "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", + "tooltip.appDiagnostics": "App Diagnostics (self-check)", + "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", + "tooltip.cancelRunningQuery": "Cancel the running query", + "tooltip.closeEsc": "Close (Esc)", + "tooltip.recheckConnectionHealth": "Re-check connection health", + "tooltip.deleteConnection": "Delete connection", + "tooltip.testConnection": "Test connection", + "tooltip.saveConnection": "Save connection", + "tooltip.activateConnection": "Activate this connection", + "tooltip.toggleDataSamplePreview": "Toggle data sample preview", + "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", + "tooltip.copySql": "Copy SQL to clipboard", + "tooltip.formatSql": "Format SQL", + "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", + "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", + "tooltip.switchToQueryMode": "Switch to Query canvas", + "tooltip.switchToDdlMode": "Switch to DDL canvas", + "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", + "tooltip.pins.inputs": "Inputs", + "tooltip.pins.outputs": "Outputs", + "tooltip.pins.none": "None", + "tooltip.tableColumns": "Columns", + "tooltip.tableColumns.none": "No detailed columns", + "window.minimize": "Minimize window", + "window.maximizeRestore": "Maximize/restore window", + "window.close": "Close window", + "menu.newDiagram": "New diagram", + "menu.openFile": "Open file", + "menu.save": "Save", + "menu.fileHistory": "File history", + "menu.shortcuts": "Keyboard shortcuts", + "menu.settings": "Settings", + "menu.importDdlSchema": "Import DDL schema", + "menu.viewDdlSql": "View DDL SQL", + "menu.executeDdl": "Execute DDL", + "menu.backToStart": "Back to start", + "toast.ddlExecuteFailed": "Failed to execute DDL.", + "toast.ddlOpenFailed": "Failed to open DDL SQL.", + "toast.ddlImportFailed": "Failed to import schema into DDL.", + "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", + "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", + "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", + "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", + "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", + "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", + "toast.ddlTableImported": "Table imported into the DDL canvas.", + "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", + "toast.ddlNoActiveConnection": "No active connection to execute DDL.", + "toast.ddlExecutedSuccess": "DDL executed successfully.", + "toast.ddlExecutedWithIssues": "DDL executed with issues.", + "toast.switchToDdl": "Switch to DDL mode to generate SQL.", + "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", + "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", + "toast.previewOpenFailed": "Failed to open preview.", + "tab.switchFailed": "Failed to switch tab: {0}", + "settings.status.darkApplied": "Dark theme applied.", + "settings.status.lightApplied": "Light theme applied.", + "settings.status.snapUpdated": "Snap updated: {0}.", + "settings.status.languageToggled": "Language toggled.", + "settings.status.languageSelected": "Language selected: {0}.", + "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", + "settings.section.appearance.title": "Themes", + "settings.section.languageRegion.title": "Idioma y región", + "settings.section.dateTime.title": "Fecha y hora", + "settings.section.keyboard.title": "Atajos de teclado", + "settings.section.privacy.title": "Privacidad", + "settings.section.notification.title": "Notificaciones", + "settings.section.accessibility.title": "Accesibilidad", + "settings.section.default.title": "Configuración", + "settings.section.appearance.subtitle": "Elige tu estilo o personaliza tu tema", + "settings.section.languageRegion.subtitle": "Administra idioma y formato regional", + "settings.section.keyboard.subtitle": "Personaliza los atajos de teclado usados por la command palette y la ejecución del canvas.", + "settings.section.wip.subtitle": "En progreso.", + "settings.section.default.subtitle": "Configuración de la aplicación", + "settings.general": "General", + "settings.nav.appearance": "Appearance", + "settings.theme.light": "Modo claro", + "settings.theme.dark": "Modo oscuro", + "settings.theme.system": "Preferencias del sistema", + "settings.gridSnap.title": "Grid Snap", + "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", + "settings.language.subtitle": "Seleccione el idioma de la aplicación.", + "settings.language.toggle": "Cambiar idioma", + "settings.language.option.ptBR": "Portugués (Brasil)", + "settings.language.option.enUS": "Inglés (Estados Unidos)", + "settings.language.option.esES": "Español (España)", + "settings.language.option.ruRU": "Ruso", + "settings.language.option.jaJP": "Japonés", + "settings.language.option.zhTW": "Chino tradicional", + "settings.themeJson.title": "JSON del tema", + "settings.themeJson.subtitle": "Pega tu JSON de tema, aplícalo y persístelo al instante.", + "settings.themeJson.apply": "Aplicar JSON", + "settings.themeJson.restoreDefault": "Restaurar tema predeterminado", + "mode.query": "Query", + "mode.ddl": "DDL", + "sidebar.left.close": "Close left sidebar", + "sidebar.left.open": "Reopen left sidebar", + "sidebar.right.close": "Close right sidebar", + "sidebar.right.open": "Reopen right sidebar", + "connection.completedTitle": "Connection completed", + "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", + "connection.close": "Close connection manager", + "connection.refreshHealth": "Refresh connection health", + "common.details": "Details", + "common.cancel": "Cancel", + "common.keep": "Keep", + "common.clear": "Clear", + "zoom.out": "Zoom out", + "zoom.in": "Zoom in", + "zoom.fit": "Fit zoom to screen", + "zoom.level": "Zoom level", + "settings.theme.mode": "Modo de tema", + "diagnostics.category.canvas": "Canvas Integrity", + "diagnostics.category.output": "Output & Execution", + "diagnostics.category.session": "Session & Safety", + "diagnostics.category.notice": "Runtime Notices", + "diagnostics.summary.ok": "All systems OK", + "diagnostics.summary.warningCount": "{0} warning(s) detected", + "diagnostics.summary.errorCount": "{0} error(s) detected", + "diagnostics.canvasMigration": "Canvas Migration", + "diagnostics.recommendation.resaveFile": "Vuelve a guardar el archivo para actualizarlo a la última versión del esquema.", + "diagnostics.canvasState.name": "Canvas State", + "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", + "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", + "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", + "diagnostics.validation.name": "Errores de validación", + "diagnostics.validation.recommendation": "Corrige los nodos resaltados antes de ejecutar la vista previa de salida", + "diagnostics.validation.errorWithWarnings": "{0} error(es) y {1} advertencia(s) en el grafo", + "diagnostics.validation.warningOnly": "{0} advertencia(s) en el grafo", + "diagnostics.validation.none": "Sin problemas de validación", + "diagnostics.orphan.name": "Nodos huérfanos", + "diagnostics.orphan.recommendation": "Usa la acción de limpieza de huérfanos para remover nodos no usados", + "diagnostics.orphan.count": "{0} nodo(s) no conectado(s) a ninguna salida", + "diagnostics.orphan.none": "No se detectaron nodos huérfanos", + "diagnostics.naming.name": "Convenciones de nombres", + "diagnostics.naming.recommendation": "Usa autocorrección de alias cuando la conformidad sea menor a 100%", + "diagnostics.naming.conformance": "Conformidad de nombres: {0}%", + "diagnostics.naming.ok": "Todos los alias siguen las convenciones de nombres (100%)", + "diagnostics.queryCompilation.name": "Compilación SQL en vivo", + "diagnostics.queryCompilation.recommendation": "Revisa el diagnóstico SQL en la salida cuando se reporten errores/advertencias.", + "diagnostics.queryCompilation.errorFallback": "La compilación SQL en vivo reportó errores.", + "diagnostics.queryCompilation.warningCounts": "{0} elemento(s) de diagnóstico, {1} advertencia(s) de guardrail.", + "diagnostics.queryCompilation.ok": "SQL en vivo compilado sin diagnósticos.", + "diagnostics.previewSafety.name": "Seguridad de vista previa", + "diagnostics.previewSafety.recommendation": "La vista previa solo ejecuta sentencias de solo lectura.", + "diagnostics.previewSafety.blocked": "El SQL actual modifica datos y está bloqueado por el modo de vista previa segura.", + "diagnostics.previewSafety.ok": "Las verificaciones de seguridad de vista previa pasaron.", + "diagnostics.previewExecution.name": "Ejecución de vista previa", + "diagnostics.previewExecution.recommendation": "Ejecuta la vista previa e inspecciona diagnósticos por errores de ejecución/runtime.", + "diagnostics.previewExecution.failed": "La ejecución de vista previa falló.", + "diagnostics.previewExecution.cancelled": "La ejecución de vista previa fue cancelada.", + "diagnostics.previewExecution.done": "{0} fila(s) en {1}ms.", + "diagnostics.previewExecution.none": "No se detectaron problemas en la ejecución de vista previa.", + "diagnostics.ddlCompilation.name": "Compilación DDL", + "diagnostics.ddlCompilation.recommendation": "Corrige los diagnósticos de compilación DDL antes de ejecutar.", + "diagnostics.ddlCompilation.failed": "La compilación DDL falló.", + "diagnostics.ddlCompilation.warningCount": "{0} advertencia(s) reportada(s) por el compilador DDL.", + "diagnostics.ddlCompilation.ok": "Compilación DDL exitosa.", + "diagnostics.ddlOutput.name": "DDL Output", + "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", + "diagnostics.ddlOutput.none": "No DDL statements generated yet.", + "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", + "diagnostics.undo.name": "Historial de deshacer", + "diagnostics.undo.recommendation": "El historial solo está en memoria; guarda tu canvas regularmente.", + "diagnostics.undo.saved": "Canvas guardado (sin cambios sin guardar).", + "diagnostics.undo.unsavedDeep": "Cambios sin guardar con {0} pasos de deshacer.", + "diagnostics.undo.unsaved": "Cambios sin guardar - {0} paso(s) de deshacer disponibles.", + "diagnostics.report.title": "AkkornStudio - Informe de diagnóstico", + "diagnostics.report.generated": "Generado", + "diagnostics.report.overall": "General", + "diagnostics.report.details": "Detalles", + "diagnostics.report.recommendation": "Recomendación", + "diagnostics.report.lastCheck": "Última verificación", + "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", + "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", + "preview.providerLabel": "Provider", + "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", + "preview.schemaAnalysis.run": "Run Analysis", + "preview.schemaAnalysis.cancel": "Cancel", + "preview.schemaAnalysis.issues": "Issues", + "preview.schemaAnalysis.clearFilters": "Clear Filters", + "preview.schemaAnalysis.clearBlacklist": "Clear Blacklist", + "preview.schemaAnalysis.severity": "Severity", + "preview.schemaAnalysis.severity.info": "Info", + "preview.schemaAnalysis.severity.warning": "Warning", + "preview.schemaAnalysis.severity.critical": "Critical", + "preview.schemaAnalysis.rule": "Rule", + "preview.schemaAnalysis.rule.fkCatalogInconsistent": "FK catalog inconsistent", + "preview.schemaAnalysis.rule.missingFk": "Missing FK", + "preview.schemaAnalysis.rule.namingConventionViolation": "Naming convention violation", + "preview.schemaAnalysis.rule.lowSemanticName": "Low semantic name", + "preview.schemaAnalysis.rule.missingRequiredComment": "Missing required comment", + "preview.schemaAnalysis.rule.nf1HintMultiValued": "1NF hint: multi-valued", + "preview.schemaAnalysis.rule.nf2HintPartialDependency": "2NF hint: partial dependency", + "preview.schemaAnalysis.rule.nf3HintTransitiveDependency": "3NF hint: transitive dependency", + "preview.schemaAnalysis.minConfidence": "Min Confidence", + "preview.schemaAnalysis.tableFilter": "Table Filter", + "preview.schemaAnalysis.tableFilterWatermark": "schema.table", + "preview.schemaAnalysis.ignore": "Execution Filters", + "preview.schemaAnalysis.ignoreViews": "Ignore views and materialized views", + "preview.schemaAnalysis.blacklist": "Table blacklist", + "preview.schemaAnalysis.blacklistAdd": "Add", + "preview.schemaAnalysis.blacklistRemove": "Remove selected", + "preview.schemaAnalysis.ignoreTable.placeholder": "schema.table", + "preview.schemaAnalysis.details": "Details", + "preview.schemaAnalysis.evidence": "Evidence", + "preview.schemaAnalysis.suggestions": "Suggestions", + "preview.schemaAnalysis.ruleDiagnostics": "Rule Diagnostics", + "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", + "preview.schemaAnalysis.copySql": "Copy SQL", + "preview.schemaAnalysis.applyToCanvas": "Apply to Canvas", + "preview.schemaAnalysis.summary.issues": "Issues:", + "preview.schemaAnalysis.summary.rawPrefix": "(raw:", + "preview.schemaAnalysis.summary.critical": "| Critical:", + "preview.schemaAnalysis.summary.warning": "| Warning:", + "preview.schemaAnalysis.summary.info": "| Info:", + "preview.schemaAnalysis.state.metadataUnavailable": "Metadata unavailable for structural analysis.", + "preview.schemaAnalysis.state.cancelled": "Analysis cancelled by the user.", + "preview.schemaAnalysis.state.partialTimeout": "Analysis finished partially due to timeout.", + "preview.schemaAnalysis.state.failed": "Structural analysis failed.", + "preview.schemaAnalysis.state.empty": "No inferable structural issue was detected.", + "preview.schemaAnalysis.state.noFilterMatch": "No issue matches the selected filters.", + "preview.schemaAnalysis.state.noIssueSelected": "No issue selected.", + "preview.schemaAnalysis.state.noSqlCandidate": "No SQL candidate available.", + "preview.schemaAnalysis.actionBlockedTooltip": "Action unavailable for the current risk level or capability.", + "common.navigate": "Navigate", + "common.close": "Close", + "common.esc": "Esc", + "common.ms": "ms", + "common.zero": "0", + "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", + "nodesList.empty": "No nodes found", + "nodesList.emptyHint": "Adjust the search term to explore available types", + "schema.emptyFiltered": "No objects found for the current filter", + "start.lastSnapshot": "Last snapshot", + "app.brandBadge": "VS", + "property.tab.properties": "Properties", + "property.tab.projectSettings": "Project Settings", + "property.nodeType": "NODE TYPE", + "property.selectNodeHint": "Select a node to edit its properties.", + "property.namingConventions": "Naming Conventions", + "property.aliasConvention": "Alias convention", + "property.enforceAliasNaming": "Enforce alias naming", + "property.warnReservedSql": "Warn on reserved SQL keywords", + "property.maxAliasLength": "Max alias length", + "property.maxAliasLengthDefault": "64", + "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", + "start.tips": "Tips", + "start.tips.quick": "Consejos rápidos", + "start.tips.item1": "1. Haz clic en Nuevo diagrama para empezar desde cero.", + "start.tips.item2": "2. Usa plantillas para acelerar el prototipado.", + "start.tips.item3": "3. Abre conexiones guardadas para cargar tablas reales.", + "start.tips.shortcut": "Atajo: CTRL+SHIFT+P abre la paleta de comandos.", + "start.workspace": "WORKSPACE", + "start.resumeTitle": "Continue where you left off", + "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", + "start.chip.quickFlow": "Quick flow", + "start.chip.templates": "Templates", + "start.chip.connections": "Connections", + "start.savedConnectionsTitle": "Saved Connections", + "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", + "start.noConnectionsTitle": "No connections configured yet", + "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", + "start.newConnection": "+ New Connection", + "start.recentProjectsTitle": "Recent Projects", + "start.searchRecent": "Search recent project...", + "start.quickActions": "Quick actions", + "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", + "start.noRecentTitle": "No recent projects yet", + "start.noRecentSubtitle": "Use the quick actions card above to get started.", + "start.exploreTemplates": "Explore templates", + "start.templatesFavoritesHint": "Favorites on top", + "start.favoriteTemplate": "Favorite template", + "node.columnSetPreview": "ColumnSet preview", + "node.view": "VIEW", + "node.tableDefinition": "Table Definition", + "node.join": "JOIN", + "node.window.addPartition": "Agregar ranura PARTITION BY", + "node.window.removePartition": "Remover ranura PARTITION BY", + "node.window.addOrder": "Agregar ranura ORDER BY", + "node.window.removeOrder": "Remover ranura ORDER BY", + "sql.keyword.select": "SELECT", + "sql.keyword.from": "FROM", + "sql.keyword.join": "JOIN", + "sql.keyword.where": "WHERE", + "sql.keyword.limit": "LIMIT", + "sqlImporter.close": "Close SQL importer", + "sqlImporter.report.imported": "Importado", + "sqlImporter.report.partial": "Parcial", + "sqlImporter.report.skipped": "Omitido", + "benchmark.close": "Close benchmark", + "benchmark.p95": "P95", + "benchmark.n": "N", + "liveSql.safePreview": "SAFE PREVIEW MODE", + "liveSql.title": "LIVE SQL", + "liveSql.blocked": "BLOCKED", + "liveSql.copy": "Copy", + "liveSql.format": "Format", + "liveSql.benchmark": "Benchmark", + "liveSql.explain": "Explain", + "liveSql.actionsHint": "Performance tools", + "ddl.dialog.title": "Ejecutar DDL", + "ddl.dialog.execute": "Ejecutar", + "ddl.dialog.cancel": "Cancelar", + "ddl.dialog.close": "Cerrar", + "ddl.dialog.stopOnError": "Detener en el primer fallo", + "ddl.dialog.confirmDestructive": "Confirmo la ejecución de sentencias destructivas (DROP TABLE)", + "ddl.dialog.reviewBeforeRun": "Revisa el script DDL antes de confirmar.", + "ddl.dialog.confirmQuestion": "¿Confirmar ejecución DDL en la base conectada?", + "ddl.dialog.irreversibleWarning": "Esta acción puede cambiar el esquema de forma irreversible.", + "ddl.dialog.mustConfirmDestructive": "Confirma la ejecución destructiva para continuar.", + "ddl.dialog.executing": "Ejecutando...", + "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", + "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", + "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", + "ddl.execute.result.failed": "Failed to execute DDL.", + "ddl.execute.result.cancelled": "Execution cancelled by the user.", + "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", + "context.deleteSingle": "Delete {0}", + "context.deleteMultiple": "Delete {0} nodes", + "context.bringForward": "Bring Forward (Ctrl+PgUp)", + "context.sendBackward": "Send Backward (Ctrl+PgDown)", + "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", + "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", + "context.normalizeLayers": "Normalize Layers", + "context.deleteWire": "Delete wire", + "context.addNode": "Add Node (Shift+A)", + "context.undoWithDescription": "Undo {0}", + "context.redo": "Redo", + "shortcuts.windowTitle": "Keyboard Shortcuts", + "shortcuts.headerTitle": "AkkornStudio - Shortcuts", + "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", + "shortcuts.filterWatermark": "Filter shortcuts by key or action...", + "shortcuts.resultCount": "{0} shortcuts", + "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", + "shortcuts.noneFound": "No shortcuts found.", + "shortcuts.section.fileGeneral": "Archivo y general", + "shortcuts.section.editing": "Edición", + "shortcuts.section.canvasNavigation": "Canvas y navegación", + "shortcuts.section.zoomPanPrecision": "Zoom, paneo y precisión", + "shortcuts.section.previewInspection": "Vista previa e inspección", + "shortcuts.key.deleteOrBackspace": "Del o Backspace", + "shortcuts.key.middleDrag": "Botón medio + arrastrar", + "shortcuts.key.rightDrag": "Botón derecho + arrastrar", + "shortcuts.key.spaceDrag": "Espacio + arrastrar", + "shortcuts.key.altLeftDrag": "Alt + arrastre izquierdo", + "shortcuts.key.arrows": "Flechas", + "shortcuts.key.shiftArrows": "Shift + Flechas", + "shortcuts.action.openShortcutScreen": "Abrir esta pantalla de atajos", + "shortcuts.action.newCanvas": "Nuevo canvas", + "shortcuts.action.openFile": "Abrir archivo", + "shortcuts.action.save": "Guardar", + "shortcuts.action.saveAs": "Guardar como", + "shortcuts.action.commandPalette": "Paleta de comandos", + "shortcuts.action.undo": "Deshacer", + "shortcuts.action.redo": "Rehacer", + "shortcuts.action.selectAll": "Seleccionar todo", + "shortcuts.action.deleteSelection": "Eliminar selección", + "shortcuts.action.closeOverlayCancel": "Cerrar overlays / cancelar acciones", + "shortcuts.action.openNodeSearch": "Abrir búsqueda de nodos", + "shortcuts.action.resetViewport": "Restablecer viewport", + "shortcuts.action.centerSelection": "Centrar selección", + "shortcuts.action.fitSelection": "Ajustar selección", + "shortcuts.action.autoLayout": "Diseño automático", + "shortcuts.action.toggleSnapToGrid": "Alternar ajuste a cuadrícula", + "shortcuts.action.bringForward": "Traer adelante", + "shortcuts.action.sendBackward": "Enviar atrás", + "shortcuts.action.bringToFront": "Traer al frente", + "shortcuts.action.sendToBack": "Enviar al fondo", + "shortcuts.action.zoomInOut": "Acercar / alejar", + "shortcuts.action.pan": "Panear", + "shortcuts.action.temporaryPan": "Paneo temporal", + "shortcuts.action.alternatePan": "Paneo alternativo", + "shortcuts.action.fineNudge": "Movimiento fino de selección", + "shortcuts.action.fastNudge": "Movimiento rápido", + "shortcuts.action.togglePreview": "Alternar vista previa de datos", + "shortcuts.action.explainPlan": "Plan de ejecución", + "shortcuts.action.runPreview": "Ejecutar vista previa", + "shortcuts.action.connectionManager": "Administrador de conexiones", + "shortcuts.action.flowVersionHistory": "Historial de versiones del flujo", + "shortcuts.resetAll": "Restablecer todo", + "shortcuts.customized": "Personalizado", + "shortcuts.default": "Predeterminado", + "shortcuts.apply": "Aplicar", + "shortcuts.reset": "Restablecer", + "shortcuts.status.resetAllSuccess": "Todos los atajos se restablecieron a los valores predeterminados.", + "shortcuts.status.updated": "Atajo actualizado.", + "shortcuts.status.reset": "Atajo restablecido al valor predeterminado.", + "shortcuts.status.updateFailed": "No se pudo actualizar el atajo.", + "toast.severity.success": "Success", + "toast.severity.warning": "Warning", + "toast.severity.error": "Error", + "toast.details.success": "Success Details", + "toast.details.warning": "Warning Details", + "toast.details.error": "Error Details", + "diagnostics.area.cteEditor": "Editor CTE", + "diagnostics.area.viewEditor": "Editor de vista", + "diagnostics.area.subEditor": "Subeditor", + "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", + "diagnostics.recommendation.reloadFileIfNeeded": "Recarga el archivo si es necesario.", + "diagnostics.viewEditor.exitFailed": "No se pudo salir: {0}", + "diagnostics.viewEditor.canvasIncomplete": "el canvas está incompleto.", + "diagnostics.viewEditor.exitRecommendation": "Conecta un ResultOutput válido o usa el comando descartar.", + "diagnostics.viewEditor.restoreParentFailed": "Falló la restauración del canvas padre. El subgrafo fue descartado.", + "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", + "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", + "diagnostics.canvasMigration.openWarning": "Open: {0}", + "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", + "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", + "diagnostics.recommendation.resaveLatestSchema": "Revisa diagnósticos y vuelve a guardar el canvas para persistir el esquema más reciente.", + "diagnostics.recommendation.saveMigratedSchema": "Revisa diagnósticos y guarda el canvas para persistir el esquema migrado.", + "file.saveDialog.title": "Save Canvas", + "file.saveDialog.suggestedName": "Query1", + "file.save.success": "Canvas saved successfully.", + "file.save.failedWithReason": "Save failed: {0}", + "file.openDialog.title": "Open Canvas", + "file.open.failedWithReason": "Open failed: {0}", + "file.open.success": "Canvas opened successfully.", + "file.open.successWithWarnings": "Canvas opened with warnings.", + "session.restore.failedWithReason": "Restore failed: {0}", + "session.restore.successWithWarnings": "Session restored with warnings.", + "session.restore.success": "Session restored successfully.", + "export.documentation.dialogTitle": "Export Flow Documentation", + "export.documentation.success": "Documentation exported successfully.", + "export.documentation.failed": "Documentation export failed.", + "export.failed.pathPermissionsHint": "Check file path and permissions.", + "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", + "export.dialogTitleByExtension": "Export as {0}", + "export.success": "Export completed successfully.", + "export.failed": "Export failed.", + "fileHistory.currentFile.none": "No file selected", + "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", + "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", + "fileHistory.status.countAvailable": "{0} local version(s) available.", + "fileHistory.restore.failedWithReason": "Restore failed: {0}", + "fileHistory.restore.successFrom": "Restored version from {0}.", + "preview.status.cancelled": "Cancelled", + "preview.status.error": "Error", + "preview.status.ready": "Ready", + "preview.runningWithMs": "Running... {0}ms", + "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", + "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", + "explain.errorWithReason": "Explain plan error: {0}", + "explain.noSql": "No SQL to explain. Build a query on the canvas first.", + "ddl.compilationFailed": "Compilation failed", + "ddl.compileErrorWithReason": "DDL compile error: {0}", + "command.undo.name": "Undo", + "command.undo.description": "Undo last action", + "command.redo.name": "Redo", + "command.redo.description": "Redo last undone action", + "command.addNode.name": "Add Node", + "command.addNode.description": "Open node search menu to add a node", + "command.bringForward.name": "Bring Forward", + "command.bringForward.description": "Move selected nodes one layer forward", + "command.sendBackward.name": "Send Backward", + "command.sendBackward.description": "Move selected nodes one layer backward", + "command.bringToFront.name": "Bring to Front", + "command.bringToFront.description": "Move selected nodes to top layer", + "command.sendToBack.name": "Send to Back", + "command.sendToBack.description": "Move selected nodes to bottom layer", + "command.normalizeLayers.name": "Normalize Layers", + "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", + "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", + "main.orphanSuffix": "Orphan(s)", + "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", + "main.namingPrefix": "Naming", + "fileHistory.compressedLabel": "Compressed:", + "schema.itemsSuffix": "item(s)", + "property.panel.title": "Properties", + "property.panel.multiSelected": "{0} nodes selected", + "sqlImporter.status.pasteSelect": "Pega una sentencia SELECT arriba y luego haz clic en Importar.", + "sqlImporter.status.inputTooLarge": "La entrada SQL es demasiado grande ({0:N0} caracteres). El límite es {1:N0}. Divide la consulta o aumenta el límite de importación.", + "sqlImporter.status.parsing": "Analizando SQL...", + "sqlImporter.status.done": "Listo - {0} importado(s), {1} parcial(es), {2} omitido(s).", + "sqlImporter.status.cancelledByUser": "Importación cancelada por el usuario.", + "sqlImporter.status.timeout": "La importación agotó el tiempo tras {0:0.#}s. Prueba una consulta más pequeña o aumenta el timeout.", + "sqlImporter.status.parseError": "Error de parseo: {0}", + "diagnostics.area.undoRedoTransaction": "Transacción Deshacer/Rehacer", + "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", + "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", + "node.preview.noCatalog": "No catalog available", + "connection.error.searchMenuNotInitialized": "el menú de búsqueda no está inicializado", + "connection.error.timeoutReachability": "Tiempo de conexión agotado - verifica que el servidor sea accesible y aumenta el timeout si es necesario.", + "connection.error.authenticationFailedForProvider": "Falló la autenticación - verifica usuario y contraseña para {0}.", + "connection.error.databaseNotFoundForProvider": "Base de datos no encontrada - confirma que el nombre exista en {0}.", + "connection.error.hostNotFound": "Host no encontrado - verifica dirección del servidor y resolución DNS.", + "connection.error.portRefused": "Conexión al puerto rechazada - verifica el puerto y que el servidor/firewall permitan acceso.", + "connection.error.sslTls": "Error SSL/TLS - verifica configuración SSL del servidor o desactiva SSL para conexiones locales.", + "connection.error.timeoutOverloaded": "Tiempo de conexión agotado - el servidor puede estar sobrecargado o inaccesible. Intenta aumentar el timeout.", + "connection.error.insufficientPrivileges": "Privilegios insuficientes - el usuario puede no tener permiso para conectarse a esta base.", + "diagnostics.area.connection": "Conexión", + "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", + "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", + "undoRedo.transaction.unnamed": "unnamed transaction", + "benchmark.runLabelDefault": "Run 1", + "benchmark.runLabelPattern": "Run {0}", + "benchmark.status.failedWithReason": "Benchmark falló: {0}", + "benchmark.status.noSql": "No hay SQL para benchmark - construye una consulta primero.", + "benchmark.status.warmupProgress": "Calentamiento {0}/{1}...", + "benchmark.status.iterationProgress": "Iteración {0}/{1}...", + "benchmark.status.done": "Listo - {0}", + "benchmark.status.cancelled": "Benchmark cancelado.", + "app.windowTitle": "AkkornStudio", + "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", + "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", + "sqlImporter.error.selectFromNotFound": "No se pudo encontrar SELECT ... FROM en la consulta.", + "sqlImporter.error.fromClauseParseFailed": "No se pudo parsear la cláusula FROM.", + "sqlImporter.error.syntaxUnterminatedString": "Error de sintaxis en línea {0}, columna {1}: literal de cadena sin cerrar.", + "sqlImporter.error.missingClosingParenthesis": "falta ')' de cierre", + "sqlImporter.error.unexpectedClosingParenthesis": "')' inesperado", + "sqlImporter.error.syntaxAtLineColumn": "Error de sintaxis en línea {0}, columna {1}: {2}.", + "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", + "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", + "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", + "errorDiagnostics.connection.label": "Connection failed", + "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", + "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", + "errorDiagnostics.authorization.label": "Authorization error", + "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", + "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", + "errorDiagnostics.timeout.label": "Query timeout", + "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", + "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", + "errorDiagnostics.schema.label": "Schema error", + "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", + "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", + "errorDiagnostics.syntax.label": "SQL syntax error", + "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", + "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", + "errorDiagnostics.compatibility.label": "Compatibility error", + "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", + "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", + "errorDiagnostics.unknown.label": "Unexpected error", + "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", + "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", + "error.mainWindow.invalidDataContext": "El DataContext de MainWindow debe ser un ShellViewModel.", + "error.mainWindow.canvasNotInitialized": "CanvasViewModel no fue inicializado.", + "error.mainWindow.ddlPreviewUnavailable": "La vista previa DDL no está disponible para el canvas actual.", + "themeJson.editor.template": "{\n \"meta\": { \"name\": \"Tema personalizado\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", + "themeJson.error.pasteBeforeApply": "Pega un JSON de tema antes de aplicar.", + "themeJson.error.invalidJson": "JSON inválido: {0}", + "themeJson.error.emptyPayload": "JSON inválido: payload vacío.", + "themeJson.error.invalidTheme": "Tema inválido: {0}", + "themeJson.error.appliedButSaveFailed": "Tema aplicado, pero no se pudo guardar: {0}", + "themeJson.success.appliedAndSaved": "Tema JSON aplicado y guardado.", + "themeJson.success.customRemoved": "Tema personalizado eliminado. Reinicia la app para volver completamente al tema predeterminado.", + "themeJson.error.restoreDefaultFailed": "Error al restaurar el tema predeterminado: {0}", + "themeValidator.error.configNull": "La configuración del tema es nula.", + "themeValidator.warning.noSections": "El tema no tiene secciones de colores o tipografía; no hay nada para aplicar.", + "themeValidator.warning.invalidColor": "{0} tiene un color inválido '{1}'. Esta clave será ignorada.", + "themeValidator.warning.sizeOutOfRange": "{0}={1} está fuera del rango (8..48). Esta clave será ignorada.", + "queryExecutor.error.openConnectionMethodNotFound": "No se puede encontrar el método OpenConnectionAsync en el orquestador", + "queryExecutor.error.openConnectionInvokeFailed": "Error al invocar OpenConnectionAsync", + "ddlImporter.warning.viewSelectNotReconstructable": "Vista '{0}': el SELECT de la vista no puede reconstruirse visualmente; edítalo manualmente en el subcanvas.", + "ddlImporter.error.tableNotFoundInMetadata": "La tabla '{0}' no se encontró en los metadatos actuales.", + "main.window.untitled": "Sin título", + "main.subEditor.noSeedProvided": "No se proporcionó seed de subeditor para {0}.", + "main.layerOrder.bringToFront": "Traer al frente", + "main.layerOrder.sendToBack": "Enviar atrás", + "main.layerOrder.bringForward": "Adelantar capa", + "main.layerOrder.sendBackward": "Retroceder capa", + "main.layerOrder.normalizeLayers": "Normalizar capas", + "export.fileType.html": "Archivos HTML", + "export.fileType.json": "Archivos JSON", + "export.fileType.csv": "Archivos CSV", + "export.fileType.excel": "Archivos Excel", + "commandPalette.templatePrefix": "Plantilla: {0}", + "themeLoader.status.notFoundWithPath": "Archivo de tema no encontrado: {0}", + "themeLoader.status.deserializedNull": "El JSON del tema se deserializó como null.", + "themeLoader.status.loaded": "JSON del tema cargado correctamente.", + "credential.error.ciphertextTooShort": "El bloque de texto cifrado es demasiado corto.", + "credential.error.dpapiWindowsOnly": "DPAPI solo está disponible en Windows.", + "credential.warning.loadVaultFailed": "No se pudo cargar el almacén de credenciales {0}: {1}", + "credential.warning.persistVaultFailed": "No se pudo guardar el almacén de credenciales {0}: {1}", + "snippetStore.warning.loadFailed": "No se pudieron cargar snippets desde {0}: {1}", + "snippetStore.warning.saveFailed": "No se pudieron guardar snippets: {0}", + "flowVersionStore.warning.loadFailed": "No se pudieron cargar versiones de flujo desde {0}: {1}", + "flowVersionStore.warning.saveFailed": "No se pudieron guardar versiones de flujo: {0}", + "queryExecutor.error.queryEmpty": "La consulta no puede estar vacía", + "queryExecutor.error.providerNotSupported": "El proveedor {0} no es compatible", + "queryExecutor.error.singleStatementOnly": "La vista previa solo acepta una única sentencia SQL.", + "queryExecutor.error.queryEmptyWithPeriod": "La consulta no puede estar vacía.", + "queryExecutor.error.readOnlyOnly": "El modo de vista previa solo admite sentencias SQL de solo lectura.", + "queryExecutor.error.namedParametersNotSupported": "El modo de vista previa no admite parámetros vinculados en SQL de ejecución. Usa literales seguros inline o ejecuta la consulta fuera de la vista previa.", + "queryExecutor.error.positionalParametersNotSupported": "El modo de vista previa no admite placeholders posicionales (? o ).", + "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "Alinear nodos seleccionados al borde inferior", + "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "Alinear nodos seleccionados al borde izquierdo", + "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "Alinear nodos seleccionados al borde derecho", + "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "Alinear nodos seleccionados al borde superior", + "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "Aplicar cambios del subcanvas CTE y volver al canvas padre", + "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "Organizar nodos automáticamente en columnas lógicas", + "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "Centrar nodos seleccionados en el eje horizontal", + "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "Centrar nodos seleccionados en el eje vertical", + "commandPalette.description.clear_canvas_and_start_fresh": "Limpiar canvas y comenzar desde cero", + "commandPalette.description.clear_node_selection": "Limpiar selección de nodos", + "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "Convertir alias a la convención configurada en ajustes del proyecto", + "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "Crear checkpoints, comparar versiones lado a lado y restaurar un estado anterior del canvas", + "commandPalette.description.delete_the_selected_nodes": "Eliminar nodos seleccionados", + "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "Descartar cambios del subeditor actual y volver al canvas padre", + "commandPalette.description.execute_the_current_query_in_preview": "Ejecutar la consulta actual en vista previa", + "commandPalette.description.fit_all_nodes_into_the_visible_area": "Ajustar todos los nodos al área visible", + "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "Generar archivo CSV desde el primer nodo de exportación CSV", + "commandPalette.description.generate_html_file_from_the_first_html_export_node": "Generar archivo HTML desde el primer nodo de exportación HTML", + "commandPalette.description.generate_json_file_from_the_first_json_export_node": "Generar archivo JSON desde el primer nodo de exportación JSON", + "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "Generar libro XLSX desde el primer nodo de exportación Excel", + "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "Inspeccionar plan de ejecución: tipos de scan, estrategias de join y estimaciones de costo", + "commandPalette.description.load_a_vsaq_canvas_file": "Cargar archivo de canvas .vsaq", + "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "Medir latencia promedio/mediana/p95 del SQL actual en N iteraciones", + "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "Abrir editor de subcanvas aislado para el nodo de definición CTE seleccionado", + "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "Abrir historial local de versiones creado en cada guardado y restaurar snapshots anteriores", + "commandPalette.description.open_output_preview_modal_for_the_active_mode": "Abrir modal de vista previa de salida para el modo activo", + "commandPalette.description.open_shortcut_reference_screen": "Abrir pantalla de referencia de atajos", + "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "Abrir administrador de conexiones para agregar, editar o cambiar conexiones de base de datos", + "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "Pegar SELECT y generar nodos automáticamente: FROM, JOIN, WHERE y LIMIT soportados", + "commandPalette.description.remove_all_nodes_not_connected_to_output": "Eliminar todos los nodos no conectados a la salida", + "commandPalette.description.reset_zoom_and_pan_to_default": "Restablecer zoom y desplazamiento a valores predeterminados", + "commandPalette.description.save_canvas_to_a_new_file": "Guardar canvas en un archivo nuevo", + "commandPalette.description.save_current_canvas": "Guardar canvas actual", + "commandPalette.description.save_markdown_documentation_of_the_current_flow": "Guardar documentación Markdown del flujo actual", + "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "Guardar nodos seleccionados como snippet reutilizable e insertarlo luego desde el buscador de nodos (⇧A)", + "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "Escanear todos los nodos de tabla del canvas para posibles joins según convenciones FK y patrones de nombres", + "commandPalette.description.select_all_nodes_on_canvas": "Seleccionar todos los nodos del canvas", + "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "Ajustar posiciones de nodos a cuadrícula de 16px (Ctrl+G)", + "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "Distribuir nodos seleccionados con espaciado horizontal igual", + "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "Distribuir nodos seleccionados con espaciado vertical igual", + "commandPalette.description.zoom_into_the_canvas": "Acercar en el canvas", + "commandPalette.description.zoom_out_of_the_canvas": "Alejar en el canvas", + "commandPalette.name.align_bottom": "Alinear Abajo", + "commandPalette.name.align_left": "Alinear Izquierda", + "commandPalette.name.align_right": "Alinear Derecha", + "commandPalette.name.align_top": "Alinear Arriba", + "commandPalette.name.analyze_all_joins": "Analizar Todos los Joins", + "commandPalette.name.auto_fix_naming": "Auto-Corregir Nombres", + "commandPalette.name.auto_layout": "Diseño automático", + "commandPalette.name.center_horizontally": "Centrar Horizontalmente", + "commandPalette.name.center_vertically": "Centrar Verticalmente", + "commandPalette.name.cleanup_orphans": "Limpiar Huérfanos", + "commandPalette.name.delete_selected": "Eliminar Seleccionados", + "commandPalette.name.deselect_all": "Deseleccionar Todo", + "commandPalette.name.discard_and_exit_editor": "Descartar y Salir del Editor", + "commandPalette.name.distribute_horizontally": "Distribuir Horizontalmente", + "commandPalette.name.distribute_vertically": "Distribuir Verticalmente", + "commandPalette.name.edit_selected_cte": "Editar CTE Seleccionado", + "commandPalette.name.exit_cte_editor": "Salir del Editor CTE", + "commandPalette.name.explain_plan": "Plan de Ejecución", + "commandPalette.name.export_csv": "Exportar CSV", + "commandPalette.name.export_documentation": "Exportar Documentación", + "commandPalette.name.export_excel": "Exportar Excel", + "commandPalette.name.export_html": "Exportar HTML", + "commandPalette.name.export_json": "Exportar JSON", + "commandPalette.name.file_save_load_history": "Historial de Guardado/Carga", + "commandPalette.name.fit_to_screen": "Ajustar a Pantalla", + "commandPalette.name.flow_version_history": "Historial de Versiones del Flujo", + "commandPalette.name.import_sql_to_graph": "Importar SQL a Grafo", + "commandPalette.name.keyboard_shortcuts": "Atajos de Teclado", + "commandPalette.name.manage_connections": "Administrar Conexiones", + "commandPalette.name.new_canvas": "Nuevo Canvas", + "commandPalette.name.open_file": "Abrir Archivo", + "commandPalette.name.reset_viewport": "Restablecer Vista", + "commandPalette.name.run_preview": "Ejecutar Vista Previa", + "commandPalette.name.run_query_benchmark": "Ejecutar Benchmark de Consulta", + "commandPalette.name.save": "Guardar", + "commandPalette.name.save_as": "Guardar Como", + "commandPalette.name.save_selection_as_snippet": "Guardar Selección como Snippet", + "commandPalette.name.select_all": "Seleccionar Todo", + "commandPalette.name.toggle_preview": "Alternar Vista Previa", + "commandPalette.name.toggle_snap_to_grid": "Alternar Ajuste a Cuadrícula", + "commandPalette.name.zoom_in": "Acercar", + "commandPalette.name.zoom_out": "Alejar", + "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 por ciento restaurar zoom paneo viewport", + "commandPalette.tags.align_bottom_edge_selection_nodes": "alinear borde inferior selección nodos", + "commandPalette.tags.align_center_middle_horizontal_nodes": "alinear centro medio horizontal nodos", + "commandPalette.tags.align_center_middle_vertical_nodes": "alinear centro medio vertical nodos", + "commandPalette.tags.align_left_edge_selection_nodes": "alinear borde izquierdo selección nodos", + "commandPalette.tags.align_right_edge_selection_nodes": "alinear borde derecho selección nodos", + "commandPalette.tags.align_top_edge_selection_nodes": "alinear borde superior selección nodos", + "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout vista restablecer zoom", + "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark rendimiento latencia tiempos perfil medir velocidad", + "commandPalette.tags.clear_selection": "limpiar selección", + "commandPalette.tags.connection_database_server_host_provider_switch": "conexión base datos servidor host proveedor cambiar", + "commandPalette.tags.create_insert_search_transform": "crear insertar buscar transformar", + "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas salir aplicar volver", + "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte vista subcanvas descartar salir forzar", + "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursivo editor subgrafo subcanvas aislar", + "commandPalette.tags.data_results_table_panel": "datos resultados tabla panel", + "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribuir espacio igual horizontal nodos", + "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribuir espacio igual vertical nodos", + "commandPalette.tags.execute_run_sql_query_results": "ejecutar correr sql consulta resultados", + "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan ejecución costo scan índice join rendimiento", + "commandPalette.tags.export_csv_file_tabular_output_save": "exportar csv archivo salida tabular guardar", + "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "exportar excel xlsx archivo salida tabular hoja cálculo guardar", + "commandPalette.tags.export_html_file_output_report_save": "exportar html archivo salida informe guardar", + "commandPalette.tags.export_json_file_output_save": "exportar json archivo salida guardar", + "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "exportar markdown doc documentación flujo guardar md", + "commandPalette.tags.export_persist_copy": "exportar persistir copia", + "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "archivo historial guardar cargar backup versiones restaurar local", + "commandPalette.tags.forward_history": "adelante historial", + "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "ayuda atajos hotkeys teclado referencia", + "commandPalette.tags.highlight_mark_all_nodes": "resaltar marcar todos nodos", + "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "importar sql pegar convertir grafo ingeniería inversa consulta", + "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analizar sugerir detectar foreign key relaciones heurística", + "commandPalette.tags.layer_z_order_back_selected_nodes": "capa orden z atrás nodos seleccionados", + "commandPalette.tags.layer_z_order_backward_selected_nodes": "capa orden z enviar atrás nodos seleccionados", + "commandPalette.tags.layer_z_order_forward_selected_nodes": "capa orden z traer adelante nodos seleccionados", + "commandPalette.tags.layer_z_order_front_selected_nodes": "capa orden z al frente nodos seleccionados", + "commandPalette.tags.layer_z_order_normalize_compact": "capa orden z normalizar compactar", + "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout organizar columnas auto legibilidad", + "commandPalette.tags.load_import_vsaq": "cargar importar vsaq", + "commandPalette.tags.magnify_enlarge": "aumentar ampliar", + "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "huérfano no usado desconectado limpiar eliminar nodos", + "commandPalette.tags.persist_write_disk": "persistir escribir disco", + "commandPalette.tags.remove_erase_nodes": "remover borrar nodos", + "commandPalette.tags.rename_alias_fix_naming_convention": "renombrar alias corregir convención nombres", + "commandPalette.tags.reset_clear_blank": "restablecer limpiar en blanco", + "commandPalette.tags.revert_back_history": "revertir volver historial", + "commandPalette.tags.shrink_reduce": "encoger reducir", + "commandPalette.tags.snap_grid_align_precision_position": "snap cuadrícula alinear precisión posición", + "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet guardar selección reutilizar plantilla favorito marcador", + "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "versión historial checkpoint diff restaurar snapshot comparar deshacer flujo", + "sqlEditor.diffPreview.title": "Transactional Diff Preview", + "sqlEditor.mutation.confirmExecute": "Confirm Execute", + "sqlEditor.tab.closeAnyway": "Close Anyway", + "sqlEditor.tab.keepTab": "Keep Tab", + "sqlEditor.status.ready": "Ready.", + "sqlEditor.telemetry.none": "No execution telemetry yet.", + "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", + "sqlEditor.telemetry.errors.none": "No aggregated errors.", + "sqlEditor.diff.none": "No transactional diff preview available.", + "sqlEditor.mutation.estimate.none": "No mutation estimate available.", + "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", + "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", + "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", + "sqlEditor.tab.noPendingClose": "No tab close pending.", + "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", + "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", + "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", + "sqlEditor.message.empty": "Execute a statement to see messages.", + "sqlEditor.message.success": "Execution completed successfully.", + "sqlEditor.result.summary.empty": "Rows: - Time: -", + "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", + "sqlEditor.file.save.canceled": "Save canceled.", + "sqlEditor.file.save.noPath": "No target path selected.", + "sqlEditor.file.save.success": "SQL file saved.", + "sqlEditor.file.save.failed": "Save failed.", + "sqlEditor.file.open.failed": "Open failed.", + "sqlEditor.file.open.notFound": "Selected SQL file was not found.", + "sqlEditor.file.open.success": "SQL file opened.", + "sqlEditor.status.executing": "Executing SQL...", + "sqlEditor.status.executingScript": "Executing SQL script...", + "sqlEditor.status.executingStep": "Executing {0}/{1}...", + "sqlEditor.status.canceling": "Canceling execution...", + "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", + "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", + "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", + "sqlEditor.status.success": "Execution succeeded.", + "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", + "sqlEditor.status.canceled": "Execution canceled.", + "sqlEditor.status.failed": "Execution failed.", + "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", + "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", + "sqlEditor.result.tabTitle": "Result {0}", + "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", + "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", + "sqlEditor.tab.closed": "Tab closed.", + "sqlEditor.tab.closeCanceled": "Tab close canceled.", + "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", + "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", + "sqlEditor.error.noConnection": "No active database connection for SQL execution.", + "sqlEditor.error.executionCanceled": "SQL execution was canceled.", + "sqlEditor.tab.scriptTitle": "Script {0}", + "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", + "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", + "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", + "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", + "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", + "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", + "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", + "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", + "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", + "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", + "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", + "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", + "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", + "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", + "sqlEditor.results.title": "Results", + "sqlEditor.saveSql.fileType": "SQL Files", + "sqlEditor.saveSql.pickerTitle": "Save SQL File", + "sqlEditor.export.pickerTitle": "Export SQL Data", + "sqlEditor.export.status.noResultTitle": "No execution result available for export.", + "sqlEditor.export.status.noResultDetail": "Execute a query first.", + "sqlEditor.export.status.successTitle": "Report exported.", + "sqlEditor.export.status.failedTitle": "Failed to export report.", + "sqlEditor.export.fileType.html": "HTML File", + "sqlEditor.export.fileType.json": "JSON File", + "sqlEditor.export.fileType.csv": "CSV File", + "sqlEditor.export.fileType.xlsx": "Excel Workbook", + "sqlEditor.export.defaultFileBase": "report", + "sqlEditor.export.defaultTitle": "SQL Report", + "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", + "sqlEditor.export.type.html.title": "HTML full-feature report", + "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", + "sqlEditor.export.type.json.title": "JSON execution contract", + "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", + "sqlEditor.export.type.csv.title": "CSV data export", + "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", + "sqlEditor.export.type.xlsx.title": "Excel workbook export", + "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", + "sqlEditor.export.dialog.windowTitle": "Export SQL Data", + "sqlEditor.export.dialog.title": "Export SQL Data", + "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", + "sqlEditor.export.dialog.confirm": "Export", + "sqlEditor.export.dialog.fileNameWatermark": "report.html", + "sqlEditor.export.dialog.titleWatermark": "SQL Report", + "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", + "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", + "sqlEditor.export.dialog.section.fileName": "FILE NAME", + "sqlEditor.export.dialog.section.reportTitle": "TITLE", + "sqlEditor.export.dialog.section.description": "DESCRIPTION", + "sqlEditor.export.dialog.section.options": "OPTIONS", + "sqlEditor.export.option.includeSchema": "Include output schema", + "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", + "sqlEditor.export.option.includeMetadata": "Include optional metadata", + "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", + "sqlEditor.export.badge.offline": "OFFLINE READY", + "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", + "sqlEditor.export.badge.dataOnly": "DATA ONLY" +} diff --git a/src/DBWeaver.UI/Assets/Localization/ja-JP.json b/src/AkkornStudio.UI/Assets/Localization/ja-JP.json similarity index 98% rename from src/DBWeaver.UI/Assets/Localization/ja-JP.json rename to src/AkkornStudio.UI/Assets/Localization/ja-JP.json index 6bad2726..6e03f23d 100644 --- a/src/DBWeaver.UI/Assets/Localization/ja-JP.json +++ b/src/AkkornStudio.UI/Assets/Localization/ja-JP.json @@ -1,1105 +1,1105 @@ -{ - "main.brand": "DBWeaver", - "main.tab.query1": "Query 1", - "main.new": "New", - "main.open": "Open", - "main.save": "Save", - "main.history": "History", - "main.layout": "Layout", - "main.preview": "Preview", - "main.undo": "Undo", - "main.redo": "Redo", - "main.cleanupOrphans": "Cleanup orphan nodes", - "main.autoFixAliasNaming": "Auto-fix alias naming", - "main.autoLayoutCanvas": "Auto layout canvas", - "main.toggleDataPreview": "Toggle data preview", - "main.language": "Language", - "main.restore.prompt": "Previous session found — restore the last canvas?", - "main.restore.button": "Restore session", - "main.cteEditor.editingPrefix": "Editing CTE: ", - "main.cteEditor.backToCanvas": "Back to Canvas", - "main.cteEditor.exitA11y": "Exit CTE editor", - "main.viewEditor.editingPrefix": "DDL > View: ", - "main.viewEditor.backToCanvas": "Back to DDL", - "main.viewEditor.exitA11y": "Exit view editor", - "connection.title": "Connection Manager", - "connection.subtitle": "作業フローを中断せずに接続の設定・テスト・有効化を行います", - "connection.none": "No connection", - "connection.active": "ACTIVE", - "connection.health.online": "Online", - "connection.health.degraded": "Degraded", - "connection.health.offline": "Offline", - "connection.tooltip.none": "No active connection — click to manage", - "connection.ping": "Ping", - "connection.saved": "SAVED CONNECTIONS", - "connection.new": "New Connection", - "connection.selectOrCreate": "Select a connection or create a new one", - "connection.name": "Connection Name", - "connection.provider": "Provider", - "connection.host": "Host", - "connection.port": "Port", - "connection.database": "Database", - "connection.sqlitePath": "SQLite Path", - "connection.sqliteBrowse": "Browse", - "connection.sqliteCreate": "Create", - "connection.username": "Username", - "connection.password": "Password", - "connection.timeout": "Timeout (seconds)", - "connection.test": "Test", - "connection.save": "Save", - "connection.connect": "Connect", - "connection.action.testConnection": "Test connection", - "connection.action.saveConnection": "Save connection", - "connection.action.connectConnection": "Connect connection", - "connection.status.connecting": "接続中...", - "connection.status.connected": "接続済み", - "connection.status.testing": "テスト中...", - "connection.status.failedPrefix": "接続に失敗しました", - "connection.status.metadataUnavailable": "接続に失敗しました: メタデータが利用できません。", - "connection.status.highLatency": "高遅延", - "connection.watermark.name": "本番DB", - "connection.watermark.host": "localhost", - "connection.watermark.port": "5432", - "connection.watermark.database": "database_name", - "connection.watermark.username": "user", - "connection.watermark.password": "••••••••", - "connection.watermark.timeout": "30", - "main.connectingDb": "Connecting to database...", - "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", - "status.nodesSeparator": " nodes · ", - "status.connectionsSuffix": " connections", - "status.undo": "Undo: ", - "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", - "connection.disconnect": "Disconnect", - "connection.action.disconnectConnection": "Disconnect connection", - "connectionTab.active": "ACTIVE CONNECTION", - "connectionTab.none": "No active connection", - "connectionTab.saved": "SAVED CONNECTIONS", - "connectionTab.new": "+ New Connection", - "schema.database": "DATABASE", - "schema.search": "Search tables, columns...", - "schema.loading": "Searching tables, columns...", - "schema.noConnection": "No Connection", - "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", - "schema.emptyNoTables": "No tables found", - "fileHistory.title": "Save/Load Version History", - "fileHistory.reload": "Reload", - "fileHistory.restoreSelected": "Restore selected", - "fileHistory.empty": "No local versions yet", - "fileHistory.emptyHint": "Save this file to generate version history.", - "preview.title": "Data Preview", - "preview.subtitle": "Review data and diagnostics before continuing", - "preview.run": "Run", - "preview.cancel": "Cancel", - "preview.tab.preview": "Preview", - "preview.tab.sql": "SQL", - "preview.close": "Close preview", - "preview.running": "Running preview query… ", - "preview.clickCancel": "Click Cancel to stop", - "preview.cancelled": "Query cancelled", - "preview.runAgain": "Press Run to execute again", - "preview.failed": "Preview execution failed", - "preview.technical": "TECHNICAL DETAILS", - "preview.noData": "No data yet", - "preview.f3Hint": "Press F3 or Space to run the current query", - "sqlImporter.title": "Import SQL to Graph", - "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", - "sqlImporter.sqlStatement": "SQL STATEMENT", - "sqlImporter.supported": "Supported: ", - "sqlImporter.import": "Import", - "sqlImporter.report": "CONVERSION REPORT", - "sqlEditor.mutation.dialogTitle": "変更の確認", - "sqlEditor.mutation.dialogSubtitle": "実行を確定する前に影響を確認してください", - "search.empty": "No nodes found", - "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", - "search.shortcut": "⇧A", - "search.spawn": "Spawn", - "commandPalette.empty": "検索に一致するコマンドがありません", - "commandPalette.search": "コマンド検索", - "commandPalette.shortcut": "CTRL+SHIFT+P", - "commandPalette.execute": "実行", - "context.editCte": "Edit Selected CTE", - "context.editViewSubcanvas": "Edit View Subcanvas", - "explain.title": "Explain Plan", - "explain.sql": "SQL", - "explain.option.analyze": "解析", - "explain.option.buffers": "バッファ", - "explain.badge.simulated": "シミュレーション", - "explain.timing.planning": "計画:", - "explain.timing.execution": "実行:", - "explain.section.snapshotComparison": "スナップショット比較", - "explain.section.indexRecommendations": "インデックスの推奨", - "explain.section.history": "履歴", - "explain.detail.estimated": "推定", - "explain.detail.actual": "実測", - "explain.detail.error": "誤差", - "explain.detail.time": "時間", - "explain.detail.loops": "ループ", - "explain.rerun": "Re-run", - "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", - "explain.running": "Running EXPLAIN…", - "explain.failed": "⚠ EXPLAIN failed", - "explain.noPlan": "No plan yet", - "explain.rerunHint": "Press Re-run to execute EXPLAIN", - "explain.header.operation": "操作", - "explain.header.cost": "コスト", - "explain.header.rows": "行数", - "explain.header.alert": "警告", - "explain.mode.list": "リスト", - "explain.mode.tree": "ツリー", - "explain.action.snapshot": "スナップショットを保存", - "explain.action.copyJson": "JSONをコピー", - "explain.action.copyText": "テキストをコピー", - "explain.action.saveJson": ".json を保存", - "explain.action.openDalibo": "Daliboで開く", - "explain.legend.seqscan": "SEQ SCAN — full table read, no index", - "explain.legend.sort": "SORT — in-memory sort", - "explain.legend.hash": "HASH — hash join", - "explain.escClose": "Esc to close", - "flowVersion.title": "Flow Version History", - "flowVersion.subtitle": "Create checkpoints, compare versions and restore", - "flowVersion.watermark": "Checkpoint label (optional)…", - "flowVersion.saveCheckpoint": "Save Checkpoint", - "flowVersion.compareMode": "Compare Mode", - "flowVersion.selectBase": "Select BASE version (from):", - "flowVersion.clickAny": "Then click any version in the list below to compare.", - "flowVersion.noCheckpoints": "No checkpoints yet", - "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", - "flowVersion.restore": "Restore", - "flowVersion.diffResults": "Diff Results", - "benchmark.title": "Query Benchmark", - "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", - "benchmark.sql": "SQL being benchmarked", - "benchmark.runLabel": "Run label", - "benchmark.runLabelWatermark": "Run 1", - "benchmark.iterations": "Iterations (1–100)", - "benchmark.warmup": "Warm-up passes (0–10)", - "benchmark.interval": "Interval between runs (ms)", - "benchmark.run": "Run Benchmark", - "benchmark.cancel": "Cancel", - "benchmark.clearHistory": "Clear History", - "benchmark.latest": "LATEST RESULT", - "benchmark.avg": "AVG", - "benchmark.median": "MEDIAN", - "benchmark.min": "MIN", - "benchmark.max": "MAX", - "benchmark.iterationsAt": " iterations · run at ", - "benchmark.history": "HISTORY", - "benchmark.header.label": "ラベル", - "benchmark.header.avg": "平均", - "benchmark.header.median": "中央値", - "benchmark.header.min": "最小", - "benchmark.header.max": "最大", - "benchmark.itersSuffix": " iters", - "diagnostics.title": "App Diagnostics", - "diagnostics.run": "Run", - "diagnostics.running": "Running checks…", - "diagnostics.ok": "OK", - "diagnostics.warning": "Warning", - "diagnostics.error": "Error", - "diagnostics.tooltip.rerun": "Re-run all checks", - "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", - "diagnostics.tooltip.close": "Close (Esc)", - "autoJoin.title": "Auto-Join Suggestions", - "autoJoin.titleForTable": "Auto-Join suggestions for {0}", - "autoJoin.acceptAll": "Accept All", - "autoJoin.accept": "Accept", - "autoJoin.skip": "Skip", - "autoJoin.allHandled": "All suggestions handled", - "autoJoin.joinKeyword": "JOIN", - "autoJoin.confidence.fkConstraint": "FK Constraint", - "autoJoin.confidence.fkReverse": "FK (Reverse)", - "autoJoin.confidence.namingMatch": "Naming Match", - "autoJoin.confidence.weakMatch": "Weak Match", - "autoJoin.runSelected": "Auto-Join Selected", - "autoJoin.noSimilarityTitle": "No automatic join found", - "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", - "autoJoin.appliedTitle": "Auto-join applied", - "autoJoin.manual.title": "Create Manual Join", - "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", - "autoJoin.manual.leftColumn": "Left column", - "autoJoin.manual.rightColumn": "Right column", - "autoJoin.manual.joinType": "Join type", - "autoJoin.manual.operator": "Operator", - "autoJoin.manual.confirm": "Create join", - "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", - "autoJoin.manualJoinCreatedTitle": "Manual join created", - "autoJoin.manualJoinFailedTitle": "Manual join could not be created", - "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", - "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", - "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", - "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", - "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", - "property.outputAlias": "OUTPUT ALIAS", - "property.sourceAlias": "SOURCE ALIAS", - "property.aliasWatermark": "e.g. MyColumn (optional)", - "property.parameters": "PARAMETERS", - "property.enabled": "Enabled", - "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", - "property.dateWatermark": "YYYY-MM-DD or leave empty", - "property.apply": "Apply", - "property.inputPins": "INPUT PINS", - "property.outputPins": "OUTPUT PINS", - "property.sqlTrace": "SQL TRACE", - "property.live": "live", - "node.numericValue": "Numeric Value", - "node.stringValue": "String Value", - "node.enterText": "Enter text", - "node.datetimeValue": "DateTime Value", - "node.valueLabel": "Value:", - "node.noInputs": "No inputs", - "node.loadingSample": "Loading sample…", - "node.previewFailed": "⚠ Preview failed", - "node.sampleRowsHint": "5 sample rows · demo data", - "sidebar.tab.nodes": "ノード", - "sidebar.tab.connection": "接続", - "sidebar.tab.schema": "スキーマ", - "sidebar.tab.diagnostics": "診断", - "sidebar.addNode": "+ Add Node (⇧A)", - "sidebar.previewF3": "Preview (F3)", - "nodesList.search": "Search nodes...", - "search.watermark": "Search nodes… (Esc to close)", - "search.snippets": "★ SNIPPETS", - "commandPalette.watermark": "コマンドを実行…(Esc で閉じる)", - "tooltip.newCanvas": "New canvas (Ctrl+N)", - "tooltip.openCanvas": "Open canvas (Ctrl+O)", - "tooltip.saveCanvas": "Save canvas (Ctrl+S)", - "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", - "tooltip.zoomOut": "Zoom out (Ctrl+-)", - "tooltip.zoomIn": "Zoom in (Ctrl++)", - "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", - "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", - "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", - "tooltip.dataPreview": "Data preview (F3)", - "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", - "tooltip.appDiagnostics": "App Diagnostics (self-check)", - "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", - "tooltip.cancelRunningQuery": "Cancel the running query", - "tooltip.closeEsc": "Close (Esc)", - "tooltip.recheckConnectionHealth": "Re-check connection health", - "tooltip.deleteConnection": "Delete connection", - "tooltip.testConnection": "Test connection", - "tooltip.saveConnection": "Save connection", - "tooltip.activateConnection": "Activate this connection", - "tooltip.toggleDataSamplePreview": "Toggle data sample preview", - "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", - "tooltip.copySql": "Copy SQL to clipboard", - "tooltip.formatSql": "Format SQL", - "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", - "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", - "tooltip.switchToQueryMode": "Switch to Query canvas", - "tooltip.switchToDdlMode": "Switch to DDL canvas", - "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", - "tooltip.pins.inputs": "Inputs", - "tooltip.pins.outputs": "Outputs", - "tooltip.pins.none": "None", - "tooltip.tableColumns": "Columns", - "tooltip.tableColumns.none": "No detailed columns", - "window.minimize": "Minimize window", - "window.maximizeRestore": "Maximize/restore window", - "window.close": "Close window", - "menu.newDiagram": "New diagram", - "menu.openFile": "Open file", - "menu.save": "Save", - "menu.fileHistory": "File history", - "menu.shortcuts": "Keyboard shortcuts", - "menu.settings": "Settings", - "menu.importDdlSchema": "Import DDL schema", - "menu.viewDdlSql": "View DDL SQL", - "menu.executeDdl": "Execute DDL", - "menu.backToStart": "Back to start", - "toast.ddlExecuteFailed": "Failed to execute DDL.", - "toast.ddlOpenFailed": "Failed to open DDL SQL.", - "toast.ddlImportFailed": "Failed to import schema into DDL.", - "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", - "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", - "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", - "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", - "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", - "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", - "toast.ddlTableImported": "Table imported into the DDL canvas.", - "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", - "toast.ddlNoActiveConnection": "No active connection to execute DDL.", - "toast.ddlExecutedSuccess": "DDL executed successfully.", - "toast.ddlExecutedWithIssues": "DDL executed with issues.", - "toast.switchToDdl": "Switch to DDL mode to generate SQL.", - "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", - "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", - "toast.previewOpenFailed": "Failed to open preview.", - "tab.switchFailed": "Failed to switch tab: {0}", - "settings.status.darkApplied": "Dark theme applied.", - "settings.status.lightApplied": "Light theme applied.", - "settings.status.snapUpdated": "Snap updated: {0}.", - "settings.status.languageToggled": "Language toggled.", - "settings.status.languageSelected": "Language selected: {0}.", - "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", - "settings.section.appearance.title": "Themes", - "settings.section.languageRegion.title": "言語と地域", - "settings.section.dateTime.title": "日付と時刻", - "settings.section.keyboard.title": "キーボードショートカット", - "settings.section.privacy.title": "プライバシー", - "settings.section.notification.title": "通知", - "settings.section.accessibility.title": "アクセシビリティ", - "settings.section.default.title": "設定", - "settings.section.appearance.subtitle": "スタイルを選択するか、テーマをカスタマイズします", - "settings.section.languageRegion.subtitle": "言語と地域フォーマットを管理します", - "settings.section.keyboard.subtitle": "コマンドパレットとキャンバス実行で使うキーボードショートカットをカスタマイズします。", - "settings.section.wip.subtitle": "作業中です。", - "settings.section.default.subtitle": "アプリケーション設定", - "settings.general": "General", - "settings.nav.appearance": "Appearance", - "settings.theme.light": "ライトモード", - "settings.theme.dark": "ダークモード", - "settings.theme.system": "システム設定", - "settings.gridSnap.title": "Grid Snap", - "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", - "settings.language.subtitle": "アプリの言語を選択してください。", - "settings.language.toggle": "言語を切り替える", - "settings.language.option.ptBR": "ポルトガル語(ブラジル)", - "settings.language.option.enUS": "英語(米国)", - "settings.language.option.esES": "スペイン語(スペイン)", - "settings.language.option.ruRU": "ロシア語", - "settings.language.option.jaJP": "日本語", - "settings.language.option.zhTW": "繁体字中国語", - "settings.themeJson.title": "テーマ JSON", - "settings.themeJson.subtitle": "テーマ JSON を貼り付けて、即時に適用・保存します。", - "settings.themeJson.apply": "JSON を適用", - "settings.themeJson.restoreDefault": "既定テーマに戻す", - "mode.query": "Query", - "mode.ddl": "DDL", - "sidebar.left.close": "Close left sidebar", - "sidebar.left.open": "Reopen left sidebar", - "sidebar.right.close": "Close right sidebar", - "sidebar.right.open": "Reopen right sidebar", - "connection.completedTitle": "Connection completed", - "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", - "connection.close": "Close connection manager", - "connection.refreshHealth": "Refresh connection health", - "common.details": "Details", - "common.cancel": "Cancel", - "common.keep": "Keep", - "common.clear": "Clear", - "zoom.out": "Zoom out", - "zoom.in": "Zoom in", - "zoom.fit": "Fit zoom to screen", - "zoom.level": "Zoom level", - "settings.theme.mode": "テーマモード", - "diagnostics.category.canvas": "Canvas Integrity", - "diagnostics.category.output": "Output & Execution", - "diagnostics.category.session": "Session & Safety", - "diagnostics.category.notice": "Runtime Notices", - "diagnostics.summary.ok": "All systems OK", - "diagnostics.summary.warningCount": "{0} warning(s) detected", - "diagnostics.summary.errorCount": "{0} error(s) detected", - "diagnostics.canvasMigration": "Canvas Migration", - "diagnostics.recommendation.resaveFile": "Re-save the file to update it to the latest schema version.", - "diagnostics.canvasState.name": "Canvas State", - "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", - "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", - "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", - "diagnostics.validation.name": "Validation Errors", - "diagnostics.validation.recommendation": "Fix highlighted nodes before running output preview", - "diagnostics.validation.errorWithWarnings": "{0} error(s) and {1} warning(s) in the graph", - "diagnostics.validation.warningOnly": "{0} warning(s) in the graph", - "diagnostics.validation.none": "No validation issues", - "diagnostics.orphan.name": "Orphan Nodes", - "diagnostics.orphan.recommendation": "Use the orphan cleanup action to remove unused nodes", - "diagnostics.orphan.count": "{0} node(s) not connected to any output", - "diagnostics.orphan.none": "No orphan nodes detected", - "diagnostics.naming.name": "Naming Conventions", - "diagnostics.naming.recommendation": "Use auto-fix alias naming when conformance is below 100%", - "diagnostics.naming.conformance": "Naming conformance: {0}%", - "diagnostics.naming.ok": "All aliases follow naming conventions (100%)", - "diagnostics.queryCompilation.name": "Live SQL Compilation", - "diagnostics.queryCompilation.recommendation": "Review SQL diagnostics in output when errors/warnings are reported.", - "diagnostics.queryCompilation.errorFallback": "Live SQL compilation reported errors.", - "diagnostics.queryCompilation.warningCounts": "{0} diagnostic item(s), {1} guardrail warning(s).", - "diagnostics.queryCompilation.ok": "Live SQL compiled without diagnostics.", - "diagnostics.previewSafety.name": "Preview Safety", - "diagnostics.previewSafety.recommendation": "Preview executes read-only statements only.", - "diagnostics.previewSafety.blocked": "Current SQL is mutating and blocked by Safe Preview mode.", - "diagnostics.previewSafety.ok": "Preview safety checks passed.", - "diagnostics.previewExecution.name": "Preview Execution", - "diagnostics.previewExecution.recommendation": "Run preview and inspect diagnostics for execution/runtime errors.", - "diagnostics.previewExecution.failed": "Preview execution failed.", - "diagnostics.previewExecution.cancelled": "Preview execution was cancelled.", - "diagnostics.previewExecution.done": "{0} row(s) in {1}ms.", - "diagnostics.previewExecution.none": "No preview execution issues detected.", - "diagnostics.ddlCompilation.name": "DDL Compilation", - "diagnostics.ddlCompilation.recommendation": "Fix DDL compile diagnostics before execution.", - "diagnostics.ddlCompilation.failed": "DDL compilation failed.", - "diagnostics.ddlCompilation.warningCount": "{0} warning(s) reported by DDL compiler.", - "diagnostics.ddlCompilation.ok": "DDL compilation succeeded.", - "diagnostics.ddlOutput.name": "DDL Output", - "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", - "diagnostics.ddlOutput.none": "No DDL statements generated yet.", - "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", - "diagnostics.undo.name": "Undo History", - "diagnostics.undo.recommendation": "History is in-memory only; save your canvas regularly.", - "diagnostics.undo.saved": "Canvas is saved (no unsaved changes).", - "diagnostics.undo.unsavedDeep": "Unsaved changes with {0} undo steps.", - "diagnostics.undo.unsaved": "Unsaved changes - {0} undo step(s) available.", - "diagnostics.report.title": "DBWeaver - Diagnostic Report", - "diagnostics.report.generated": "Generated", - "diagnostics.report.overall": "Overall", - "diagnostics.report.details": "Details", - "diagnostics.report.recommendation": "Recommendation", - "diagnostics.report.lastCheck": "Last Check", - "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", - "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", - "preview.providerLabel": "Provider", - "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", - "preview.schemaAnalysis.run": "Run Analysis", - "preview.schemaAnalysis.cancel": "Cancel", - "preview.schemaAnalysis.issues": "Issues", - "preview.schemaAnalysis.clearFilters": "Clear Filters", - "preview.schemaAnalysis.clearBlacklist": "Clear Blacklist", - "preview.schemaAnalysis.severity": "Severity", - "preview.schemaAnalysis.severity.info": "Info", - "preview.schemaAnalysis.severity.warning": "Warning", - "preview.schemaAnalysis.severity.critical": "Critical", - "preview.schemaAnalysis.rule": "Rule", - "preview.schemaAnalysis.rule.fkCatalogInconsistent": "FK catalog inconsistent", - "preview.schemaAnalysis.rule.missingFk": "Missing FK", - "preview.schemaAnalysis.rule.namingConventionViolation": "Naming convention violation", - "preview.schemaAnalysis.rule.lowSemanticName": "Low semantic name", - "preview.schemaAnalysis.rule.missingRequiredComment": "Missing required comment", - "preview.schemaAnalysis.rule.nf1HintMultiValued": "1NF hint: multi-valued", - "preview.schemaAnalysis.rule.nf2HintPartialDependency": "2NF hint: partial dependency", - "preview.schemaAnalysis.rule.nf3HintTransitiveDependency": "3NF hint: transitive dependency", - "preview.schemaAnalysis.minConfidence": "Min Confidence", - "preview.schemaAnalysis.tableFilter": "Table Filter", - "preview.schemaAnalysis.tableFilterWatermark": "schema.table", - "preview.schemaAnalysis.ignore": "Execution Filters", - "preview.schemaAnalysis.ignoreViews": "Ignore views and materialized views", - "preview.schemaAnalysis.blacklist": "Table blacklist", - "preview.schemaAnalysis.blacklistAdd": "Add", - "preview.schemaAnalysis.blacklistRemove": "Remove selected", - "preview.schemaAnalysis.ignoreTable.placeholder": "schema.table", - "preview.schemaAnalysis.details": "Details", - "preview.schemaAnalysis.evidence": "Evidence", - "preview.schemaAnalysis.suggestions": "Suggestions", - "preview.schemaAnalysis.ruleDiagnostics": "Rule Diagnostics", - "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", - "preview.schemaAnalysis.copySql": "Copy SQL", - "preview.schemaAnalysis.applyToCanvas": "Apply to Canvas", - "preview.schemaAnalysis.summary.issues": "Issues:", - "preview.schemaAnalysis.summary.rawPrefix": "(raw:", - "preview.schemaAnalysis.summary.critical": "| Critical:", - "preview.schemaAnalysis.summary.warning": "| Warning:", - "preview.schemaAnalysis.summary.info": "| Info:", - "preview.schemaAnalysis.state.metadataUnavailable": "Metadata unavailable for structural analysis.", - "preview.schemaAnalysis.state.cancelled": "Analysis cancelled by the user.", - "preview.schemaAnalysis.state.partialTimeout": "Analysis finished partially due to timeout.", - "preview.schemaAnalysis.state.failed": "Structural analysis failed.", - "preview.schemaAnalysis.state.empty": "No inferable structural issue was detected.", - "preview.schemaAnalysis.state.noFilterMatch": "No issue matches the selected filters.", - "preview.schemaAnalysis.state.noIssueSelected": "No issue selected.", - "preview.schemaAnalysis.state.noSqlCandidate": "No SQL candidate available.", - "preview.schemaAnalysis.actionBlockedTooltip": "Action unavailable for the current risk level or capability.", - "common.navigate": "Navigate", - "common.close": "Close", - "common.esc": "Esc", - "common.ms": "ms", - "common.zero": "0", - "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", - "nodesList.empty": "No nodes found", - "nodesList.emptyHint": "Adjust the search term to explore available types", - "schema.emptyFiltered": "No objects found for the current filter", - "start.lastSnapshot": "Last snapshot", - "app.brandBadge": "VS", - "property.tab.properties": "Properties", - "property.tab.projectSettings": "Project Settings", - "property.nodeType": "NODE TYPE", - "property.selectNodeHint": "Select a node to edit its properties.", - "property.namingConventions": "Naming Conventions", - "property.aliasConvention": "Alias convention", - "property.enforceAliasNaming": "Enforce alias naming", - "property.warnReservedSql": "Warn on reserved SQL keywords", - "property.maxAliasLength": "Max alias length", - "property.maxAliasLengthDefault": "64", - "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", - "start.tips": "Tips", - "start.tips.quick": "Quick tips", - "start.tips.item1": "1. Click New Diagram to start from scratch.", - "start.tips.item2": "2. Use templates to speed up prototyping.", - "start.tips.item3": "3. Open saved connections to load real tables.", - "start.tips.shortcut": "Shortcut: CTRL+SHIFT+P opens the command palette.", - "start.workspace": "WORKSPACE", - "start.resumeTitle": "Continue where you left off", - "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", - "start.chip.quickFlow": "Quick flow", - "start.chip.templates": "Templates", - "start.chip.connections": "Connections", - "start.savedConnectionsTitle": "Saved Connections", - "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", - "start.noConnectionsTitle": "No connections configured yet", - "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", - "start.newConnection": "+ New Connection", - "start.recentProjectsTitle": "Recent Projects", - "start.searchRecent": "Search recent project...", - "start.quickActions": "Quick actions", - "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", - "start.noRecentTitle": "No recent projects yet", - "start.noRecentSubtitle": "Use the quick actions card above to get started.", - "start.exploreTemplates": "Explore templates", - "start.templatesFavoritesHint": "Favorites on top", - "start.favoriteTemplate": "Favorite template", - "node.columnSetPreview": "ColumnSet preview", - "node.view": "VIEW", - "node.tableDefinition": "Table Definition", - "node.join": "JOIN", - "node.window.addPartition": "Add PARTITION BY slot", - "node.window.removePartition": "Remove PARTITION BY slot", - "node.window.addOrder": "Add ORDER BY slot", - "node.window.removeOrder": "Remove ORDER BY slot", - "sql.keyword.select": "SELECT", - "sql.keyword.from": "FROM", - "sql.keyword.join": "JOIN", - "sql.keyword.where": "WHERE", - "sql.keyword.limit": "LIMIT", - "sqlImporter.close": "Close SQL importer", - "sqlImporter.report.imported": "Imported", - "sqlImporter.report.partial": "Partial", - "sqlImporter.report.skipped": "Skipped", - "benchmark.close": "Close benchmark", - "benchmark.p95": "P95", - "benchmark.n": "N", - "liveSql.safePreview": "SAFE PREVIEW MODE", - "liveSql.title": "LIVE SQL", - "liveSql.blocked": "BLOCKED", - "liveSql.copy": "Copy", - "liveSql.format": "Format", - "liveSql.benchmark": "Benchmark", - "liveSql.explain": "Explain", - "liveSql.actionsHint": "Performance tools", - "ddl.dialog.title": "Execute DDL", - "ddl.dialog.execute": "Execute", - "ddl.dialog.cancel": "Cancel", - "ddl.dialog.close": "Close", - "ddl.dialog.stopOnError": "Stop on first failure", - "ddl.dialog.confirmDestructive": "I confirm execution of destructive statements (DROP TABLE)", - "ddl.dialog.reviewBeforeRun": "Review the DDL script before confirming.", - "ddl.dialog.confirmQuestion": "Confirm DDL execution on the connected database?", - "ddl.dialog.irreversibleWarning": "This action can change the schema irreversibly.", - "ddl.dialog.mustConfirmDestructive": "Confirm destructive execution to continue.", - "ddl.dialog.executing": "Executing...", - "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", - "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", - "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", - "ddl.execute.result.failed": "Failed to execute DDL.", - "ddl.execute.result.cancelled": "Execution cancelled by the user.", - "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", - "context.deleteSingle": "Delete {0}", - "context.deleteMultiple": "Delete {0} nodes", - "context.bringForward": "Bring Forward (Ctrl+PgUp)", - "context.sendBackward": "Send Backward (Ctrl+PgDown)", - "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", - "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", - "context.normalizeLayers": "Normalize Layers", - "context.deleteWire": "Delete wire", - "context.addNode": "Add Node (Shift+A)", - "context.undoWithDescription": "Undo {0}", - "context.redo": "Redo", - "shortcuts.windowTitle": "Keyboard Shortcuts", - "shortcuts.headerTitle": "DBWeaver - Shortcuts", - "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", - "shortcuts.filterWatermark": "Filter shortcuts by key or action...", - "shortcuts.resultCount": "{0} shortcuts", - "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", - "shortcuts.noneFound": "No shortcuts found.", - "shortcuts.section.fileGeneral": "ファイルと一般", - "shortcuts.section.editing": "編集", - "shortcuts.section.canvasNavigation": "キャンバスとナビゲーション", - "shortcuts.section.zoomPanPrecision": "ズーム、パン、精密操作", - "shortcuts.section.previewInspection": "プレビューと検査", - "shortcuts.key.deleteOrBackspace": "Del または Backspace", - "shortcuts.key.middleDrag": "中ボタン + ドラッグ", - "shortcuts.key.rightDrag": "右ボタン + ドラッグ", - "shortcuts.key.spaceDrag": "Space + ドラッグ", - "shortcuts.key.altLeftDrag": "Alt + 左ドラッグ", - "shortcuts.key.arrows": "矢印キー", - "shortcuts.key.shiftArrows": "Shift + 矢印キー", - "shortcuts.action.openShortcutScreen": "このショートカット画面を開く", - "shortcuts.action.newCanvas": "新しいキャンバス", - "shortcuts.action.openFile": "ファイルを開く", - "shortcuts.action.save": "保存", - "shortcuts.action.saveAs": "名前を付けて保存", - "shortcuts.action.commandPalette": "コマンドパレット", - "shortcuts.action.undo": "元に戻す", - "shortcuts.action.redo": "やり直し", - "shortcuts.action.selectAll": "すべて選択", - "shortcuts.action.deleteSelection": "選択を削除", - "shortcuts.action.closeOverlayCancel": "オーバーレイを閉じる / 操作をキャンセル", - "shortcuts.action.openNodeSearch": "ノード検索を開く", - "shortcuts.action.resetViewport": "ビューポートをリセット", - "shortcuts.action.centerSelection": "選択を中央に配置", - "shortcuts.action.fitSelection": "選択にフィット", - "shortcuts.action.autoLayout": "自動レイアウト", - "shortcuts.action.toggleSnapToGrid": "グリッドスナップ切替", - "shortcuts.action.bringForward": "前面へ", - "shortcuts.action.sendBackward": "背面へ", - "shortcuts.action.bringToFront": "最前面へ", - "shortcuts.action.sendToBack": "最背面へ", - "shortcuts.action.zoomInOut": "ズームイン / アウト", - "shortcuts.action.pan": "パン", - "shortcuts.action.temporaryPan": "一時パン", - "shortcuts.action.alternatePan": "代替パン", - "shortcuts.action.fineNudge": "選択を微調整移動", - "shortcuts.action.fastNudge": "高速移動", - "shortcuts.action.togglePreview": "データプレビュー切替", - "shortcuts.action.explainPlan": "実行計画", - "shortcuts.action.runPreview": "プレビュー実行", - "shortcuts.action.connectionManager": "接続マネージャー", - "shortcuts.action.flowVersionHistory": "フローバージョン履歴", - "shortcuts.resetAll": "すべてリセット", - "shortcuts.customized": "カスタム", - "shortcuts.default": "デフォルト", - "shortcuts.apply": "適用", - "shortcuts.reset": "リセット", - "shortcuts.status.resetAllSuccess": "すべてのショートカットをデフォルトに戻しました。", - "shortcuts.status.updated": "ショートカットを更新しました。", - "shortcuts.status.reset": "ショートカットをデフォルトに戻しました。", - "shortcuts.status.updateFailed": "ショートカットを更新できませんでした。", - "toast.severity.success": "Success", - "toast.severity.warning": "Warning", - "toast.severity.error": "Error", - "toast.details.success": "Success Details", - "toast.details.warning": "Warning Details", - "toast.details.error": "Error Details", - "diagnostics.area.cteEditor": "CTE Editor", - "diagnostics.area.viewEditor": "View Editor", - "diagnostics.area.subEditor": "Sub-editor", - "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", - "diagnostics.recommendation.reloadFileIfNeeded": "Reload the file if needed.", - "diagnostics.viewEditor.exitFailed": "Could not exit: {0}", - "diagnostics.viewEditor.canvasIncomplete": "the canvas is incomplete.", - "diagnostics.viewEditor.exitRecommendation": "Connect a valid ResultOutput or use the discard command.", - "diagnostics.viewEditor.restoreParentFailed": "Failed to restore the parent canvas. The subgraph was discarded.", - "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", - "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", - "diagnostics.canvasMigration.openWarning": "Open: {0}", - "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", - "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", - "diagnostics.recommendation.resaveLatestSchema": "Review diagnostics and re-save the canvas to persist the latest schema.", - "diagnostics.recommendation.saveMigratedSchema": "Review diagnostics and save the canvas to persist the migrated schema.", - "file.saveDialog.title": "Save Canvas", - "file.saveDialog.suggestedName": "Query1", - "file.save.success": "Canvas saved successfully.", - "file.save.failedWithReason": "Save failed: {0}", - "file.openDialog.title": "Open Canvas", - "file.open.failedWithReason": "Open failed: {0}", - "file.open.success": "Canvas opened successfully.", - "file.open.successWithWarnings": "Canvas opened with warnings.", - "session.restore.failedWithReason": "Restore failed: {0}", - "session.restore.successWithWarnings": "Session restored with warnings.", - "session.restore.success": "Session restored successfully.", - "export.documentation.dialogTitle": "Export Flow Documentation", - "export.documentation.success": "Documentation exported successfully.", - "export.documentation.failed": "Documentation export failed.", - "export.failed.pathPermissionsHint": "Check file path and permissions.", - "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", - "export.dialogTitleByExtension": "Export as {0}", - "export.success": "Export completed successfully.", - "export.failed": "Export failed.", - "fileHistory.currentFile.none": "No file selected", - "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", - "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", - "fileHistory.status.countAvailable": "{0} local version(s) available.", - "fileHistory.restore.failedWithReason": "Restore failed: {0}", - "fileHistory.restore.successFrom": "Restored version from {0}.", - "preview.status.cancelled": "Cancelled", - "preview.status.error": "Error", - "preview.status.ready": "Ready", - "preview.runningWithMs": "Running... {0}ms", - "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", - "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", - "explain.errorWithReason": "Explain plan error: {0}", - "explain.noSql": "No SQL to explain. Build a query on the canvas first.", - "ddl.compilationFailed": "Compilation failed", - "ddl.compileErrorWithReason": "DDL compile error: {0}", - "command.undo.name": "Undo", - "command.undo.description": "Undo last action", - "command.redo.name": "Redo", - "command.redo.description": "Redo last undone action", - "command.addNode.name": "Add Node", - "command.addNode.description": "Open node search menu to add a node", - "command.bringForward.name": "Bring Forward", - "command.bringForward.description": "Move selected nodes one layer forward", - "command.sendBackward.name": "Send Backward", - "command.sendBackward.description": "Move selected nodes one layer backward", - "command.bringToFront.name": "Bring to Front", - "command.bringToFront.description": "Move selected nodes to top layer", - "command.sendToBack.name": "Send to Back", - "command.sendToBack.description": "Move selected nodes to bottom layer", - "command.normalizeLayers.name": "Normalize Layers", - "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", - "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", - "main.orphanSuffix": "Orphan(s)", - "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", - "main.namingPrefix": "Naming", - "fileHistory.compressedLabel": "Compressed:", - "schema.itemsSuffix": "item(s)", - "property.panel.title": "Properties", - "property.panel.multiSelected": "{0} nodes selected", - "sqlImporter.status.pasteSelect": "Paste a SELECT statement above, then click Import.", - "sqlImporter.status.inputTooLarge": "SQL input is too large ({0:N0} chars). Limit is {1:N0}. Split the query or increase the import limit.", - "sqlImporter.status.parsing": "Parsing SQL...", - "sqlImporter.status.done": "Done - {0} imported, {1} partial, {2} skipped.", - "sqlImporter.status.cancelledByUser": "Import cancelled by user.", - "sqlImporter.status.timeout": "Import timed out after {0:0.#}s. Try a smaller query or increase timeout.", - "sqlImporter.status.parseError": "Parse error: {0}", - "diagnostics.area.undoRedoTransaction": "Undo/Redo Transaction", - "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", - "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", - "node.preview.noCatalog": "No catalog available", - "connection.error.searchMenuNotInitialized": "search menu not initialized", - "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", - "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", - "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", - "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", - "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", - "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", - "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", - "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", - "diagnostics.area.connection": "Connection", - "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", - "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", - "undoRedo.transaction.unnamed": "unnamed transaction", - "benchmark.runLabelDefault": "Run 1", - "benchmark.runLabelPattern": "Run {0}", - "benchmark.status.failedWithReason": "Benchmark failed: {0}", - "benchmark.status.noSql": "No SQL to benchmark - build a query first.", - "benchmark.status.warmupProgress": "Warm-up {0}/{1}...", - "benchmark.status.iterationProgress": "Iteration {0}/{1}...", - "benchmark.status.done": "Done - {0}", - "benchmark.status.cancelled": "Benchmark cancelled.", - "app.windowTitle": "DBWeaver", - "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", - "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", - "sqlImporter.error.selectFromNotFound": "Could not find SELECT ... FROM in the query.", - "sqlImporter.error.fromClauseParseFailed": "Could not parse FROM clause.", - "sqlImporter.error.syntaxUnterminatedString": "Syntax error at line {0}, column {1}: unterminated string literal.", - "sqlImporter.error.missingClosingParenthesis": "missing closing ')'", - "sqlImporter.error.unexpectedClosingParenthesis": "unexpected ')'", - "sqlImporter.error.syntaxAtLineColumn": "Syntax error at line {0}, column {1}: {2}.", - "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", - "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", - "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", - "errorDiagnostics.connection.label": "Connection failed", - "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", - "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", - "errorDiagnostics.authorization.label": "Authorization error", - "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", - "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", - "errorDiagnostics.timeout.label": "Query timeout", - "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", - "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", - "errorDiagnostics.schema.label": "Schema error", - "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", - "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", - "errorDiagnostics.syntax.label": "SQL syntax error", - "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", - "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", - "errorDiagnostics.compatibility.label": "Compatibility error", - "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", - "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", - "errorDiagnostics.unknown.label": "Unexpected error", - "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", - "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", - "error.mainWindow.invalidDataContext": "MainWindow の DataContext は ShellViewModel である必要があります。", - "error.mainWindow.canvasNotInitialized": "CanvasViewModel が初期化されていません。", - "error.mainWindow.ddlPreviewUnavailable": "現在のキャンバスではDDLプレビューを利用できません。", - "themeJson.editor.template": "{\n \"meta\": { \"name\": \"カスタムテーマ\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", - "themeJson.error.pasteBeforeApply": "適用する前にテーマJSONを貼り付けてください。", - "themeJson.error.invalidJson": "無効なJSON: {0}", - "themeJson.error.emptyPayload": "無効なJSON: payloadが空です。", - "themeJson.error.invalidTheme": "無効なテーマ: {0}", - "themeJson.error.appliedButSaveFailed": "テーマは適用されましたが保存に失敗しました: {0}", - "themeJson.success.appliedAndSaved": "JSONテーマを適用して保存しました。", - "themeJson.success.customRemoved": "カスタムテーマを削除しました。既定テーマへ完全に戻すにはアプリを再起動してください。", - "themeJson.error.restoreDefaultFailed": "既定テーマの復元に失敗しました: {0}", - "themeValidator.error.configNull": "テーマ設定がnullです。", - "themeValidator.warning.noSections": "テーマに色またはタイポグラフィのセクションがありません。適用する内容がありません。", - "themeValidator.warning.invalidColor": "{0} の色 '{1}' は無効です。このキーは無視されます。", - "themeValidator.warning.sizeOutOfRange": "{0}={1} は範囲外です (8..48)。このキーは無視されます。", - "queryExecutor.error.openConnectionMethodNotFound": "orchestratorにOpenConnectionAsyncメソッドが見つかりません", - "queryExecutor.error.openConnectionInvokeFailed": "OpenConnectionAsyncの呼び出しに失敗しました", - "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}': ビューのSELECTは視覚的に再構築できません。subcanvasで手動編集してください。", - "ddlImporter.error.tableNotFoundInMetadata": "テーブル '{0}' は現在のメタデータに見つかりませんでした。", - "main.window.untitled": "無題", - "main.subEditor.noSeedProvided": "{0} のサブエディターseedが提供されませんでした。", - "main.layerOrder.bringToFront": "最前面へ", - "main.layerOrder.sendToBack": "最背面へ", - "main.layerOrder.bringForward": "前面へ移動", - "main.layerOrder.sendBackward": "背面へ移動", - "main.layerOrder.normalizeLayers": "レイヤーを正規化", - "export.fileType.html": "HTMLファイル", - "export.fileType.json": "JSONファイル", - "export.fileType.csv": "CSVファイル", - "export.fileType.excel": "Excelファイル", - "commandPalette.templatePrefix": "テンプレート: {0}", - "themeLoader.status.notFoundWithPath": "テーマファイルが見つかりません: {0}", - "themeLoader.status.deserializedNull": "テーマJSONのデシリアライズ結果が null でした。", - "themeLoader.status.loaded": "テーマJSONを正常に読み込みました。", - "credential.error.ciphertextTooShort": "暗号文ブロブが短すぎます。", - "credential.error.dpapiWindowsOnly": "DPAPI は Windows でのみ利用できます。", - "credential.warning.loadVaultFailed": "資格情報ボルト {0} の読み込みに失敗しました: {1}", - "credential.warning.persistVaultFailed": "資格情報ボルト {0} の保存に失敗しました: {1}", - "snippetStore.warning.loadFailed": "{0} からスニペットの読み込みに失敗しました: {1}", - "snippetStore.warning.saveFailed": "スニペットの保存に失敗しました: {0}", - "flowVersionStore.warning.loadFailed": "{0} からフローバージョンの読み込みに失敗しました: {1}", - "flowVersionStore.warning.saveFailed": "フローバージョンの保存に失敗しました: {0}", - "queryExecutor.error.queryEmpty": "クエリは空にできません", - "queryExecutor.error.providerNotSupported": "プロバイダー {0} はサポートされていません", - "queryExecutor.error.singleStatementOnly": "プレビューは単一のSQL文のみ受け付けます。", - "queryExecutor.error.queryEmptyWithPeriod": "クエリは空にできません。", - "queryExecutor.error.readOnlyOnly": "プレビューモードは読み取り専用SQLのみサポートします。", - "queryExecutor.error.namedParametersNotSupported": "プレビューモードは実行SQLのバインドパラメータをサポートしません。安全なリテラルをインラインで指定するか、プレビュー外で実行してください。", - "queryExecutor.error.positionalParametersNotSupported": "プレビューモードは位置パラメータプレースホルダー(? または )をサポートしません。", - "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "選択したノードを下端に揃えます", - "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "選択したノードを左端に揃えます", - "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "選択したノードを右端に揃えます", - "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "選択したノードを上端に揃えます", - "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "CTEサブキャンバスの編集を適用して親キャンバスに戻ります", - "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "ノードを論理的な列に自動配置します", - "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "選択したノードを水平軸で中央揃えします", - "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "選択したノードを垂直軸で中央揃えします", - "commandPalette.description.clear_canvas_and_start_fresh": "キャンバスをクリアして最初から開始します", - "commandPalette.description.clear_node_selection": "ノードの選択を解除します", - "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "エイリアスをプロジェクト設定の命名規則に変換します", - "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "チェックポイントを作成し、バージョン比較と以前の状態への復元を行います", - "commandPalette.description.delete_the_selected_nodes": "選択したノードを削除します", - "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "現在のサブエディタの編集を破棄して親キャンバスに戻ります", - "commandPalette.description.execute_the_current_query_in_preview": "プレビューで現在のクエリを実行します", - "commandPalette.description.fit_all_nodes_into_the_visible_area": "すべてのノードを表示領域に収めます", - "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "最初のCSVエクスポートノードからCSVファイルを生成します", - "commandPalette.description.generate_html_file_from_the_first_html_export_node": "最初のHTMLエクスポートノードからHTMLファイルを生成します", - "commandPalette.description.generate_json_file_from_the_first_json_export_node": "最初のJSONエクスポートノードからJSONファイルを生成します", - "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "最初のExcelエクスポートノードからXLSXブックを生成します", - "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "クエリ実行計画を確認し、スキャン種別・結合戦略・コスト見積りを確認します", - "commandPalette.description.load_a_vsaq_canvas_file": ".vsaq キャンバスファイルを読み込みます", - "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "現在のSQLをN回実行して平均/中央値/p95レイテンシを測定します", - "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "選択したCTE定義ノードの分離サブキャンバスエディタを開きます", - "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "保存ごとに作成されるローカル履歴を開き、過去スナップショットを復元します", - "commandPalette.description.open_output_preview_modal_for_the_active_mode": "アクティブモードの出力プレビューモーダルを開きます", - "commandPalette.description.open_shortcut_reference_screen": "ショートカット一覧画面を開きます", - "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "接続マネージャーを開いてDB接続の追加・編集・切替を行います", - "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "SELECT文を貼り付けてノードを自動生成します(FROM/JOIN/WHERE/LIMIT対応)", - "commandPalette.description.remove_all_nodes_not_connected_to_output": "出力に接続されていないノードをすべて削除します", - "commandPalette.description.reset_zoom_and_pan_to_default": "ズームとパンを初期値に戻します", - "commandPalette.description.save_canvas_to_a_new_file": "キャンバスを新しいファイルに保存します", - "commandPalette.description.save_current_canvas": "現在のキャンバスを保存します", - "commandPalette.description.save_markdown_documentation_of_the_current_flow": "現在のフローのMarkdownドキュメントを保存します", - "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "選択ノードを再利用可能なスニペットとして保存し、後でノード検索メニュー(⇧A)から挿入します", - "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "FK規約と命名パターンに基づいてテーブルソースノード間のJOIN候補を検出します", - "commandPalette.description.select_all_nodes_on_canvas": "キャンバス上のすべてのノードを選択します", - "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "ノード位置を16pxグリッドにスナップします(Ctrl+G)", - "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "選択ノードを水平方向に等間隔で配置します", - "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "選択ノードを垂直方向に等間隔で配置します", - "commandPalette.description.zoom_into_the_canvas": "キャンバスを拡大します", - "commandPalette.description.zoom_out_of_the_canvas": "キャンバスを縮小します", - "commandPalette.name.align_bottom": "下揃え", - "commandPalette.name.align_left": "左揃え", - "commandPalette.name.align_right": "右揃え", - "commandPalette.name.align_top": "上揃え", - "commandPalette.name.analyze_all_joins": "すべてのJOINを解析", - "commandPalette.name.auto_fix_naming": "命名を自動修正", - "commandPalette.name.auto_layout": "自動レイアウト", - "commandPalette.name.center_horizontally": "水平中央揃え", - "commandPalette.name.center_vertically": "垂直中央揃え", - "commandPalette.name.cleanup_orphans": "孤立ノードをクリーンアップ", - "commandPalette.name.delete_selected": "選択を削除", - "commandPalette.name.deselect_all": "すべて選択解除", - "commandPalette.name.discard_and_exit_editor": "破棄してエディタを終了", - "commandPalette.name.distribute_horizontally": "水平方向に均等配置", - "commandPalette.name.distribute_vertically": "垂直方向に均等配置", - "commandPalette.name.edit_selected_cte": "選択したCTEを編集", - "commandPalette.name.exit_cte_editor": "CTEエディタを終了", - "commandPalette.name.explain_plan": "実行計画", - "commandPalette.name.export_csv": "CSVをエクスポート", - "commandPalette.name.export_documentation": "ドキュメントをエクスポート", - "commandPalette.name.export_excel": "Excelをエクスポート", - "commandPalette.name.export_html": "HTMLをエクスポート", - "commandPalette.name.export_json": "JSONをエクスポート", - "commandPalette.name.file_save_load_history": "保存/読み込み履歴", - "commandPalette.name.fit_to_screen": "画面にフィット", - "commandPalette.name.flow_version_history": "フローバージョン履歴", - "commandPalette.name.import_sql_to_graph": "SQLをグラフにインポート", - "commandPalette.name.keyboard_shortcuts": "キーボードショートカット", - "commandPalette.name.manage_connections": "接続を管理", - "commandPalette.name.new_canvas": "新しいキャンバス", - "commandPalette.name.open_file": "ファイルを開く", - "commandPalette.name.reset_viewport": "ビューポートをリセット", - "commandPalette.name.run_preview": "プレビューを実行", - "commandPalette.name.run_query_benchmark": "クエリベンチマークを実行", - "commandPalette.name.save": "保存", - "commandPalette.name.save_as": "名前を付けて保存", - "commandPalette.name.save_selection_as_snippet": "選択をスニペットとして保存", - "commandPalette.name.select_all": "すべて選択", - "commandPalette.name.toggle_preview": "プレビューを切り替え", - "commandPalette.name.toggle_snap_to_grid": "グリッドスナップを切り替え", - "commandPalette.name.zoom_in": "ズームイン", - "commandPalette.name.zoom_out": "ズームアウト", - "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", - "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", - "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", - "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", - "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", - "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", - "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", - "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", - "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", - "commandPalette.tags.clear_selection": "clear selection", - "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", - "commandPalette.tags.create_insert_search_transform": "create insert search transform", - "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", - "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", - "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", - "commandPalette.tags.data_results_table_panel": "data results table panel", - "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", - "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", - "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", - "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", - "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", - "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", - "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", - "commandPalette.tags.export_json_file_output_save": "export json file output save", - "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", - "commandPalette.tags.export_persist_copy": "export persist copy", - "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", - "commandPalette.tags.forward_history": "forward history", - "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", - "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", - "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", - "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", - "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", - "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", - "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", - "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", - "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", - "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", - "commandPalette.tags.load_import_vsaq": "load import vsaq", - "commandPalette.tags.magnify_enlarge": "magnify enlarge", - "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", - "commandPalette.tags.persist_write_disk": "persist write disk", - "commandPalette.tags.remove_erase_nodes": "remove erase nodes", - "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", - "commandPalette.tags.reset_clear_blank": "reset clear blank", - "commandPalette.tags.revert_back_history": "revert back history", - "commandPalette.tags.shrink_reduce": "shrink reduce", - "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", - "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", - "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", - "sqlEditor.diffPreview.title": "Transactional Diff Preview", - "sqlEditor.mutation.confirmExecute": "Confirm Execute", - "sqlEditor.tab.closeAnyway": "Close Anyway", - "sqlEditor.tab.keepTab": "Keep Tab", - "sqlEditor.status.ready": "Ready.", - "sqlEditor.telemetry.none": "No execution telemetry yet.", - "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", - "sqlEditor.telemetry.errors.none": "No aggregated errors.", - "sqlEditor.diff.none": "No transactional diff preview available.", - "sqlEditor.mutation.estimate.none": "No mutation estimate available.", - "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", - "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", - "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", - "sqlEditor.tab.noPendingClose": "No tab close pending.", - "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", - "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", - "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", - "sqlEditor.message.empty": "Execute a statement to see messages.", - "sqlEditor.message.success": "Execution completed successfully.", - "sqlEditor.result.summary.empty": "Rows: - Time: -", - "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", - "sqlEditor.file.save.canceled": "Save canceled.", - "sqlEditor.file.save.noPath": "No target path selected.", - "sqlEditor.file.save.success": "SQL file saved.", - "sqlEditor.file.save.failed": "Save failed.", - "sqlEditor.file.open.failed": "Open failed.", - "sqlEditor.file.open.notFound": "Selected SQL file was not found.", - "sqlEditor.file.open.success": "SQL file opened.", - "sqlEditor.status.executing": "Executing SQL...", - "sqlEditor.status.executingScript": "Executing SQL script...", - "sqlEditor.status.executingStep": "Executing {0}/{1}...", - "sqlEditor.status.canceling": "Canceling execution...", - "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", - "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", - "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", - "sqlEditor.status.success": "Execution succeeded.", - "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", - "sqlEditor.status.canceled": "Execution canceled.", - "sqlEditor.status.failed": "Execution failed.", - "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", - "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", - "sqlEditor.result.tabTitle": "Result {0}", - "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", - "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", - "sqlEditor.tab.closed": "Tab closed.", - "sqlEditor.tab.closeCanceled": "Tab close canceled.", - "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", - "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", - "sqlEditor.error.noConnection": "No active database connection for SQL execution.", - "sqlEditor.error.executionCanceled": "SQL execution was canceled.", - "sqlEditor.tab.scriptTitle": "Script {0}", - "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", - "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", - "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", - "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", - "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", - "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", - "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", - "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", - "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", - "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", - "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", - "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", - "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", - "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", - "sqlEditor.results.title": "Results", - "sqlEditor.saveSql.fileType": "SQL Files", - "sqlEditor.saveSql.pickerTitle": "Save SQL File", - "sqlEditor.export.pickerTitle": "Export SQL Data", - "sqlEditor.export.status.noResultTitle": "No execution result available for export.", - "sqlEditor.export.status.noResultDetail": "Execute a query first.", - "sqlEditor.export.status.successTitle": "Report exported.", - "sqlEditor.export.status.failedTitle": "Failed to export report.", - "sqlEditor.export.fileType.html": "HTML File", - "sqlEditor.export.fileType.json": "JSON File", - "sqlEditor.export.fileType.csv": "CSV File", - "sqlEditor.export.fileType.xlsx": "Excel Workbook", - "sqlEditor.export.defaultFileBase": "report", - "sqlEditor.export.defaultTitle": "SQL Report", - "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", - "sqlEditor.export.type.html.title": "HTML full-feature report", - "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", - "sqlEditor.export.type.json.title": "JSON execution contract", - "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", - "sqlEditor.export.type.csv.title": "CSV data export", - "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", - "sqlEditor.export.type.xlsx.title": "Excel workbook export", - "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", - "sqlEditor.export.dialog.windowTitle": "Export SQL Data", - "sqlEditor.export.dialog.title": "Export SQL Data", - "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", - "sqlEditor.export.dialog.confirm": "Export", - "sqlEditor.export.dialog.fileNameWatermark": "report.html", - "sqlEditor.export.dialog.titleWatermark": "SQL Report", - "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", - "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", - "sqlEditor.export.dialog.section.fileName": "FILE NAME", - "sqlEditor.export.dialog.section.reportTitle": "TITLE", - "sqlEditor.export.dialog.section.description": "DESCRIPTION", - "sqlEditor.export.dialog.section.options": "OPTIONS", - "sqlEditor.export.option.includeSchema": "Include output schema", - "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", - "sqlEditor.export.option.includeMetadata": "Include optional metadata", - "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", - "sqlEditor.export.badge.offline": "OFFLINE READY", - "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", - "sqlEditor.export.badge.dataOnly": "DATA ONLY" -} +{ + "main.brand": "AkkornStudio", + "main.tab.query1": "Query 1", + "main.new": "New", + "main.open": "Open", + "main.save": "Save", + "main.history": "History", + "main.layout": "Layout", + "main.preview": "Preview", + "main.undo": "Undo", + "main.redo": "Redo", + "main.cleanupOrphans": "Cleanup orphan nodes", + "main.autoFixAliasNaming": "Auto-fix alias naming", + "main.autoLayoutCanvas": "Auto layout canvas", + "main.toggleDataPreview": "Toggle data preview", + "main.language": "Language", + "main.restore.prompt": "Previous session found — restore the last canvas?", + "main.restore.button": "Restore session", + "main.cteEditor.editingPrefix": "Editing CTE: ", + "main.cteEditor.backToCanvas": "Back to Canvas", + "main.cteEditor.exitA11y": "Exit CTE editor", + "main.viewEditor.editingPrefix": "DDL > View: ", + "main.viewEditor.backToCanvas": "Back to DDL", + "main.viewEditor.exitA11y": "Exit view editor", + "connection.title": "Connection Manager", + "connection.subtitle": "作業フローを中断せずに接続の設定・テスト・有効化を行います", + "connection.none": "No connection", + "connection.active": "ACTIVE", + "connection.health.online": "Online", + "connection.health.degraded": "Degraded", + "connection.health.offline": "Offline", + "connection.tooltip.none": "No active connection — click to manage", + "connection.ping": "Ping", + "connection.saved": "SAVED CONNECTIONS", + "connection.new": "New Connection", + "connection.selectOrCreate": "Select a connection or create a new one", + "connection.name": "Connection Name", + "connection.provider": "Provider", + "connection.host": "Host", + "connection.port": "Port", + "connection.database": "Database", + "connection.sqlitePath": "SQLite Path", + "connection.sqliteBrowse": "Browse", + "connection.sqliteCreate": "Create", + "connection.username": "Username", + "connection.password": "Password", + "connection.timeout": "Timeout (seconds)", + "connection.test": "Test", + "connection.save": "Save", + "connection.connect": "Connect", + "connection.action.testConnection": "Test connection", + "connection.action.saveConnection": "Save connection", + "connection.action.connectConnection": "Connect connection", + "connection.status.connecting": "接続中...", + "connection.status.connected": "接続済み", + "connection.status.testing": "テスト中...", + "connection.status.failedPrefix": "接続に失敗しました", + "connection.status.metadataUnavailable": "接続に失敗しました: メタデータが利用できません。", + "connection.status.highLatency": "高遅延", + "connection.watermark.name": "本番DB", + "connection.watermark.host": "localhost", + "connection.watermark.port": "5432", + "connection.watermark.database": "database_name", + "connection.watermark.username": "user", + "connection.watermark.password": "••••••••", + "connection.watermark.timeout": "30", + "main.connectingDb": "Connecting to database...", + "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", + "status.nodesSeparator": " nodes · ", + "status.connectionsSuffix": " connections", + "status.undo": "Undo: ", + "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", + "connection.disconnect": "Disconnect", + "connection.action.disconnectConnection": "Disconnect connection", + "connectionTab.active": "ACTIVE CONNECTION", + "connectionTab.none": "No active connection", + "connectionTab.saved": "SAVED CONNECTIONS", + "connectionTab.new": "+ New Connection", + "schema.database": "DATABASE", + "schema.search": "Search tables, columns...", + "schema.loading": "Searching tables, columns...", + "schema.noConnection": "No Connection", + "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", + "schema.emptyNoTables": "No tables found", + "fileHistory.title": "Save/Load Version History", + "fileHistory.reload": "Reload", + "fileHistory.restoreSelected": "Restore selected", + "fileHistory.empty": "No local versions yet", + "fileHistory.emptyHint": "Save this file to generate version history.", + "preview.title": "Data Preview", + "preview.subtitle": "Review data and diagnostics before continuing", + "preview.run": "Run", + "preview.cancel": "Cancel", + "preview.tab.preview": "Preview", + "preview.tab.sql": "SQL", + "preview.close": "Close preview", + "preview.running": "Running preview query… ", + "preview.clickCancel": "Click Cancel to stop", + "preview.cancelled": "Query cancelled", + "preview.runAgain": "Press Run to execute again", + "preview.failed": "Preview execution failed", + "preview.technical": "TECHNICAL DETAILS", + "preview.noData": "No data yet", + "preview.f3Hint": "Press F3 or Space to run the current query", + "sqlImporter.title": "Import SQL to Graph", + "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", + "sqlImporter.sqlStatement": "SQL STATEMENT", + "sqlImporter.supported": "Supported: ", + "sqlImporter.import": "Import", + "sqlImporter.report": "CONVERSION REPORT", + "sqlEditor.mutation.dialogTitle": "変更の確認", + "sqlEditor.mutation.dialogSubtitle": "実行を確定する前に影響を確認してください", + "search.empty": "No nodes found", + "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", + "search.shortcut": "⇧A", + "search.spawn": "Spawn", + "commandPalette.empty": "検索に一致するコマンドがありません", + "commandPalette.search": "コマンド検索", + "commandPalette.shortcut": "CTRL+SHIFT+P", + "commandPalette.execute": "実行", + "context.editCte": "Edit Selected CTE", + "context.editViewSubcanvas": "Edit View Subcanvas", + "explain.title": "Explain Plan", + "explain.sql": "SQL", + "explain.option.analyze": "解析", + "explain.option.buffers": "バッファ", + "explain.badge.simulated": "シミュレーション", + "explain.timing.planning": "計画:", + "explain.timing.execution": "実行:", + "explain.section.snapshotComparison": "スナップショット比較", + "explain.section.indexRecommendations": "インデックスの推奨", + "explain.section.history": "履歴", + "explain.detail.estimated": "推定", + "explain.detail.actual": "実測", + "explain.detail.error": "誤差", + "explain.detail.time": "時間", + "explain.detail.loops": "ループ", + "explain.rerun": "Re-run", + "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", + "explain.running": "Running EXPLAIN…", + "explain.failed": "⚠ EXPLAIN failed", + "explain.noPlan": "No plan yet", + "explain.rerunHint": "Press Re-run to execute EXPLAIN", + "explain.header.operation": "操作", + "explain.header.cost": "コスト", + "explain.header.rows": "行数", + "explain.header.alert": "警告", + "explain.mode.list": "リスト", + "explain.mode.tree": "ツリー", + "explain.action.snapshot": "スナップショットを保存", + "explain.action.copyJson": "JSONをコピー", + "explain.action.copyText": "テキストをコピー", + "explain.action.saveJson": ".json を保存", + "explain.action.openDalibo": "Daliboで開く", + "explain.legend.seqscan": "SEQ SCAN — full table read, no index", + "explain.legend.sort": "SORT — in-memory sort", + "explain.legend.hash": "HASH — hash join", + "explain.escClose": "Esc to close", + "flowVersion.title": "Flow Version History", + "flowVersion.subtitle": "Create checkpoints, compare versions and restore", + "flowVersion.watermark": "Checkpoint label (optional)…", + "flowVersion.saveCheckpoint": "Save Checkpoint", + "flowVersion.compareMode": "Compare Mode", + "flowVersion.selectBase": "Select BASE version (from):", + "flowVersion.clickAny": "Then click any version in the list below to compare.", + "flowVersion.noCheckpoints": "No checkpoints yet", + "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", + "flowVersion.restore": "Restore", + "flowVersion.diffResults": "Diff Results", + "benchmark.title": "Query Benchmark", + "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", + "benchmark.sql": "SQL being benchmarked", + "benchmark.runLabel": "Run label", + "benchmark.runLabelWatermark": "Run 1", + "benchmark.iterations": "Iterations (1–100)", + "benchmark.warmup": "Warm-up passes (0–10)", + "benchmark.interval": "Interval between runs (ms)", + "benchmark.run": "Run Benchmark", + "benchmark.cancel": "Cancel", + "benchmark.clearHistory": "Clear History", + "benchmark.latest": "LATEST RESULT", + "benchmark.avg": "AVG", + "benchmark.median": "MEDIAN", + "benchmark.min": "MIN", + "benchmark.max": "MAX", + "benchmark.iterationsAt": " iterations · run at ", + "benchmark.history": "HISTORY", + "benchmark.header.label": "ラベル", + "benchmark.header.avg": "平均", + "benchmark.header.median": "中央値", + "benchmark.header.min": "最小", + "benchmark.header.max": "最大", + "benchmark.itersSuffix": " iters", + "diagnostics.title": "App Diagnostics", + "diagnostics.run": "Run", + "diagnostics.running": "Running checks…", + "diagnostics.ok": "OK", + "diagnostics.warning": "Warning", + "diagnostics.error": "Error", + "diagnostics.tooltip.rerun": "Re-run all checks", + "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", + "diagnostics.tooltip.close": "Close (Esc)", + "autoJoin.title": "Auto-Join Suggestions", + "autoJoin.titleForTable": "Auto-Join suggestions for {0}", + "autoJoin.acceptAll": "Accept All", + "autoJoin.accept": "Accept", + "autoJoin.skip": "Skip", + "autoJoin.allHandled": "All suggestions handled", + "autoJoin.joinKeyword": "JOIN", + "autoJoin.confidence.fkConstraint": "FK Constraint", + "autoJoin.confidence.fkReverse": "FK (Reverse)", + "autoJoin.confidence.namingMatch": "Naming Match", + "autoJoin.confidence.weakMatch": "Weak Match", + "autoJoin.runSelected": "Auto-Join Selected", + "autoJoin.noSimilarityTitle": "No automatic join found", + "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", + "autoJoin.appliedTitle": "Auto-join applied", + "autoJoin.manual.title": "Create Manual Join", + "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", + "autoJoin.manual.leftColumn": "Left column", + "autoJoin.manual.rightColumn": "Right column", + "autoJoin.manual.joinType": "Join type", + "autoJoin.manual.operator": "Operator", + "autoJoin.manual.confirm": "Create join", + "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", + "autoJoin.manualJoinCreatedTitle": "Manual join created", + "autoJoin.manualJoinFailedTitle": "Manual join could not be created", + "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", + "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", + "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", + "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", + "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", + "property.outputAlias": "OUTPUT ALIAS", + "property.sourceAlias": "SOURCE ALIAS", + "property.aliasWatermark": "e.g. MyColumn (optional)", + "property.parameters": "PARAMETERS", + "property.enabled": "Enabled", + "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", + "property.dateWatermark": "YYYY-MM-DD or leave empty", + "property.apply": "Apply", + "property.inputPins": "INPUT PINS", + "property.outputPins": "OUTPUT PINS", + "property.sqlTrace": "SQL TRACE", + "property.live": "live", + "node.numericValue": "Numeric Value", + "node.stringValue": "String Value", + "node.enterText": "Enter text", + "node.datetimeValue": "DateTime Value", + "node.valueLabel": "Value:", + "node.noInputs": "No inputs", + "node.loadingSample": "Loading sample…", + "node.previewFailed": "⚠ Preview failed", + "node.sampleRowsHint": "5 sample rows · demo data", + "sidebar.tab.nodes": "ノード", + "sidebar.tab.connection": "接続", + "sidebar.tab.schema": "スキーマ", + "sidebar.tab.diagnostics": "診断", + "sidebar.addNode": "+ Add Node (⇧A)", + "sidebar.previewF3": "Preview (F3)", + "nodesList.search": "Search nodes...", + "search.watermark": "Search nodes… (Esc to close)", + "search.snippets": "★ SNIPPETS", + "commandPalette.watermark": "コマンドを実行…(Esc で閉じる)", + "tooltip.newCanvas": "New canvas (Ctrl+N)", + "tooltip.openCanvas": "Open canvas (Ctrl+O)", + "tooltip.saveCanvas": "Save canvas (Ctrl+S)", + "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", + "tooltip.zoomOut": "Zoom out (Ctrl+-)", + "tooltip.zoomIn": "Zoom in (Ctrl++)", + "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", + "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", + "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", + "tooltip.dataPreview": "Data preview (F3)", + "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", + "tooltip.appDiagnostics": "App Diagnostics (self-check)", + "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", + "tooltip.cancelRunningQuery": "Cancel the running query", + "tooltip.closeEsc": "Close (Esc)", + "tooltip.recheckConnectionHealth": "Re-check connection health", + "tooltip.deleteConnection": "Delete connection", + "tooltip.testConnection": "Test connection", + "tooltip.saveConnection": "Save connection", + "tooltip.activateConnection": "Activate this connection", + "tooltip.toggleDataSamplePreview": "Toggle data sample preview", + "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", + "tooltip.copySql": "Copy SQL to clipboard", + "tooltip.formatSql": "Format SQL", + "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", + "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", + "tooltip.switchToQueryMode": "Switch to Query canvas", + "tooltip.switchToDdlMode": "Switch to DDL canvas", + "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", + "tooltip.pins.inputs": "Inputs", + "tooltip.pins.outputs": "Outputs", + "tooltip.pins.none": "None", + "tooltip.tableColumns": "Columns", + "tooltip.tableColumns.none": "No detailed columns", + "window.minimize": "Minimize window", + "window.maximizeRestore": "Maximize/restore window", + "window.close": "Close window", + "menu.newDiagram": "New diagram", + "menu.openFile": "Open file", + "menu.save": "Save", + "menu.fileHistory": "File history", + "menu.shortcuts": "Keyboard shortcuts", + "menu.settings": "Settings", + "menu.importDdlSchema": "Import DDL schema", + "menu.viewDdlSql": "View DDL SQL", + "menu.executeDdl": "Execute DDL", + "menu.backToStart": "Back to start", + "toast.ddlExecuteFailed": "Failed to execute DDL.", + "toast.ddlOpenFailed": "Failed to open DDL SQL.", + "toast.ddlImportFailed": "Failed to import schema into DDL.", + "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", + "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", + "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", + "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", + "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", + "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", + "toast.ddlTableImported": "Table imported into the DDL canvas.", + "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", + "toast.ddlNoActiveConnection": "No active connection to execute DDL.", + "toast.ddlExecutedSuccess": "DDL executed successfully.", + "toast.ddlExecutedWithIssues": "DDL executed with issues.", + "toast.switchToDdl": "Switch to DDL mode to generate SQL.", + "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", + "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", + "toast.previewOpenFailed": "Failed to open preview.", + "tab.switchFailed": "Failed to switch tab: {0}", + "settings.status.darkApplied": "Dark theme applied.", + "settings.status.lightApplied": "Light theme applied.", + "settings.status.snapUpdated": "Snap updated: {0}.", + "settings.status.languageToggled": "Language toggled.", + "settings.status.languageSelected": "Language selected: {0}.", + "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", + "settings.section.appearance.title": "Themes", + "settings.section.languageRegion.title": "言語と地域", + "settings.section.dateTime.title": "日付と時刻", + "settings.section.keyboard.title": "キーボードショートカット", + "settings.section.privacy.title": "プライバシー", + "settings.section.notification.title": "通知", + "settings.section.accessibility.title": "アクセシビリティ", + "settings.section.default.title": "設定", + "settings.section.appearance.subtitle": "スタイルを選択するか、テーマをカスタマイズします", + "settings.section.languageRegion.subtitle": "言語と地域フォーマットを管理します", + "settings.section.keyboard.subtitle": "コマンドパレットとキャンバス実行で使うキーボードショートカットをカスタマイズします。", + "settings.section.wip.subtitle": "作業中です。", + "settings.section.default.subtitle": "アプリケーション設定", + "settings.general": "General", + "settings.nav.appearance": "Appearance", + "settings.theme.light": "ライトモード", + "settings.theme.dark": "ダークモード", + "settings.theme.system": "システム設定", + "settings.gridSnap.title": "Grid Snap", + "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", + "settings.language.subtitle": "アプリの言語を選択してください。", + "settings.language.toggle": "言語を切り替える", + "settings.language.option.ptBR": "ポルトガル語(ブラジル)", + "settings.language.option.enUS": "英語(米国)", + "settings.language.option.esES": "スペイン語(スペイン)", + "settings.language.option.ruRU": "ロシア語", + "settings.language.option.jaJP": "日本語", + "settings.language.option.zhTW": "繁体字中国語", + "settings.themeJson.title": "テーマ JSON", + "settings.themeJson.subtitle": "テーマ JSON を貼り付けて、即時に適用・保存します。", + "settings.themeJson.apply": "JSON を適用", + "settings.themeJson.restoreDefault": "既定テーマに戻す", + "mode.query": "Query", + "mode.ddl": "DDL", + "sidebar.left.close": "Close left sidebar", + "sidebar.left.open": "Reopen left sidebar", + "sidebar.right.close": "Close right sidebar", + "sidebar.right.open": "Reopen right sidebar", + "connection.completedTitle": "Connection completed", + "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", + "connection.close": "Close connection manager", + "connection.refreshHealth": "Refresh connection health", + "common.details": "Details", + "common.cancel": "Cancel", + "common.keep": "Keep", + "common.clear": "Clear", + "zoom.out": "Zoom out", + "zoom.in": "Zoom in", + "zoom.fit": "Fit zoom to screen", + "zoom.level": "Zoom level", + "settings.theme.mode": "テーマモード", + "diagnostics.category.canvas": "Canvas Integrity", + "diagnostics.category.output": "Output & Execution", + "diagnostics.category.session": "Session & Safety", + "diagnostics.category.notice": "Runtime Notices", + "diagnostics.summary.ok": "All systems OK", + "diagnostics.summary.warningCount": "{0} warning(s) detected", + "diagnostics.summary.errorCount": "{0} error(s) detected", + "diagnostics.canvasMigration": "Canvas Migration", + "diagnostics.recommendation.resaveFile": "Re-save the file to update it to the latest schema version.", + "diagnostics.canvasState.name": "Canvas State", + "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", + "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", + "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", + "diagnostics.validation.name": "Validation Errors", + "diagnostics.validation.recommendation": "Fix highlighted nodes before running output preview", + "diagnostics.validation.errorWithWarnings": "{0} error(s) and {1} warning(s) in the graph", + "diagnostics.validation.warningOnly": "{0} warning(s) in the graph", + "diagnostics.validation.none": "No validation issues", + "diagnostics.orphan.name": "Orphan Nodes", + "diagnostics.orphan.recommendation": "Use the orphan cleanup action to remove unused nodes", + "diagnostics.orphan.count": "{0} node(s) not connected to any output", + "diagnostics.orphan.none": "No orphan nodes detected", + "diagnostics.naming.name": "Naming Conventions", + "diagnostics.naming.recommendation": "Use auto-fix alias naming when conformance is below 100%", + "diagnostics.naming.conformance": "Naming conformance: {0}%", + "diagnostics.naming.ok": "All aliases follow naming conventions (100%)", + "diagnostics.queryCompilation.name": "Live SQL Compilation", + "diagnostics.queryCompilation.recommendation": "Review SQL diagnostics in output when errors/warnings are reported.", + "diagnostics.queryCompilation.errorFallback": "Live SQL compilation reported errors.", + "diagnostics.queryCompilation.warningCounts": "{0} diagnostic item(s), {1} guardrail warning(s).", + "diagnostics.queryCompilation.ok": "Live SQL compiled without diagnostics.", + "diagnostics.previewSafety.name": "Preview Safety", + "diagnostics.previewSafety.recommendation": "Preview executes read-only statements only.", + "diagnostics.previewSafety.blocked": "Current SQL is mutating and blocked by Safe Preview mode.", + "diagnostics.previewSafety.ok": "Preview safety checks passed.", + "diagnostics.previewExecution.name": "Preview Execution", + "diagnostics.previewExecution.recommendation": "Run preview and inspect diagnostics for execution/runtime errors.", + "diagnostics.previewExecution.failed": "Preview execution failed.", + "diagnostics.previewExecution.cancelled": "Preview execution was cancelled.", + "diagnostics.previewExecution.done": "{0} row(s) in {1}ms.", + "diagnostics.previewExecution.none": "No preview execution issues detected.", + "diagnostics.ddlCompilation.name": "DDL Compilation", + "diagnostics.ddlCompilation.recommendation": "Fix DDL compile diagnostics before execution.", + "diagnostics.ddlCompilation.failed": "DDL compilation failed.", + "diagnostics.ddlCompilation.warningCount": "{0} warning(s) reported by DDL compiler.", + "diagnostics.ddlCompilation.ok": "DDL compilation succeeded.", + "diagnostics.ddlOutput.name": "DDL Output", + "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", + "diagnostics.ddlOutput.none": "No DDL statements generated yet.", + "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", + "diagnostics.undo.name": "Undo History", + "diagnostics.undo.recommendation": "History is in-memory only; save your canvas regularly.", + "diagnostics.undo.saved": "Canvas is saved (no unsaved changes).", + "diagnostics.undo.unsavedDeep": "Unsaved changes with {0} undo steps.", + "diagnostics.undo.unsaved": "Unsaved changes - {0} undo step(s) available.", + "diagnostics.report.title": "AkkornStudio - Diagnostic Report", + "diagnostics.report.generated": "Generated", + "diagnostics.report.overall": "Overall", + "diagnostics.report.details": "Details", + "diagnostics.report.recommendation": "Recommendation", + "diagnostics.report.lastCheck": "Last Check", + "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", + "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", + "preview.providerLabel": "Provider", + "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", + "preview.schemaAnalysis.run": "Run Analysis", + "preview.schemaAnalysis.cancel": "Cancel", + "preview.schemaAnalysis.issues": "Issues", + "preview.schemaAnalysis.clearFilters": "Clear Filters", + "preview.schemaAnalysis.clearBlacklist": "Clear Blacklist", + "preview.schemaAnalysis.severity": "Severity", + "preview.schemaAnalysis.severity.info": "Info", + "preview.schemaAnalysis.severity.warning": "Warning", + "preview.schemaAnalysis.severity.critical": "Critical", + "preview.schemaAnalysis.rule": "Rule", + "preview.schemaAnalysis.rule.fkCatalogInconsistent": "FK catalog inconsistent", + "preview.schemaAnalysis.rule.missingFk": "Missing FK", + "preview.schemaAnalysis.rule.namingConventionViolation": "Naming convention violation", + "preview.schemaAnalysis.rule.lowSemanticName": "Low semantic name", + "preview.schemaAnalysis.rule.missingRequiredComment": "Missing required comment", + "preview.schemaAnalysis.rule.nf1HintMultiValued": "1NF hint: multi-valued", + "preview.schemaAnalysis.rule.nf2HintPartialDependency": "2NF hint: partial dependency", + "preview.schemaAnalysis.rule.nf3HintTransitiveDependency": "3NF hint: transitive dependency", + "preview.schemaAnalysis.minConfidence": "Min Confidence", + "preview.schemaAnalysis.tableFilter": "Table Filter", + "preview.schemaAnalysis.tableFilterWatermark": "schema.table", + "preview.schemaAnalysis.ignore": "Execution Filters", + "preview.schemaAnalysis.ignoreViews": "Ignore views and materialized views", + "preview.schemaAnalysis.blacklist": "Table blacklist", + "preview.schemaAnalysis.blacklistAdd": "Add", + "preview.schemaAnalysis.blacklistRemove": "Remove selected", + "preview.schemaAnalysis.ignoreTable.placeholder": "schema.table", + "preview.schemaAnalysis.details": "Details", + "preview.schemaAnalysis.evidence": "Evidence", + "preview.schemaAnalysis.suggestions": "Suggestions", + "preview.schemaAnalysis.ruleDiagnostics": "Rule Diagnostics", + "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", + "preview.schemaAnalysis.copySql": "Copy SQL", + "preview.schemaAnalysis.applyToCanvas": "Apply to Canvas", + "preview.schemaAnalysis.summary.issues": "Issues:", + "preview.schemaAnalysis.summary.rawPrefix": "(raw:", + "preview.schemaAnalysis.summary.critical": "| Critical:", + "preview.schemaAnalysis.summary.warning": "| Warning:", + "preview.schemaAnalysis.summary.info": "| Info:", + "preview.schemaAnalysis.state.metadataUnavailable": "Metadata unavailable for structural analysis.", + "preview.schemaAnalysis.state.cancelled": "Analysis cancelled by the user.", + "preview.schemaAnalysis.state.partialTimeout": "Analysis finished partially due to timeout.", + "preview.schemaAnalysis.state.failed": "Structural analysis failed.", + "preview.schemaAnalysis.state.empty": "No inferable structural issue was detected.", + "preview.schemaAnalysis.state.noFilterMatch": "No issue matches the selected filters.", + "preview.schemaAnalysis.state.noIssueSelected": "No issue selected.", + "preview.schemaAnalysis.state.noSqlCandidate": "No SQL candidate available.", + "preview.schemaAnalysis.actionBlockedTooltip": "Action unavailable for the current risk level or capability.", + "common.navigate": "Navigate", + "common.close": "Close", + "common.esc": "Esc", + "common.ms": "ms", + "common.zero": "0", + "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", + "nodesList.empty": "No nodes found", + "nodesList.emptyHint": "Adjust the search term to explore available types", + "schema.emptyFiltered": "No objects found for the current filter", + "start.lastSnapshot": "Last snapshot", + "app.brandBadge": "VS", + "property.tab.properties": "Properties", + "property.tab.projectSettings": "Project Settings", + "property.nodeType": "NODE TYPE", + "property.selectNodeHint": "Select a node to edit its properties.", + "property.namingConventions": "Naming Conventions", + "property.aliasConvention": "Alias convention", + "property.enforceAliasNaming": "Enforce alias naming", + "property.warnReservedSql": "Warn on reserved SQL keywords", + "property.maxAliasLength": "Max alias length", + "property.maxAliasLengthDefault": "64", + "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", + "start.tips": "Tips", + "start.tips.quick": "Quick tips", + "start.tips.item1": "1. Click New Diagram to start from scratch.", + "start.tips.item2": "2. Use templates to speed up prototyping.", + "start.tips.item3": "3. Open saved connections to load real tables.", + "start.tips.shortcut": "Shortcut: CTRL+SHIFT+P opens the command palette.", + "start.workspace": "WORKSPACE", + "start.resumeTitle": "Continue where you left off", + "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", + "start.chip.quickFlow": "Quick flow", + "start.chip.templates": "Templates", + "start.chip.connections": "Connections", + "start.savedConnectionsTitle": "Saved Connections", + "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", + "start.noConnectionsTitle": "No connections configured yet", + "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", + "start.newConnection": "+ New Connection", + "start.recentProjectsTitle": "Recent Projects", + "start.searchRecent": "Search recent project...", + "start.quickActions": "Quick actions", + "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", + "start.noRecentTitle": "No recent projects yet", + "start.noRecentSubtitle": "Use the quick actions card above to get started.", + "start.exploreTemplates": "Explore templates", + "start.templatesFavoritesHint": "Favorites on top", + "start.favoriteTemplate": "Favorite template", + "node.columnSetPreview": "ColumnSet preview", + "node.view": "VIEW", + "node.tableDefinition": "Table Definition", + "node.join": "JOIN", + "node.window.addPartition": "Add PARTITION BY slot", + "node.window.removePartition": "Remove PARTITION BY slot", + "node.window.addOrder": "Add ORDER BY slot", + "node.window.removeOrder": "Remove ORDER BY slot", + "sql.keyword.select": "SELECT", + "sql.keyword.from": "FROM", + "sql.keyword.join": "JOIN", + "sql.keyword.where": "WHERE", + "sql.keyword.limit": "LIMIT", + "sqlImporter.close": "Close SQL importer", + "sqlImporter.report.imported": "Imported", + "sqlImporter.report.partial": "Partial", + "sqlImporter.report.skipped": "Skipped", + "benchmark.close": "Close benchmark", + "benchmark.p95": "P95", + "benchmark.n": "N", + "liveSql.safePreview": "SAFE PREVIEW MODE", + "liveSql.title": "LIVE SQL", + "liveSql.blocked": "BLOCKED", + "liveSql.copy": "Copy", + "liveSql.format": "Format", + "liveSql.benchmark": "Benchmark", + "liveSql.explain": "Explain", + "liveSql.actionsHint": "Performance tools", + "ddl.dialog.title": "Execute DDL", + "ddl.dialog.execute": "Execute", + "ddl.dialog.cancel": "Cancel", + "ddl.dialog.close": "Close", + "ddl.dialog.stopOnError": "Stop on first failure", + "ddl.dialog.confirmDestructive": "I confirm execution of destructive statements (DROP TABLE)", + "ddl.dialog.reviewBeforeRun": "Review the DDL script before confirming.", + "ddl.dialog.confirmQuestion": "Confirm DDL execution on the connected database?", + "ddl.dialog.irreversibleWarning": "This action can change the schema irreversibly.", + "ddl.dialog.mustConfirmDestructive": "Confirm destructive execution to continue.", + "ddl.dialog.executing": "Executing...", + "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", + "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", + "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", + "ddl.execute.result.failed": "Failed to execute DDL.", + "ddl.execute.result.cancelled": "Execution cancelled by the user.", + "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", + "context.deleteSingle": "Delete {0}", + "context.deleteMultiple": "Delete {0} nodes", + "context.bringForward": "Bring Forward (Ctrl+PgUp)", + "context.sendBackward": "Send Backward (Ctrl+PgDown)", + "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", + "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", + "context.normalizeLayers": "Normalize Layers", + "context.deleteWire": "Delete wire", + "context.addNode": "Add Node (Shift+A)", + "context.undoWithDescription": "Undo {0}", + "context.redo": "Redo", + "shortcuts.windowTitle": "Keyboard Shortcuts", + "shortcuts.headerTitle": "AkkornStudio - Shortcuts", + "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", + "shortcuts.filterWatermark": "Filter shortcuts by key or action...", + "shortcuts.resultCount": "{0} shortcuts", + "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", + "shortcuts.noneFound": "No shortcuts found.", + "shortcuts.section.fileGeneral": "ファイルと一般", + "shortcuts.section.editing": "編集", + "shortcuts.section.canvasNavigation": "キャンバスとナビゲーション", + "shortcuts.section.zoomPanPrecision": "ズーム、パン、精密操作", + "shortcuts.section.previewInspection": "プレビューと検査", + "shortcuts.key.deleteOrBackspace": "Del または Backspace", + "shortcuts.key.middleDrag": "中ボタン + ドラッグ", + "shortcuts.key.rightDrag": "右ボタン + ドラッグ", + "shortcuts.key.spaceDrag": "Space + ドラッグ", + "shortcuts.key.altLeftDrag": "Alt + 左ドラッグ", + "shortcuts.key.arrows": "矢印キー", + "shortcuts.key.shiftArrows": "Shift + 矢印キー", + "shortcuts.action.openShortcutScreen": "このショートカット画面を開く", + "shortcuts.action.newCanvas": "新しいキャンバス", + "shortcuts.action.openFile": "ファイルを開く", + "shortcuts.action.save": "保存", + "shortcuts.action.saveAs": "名前を付けて保存", + "shortcuts.action.commandPalette": "コマンドパレット", + "shortcuts.action.undo": "元に戻す", + "shortcuts.action.redo": "やり直し", + "shortcuts.action.selectAll": "すべて選択", + "shortcuts.action.deleteSelection": "選択を削除", + "shortcuts.action.closeOverlayCancel": "オーバーレイを閉じる / 操作をキャンセル", + "shortcuts.action.openNodeSearch": "ノード検索を開く", + "shortcuts.action.resetViewport": "ビューポートをリセット", + "shortcuts.action.centerSelection": "選択を中央に配置", + "shortcuts.action.fitSelection": "選択にフィット", + "shortcuts.action.autoLayout": "自動レイアウト", + "shortcuts.action.toggleSnapToGrid": "グリッドスナップ切替", + "shortcuts.action.bringForward": "前面へ", + "shortcuts.action.sendBackward": "背面へ", + "shortcuts.action.bringToFront": "最前面へ", + "shortcuts.action.sendToBack": "最背面へ", + "shortcuts.action.zoomInOut": "ズームイン / アウト", + "shortcuts.action.pan": "パン", + "shortcuts.action.temporaryPan": "一時パン", + "shortcuts.action.alternatePan": "代替パン", + "shortcuts.action.fineNudge": "選択を微調整移動", + "shortcuts.action.fastNudge": "高速移動", + "shortcuts.action.togglePreview": "データプレビュー切替", + "shortcuts.action.explainPlan": "実行計画", + "shortcuts.action.runPreview": "プレビュー実行", + "shortcuts.action.connectionManager": "接続マネージャー", + "shortcuts.action.flowVersionHistory": "フローバージョン履歴", + "shortcuts.resetAll": "すべてリセット", + "shortcuts.customized": "カスタム", + "shortcuts.default": "デフォルト", + "shortcuts.apply": "適用", + "shortcuts.reset": "リセット", + "shortcuts.status.resetAllSuccess": "すべてのショートカットをデフォルトに戻しました。", + "shortcuts.status.updated": "ショートカットを更新しました。", + "shortcuts.status.reset": "ショートカットをデフォルトに戻しました。", + "shortcuts.status.updateFailed": "ショートカットを更新できませんでした。", + "toast.severity.success": "Success", + "toast.severity.warning": "Warning", + "toast.severity.error": "Error", + "toast.details.success": "Success Details", + "toast.details.warning": "Warning Details", + "toast.details.error": "Error Details", + "diagnostics.area.cteEditor": "CTE Editor", + "diagnostics.area.viewEditor": "View Editor", + "diagnostics.area.subEditor": "Sub-editor", + "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", + "diagnostics.recommendation.reloadFileIfNeeded": "Reload the file if needed.", + "diagnostics.viewEditor.exitFailed": "Could not exit: {0}", + "diagnostics.viewEditor.canvasIncomplete": "the canvas is incomplete.", + "diagnostics.viewEditor.exitRecommendation": "Connect a valid ResultOutput or use the discard command.", + "diagnostics.viewEditor.restoreParentFailed": "Failed to restore the parent canvas. The subgraph was discarded.", + "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", + "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", + "diagnostics.canvasMigration.openWarning": "Open: {0}", + "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", + "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", + "diagnostics.recommendation.resaveLatestSchema": "Review diagnostics and re-save the canvas to persist the latest schema.", + "diagnostics.recommendation.saveMigratedSchema": "Review diagnostics and save the canvas to persist the migrated schema.", + "file.saveDialog.title": "Save Canvas", + "file.saveDialog.suggestedName": "Query1", + "file.save.success": "Canvas saved successfully.", + "file.save.failedWithReason": "Save failed: {0}", + "file.openDialog.title": "Open Canvas", + "file.open.failedWithReason": "Open failed: {0}", + "file.open.success": "Canvas opened successfully.", + "file.open.successWithWarnings": "Canvas opened with warnings.", + "session.restore.failedWithReason": "Restore failed: {0}", + "session.restore.successWithWarnings": "Session restored with warnings.", + "session.restore.success": "Session restored successfully.", + "export.documentation.dialogTitle": "Export Flow Documentation", + "export.documentation.success": "Documentation exported successfully.", + "export.documentation.failed": "Documentation export failed.", + "export.failed.pathPermissionsHint": "Check file path and permissions.", + "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", + "export.dialogTitleByExtension": "Export as {0}", + "export.success": "Export completed successfully.", + "export.failed": "Export failed.", + "fileHistory.currentFile.none": "No file selected", + "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", + "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", + "fileHistory.status.countAvailable": "{0} local version(s) available.", + "fileHistory.restore.failedWithReason": "Restore failed: {0}", + "fileHistory.restore.successFrom": "Restored version from {0}.", + "preview.status.cancelled": "Cancelled", + "preview.status.error": "Error", + "preview.status.ready": "Ready", + "preview.runningWithMs": "Running... {0}ms", + "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", + "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", + "explain.errorWithReason": "Explain plan error: {0}", + "explain.noSql": "No SQL to explain. Build a query on the canvas first.", + "ddl.compilationFailed": "Compilation failed", + "ddl.compileErrorWithReason": "DDL compile error: {0}", + "command.undo.name": "Undo", + "command.undo.description": "Undo last action", + "command.redo.name": "Redo", + "command.redo.description": "Redo last undone action", + "command.addNode.name": "Add Node", + "command.addNode.description": "Open node search menu to add a node", + "command.bringForward.name": "Bring Forward", + "command.bringForward.description": "Move selected nodes one layer forward", + "command.sendBackward.name": "Send Backward", + "command.sendBackward.description": "Move selected nodes one layer backward", + "command.bringToFront.name": "Bring to Front", + "command.bringToFront.description": "Move selected nodes to top layer", + "command.sendToBack.name": "Send to Back", + "command.sendToBack.description": "Move selected nodes to bottom layer", + "command.normalizeLayers.name": "Normalize Layers", + "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", + "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", + "main.orphanSuffix": "Orphan(s)", + "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", + "main.namingPrefix": "Naming", + "fileHistory.compressedLabel": "Compressed:", + "schema.itemsSuffix": "item(s)", + "property.panel.title": "Properties", + "property.panel.multiSelected": "{0} nodes selected", + "sqlImporter.status.pasteSelect": "Paste a SELECT statement above, then click Import.", + "sqlImporter.status.inputTooLarge": "SQL input is too large ({0:N0} chars). Limit is {1:N0}. Split the query or increase the import limit.", + "sqlImporter.status.parsing": "Parsing SQL...", + "sqlImporter.status.done": "Done - {0} imported, {1} partial, {2} skipped.", + "sqlImporter.status.cancelledByUser": "Import cancelled by user.", + "sqlImporter.status.timeout": "Import timed out after {0:0.#}s. Try a smaller query or increase timeout.", + "sqlImporter.status.parseError": "Parse error: {0}", + "diagnostics.area.undoRedoTransaction": "Undo/Redo Transaction", + "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", + "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", + "node.preview.noCatalog": "No catalog available", + "connection.error.searchMenuNotInitialized": "search menu not initialized", + "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", + "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", + "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", + "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", + "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", + "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", + "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", + "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", + "diagnostics.area.connection": "Connection", + "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", + "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", + "undoRedo.transaction.unnamed": "unnamed transaction", + "benchmark.runLabelDefault": "Run 1", + "benchmark.runLabelPattern": "Run {0}", + "benchmark.status.failedWithReason": "Benchmark failed: {0}", + "benchmark.status.noSql": "No SQL to benchmark - build a query first.", + "benchmark.status.warmupProgress": "Warm-up {0}/{1}...", + "benchmark.status.iterationProgress": "Iteration {0}/{1}...", + "benchmark.status.done": "Done - {0}", + "benchmark.status.cancelled": "Benchmark cancelled.", + "app.windowTitle": "AkkornStudio", + "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", + "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", + "sqlImporter.error.selectFromNotFound": "Could not find SELECT ... FROM in the query.", + "sqlImporter.error.fromClauseParseFailed": "Could not parse FROM clause.", + "sqlImporter.error.syntaxUnterminatedString": "Syntax error at line {0}, column {1}: unterminated string literal.", + "sqlImporter.error.missingClosingParenthesis": "missing closing ')'", + "sqlImporter.error.unexpectedClosingParenthesis": "unexpected ')'", + "sqlImporter.error.syntaxAtLineColumn": "Syntax error at line {0}, column {1}: {2}.", + "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", + "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", + "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", + "errorDiagnostics.connection.label": "Connection failed", + "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", + "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", + "errorDiagnostics.authorization.label": "Authorization error", + "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", + "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", + "errorDiagnostics.timeout.label": "Query timeout", + "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", + "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", + "errorDiagnostics.schema.label": "Schema error", + "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", + "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", + "errorDiagnostics.syntax.label": "SQL syntax error", + "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", + "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", + "errorDiagnostics.compatibility.label": "Compatibility error", + "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", + "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", + "errorDiagnostics.unknown.label": "Unexpected error", + "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", + "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", + "error.mainWindow.invalidDataContext": "MainWindow の DataContext は ShellViewModel である必要があります。", + "error.mainWindow.canvasNotInitialized": "CanvasViewModel が初期化されていません。", + "error.mainWindow.ddlPreviewUnavailable": "現在のキャンバスではDDLプレビューを利用できません。", + "themeJson.editor.template": "{\n \"meta\": { \"name\": \"カスタムテーマ\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", + "themeJson.error.pasteBeforeApply": "適用する前にテーマJSONを貼り付けてください。", + "themeJson.error.invalidJson": "無効なJSON: {0}", + "themeJson.error.emptyPayload": "無効なJSON: payloadが空です。", + "themeJson.error.invalidTheme": "無効なテーマ: {0}", + "themeJson.error.appliedButSaveFailed": "テーマは適用されましたが保存に失敗しました: {0}", + "themeJson.success.appliedAndSaved": "JSONテーマを適用して保存しました。", + "themeJson.success.customRemoved": "カスタムテーマを削除しました。既定テーマへ完全に戻すにはアプリを再起動してください。", + "themeJson.error.restoreDefaultFailed": "既定テーマの復元に失敗しました: {0}", + "themeValidator.error.configNull": "テーマ設定がnullです。", + "themeValidator.warning.noSections": "テーマに色またはタイポグラフィのセクションがありません。適用する内容がありません。", + "themeValidator.warning.invalidColor": "{0} の色 '{1}' は無効です。このキーは無視されます。", + "themeValidator.warning.sizeOutOfRange": "{0}={1} は範囲外です (8..48)。このキーは無視されます。", + "queryExecutor.error.openConnectionMethodNotFound": "orchestratorにOpenConnectionAsyncメソッドが見つかりません", + "queryExecutor.error.openConnectionInvokeFailed": "OpenConnectionAsyncの呼び出しに失敗しました", + "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}': ビューのSELECTは視覚的に再構築できません。subcanvasで手動編集してください。", + "ddlImporter.error.tableNotFoundInMetadata": "テーブル '{0}' は現在のメタデータに見つかりませんでした。", + "main.window.untitled": "無題", + "main.subEditor.noSeedProvided": "{0} のサブエディターseedが提供されませんでした。", + "main.layerOrder.bringToFront": "最前面へ", + "main.layerOrder.sendToBack": "最背面へ", + "main.layerOrder.bringForward": "前面へ移動", + "main.layerOrder.sendBackward": "背面へ移動", + "main.layerOrder.normalizeLayers": "レイヤーを正規化", + "export.fileType.html": "HTMLファイル", + "export.fileType.json": "JSONファイル", + "export.fileType.csv": "CSVファイル", + "export.fileType.excel": "Excelファイル", + "commandPalette.templatePrefix": "テンプレート: {0}", + "themeLoader.status.notFoundWithPath": "テーマファイルが見つかりません: {0}", + "themeLoader.status.deserializedNull": "テーマJSONのデシリアライズ結果が null でした。", + "themeLoader.status.loaded": "テーマJSONを正常に読み込みました。", + "credential.error.ciphertextTooShort": "暗号文ブロブが短すぎます。", + "credential.error.dpapiWindowsOnly": "DPAPI は Windows でのみ利用できます。", + "credential.warning.loadVaultFailed": "資格情報ボルト {0} の読み込みに失敗しました: {1}", + "credential.warning.persistVaultFailed": "資格情報ボルト {0} の保存に失敗しました: {1}", + "snippetStore.warning.loadFailed": "{0} からスニペットの読み込みに失敗しました: {1}", + "snippetStore.warning.saveFailed": "スニペットの保存に失敗しました: {0}", + "flowVersionStore.warning.loadFailed": "{0} からフローバージョンの読み込みに失敗しました: {1}", + "flowVersionStore.warning.saveFailed": "フローバージョンの保存に失敗しました: {0}", + "queryExecutor.error.queryEmpty": "クエリは空にできません", + "queryExecutor.error.providerNotSupported": "プロバイダー {0} はサポートされていません", + "queryExecutor.error.singleStatementOnly": "プレビューは単一のSQL文のみ受け付けます。", + "queryExecutor.error.queryEmptyWithPeriod": "クエリは空にできません。", + "queryExecutor.error.readOnlyOnly": "プレビューモードは読み取り専用SQLのみサポートします。", + "queryExecutor.error.namedParametersNotSupported": "プレビューモードは実行SQLのバインドパラメータをサポートしません。安全なリテラルをインラインで指定するか、プレビュー外で実行してください。", + "queryExecutor.error.positionalParametersNotSupported": "プレビューモードは位置パラメータプレースホルダー(? または )をサポートしません。", + "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "選択したノードを下端に揃えます", + "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "選択したノードを左端に揃えます", + "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "選択したノードを右端に揃えます", + "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "選択したノードを上端に揃えます", + "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "CTEサブキャンバスの編集を適用して親キャンバスに戻ります", + "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "ノードを論理的な列に自動配置します", + "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "選択したノードを水平軸で中央揃えします", + "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "選択したノードを垂直軸で中央揃えします", + "commandPalette.description.clear_canvas_and_start_fresh": "キャンバスをクリアして最初から開始します", + "commandPalette.description.clear_node_selection": "ノードの選択を解除します", + "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "エイリアスをプロジェクト設定の命名規則に変換します", + "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "チェックポイントを作成し、バージョン比較と以前の状態への復元を行います", + "commandPalette.description.delete_the_selected_nodes": "選択したノードを削除します", + "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "現在のサブエディタの編集を破棄して親キャンバスに戻ります", + "commandPalette.description.execute_the_current_query_in_preview": "プレビューで現在のクエリを実行します", + "commandPalette.description.fit_all_nodes_into_the_visible_area": "すべてのノードを表示領域に収めます", + "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "最初のCSVエクスポートノードからCSVファイルを生成します", + "commandPalette.description.generate_html_file_from_the_first_html_export_node": "最初のHTMLエクスポートノードからHTMLファイルを生成します", + "commandPalette.description.generate_json_file_from_the_first_json_export_node": "最初のJSONエクスポートノードからJSONファイルを生成します", + "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "最初のExcelエクスポートノードからXLSXブックを生成します", + "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "クエリ実行計画を確認し、スキャン種別・結合戦略・コスト見積りを確認します", + "commandPalette.description.load_a_vsaq_canvas_file": ".vsaq キャンバスファイルを読み込みます", + "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "現在のSQLをN回実行して平均/中央値/p95レイテンシを測定します", + "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "選択したCTE定義ノードの分離サブキャンバスエディタを開きます", + "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "保存ごとに作成されるローカル履歴を開き、過去スナップショットを復元します", + "commandPalette.description.open_output_preview_modal_for_the_active_mode": "アクティブモードの出力プレビューモーダルを開きます", + "commandPalette.description.open_shortcut_reference_screen": "ショートカット一覧画面を開きます", + "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "接続マネージャーを開いてDB接続の追加・編集・切替を行います", + "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "SELECT文を貼り付けてノードを自動生成します(FROM/JOIN/WHERE/LIMIT対応)", + "commandPalette.description.remove_all_nodes_not_connected_to_output": "出力に接続されていないノードをすべて削除します", + "commandPalette.description.reset_zoom_and_pan_to_default": "ズームとパンを初期値に戻します", + "commandPalette.description.save_canvas_to_a_new_file": "キャンバスを新しいファイルに保存します", + "commandPalette.description.save_current_canvas": "現在のキャンバスを保存します", + "commandPalette.description.save_markdown_documentation_of_the_current_flow": "現在のフローのMarkdownドキュメントを保存します", + "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "選択ノードを再利用可能なスニペットとして保存し、後でノード検索メニュー(⇧A)から挿入します", + "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "FK規約と命名パターンに基づいてテーブルソースノード間のJOIN候補を検出します", + "commandPalette.description.select_all_nodes_on_canvas": "キャンバス上のすべてのノードを選択します", + "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "ノード位置を16pxグリッドにスナップします(Ctrl+G)", + "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "選択ノードを水平方向に等間隔で配置します", + "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "選択ノードを垂直方向に等間隔で配置します", + "commandPalette.description.zoom_into_the_canvas": "キャンバスを拡大します", + "commandPalette.description.zoom_out_of_the_canvas": "キャンバスを縮小します", + "commandPalette.name.align_bottom": "下揃え", + "commandPalette.name.align_left": "左揃え", + "commandPalette.name.align_right": "右揃え", + "commandPalette.name.align_top": "上揃え", + "commandPalette.name.analyze_all_joins": "すべてのJOINを解析", + "commandPalette.name.auto_fix_naming": "命名を自動修正", + "commandPalette.name.auto_layout": "自動レイアウト", + "commandPalette.name.center_horizontally": "水平中央揃え", + "commandPalette.name.center_vertically": "垂直中央揃え", + "commandPalette.name.cleanup_orphans": "孤立ノードをクリーンアップ", + "commandPalette.name.delete_selected": "選択を削除", + "commandPalette.name.deselect_all": "すべて選択解除", + "commandPalette.name.discard_and_exit_editor": "破棄してエディタを終了", + "commandPalette.name.distribute_horizontally": "水平方向に均等配置", + "commandPalette.name.distribute_vertically": "垂直方向に均等配置", + "commandPalette.name.edit_selected_cte": "選択したCTEを編集", + "commandPalette.name.exit_cte_editor": "CTEエディタを終了", + "commandPalette.name.explain_plan": "実行計画", + "commandPalette.name.export_csv": "CSVをエクスポート", + "commandPalette.name.export_documentation": "ドキュメントをエクスポート", + "commandPalette.name.export_excel": "Excelをエクスポート", + "commandPalette.name.export_html": "HTMLをエクスポート", + "commandPalette.name.export_json": "JSONをエクスポート", + "commandPalette.name.file_save_load_history": "保存/読み込み履歴", + "commandPalette.name.fit_to_screen": "画面にフィット", + "commandPalette.name.flow_version_history": "フローバージョン履歴", + "commandPalette.name.import_sql_to_graph": "SQLをグラフにインポート", + "commandPalette.name.keyboard_shortcuts": "キーボードショートカット", + "commandPalette.name.manage_connections": "接続を管理", + "commandPalette.name.new_canvas": "新しいキャンバス", + "commandPalette.name.open_file": "ファイルを開く", + "commandPalette.name.reset_viewport": "ビューポートをリセット", + "commandPalette.name.run_preview": "プレビューを実行", + "commandPalette.name.run_query_benchmark": "クエリベンチマークを実行", + "commandPalette.name.save": "保存", + "commandPalette.name.save_as": "名前を付けて保存", + "commandPalette.name.save_selection_as_snippet": "選択をスニペットとして保存", + "commandPalette.name.select_all": "すべて選択", + "commandPalette.name.toggle_preview": "プレビューを切り替え", + "commandPalette.name.toggle_snap_to_grid": "グリッドスナップを切り替え", + "commandPalette.name.zoom_in": "ズームイン", + "commandPalette.name.zoom_out": "ズームアウト", + "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", + "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", + "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", + "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", + "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", + "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", + "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", + "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", + "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", + "commandPalette.tags.clear_selection": "clear selection", + "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", + "commandPalette.tags.create_insert_search_transform": "create insert search transform", + "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", + "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", + "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", + "commandPalette.tags.data_results_table_panel": "data results table panel", + "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", + "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", + "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", + "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", + "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", + "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", + "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", + "commandPalette.tags.export_json_file_output_save": "export json file output save", + "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", + "commandPalette.tags.export_persist_copy": "export persist copy", + "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", + "commandPalette.tags.forward_history": "forward history", + "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", + "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", + "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", + "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", + "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", + "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", + "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", + "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", + "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", + "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", + "commandPalette.tags.load_import_vsaq": "load import vsaq", + "commandPalette.tags.magnify_enlarge": "magnify enlarge", + "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", + "commandPalette.tags.persist_write_disk": "persist write disk", + "commandPalette.tags.remove_erase_nodes": "remove erase nodes", + "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", + "commandPalette.tags.reset_clear_blank": "reset clear blank", + "commandPalette.tags.revert_back_history": "revert back history", + "commandPalette.tags.shrink_reduce": "shrink reduce", + "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", + "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", + "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", + "sqlEditor.diffPreview.title": "Transactional Diff Preview", + "sqlEditor.mutation.confirmExecute": "Confirm Execute", + "sqlEditor.tab.closeAnyway": "Close Anyway", + "sqlEditor.tab.keepTab": "Keep Tab", + "sqlEditor.status.ready": "Ready.", + "sqlEditor.telemetry.none": "No execution telemetry yet.", + "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", + "sqlEditor.telemetry.errors.none": "No aggregated errors.", + "sqlEditor.diff.none": "No transactional diff preview available.", + "sqlEditor.mutation.estimate.none": "No mutation estimate available.", + "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", + "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", + "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", + "sqlEditor.tab.noPendingClose": "No tab close pending.", + "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", + "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", + "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", + "sqlEditor.message.empty": "Execute a statement to see messages.", + "sqlEditor.message.success": "Execution completed successfully.", + "sqlEditor.result.summary.empty": "Rows: - Time: -", + "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", + "sqlEditor.file.save.canceled": "Save canceled.", + "sqlEditor.file.save.noPath": "No target path selected.", + "sqlEditor.file.save.success": "SQL file saved.", + "sqlEditor.file.save.failed": "Save failed.", + "sqlEditor.file.open.failed": "Open failed.", + "sqlEditor.file.open.notFound": "Selected SQL file was not found.", + "sqlEditor.file.open.success": "SQL file opened.", + "sqlEditor.status.executing": "Executing SQL...", + "sqlEditor.status.executingScript": "Executing SQL script...", + "sqlEditor.status.executingStep": "Executing {0}/{1}...", + "sqlEditor.status.canceling": "Canceling execution...", + "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", + "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", + "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", + "sqlEditor.status.success": "Execution succeeded.", + "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", + "sqlEditor.status.canceled": "Execution canceled.", + "sqlEditor.status.failed": "Execution failed.", + "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", + "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", + "sqlEditor.result.tabTitle": "Result {0}", + "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", + "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", + "sqlEditor.tab.closed": "Tab closed.", + "sqlEditor.tab.closeCanceled": "Tab close canceled.", + "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", + "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", + "sqlEditor.error.noConnection": "No active database connection for SQL execution.", + "sqlEditor.error.executionCanceled": "SQL execution was canceled.", + "sqlEditor.tab.scriptTitle": "Script {0}", + "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", + "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", + "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", + "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", + "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", + "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", + "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", + "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", + "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", + "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", + "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", + "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", + "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", + "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", + "sqlEditor.results.title": "Results", + "sqlEditor.saveSql.fileType": "SQL Files", + "sqlEditor.saveSql.pickerTitle": "Save SQL File", + "sqlEditor.export.pickerTitle": "Export SQL Data", + "sqlEditor.export.status.noResultTitle": "No execution result available for export.", + "sqlEditor.export.status.noResultDetail": "Execute a query first.", + "sqlEditor.export.status.successTitle": "Report exported.", + "sqlEditor.export.status.failedTitle": "Failed to export report.", + "sqlEditor.export.fileType.html": "HTML File", + "sqlEditor.export.fileType.json": "JSON File", + "sqlEditor.export.fileType.csv": "CSV File", + "sqlEditor.export.fileType.xlsx": "Excel Workbook", + "sqlEditor.export.defaultFileBase": "report", + "sqlEditor.export.defaultTitle": "SQL Report", + "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", + "sqlEditor.export.type.html.title": "HTML full-feature report", + "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", + "sqlEditor.export.type.json.title": "JSON execution contract", + "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", + "sqlEditor.export.type.csv.title": "CSV data export", + "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", + "sqlEditor.export.type.xlsx.title": "Excel workbook export", + "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", + "sqlEditor.export.dialog.windowTitle": "Export SQL Data", + "sqlEditor.export.dialog.title": "Export SQL Data", + "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", + "sqlEditor.export.dialog.confirm": "Export", + "sqlEditor.export.dialog.fileNameWatermark": "report.html", + "sqlEditor.export.dialog.titleWatermark": "SQL Report", + "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", + "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", + "sqlEditor.export.dialog.section.fileName": "FILE NAME", + "sqlEditor.export.dialog.section.reportTitle": "TITLE", + "sqlEditor.export.dialog.section.description": "DESCRIPTION", + "sqlEditor.export.dialog.section.options": "OPTIONS", + "sqlEditor.export.option.includeSchema": "Include output schema", + "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", + "sqlEditor.export.option.includeMetadata": "Include optional metadata", + "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", + "sqlEditor.export.badge.offline": "OFFLINE READY", + "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", + "sqlEditor.export.badge.dataOnly": "DATA ONLY" +} diff --git a/src/DBWeaver.UI/Assets/Localization/pt-BR.json b/src/AkkornStudio.UI/Assets/Localization/pt-BR.json similarity index 98% rename from src/DBWeaver.UI/Assets/Localization/pt-BR.json rename to src/AkkornStudio.UI/Assets/Localization/pt-BR.json index 70f89f36..f0431953 100644 --- a/src/DBWeaver.UI/Assets/Localization/pt-BR.json +++ b/src/AkkornStudio.UI/Assets/Localization/pt-BR.json @@ -1,1151 +1,1151 @@ -{ - "main.brand": "DBWeaver", - "main.tab.query1": "Consulta 1", - "main.new": "Novo", - "main.open": "Abrir", - "main.save": "Salvar", - "main.history": "Histórico", - "main.layout": "Layout", - "main.preview": "Prévia", - "main.undo": "Desfazer", - "main.redo": "Refazer", - "main.cleanupOrphans": "Limpar nós órfãos", - "main.autoFixAliasNaming": "Autoajustar nomenclatura de aliases", - "main.autoLayoutCanvas": "Layout automático do canvas", - "main.toggleDataPreview": "Alternar prévia de dados", - "main.language": "Idioma", - "main.restore.prompt": "Sessão anterior encontrada — deseja restaurar o último canvas?", - "main.restore.button": "Restaurar sessão", - "main.cteEditor.editingPrefix": "Editando CTE: ", - "main.cteEditor.backToCanvas": "Voltar ao Canvas", - "main.cteEditor.exitA11y": "Sair do editor de CTE", - "main.viewEditor.editingPrefix": "DDL > View: ", - "main.viewEditor.backToCanvas": "Voltar ao DDL", - "main.viewEditor.exitA11y": "Sair do editor de view", - "connection.title": "Gerenciador de Conexões", - "connection.subtitle": "Configure, teste e ative conexões sem sair do fluxo", - "connection.none": "Sem conexão", - "connection.active": "ATIVA", - "connection.health.online": "Online", - "connection.health.degraded": "Degradada", - "connection.health.offline": "Offline", - "connection.tooltip.none": "Nenhuma conexão ativa — clique para gerenciar", - "connection.ping": "Ping", - "connection.saved": "CONEXÕES SALVAS", - "connection.new": "Nova Conexão", - "connection.selectOrCreate": "Selecione uma conexão ou crie uma nova", - "connection.name": "Nome da Conexão", - "connection.provider": "Provedor", - "connection.host": "Host", - "connection.port": "Porta", - "connection.database": "Banco de Dados", - "connection.sqlitePath": "Caminho SQLite", - "connection.sqliteBrowse": "Procurar", - "connection.sqliteCreate": "Criar", - "connection.username": "Usuário", - "connection.password": "Senha", - "connection.timeout": "Timeout (segundos)", - "connection.test": "Testar", - "connection.save": "Salvar", - "connection.connect": "Conectar", - "connection.action.testConnection": "Testar conexão", - "connection.action.saveConnection": "Salvar conexão", - "connection.action.connectConnection": "Conectar conexão", - "connection.status.connecting": "Conectando...", - "connection.status.connected": "Conectado", - "connection.status.testing": "Testando...", - "connection.status.failedPrefix": "Falha na conexão", - "connection.status.metadataUnavailable": "Falha na conexão: metadados indisponíveis.", - "connection.status.highLatency": "latência alta", - "connection.watermark.name": "Meu Banco de Produção", - "connection.watermark.host": "localhost", - "connection.watermark.port": "5432", - "connection.watermark.database": "nome_do_banco", - "connection.watermark.username": "usuario", - "connection.watermark.password": "••••••••", - "connection.watermark.timeout": "30", - "main.connectingDb": "Conectando ao banco de dados...", - "main.emptyHint": "Pressione ⇧A para adicionar seu primeiro nó, ou arraste uma tabela da barra lateral", - "status.nodesSeparator": " nós · ", - "status.connectionsSuffix": " conexões", - "status.undo": "Desfazer: ", - "status.shortcuts": "⇧A Nós · F3 Prévia · Ctrl+Z Desfazer · Del Remover · Alt+Arrastar Pan", - "connection.disconnect": "Desconectar", - "connection.action.disconnectConnection": "Desconectar conexão", - "connectionTab.active": "CONEXÃO ATIVA", - "connectionTab.none": "Nenhuma conexão ativa", - "connectionTab.saved": "CONEXÕES SALVAS", - "connectionTab.new": "+ Nova Conexão", - "schema.database": "BANCO DE DADOS", - "schema.search": "Buscar tabelas, colunas...", - "schema.loading": "Buscando tabelas, colunas...", - "schema.noConnection": "Sem conexão", - "schema.noConnectionHint": "Conecte-se a um banco para ver tabelas, colunas e relacionamentos", - "schema.emptyNoTables": "Nenhuma tabela encontrada", - "fileHistory.title": "Histórico de Versões (Salvar/Carregar)", - "fileHistory.reload": "Recarregar", - "fileHistory.restoreSelected": "Restaurar selecionada", - "fileHistory.empty": "Nenhuma versão local ainda", - "fileHistory.emptyHint": "Salve este arquivo para gerar histórico de versões.", - "preview.title": "Prévia de Dados", - "preview.subtitle": "Revise dados e diagnósticos antes de continuar", - "preview.run": "Executar", - "preview.cancel": "Cancelar", - "preview.tab.preview": "Prévia", - "preview.tab.sql": "SQL", - "preview.close": "Fechar prévia", - "preview.running": "Executando consulta de prévia… ", - "preview.clickCancel": "Clique em Cancelar para interromper", - "preview.cancelled": "Consulta cancelada", - "preview.runAgain": "Pressione Executar para tentar novamente", - "preview.failed": "Falha na execução da prévia", - "preview.technical": "DETALHES TÉCNICOS", - "preview.noData": "Sem dados ainda", - "preview.f3Hint": "Pressione F3 ou Espaço para executar a consulta atual", - "sqlImporter.title": "Importar SQL para Grafo", - "sqlImporter.subtitle": "Cole uma instrução SELECT — os nós são criados automaticamente", - "sqlImporter.sqlStatement": "INSTRUÇÃO SQL", - "sqlImporter.supported": "Suportado: ", - "sqlImporter.import": "Importar", - "sqlImporter.report": "RELATÓRIO DE CONVERSÃO", - "sqlEditor.mutation.dialogTitle": "Confirmacao de mutacao", - "sqlEditor.mutation.dialogSubtitle": "Revise o impacto antes de confirmar a execucao", - "search.empty": "Nenhum nó encontrado", - "search.emptyHint": "Tente buscar por UPPER, JSON, CAST, AND…", - "search.shortcut": "⇧A", - "search.spawn": "Criar", - "commandPalette.empty": "Nenhum comando corresponde à sua busca", - "commandPalette.search": "Busca de comandos", - "commandPalette.shortcut": "CTRL+SHIFT+P", - "commandPalette.execute": "Executar", - "context.editCte": "Editar CTE Selecionada", - "context.editViewSubcanvas": "Editar Subcanvas da View", - "explain.title": "Plano de Execução", - "explain.sql": "SQL", - "explain.option.analyze": "Analisar", - "explain.option.buffers": "Buffers", - "explain.badge.simulated": "SIMULADO", - "explain.timing.planning": "Planejamento:", - "explain.timing.execution": "Execucao:", - "explain.section.snapshotComparison": "Comparacao de snapshots", - "explain.section.indexRecommendations": "Recomendacoes de indice", - "explain.section.history": "Historico", - "explain.detail.estimated": "Estimadas", - "explain.detail.actual": "Atuais", - "explain.detail.error": "Erro", - "explain.detail.time": "Tempo", - "explain.detail.loops": "Loops", - "explain.rerun": "Reexecutar", - "explain.alertSuffix": " operação(ões) cara(s) detectada(s) — considere adicionar índices", - "explain.running": "Executando EXPLAIN…", - "explain.failed": "⚠ EXPLAIN falhou", - "explain.noPlan": "Nenhum plano ainda", - "explain.rerunHint": "Pressione Reexecutar para executar EXPLAIN", - "explain.header.operation": "OPERAÇÃO", - "explain.header.cost": "CUSTO", - "explain.header.rows": "LINHAS", - "explain.header.err": "ERR", - "explain.header.alert": "ALERTA", - "explain.snapshot.labelA": "A", - "explain.snapshot.labelB": "B", - "explain.mode.list": "Lista", - "explain.mode.tree": "Arvore", - "explain.action.snapshot": "Salvar snapshot", - "explain.action.copyJson": "Copiar JSON", - "explain.action.copyText": "Copiar texto", - "explain.action.saveJson": "Salvar .json", - "explain.action.openDalibo": "Abrir no Dalibo", - "explain.legend.seqscan": "SEQ SCAN — leitura completa da tabela, sem índice", - "explain.legend.sort": "SORT — ordenação em memória", - "explain.legend.hash": "HASH — junção por hash", - "explain.escClose": "Esc para fechar", - "flowVersion.title": "Histórico de Versões do Fluxo", - "flowVersion.subtitle": "Crie checkpoints, compare versões e restaure", - "flowVersion.watermark": "Rótulo do checkpoint (opcional)…", - "flowVersion.saveCheckpoint": "Salvar Checkpoint", - "flowVersion.compareMode": "Modo de Comparação", - "flowVersion.selectBase": "Selecione a versão BASE (de):", - "flowVersion.clickAny": "Depois clique em qualquer versão da lista abaixo para comparar.", - "flowVersion.noCheckpoints": "Nenhum checkpoint ainda", - "flowVersion.noCheckpointsHint": "Salve um checkpoint acima para começar a rastrear versões.", - "flowVersion.restore": "Restaurar", - "flowVersion.diffResults": "Resultados da Diferença", - "benchmark.title": "Benchmark de Consulta", - "benchmark.subtitle": "Mede latência média / mediana / p95 em N iterações", - "benchmark.sql": "SQL sendo avaliado", - "benchmark.runLabel": "Rótulo da execução", - "benchmark.runLabelWatermark": "Execução 1", - "benchmark.iterations": "Iterações (1–100)", - "benchmark.warmup": "Passes de aquecimento (0–10)", - "benchmark.interval": "Intervalo entre execuções (ms)", - "benchmark.run": "Executar Benchmark", - "benchmark.cancel": "Cancelar", - "benchmark.clearHistory": "Limpar Histórico", - "benchmark.latest": "ÚLTIMO RESULTADO", - "benchmark.avg": "MÉDIA", - "benchmark.median": "MEDIANA", - "benchmark.min": "MÍN", - "benchmark.max": "MÁX", - "benchmark.iterationsAt": " iterações · executado às ", - "benchmark.history": "HISTÓRICO", - "benchmark.header.label": "Rótulo", - "benchmark.header.avg": "Média", - "benchmark.header.median": "Mediana", - "benchmark.header.min": "Mín", - "benchmark.header.max": "Máx", - "benchmark.itersSuffix": " iterações", - "diagnostics.title": "Diagnóstico do Aplicativo", - "diagnostics.run": "Executar", - "diagnostics.running": "Executando verificações…", - "diagnostics.ok": "OK", - "diagnostics.warning": "Aviso", - "diagnostics.error": "Erro", - "diagnostics.tooltip.rerun": "Executar novamente todas as verificações", - "diagnostics.tooltip.copy": "Copiar relatório de diagnóstico para a área de transferência", - "diagnostics.tooltip.close": "Fechar (Esc)", - "autoJoin.title": "Sugestões de Auto-Join", - "autoJoin.titleForTable": "Sugestões de Auto-Join para {0}", - "autoJoin.acceptAll": "Aceitar Todas", - "autoJoin.accept": "Aceitar", - "autoJoin.skip": "Pular", - "autoJoin.allHandled": "Todas as sugestões foram tratadas", - "autoJoin.joinKeyword": "JOIN", - "autoJoin.confidence.fkConstraint": "Restrição FK", - "autoJoin.confidence.fkReverse": "FK (Reversa)", - "autoJoin.confidence.namingMatch": "Correspondência por nome", - "autoJoin.confidence.weakMatch": "Correspondência fraca", - "autoJoin.runSelected": "Auto-Join Selecionado", - "autoJoin.noSimilarityTitle": "Nenhum auto join encontrado", - "autoJoin.noSimilarityDetails": "Escolha manualmente as colunas para criar um join simples.", - "autoJoin.appliedTitle": "Auto-join aplicado", - "autoJoin.manual.title": "Criar Join Manual", - "autoJoin.manual.subtitle": "Nenhuma similaridade confiável foi encontrada. Selecione uma coluna de cada tabela.", - "autoJoin.manual.leftColumn": "Coluna da esquerda", - "autoJoin.manual.rightColumn": "Coluna da direita", - "autoJoin.manual.joinType": "Tipo de join", - "autoJoin.manual.operator": "Operador", - "autoJoin.manual.confirm": "Criar join", - "autoJoin.manual.noCompatible": "Nao ha colunas compativeis para o tipo de pin selecionado na esquerda.", - "autoJoin.manualJoinCreatedTitle": "Join manual criado", - "autoJoin.manualJoinFailedTitle": "Não foi possível criar o join manual", - "autoJoin.manualJoinFailedDetails": "Verifique as colunas selecionadas e joins existentes, depois tente novamente.", - "autoJoin.multipleCandidatesTitle": "Múltiplas opções de join encontradas", - "autoJoin.multipleCandidatesDetails": "{0} combinações possíveis foram encontradas. Confirme quais colunas devem ser usadas.", - "autoJoin.suggestionsFoundTitle": "Sugestões de auto-join disponíveis", - "autoJoin.suggestionsFoundDetails": "{0} sugestão(ões) encontrada(s). Selecione duas tabelas e execute Auto-Join Selecionado.", - "property.outputAlias": "ALIAS DE SAÍDA", - "property.sourceAlias": "ALIAS DE FONTE", - "property.aliasWatermark": "ex.: MinhaColuna (opcional)", - "property.parameters": "PARÂMETROS", - "property.enabled": "Habilitado", - "property.datetimeWatermark": "AAAA-MM-DDTHH:mm:ss ou deixe vazio", - "property.dateWatermark": "AAAA-MM-DD ou deixe vazio", - "property.apply": "Aplicar", - "property.inputPins": "PINS DE ENTRADA", - "property.outputPins": "PINS DE SAÍDA", - "property.sqlTrace": "RASTREIO SQL", - "property.live": "ao vivo", - "node.numericValue": "Valor Numérico", - "node.stringValue": "Valor de Texto", - "node.enterText": "Digite o texto", - "node.datetimeValue": "Valor de Data/Hora", - "node.valueLabel": "Valor:", - "node.noInputs": "Sem entradas", - "node.loadingSample": "Carregando amostra…", - "node.previewFailed": "⚠ Falha na prévia", - "node.sampleRowsHint": "5 linhas de amostra · dados de demonstração", - "sidebar.tab.nodes": "Nós", - "sidebar.tab.connection": "Conexão", - "sidebar.tab.schema": "Esquema", - "sidebar.tab.diagnostics": "Diagnósticos", - "sidebar.addNode": "+ Adicionar Nó (⇧A)", - "sidebar.previewF3": "Prévia (F3)", - "nodesList.search": "Buscar nós...", - "search.watermark": "Buscar nós… (Esc para fechar)", - "search.snippets": "★ SNIPPETS", - "commandPalette.watermark": "Executar um comando… (Esc para fechar)", - "tooltip.newCanvas": "Novo canvas (Ctrl+N)", - "tooltip.openCanvas": "Abrir canvas (Ctrl+O)", - "tooltip.saveCanvas": "Salvar canvas (Ctrl+S)", - "tooltip.fileHistory": "Histórico local de salvar/carregar (Ctrl+Alt+H)", - "tooltip.zoomOut": "Diminuir zoom (Ctrl+-)", - "tooltip.zoomIn": "Aumentar zoom (Ctrl++)", - "tooltip.fitToScreen": "Ajustar à tela (Ctrl+0)", - "tooltip.autoLayout": "Layout automático — organiza os nós em colunas lógicas (Ctrl+L, Ctrl+Z para desfazer)", - "tooltip.snapToGrid": "Alternar snap na grade (Ctrl+G)", - "tooltip.dataPreview": "Prévia de dados (F3)", - "tooltip.toggleLanguage": "Alternar idioma (pt-BR / en-US)", - "tooltip.appDiagnostics": "Diagnóstico do app (autoavaliação)", - "tooltip.keyboardShortcuts": "Atalhos do teclado (F1)", - "tooltip.cancelRunningQuery": "Cancelar a consulta em execução", - "tooltip.closeEsc": "Fechar (Esc)", - "tooltip.recheckConnectionHealth": "Verificar novamente a saúde da conexão", - "tooltip.deleteConnection": "Excluir conexão", - "tooltip.testConnection": "Testar conexão", - "tooltip.saveConnection": "Salvar conexão", - "tooltip.activateConnection": "Ativar esta conexão", - "tooltip.toggleDataSamplePreview": "Alternar prévia de dados de amostra", - "tooltip.liveSqlMutatingBlocked": "Este SQL contém um comando que altera dados e não pode ser executado no Modo de Prévia Segura", - "tooltip.copySql": "Copiar SQL para a área de transferência", - "tooltip.formatSql": "Formatar SQL", - "tooltip.openBenchmark": "Abrir Benchmark de Consulta (medir média / mediana / p95 de latência)", - "tooltip.openExplainPlan": "Abrir inspetor de Explain Plan — visualizar o plano de execução da consulta", - "tooltip.switchToQueryMode": "Alternar para o canvas de Query", - "tooltip.switchToDdlMode": "Alternar para o canvas de DDL", - "tooltip.switchToSqlMode": "Alternar para o editor SQL", - "tooltip.autoJoinSelected": "Tentar auto-join entre as duas tabelas selecionadas", - "tooltip.pins.inputs": "Entradas", - "tooltip.pins.outputs": "Saídas", - "tooltip.pins.none": "Nenhum", - "tooltip.tableColumns": "Colunas", - "tooltip.tableColumns.none": "Sem colunas detalhadas", - "window.minimize": "Minimizar janela", - "window.maximizeRestore": "Maximizar/restaurar janela", - "window.close": "Fechar janela", - "menu.newDiagram": "Novo diagrama", - "menu.openFile": "Abrir arquivo", - "menu.save": "Salvar", - "menu.fileHistory": "Histórico de arquivos", - "menu.shortcuts": "Atalhos de teclado", - "menu.settings": "Configurações", - "menu.group.project": "PROJETO", - "menu.group.currentMode": "MODO ATUAL", - "menu.group.tools": "FERRAMENTAS", - "menu.reason.ddlOnly": "Disponível apenas no modo DDL.", - "menu.importSqlQuery": "Importar SQL para Query", - "menu.importDdlSchema": "Importar Schema DDL", - "menu.viewDdlSql": "Ver SQL DDL", - "menu.executeDdl": "Executar DDL", - "menu.backToStart": "Voltar para início", - "toast.ddlExecuteFailed": "Falha ao executar DDL.", - "toast.ddlOpenFailed": "Falha ao abrir SQL DDL.", - "toast.ddlImportFailed": "Falha ao importar schema para DDL.", - "toast.ddlConnectToImportSchema": "Conecte-se a um banco para importar schema no canvas DDL.", - "toast.ddlNoTablesFound": "Nenhuma tabela encontrada para importar no modo DDL.", - "toast.ddlSchemaImported": "Schema importado para o canvas DDL.", - "toast.ddlImportSummary": "{0} tabela(s), {1} coluna(s), {2} FK(s), {3} indice(s) unicos.", - "toast.ddlConnectToImportTable": "Conecte-se a um banco para importar tabelas no canvas DDL.", - "toast.ddlTableAlreadyExists": "A tabela '{0}' ja existe no canvas DDL.", - "toast.ddlTableImported": "Tabela importada para o canvas DDL.", - "toast.ddlTableImportSummary": "Nos: +{0}, conexoes: +{1}, FKs: +{2}.", - "toast.ddlNoActiveConnection": "Nenhuma conexão ativa para executar DDL.", - "toast.ddlExecutedSuccess": "DDL executado com sucesso.", - "toast.ddlExecutedWithIssues": "DDL executado com falhas.", - "toast.switchToDdl": "Alterne para o modo DDL para gerar SQL.", - "toast.ddlInvalid": "DDL inválido. Corrija os erros antes de continuar.", - "toast.ddlNoStatements": "Nenhum statement DDL foi gerado no canvas.", - "toast.previewOpenFailed": "Falha ao abrir preview.", - "tab.switchFailed": "Falha ao alternar aba: {0}", - "settings.status.darkApplied": "Tema escuro aplicado.", - "settings.status.lightApplied": "Tema claro aplicado.", - "settings.status.snapUpdated": "Snap atualizado: {0}.", - "settings.status.languageToggled": "Idioma alternado.", - "settings.status.languageSelected": "Idioma selecionado: {0}.", - "settings.status.themeEditorReady": "Editor de tema pronto. Aplique para salvar e usar o tema.", - "settings.section.appearance.title": "Temas", - "settings.section.languageRegion.title": "Idioma e Região", - "settings.section.dateTime.title": "Data e Hora", - "settings.section.keyboard.title": "Atalhos de Teclado", - "settings.section.privacy.title": "Privacidade", - "settings.section.notification.title": "Notificações", - "settings.section.accessibility.title": "Acessibilidade", - "settings.section.default.title": "Configurações", - "settings.section.appearance.subtitle": "Escolha o estilo ou personalize seu tema", - "settings.section.languageRegion.subtitle": "Gerencie idioma e formatação regional", - "settings.section.keyboard.subtitle": "Personalize os atalhos de teclado usados pela command palette e execução do canvas.", - "settings.section.wip.subtitle": "Trabalho em progresso.", - "settings.section.default.subtitle": "Configurações da aplicação", - "settings.general": "Geral", - "settings.nav.appearance": "Aparência", - "settings.theme.light": "Modo Claro", - "settings.theme.dark": "Modo Escuro", - "settings.theme.system": "Preferências do Sistema", - "settings.gridSnap.title": "Snap na Grade", - "settings.gridSnap.subtitle": "Controla o snap dos nós no canvas.", - "settings.language.subtitle": "Alterna entre PT-BR e EN-US.", - "settings.language.toggle": "Alternar idioma", - "settings.language.option.ptBR": "Português (Brasil)", - "settings.language.option.enUS": "Inglês (Estados Unidos)", - "settings.language.option.esES": "Espanhol (Espanha)", - "settings.language.option.ruRU": "Russo", - "settings.language.option.jaJP": "Japonês", - "settings.language.option.zhTW": "Chinês Tradicional", - "settings.themeJson.title": "Theme JSON", - "settings.themeJson.subtitle": "Cole o JSON do tema, aplique e persista instantaneamente.", - "settings.themeJson.apply": "Aplicar JSON", - "settings.themeJson.restoreDefault": "Restaurar tema padrão", - "mode.query": "Query", - "mode.ddl": "DDL", - "mode.sql": "SQL", - "sidebar.left.close": "Fechar barra lateral esquerda", - "sidebar.left.open": "Reabrir barra lateral esquerda", - "sidebar.right.close": "Fechar barra lateral direita", - "sidebar.right.open": "Reabrir barra lateral direita", - "connection.completedTitle": "Conexão concluída", - "connection.clearCanvasPrompt": "Deseja limpar o canvas atual para começar com a nova conexão?", - "connection.close": "Fechar gerenciador de conexões", - "connection.refreshHealth": "Atualizar saúde da conexão", - "common.details": "Detalhes", - "common.cancel": "Cancelar", - "common.keep": "Manter", - "common.clear": "Limpar", - "zoom.out": "Diminuir zoom", - "zoom.in": "Aumentar zoom", - "zoom.fit": "Ajustar zoom à tela", - "zoom.level": "Nível de zoom", - "settings.theme.mode": "Modo de tema", - "diagnostics.category.canvas": "Integridade do Canvas", - "diagnostics.category.output": "Saída e Execução", - "diagnostics.category.session": "Sessão e Segurança", - "diagnostics.category.notice": "Avisos em Tempo de Execução", - "diagnostics.summary.ok": "Sistema sem problemas", - "diagnostics.summary.warningCount": "{0} aviso(s) detectado(s)", - "diagnostics.summary.errorCount": "{0} erro(s) detectado(s)", - "diagnostics.canvasMigration": "Migração de Canvas", - "diagnostics.recommendation.resaveFile": "Salve novamente o arquivo para atualizar para a versão mais recente do schema.", - "diagnostics.canvasState.name": "Estado do Canvas", - "diagnostics.canvasState.recommendation": "Adicione pelo menos um {0} e um {1}", - "diagnostics.canvasState.empty": "Canvas vazio - nenhum nó presente", - "diagnostics.canvasState.counts": "{0} nó(s), {1} conexão(ões)", - "diagnostics.validation.name": "Erros de Validação", - "diagnostics.validation.recommendation": "Corrija os nós destacados antes de abrir a prévia de saída", - "diagnostics.validation.errorWithWarnings": "{0} erro(s) e {1} aviso(s) no grafo", - "diagnostics.validation.warningOnly": "{0} aviso(s) no grafo", - "diagnostics.validation.none": "Sem problemas de validação", - "diagnostics.orphan.name": "Nós Órfãos", - "diagnostics.orphan.recommendation": "Use a limpeza de órfãos para remover nós sem uso", - "diagnostics.orphan.count": "{0} nó(s) não conectado(s) a nenhuma saída", - "diagnostics.orphan.none": "Nenhum nó órfão detectado", - "diagnostics.naming.name": "Convenções de Nome", - "diagnostics.naming.recommendation": "Use correção automática quando a conformidade estiver abaixo de 100%", - "diagnostics.naming.conformance": "Conformidade de nomes: {0}%", - "diagnostics.naming.ok": "Todos os aliases seguem as convenções (100%)", - "diagnostics.queryCompilation.name": "Compilação SQL ao Vivo", - "diagnostics.queryCompilation.recommendation": "Revise os diagnósticos SQL quando houver erros/avisos", - "diagnostics.queryCompilation.errorFallback": "A compilação SQL ao vivo reportou erros.", - "diagnostics.queryCompilation.warningCounts": "{0} item(ns) de diagnóstico, {1} aviso(s) de guardrail.", - "diagnostics.queryCompilation.ok": "SQL ao vivo compilado sem diagnósticos.", - "diagnostics.previewSafety.name": "Segurança da Prévia", - "diagnostics.previewSafety.recommendation": "A prévia executa apenas comandos de leitura.", - "diagnostics.previewSafety.blocked": "O SQL atual altera dados e foi bloqueado pelo modo seguro.", - "diagnostics.previewSafety.ok": "Verificações de segurança da prévia concluídas.", - "diagnostics.previewExecution.name": "Execução da Prévia", - "diagnostics.previewExecution.recommendation": "Execute a prévia e revise diagnósticos de execução", - "diagnostics.previewExecution.failed": "Falha na execução da prévia.", - "diagnostics.previewExecution.cancelled": "Execução da prévia cancelada.", - "diagnostics.previewExecution.done": "{0} linha(s) em {1}ms.", - "diagnostics.previewExecution.none": "Nenhum problema de execução de prévia detectado.", - "diagnostics.ddlCompilation.name": "Compilação DDL", - "diagnostics.ddlCompilation.recommendation": "Corrija diagnósticos de compilação DDL antes de executar", - "diagnostics.ddlCompilation.failed": "Falha na compilação DDL.", - "diagnostics.ddlCompilation.warningCount": "{0} aviso(s) reportado(s) pelo compilador DDL.", - "diagnostics.ddlCompilation.ok": "Compilação DDL concluída com sucesso.", - "diagnostics.ddlOutput.name": "Saída DDL", - "diagnostics.ddlOutput.recommendation": "Complete os nós DDL até gerar ao menos um statement", - "diagnostics.ddlOutput.none": "Nenhum statement DDL gerado ainda.", - "diagnostics.ddlOutput.lines": "{0} linha(s) de DDL gerada(s).", - "diagnostics.undo.name": "Histórico de Desfazer", - "diagnostics.undo.recommendation": "O histórico fica apenas em memória; salve o canvas regularmente.", - "diagnostics.undo.saved": "Canvas salvo (sem mudanças pendentes).", - "diagnostics.undo.unsavedDeep": "Mudanças não salvas com {0} passos de desfazer.", - "diagnostics.undo.unsaved": "Mudanças não salvas - {0} passo(s) de desfazer disponível(is).", - "diagnostics.report.title": "DBWeaver - Relatório de Diagnóstico", - "diagnostics.report.generated": "Gerado", - "diagnostics.report.overall": "Resumo", - "diagnostics.report.details": "Detalhes", - "diagnostics.report.recommendation": "Recomendação", - "diagnostics.report.lastCheck": "Última verificação", - "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", - "node.datetimeFormat": "AAAA-MM-DDTHH:mm:ss", - "preview.providerLabel": "Provedor", - "preview.ddlDiagnosticsHint": "Detalhes de erros/avisos disponíveis em Diagnósticos.", - "preview.schemaAnalysis.run": "Executar análise", - "preview.schemaAnalysis.cancel": "Cancelar", - "preview.schemaAnalysis.issues": "Issues", - "preview.schemaAnalysis.clearFilters": "Limpar filtros", - "preview.schemaAnalysis.clearBlacklist": "Limpar blacklist", - "preview.schemaAnalysis.severity": "Severidade", - "preview.schemaAnalysis.severity.info": "Info", - "preview.schemaAnalysis.severity.warning": "Aviso", - "preview.schemaAnalysis.severity.critical": "Crítico", - "preview.schemaAnalysis.rule": "Regra", - "preview.schemaAnalysis.rule.fkCatalogInconsistent": "Catálogo de FK inconsistente", - "preview.schemaAnalysis.rule.missingFk": "FK ausente", - "preview.schemaAnalysis.rule.namingConventionViolation": "Violação de convenção de nomes", - "preview.schemaAnalysis.rule.lowSemanticName": "Nome com baixa semântica", - "preview.schemaAnalysis.rule.missingRequiredComment": "Comentário obrigatório ausente", - "preview.schemaAnalysis.rule.nf1HintMultiValued": "Indício 1FN: multi-valorado", - "preview.schemaAnalysis.rule.nf2HintPartialDependency": "Indício 2FN: dependência parcial", - "preview.schemaAnalysis.rule.nf3HintTransitiveDependency": "Indício 3FN: dependência transitiva", - "preview.schemaAnalysis.minConfidence": "Confiança mínima", - "preview.schemaAnalysis.tableFilter": "Filtro de tabela", - "preview.schemaAnalysis.tableFilterWatermark": "schema.tabela", - "preview.schemaAnalysis.ignore": "Filtros de execução", - "preview.schemaAnalysis.ignoreViews": "Ignorar views e materialized views", - "preview.schemaAnalysis.blacklist": "Blacklist de tabelas", - "preview.schemaAnalysis.blacklistAdd": "Adicionar", - "preview.schemaAnalysis.blacklistRemove": "Remover selecionada", - "preview.schemaAnalysis.ignoreTable.placeholder": "schema.tabela", - "preview.schemaAnalysis.details": "Detalhes", - "preview.schemaAnalysis.evidence": "Evidências", - "preview.schemaAnalysis.suggestions": "Sugestões", - "preview.schemaAnalysis.ruleDiagnostics": "Diagnósticos da regra", - "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", - "preview.schemaAnalysis.copySql": "Copiar SQL", - "preview.schemaAnalysis.applyToCanvas": "Aplicar no canvas", - "preview.schemaAnalysis.summary.issues": "Issues:", - "preview.schemaAnalysis.summary.rawPrefix": "(bruto:", - "preview.schemaAnalysis.summary.critical": "| Crítico:", - "preview.schemaAnalysis.summary.warning": "| Aviso:", - "preview.schemaAnalysis.summary.info": "| Info:", - "preview.schemaAnalysis.state.metadataUnavailable": "Metadata indisponível para análise estrutural.", - "preview.schemaAnalysis.state.cancelled": "Análise cancelada pelo usuário.", - "preview.schemaAnalysis.state.partialTimeout": "Análise finalizada parcialmente por timeout.", - "preview.schemaAnalysis.state.failed": "Falha na análise estrutural.", - "preview.schemaAnalysis.state.empty": "Nenhum problema estrutural inferível foi detectado.", - "preview.schemaAnalysis.state.noFilterMatch": "Nenhuma issue corresponde aos filtros selecionados.", - "preview.schemaAnalysis.state.noIssueSelected": "Nenhuma issue selecionada.", - "preview.schemaAnalysis.state.noSqlCandidate": "Nenhum SQL candidate disponível.", - "preview.schemaAnalysis.actionBlockedTooltip": "Ação indisponível para o nível de risco ou capacidade atual.", - "common.navigate": "Navegar", - "common.close": "Fechar", - "common.esc": "Esc", - "common.ms": "ms", - "common.zero": "0", - "diagnostics.tip": "Dica: verifique Diagnósticos sempre que preview/saída reportar avisos ou erros.", - "nodesList.empty": "Nenhum nó encontrado", - "nodesList.emptyHint": "Ajuste o termo de busca para explorar os tipos disponíveis", - "schema.emptyFiltered": "Nenhum objeto encontrado para o filtro atual", - "start.lastSnapshot": "Último snapshot", - "app.brandBadge": "VS", - "property.tab.properties": "Propriedades", - "property.tab.projectSettings": "Configurações do Projeto", - "property.nodeType": "TIPO DE NÓ", - "property.selectNodeHint": "Selecione um nó para editar suas propriedades.", - "property.namingConventions": "Convenções de Nomenclatura", - "property.aliasConvention": "Convenção de alias", - "property.enforceAliasNaming": "Forçar nomenclatura de alias", - "property.warnReservedSql": "Alertar sobre palavras reservadas SQL", - "property.maxAliasLength": "Tamanho máximo do alias", - "property.maxAliasLengthDefault": "64", - "property.namingSettingsHint": "Essas configurações são por projeto e são usadas por validação e helpers de nomenclatura.", - "start.tips": "Dicas", - "start.tips.quick": "Dicas rápidas", - "start.tips.item1": "1. Clique em Novo Diagrama para iniciar do zero.", - "start.tips.item2": "2. Use templates para acelerar protótipos.", - "start.tips.item3": "3. Abra conexões salvas para carregar tabelas reais.", - "start.tips.shortcut": "Atalho: CTRL+SHIFT+P abre a paleta de comandos.", - "start.workspace": "WORKSPACE", - "start.resumeTitle": "Continuar de onde parou", - "start.resumeSubtitle": "Retome rapidamente um projeto recente ou comece um novo diagrama.", - "start.chip.quickFlow": "Fluxo rápido", - "start.chip.templates": "Templates", - "start.chip.connections": "Conexões", - "start.savedConnectionsTitle": "Conexões salvas", - "start.savedConnectionsSubtitle": "Conecte-se rapidamente a um banco para carregar schema e tabelas.", - "start.noConnectionsTitle": "Nenhuma conexão configurada ainda", - "start.noConnectionsSubtitle": "Crie uma conexão para explorar tabelas reais no editor.", - "start.newConnection": "+ Nova Conexão", - "start.recentProjectsTitle": "Projetos Recentes", - "start.searchRecent": "Buscar projeto recente...", - "start.quickActions": "Ações rápidas", - "start.quickActionsSubtitle": "Abra um arquivo existente ou inicie um novo diagrama.", - "start.noRecentTitle": "Nenhum projeto recente ainda", - "start.noRecentSubtitle": "Use o card de ações rápidas acima para começar.", - "start.exploreTemplates": "Explorar templates", - "start.templatesFavoritesHint": "Favoritos no topo", - "start.favoriteTemplate": "Favoritar template", - "node.columnSetPreview": "Prévia de ColumnSet", - "node.view": "VIEW", - "node.tableDefinition": "Definição da Tabela", - "node.join": "JOIN", - "node.window.addPartition": "Adicionar slot PARTITION BY", - "node.window.removePartition": "Remover slot PARTITION BY", - "node.window.addOrder": "Adicionar slot ORDER BY", - "node.window.removeOrder": "Remover slot ORDER BY", - "sql.keyword.select": "SELECT", - "sql.keyword.from": "FROM", - "sql.keyword.join": "JOIN", - "sql.keyword.where": "WHERE", - "sql.keyword.limit": "LIMIT", - "sqlImporter.close": "Fechar importador SQL", - "sqlImporter.report.imported": "Importados", - "sqlImporter.report.partial": "Parciais", - "sqlImporter.report.skipped": "Ignorados", - "sqlImporter.confirmClearCanvas": "A importação SQL irá limpar o canvas atual. Deseja continuar?", - "sqlImporter.confirmProceed": "Continuar importação", - "benchmark.close": "Fechar benchmark", - "benchmark.p95": "P95", - "benchmark.n": "N", - "liveSql.safePreview": "MODO DE PRÉVIA SEGURA", - "liveSql.title": "SQL AO VIVO", - "liveSql.blocked": "BLOQUEADO", - "liveSql.copy": "Copiar", - "liveSql.format": "Formatar", - "liveSql.benchmark": "Benchmark", - "liveSql.explain": "Explain", - "liveSql.actionsHint": "Ferramentas de desempenho", - "ddl.dialog.title": "Executar DDL", - "ddl.dialog.execute": "Executar", - "ddl.dialog.cancel": "Cancelar", - "ddl.dialog.close": "Fechar", - "ddl.dialog.stopOnError": "Parar na primeira falha", - "ddl.dialog.confirmDestructive": "Confirmo execução de statements destrutivos (DROP TABLE)", - "ddl.dialog.reviewBeforeRun": "Revise o script DDL antes de confirmar.", - "ddl.dialog.confirmQuestion": "Confirma a execução do DDL no banco conectado?", - "ddl.dialog.irreversibleWarning": "Esta ação pode alterar o schema de forma irreversível.", - "ddl.dialog.mustConfirmDestructive": "Confirme a execução destrutiva para continuar.", - "ddl.dialog.executing": "Executando...", - "ddl.execute.result.summary": "Statements: {0} | Sucesso: {1} | Falhas: {2} | Tempo: {3:0}ms", - "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", - "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", - "ddl.execute.result.failed": "Falha ao executar DDL.", - "ddl.execute.result.cancelled": "Execução cancelada pelo usuário.", - "ddl.execute.result.cancelledDetails": "A execução DDL foi interrompida antes da conclusão.", - "context.deleteSingle": "Excluir {0}", - "context.deleteMultiple": "Excluir {0} nós", - "context.bringForward": "Trazer para frente (Ctrl+PgUp)", - "context.sendBackward": "Enviar para trás (Ctrl+PgDown)", - "context.bringToFront": "Trazer para o topo (Ctrl+Shift+PgUp)", - "context.sendToBack": "Enviar para o fundo (Ctrl+Shift+PgDown)", - "context.normalizeLayers": "Normalizar camadas", - "context.deleteWire": "Excluir conexão", - "context.addNode": "Adicionar nó (Shift+A)", - "context.undoWithDescription": "Desfazer {0}", - "context.redo": "Refazer", - "shortcuts.windowTitle": "Atalhos de Teclado", - "shortcuts.headerTitle": "DBWeaver - Atalhos", - "shortcuts.headerHint": "Dica: use CTRL+SHIFT+P para abrir a Command Palette e pesquisar comandos.", - "shortcuts.filterWatermark": "Filtrar atalhos por tecla ou ação...", - "shortcuts.resultCount": "{0} atalhos", - "shortcuts.resultFilter": "{0} resultado(s) para \"{1}\"", - "shortcuts.noneFound": "Nenhum atalho encontrado.", - "shortcuts.section.fileGeneral": "Arquivo e geral", - "shortcuts.section.editing": "Edição", - "shortcuts.section.canvasNavigation": "Canvas e navegação", - "shortcuts.section.zoomPanPrecision": "Zoom, pan e precisão", - "shortcuts.section.previewInspection": "Preview e inspeção", - "shortcuts.key.deleteOrBackspace": "Del ou Backspace", - "shortcuts.key.middleDrag": "Botão do meio + arrastar", - "shortcuts.key.rightDrag": "Botão direito + arrastar", - "shortcuts.key.spaceDrag": "Space + arrastar", - "shortcuts.key.altLeftDrag": "Alt + arrastar esquerdo", - "shortcuts.key.arrows": "Setas", - "shortcuts.key.shiftArrows": "Shift + Setas", - "shortcuts.action.openShortcutScreen": "Abrir esta tela de atalhos", - "shortcuts.action.newCanvas": "Novo canvas", - "shortcuts.action.openFile": "Abrir arquivo", - "shortcuts.action.save": "Salvar", - "shortcuts.action.saveAs": "Salvar como", - "shortcuts.action.commandPalette": "Command Palette", - "shortcuts.action.undo": "Desfazer", - "shortcuts.action.redo": "Refazer", - "shortcuts.action.selectAll": "Selecionar todos", - "shortcuts.action.deleteSelection": "Excluir seleção", - "shortcuts.action.closeOverlayCancel": "Fechar overlays / cancelar ações", - "shortcuts.action.openNodeSearch": "Abrir busca de nodes", - "shortcuts.action.resetViewport": "Reset de viewport", - "shortcuts.action.centerSelection": "Centralizar seleção", - "shortcuts.action.fitSelection": "Enquadrar seleção", - "shortcuts.action.autoLayout": "Auto Layout", - "shortcuts.action.toggleSnapToGrid": "Toggle Snap to Grid", - "shortcuts.action.bringForward": "Trazer para frente", - "shortcuts.action.sendBackward": "Enviar para trás", - "shortcuts.action.bringToFront": "Trazer para o topo", - "shortcuts.action.sendToBack": "Enviar para o fundo", - "shortcuts.action.zoomInOut": "Zoom in / out", - "shortcuts.action.pan": "Pan", - "shortcuts.action.temporaryPan": "Pan temporário", - "shortcuts.action.alternatePan": "Pan alternativo", - "shortcuts.action.fineNudge": "Nudge fino da seleção", - "shortcuts.action.fastNudge": "Nudge acelerado", - "shortcuts.action.togglePreview": "Toggle data preview", - "shortcuts.action.explainPlan": "Explain plan", - "shortcuts.action.runPreview": "Run preview", - "shortcuts.action.connectionManager": "Connection manager", - "shortcuts.action.flowVersionHistory": "Flow version history", - "shortcuts.resetAll": "Resetar tudo", - "shortcuts.customized": "Customizado", - "shortcuts.default": "Padrão", - "shortcuts.apply": "Aplicar", - "shortcuts.reset": "Resetar", - "shortcuts.status.resetAllSuccess": "Todos os atalhos foram resetados para o padrão.", - "shortcuts.status.updated": "Atalho atualizado.", - "shortcuts.status.reset": "Atalho resetado para o padrão.", - "shortcuts.status.updateFailed": "Não foi possível atualizar o atalho.", - "toast.severity.success": "Sucesso", - "toast.severity.warning": "Aviso", - "toast.severity.error": "Erro", - "toast.details.success": "Detalhes de Sucesso", - "toast.details.warning": "Detalhes de Aviso", - "toast.details.error": "Detalhes de Erro", - "diagnostics.area.cteEditor": "Editor de CTE", - "diagnostics.area.viewEditor": "Editor de View", - "diagnostics.area.subEditor": "Sub-editor", - "diagnostics.cteEditor.restoreParentFailed": "Falha ao restaurar o canvas pai. As edições do CTE foram descartadas.", - "diagnostics.recommendation.reloadFileIfNeeded": "Recarregue o arquivo se necessário.", - "diagnostics.viewEditor.exitFailed": "Não foi possível sair: {0}", - "diagnostics.viewEditor.canvasIncomplete": "o canvas está incompleto.", - "diagnostics.viewEditor.exitRecommendation": "Conecte um ResultOutput válido ou use o comando de descarte.", - "diagnostics.viewEditor.restoreParentFailed": "Falha ao restaurar o canvas pai. O subgrafo foi descartado.", - "diagnostics.subEditor.executeFailed": "Falha ao executar ação do editor: {0}", - "diagnostics.subEditor.executeRecommendation": "Tente novamente. Se persistir, recarregue o canvas.", - "diagnostics.canvasMigration.openWarning": "Abertura: {0}", - "diagnostics.canvasMigration.sessionRestoreWarning": "Restauração de sessão: {0}", - "diagnostics.canvasMigration.versionRestoreWarning": "Restauração de versão: {0}", - "diagnostics.recommendation.resaveLatestSchema": "Revise os diagnósticos e salve novamente o canvas para persistir o schema mais recente.", - "diagnostics.recommendation.saveMigratedSchema": "Revise os diagnósticos e salve o canvas para persistir o schema migrado.", - "file.saveDialog.title": "Salvar Canvas", - "file.saveDialog.suggestedName": "Query1", - "file.save.success": "Canvas salvo com sucesso.", - "file.save.failedWithReason": "Falha ao salvar: {0}", - "file.openDialog.title": "Abrir Canvas", - "file.openDialog.canvasType": "Canvas SQL Architect", - "file.open.failedWithReason": "Falha ao abrir: {0}", - "file.open.success": "Canvas aberto com sucesso.", - "file.open.successWithWarnings": "Canvas aberto com avisos.", - "session.restore.failedWithReason": "Falha ao restaurar: {0}", - "session.restore.successWithWarnings": "Sessão restaurada com avisos.", - "session.restore.success": "Sessão restaurada com sucesso.", - "export.documentation.dialogTitle": "Exportar Documentação do Fluxo", - "export.documentation.success": "Documentação exportada com sucesso.", - "export.documentation.failed": "Falha ao exportar documentação.", - "export.failed.pathPermissionsHint": "Verifique o caminho do arquivo e as permissões.", - "export.nodeNotFound": "Nenhum nó de Exportação {0} foi encontrado no canvas. Adicione um pelo menu de busca de nós.", - "export.dialogTitleByExtension": "Exportar como {0}", - "export.success": "Exportação concluída com sucesso.", - "export.failed": "Falha na exportação.", - "fileHistory.currentFile.none": "Nenhum arquivo selecionado", - "fileHistory.status.saveFirst": "Salve o canvas primeiro para habilitar o histórico local.", - "fileHistory.status.noneFound": "Nenhuma versão local encontrada ainda. Salve este arquivo para criar entradas no histórico.", - "fileHistory.status.countAvailable": "{0} versão(ões) local(is) disponível(is).", - "fileHistory.restore.failedWithReason": "Falha ao restaurar: {0}", - "fileHistory.restore.successFrom": "Versão restaurada de {0}.", - "preview.status.cancelled": "Cancelado", - "preview.status.error": "Erro", - "preview.status.ready": "Pronto", - "preview.runningWithMs": "Executando... {0}ms", - "preview.runningWithTimeout": "Executando... {0}ms (timeout: {1}s)", - "preview.runningSlowWithTimeout": "Executando... {0}ms (timeout: {1}s) · Consulta lenta, timeout em {2}s", - "explain.errorWithReason": "Erro no explain plan: {0}", - "explain.noSql": "Nenhum SQL para explicar. Monte uma consulta no canvas primeiro.", - "ddl.compilationFailed": "Falha na compilação", - "ddl.compileErrorWithReason": "Erro de compilação DDL: {0}", - "command.undo.name": "Desfazer", - "command.undo.description": "Desfazer última ação", - "command.redo.name": "Refazer", - "command.redo.description": "Refazer última ação desfeita", - "command.addNode.name": "Adicionar nó", - "command.addNode.description": "Abrir menu de busca para adicionar um nó", - "command.bringForward.name": "Trazer para frente", - "command.bringForward.description": "Mover nós selecionados uma camada para frente", - "command.sendBackward.name": "Enviar para trás", - "command.sendBackward.description": "Mover nós selecionados uma camada para trás", - "command.bringToFront.name": "Trazer para o topo", - "command.bringToFront.description": "Mover nós selecionados para a camada superior", - "command.sendToBack.name": "Enviar para o fundo", - "command.sendToBack.description": "Mover nós selecionados para a camada inferior", - "command.normalizeLayers.name": "Normalizar camadas", - "command.normalizeLayers.description": "Compactar índices de camada para uma ordem limpa de 0..N", - "tooltip.cleanupOrphans": "Remover nós órfãos não conectados ao output (Ctrl+Z para desfazer)", - "main.orphanSuffix": "Órfão(s)", - "tooltip.autoFixAliasNaming": "Corrigir aliases para snake_case (Ctrl+Z para desfazer)", - "main.namingPrefix": "Nomenclatura", - "fileHistory.compressedLabel": "Comprimido:", - "schema.itemsSuffix": "item(ns)", - "property.panel.title": "Propriedades", - "property.panel.multiSelected": "{0} nós selecionados", - "sqlImporter.status.pasteSelect": "Cole um SELECT acima e clique em Importar.", - "sqlImporter.status.inputTooLarge": "A entrada SQL é muito grande ({0:N0} caracteres). O limite é {1:N0}. Divida a consulta ou aumente o limite de importação.", - "sqlImporter.status.parsing": "Analisando SQL...", - "sqlImporter.status.done": "Concluído - {0} importado(s), {1} parcial(is), {2} ignorado(s).", - "sqlImporter.status.cancelledByUser": "Importação cancelada pelo usuário.", - "sqlImporter.status.timeout": "A importação excedeu o tempo após {0:0.#}s. Tente uma consulta menor ou aumente o timeout.", - "sqlImporter.status.parseError": "Erro de parsing: {0}", - "sqlImporter.status.clearConfirmationRequired": "A importação SQL vai limpar o canvas atual. Confirme para continuar.", - "sqlImporter.status.clearConfirmationCancelled": "Importação cancelada. O canvas atual foi mantido.", - "diagnostics.area.undoRedoTransaction": "Transação Desfazer/Refazer", - "undoRedo.rollbackExecuted": "Rollback executado para '{0}' ({1} operação(ões) revertida(s)).", - "undoRedo.rollbackRecommendation": "Revise o estado do canvas e tente novamente a ação, se necessário.", - "node.preview.noCatalog": "Catálogo indisponível", - "connection.error.searchMenuNotInitialized": "menu de busca não inicializado", - "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", - "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", - "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", - "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", - "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", - "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", - "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", - "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", - "diagnostics.area.connection": "Conexão", - "connection.warning.canvasMayContainOldTables": "O canvas ainda pode conter tabelas de uma conexão anterior.", - "connection.warning.canvasMayContainOldTablesRecommendation": "Limpe o canvas manualmente ou reconecte e escolha manter/limpar novamente.", - "undoRedo.transaction.unnamed": "transação sem nome", - "benchmark.runLabelDefault": "Execução 1", - "benchmark.runLabelPattern": "Execução {0}", - "benchmark.status.failedWithReason": "Falha no benchmark: {0}", - "benchmark.status.noSql": "Nenhum SQL para benchmark - monte uma consulta primeiro.", - "benchmark.status.warmupProgress": "Aquecimento {0}/{1}...", - "benchmark.status.iterationProgress": "Iteração {0}/{1}...", - "benchmark.status.done": "Concluído - {0}", - "benchmark.status.cancelled": "Benchmark cancelado.", - "app.windowTitle": "DBWeaver", - "preview.error.safePreviewBlocked": "Modo Preview Seguro: comandos que alteram dados (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) não podem ser executados no preview.", - "preview.error.noActiveConnection": "Nenhuma conexão de banco ativa. Conecte-se a um banco primeiro.", - "sqlImporter.error.selectFromNotFound": "Não foi possível encontrar SELECT ... FROM na consulta.", - "sqlImporter.error.fromClauseParseFailed": "Não foi possível interpretar a cláusula FROM.", - "sqlImporter.error.syntaxUnterminatedString": "Erro de sintaxe na linha {0}, coluna {1}: literal de string não terminada.", - "sqlImporter.error.missingClosingParenthesis": "faltando ')' de fechamento", - "sqlImporter.error.unexpectedClosingParenthesis": "')' inesperado", - "sqlImporter.error.syntaxAtLineColumn": "Erro de sintaxe na linha {0}, coluna {1}: {2}.", - "errorDiagnostics.safePreview.label": "Bloqueado pelo Modo Preview Seguro", - "errorDiagnostics.safePreview.friendly": "Este SQL contém um comando de mutação de dados e não pode ser executado no preview.", - "errorDiagnostics.safePreview.suggestion": "Remova ou substitua o comando de mutação (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) antes de executar o preview.", - "errorDiagnostics.connection.label": "Falha de conexão", - "errorDiagnostics.connection.friendly": "Não foi possível alcançar o servidor de banco. O host pode estar indisponível, inacessível ou bloqueando conexões.", - "errorDiagnostics.connection.suggestion": "Verifique endereço e porta do servidor, confirme que o banco está em execução e confira as regras de firewall.", - "errorDiagnostics.authorization.label": "Erro de autorização", - "errorDiagnostics.authorization.friendly": "As credenciais atuais não têm permissão para executar esta operação.", - "errorDiagnostics.authorization.suggestion": "Confirme que o usuário do banco tem privilégios de SELECT no schema/tabela de destino, ou contate o DBA.", - "errorDiagnostics.timeout.label": "Timeout de consulta", - "errorDiagnostics.timeout.friendly": "A consulta demorou demais para concluir e foi cancelada pelo servidor ou cliente.", - "errorDiagnostics.timeout.suggestion": "Adicione WHERE ou LIMIT para reduzir o volume de dados, ou aumente o timeout da consulta nas configurações de conexão.", - "errorDiagnostics.schema.label": "Erro de schema", - "errorDiagnostics.schema.friendly": "Uma tabela, coluna ou objeto referenciado não foi encontrado no banco.", - "errorDiagnostics.schema.suggestion": "Verifique nomes de tabela/coluna e confirme que o schema corresponde à conexão ativa.", - "errorDiagnostics.syntax.label": "Erro de sintaxe SQL", - "errorDiagnostics.syntax.friendly": "A consulta contém erro de sintaxe e não pôde ser interpretada pelo mecanismo do banco.", - "errorDiagnostics.syntax.suggestion": "Revise o SQL destacado procurando typos, parênteses desalinhados ou cláusulas não suportadas pelo provider ativo.", - "errorDiagnostics.compatibility.label": "Erro de compatibilidade", - "errorDiagnostics.compatibility.friendly": "Uma função, operador ou construção de sintaxe não é suportada pelo provider de banco ativo.", - "errorDiagnostics.compatibility.suggestion": "Troque para o provider correto na barra SQL, ou substitua a construção não suportada por equivalente.", - "errorDiagnostics.unknown.label": "Erro inesperado", - "errorDiagnostics.unknown.friendly": "Ocorreu um erro ao executar a consulta de preview.", - "errorDiagnostics.unknown.suggestion": "Verifique os detalhes técnicos abaixo e confirme que o canvas está configurado corretamente.", - "error.mainWindow.invalidDataContext": "O DataContext da MainWindow deve ser um ShellViewModel.", - "error.mainWindow.canvasNotInitialized": "CanvasViewModel não foi inicializado.", - "error.mainWindow.ddlPreviewUnavailable": "Preview DDL indisponível para o canvas atual.", - "themeJson.editor.template": "{\n \"meta\": { \"name\": \"Tema Personalizado\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", - "themeJson.error.pasteBeforeApply": "Cole um JSON de tema antes de aplicar.", - "themeJson.error.invalidJson": "JSON inválido: {0}", - "themeJson.error.emptyPayload": "JSON inválido: payload vazio.", - "themeJson.error.invalidTheme": "Tema inválido: {0}", - "themeJson.error.appliedButSaveFailed": "Tema aplicado, mas falhou ao salvar: {0}", - "themeJson.success.appliedAndSaved": "Tema JSON aplicado e salvo.", - "themeJson.success.customRemoved": "Tema personalizado removido. Reinicie o app para voltar totalmente ao tema padrão.", - "themeJson.error.restoreDefaultFailed": "Falha ao restaurar tema padrão: {0}", - "themeValidator.error.configNull": "A configuração de tema está nula.", - "themeValidator.warning.noSections": "O tema não possui seções de cores ou tipografia; nada para aplicar.", - "themeValidator.warning.invalidColor": "{0} possui cor inválida '{1}'. Esta chave será ignorada.", - "themeValidator.warning.sizeOutOfRange": "{0}={1} está fora do intervalo (8..48). Esta chave será ignorada.", - "queryExecutor.error.openConnectionMethodNotFound": "Não foi possível encontrar o método OpenConnectionAsync no orquestrador", - "queryExecutor.error.openConnectionInvokeFailed": "Falha ao invocar OpenConnectionAsync", - "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}': o SELECT da view não pode ser reconstruído visualmente - edite manualmente no subcanvas.", - "ddlImporter.error.tableNotFoundInMetadata": "A tabela '{0}' não foi encontrada nos metadados atuais.", - "main.window.untitled": "Sem título", - "main.subEditor.noSeedProvided": "Nenhum seed de subeditor foi fornecido para {0}.", - "main.layerOrder.bringToFront": "Trazer para frente", - "main.layerOrder.sendToBack": "Enviar para trás", - "main.layerOrder.bringForward": "Avançar camada", - "main.layerOrder.sendBackward": "Recuar camada", - "main.layerOrder.normalizeLayers": "Normalizar camadas", - "export.fileType.html": "Arquivos HTML", - "export.fileType.json": "Arquivos JSON", - "export.fileType.csv": "Arquivos CSV", - "export.fileType.excel": "Arquivos Excel", - "commandPalette.templatePrefix": "Template: {0}", - "themeLoader.status.notFoundWithPath": "Arquivo de tema não encontrado: {0}", - "themeLoader.status.deserializedNull": "O JSON de tema foi desserializado para null.", - "themeLoader.status.loaded": "JSON de tema carregado com sucesso.", - "credential.error.ciphertextTooShort": "O blob de texto cifrado está muito curto.", - "credential.error.dpapiWindowsOnly": "DPAPI está disponível apenas no Windows.", - "credential.warning.loadVaultFailed": "Falha ao carregar o cofre de credenciais '{0}': {1}", - "credential.warning.persistVaultFailed": "Falha ao persistir o cofre de credenciais '{0}': {1}", - "snippetStore.warning.loadFailed": "Falha ao carregar snippets de '{0}': {1}", - "snippetStore.warning.saveFailed": "Falha ao salvar snippets: {0}", - "flowVersionStore.warning.loadFailed": "Falha ao carregar versões de fluxo de '{0}': {1}", - "flowVersionStore.warning.saveFailed": "Falha ao salvar versões de fluxo: {0}", - "queryExecutor.error.queryEmpty": "A consulta não pode estar vazia", - "queryExecutor.error.providerNotSupported": "O provider {0} não é suportado", - "queryExecutor.error.singleStatementOnly": "O preview aceita apenas uma única instrução SQL.", - "queryExecutor.error.queryEmptyWithPeriod": "A consulta não pode estar vazia.", - "queryExecutor.error.readOnlyOnly": "O modo preview suporta apenas instruções SQL somente leitura.", - "queryExecutor.error.namedParametersNotSupported": "O modo preview não suporta parâmetros nomeados na execução SQL. Use literais seguros inline ou execute a consulta fora do preview.", - "queryExecutor.error.positionalParametersNotSupported": "O modo preview não suporta placeholders posicionais ('?' ou '$1').", - "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "Align selected nodes to the bottom edge", - "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "Align selected nodes to the leftmost edge", - "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "Align selected nodes to the rightmost edge", - "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "Align selected nodes to the topmost edge", - "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "Apply CTE sub-canvas edits and return to the parent canvas", - "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "Arrange nodes into logical columns automatically", - "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "Centre selected nodes on a horizontal axis", - "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "Centre selected nodes on a vertical axis", - "commandPalette.description.clear_canvas_and_start_fresh": "Clear canvas and start fresh", - "commandPalette.description.clear_node_selection": "Clear node selection", - "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "Convert aliases to the convention configured in project settings", - "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "Create checkpoints, compare versions side-by-side and restore a previous canvas state", - "commandPalette.description.delete_the_selected_nodes": "Delete the selected nodes", - "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "Discard current sub-editor edits and return to the parent canvas", - "commandPalette.description.execute_the_current_query_in_preview": "Execute the current query in preview", - "commandPalette.description.fit_all_nodes_into_the_visible_area": "Fit all nodes into the visible area", - "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "Generate CSV file from the first CSV Export node", - "commandPalette.description.generate_html_file_from_the_first_html_export_node": "Generate HTML file from the first HTML Export node", - "commandPalette.description.generate_json_file_from_the_first_json_export_node": "Generate JSON file from the first JSON Export node", - "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "Generate XLSX workbook from the first Excel Export node", - "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "Inspect the query execution plan — see scan types, join strategies, and cost estimates", - "commandPalette.description.load_a_vsaq_canvas_file": "Load a .vsaq canvas file", - "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "Measure avg / median / p95 latency of the current SQL over N iterations", - "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "Open isolated sub-canvas editor for the selected CTE Definition node", - "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "Open local file version history created on each save and restore previous saved snapshots", - "commandPalette.description.open_output_preview_modal_for_the_active_mode": "Open output preview modal for the active mode", - "commandPalette.description.open_shortcut_reference_screen": "Open shortcut reference screen", - "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "Open the connection manager to add, edit or switch database connections", - "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "Paste a SELECT statement and generate nodes automatically — FROM, JOIN, WHERE, LIMIT are supported", - "commandPalette.description.remove_all_nodes_not_connected_to_output": "Remove all nodes not connected to output", - "commandPalette.description.reset_zoom_and_pan_to_default": "Reset zoom and pan to default", - "commandPalette.description.save_canvas_to_a_new_file": "Save canvas to a new file", - "commandPalette.description.save_current_canvas": "Save current canvas", - "commandPalette.description.save_markdown_documentation_of_the_current_flow": "Save Markdown documentation of the current flow", - "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "Save the selected nodes as a reusable snippet — insert it later via the node search menu (⇧A)", - "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "Scan all table-source nodes on the canvas for possible join relationships based on FK conventions and naming patterns", - "commandPalette.description.select_all_nodes_on_canvas": "Select all nodes on canvas", - "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "Snap node positions to 16px grid (Ctrl+G)", - "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "Spread selected nodes with equal horizontal spacing", - "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "Spread selected nodes with equal vertical spacing", - "commandPalette.description.zoom_into_the_canvas": "Zoom into the canvas", - "commandPalette.description.zoom_out_of_the_canvas": "Zoom out of the canvas", - "commandPalette.name.align_bottom": "Align Bottom", - "commandPalette.name.align_left": "Align Left", - "commandPalette.name.align_right": "Align Right", - "commandPalette.name.align_top": "Align Top", - "commandPalette.name.analyze_all_joins": "Analyze All Joins", - "commandPalette.name.auto_fix_naming": "Auto-Fix Naming", - "commandPalette.name.auto_layout": "Auto Layout", - "commandPalette.name.center_horizontally": "Center Horizontally", - "commandPalette.name.center_vertically": "Center Vertically", - "commandPalette.name.cleanup_orphans": "Cleanup Orphans", - "commandPalette.name.delete_selected": "Delete Selected", - "commandPalette.name.deselect_all": "Deselect All", - "commandPalette.name.discard_and_exit_editor": "Discard and Exit Editor", - "commandPalette.name.distribute_horizontally": "Distribute Horizontally", - "commandPalette.name.distribute_vertically": "Distribute Vertically", - "commandPalette.name.edit_selected_cte": "Edit Selected CTE", - "commandPalette.name.exit_cte_editor": "Exit CTE Editor", - "commandPalette.name.explain_plan": "Explain Plan", - "commandPalette.name.export_csv": "Export CSV", - "commandPalette.name.export_documentation": "Export Documentation", - "commandPalette.name.export_excel": "Export Excel", - "commandPalette.name.export_html": "Export HTML", - "commandPalette.name.export_json": "Export JSON", - "commandPalette.name.file_save_load_history": "File Save/Load History", - "commandPalette.name.fit_to_screen": "Fit to Screen", - "commandPalette.name.flow_version_history": "Flow Version History", - "commandPalette.name.import_sql_to_graph": "Import SQL to Graph", - "commandPalette.name.keyboard_shortcuts": "Keyboard Shortcuts", - "commandPalette.name.manage_connections": "Manage Connections", - "commandPalette.name.new_canvas": "New Canvas", - "commandPalette.name.open_file": "Open File", - "commandPalette.name.reset_viewport": "Reset Viewport", - "commandPalette.name.run_preview": "Run Preview", - "commandPalette.name.run_query_benchmark": "Run Query Benchmark", - "commandPalette.name.save": "Save", - "commandPalette.name.save_as": "Save As", - "commandPalette.name.save_selection_as_snippet": "Save Selection as Snippet", - "commandPalette.name.select_all": "Select All", - "commandPalette.name.toggle_preview": "Toggle Preview", - "commandPalette.name.toggle_snap_to_grid": "Toggle Snap to Grid", - "commandPalette.name.zoom_in": "Zoom In", - "commandPalette.name.zoom_out": "Zoom Out", - "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", - "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", - "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", - "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", - "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", - "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", - "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", - "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", - "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", - "commandPalette.tags.clear_selection": "clear selection", - "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", - "commandPalette.tags.create_insert_search_transform": "create insert search transform", - "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", - "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", - "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", - "commandPalette.tags.data_results_table_panel": "data results table panel", - "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", - "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", - "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", - "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", - "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", - "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", - "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", - "commandPalette.tags.export_json_file_output_save": "export json file output save", - "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", - "commandPalette.tags.export_persist_copy": "export persist copy", - "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", - "commandPalette.tags.forward_history": "forward history", - "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", - "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", - "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", - "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", - "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", - "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", - "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", - "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", - "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", - "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", - "commandPalette.tags.load_import_vsaq": "load import vsaq", - "commandPalette.tags.magnify_enlarge": "magnify enlarge", - "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", - "commandPalette.tags.persist_write_disk": "persist write disk", - "commandPalette.tags.remove_erase_nodes": "remove erase nodes", - "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", - "commandPalette.tags.reset_clear_blank": "reset clear blank", - "commandPalette.tags.revert_back_history": "revert back history", - "commandPalette.tags.shrink_reduce": "shrink reduce", - "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", - "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", - "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", - "sqlEditor.diffPreview.title": "Previa de diff transacional", - "sqlEditor.mutation.confirmExecute": "Confirmar execucao", - "sqlEditor.tab.closeAnyway": "Fechar mesmo assim", - "sqlEditor.tab.keepTab": "Manter aba", - "sqlEditor.status.ready": "Pronto.", - "sqlEditor.telemetry.none": "Sem telemetria de execucao ainda.", - "sqlEditor.telemetry.summary": "Instrucoes: {0} Sucesso: {1} Falhas: {2} Total: {3} ms", - "sqlEditor.telemetry.errors.none": "Sem erros agregados.", - "sqlEditor.diff.none": "Sem diff transacional disponivel.", - "sqlEditor.mutation.estimate.none": "Sem estimativa de mutacao disponivel.", - "sqlEditor.mutation.estimate.value": "Linhas afetadas estimadas: {0}", - "sqlEditor.mutation.estimate.unavailable": "Nao foi possivel estimar as linhas afetadas automaticamente.", - "sqlEditor.tab.closePending": "Alteracoes nao salvas detectadas. Confirme o fechamento da aba.", - "sqlEditor.tab.noPendingClose": "Nao ha fechamento de aba pendente.", - "sqlEditor.tab.manyWarning": "Quantidade alta de abas: {0} abas abertas.", - "sqlEditor.mutation.pending.none": "Sem confirmacao de mutacao pendente.", - "sqlEditor.mutation.pending.required": "A mutacao exige confirmacao antes da execucao.", - "sqlEditor.message.empty": "Execute uma instrucao para ver mensagens.", - "sqlEditor.message.success": "Execucao concluida com sucesso.", - "sqlEditor.result.summary.empty": "Linhas: - Tempo: -", - "sqlEditor.result.summary": "Linhas: {0} Tempo: {1} ms", - "sqlEditor.file.save.canceled": "Salvamento cancelado.", - "sqlEditor.file.save.noPath": "Nenhum caminho de destino selecionado.", - "sqlEditor.file.save.success": "Arquivo SQL salvo.", - "sqlEditor.file.save.failed": "Falha ao salvar arquivo SQL.", - "sqlEditor.file.open.failed": "Falha ao abrir arquivo SQL.", - "sqlEditor.file.open.notFound": "O arquivo SQL selecionado nao foi encontrado.", - "sqlEditor.file.open.success": "Arquivo SQL aberto.", - "sqlEditor.status.executing": "Executando SQL...", - "sqlEditor.status.executingScript": "Executando script SQL...", - "sqlEditor.status.executingStep": "Executando {0}/{1}...", - "sqlEditor.status.canceling": "Cancelando execucao...", - "sqlEditor.status.executingConfirmedMutation": "Executando mutacao confirmada...", - "sqlEditor.status.mutationCanceled": "Execucao da mutacao cancelada.", - "sqlEditor.detail.statementNotExecuted": "A instrucao nao foi executada.", - "sqlEditor.status.success": "Execucao concluida com sucesso.", - "sqlEditor.detail.rowsAndTime": "{0} linha(s) em {1} ms.", - "sqlEditor.status.canceled": "Execucao cancelada.", - "sqlEditor.status.failed": "Falha na execucao.", - "sqlEditor.status.confirmationRequired": "Confirmacao necessaria antes da execucao.", - "sqlEditor.error.mutationConfirmationRequired": "Confirmacao de mutacao necessaria.", - "sqlEditor.result.tabTitle": "Resultado {0}", - "sqlEditor.tab.closeRequiresConfirmation": "O fechamento da aba exige confirmacao.", - "sqlEditor.tab.unsavedDetail": "Esta aba possui alteracoes nao salvas.", - "sqlEditor.tab.closed": "Aba fechada.", - "sqlEditor.tab.closeCanceled": "Fechamento da aba cancelado.", - "sqlEditor.tab.closeCanceledDetail": "Aba com alteracoes nao salvas mantida aberta.", - "sqlEditor.error.noStatementSelected": "Nenhuma instrucao SQL selecionada para execucao.", - "sqlEditor.error.noConnection": "Nenhuma conexao ativa para execucao SQL.", - "sqlEditor.error.executionCanceled": "A execucao SQL foi cancelada.", - "sqlEditor.tab.scriptTitle": "Script {0}", - "sqlEditor.guard.delete.noWhere.message": "DELETE sem WHERE pode remover todas as linhas.", - "sqlEditor.guard.delete.noWhere.recommendation": "Adicione uma clausula WHERE restritiva antes de executar.", - "sqlEditor.guard.delete.trivialWhere.message": "DELETE possui uma clausula WHERE trivialmente verdadeira.", - "sqlEditor.guard.delete.trivialWhere.recommendation": "Use um filtro seletivo para atingir apenas as linhas desejadas.", - "sqlEditor.guard.update.noWhere.message": "UPDATE sem WHERE pode afetar todas as linhas.", - "sqlEditor.guard.update.noWhere.recommendation": "Adicione uma clausula WHERE restritiva antes de executar.", - "sqlEditor.guard.update.trivialWhere.message": "UPDATE possui uma clausula WHERE trivialmente verdadeira.", - "sqlEditor.guard.update.trivialWhere.recommendation": "Use um filtro seletivo para atingir apenas as linhas desejadas.", - "sqlEditor.guard.insert.noColumnList.message": "INSERT sem lista explicita de colunas e fragil diante de mudancas de schema.", - "sqlEditor.guard.insert.noColumnList.recommendation": "Prefira INSERT INTO tabela(col1, col2, ...) VALUES (...).", - "sqlEditor.guard.ddl.message": "Uma instrucao DDL pode causar mudancas estruturais no banco de dados.", - "sqlEditor.guard.ddl.recommendation": "Confirme a execucao apenas quando as mudancas de schema forem intencionais.", - "sqlEditor.diff.unavailable.noPreview": "Sem previa de diff transacional disponivel para esta instrucao.", - "sqlEditor.diff.unavailable.parseError": "Nao foi possivel identificar o alvo da mutacao para gerar a previa de diff transacional.", - "sqlEditor.diff.unavailable.connection": "Previa de diff transacional indisponivel por limitacoes de conexao ou consulta.", - "sqlEditor.diff.deleteSummary": "Previa de diff transacional (ROLLBACK garantido): tabela {0}, total de linhas antes {1}, afetadas {2}, total de linhas depois {3}.", - "sqlEditor.diff.updateSummary": "Previa de diff transacional (ROLLBACK garantido): tabela {0}, total de linhas antes {1}, linhas candidatas afetadas {2}, total de linhas depois {3}.", - "sqlEditor.diff.unavailable.unsupportedStatement": "A previa de diff transacional atualmente suporta apenas UPDATE e DELETE.", - "sqlEditor.results.title": "Resultados", - "sqlEditor.results.filterWatermark": "Filtrar resultados", - "sqlEditor.results.rows.countZero": "0 linhas", - "sqlEditor.results.rows.countSingle": "{0} linhas", - "sqlEditor.results.rows.countFiltered": "{0} de {1} linhas", - "sqlEditor.results.context.copyCell": "Copiar célula", - "sqlEditor.results.context.copyRow": "Copiar linha", - "sqlEditor.results.context.hideColumn": "Ocultar coluna", - "sqlEditor.results.context.showAllColumns": "Mostrar todas as colunas", - "sqlEditor.sidebar.messages": "MENSAGENS", - "sqlEditor.sidebar.history": "HISTÓRICO", - "sqlEditor.sidebar.connection.none": "Sem conexão", - "sqlEditor.sidebar.connection.connectHint": "Conecte-se para carregar metadados e executar consultas.", - "sqlEditor.sidebar.connection.connect": "Conectar", - "sqlEditor.sidebar.schema.searchWatermark": "Buscar tabela ou coluna", - "sqlEditor.sidebar.schema.reloadTooltip": "Recarregar metadados", - "sqlEditor.sidebar.schema.connectHint": "Conecte-se para visualizar o schema completo.", - "sqlEditor.history.searchWatermark": "Buscar no histórico", - "sqlEditor.history.navigationHint": "Setas percorrem o histórico, Enter executa selecionado e Esc limpa a busca.", - "sqlEditor.history.noSearchResults": "Nenhum item encontrado para a busca.", - "sqlEditor.history.use": "Usar", - "sqlEditor.history.copy": "Copiar", - "sqlEditor.history.run": "Executar", - "sqlEditor.history.copiedFromHistory": "SQL copiado do histórico.", - "sqlEditor.toast.scriptSuccessTitle": "Script executado.", - "sqlEditor.toast.scriptSuccessDetail": "{0} instrução(ões) executada(s) com sucesso.", - "sqlEditor.toast.scriptWarningTitle": "Script executado com falhas.", - "sqlEditor.toast.scriptWarningDetail": "{0} de {1} instrução(ões) falharam.", - "sqlEditor.toast.resultErrorTitle": "Falha ao executar instrução.", - "sqlEditor.toast.resultSuccessTitle": "Execução concluída com sucesso.", - "sqlEditor.export.action": "Exportar relatório", - "sqlEditor.openSql.pickerTitle": "Abrir Arquivo SQL", - "sqlEditor.saveSql.fileType": "Arquivos SQL", - "sqlEditor.saveSql.pickerTitle": "Salvar Arquivo SQL", - "sqlEditor.export.pickerTitle": "Exportar Dados SQL", - "sqlEditor.export.status.noResultTitle": "Nenhum resultado de execucao disponivel para exportacao.", - "sqlEditor.export.status.noResultDetail": "Execute uma consulta primeiro.", - "sqlEditor.export.status.successTitle": "Relatorio exportado.", - "sqlEditor.export.status.failedTitle": "Falha ao exportar relatorio.", - "sqlEditor.export.fileType.html": "Arquivo HTML", - "sqlEditor.export.fileType.json": "Arquivo JSON", - "sqlEditor.export.fileType.csv": "Arquivo CSV", - "sqlEditor.export.fileType.xlsx": "Pasta de Trabalho Excel", - "sqlEditor.export.defaultFileBase": "relatorio", - "sqlEditor.export.defaultTitle": "Relatorio SQL", - "sqlEditor.export.error.typeRequired": "Um tipo de relatorio deve ser selecionado antes da exportacao.", - "sqlEditor.export.type.html.title": "Relatorio HTML completo", - "sqlEditor.export.type.html.description": "Artefato HTML autonomo e orientado a SQL para auditoria offline.", - "sqlEditor.export.type.json.title": "Contrato de execucao JSON", - "sqlEditor.export.type.json.description": "Payload legivel por maquina com SQL, metadados e resultado da execucao.", - "sqlEditor.export.type.csv.title": "Exportacao de dados CSV", - "sqlEditor.export.type.csv.description": "Apenas dados tabulares do resultado, ideal para planilhas.", - "sqlEditor.export.type.xlsx.title": "Exportacao de pasta Excel", - "sqlEditor.export.type.xlsx.description": "Pasta de trabalho com os dados tabulares do resultado.", - "sqlEditor.export.dialog.windowTitle": "Exportar Dados SQL", - "sqlEditor.export.dialog.title": "Exportar Dados SQL", - "sqlEditor.export.dialog.subtitle": "Escolha o formato e os metadados do artefato antes de exportar.", - "sqlEditor.export.dialog.confirm": "Exportar", - "sqlEditor.export.dialog.fileNameWatermark": "relatorio.html", - "sqlEditor.export.dialog.titleWatermark": "Relatorio SQL", - "sqlEditor.export.dialog.descriptionWatermark": "Contexto adicional para auditoria e compartilhamento.", - "sqlEditor.export.dialog.section.reportType": "TIPO DE RELATORIO", - "sqlEditor.export.dialog.section.fileName": "NOME DO ARQUIVO", - "sqlEditor.export.dialog.section.reportTitle": "TITULO", - "sqlEditor.export.dialog.section.description": "DESCRICAO", - "sqlEditor.export.dialog.section.options": "OPCOES", - "sqlEditor.export.option.includeSchema": "Incluir schema de saida", - "sqlEditor.export.option.includeNodeDetails": "Incluir placeholders de no/conexao no JSON", - "sqlEditor.export.option.includeMetadata": "Incluir metadados opcionais", - "sqlEditor.export.option.useDashForEmpty": "Usar '-' para campos vazios", - "sqlEditor.export.badge.offline": "PRONTO PARA OFFLINE", - "sqlEditor.export.badge.structured": "PAYLOAD ESTRUTURADO", - "sqlEditor.export.badge.dataOnly": "SOMENTE DADOS" -} +{ + "main.brand": "AkkornStudio", + "main.tab.query1": "Consulta 1", + "main.new": "Novo", + "main.open": "Abrir", + "main.save": "Salvar", + "main.history": "Histórico", + "main.layout": "Layout", + "main.preview": "Prévia", + "main.undo": "Desfazer", + "main.redo": "Refazer", + "main.cleanupOrphans": "Limpar nós órfãos", + "main.autoFixAliasNaming": "Autoajustar nomenclatura de aliases", + "main.autoLayoutCanvas": "Layout automático do canvas", + "main.toggleDataPreview": "Alternar prévia de dados", + "main.language": "Idioma", + "main.restore.prompt": "Sessão anterior encontrada — deseja restaurar o último canvas?", + "main.restore.button": "Restaurar sessão", + "main.cteEditor.editingPrefix": "Editando CTE: ", + "main.cteEditor.backToCanvas": "Voltar ao Canvas", + "main.cteEditor.exitA11y": "Sair do editor de CTE", + "main.viewEditor.editingPrefix": "DDL > View: ", + "main.viewEditor.backToCanvas": "Voltar ao DDL", + "main.viewEditor.exitA11y": "Sair do editor de view", + "connection.title": "Gerenciador de Conexões", + "connection.subtitle": "Configure, teste e ative conexões sem sair do fluxo", + "connection.none": "Sem conexão", + "connection.active": "ATIVA", + "connection.health.online": "Online", + "connection.health.degraded": "Degradada", + "connection.health.offline": "Offline", + "connection.tooltip.none": "Nenhuma conexão ativa — clique para gerenciar", + "connection.ping": "Ping", + "connection.saved": "CONEXÕES SALVAS", + "connection.new": "Nova Conexão", + "connection.selectOrCreate": "Selecione uma conexão ou crie uma nova", + "connection.name": "Nome da Conexão", + "connection.provider": "Provedor", + "connection.host": "Host", + "connection.port": "Porta", + "connection.database": "Banco de Dados", + "connection.sqlitePath": "Caminho SQLite", + "connection.sqliteBrowse": "Procurar", + "connection.sqliteCreate": "Criar", + "connection.username": "Usuário", + "connection.password": "Senha", + "connection.timeout": "Timeout (segundos)", + "connection.test": "Testar", + "connection.save": "Salvar", + "connection.connect": "Conectar", + "connection.action.testConnection": "Testar conexão", + "connection.action.saveConnection": "Salvar conexão", + "connection.action.connectConnection": "Conectar conexão", + "connection.status.connecting": "Conectando...", + "connection.status.connected": "Conectado", + "connection.status.testing": "Testando...", + "connection.status.failedPrefix": "Falha na conexão", + "connection.status.metadataUnavailable": "Falha na conexão: metadados indisponíveis.", + "connection.status.highLatency": "latência alta", + "connection.watermark.name": "Meu Banco de Produção", + "connection.watermark.host": "localhost", + "connection.watermark.port": "5432", + "connection.watermark.database": "nome_do_banco", + "connection.watermark.username": "usuario", + "connection.watermark.password": "••••••••", + "connection.watermark.timeout": "30", + "main.connectingDb": "Conectando ao banco de dados...", + "main.emptyHint": "Pressione ⇧A para adicionar seu primeiro nó, ou arraste uma tabela da barra lateral", + "status.nodesSeparator": " nós · ", + "status.connectionsSuffix": " conexões", + "status.undo": "Desfazer: ", + "status.shortcuts": "⇧A Nós · F3 Prévia · Ctrl+Z Desfazer · Del Remover · Alt+Arrastar Pan", + "connection.disconnect": "Desconectar", + "connection.action.disconnectConnection": "Desconectar conexão", + "connectionTab.active": "CONEXÃO ATIVA", + "connectionTab.none": "Nenhuma conexão ativa", + "connectionTab.saved": "CONEXÕES SALVAS", + "connectionTab.new": "+ Nova Conexão", + "schema.database": "BANCO DE DADOS", + "schema.search": "Buscar tabelas, colunas...", + "schema.loading": "Buscando tabelas, colunas...", + "schema.noConnection": "Sem conexão", + "schema.noConnectionHint": "Conecte-se a um banco para ver tabelas, colunas e relacionamentos", + "schema.emptyNoTables": "Nenhuma tabela encontrada", + "fileHistory.title": "Histórico de Versões (Salvar/Carregar)", + "fileHistory.reload": "Recarregar", + "fileHistory.restoreSelected": "Restaurar selecionada", + "fileHistory.empty": "Nenhuma versão local ainda", + "fileHistory.emptyHint": "Salve este arquivo para gerar histórico de versões.", + "preview.title": "Prévia de Dados", + "preview.subtitle": "Revise dados e diagnósticos antes de continuar", + "preview.run": "Executar", + "preview.cancel": "Cancelar", + "preview.tab.preview": "Prévia", + "preview.tab.sql": "SQL", + "preview.close": "Fechar prévia", + "preview.running": "Executando consulta de prévia… ", + "preview.clickCancel": "Clique em Cancelar para interromper", + "preview.cancelled": "Consulta cancelada", + "preview.runAgain": "Pressione Executar para tentar novamente", + "preview.failed": "Falha na execução da prévia", + "preview.technical": "DETALHES TÉCNICOS", + "preview.noData": "Sem dados ainda", + "preview.f3Hint": "Pressione F3 ou Espaço para executar a consulta atual", + "sqlImporter.title": "Importar SQL para Grafo", + "sqlImporter.subtitle": "Cole uma instrução SELECT — os nós são criados automaticamente", + "sqlImporter.sqlStatement": "INSTRUÇÃO SQL", + "sqlImporter.supported": "Suportado: ", + "sqlImporter.import": "Importar", + "sqlImporter.report": "RELATÓRIO DE CONVERSÃO", + "sqlEditor.mutation.dialogTitle": "Confirmacao de mutacao", + "sqlEditor.mutation.dialogSubtitle": "Revise o impacto antes de confirmar a execucao", + "search.empty": "Nenhum nó encontrado", + "search.emptyHint": "Tente buscar por UPPER, JSON, CAST, AND…", + "search.shortcut": "⇧A", + "search.spawn": "Criar", + "commandPalette.empty": "Nenhum comando corresponde à sua busca", + "commandPalette.search": "Busca de comandos", + "commandPalette.shortcut": "CTRL+SHIFT+P", + "commandPalette.execute": "Executar", + "context.editCte": "Editar CTE Selecionada", + "context.editViewSubcanvas": "Editar Subcanvas da View", + "explain.title": "Plano de Execução", + "explain.sql": "SQL", + "explain.option.analyze": "Analisar", + "explain.option.buffers": "Buffers", + "explain.badge.simulated": "SIMULADO", + "explain.timing.planning": "Planejamento:", + "explain.timing.execution": "Execucao:", + "explain.section.snapshotComparison": "Comparacao de snapshots", + "explain.section.indexRecommendations": "Recomendacoes de indice", + "explain.section.history": "Historico", + "explain.detail.estimated": "Estimadas", + "explain.detail.actual": "Atuais", + "explain.detail.error": "Erro", + "explain.detail.time": "Tempo", + "explain.detail.loops": "Loops", + "explain.rerun": "Reexecutar", + "explain.alertSuffix": " operação(ões) cara(s) detectada(s) — considere adicionar índices", + "explain.running": "Executando EXPLAIN…", + "explain.failed": "⚠ EXPLAIN falhou", + "explain.noPlan": "Nenhum plano ainda", + "explain.rerunHint": "Pressione Reexecutar para executar EXPLAIN", + "explain.header.operation": "OPERAÇÃO", + "explain.header.cost": "CUSTO", + "explain.header.rows": "LINHAS", + "explain.header.err": "ERR", + "explain.header.alert": "ALERTA", + "explain.snapshot.labelA": "A", + "explain.snapshot.labelB": "B", + "explain.mode.list": "Lista", + "explain.mode.tree": "Arvore", + "explain.action.snapshot": "Salvar snapshot", + "explain.action.copyJson": "Copiar JSON", + "explain.action.copyText": "Copiar texto", + "explain.action.saveJson": "Salvar .json", + "explain.action.openDalibo": "Abrir no Dalibo", + "explain.legend.seqscan": "SEQ SCAN — leitura completa da tabela, sem índice", + "explain.legend.sort": "SORT — ordenação em memória", + "explain.legend.hash": "HASH — junção por hash", + "explain.escClose": "Esc para fechar", + "flowVersion.title": "Histórico de Versões do Fluxo", + "flowVersion.subtitle": "Crie checkpoints, compare versões e restaure", + "flowVersion.watermark": "Rótulo do checkpoint (opcional)…", + "flowVersion.saveCheckpoint": "Salvar Checkpoint", + "flowVersion.compareMode": "Modo de Comparação", + "flowVersion.selectBase": "Selecione a versão BASE (de):", + "flowVersion.clickAny": "Depois clique em qualquer versão da lista abaixo para comparar.", + "flowVersion.noCheckpoints": "Nenhum checkpoint ainda", + "flowVersion.noCheckpointsHint": "Salve um checkpoint acima para começar a rastrear versões.", + "flowVersion.restore": "Restaurar", + "flowVersion.diffResults": "Resultados da Diferença", + "benchmark.title": "Benchmark de Consulta", + "benchmark.subtitle": "Mede latência média / mediana / p95 em N iterações", + "benchmark.sql": "SQL sendo avaliado", + "benchmark.runLabel": "Rótulo da execução", + "benchmark.runLabelWatermark": "Execução 1", + "benchmark.iterations": "Iterações (1–100)", + "benchmark.warmup": "Passes de aquecimento (0–10)", + "benchmark.interval": "Intervalo entre execuções (ms)", + "benchmark.run": "Executar Benchmark", + "benchmark.cancel": "Cancelar", + "benchmark.clearHistory": "Limpar Histórico", + "benchmark.latest": "ÚLTIMO RESULTADO", + "benchmark.avg": "MÉDIA", + "benchmark.median": "MEDIANA", + "benchmark.min": "MÍN", + "benchmark.max": "MÁX", + "benchmark.iterationsAt": " iterações · executado às ", + "benchmark.history": "HISTÓRICO", + "benchmark.header.label": "Rótulo", + "benchmark.header.avg": "Média", + "benchmark.header.median": "Mediana", + "benchmark.header.min": "Mín", + "benchmark.header.max": "Máx", + "benchmark.itersSuffix": " iterações", + "diagnostics.title": "Diagnóstico do Aplicativo", + "diagnostics.run": "Executar", + "diagnostics.running": "Executando verificações…", + "diagnostics.ok": "OK", + "diagnostics.warning": "Aviso", + "diagnostics.error": "Erro", + "diagnostics.tooltip.rerun": "Executar novamente todas as verificações", + "diagnostics.tooltip.copy": "Copiar relatório de diagnóstico para a área de transferência", + "diagnostics.tooltip.close": "Fechar (Esc)", + "autoJoin.title": "Sugestões de Auto-Join", + "autoJoin.titleForTable": "Sugestões de Auto-Join para {0}", + "autoJoin.acceptAll": "Aceitar Todas", + "autoJoin.accept": "Aceitar", + "autoJoin.skip": "Pular", + "autoJoin.allHandled": "Todas as sugestões foram tratadas", + "autoJoin.joinKeyword": "JOIN", + "autoJoin.confidence.fkConstraint": "Restrição FK", + "autoJoin.confidence.fkReverse": "FK (Reversa)", + "autoJoin.confidence.namingMatch": "Correspondência por nome", + "autoJoin.confidence.weakMatch": "Correspondência fraca", + "autoJoin.runSelected": "Auto-Join Selecionado", + "autoJoin.noSimilarityTitle": "Nenhum auto join encontrado", + "autoJoin.noSimilarityDetails": "Escolha manualmente as colunas para criar um join simples.", + "autoJoin.appliedTitle": "Auto-join aplicado", + "autoJoin.manual.title": "Criar Join Manual", + "autoJoin.manual.subtitle": "Nenhuma similaridade confiável foi encontrada. Selecione uma coluna de cada tabela.", + "autoJoin.manual.leftColumn": "Coluna da esquerda", + "autoJoin.manual.rightColumn": "Coluna da direita", + "autoJoin.manual.joinType": "Tipo de join", + "autoJoin.manual.operator": "Operador", + "autoJoin.manual.confirm": "Criar join", + "autoJoin.manual.noCompatible": "Nao ha colunas compativeis para o tipo de pin selecionado na esquerda.", + "autoJoin.manualJoinCreatedTitle": "Join manual criado", + "autoJoin.manualJoinFailedTitle": "Não foi possível criar o join manual", + "autoJoin.manualJoinFailedDetails": "Verifique as colunas selecionadas e joins existentes, depois tente novamente.", + "autoJoin.multipleCandidatesTitle": "Múltiplas opções de join encontradas", + "autoJoin.multipleCandidatesDetails": "{0} combinações possíveis foram encontradas. Confirme quais colunas devem ser usadas.", + "autoJoin.suggestionsFoundTitle": "Sugestões de auto-join disponíveis", + "autoJoin.suggestionsFoundDetails": "{0} sugestão(ões) encontrada(s). Selecione duas tabelas e execute Auto-Join Selecionado.", + "property.outputAlias": "ALIAS DE SAÍDA", + "property.sourceAlias": "ALIAS DE FONTE", + "property.aliasWatermark": "ex.: MinhaColuna (opcional)", + "property.parameters": "PARÂMETROS", + "property.enabled": "Habilitado", + "property.datetimeWatermark": "AAAA-MM-DDTHH:mm:ss ou deixe vazio", + "property.dateWatermark": "AAAA-MM-DD ou deixe vazio", + "property.apply": "Aplicar", + "property.inputPins": "PINS DE ENTRADA", + "property.outputPins": "PINS DE SAÍDA", + "property.sqlTrace": "RASTREIO SQL", + "property.live": "ao vivo", + "node.numericValue": "Valor Numérico", + "node.stringValue": "Valor de Texto", + "node.enterText": "Digite o texto", + "node.datetimeValue": "Valor de Data/Hora", + "node.valueLabel": "Valor:", + "node.noInputs": "Sem entradas", + "node.loadingSample": "Carregando amostra…", + "node.previewFailed": "⚠ Falha na prévia", + "node.sampleRowsHint": "5 linhas de amostra · dados de demonstração", + "sidebar.tab.nodes": "Nós", + "sidebar.tab.connection": "Conexão", + "sidebar.tab.schema": "Esquema", + "sidebar.tab.diagnostics": "Diagnósticos", + "sidebar.addNode": "+ Adicionar Nó (⇧A)", + "sidebar.previewF3": "Prévia (F3)", + "nodesList.search": "Buscar nós...", + "search.watermark": "Buscar nós… (Esc para fechar)", + "search.snippets": "★ SNIPPETS", + "commandPalette.watermark": "Executar um comando… (Esc para fechar)", + "tooltip.newCanvas": "Novo canvas (Ctrl+N)", + "tooltip.openCanvas": "Abrir canvas (Ctrl+O)", + "tooltip.saveCanvas": "Salvar canvas (Ctrl+S)", + "tooltip.fileHistory": "Histórico local de salvar/carregar (Ctrl+Alt+H)", + "tooltip.zoomOut": "Diminuir zoom (Ctrl+-)", + "tooltip.zoomIn": "Aumentar zoom (Ctrl++)", + "tooltip.fitToScreen": "Ajustar à tela (Ctrl+0)", + "tooltip.autoLayout": "Layout automático — organiza os nós em colunas lógicas (Ctrl+L, Ctrl+Z para desfazer)", + "tooltip.snapToGrid": "Alternar snap na grade (Ctrl+G)", + "tooltip.dataPreview": "Prévia de dados (F3)", + "tooltip.toggleLanguage": "Alternar idioma (pt-BR / en-US)", + "tooltip.appDiagnostics": "Diagnóstico do app (autoavaliação)", + "tooltip.keyboardShortcuts": "Atalhos do teclado (F1)", + "tooltip.cancelRunningQuery": "Cancelar a consulta em execução", + "tooltip.closeEsc": "Fechar (Esc)", + "tooltip.recheckConnectionHealth": "Verificar novamente a saúde da conexão", + "tooltip.deleteConnection": "Excluir conexão", + "tooltip.testConnection": "Testar conexão", + "tooltip.saveConnection": "Salvar conexão", + "tooltip.activateConnection": "Ativar esta conexão", + "tooltip.toggleDataSamplePreview": "Alternar prévia de dados de amostra", + "tooltip.liveSqlMutatingBlocked": "Este SQL contém um comando que altera dados e não pode ser executado no Modo de Prévia Segura", + "tooltip.copySql": "Copiar SQL para a área de transferência", + "tooltip.formatSql": "Formatar SQL", + "tooltip.openBenchmark": "Abrir Benchmark de Consulta (medir média / mediana / p95 de latência)", + "tooltip.openExplainPlan": "Abrir inspetor de Explain Plan — visualizar o plano de execução da consulta", + "tooltip.switchToQueryMode": "Alternar para o canvas de Query", + "tooltip.switchToDdlMode": "Alternar para o canvas de DDL", + "tooltip.switchToSqlMode": "Alternar para o editor SQL", + "tooltip.autoJoinSelected": "Tentar auto-join entre as duas tabelas selecionadas", + "tooltip.pins.inputs": "Entradas", + "tooltip.pins.outputs": "Saídas", + "tooltip.pins.none": "Nenhum", + "tooltip.tableColumns": "Colunas", + "tooltip.tableColumns.none": "Sem colunas detalhadas", + "window.minimize": "Minimizar janela", + "window.maximizeRestore": "Maximizar/restaurar janela", + "window.close": "Fechar janela", + "menu.newDiagram": "Novo diagrama", + "menu.openFile": "Abrir arquivo", + "menu.save": "Salvar", + "menu.fileHistory": "Histórico de arquivos", + "menu.shortcuts": "Atalhos de teclado", + "menu.settings": "Configurações", + "menu.group.project": "PROJETO", + "menu.group.currentMode": "MODO ATUAL", + "menu.group.tools": "FERRAMENTAS", + "menu.reason.ddlOnly": "Disponível apenas no modo DDL.", + "menu.importSqlQuery": "Importar SQL para Query", + "menu.importDdlSchema": "Importar Schema DDL", + "menu.viewDdlSql": "Ver SQL DDL", + "menu.executeDdl": "Executar DDL", + "menu.backToStart": "Voltar para início", + "toast.ddlExecuteFailed": "Falha ao executar DDL.", + "toast.ddlOpenFailed": "Falha ao abrir SQL DDL.", + "toast.ddlImportFailed": "Falha ao importar schema para DDL.", + "toast.ddlConnectToImportSchema": "Conecte-se a um banco para importar schema no canvas DDL.", + "toast.ddlNoTablesFound": "Nenhuma tabela encontrada para importar no modo DDL.", + "toast.ddlSchemaImported": "Schema importado para o canvas DDL.", + "toast.ddlImportSummary": "{0} tabela(s), {1} coluna(s), {2} FK(s), {3} indice(s) unicos.", + "toast.ddlConnectToImportTable": "Conecte-se a um banco para importar tabelas no canvas DDL.", + "toast.ddlTableAlreadyExists": "A tabela '{0}' ja existe no canvas DDL.", + "toast.ddlTableImported": "Tabela importada para o canvas DDL.", + "toast.ddlTableImportSummary": "Nos: +{0}, conexoes: +{1}, FKs: +{2}.", + "toast.ddlNoActiveConnection": "Nenhuma conexão ativa para executar DDL.", + "toast.ddlExecutedSuccess": "DDL executado com sucesso.", + "toast.ddlExecutedWithIssues": "DDL executado com falhas.", + "toast.switchToDdl": "Alterne para o modo DDL para gerar SQL.", + "toast.ddlInvalid": "DDL inválido. Corrija os erros antes de continuar.", + "toast.ddlNoStatements": "Nenhum statement DDL foi gerado no canvas.", + "toast.previewOpenFailed": "Falha ao abrir preview.", + "tab.switchFailed": "Falha ao alternar aba: {0}", + "settings.status.darkApplied": "Tema escuro aplicado.", + "settings.status.lightApplied": "Tema claro aplicado.", + "settings.status.snapUpdated": "Snap atualizado: {0}.", + "settings.status.languageToggled": "Idioma alternado.", + "settings.status.languageSelected": "Idioma selecionado: {0}.", + "settings.status.themeEditorReady": "Editor de tema pronto. Aplique para salvar e usar o tema.", + "settings.section.appearance.title": "Temas", + "settings.section.languageRegion.title": "Idioma e Região", + "settings.section.dateTime.title": "Data e Hora", + "settings.section.keyboard.title": "Atalhos de Teclado", + "settings.section.privacy.title": "Privacidade", + "settings.section.notification.title": "Notificações", + "settings.section.accessibility.title": "Acessibilidade", + "settings.section.default.title": "Configurações", + "settings.section.appearance.subtitle": "Escolha o estilo ou personalize seu tema", + "settings.section.languageRegion.subtitle": "Gerencie idioma e formatação regional", + "settings.section.keyboard.subtitle": "Personalize os atalhos de teclado usados pela command palette e execução do canvas.", + "settings.section.wip.subtitle": "Trabalho em progresso.", + "settings.section.default.subtitle": "Configurações da aplicação", + "settings.general": "Geral", + "settings.nav.appearance": "Aparência", + "settings.theme.light": "Modo Claro", + "settings.theme.dark": "Modo Escuro", + "settings.theme.system": "Preferências do Sistema", + "settings.gridSnap.title": "Snap na Grade", + "settings.gridSnap.subtitle": "Controla o snap dos nós no canvas.", + "settings.language.subtitle": "Alterna entre PT-BR e EN-US.", + "settings.language.toggle": "Alternar idioma", + "settings.language.option.ptBR": "Português (Brasil)", + "settings.language.option.enUS": "Inglês (Estados Unidos)", + "settings.language.option.esES": "Espanhol (Espanha)", + "settings.language.option.ruRU": "Russo", + "settings.language.option.jaJP": "Japonês", + "settings.language.option.zhTW": "Chinês Tradicional", + "settings.themeJson.title": "Theme JSON", + "settings.themeJson.subtitle": "Cole o JSON do tema, aplique e persista instantaneamente.", + "settings.themeJson.apply": "Aplicar JSON", + "settings.themeJson.restoreDefault": "Restaurar tema padrão", + "mode.query": "Query", + "mode.ddl": "DDL", + "mode.sql": "SQL", + "sidebar.left.close": "Fechar barra lateral esquerda", + "sidebar.left.open": "Reabrir barra lateral esquerda", + "sidebar.right.close": "Fechar barra lateral direita", + "sidebar.right.open": "Reabrir barra lateral direita", + "connection.completedTitle": "Conexão concluída", + "connection.clearCanvasPrompt": "Deseja limpar o canvas atual para começar com a nova conexão?", + "connection.close": "Fechar gerenciador de conexões", + "connection.refreshHealth": "Atualizar saúde da conexão", + "common.details": "Detalhes", + "common.cancel": "Cancelar", + "common.keep": "Manter", + "common.clear": "Limpar", + "zoom.out": "Diminuir zoom", + "zoom.in": "Aumentar zoom", + "zoom.fit": "Ajustar zoom à tela", + "zoom.level": "Nível de zoom", + "settings.theme.mode": "Modo de tema", + "diagnostics.category.canvas": "Integridade do Canvas", + "diagnostics.category.output": "Saída e Execução", + "diagnostics.category.session": "Sessão e Segurança", + "diagnostics.category.notice": "Avisos em Tempo de Execução", + "diagnostics.summary.ok": "Sistema sem problemas", + "diagnostics.summary.warningCount": "{0} aviso(s) detectado(s)", + "diagnostics.summary.errorCount": "{0} erro(s) detectado(s)", + "diagnostics.canvasMigration": "Migração de Canvas", + "diagnostics.recommendation.resaveFile": "Salve novamente o arquivo para atualizar para a versão mais recente do schema.", + "diagnostics.canvasState.name": "Estado do Canvas", + "diagnostics.canvasState.recommendation": "Adicione pelo menos um {0} e um {1}", + "diagnostics.canvasState.empty": "Canvas vazio - nenhum nó presente", + "diagnostics.canvasState.counts": "{0} nó(s), {1} conexão(ões)", + "diagnostics.validation.name": "Erros de Validação", + "diagnostics.validation.recommendation": "Corrija os nós destacados antes de abrir a prévia de saída", + "diagnostics.validation.errorWithWarnings": "{0} erro(s) e {1} aviso(s) no grafo", + "diagnostics.validation.warningOnly": "{0} aviso(s) no grafo", + "diagnostics.validation.none": "Sem problemas de validação", + "diagnostics.orphan.name": "Nós Órfãos", + "diagnostics.orphan.recommendation": "Use a limpeza de órfãos para remover nós sem uso", + "diagnostics.orphan.count": "{0} nó(s) não conectado(s) a nenhuma saída", + "diagnostics.orphan.none": "Nenhum nó órfão detectado", + "diagnostics.naming.name": "Convenções de Nome", + "diagnostics.naming.recommendation": "Use correção automática quando a conformidade estiver abaixo de 100%", + "diagnostics.naming.conformance": "Conformidade de nomes: {0}%", + "diagnostics.naming.ok": "Todos os aliases seguem as convenções (100%)", + "diagnostics.queryCompilation.name": "Compilação SQL ao Vivo", + "diagnostics.queryCompilation.recommendation": "Revise os diagnósticos SQL quando houver erros/avisos", + "diagnostics.queryCompilation.errorFallback": "A compilação SQL ao vivo reportou erros.", + "diagnostics.queryCompilation.warningCounts": "{0} item(ns) de diagnóstico, {1} aviso(s) de guardrail.", + "diagnostics.queryCompilation.ok": "SQL ao vivo compilado sem diagnósticos.", + "diagnostics.previewSafety.name": "Segurança da Prévia", + "diagnostics.previewSafety.recommendation": "A prévia executa apenas comandos de leitura.", + "diagnostics.previewSafety.blocked": "O SQL atual altera dados e foi bloqueado pelo modo seguro.", + "diagnostics.previewSafety.ok": "Verificações de segurança da prévia concluídas.", + "diagnostics.previewExecution.name": "Execução da Prévia", + "diagnostics.previewExecution.recommendation": "Execute a prévia e revise diagnósticos de execução", + "diagnostics.previewExecution.failed": "Falha na execução da prévia.", + "diagnostics.previewExecution.cancelled": "Execução da prévia cancelada.", + "diagnostics.previewExecution.done": "{0} linha(s) em {1}ms.", + "diagnostics.previewExecution.none": "Nenhum problema de execução de prévia detectado.", + "diagnostics.ddlCompilation.name": "Compilação DDL", + "diagnostics.ddlCompilation.recommendation": "Corrija diagnósticos de compilação DDL antes de executar", + "diagnostics.ddlCompilation.failed": "Falha na compilação DDL.", + "diagnostics.ddlCompilation.warningCount": "{0} aviso(s) reportado(s) pelo compilador DDL.", + "diagnostics.ddlCompilation.ok": "Compilação DDL concluída com sucesso.", + "diagnostics.ddlOutput.name": "Saída DDL", + "diagnostics.ddlOutput.recommendation": "Complete os nós DDL até gerar ao menos um statement", + "diagnostics.ddlOutput.none": "Nenhum statement DDL gerado ainda.", + "diagnostics.ddlOutput.lines": "{0} linha(s) de DDL gerada(s).", + "diagnostics.undo.name": "Histórico de Desfazer", + "diagnostics.undo.recommendation": "O histórico fica apenas em memória; salve o canvas regularmente.", + "diagnostics.undo.saved": "Canvas salvo (sem mudanças pendentes).", + "diagnostics.undo.unsavedDeep": "Mudanças não salvas com {0} passos de desfazer.", + "diagnostics.undo.unsaved": "Mudanças não salvas - {0} passo(s) de desfazer disponível(is).", + "diagnostics.report.title": "AkkornStudio - Relatório de Diagnóstico", + "diagnostics.report.generated": "Gerado", + "diagnostics.report.overall": "Resumo", + "diagnostics.report.details": "Detalhes", + "diagnostics.report.recommendation": "Recomendação", + "diagnostics.report.lastCheck": "Última verificação", + "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", + "node.datetimeFormat": "AAAA-MM-DDTHH:mm:ss", + "preview.providerLabel": "Provedor", + "preview.ddlDiagnosticsHint": "Detalhes de erros/avisos disponíveis em Diagnósticos.", + "preview.schemaAnalysis.run": "Executar análise", + "preview.schemaAnalysis.cancel": "Cancelar", + "preview.schemaAnalysis.issues": "Issues", + "preview.schemaAnalysis.clearFilters": "Limpar filtros", + "preview.schemaAnalysis.clearBlacklist": "Limpar blacklist", + "preview.schemaAnalysis.severity": "Severidade", + "preview.schemaAnalysis.severity.info": "Info", + "preview.schemaAnalysis.severity.warning": "Aviso", + "preview.schemaAnalysis.severity.critical": "Crítico", + "preview.schemaAnalysis.rule": "Regra", + "preview.schemaAnalysis.rule.fkCatalogInconsistent": "Catálogo de FK inconsistente", + "preview.schemaAnalysis.rule.missingFk": "FK ausente", + "preview.schemaAnalysis.rule.namingConventionViolation": "Violação de convenção de nomes", + "preview.schemaAnalysis.rule.lowSemanticName": "Nome com baixa semântica", + "preview.schemaAnalysis.rule.missingRequiredComment": "Comentário obrigatório ausente", + "preview.schemaAnalysis.rule.nf1HintMultiValued": "Indício 1FN: multi-valorado", + "preview.schemaAnalysis.rule.nf2HintPartialDependency": "Indício 2FN: dependência parcial", + "preview.schemaAnalysis.rule.nf3HintTransitiveDependency": "Indício 3FN: dependência transitiva", + "preview.schemaAnalysis.minConfidence": "Confiança mínima", + "preview.schemaAnalysis.tableFilter": "Filtro de tabela", + "preview.schemaAnalysis.tableFilterWatermark": "schema.tabela", + "preview.schemaAnalysis.ignore": "Filtros de execução", + "preview.schemaAnalysis.ignoreViews": "Ignorar views e materialized views", + "preview.schemaAnalysis.blacklist": "Blacklist de tabelas", + "preview.schemaAnalysis.blacklistAdd": "Adicionar", + "preview.schemaAnalysis.blacklistRemove": "Remover selecionada", + "preview.schemaAnalysis.ignoreTable.placeholder": "schema.tabela", + "preview.schemaAnalysis.details": "Detalhes", + "preview.schemaAnalysis.evidence": "Evidências", + "preview.schemaAnalysis.suggestions": "Sugestões", + "preview.schemaAnalysis.ruleDiagnostics": "Diagnósticos da regra", + "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", + "preview.schemaAnalysis.copySql": "Copiar SQL", + "preview.schemaAnalysis.applyToCanvas": "Aplicar no canvas", + "preview.schemaAnalysis.summary.issues": "Issues:", + "preview.schemaAnalysis.summary.rawPrefix": "(bruto:", + "preview.schemaAnalysis.summary.critical": "| Crítico:", + "preview.schemaAnalysis.summary.warning": "| Aviso:", + "preview.schemaAnalysis.summary.info": "| Info:", + "preview.schemaAnalysis.state.metadataUnavailable": "Metadata indisponível para análise estrutural.", + "preview.schemaAnalysis.state.cancelled": "Análise cancelada pelo usuário.", + "preview.schemaAnalysis.state.partialTimeout": "Análise finalizada parcialmente por timeout.", + "preview.schemaAnalysis.state.failed": "Falha na análise estrutural.", + "preview.schemaAnalysis.state.empty": "Nenhum problema estrutural inferível foi detectado.", + "preview.schemaAnalysis.state.noFilterMatch": "Nenhuma issue corresponde aos filtros selecionados.", + "preview.schemaAnalysis.state.noIssueSelected": "Nenhuma issue selecionada.", + "preview.schemaAnalysis.state.noSqlCandidate": "Nenhum SQL candidate disponível.", + "preview.schemaAnalysis.actionBlockedTooltip": "Ação indisponível para o nível de risco ou capacidade atual.", + "common.navigate": "Navegar", + "common.close": "Fechar", + "common.esc": "Esc", + "common.ms": "ms", + "common.zero": "0", + "diagnostics.tip": "Dica: verifique Diagnósticos sempre que preview/saída reportar avisos ou erros.", + "nodesList.empty": "Nenhum nó encontrado", + "nodesList.emptyHint": "Ajuste o termo de busca para explorar os tipos disponíveis", + "schema.emptyFiltered": "Nenhum objeto encontrado para o filtro atual", + "start.lastSnapshot": "Último snapshot", + "app.brandBadge": "VS", + "property.tab.properties": "Propriedades", + "property.tab.projectSettings": "Configurações do Projeto", + "property.nodeType": "TIPO DE NÓ", + "property.selectNodeHint": "Selecione um nó para editar suas propriedades.", + "property.namingConventions": "Convenções de Nomenclatura", + "property.aliasConvention": "Convenção de alias", + "property.enforceAliasNaming": "Forçar nomenclatura de alias", + "property.warnReservedSql": "Alertar sobre palavras reservadas SQL", + "property.maxAliasLength": "Tamanho máximo do alias", + "property.maxAliasLengthDefault": "64", + "property.namingSettingsHint": "Essas configurações são por projeto e são usadas por validação e helpers de nomenclatura.", + "start.tips": "Dicas", + "start.tips.quick": "Dicas rápidas", + "start.tips.item1": "1. Clique em Novo Diagrama para iniciar do zero.", + "start.tips.item2": "2. Use templates para acelerar protótipos.", + "start.tips.item3": "3. Abra conexões salvas para carregar tabelas reais.", + "start.tips.shortcut": "Atalho: CTRL+SHIFT+P abre a paleta de comandos.", + "start.workspace": "WORKSPACE", + "start.resumeTitle": "Continuar de onde parou", + "start.resumeSubtitle": "Retome rapidamente um projeto recente ou comece um novo diagrama.", + "start.chip.quickFlow": "Fluxo rápido", + "start.chip.templates": "Templates", + "start.chip.connections": "Conexões", + "start.savedConnectionsTitle": "Conexões salvas", + "start.savedConnectionsSubtitle": "Conecte-se rapidamente a um banco para carregar schema e tabelas.", + "start.noConnectionsTitle": "Nenhuma conexão configurada ainda", + "start.noConnectionsSubtitle": "Crie uma conexão para explorar tabelas reais no editor.", + "start.newConnection": "+ Nova Conexão", + "start.recentProjectsTitle": "Projetos Recentes", + "start.searchRecent": "Buscar projeto recente...", + "start.quickActions": "Ações rápidas", + "start.quickActionsSubtitle": "Abra um arquivo existente ou inicie um novo diagrama.", + "start.noRecentTitle": "Nenhum projeto recente ainda", + "start.noRecentSubtitle": "Use o card de ações rápidas acima para começar.", + "start.exploreTemplates": "Explorar templates", + "start.templatesFavoritesHint": "Favoritos no topo", + "start.favoriteTemplate": "Favoritar template", + "node.columnSetPreview": "Prévia de ColumnSet", + "node.view": "VIEW", + "node.tableDefinition": "Definição da Tabela", + "node.join": "JOIN", + "node.window.addPartition": "Adicionar slot PARTITION BY", + "node.window.removePartition": "Remover slot PARTITION BY", + "node.window.addOrder": "Adicionar slot ORDER BY", + "node.window.removeOrder": "Remover slot ORDER BY", + "sql.keyword.select": "SELECT", + "sql.keyword.from": "FROM", + "sql.keyword.join": "JOIN", + "sql.keyword.where": "WHERE", + "sql.keyword.limit": "LIMIT", + "sqlImporter.close": "Fechar importador SQL", + "sqlImporter.report.imported": "Importados", + "sqlImporter.report.partial": "Parciais", + "sqlImporter.report.skipped": "Ignorados", + "sqlImporter.confirmClearCanvas": "A importação SQL irá limpar o canvas atual. Deseja continuar?", + "sqlImporter.confirmProceed": "Continuar importação", + "benchmark.close": "Fechar benchmark", + "benchmark.p95": "P95", + "benchmark.n": "N", + "liveSql.safePreview": "MODO DE PRÉVIA SEGURA", + "liveSql.title": "SQL AO VIVO", + "liveSql.blocked": "BLOQUEADO", + "liveSql.copy": "Copiar", + "liveSql.format": "Formatar", + "liveSql.benchmark": "Benchmark", + "liveSql.explain": "Explain", + "liveSql.actionsHint": "Ferramentas de desempenho", + "ddl.dialog.title": "Executar DDL", + "ddl.dialog.execute": "Executar", + "ddl.dialog.cancel": "Cancelar", + "ddl.dialog.close": "Fechar", + "ddl.dialog.stopOnError": "Parar na primeira falha", + "ddl.dialog.confirmDestructive": "Confirmo execução de statements destrutivos (DROP TABLE)", + "ddl.dialog.reviewBeforeRun": "Revise o script DDL antes de confirmar.", + "ddl.dialog.confirmQuestion": "Confirma a execução do DDL no banco conectado?", + "ddl.dialog.irreversibleWarning": "Esta ação pode alterar o schema de forma irreversível.", + "ddl.dialog.mustConfirmDestructive": "Confirme a execução destrutiva para continuar.", + "ddl.dialog.executing": "Executando...", + "ddl.execute.result.summary": "Statements: {0} | Sucesso: {1} | Falhas: {2} | Tempo: {3:0}ms", + "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", + "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", + "ddl.execute.result.failed": "Falha ao executar DDL.", + "ddl.execute.result.cancelled": "Execução cancelada pelo usuário.", + "ddl.execute.result.cancelledDetails": "A execução DDL foi interrompida antes da conclusão.", + "context.deleteSingle": "Excluir {0}", + "context.deleteMultiple": "Excluir {0} nós", + "context.bringForward": "Trazer para frente (Ctrl+PgUp)", + "context.sendBackward": "Enviar para trás (Ctrl+PgDown)", + "context.bringToFront": "Trazer para o topo (Ctrl+Shift+PgUp)", + "context.sendToBack": "Enviar para o fundo (Ctrl+Shift+PgDown)", + "context.normalizeLayers": "Normalizar camadas", + "context.deleteWire": "Excluir conexão", + "context.addNode": "Adicionar nó (Shift+A)", + "context.undoWithDescription": "Desfazer {0}", + "context.redo": "Refazer", + "shortcuts.windowTitle": "Atalhos de Teclado", + "shortcuts.headerTitle": "AkkornStudio - Atalhos", + "shortcuts.headerHint": "Dica: use CTRL+SHIFT+P para abrir a Command Palette e pesquisar comandos.", + "shortcuts.filterWatermark": "Filtrar atalhos por tecla ou ação...", + "shortcuts.resultCount": "{0} atalhos", + "shortcuts.resultFilter": "{0} resultado(s) para \"{1}\"", + "shortcuts.noneFound": "Nenhum atalho encontrado.", + "shortcuts.section.fileGeneral": "Arquivo e geral", + "shortcuts.section.editing": "Edição", + "shortcuts.section.canvasNavigation": "Canvas e navegação", + "shortcuts.section.zoomPanPrecision": "Zoom, pan e precisão", + "shortcuts.section.previewInspection": "Preview e inspeção", + "shortcuts.key.deleteOrBackspace": "Del ou Backspace", + "shortcuts.key.middleDrag": "Botão do meio + arrastar", + "shortcuts.key.rightDrag": "Botão direito + arrastar", + "shortcuts.key.spaceDrag": "Space + arrastar", + "shortcuts.key.altLeftDrag": "Alt + arrastar esquerdo", + "shortcuts.key.arrows": "Setas", + "shortcuts.key.shiftArrows": "Shift + Setas", + "shortcuts.action.openShortcutScreen": "Abrir esta tela de atalhos", + "shortcuts.action.newCanvas": "Novo canvas", + "shortcuts.action.openFile": "Abrir arquivo", + "shortcuts.action.save": "Salvar", + "shortcuts.action.saveAs": "Salvar como", + "shortcuts.action.commandPalette": "Command Palette", + "shortcuts.action.undo": "Desfazer", + "shortcuts.action.redo": "Refazer", + "shortcuts.action.selectAll": "Selecionar todos", + "shortcuts.action.deleteSelection": "Excluir seleção", + "shortcuts.action.closeOverlayCancel": "Fechar overlays / cancelar ações", + "shortcuts.action.openNodeSearch": "Abrir busca de nodes", + "shortcuts.action.resetViewport": "Reset de viewport", + "shortcuts.action.centerSelection": "Centralizar seleção", + "shortcuts.action.fitSelection": "Enquadrar seleção", + "shortcuts.action.autoLayout": "Auto Layout", + "shortcuts.action.toggleSnapToGrid": "Toggle Snap to Grid", + "shortcuts.action.bringForward": "Trazer para frente", + "shortcuts.action.sendBackward": "Enviar para trás", + "shortcuts.action.bringToFront": "Trazer para o topo", + "shortcuts.action.sendToBack": "Enviar para o fundo", + "shortcuts.action.zoomInOut": "Zoom in / out", + "shortcuts.action.pan": "Pan", + "shortcuts.action.temporaryPan": "Pan temporário", + "shortcuts.action.alternatePan": "Pan alternativo", + "shortcuts.action.fineNudge": "Nudge fino da seleção", + "shortcuts.action.fastNudge": "Nudge acelerado", + "shortcuts.action.togglePreview": "Toggle data preview", + "shortcuts.action.explainPlan": "Explain plan", + "shortcuts.action.runPreview": "Run preview", + "shortcuts.action.connectionManager": "Connection manager", + "shortcuts.action.flowVersionHistory": "Flow version history", + "shortcuts.resetAll": "Resetar tudo", + "shortcuts.customized": "Customizado", + "shortcuts.default": "Padrão", + "shortcuts.apply": "Aplicar", + "shortcuts.reset": "Resetar", + "shortcuts.status.resetAllSuccess": "Todos os atalhos foram resetados para o padrão.", + "shortcuts.status.updated": "Atalho atualizado.", + "shortcuts.status.reset": "Atalho resetado para o padrão.", + "shortcuts.status.updateFailed": "Não foi possível atualizar o atalho.", + "toast.severity.success": "Sucesso", + "toast.severity.warning": "Aviso", + "toast.severity.error": "Erro", + "toast.details.success": "Detalhes de Sucesso", + "toast.details.warning": "Detalhes de Aviso", + "toast.details.error": "Detalhes de Erro", + "diagnostics.area.cteEditor": "Editor de CTE", + "diagnostics.area.viewEditor": "Editor de View", + "diagnostics.area.subEditor": "Sub-editor", + "diagnostics.cteEditor.restoreParentFailed": "Falha ao restaurar o canvas pai. As edições do CTE foram descartadas.", + "diagnostics.recommendation.reloadFileIfNeeded": "Recarregue o arquivo se necessário.", + "diagnostics.viewEditor.exitFailed": "Não foi possível sair: {0}", + "diagnostics.viewEditor.canvasIncomplete": "o canvas está incompleto.", + "diagnostics.viewEditor.exitRecommendation": "Conecte um ResultOutput válido ou use o comando de descarte.", + "diagnostics.viewEditor.restoreParentFailed": "Falha ao restaurar o canvas pai. O subgrafo foi descartado.", + "diagnostics.subEditor.executeFailed": "Falha ao executar ação do editor: {0}", + "diagnostics.subEditor.executeRecommendation": "Tente novamente. Se persistir, recarregue o canvas.", + "diagnostics.canvasMigration.openWarning": "Abertura: {0}", + "diagnostics.canvasMigration.sessionRestoreWarning": "Restauração de sessão: {0}", + "diagnostics.canvasMigration.versionRestoreWarning": "Restauração de versão: {0}", + "diagnostics.recommendation.resaveLatestSchema": "Revise os diagnósticos e salve novamente o canvas para persistir o schema mais recente.", + "diagnostics.recommendation.saveMigratedSchema": "Revise os diagnósticos e salve o canvas para persistir o schema migrado.", + "file.saveDialog.title": "Salvar Canvas", + "file.saveDialog.suggestedName": "Query1", + "file.save.success": "Canvas salvo com sucesso.", + "file.save.failedWithReason": "Falha ao salvar: {0}", + "file.openDialog.title": "Abrir Canvas", + "file.openDialog.canvasType": "Canvas SQL Architect", + "file.open.failedWithReason": "Falha ao abrir: {0}", + "file.open.success": "Canvas aberto com sucesso.", + "file.open.successWithWarnings": "Canvas aberto com avisos.", + "session.restore.failedWithReason": "Falha ao restaurar: {0}", + "session.restore.successWithWarnings": "Sessão restaurada com avisos.", + "session.restore.success": "Sessão restaurada com sucesso.", + "export.documentation.dialogTitle": "Exportar Documentação do Fluxo", + "export.documentation.success": "Documentação exportada com sucesso.", + "export.documentation.failed": "Falha ao exportar documentação.", + "export.failed.pathPermissionsHint": "Verifique o caminho do arquivo e as permissões.", + "export.nodeNotFound": "Nenhum nó de Exportação {0} foi encontrado no canvas. Adicione um pelo menu de busca de nós.", + "export.dialogTitleByExtension": "Exportar como {0}", + "export.success": "Exportação concluída com sucesso.", + "export.failed": "Falha na exportação.", + "fileHistory.currentFile.none": "Nenhum arquivo selecionado", + "fileHistory.status.saveFirst": "Salve o canvas primeiro para habilitar o histórico local.", + "fileHistory.status.noneFound": "Nenhuma versão local encontrada ainda. Salve este arquivo para criar entradas no histórico.", + "fileHistory.status.countAvailable": "{0} versão(ões) local(is) disponível(is).", + "fileHistory.restore.failedWithReason": "Falha ao restaurar: {0}", + "fileHistory.restore.successFrom": "Versão restaurada de {0}.", + "preview.status.cancelled": "Cancelado", + "preview.status.error": "Erro", + "preview.status.ready": "Pronto", + "preview.runningWithMs": "Executando... {0}ms", + "preview.runningWithTimeout": "Executando... {0}ms (timeout: {1}s)", + "preview.runningSlowWithTimeout": "Executando... {0}ms (timeout: {1}s) · Consulta lenta, timeout em {2}s", + "explain.errorWithReason": "Erro no explain plan: {0}", + "explain.noSql": "Nenhum SQL para explicar. Monte uma consulta no canvas primeiro.", + "ddl.compilationFailed": "Falha na compilação", + "ddl.compileErrorWithReason": "Erro de compilação DDL: {0}", + "command.undo.name": "Desfazer", + "command.undo.description": "Desfazer última ação", + "command.redo.name": "Refazer", + "command.redo.description": "Refazer última ação desfeita", + "command.addNode.name": "Adicionar nó", + "command.addNode.description": "Abrir menu de busca para adicionar um nó", + "command.bringForward.name": "Trazer para frente", + "command.bringForward.description": "Mover nós selecionados uma camada para frente", + "command.sendBackward.name": "Enviar para trás", + "command.sendBackward.description": "Mover nós selecionados uma camada para trás", + "command.bringToFront.name": "Trazer para o topo", + "command.bringToFront.description": "Mover nós selecionados para a camada superior", + "command.sendToBack.name": "Enviar para o fundo", + "command.sendToBack.description": "Mover nós selecionados para a camada inferior", + "command.normalizeLayers.name": "Normalizar camadas", + "command.normalizeLayers.description": "Compactar índices de camada para uma ordem limpa de 0..N", + "tooltip.cleanupOrphans": "Remover nós órfãos não conectados ao output (Ctrl+Z para desfazer)", + "main.orphanSuffix": "Órfão(s)", + "tooltip.autoFixAliasNaming": "Corrigir aliases para snake_case (Ctrl+Z para desfazer)", + "main.namingPrefix": "Nomenclatura", + "fileHistory.compressedLabel": "Comprimido:", + "schema.itemsSuffix": "item(ns)", + "property.panel.title": "Propriedades", + "property.panel.multiSelected": "{0} nós selecionados", + "sqlImporter.status.pasteSelect": "Cole um SELECT acima e clique em Importar.", + "sqlImporter.status.inputTooLarge": "A entrada SQL é muito grande ({0:N0} caracteres). O limite é {1:N0}. Divida a consulta ou aumente o limite de importação.", + "sqlImporter.status.parsing": "Analisando SQL...", + "sqlImporter.status.done": "Concluído - {0} importado(s), {1} parcial(is), {2} ignorado(s).", + "sqlImporter.status.cancelledByUser": "Importação cancelada pelo usuário.", + "sqlImporter.status.timeout": "A importação excedeu o tempo após {0:0.#}s. Tente uma consulta menor ou aumente o timeout.", + "sqlImporter.status.parseError": "Erro de parsing: {0}", + "sqlImporter.status.clearConfirmationRequired": "A importação SQL vai limpar o canvas atual. Confirme para continuar.", + "sqlImporter.status.clearConfirmationCancelled": "Importação cancelada. O canvas atual foi mantido.", + "diagnostics.area.undoRedoTransaction": "Transação Desfazer/Refazer", + "undoRedo.rollbackExecuted": "Rollback executado para '{0}' ({1} operação(ões) revertida(s)).", + "undoRedo.rollbackRecommendation": "Revise o estado do canvas e tente novamente a ação, se necessário.", + "node.preview.noCatalog": "Catálogo indisponível", + "connection.error.searchMenuNotInitialized": "menu de busca não inicializado", + "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", + "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", + "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", + "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", + "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", + "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", + "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", + "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", + "diagnostics.area.connection": "Conexão", + "connection.warning.canvasMayContainOldTables": "O canvas ainda pode conter tabelas de uma conexão anterior.", + "connection.warning.canvasMayContainOldTablesRecommendation": "Limpe o canvas manualmente ou reconecte e escolha manter/limpar novamente.", + "undoRedo.transaction.unnamed": "transação sem nome", + "benchmark.runLabelDefault": "Execução 1", + "benchmark.runLabelPattern": "Execução {0}", + "benchmark.status.failedWithReason": "Falha no benchmark: {0}", + "benchmark.status.noSql": "Nenhum SQL para benchmark - monte uma consulta primeiro.", + "benchmark.status.warmupProgress": "Aquecimento {0}/{1}...", + "benchmark.status.iterationProgress": "Iteração {0}/{1}...", + "benchmark.status.done": "Concluído - {0}", + "benchmark.status.cancelled": "Benchmark cancelado.", + "app.windowTitle": "AkkornStudio", + "preview.error.safePreviewBlocked": "Modo Preview Seguro: comandos que alteram dados (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) não podem ser executados no preview.", + "preview.error.noActiveConnection": "Nenhuma conexão de banco ativa. Conecte-se a um banco primeiro.", + "sqlImporter.error.selectFromNotFound": "Não foi possível encontrar SELECT ... FROM na consulta.", + "sqlImporter.error.fromClauseParseFailed": "Não foi possível interpretar a cláusula FROM.", + "sqlImporter.error.syntaxUnterminatedString": "Erro de sintaxe na linha {0}, coluna {1}: literal de string não terminada.", + "sqlImporter.error.missingClosingParenthesis": "faltando ')' de fechamento", + "sqlImporter.error.unexpectedClosingParenthesis": "')' inesperado", + "sqlImporter.error.syntaxAtLineColumn": "Erro de sintaxe na linha {0}, coluna {1}: {2}.", + "errorDiagnostics.safePreview.label": "Bloqueado pelo Modo Preview Seguro", + "errorDiagnostics.safePreview.friendly": "Este SQL contém um comando de mutação de dados e não pode ser executado no preview.", + "errorDiagnostics.safePreview.suggestion": "Remova ou substitua o comando de mutação (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) antes de executar o preview.", + "errorDiagnostics.connection.label": "Falha de conexão", + "errorDiagnostics.connection.friendly": "Não foi possível alcançar o servidor de banco. O host pode estar indisponível, inacessível ou bloqueando conexões.", + "errorDiagnostics.connection.suggestion": "Verifique endereço e porta do servidor, confirme que o banco está em execução e confira as regras de firewall.", + "errorDiagnostics.authorization.label": "Erro de autorização", + "errorDiagnostics.authorization.friendly": "As credenciais atuais não têm permissão para executar esta operação.", + "errorDiagnostics.authorization.suggestion": "Confirme que o usuário do banco tem privilégios de SELECT no schema/tabela de destino, ou contate o DBA.", + "errorDiagnostics.timeout.label": "Timeout de consulta", + "errorDiagnostics.timeout.friendly": "A consulta demorou demais para concluir e foi cancelada pelo servidor ou cliente.", + "errorDiagnostics.timeout.suggestion": "Adicione WHERE ou LIMIT para reduzir o volume de dados, ou aumente o timeout da consulta nas configurações de conexão.", + "errorDiagnostics.schema.label": "Erro de schema", + "errorDiagnostics.schema.friendly": "Uma tabela, coluna ou objeto referenciado não foi encontrado no banco.", + "errorDiagnostics.schema.suggestion": "Verifique nomes de tabela/coluna e confirme que o schema corresponde à conexão ativa.", + "errorDiagnostics.syntax.label": "Erro de sintaxe SQL", + "errorDiagnostics.syntax.friendly": "A consulta contém erro de sintaxe e não pôde ser interpretada pelo mecanismo do banco.", + "errorDiagnostics.syntax.suggestion": "Revise o SQL destacado procurando typos, parênteses desalinhados ou cláusulas não suportadas pelo provider ativo.", + "errorDiagnostics.compatibility.label": "Erro de compatibilidade", + "errorDiagnostics.compatibility.friendly": "Uma função, operador ou construção de sintaxe não é suportada pelo provider de banco ativo.", + "errorDiagnostics.compatibility.suggestion": "Troque para o provider correto na barra SQL, ou substitua a construção não suportada por equivalente.", + "errorDiagnostics.unknown.label": "Erro inesperado", + "errorDiagnostics.unknown.friendly": "Ocorreu um erro ao executar a consulta de preview.", + "errorDiagnostics.unknown.suggestion": "Verifique os detalhes técnicos abaixo e confirme que o canvas está configurado corretamente.", + "error.mainWindow.invalidDataContext": "O DataContext da MainWindow deve ser um ShellViewModel.", + "error.mainWindow.canvasNotInitialized": "CanvasViewModel não foi inicializado.", + "error.mainWindow.ddlPreviewUnavailable": "Preview DDL indisponível para o canvas atual.", + "themeJson.editor.template": "{\n \"meta\": { \"name\": \"Tema Personalizado\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", + "themeJson.error.pasteBeforeApply": "Cole um JSON de tema antes de aplicar.", + "themeJson.error.invalidJson": "JSON inválido: {0}", + "themeJson.error.emptyPayload": "JSON inválido: payload vazio.", + "themeJson.error.invalidTheme": "Tema inválido: {0}", + "themeJson.error.appliedButSaveFailed": "Tema aplicado, mas falhou ao salvar: {0}", + "themeJson.success.appliedAndSaved": "Tema JSON aplicado e salvo.", + "themeJson.success.customRemoved": "Tema personalizado removido. Reinicie o app para voltar totalmente ao tema padrão.", + "themeJson.error.restoreDefaultFailed": "Falha ao restaurar tema padrão: {0}", + "themeValidator.error.configNull": "A configuração de tema está nula.", + "themeValidator.warning.noSections": "O tema não possui seções de cores ou tipografia; nada para aplicar.", + "themeValidator.warning.invalidColor": "{0} possui cor inválida '{1}'. Esta chave será ignorada.", + "themeValidator.warning.sizeOutOfRange": "{0}={1} está fora do intervalo (8..48). Esta chave será ignorada.", + "queryExecutor.error.openConnectionMethodNotFound": "Não foi possível encontrar o método OpenConnectionAsync no orquestrador", + "queryExecutor.error.openConnectionInvokeFailed": "Falha ao invocar OpenConnectionAsync", + "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}': o SELECT da view não pode ser reconstruído visualmente - edite manualmente no subcanvas.", + "ddlImporter.error.tableNotFoundInMetadata": "A tabela '{0}' não foi encontrada nos metadados atuais.", + "main.window.untitled": "Sem título", + "main.subEditor.noSeedProvided": "Nenhum seed de subeditor foi fornecido para {0}.", + "main.layerOrder.bringToFront": "Trazer para frente", + "main.layerOrder.sendToBack": "Enviar para trás", + "main.layerOrder.bringForward": "Avançar camada", + "main.layerOrder.sendBackward": "Recuar camada", + "main.layerOrder.normalizeLayers": "Normalizar camadas", + "export.fileType.html": "Arquivos HTML", + "export.fileType.json": "Arquivos JSON", + "export.fileType.csv": "Arquivos CSV", + "export.fileType.excel": "Arquivos Excel", + "commandPalette.templatePrefix": "Template: {0}", + "themeLoader.status.notFoundWithPath": "Arquivo de tema não encontrado: {0}", + "themeLoader.status.deserializedNull": "O JSON de tema foi desserializado para null.", + "themeLoader.status.loaded": "JSON de tema carregado com sucesso.", + "credential.error.ciphertextTooShort": "O blob de texto cifrado está muito curto.", + "credential.error.dpapiWindowsOnly": "DPAPI está disponível apenas no Windows.", + "credential.warning.loadVaultFailed": "Falha ao carregar o cofre de credenciais '{0}': {1}", + "credential.warning.persistVaultFailed": "Falha ao persistir o cofre de credenciais '{0}': {1}", + "snippetStore.warning.loadFailed": "Falha ao carregar snippets de '{0}': {1}", + "snippetStore.warning.saveFailed": "Falha ao salvar snippets: {0}", + "flowVersionStore.warning.loadFailed": "Falha ao carregar versões de fluxo de '{0}': {1}", + "flowVersionStore.warning.saveFailed": "Falha ao salvar versões de fluxo: {0}", + "queryExecutor.error.queryEmpty": "A consulta não pode estar vazia", + "queryExecutor.error.providerNotSupported": "O provider {0} não é suportado", + "queryExecutor.error.singleStatementOnly": "O preview aceita apenas uma única instrução SQL.", + "queryExecutor.error.queryEmptyWithPeriod": "A consulta não pode estar vazia.", + "queryExecutor.error.readOnlyOnly": "O modo preview suporta apenas instruções SQL somente leitura.", + "queryExecutor.error.namedParametersNotSupported": "O modo preview não suporta parâmetros nomeados na execução SQL. Use literais seguros inline ou execute a consulta fora do preview.", + "queryExecutor.error.positionalParametersNotSupported": "O modo preview não suporta placeholders posicionais ('?' ou '$1').", + "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "Align selected nodes to the bottom edge", + "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "Align selected nodes to the leftmost edge", + "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "Align selected nodes to the rightmost edge", + "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "Align selected nodes to the topmost edge", + "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "Apply CTE sub-canvas edits and return to the parent canvas", + "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "Arrange nodes into logical columns automatically", + "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "Centre selected nodes on a horizontal axis", + "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "Centre selected nodes on a vertical axis", + "commandPalette.description.clear_canvas_and_start_fresh": "Clear canvas and start fresh", + "commandPalette.description.clear_node_selection": "Clear node selection", + "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "Convert aliases to the convention configured in project settings", + "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "Create checkpoints, compare versions side-by-side and restore a previous canvas state", + "commandPalette.description.delete_the_selected_nodes": "Delete the selected nodes", + "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "Discard current sub-editor edits and return to the parent canvas", + "commandPalette.description.execute_the_current_query_in_preview": "Execute the current query in preview", + "commandPalette.description.fit_all_nodes_into_the_visible_area": "Fit all nodes into the visible area", + "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "Generate CSV file from the first CSV Export node", + "commandPalette.description.generate_html_file_from_the_first_html_export_node": "Generate HTML file from the first HTML Export node", + "commandPalette.description.generate_json_file_from_the_first_json_export_node": "Generate JSON file from the first JSON Export node", + "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "Generate XLSX workbook from the first Excel Export node", + "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "Inspect the query execution plan — see scan types, join strategies, and cost estimates", + "commandPalette.description.load_a_vsaq_canvas_file": "Load a .vsaq canvas file", + "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "Measure avg / median / p95 latency of the current SQL over N iterations", + "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "Open isolated sub-canvas editor for the selected CTE Definition node", + "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "Open local file version history created on each save and restore previous saved snapshots", + "commandPalette.description.open_output_preview_modal_for_the_active_mode": "Open output preview modal for the active mode", + "commandPalette.description.open_shortcut_reference_screen": "Open shortcut reference screen", + "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "Open the connection manager to add, edit or switch database connections", + "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "Paste a SELECT statement and generate nodes automatically — FROM, JOIN, WHERE, LIMIT are supported", + "commandPalette.description.remove_all_nodes_not_connected_to_output": "Remove all nodes not connected to output", + "commandPalette.description.reset_zoom_and_pan_to_default": "Reset zoom and pan to default", + "commandPalette.description.save_canvas_to_a_new_file": "Save canvas to a new file", + "commandPalette.description.save_current_canvas": "Save current canvas", + "commandPalette.description.save_markdown_documentation_of_the_current_flow": "Save Markdown documentation of the current flow", + "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "Save the selected nodes as a reusable snippet — insert it later via the node search menu (⇧A)", + "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "Scan all table-source nodes on the canvas for possible join relationships based on FK conventions and naming patterns", + "commandPalette.description.select_all_nodes_on_canvas": "Select all nodes on canvas", + "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "Snap node positions to 16px grid (Ctrl+G)", + "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "Spread selected nodes with equal horizontal spacing", + "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "Spread selected nodes with equal vertical spacing", + "commandPalette.description.zoom_into_the_canvas": "Zoom into the canvas", + "commandPalette.description.zoom_out_of_the_canvas": "Zoom out of the canvas", + "commandPalette.name.align_bottom": "Align Bottom", + "commandPalette.name.align_left": "Align Left", + "commandPalette.name.align_right": "Align Right", + "commandPalette.name.align_top": "Align Top", + "commandPalette.name.analyze_all_joins": "Analyze All Joins", + "commandPalette.name.auto_fix_naming": "Auto-Fix Naming", + "commandPalette.name.auto_layout": "Auto Layout", + "commandPalette.name.center_horizontally": "Center Horizontally", + "commandPalette.name.center_vertically": "Center Vertically", + "commandPalette.name.cleanup_orphans": "Cleanup Orphans", + "commandPalette.name.delete_selected": "Delete Selected", + "commandPalette.name.deselect_all": "Deselect All", + "commandPalette.name.discard_and_exit_editor": "Discard and Exit Editor", + "commandPalette.name.distribute_horizontally": "Distribute Horizontally", + "commandPalette.name.distribute_vertically": "Distribute Vertically", + "commandPalette.name.edit_selected_cte": "Edit Selected CTE", + "commandPalette.name.exit_cte_editor": "Exit CTE Editor", + "commandPalette.name.explain_plan": "Explain Plan", + "commandPalette.name.export_csv": "Export CSV", + "commandPalette.name.export_documentation": "Export Documentation", + "commandPalette.name.export_excel": "Export Excel", + "commandPalette.name.export_html": "Export HTML", + "commandPalette.name.export_json": "Export JSON", + "commandPalette.name.file_save_load_history": "File Save/Load History", + "commandPalette.name.fit_to_screen": "Fit to Screen", + "commandPalette.name.flow_version_history": "Flow Version History", + "commandPalette.name.import_sql_to_graph": "Import SQL to Graph", + "commandPalette.name.keyboard_shortcuts": "Keyboard Shortcuts", + "commandPalette.name.manage_connections": "Manage Connections", + "commandPalette.name.new_canvas": "New Canvas", + "commandPalette.name.open_file": "Open File", + "commandPalette.name.reset_viewport": "Reset Viewport", + "commandPalette.name.run_preview": "Run Preview", + "commandPalette.name.run_query_benchmark": "Run Query Benchmark", + "commandPalette.name.save": "Save", + "commandPalette.name.save_as": "Save As", + "commandPalette.name.save_selection_as_snippet": "Save Selection as Snippet", + "commandPalette.name.select_all": "Select All", + "commandPalette.name.toggle_preview": "Toggle Preview", + "commandPalette.name.toggle_snap_to_grid": "Toggle Snap to Grid", + "commandPalette.name.zoom_in": "Zoom In", + "commandPalette.name.zoom_out": "Zoom Out", + "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", + "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", + "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", + "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", + "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", + "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", + "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", + "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", + "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", + "commandPalette.tags.clear_selection": "clear selection", + "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", + "commandPalette.tags.create_insert_search_transform": "create insert search transform", + "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", + "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", + "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", + "commandPalette.tags.data_results_table_panel": "data results table panel", + "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", + "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", + "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", + "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", + "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", + "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", + "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", + "commandPalette.tags.export_json_file_output_save": "export json file output save", + "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", + "commandPalette.tags.export_persist_copy": "export persist copy", + "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", + "commandPalette.tags.forward_history": "forward history", + "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", + "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", + "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", + "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", + "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", + "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", + "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", + "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", + "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", + "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", + "commandPalette.tags.load_import_vsaq": "load import vsaq", + "commandPalette.tags.magnify_enlarge": "magnify enlarge", + "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", + "commandPalette.tags.persist_write_disk": "persist write disk", + "commandPalette.tags.remove_erase_nodes": "remove erase nodes", + "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", + "commandPalette.tags.reset_clear_blank": "reset clear blank", + "commandPalette.tags.revert_back_history": "revert back history", + "commandPalette.tags.shrink_reduce": "shrink reduce", + "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", + "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", + "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", + "sqlEditor.diffPreview.title": "Previa de diff transacional", + "sqlEditor.mutation.confirmExecute": "Confirmar execucao", + "sqlEditor.tab.closeAnyway": "Fechar mesmo assim", + "sqlEditor.tab.keepTab": "Manter aba", + "sqlEditor.status.ready": "Pronto.", + "sqlEditor.telemetry.none": "Sem telemetria de execucao ainda.", + "sqlEditor.telemetry.summary": "Instrucoes: {0} Sucesso: {1} Falhas: {2} Total: {3} ms", + "sqlEditor.telemetry.errors.none": "Sem erros agregados.", + "sqlEditor.diff.none": "Sem diff transacional disponivel.", + "sqlEditor.mutation.estimate.none": "Sem estimativa de mutacao disponivel.", + "sqlEditor.mutation.estimate.value": "Linhas afetadas estimadas: {0}", + "sqlEditor.mutation.estimate.unavailable": "Nao foi possivel estimar as linhas afetadas automaticamente.", + "sqlEditor.tab.closePending": "Alteracoes nao salvas detectadas. Confirme o fechamento da aba.", + "sqlEditor.tab.noPendingClose": "Nao ha fechamento de aba pendente.", + "sqlEditor.tab.manyWarning": "Quantidade alta de abas: {0} abas abertas.", + "sqlEditor.mutation.pending.none": "Sem confirmacao de mutacao pendente.", + "sqlEditor.mutation.pending.required": "A mutacao exige confirmacao antes da execucao.", + "sqlEditor.message.empty": "Execute uma instrucao para ver mensagens.", + "sqlEditor.message.success": "Execucao concluida com sucesso.", + "sqlEditor.result.summary.empty": "Linhas: - Tempo: -", + "sqlEditor.result.summary": "Linhas: {0} Tempo: {1} ms", + "sqlEditor.file.save.canceled": "Salvamento cancelado.", + "sqlEditor.file.save.noPath": "Nenhum caminho de destino selecionado.", + "sqlEditor.file.save.success": "Arquivo SQL salvo.", + "sqlEditor.file.save.failed": "Falha ao salvar arquivo SQL.", + "sqlEditor.file.open.failed": "Falha ao abrir arquivo SQL.", + "sqlEditor.file.open.notFound": "O arquivo SQL selecionado nao foi encontrado.", + "sqlEditor.file.open.success": "Arquivo SQL aberto.", + "sqlEditor.status.executing": "Executando SQL...", + "sqlEditor.status.executingScript": "Executando script SQL...", + "sqlEditor.status.executingStep": "Executando {0}/{1}...", + "sqlEditor.status.canceling": "Cancelando execucao...", + "sqlEditor.status.executingConfirmedMutation": "Executando mutacao confirmada...", + "sqlEditor.status.mutationCanceled": "Execucao da mutacao cancelada.", + "sqlEditor.detail.statementNotExecuted": "A instrucao nao foi executada.", + "sqlEditor.status.success": "Execucao concluida com sucesso.", + "sqlEditor.detail.rowsAndTime": "{0} linha(s) em {1} ms.", + "sqlEditor.status.canceled": "Execucao cancelada.", + "sqlEditor.status.failed": "Falha na execucao.", + "sqlEditor.status.confirmationRequired": "Confirmacao necessaria antes da execucao.", + "sqlEditor.error.mutationConfirmationRequired": "Confirmacao de mutacao necessaria.", + "sqlEditor.result.tabTitle": "Resultado {0}", + "sqlEditor.tab.closeRequiresConfirmation": "O fechamento da aba exige confirmacao.", + "sqlEditor.tab.unsavedDetail": "Esta aba possui alteracoes nao salvas.", + "sqlEditor.tab.closed": "Aba fechada.", + "sqlEditor.tab.closeCanceled": "Fechamento da aba cancelado.", + "sqlEditor.tab.closeCanceledDetail": "Aba com alteracoes nao salvas mantida aberta.", + "sqlEditor.error.noStatementSelected": "Nenhuma instrucao SQL selecionada para execucao.", + "sqlEditor.error.noConnection": "Nenhuma conexao ativa para execucao SQL.", + "sqlEditor.error.executionCanceled": "A execucao SQL foi cancelada.", + "sqlEditor.tab.scriptTitle": "Script {0}", + "sqlEditor.guard.delete.noWhere.message": "DELETE sem WHERE pode remover todas as linhas.", + "sqlEditor.guard.delete.noWhere.recommendation": "Adicione uma clausula WHERE restritiva antes de executar.", + "sqlEditor.guard.delete.trivialWhere.message": "DELETE possui uma clausula WHERE trivialmente verdadeira.", + "sqlEditor.guard.delete.trivialWhere.recommendation": "Use um filtro seletivo para atingir apenas as linhas desejadas.", + "sqlEditor.guard.update.noWhere.message": "UPDATE sem WHERE pode afetar todas as linhas.", + "sqlEditor.guard.update.noWhere.recommendation": "Adicione uma clausula WHERE restritiva antes de executar.", + "sqlEditor.guard.update.trivialWhere.message": "UPDATE possui uma clausula WHERE trivialmente verdadeira.", + "sqlEditor.guard.update.trivialWhere.recommendation": "Use um filtro seletivo para atingir apenas as linhas desejadas.", + "sqlEditor.guard.insert.noColumnList.message": "INSERT sem lista explicita de colunas e fragil diante de mudancas de schema.", + "sqlEditor.guard.insert.noColumnList.recommendation": "Prefira INSERT INTO tabela(col1, col2, ...) VALUES (...).", + "sqlEditor.guard.ddl.message": "Uma instrucao DDL pode causar mudancas estruturais no banco de dados.", + "sqlEditor.guard.ddl.recommendation": "Confirme a execucao apenas quando as mudancas de schema forem intencionais.", + "sqlEditor.diff.unavailable.noPreview": "Sem previa de diff transacional disponivel para esta instrucao.", + "sqlEditor.diff.unavailable.parseError": "Nao foi possivel identificar o alvo da mutacao para gerar a previa de diff transacional.", + "sqlEditor.diff.unavailable.connection": "Previa de diff transacional indisponivel por limitacoes de conexao ou consulta.", + "sqlEditor.diff.deleteSummary": "Previa de diff transacional (ROLLBACK garantido): tabela {0}, total de linhas antes {1}, afetadas {2}, total de linhas depois {3}.", + "sqlEditor.diff.updateSummary": "Previa de diff transacional (ROLLBACK garantido): tabela {0}, total de linhas antes {1}, linhas candidatas afetadas {2}, total de linhas depois {3}.", + "sqlEditor.diff.unavailable.unsupportedStatement": "A previa de diff transacional atualmente suporta apenas UPDATE e DELETE.", + "sqlEditor.results.title": "Resultados", + "sqlEditor.results.filterWatermark": "Filtrar resultados", + "sqlEditor.results.rows.countZero": "0 linhas", + "sqlEditor.results.rows.countSingle": "{0} linhas", + "sqlEditor.results.rows.countFiltered": "{0} de {1} linhas", + "sqlEditor.results.context.copyCell": "Copiar célula", + "sqlEditor.results.context.copyRow": "Copiar linha", + "sqlEditor.results.context.hideColumn": "Ocultar coluna", + "sqlEditor.results.context.showAllColumns": "Mostrar todas as colunas", + "sqlEditor.sidebar.messages": "MENSAGENS", + "sqlEditor.sidebar.history": "HISTÓRICO", + "sqlEditor.sidebar.connection.none": "Sem conexão", + "sqlEditor.sidebar.connection.connectHint": "Conecte-se para carregar metadados e executar consultas.", + "sqlEditor.sidebar.connection.connect": "Conectar", + "sqlEditor.sidebar.schema.searchWatermark": "Buscar tabela ou coluna", + "sqlEditor.sidebar.schema.reloadTooltip": "Recarregar metadados", + "sqlEditor.sidebar.schema.connectHint": "Conecte-se para visualizar o schema completo.", + "sqlEditor.history.searchWatermark": "Buscar no histórico", + "sqlEditor.history.navigationHint": "Setas percorrem o histórico, Enter executa selecionado e Esc limpa a busca.", + "sqlEditor.history.noSearchResults": "Nenhum item encontrado para a busca.", + "sqlEditor.history.use": "Usar", + "sqlEditor.history.copy": "Copiar", + "sqlEditor.history.run": "Executar", + "sqlEditor.history.copiedFromHistory": "SQL copiado do histórico.", + "sqlEditor.toast.scriptSuccessTitle": "Script executado.", + "sqlEditor.toast.scriptSuccessDetail": "{0} instrução(ões) executada(s) com sucesso.", + "sqlEditor.toast.scriptWarningTitle": "Script executado com falhas.", + "sqlEditor.toast.scriptWarningDetail": "{0} de {1} instrução(ões) falharam.", + "sqlEditor.toast.resultErrorTitle": "Falha ao executar instrução.", + "sqlEditor.toast.resultSuccessTitle": "Execução concluída com sucesso.", + "sqlEditor.export.action": "Exportar relatório", + "sqlEditor.openSql.pickerTitle": "Abrir Arquivo SQL", + "sqlEditor.saveSql.fileType": "Arquivos SQL", + "sqlEditor.saveSql.pickerTitle": "Salvar Arquivo SQL", + "sqlEditor.export.pickerTitle": "Exportar Dados SQL", + "sqlEditor.export.status.noResultTitle": "Nenhum resultado de execucao disponivel para exportacao.", + "sqlEditor.export.status.noResultDetail": "Execute uma consulta primeiro.", + "sqlEditor.export.status.successTitle": "Relatorio exportado.", + "sqlEditor.export.status.failedTitle": "Falha ao exportar relatorio.", + "sqlEditor.export.fileType.html": "Arquivo HTML", + "sqlEditor.export.fileType.json": "Arquivo JSON", + "sqlEditor.export.fileType.csv": "Arquivo CSV", + "sqlEditor.export.fileType.xlsx": "Pasta de Trabalho Excel", + "sqlEditor.export.defaultFileBase": "relatorio", + "sqlEditor.export.defaultTitle": "Relatorio SQL", + "sqlEditor.export.error.typeRequired": "Um tipo de relatorio deve ser selecionado antes da exportacao.", + "sqlEditor.export.type.html.title": "Relatorio HTML completo", + "sqlEditor.export.type.html.description": "Artefato HTML autonomo e orientado a SQL para auditoria offline.", + "sqlEditor.export.type.json.title": "Contrato de execucao JSON", + "sqlEditor.export.type.json.description": "Payload legivel por maquina com SQL, metadados e resultado da execucao.", + "sqlEditor.export.type.csv.title": "Exportacao de dados CSV", + "sqlEditor.export.type.csv.description": "Apenas dados tabulares do resultado, ideal para planilhas.", + "sqlEditor.export.type.xlsx.title": "Exportacao de pasta Excel", + "sqlEditor.export.type.xlsx.description": "Pasta de trabalho com os dados tabulares do resultado.", + "sqlEditor.export.dialog.windowTitle": "Exportar Dados SQL", + "sqlEditor.export.dialog.title": "Exportar Dados SQL", + "sqlEditor.export.dialog.subtitle": "Escolha o formato e os metadados do artefato antes de exportar.", + "sqlEditor.export.dialog.confirm": "Exportar", + "sqlEditor.export.dialog.fileNameWatermark": "relatorio.html", + "sqlEditor.export.dialog.titleWatermark": "Relatorio SQL", + "sqlEditor.export.dialog.descriptionWatermark": "Contexto adicional para auditoria e compartilhamento.", + "sqlEditor.export.dialog.section.reportType": "TIPO DE RELATORIO", + "sqlEditor.export.dialog.section.fileName": "NOME DO ARQUIVO", + "sqlEditor.export.dialog.section.reportTitle": "TITULO", + "sqlEditor.export.dialog.section.description": "DESCRICAO", + "sqlEditor.export.dialog.section.options": "OPCOES", + "sqlEditor.export.option.includeSchema": "Incluir schema de saida", + "sqlEditor.export.option.includeNodeDetails": "Incluir placeholders de no/conexao no JSON", + "sqlEditor.export.option.includeMetadata": "Incluir metadados opcionais", + "sqlEditor.export.option.useDashForEmpty": "Usar '-' para campos vazios", + "sqlEditor.export.badge.offline": "PRONTO PARA OFFLINE", + "sqlEditor.export.badge.structured": "PAYLOAD ESTRUTURADO", + "sqlEditor.export.badge.dataOnly": "SOMENTE DADOS" +} diff --git a/src/DBWeaver.UI/Assets/Localization/ru-RU.json b/src/AkkornStudio.UI/Assets/Localization/ru-RU.json similarity index 98% rename from src/DBWeaver.UI/Assets/Localization/ru-RU.json rename to src/AkkornStudio.UI/Assets/Localization/ru-RU.json index 790c6894..1dda0c27 100644 --- a/src/DBWeaver.UI/Assets/Localization/ru-RU.json +++ b/src/AkkornStudio.UI/Assets/Localization/ru-RU.json @@ -1,1087 +1,1087 @@ -{ - "main.brand": "DBWeaver", - "main.tab.query1": "Query 1", - "main.new": "New", - "main.open": "Open", - "main.save": "Save", - "main.history": "History", - "main.layout": "Layout", - "main.preview": "Preview", - "main.undo": "Undo", - "main.redo": "Redo", - "main.cleanupOrphans": "Cleanup orphan nodes", - "main.autoFixAliasNaming": "Auto-fix alias naming", - "main.autoLayoutCanvas": "Auto layout canvas", - "main.toggleDataPreview": "Toggle data preview", - "main.language": "Language", - "main.restore.prompt": "Previous session found — restore the last canvas?", - "main.restore.button": "Restore session", - "main.cteEditor.editingPrefix": "Editing CTE: ", - "main.cteEditor.backToCanvas": "Back to Canvas", - "main.cteEditor.exitA11y": "Exit CTE editor", - "main.viewEditor.editingPrefix": "DDL > View: ", - "main.viewEditor.backToCanvas": "Back to DDL", - "main.viewEditor.exitA11y": "Exit view editor", - "connection.title": "Connection Manager", - "connection.subtitle": "Настраивайте, проверяйте и активируйте подключения, не прерывая поток", - "connection.none": "No connection", - "connection.active": "ACTIVE", - "connection.health.online": "Online", - "connection.health.degraded": "Degraded", - "connection.health.offline": "Offline", - "connection.tooltip.none": "No active connection — click to manage", - "connection.ping": "Ping", - "connection.saved": "SAVED CONNECTIONS", - "connection.new": "New Connection", - "connection.selectOrCreate": "Select a connection or create a new one", - "connection.name": "Connection Name", - "connection.provider": "Provider", - "connection.host": "Host", - "connection.port": "Port", - "connection.database": "Database", - "connection.sqlitePath": "SQLite Path", - "connection.sqliteBrowse": "Browse", - "connection.sqliteCreate": "Create", - "connection.username": "Username", - "connection.password": "Password", - "connection.timeout": "Timeout (seconds)", - "connection.test": "Test", - "connection.save": "Save", - "connection.connect": "Connect", - "connection.action.testConnection": "Test connection", - "connection.action.saveConnection": "Save connection", - "connection.action.connectConnection": "Connect connection", - "connection.status.connecting": "Подключение...", - "connection.status.connected": "Подключено", - "connection.status.testing": "Проверка...", - "connection.status.failedPrefix": "Сбой подключения", - "connection.status.metadataUnavailable": "Сбой подключения: метаданные недоступны.", - "connection.status.highLatency": "высокая задержка", - "connection.watermark.name": "Моя Prod БД", - "connection.watermark.host": "localhost", - "connection.watermark.port": "5432", - "connection.watermark.database": "database_name", - "connection.watermark.username": "user", - "connection.watermark.password": "••••••••", - "connection.watermark.timeout": "30", - "main.connectingDb": "Connecting to database...", - "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", - "status.nodesSeparator": " nodes · ", - "status.connectionsSuffix": " connections", - "status.undo": "Undo: ", - "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", - "connection.disconnect": "Disconnect", - "connection.action.disconnectConnection": "Disconnect connection", - "connectionTab.active": "ACTIVE CONNECTION", - "connectionTab.none": "No active connection", - "connectionTab.saved": "SAVED CONNECTIONS", - "connectionTab.new": "+ New Connection", - "schema.database": "DATABASE", - "schema.search": "Search tables, columns...", - "schema.loading": "Searching tables, columns...", - "schema.noConnection": "No Connection", - "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", - "schema.emptyNoTables": "No tables found", - "fileHistory.title": "Save/Load Version History", - "fileHistory.reload": "Reload", - "fileHistory.restoreSelected": "Restore selected", - "fileHistory.empty": "No local versions yet", - "fileHistory.emptyHint": "Save this file to generate version history.", - "preview.title": "Data Preview", - "preview.subtitle": "Review data and diagnostics before continuing", - "preview.run": "Run", - "preview.cancel": "Cancel", - "preview.tab.preview": "Preview", - "preview.tab.sql": "SQL", - "preview.close": "Close preview", - "preview.running": "Running preview query… ", - "preview.clickCancel": "Click Cancel to stop", - "preview.cancelled": "Query cancelled", - "preview.runAgain": "Press Run to execute again", - "preview.failed": "Preview execution failed", - "preview.technical": "TECHNICAL DETAILS", - "preview.noData": "No data yet", - "preview.f3Hint": "Press F3 or Space to run the current query", - "sqlImporter.title": "Import SQL to Graph", - "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", - "sqlImporter.sqlStatement": "SQL STATEMENT", - "sqlImporter.supported": "Supported: ", - "sqlImporter.import": "Import", - "sqlImporter.report": "CONVERSION REPORT", - "sqlEditor.mutation.dialogTitle": "Подтверждение изменения", - "sqlEditor.mutation.dialogSubtitle": "Проверьте влияние перед подтверждением выполнения", - "search.empty": "No nodes found", - "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", - "search.shortcut": "⇧A", - "search.spawn": "Spawn", - "commandPalette.empty": "Нет команд, соответствующих поиску", - "commandPalette.search": "Поиск команд", - "commandPalette.shortcut": "CTRL+SHIFT+P", - "commandPalette.execute": "Выполнить", - "context.editCte": "Edit Selected CTE", - "context.editViewSubcanvas": "Edit View Subcanvas", - "explain.title": "Explain Plan", - "explain.sql": "SQL", - "explain.option.analyze": "Анализ", - "explain.option.buffers": "Буферы", - "explain.badge.simulated": "СИМУЛИРОВАНО", - "explain.timing.planning": "Планирование:", - "explain.timing.execution": "Выполнение:", - "explain.section.snapshotComparison": "Сравнение снимков", - "explain.section.indexRecommendations": "Рекомендации по индексам", - "explain.section.history": "История", - "explain.detail.estimated": "Оценочные", - "explain.detail.actual": "Фактические", - "explain.detail.error": "Ошибка", - "explain.detail.time": "Время", - "explain.detail.loops": "Циклы", - "explain.rerun": "Re-run", - "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", - "explain.running": "Running EXPLAIN…", - "explain.failed": "⚠ EXPLAIN failed", - "explain.noPlan": "No plan yet", - "explain.rerunHint": "Press Re-run to execute EXPLAIN", - "explain.header.operation": "ОПЕРАЦИЯ", - "explain.header.cost": "СТОИМОСТЬ", - "explain.header.rows": "СТРОКИ", - "explain.header.alert": "ПРЕДУПРЕЖДЕНИЕ", - "explain.mode.list": "Список", - "explain.mode.tree": "Дерево", - "explain.action.snapshot": "Сохранить снимок", - "explain.action.copyJson": "Копировать JSON", - "explain.action.copyText": "Копировать текст", - "explain.action.saveJson": "Сохранить .json", - "explain.action.openDalibo": "Открыть в Dalibo", - "explain.legend.seqscan": "SEQ SCAN — full table read, no index", - "explain.legend.sort": "SORT — in-memory sort", - "explain.legend.hash": "HASH — hash join", - "explain.escClose": "Esc to close", - "flowVersion.title": "Flow Version History", - "flowVersion.subtitle": "Create checkpoints, compare versions and restore", - "flowVersion.watermark": "Checkpoint label (optional)…", - "flowVersion.saveCheckpoint": "Save Checkpoint", - "flowVersion.compareMode": "Compare Mode", - "flowVersion.selectBase": "Select BASE version (from):", - "flowVersion.clickAny": "Then click any version in the list below to compare.", - "flowVersion.noCheckpoints": "No checkpoints yet", - "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", - "flowVersion.restore": "Restore", - "flowVersion.diffResults": "Diff Results", - "benchmark.title": "Query Benchmark", - "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", - "benchmark.sql": "SQL being benchmarked", - "benchmark.runLabel": "Run label", - "benchmark.runLabelWatermark": "Run 1", - "benchmark.iterations": "Iterations (1–100)", - "benchmark.warmup": "Warm-up passes (0–10)", - "benchmark.interval": "Interval between runs (ms)", - "benchmark.run": "Run Benchmark", - "benchmark.cancel": "Cancel", - "benchmark.clearHistory": "Clear History", - "benchmark.latest": "LATEST RESULT", - "benchmark.avg": "AVG", - "benchmark.median": "MEDIAN", - "benchmark.min": "MIN", - "benchmark.max": "MAX", - "benchmark.iterationsAt": " iterations · run at ", - "benchmark.history": "HISTORY", - "benchmark.header.label": "Метка", - "benchmark.header.avg": "Средн.", - "benchmark.header.median": "Медиана", - "benchmark.header.min": "Мин", - "benchmark.header.max": "Макс", - "benchmark.itersSuffix": " iters", - "diagnostics.title": "App Diagnostics", - "diagnostics.run": "Run", - "diagnostics.running": "Running checks…", - "diagnostics.ok": "OK", - "diagnostics.warning": "Warning", - "diagnostics.error": "Error", - "diagnostics.tooltip.rerun": "Re-run all checks", - "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", - "diagnostics.tooltip.close": "Close (Esc)", - "autoJoin.title": "Auto-Join Suggestions", - "autoJoin.titleForTable": "Auto-Join suggestions for {0}", - "autoJoin.acceptAll": "Accept All", - "autoJoin.accept": "Accept", - "autoJoin.skip": "Skip", - "autoJoin.allHandled": "All suggestions handled", - "autoJoin.joinKeyword": "JOIN", - "autoJoin.confidence.fkConstraint": "FK Constraint", - "autoJoin.confidence.fkReverse": "FK (Reverse)", - "autoJoin.confidence.namingMatch": "Naming Match", - "autoJoin.confidence.weakMatch": "Weak Match", - "autoJoin.runSelected": "Auto-Join Selected", - "autoJoin.noSimilarityTitle": "No automatic join found", - "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", - "autoJoin.appliedTitle": "Auto-join applied", - "autoJoin.manual.title": "Create Manual Join", - "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", - "autoJoin.manual.leftColumn": "Left column", - "autoJoin.manual.rightColumn": "Right column", - "autoJoin.manual.joinType": "Join type", - "autoJoin.manual.operator": "Operator", - "autoJoin.manual.confirm": "Create join", - "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", - "autoJoin.manualJoinCreatedTitle": "Manual join created", - "autoJoin.manualJoinFailedTitle": "Manual join could not be created", - "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", - "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", - "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", - "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", - "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", - "property.outputAlias": "OUTPUT ALIAS", - "property.sourceAlias": "SOURCE ALIAS", - "property.aliasWatermark": "e.g. MyColumn (optional)", - "property.parameters": "PARAMETERS", - "property.enabled": "Enabled", - "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", - "property.dateWatermark": "YYYY-MM-DD or leave empty", - "property.apply": "Apply", - "property.inputPins": "INPUT PINS", - "property.outputPins": "OUTPUT PINS", - "property.sqlTrace": "SQL TRACE", - "property.live": "live", - "node.numericValue": "Numeric Value", - "node.stringValue": "String Value", - "node.enterText": "Enter text", - "node.datetimeValue": "DateTime Value", - "node.valueLabel": "Value:", - "node.noInputs": "No inputs", - "node.loadingSample": "Loading sample…", - "node.previewFailed": "⚠ Preview failed", - "node.sampleRowsHint": "5 sample rows · demo data", - "sidebar.tab.nodes": "Узлы", - "sidebar.tab.connection": "Подключение", - "sidebar.tab.schema": "Схема", - "sidebar.tab.diagnostics": "Диагностика", - "sidebar.addNode": "+ Add Node (⇧A)", - "sidebar.previewF3": "Preview (F3)", - "nodesList.search": "Search nodes...", - "search.watermark": "Search nodes… (Esc to close)", - "search.snippets": "★ SNIPPETS", - "commandPalette.watermark": "Выполнить команду… (Esc для закрытия)", - "tooltip.newCanvas": "New canvas (Ctrl+N)", - "tooltip.openCanvas": "Open canvas (Ctrl+O)", - "tooltip.saveCanvas": "Save canvas (Ctrl+S)", - "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", - "tooltip.zoomOut": "Zoom out (Ctrl+-)", - "tooltip.zoomIn": "Zoom in (Ctrl++)", - "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", - "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", - "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", - "tooltip.dataPreview": "Data preview (F3)", - "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", - "tooltip.appDiagnostics": "App Diagnostics (self-check)", - "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", - "tooltip.cancelRunningQuery": "Cancel the running query", - "tooltip.closeEsc": "Close (Esc)", - "tooltip.recheckConnectionHealth": "Re-check connection health", - "tooltip.deleteConnection": "Delete connection", - "tooltip.testConnection": "Test connection", - "tooltip.saveConnection": "Save connection", - "tooltip.activateConnection": "Activate this connection", - "tooltip.toggleDataSamplePreview": "Toggle data sample preview", - "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", - "tooltip.copySql": "Copy SQL to clipboard", - "tooltip.formatSql": "Format SQL", - "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", - "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", - "tooltip.switchToQueryMode": "Switch to Query canvas", - "tooltip.switchToDdlMode": "Switch to DDL canvas", - "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", - "tooltip.pins.inputs": "Inputs", - "tooltip.pins.outputs": "Outputs", - "tooltip.pins.none": "None", - "tooltip.tableColumns": "Columns", - "tooltip.tableColumns.none": "No detailed columns", - "window.minimize": "Minimize window", - "window.maximizeRestore": "Maximize/restore window", - "window.close": "Close window", - "menu.newDiagram": "New diagram", - "menu.openFile": "Open file", - "menu.save": "Save", - "menu.fileHistory": "File history", - "menu.shortcuts": "Keyboard shortcuts", - "menu.settings": "Settings", - "menu.importDdlSchema": "Import DDL schema", - "menu.viewDdlSql": "View DDL SQL", - "menu.executeDdl": "Execute DDL", - "menu.backToStart": "Back to start", - "toast.ddlExecuteFailed": "Failed to execute DDL.", - "toast.ddlOpenFailed": "Failed to open DDL SQL.", - "toast.ddlImportFailed": "Failed to import schema into DDL.", - "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", - "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", - "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", - "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", - "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", - "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", - "toast.ddlTableImported": "Table imported into the DDL canvas.", - "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", - "toast.ddlNoActiveConnection": "No active connection to execute DDL.", - "toast.ddlExecutedSuccess": "DDL executed successfully.", - "toast.ddlExecutedWithIssues": "DDL executed with issues.", - "toast.switchToDdl": "Switch to DDL mode to generate SQL.", - "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", - "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", - "toast.previewOpenFailed": "Failed to open preview.", - "tab.switchFailed": "Failed to switch tab: {0}", - "settings.status.darkApplied": "Dark theme applied.", - "settings.status.lightApplied": "Light theme applied.", - "settings.status.snapUpdated": "Snap updated: {0}.", - "settings.status.languageToggled": "Language toggled.", - "settings.status.languageSelected": "Language selected: {0}.", - "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", - "settings.section.appearance.title": "Themes", - "settings.section.languageRegion.title": "Язык и регион", - "settings.section.dateTime.title": "Дата и время", - "settings.section.keyboard.title": "Горячие клавиши", - "settings.section.privacy.title": "Конфиденциальность", - "settings.section.notification.title": "Уведомления", - "settings.section.accessibility.title": "Доступность", - "settings.section.default.title": "Настройки", - "settings.section.appearance.subtitle": "Выберите стиль или настройте тему", - "settings.section.languageRegion.subtitle": "Управляйте языком и региональным форматированием", - "settings.section.keyboard.subtitle": "Настройте сочетания клавиш для command palette и выполнения на canvas.", - "settings.section.wip.subtitle": "В разработке.", - "settings.section.default.subtitle": "Настройки приложения", - "settings.general": "General", - "settings.nav.appearance": "Appearance", - "settings.theme.light": "Светлая тема", - "settings.theme.dark": "Тёмная тема", - "settings.theme.system": "Системная тема", - "settings.gridSnap.title": "Grid Snap", - "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", - "settings.language.subtitle": "Выберите язык приложения.", - "settings.language.toggle": "Сменить язык", - "settings.language.option.ptBR": "Португальский (Бразилия)", - "settings.language.option.enUS": "Английский (США)", - "settings.language.option.esES": "Испанский (Испания)", - "settings.language.option.ruRU": "Русский", - "settings.language.option.jaJP": "Японский", - "settings.language.option.zhTW": "Традиционный китайский", - "settings.themeJson.title": "JSON темы", - "settings.themeJson.subtitle": "Вставьте JSON темы, примените и сохраните сразу.", - "settings.themeJson.apply": "Применить JSON", - "settings.themeJson.restoreDefault": "Восстановить тему по умолчанию", - "mode.query": "Query", - "mode.ddl": "DDL", - "sidebar.left.close": "Close left sidebar", - "sidebar.left.open": "Reopen left sidebar", - "sidebar.right.close": "Close right sidebar", - "sidebar.right.open": "Reopen right sidebar", - "connection.completedTitle": "Connection completed", - "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", - "connection.close": "Close connection manager", - "connection.refreshHealth": "Refresh connection health", - "common.details": "Details", - "common.cancel": "Cancel", - "common.keep": "Keep", - "common.clear": "Clear", - "zoom.out": "Zoom out", - "zoom.in": "Zoom in", - "zoom.fit": "Fit zoom to screen", - "zoom.level": "Zoom level", - "settings.theme.mode": "Режим темы", - "diagnostics.category.canvas": "Canvas Integrity", - "diagnostics.category.output": "Output & Execution", - "diagnostics.category.session": "Session & Safety", - "diagnostics.category.notice": "Runtime Notices", - "diagnostics.summary.ok": "All systems OK", - "diagnostics.summary.warningCount": "{0} warning(s) detected", - "diagnostics.summary.errorCount": "{0} error(s) detected", - "diagnostics.canvasMigration": "Canvas Migration", - "diagnostics.recommendation.resaveFile": "Re-save the file to update it to the latest schema version.", - "diagnostics.canvasState.name": "Canvas State", - "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", - "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", - "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", - "diagnostics.validation.name": "Validation Errors", - "diagnostics.validation.recommendation": "Fix highlighted nodes before running output preview", - "diagnostics.validation.errorWithWarnings": "{0} error(s) and {1} warning(s) in the graph", - "diagnostics.validation.warningOnly": "{0} warning(s) in the graph", - "diagnostics.validation.none": "No validation issues", - "diagnostics.orphan.name": "Orphan Nodes", - "diagnostics.orphan.recommendation": "Use the orphan cleanup action to remove unused nodes", - "diagnostics.orphan.count": "{0} node(s) not connected to any output", - "diagnostics.orphan.none": "No orphan nodes detected", - "diagnostics.naming.name": "Naming Conventions", - "diagnostics.naming.recommendation": "Use auto-fix alias naming when conformance is below 100%", - "diagnostics.naming.conformance": "Naming conformance: {0}%", - "diagnostics.naming.ok": "All aliases follow naming conventions (100%)", - "diagnostics.queryCompilation.name": "Live SQL Compilation", - "diagnostics.queryCompilation.recommendation": "Review SQL diagnostics in output when errors/warnings are reported.", - "diagnostics.queryCompilation.errorFallback": "Live SQL compilation reported errors.", - "diagnostics.queryCompilation.warningCounts": "{0} diagnostic item(s), {1} guardrail warning(s).", - "diagnostics.queryCompilation.ok": "Live SQL compiled without diagnostics.", - "diagnostics.previewSafety.name": "Preview Safety", - "diagnostics.previewSafety.recommendation": "Preview executes read-only statements only.", - "diagnostics.previewSafety.blocked": "Current SQL is mutating and blocked by Safe Preview mode.", - "diagnostics.previewSafety.ok": "Preview safety checks passed.", - "diagnostics.previewExecution.name": "Preview Execution", - "diagnostics.previewExecution.recommendation": "Run preview and inspect diagnostics for execution/runtime errors.", - "diagnostics.previewExecution.failed": "Preview execution failed.", - "diagnostics.previewExecution.cancelled": "Preview execution was cancelled.", - "diagnostics.previewExecution.done": "{0} row(s) in {1}ms.", - "diagnostics.previewExecution.none": "No preview execution issues detected.", - "diagnostics.ddlCompilation.name": "DDL Compilation", - "diagnostics.ddlCompilation.recommendation": "Fix DDL compile diagnostics before execution.", - "diagnostics.ddlCompilation.failed": "DDL compilation failed.", - "diagnostics.ddlCompilation.warningCount": "{0} warning(s) reported by DDL compiler.", - "diagnostics.ddlCompilation.ok": "DDL compilation succeeded.", - "diagnostics.ddlOutput.name": "DDL Output", - "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", - "diagnostics.ddlOutput.none": "No DDL statements generated yet.", - "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", - "diagnostics.undo.name": "Undo History", - "diagnostics.undo.recommendation": "History is in-memory only; save your canvas regularly.", - "diagnostics.undo.saved": "Canvas is saved (no unsaved changes).", - "diagnostics.undo.unsavedDeep": "Unsaved changes with {0} undo steps.", - "diagnostics.undo.unsaved": "Unsaved changes - {0} undo step(s) available.", - "diagnostics.report.title": "DBWeaver - Diagnostic Report", - "diagnostics.report.generated": "Generated", - "diagnostics.report.overall": "Overall", - "diagnostics.report.details": "Details", - "diagnostics.report.recommendation": "Recommendation", - "diagnostics.report.lastCheck": "Last Check", - "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", - "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", - "preview.providerLabel": "Provider", - "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", - "preview.schemaAnalysis.run": "Run Analysis", - "preview.schemaAnalysis.cancel": "Cancel", - "preview.schemaAnalysis.issues": "Issues", - "preview.schemaAnalysis.clearFilters": "Clear Filters", - "preview.schemaAnalysis.severity": "Severity", - "preview.schemaAnalysis.rule": "Rule", - "preview.schemaAnalysis.minConfidence": "Min Confidence", - "preview.schemaAnalysis.tableFilter": "Table Filter", - "preview.schemaAnalysis.tableFilterWatermark": "schema.table", - "preview.schemaAnalysis.details": "Details", - "preview.schemaAnalysis.evidence": "Evidence", - "preview.schemaAnalysis.suggestions": "Suggestions", - "preview.schemaAnalysis.ruleDiagnostics": "Rule Diagnostics", - "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", - "preview.schemaAnalysis.copySql": "Copy SQL", - "preview.schemaAnalysis.applyToCanvas": "Apply to Canvas", - "preview.schemaAnalysis.summary.issues": "Issues:", - "preview.schemaAnalysis.summary.rawPrefix": "(raw:", - "preview.schemaAnalysis.summary.critical": "| Critical:", - "preview.schemaAnalysis.summary.warning": "| Warning:", - "preview.schemaAnalysis.summary.info": "| Info:", - "preview.schemaAnalysis.state.metadataUnavailable": "Metadata unavailable for structural analysis.", - "preview.schemaAnalysis.state.cancelled": "Analysis cancelled by the user.", - "preview.schemaAnalysis.state.partialTimeout": "Analysis finished partially due to timeout.", - "preview.schemaAnalysis.state.failed": "Structural analysis failed.", - "preview.schemaAnalysis.state.empty": "No inferable structural issue was detected.", - "preview.schemaAnalysis.state.noFilterMatch": "No issue matches the selected filters.", - "preview.schemaAnalysis.state.noIssueSelected": "No issue selected.", - "preview.schemaAnalysis.state.noSqlCandidate": "No SQL candidate available.", - "preview.schemaAnalysis.actionBlockedTooltip": "Action unavailable for the current risk level or capability.", - "common.navigate": "Navigate", - "common.close": "Close", - "common.esc": "Esc", - "common.ms": "ms", - "common.zero": "0", - "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", - "nodesList.empty": "No nodes found", - "nodesList.emptyHint": "Adjust the search term to explore available types", - "schema.emptyFiltered": "No objects found for the current filter", - "start.lastSnapshot": "Last snapshot", - "app.brandBadge": "VS", - "property.tab.properties": "Properties", - "property.tab.projectSettings": "Project Settings", - "property.nodeType": "NODE TYPE", - "property.selectNodeHint": "Select a node to edit its properties.", - "property.namingConventions": "Naming Conventions", - "property.aliasConvention": "Alias convention", - "property.enforceAliasNaming": "Enforce alias naming", - "property.warnReservedSql": "Warn on reserved SQL keywords", - "property.maxAliasLength": "Max alias length", - "property.maxAliasLengthDefault": "64", - "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", - "start.tips": "Tips", - "start.tips.quick": "Quick tips", - "start.tips.item1": "1. Click New Diagram to start from scratch.", - "start.tips.item2": "2. Use templates to speed up prototyping.", - "start.tips.item3": "3. Open saved connections to load real tables.", - "start.tips.shortcut": "Shortcut: CTRL+SHIFT+P opens the command palette.", - "start.workspace": "WORKSPACE", - "start.resumeTitle": "Continue where you left off", - "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", - "start.chip.quickFlow": "Quick flow", - "start.chip.templates": "Templates", - "start.chip.connections": "Connections", - "start.savedConnectionsTitle": "Saved Connections", - "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", - "start.noConnectionsTitle": "No connections configured yet", - "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", - "start.newConnection": "+ New Connection", - "start.recentProjectsTitle": "Recent Projects", - "start.searchRecent": "Search recent project...", - "start.quickActions": "Quick actions", - "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", - "start.noRecentTitle": "No recent projects yet", - "start.noRecentSubtitle": "Use the quick actions card above to get started.", - "start.exploreTemplates": "Explore templates", - "start.templatesFavoritesHint": "Favorites on top", - "start.favoriteTemplate": "Favorite template", - "node.columnSetPreview": "ColumnSet preview", - "node.view": "VIEW", - "node.tableDefinition": "Table Definition", - "node.join": "JOIN", - "node.window.addPartition": "Add PARTITION BY slot", - "node.window.removePartition": "Remove PARTITION BY slot", - "node.window.addOrder": "Add ORDER BY slot", - "node.window.removeOrder": "Remove ORDER BY slot", - "sql.keyword.select": "SELECT", - "sql.keyword.from": "FROM", - "sql.keyword.join": "JOIN", - "sql.keyword.where": "WHERE", - "sql.keyword.limit": "LIMIT", - "sqlImporter.close": "Close SQL importer", - "sqlImporter.report.imported": "Imported", - "sqlImporter.report.partial": "Partial", - "sqlImporter.report.skipped": "Skipped", - "benchmark.close": "Close benchmark", - "benchmark.p95": "P95", - "benchmark.n": "N", - "liveSql.safePreview": "SAFE PREVIEW MODE", - "liveSql.title": "LIVE SQL", - "liveSql.blocked": "BLOCKED", - "liveSql.copy": "Copy", - "liveSql.format": "Format", - "liveSql.benchmark": "Benchmark", - "liveSql.explain": "Explain", - "liveSql.actionsHint": "Performance tools", - "ddl.dialog.title": "Execute DDL", - "ddl.dialog.execute": "Execute", - "ddl.dialog.cancel": "Cancel", - "ddl.dialog.close": "Close", - "ddl.dialog.stopOnError": "Stop on first failure", - "ddl.dialog.confirmDestructive": "I confirm execution of destructive statements (DROP TABLE)", - "ddl.dialog.reviewBeforeRun": "Review the DDL script before confirming.", - "ddl.dialog.confirmQuestion": "Confirm DDL execution on the connected database?", - "ddl.dialog.irreversibleWarning": "This action can change the schema irreversibly.", - "ddl.dialog.mustConfirmDestructive": "Confirm destructive execution to continue.", - "ddl.dialog.executing": "Executing...", - "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", - "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", - "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", - "ddl.execute.result.failed": "Failed to execute DDL.", - "ddl.execute.result.cancelled": "Execution cancelled by the user.", - "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", - "context.deleteSingle": "Delete {0}", - "context.deleteMultiple": "Delete {0} nodes", - "context.bringForward": "Bring Forward (Ctrl+PgUp)", - "context.sendBackward": "Send Backward (Ctrl+PgDown)", - "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", - "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", - "context.normalizeLayers": "Normalize Layers", - "context.deleteWire": "Delete wire", - "context.addNode": "Add Node (Shift+A)", - "context.undoWithDescription": "Undo {0}", - "context.redo": "Redo", - "shortcuts.windowTitle": "Keyboard Shortcuts", - "shortcuts.headerTitle": "DBWeaver - Shortcuts", - "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", - "shortcuts.filterWatermark": "Filter shortcuts by key or action...", - "shortcuts.resultCount": "{0} shortcuts", - "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", - "shortcuts.noneFound": "No shortcuts found.", - "shortcuts.section.fileGeneral": "Файл и общее", - "shortcuts.section.editing": "Редактирование", - "shortcuts.section.canvasNavigation": "Холст и навигация", - "shortcuts.section.zoomPanPrecision": "Масштаб, панорамирование и точность", - "shortcuts.section.previewInspection": "Предпросмотр и инспекция", - "shortcuts.key.deleteOrBackspace": "Del или Backspace", - "shortcuts.key.middleDrag": "Средняя кнопка + перетаскивание", - "shortcuts.key.rightDrag": "Правая кнопка + перетаскивание", - "shortcuts.key.spaceDrag": "Пробел + перетаскивание", - "shortcuts.key.altLeftDrag": "Alt + перетаскивание левой кнопкой", - "shortcuts.key.arrows": "Стрелки", - "shortcuts.key.shiftArrows": "Shift + Стрелки", - "shortcuts.action.openShortcutScreen": "Открыть этот экран горячих клавиш", - "shortcuts.action.newCanvas": "Новый холст", - "shortcuts.action.openFile": "Открыть файл", - "shortcuts.action.save": "Сохранить", - "shortcuts.action.saveAs": "Сохранить как", - "shortcuts.action.commandPalette": "Палитра команд", - "shortcuts.action.undo": "Отменить", - "shortcuts.action.redo": "Повторить", - "shortcuts.action.selectAll": "Выделить всё", - "shortcuts.action.deleteSelection": "Удалить выделение", - "shortcuts.action.closeOverlayCancel": "Закрыть оверлеи / отменить действия", - "shortcuts.action.openNodeSearch": "Открыть поиск узлов", - "shortcuts.action.resetViewport": "Сбросить viewport", - "shortcuts.action.centerSelection": "Центрировать выделение", - "shortcuts.action.fitSelection": "Подогнать выделение", - "shortcuts.action.autoLayout": "Авторазмещение", - "shortcuts.action.toggleSnapToGrid": "Переключить привязку к сетке", - "shortcuts.action.bringForward": "На слой выше", - "shortcuts.action.sendBackward": "На слой ниже", - "shortcuts.action.bringToFront": "На передний план", - "shortcuts.action.sendToBack": "На задний план", - "shortcuts.action.zoomInOut": "Увеличить / уменьшить", - "shortcuts.action.pan": "Панорамирование", - "shortcuts.action.temporaryPan": "Временное панорамирование", - "shortcuts.action.alternatePan": "Альтернативное панорамирование", - "shortcuts.action.fineNudge": "Точный сдвиг выделения", - "shortcuts.action.fastNudge": "Быстрый сдвиг", - "shortcuts.action.togglePreview": "Переключить предпросмотр данных", - "shortcuts.action.explainPlan": "План выполнения", - "shortcuts.action.runPreview": "Запустить предпросмотр", - "shortcuts.action.connectionManager": "Менеджер подключений", - "shortcuts.action.flowVersionHistory": "История версий потока", - "shortcuts.resetAll": "Сбросить всё", - "shortcuts.customized": "Изменено", - "shortcuts.default": "По умолчанию", - "shortcuts.apply": "Применить", - "shortcuts.reset": "Сбросить", - "shortcuts.status.resetAllSuccess": "Все сочетания сброшены к значениям по умолчанию.", - "shortcuts.status.updated": "Сочетание обновлено.", - "shortcuts.status.reset": "Сочетание сброшено по умолчанию.", - "shortcuts.status.updateFailed": "Не удалось обновить сочетание.", - "toast.severity.success": "Success", - "toast.severity.warning": "Warning", - "toast.severity.error": "Error", - "toast.details.success": "Success Details", - "toast.details.warning": "Warning Details", - "toast.details.error": "Error Details", - "diagnostics.area.cteEditor": "CTE Editor", - "diagnostics.area.viewEditor": "View Editor", - "diagnostics.area.subEditor": "Sub-editor", - "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", - "diagnostics.recommendation.reloadFileIfNeeded": "Reload the file if needed.", - "diagnostics.viewEditor.exitFailed": "Could not exit: {0}", - "diagnostics.viewEditor.canvasIncomplete": "the canvas is incomplete.", - "diagnostics.viewEditor.exitRecommendation": "Connect a valid ResultOutput or use the discard command.", - "diagnostics.viewEditor.restoreParentFailed": "Failed to restore the parent canvas. The subgraph was discarded.", - "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", - "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", - "diagnostics.canvasMigration.openWarning": "Open: {0}", - "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", - "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", - "diagnostics.recommendation.resaveLatestSchema": "Review diagnostics and re-save the canvas to persist the latest schema.", - "diagnostics.recommendation.saveMigratedSchema": "Review diagnostics and save the canvas to persist the migrated schema.", - "file.saveDialog.title": "Save Canvas", - "file.saveDialog.suggestedName": "Query1", - "file.save.success": "Canvas saved successfully.", - "file.save.failedWithReason": "Save failed: {0}", - "file.openDialog.title": "Open Canvas", - "file.open.failedWithReason": "Open failed: {0}", - "file.open.success": "Canvas opened successfully.", - "file.open.successWithWarnings": "Canvas opened with warnings.", - "session.restore.failedWithReason": "Restore failed: {0}", - "session.restore.successWithWarnings": "Session restored with warnings.", - "session.restore.success": "Session restored successfully.", - "export.documentation.dialogTitle": "Export Flow Documentation", - "export.documentation.success": "Documentation exported successfully.", - "export.documentation.failed": "Documentation export failed.", - "export.failed.pathPermissionsHint": "Check file path and permissions.", - "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", - "export.dialogTitleByExtension": "Export as {0}", - "export.success": "Export completed successfully.", - "export.failed": "Export failed.", - "fileHistory.currentFile.none": "No file selected", - "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", - "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", - "fileHistory.status.countAvailable": "{0} local version(s) available.", - "fileHistory.restore.failedWithReason": "Restore failed: {0}", - "fileHistory.restore.successFrom": "Restored version from {0}.", - "preview.status.cancelled": "Cancelled", - "preview.status.error": "Error", - "preview.status.ready": "Ready", - "preview.runningWithMs": "Running... {0}ms", - "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", - "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", - "explain.errorWithReason": "Explain plan error: {0}", - "explain.noSql": "No SQL to explain. Build a query on the canvas first.", - "ddl.compilationFailed": "Compilation failed", - "ddl.compileErrorWithReason": "DDL compile error: {0}", - "command.undo.name": "Undo", - "command.undo.description": "Undo last action", - "command.redo.name": "Redo", - "command.redo.description": "Redo last undone action", - "command.addNode.name": "Add Node", - "command.addNode.description": "Open node search menu to add a node", - "command.bringForward.name": "Bring Forward", - "command.bringForward.description": "Move selected nodes one layer forward", - "command.sendBackward.name": "Send Backward", - "command.sendBackward.description": "Move selected nodes one layer backward", - "command.bringToFront.name": "Bring to Front", - "command.bringToFront.description": "Move selected nodes to top layer", - "command.sendToBack.name": "Send to Back", - "command.sendToBack.description": "Move selected nodes to bottom layer", - "command.normalizeLayers.name": "Normalize Layers", - "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", - "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", - "main.orphanSuffix": "Orphan(s)", - "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", - "main.namingPrefix": "Naming", - "fileHistory.compressedLabel": "Compressed:", - "schema.itemsSuffix": "item(s)", - "property.panel.title": "Properties", - "property.panel.multiSelected": "{0} nodes selected", - "sqlImporter.status.pasteSelect": "Paste a SELECT statement above, then click Import.", - "sqlImporter.status.inputTooLarge": "SQL input is too large ({0:N0} chars). Limit is {1:N0}. Split the query or increase the import limit.", - "sqlImporter.status.parsing": "Parsing SQL...", - "sqlImporter.status.done": "Done - {0} imported, {1} partial, {2} skipped.", - "sqlImporter.status.cancelledByUser": "Import cancelled by user.", - "sqlImporter.status.timeout": "Import timed out after {0:0.#}s. Try a smaller query or increase timeout.", - "sqlImporter.status.parseError": "Parse error: {0}", - "diagnostics.area.undoRedoTransaction": "Undo/Redo Transaction", - "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", - "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", - "node.preview.noCatalog": "No catalog available", - "connection.error.searchMenuNotInitialized": "search menu not initialized", - "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", - "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", - "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", - "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", - "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", - "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", - "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", - "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", - "diagnostics.area.connection": "Connection", - "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", - "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", - "undoRedo.transaction.unnamed": "unnamed transaction", - "benchmark.runLabelDefault": "Run 1", - "benchmark.runLabelPattern": "Run {0}", - "benchmark.status.failedWithReason": "Benchmark failed: {0}", - "benchmark.status.noSql": "No SQL to benchmark - build a query first.", - "benchmark.status.warmupProgress": "Warm-up {0}/{1}...", - "benchmark.status.iterationProgress": "Iteration {0}/{1}...", - "benchmark.status.done": "Done - {0}", - "benchmark.status.cancelled": "Benchmark cancelled.", - "app.windowTitle": "DBWeaver", - "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", - "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", - "sqlImporter.error.selectFromNotFound": "Could not find SELECT ... FROM in the query.", - "sqlImporter.error.fromClauseParseFailed": "Could not parse FROM clause.", - "sqlImporter.error.syntaxUnterminatedString": "Syntax error at line {0}, column {1}: unterminated string literal.", - "sqlImporter.error.missingClosingParenthesis": "missing closing ')'", - "sqlImporter.error.unexpectedClosingParenthesis": "unexpected ')'", - "sqlImporter.error.syntaxAtLineColumn": "Syntax error at line {0}, column {1}: {2}.", - "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", - "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", - "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", - "errorDiagnostics.connection.label": "Connection failed", - "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", - "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", - "errorDiagnostics.authorization.label": "Authorization error", - "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", - "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", - "errorDiagnostics.timeout.label": "Query timeout", - "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", - "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", - "errorDiagnostics.schema.label": "Schema error", - "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", - "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", - "errorDiagnostics.syntax.label": "SQL syntax error", - "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", - "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", - "errorDiagnostics.compatibility.label": "Compatibility error", - "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", - "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", - "errorDiagnostics.unknown.label": "Unexpected error", - "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", - "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", - "error.mainWindow.invalidDataContext": "DataContext MainWindow должен быть ShellViewModel.", - "error.mainWindow.canvasNotInitialized": "CanvasViewModel не был инициализирован.", - "error.mainWindow.ddlPreviewUnavailable": "DDL-предпросмотр недоступен для текущего canvas.", - "themeJson.editor.template": "{\n \"meta\": { \"name\": \"Пользовательская тема\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", - "themeJson.error.pasteBeforeApply": "Вставьте JSON темы перед применением.", - "themeJson.error.invalidJson": "Некорректный JSON: {0}", - "themeJson.error.emptyPayload": "Некорректный JSON: пустой payload.", - "themeJson.error.invalidTheme": "Некорректная тема: {0}", - "themeJson.error.appliedButSaveFailed": "Тема применена, но не удалось сохранить: {0}", - "themeJson.success.appliedAndSaved": "JSON-тема применена и сохранена.", - "themeJson.success.customRemoved": "Пользовательская тема удалена. Перезапустите приложение для полного возврата к теме по умолчанию.", - "themeJson.error.restoreDefaultFailed": "Не удалось восстановить тему по умолчанию: {0}", - "themeValidator.error.configNull": "Конфигурация темы пуста.", - "themeValidator.warning.noSections": "В теме нет разделов цветов или типографики; применять нечего.", - "themeValidator.warning.invalidColor": "{0} содержит некорректный цвет '{1}'. Этот ключ будет проигнорирован.", - "themeValidator.warning.sizeOutOfRange": "{0}={1} вне диапазона (8..48). Этот ключ будет проигнорирован.", - "queryExecutor.error.openConnectionMethodNotFound": "Не удалось найти метод OpenConnectionAsync в оркестраторе", - "queryExecutor.error.openConnectionInvokeFailed": "Не удалось вызвать OpenConnectionAsync", - "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}': SELECT представления нельзя визуально восстановить — отредактируйте вручную в subcanvas.", - "ddlImporter.error.tableNotFoundInMetadata": "Таблица '{0}' не найдена в текущих метаданных.", - "main.window.untitled": "Без названия", - "main.subEditor.noSeedProvided": "Для {0} не предоставлен seed подредактора.", - "main.layerOrder.bringToFront": "На передний план", - "main.layerOrder.sendToBack": "На задний план", - "main.layerOrder.bringForward": "Поднять слой", - "main.layerOrder.sendBackward": "Опустить слой", - "main.layerOrder.normalizeLayers": "Нормализовать слои", - "export.fileType.html": "Файлы HTML", - "export.fileType.json": "Файлы JSON", - "export.fileType.csv": "Файлы CSV", - "export.fileType.excel": "Файлы Excel", - "commandPalette.templatePrefix": "Шаблон: {0}", - "themeLoader.status.notFoundWithPath": "Файл темы не найден: {0}", - "themeLoader.status.deserializedNull": "JSON темы десериализован в null.", - "themeLoader.status.loaded": "JSON темы успешно загружен.", - "credential.error.ciphertextTooShort": "Блок шифртекста слишком короткий.", - "credential.error.dpapiWindowsOnly": "DPAPI доступен только в Windows.", - "credential.warning.loadVaultFailed": "Не удалось загрузить хранилище учетных данных {0}: {1}", - "credential.warning.persistVaultFailed": "Не удалось сохранить хранилище учетных данных {0}: {1}", - "snippetStore.warning.loadFailed": "Не удалось загрузить сниппеты из {0}: {1}", - "snippetStore.warning.saveFailed": "Не удалось сохранить сниппеты: {0}", - "flowVersionStore.warning.loadFailed": "Не удалось загрузить версии потока из {0}: {1}", - "flowVersionStore.warning.saveFailed": "Не удалось сохранить версии потока: {0}", - "queryExecutor.error.queryEmpty": "Запрос не может быть пустым", - "queryExecutor.error.providerNotSupported": "Провайдер {0} не поддерживается", - "queryExecutor.error.singleStatementOnly": "Предпросмотр принимает только один SQL-оператор.", - "queryExecutor.error.queryEmptyWithPeriod": "Запрос не может быть пустым.", - "queryExecutor.error.readOnlyOnly": "Режим предпросмотра поддерживает только SQL-запросы только для чтения.", - "queryExecutor.error.namedParametersNotSupported": "Режим предпросмотра не поддерживает именованные параметры в SQL. Используйте безопасные литералы inline или выполните запрос вне предпросмотра.", - "queryExecutor.error.positionalParametersNotSupported": "Режим предпросмотра не поддерживает позиционные плейсхолдеры (? или ).", - "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "Выровнять выбранные узлы по нижнему краю", - "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "Выровнять выбранные узлы по левому краю", - "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "Выровнять выбранные узлы по правому краю", - "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "Выровнять выбранные узлы по верхнему краю", - "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "Применить изменения подхолста CTE и вернуться на родительский холст", - "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "Автоматически расположить узлы по логическим колонкам", - "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "Центрировать выбранные узлы по горизонтали", - "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "Центрировать выбранные узлы по вертикали", - "commandPalette.description.clear_canvas_and_start_fresh": "Очистить холст и начать заново", - "commandPalette.description.clear_node_selection": "Снять выделение узлов", - "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "Преобразовать алиасы по правилу, заданному в настройках проекта", - "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "Создавать контрольные точки, сравнивать версии и восстанавливать предыдущее состояние холста", - "commandPalette.description.delete_the_selected_nodes": "Удалить выбранные узлы", - "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "Отменить изменения в подредакторе и вернуться на родительский холст", - "commandPalette.description.execute_the_current_query_in_preview": "Выполнить текущий запрос в предпросмотре", - "commandPalette.description.fit_all_nodes_into_the_visible_area": "Уместить все узлы в видимой области", - "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "Сгенерировать CSV из первого узла экспорта CSV", - "commandPalette.description.generate_html_file_from_the_first_html_export_node": "Сгенерировать HTML из первого узла экспорта HTML", - "commandPalette.description.generate_json_file_from_the_first_json_export_node": "Сгенерировать JSON из первого узла экспорта JSON", - "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "Сгенерировать книгу XLSX из первого узла экспорта Excel", - "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "Проверить план выполнения запроса: типы сканирования, стратегии join и оценки стоимости", - "commandPalette.description.load_a_vsaq_canvas_file": "Загрузить файл холста .vsaq", - "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "Измерить avg / median / p95 задержку текущего SQL за N итераций", - "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "Открыть изолированный редактор подхолста для выбранного узла определения CTE", - "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "Открыть локальную историю версий, создаваемую при сохранении, и восстановить предыдущие снимки", - "commandPalette.description.open_output_preview_modal_for_the_active_mode": "Открыть модальное окно предпросмотра вывода для активного режима", - "commandPalette.description.open_shortcut_reference_screen": "Открыть экран справки по горячим клавишам", - "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "Открыть менеджер подключений для добавления, редактирования и переключения БД-подключений", - "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "Вставить SELECT и автоматически сгенерировать узлы — поддерживаются FROM, JOIN, WHERE, LIMIT", - "commandPalette.description.remove_all_nodes_not_connected_to_output": "Удалить все узлы, не подключенные к выходу", - "commandPalette.description.reset_zoom_and_pan_to_default": "Сбросить масштаб и смещение к значениям по умолчанию", - "commandPalette.description.save_canvas_to_a_new_file": "Сохранить холст в новый файл", - "commandPalette.description.save_current_canvas": "Сохранить текущий холст", - "commandPalette.description.save_markdown_documentation_of_the_current_flow": "Сохранить Markdown-документацию текущего потока", - "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "Сохранить выбранные узлы как переиспользуемый сниппет и вставить позже через меню поиска узлов (⇧A)", - "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "Проверить все исходные табличные узлы на холсте на возможные связи join по FK-конвенциям и шаблонам имен", - "commandPalette.description.select_all_nodes_on_canvas": "Выбрать все узлы на холсте", - "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "Привязать позиции узлов к сетке 16px (Ctrl+G)", - "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "Распределить выбранные узлы с равным горизонтальным интервалом", - "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "Распределить выбранные узлы с равным вертикальным интервалом", - "commandPalette.description.zoom_into_the_canvas": "Увеличить холст", - "commandPalette.description.zoom_out_of_the_canvas": "Уменьшить холст", - "commandPalette.name.align_bottom": "Выровнять по низу", - "commandPalette.name.align_left": "Выровнять по левому краю", - "commandPalette.name.align_right": "Выровнять по правому краю", - "commandPalette.name.align_top": "Выровнять по верху", - "commandPalette.name.analyze_all_joins": "Анализировать все JOIN", - "commandPalette.name.auto_fix_naming": "Автоисправление имен", - "commandPalette.name.auto_layout": "Авторазмещение", - "commandPalette.name.center_horizontally": "Центрировать по горизонтали", - "commandPalette.name.center_vertically": "Центрировать по вертикали", - "commandPalette.name.cleanup_orphans": "Очистить сироты", - "commandPalette.name.delete_selected": "Удалить выделенные", - "commandPalette.name.deselect_all": "Снять выделение", - "commandPalette.name.discard_and_exit_editor": "Отменить и выйти из редактора", - "commandPalette.name.distribute_horizontally": "Распределить по горизонтали", - "commandPalette.name.distribute_vertically": "Распределить по вертикали", - "commandPalette.name.edit_selected_cte": "Редактировать выбранный CTE", - "commandPalette.name.exit_cte_editor": "Выйти из редактора CTE", - "commandPalette.name.explain_plan": "План выполнения", - "commandPalette.name.export_csv": "Экспорт CSV", - "commandPalette.name.export_documentation": "Экспорт документации", - "commandPalette.name.export_excel": "Экспорт Excel", - "commandPalette.name.export_html": "Экспорт HTML", - "commandPalette.name.export_json": "Экспорт JSON", - "commandPalette.name.file_save_load_history": "История сохранений/загрузок", - "commandPalette.name.fit_to_screen": "Уместить на экран", - "commandPalette.name.flow_version_history": "История версий потока", - "commandPalette.name.import_sql_to_graph": "Импорт SQL в граф", - "commandPalette.name.keyboard_shortcuts": "Горячие клавиши", - "commandPalette.name.manage_connections": "Управление подключениями", - "commandPalette.name.new_canvas": "Новый canvas", - "commandPalette.name.open_file": "Открыть файл", - "commandPalette.name.reset_viewport": "Сбросить область просмотра", - "commandPalette.name.run_preview": "Запустить предпросмотр", - "commandPalette.name.run_query_benchmark": "Запустить бенчмарк запроса", - "commandPalette.name.save": "Сохранить", - "commandPalette.name.save_as": "Сохранить как", - "commandPalette.name.save_selection_as_snippet": "Сохранить выделение как сниппет", - "commandPalette.name.select_all": "Выделить все", - "commandPalette.name.toggle_preview": "Переключить предпросмотр", - "commandPalette.name.toggle_snap_to_grid": "Переключить привязку к сетке", - "commandPalette.name.zoom_in": "Увеличить", - "commandPalette.name.zoom_out": "Уменьшить", - "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", - "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", - "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", - "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", - "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", - "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", - "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", - "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", - "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", - "commandPalette.tags.clear_selection": "clear selection", - "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", - "commandPalette.tags.create_insert_search_transform": "create insert search transform", - "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", - "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", - "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", - "commandPalette.tags.data_results_table_panel": "data results table panel", - "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", - "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", - "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", - "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", - "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", - "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", - "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", - "commandPalette.tags.export_json_file_output_save": "export json file output save", - "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", - "commandPalette.tags.export_persist_copy": "export persist copy", - "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", - "commandPalette.tags.forward_history": "forward history", - "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", - "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", - "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", - "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", - "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", - "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", - "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", - "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", - "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", - "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", - "commandPalette.tags.load_import_vsaq": "load import vsaq", - "commandPalette.tags.magnify_enlarge": "magnify enlarge", - "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", - "commandPalette.tags.persist_write_disk": "persist write disk", - "commandPalette.tags.remove_erase_nodes": "remove erase nodes", - "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", - "commandPalette.tags.reset_clear_blank": "reset clear blank", - "commandPalette.tags.revert_back_history": "revert back history", - "commandPalette.tags.shrink_reduce": "shrink reduce", - "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", - "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", - "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", - "sqlEditor.diffPreview.title": "Transactional Diff Preview", - "sqlEditor.mutation.confirmExecute": "Confirm Execute", - "sqlEditor.tab.closeAnyway": "Close Anyway", - "sqlEditor.tab.keepTab": "Keep Tab", - "sqlEditor.status.ready": "Ready.", - "sqlEditor.telemetry.none": "No execution telemetry yet.", - "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", - "sqlEditor.telemetry.errors.none": "No aggregated errors.", - "sqlEditor.diff.none": "No transactional diff preview available.", - "sqlEditor.mutation.estimate.none": "No mutation estimate available.", - "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", - "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", - "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", - "sqlEditor.tab.noPendingClose": "No tab close pending.", - "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", - "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", - "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", - "sqlEditor.message.empty": "Execute a statement to see messages.", - "sqlEditor.message.success": "Execution completed successfully.", - "sqlEditor.result.summary.empty": "Rows: - Time: -", - "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", - "sqlEditor.file.save.canceled": "Save canceled.", - "sqlEditor.file.save.noPath": "No target path selected.", - "sqlEditor.file.save.success": "SQL file saved.", - "sqlEditor.file.save.failed": "Save failed.", - "sqlEditor.file.open.failed": "Open failed.", - "sqlEditor.file.open.notFound": "Selected SQL file was not found.", - "sqlEditor.file.open.success": "SQL file opened.", - "sqlEditor.status.executing": "Executing SQL...", - "sqlEditor.status.executingScript": "Executing SQL script...", - "sqlEditor.status.executingStep": "Executing {0}/{1}...", - "sqlEditor.status.canceling": "Canceling execution...", - "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", - "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", - "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", - "sqlEditor.status.success": "Execution succeeded.", - "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", - "sqlEditor.status.canceled": "Execution canceled.", - "sqlEditor.status.failed": "Execution failed.", - "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", - "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", - "sqlEditor.result.tabTitle": "Result {0}", - "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", - "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", - "sqlEditor.tab.closed": "Tab closed.", - "sqlEditor.tab.closeCanceled": "Tab close canceled.", - "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", - "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", - "sqlEditor.error.noConnection": "No active database connection for SQL execution.", - "sqlEditor.error.executionCanceled": "SQL execution was canceled.", - "sqlEditor.tab.scriptTitle": "Script {0}", - "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", - "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", - "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", - "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", - "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", - "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", - "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", - "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", - "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", - "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", - "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", - "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", - "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", - "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", - "sqlEditor.results.title": "Results", - "sqlEditor.saveSql.fileType": "SQL Files", - "sqlEditor.saveSql.pickerTitle": "Save SQL File", - "sqlEditor.export.pickerTitle": "Export SQL Data", - "sqlEditor.export.status.noResultTitle": "No execution result available for export.", - "sqlEditor.export.status.noResultDetail": "Execute a query first.", - "sqlEditor.export.status.successTitle": "Report exported.", - "sqlEditor.export.status.failedTitle": "Failed to export report.", - "sqlEditor.export.fileType.html": "HTML File", - "sqlEditor.export.fileType.json": "JSON File", - "sqlEditor.export.fileType.csv": "CSV File", - "sqlEditor.export.fileType.xlsx": "Excel Workbook", - "sqlEditor.export.defaultFileBase": "report", - "sqlEditor.export.defaultTitle": "SQL Report", - "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", - "sqlEditor.export.type.html.title": "HTML full-feature report", - "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", - "sqlEditor.export.type.json.title": "JSON execution contract", - "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", - "sqlEditor.export.type.csv.title": "CSV data export", - "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", - "sqlEditor.export.type.xlsx.title": "Excel workbook export", - "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", - "sqlEditor.export.dialog.windowTitle": "Export SQL Data", - "sqlEditor.export.dialog.title": "Export SQL Data", - "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", - "sqlEditor.export.dialog.confirm": "Export", - "sqlEditor.export.dialog.fileNameWatermark": "report.html", - "sqlEditor.export.dialog.titleWatermark": "SQL Report", - "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", - "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", - "sqlEditor.export.dialog.section.fileName": "FILE NAME", - "sqlEditor.export.dialog.section.reportTitle": "TITLE", - "sqlEditor.export.dialog.section.description": "DESCRIPTION", - "sqlEditor.export.dialog.section.options": "OPTIONS", - "sqlEditor.export.option.includeSchema": "Include output schema", - "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", - "sqlEditor.export.option.includeMetadata": "Include optional metadata", - "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", - "sqlEditor.export.badge.offline": "OFFLINE READY", - "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", - "sqlEditor.export.badge.dataOnly": "DATA ONLY" -} +{ + "main.brand": "AkkornStudio", + "main.tab.query1": "Query 1", + "main.new": "New", + "main.open": "Open", + "main.save": "Save", + "main.history": "History", + "main.layout": "Layout", + "main.preview": "Preview", + "main.undo": "Undo", + "main.redo": "Redo", + "main.cleanupOrphans": "Cleanup orphan nodes", + "main.autoFixAliasNaming": "Auto-fix alias naming", + "main.autoLayoutCanvas": "Auto layout canvas", + "main.toggleDataPreview": "Toggle data preview", + "main.language": "Language", + "main.restore.prompt": "Previous session found — restore the last canvas?", + "main.restore.button": "Restore session", + "main.cteEditor.editingPrefix": "Editing CTE: ", + "main.cteEditor.backToCanvas": "Back to Canvas", + "main.cteEditor.exitA11y": "Exit CTE editor", + "main.viewEditor.editingPrefix": "DDL > View: ", + "main.viewEditor.backToCanvas": "Back to DDL", + "main.viewEditor.exitA11y": "Exit view editor", + "connection.title": "Connection Manager", + "connection.subtitle": "Настраивайте, проверяйте и активируйте подключения, не прерывая поток", + "connection.none": "No connection", + "connection.active": "ACTIVE", + "connection.health.online": "Online", + "connection.health.degraded": "Degraded", + "connection.health.offline": "Offline", + "connection.tooltip.none": "No active connection — click to manage", + "connection.ping": "Ping", + "connection.saved": "SAVED CONNECTIONS", + "connection.new": "New Connection", + "connection.selectOrCreate": "Select a connection or create a new one", + "connection.name": "Connection Name", + "connection.provider": "Provider", + "connection.host": "Host", + "connection.port": "Port", + "connection.database": "Database", + "connection.sqlitePath": "SQLite Path", + "connection.sqliteBrowse": "Browse", + "connection.sqliteCreate": "Create", + "connection.username": "Username", + "connection.password": "Password", + "connection.timeout": "Timeout (seconds)", + "connection.test": "Test", + "connection.save": "Save", + "connection.connect": "Connect", + "connection.action.testConnection": "Test connection", + "connection.action.saveConnection": "Save connection", + "connection.action.connectConnection": "Connect connection", + "connection.status.connecting": "Подключение...", + "connection.status.connected": "Подключено", + "connection.status.testing": "Проверка...", + "connection.status.failedPrefix": "Сбой подключения", + "connection.status.metadataUnavailable": "Сбой подключения: метаданные недоступны.", + "connection.status.highLatency": "высокая задержка", + "connection.watermark.name": "Моя Prod БД", + "connection.watermark.host": "localhost", + "connection.watermark.port": "5432", + "connection.watermark.database": "database_name", + "connection.watermark.username": "user", + "connection.watermark.password": "••••••••", + "connection.watermark.timeout": "30", + "main.connectingDb": "Connecting to database...", + "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", + "status.nodesSeparator": " nodes · ", + "status.connectionsSuffix": " connections", + "status.undo": "Undo: ", + "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", + "connection.disconnect": "Disconnect", + "connection.action.disconnectConnection": "Disconnect connection", + "connectionTab.active": "ACTIVE CONNECTION", + "connectionTab.none": "No active connection", + "connectionTab.saved": "SAVED CONNECTIONS", + "connectionTab.new": "+ New Connection", + "schema.database": "DATABASE", + "schema.search": "Search tables, columns...", + "schema.loading": "Searching tables, columns...", + "schema.noConnection": "No Connection", + "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", + "schema.emptyNoTables": "No tables found", + "fileHistory.title": "Save/Load Version History", + "fileHistory.reload": "Reload", + "fileHistory.restoreSelected": "Restore selected", + "fileHistory.empty": "No local versions yet", + "fileHistory.emptyHint": "Save this file to generate version history.", + "preview.title": "Data Preview", + "preview.subtitle": "Review data and diagnostics before continuing", + "preview.run": "Run", + "preview.cancel": "Cancel", + "preview.tab.preview": "Preview", + "preview.tab.sql": "SQL", + "preview.close": "Close preview", + "preview.running": "Running preview query… ", + "preview.clickCancel": "Click Cancel to stop", + "preview.cancelled": "Query cancelled", + "preview.runAgain": "Press Run to execute again", + "preview.failed": "Preview execution failed", + "preview.technical": "TECHNICAL DETAILS", + "preview.noData": "No data yet", + "preview.f3Hint": "Press F3 or Space to run the current query", + "sqlImporter.title": "Import SQL to Graph", + "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", + "sqlImporter.sqlStatement": "SQL STATEMENT", + "sqlImporter.supported": "Supported: ", + "sqlImporter.import": "Import", + "sqlImporter.report": "CONVERSION REPORT", + "sqlEditor.mutation.dialogTitle": "Подтверждение изменения", + "sqlEditor.mutation.dialogSubtitle": "Проверьте влияние перед подтверждением выполнения", + "search.empty": "No nodes found", + "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", + "search.shortcut": "⇧A", + "search.spawn": "Spawn", + "commandPalette.empty": "Нет команд, соответствующих поиску", + "commandPalette.search": "Поиск команд", + "commandPalette.shortcut": "CTRL+SHIFT+P", + "commandPalette.execute": "Выполнить", + "context.editCte": "Edit Selected CTE", + "context.editViewSubcanvas": "Edit View Subcanvas", + "explain.title": "Explain Plan", + "explain.sql": "SQL", + "explain.option.analyze": "Анализ", + "explain.option.buffers": "Буферы", + "explain.badge.simulated": "СИМУЛИРОВАНО", + "explain.timing.planning": "Планирование:", + "explain.timing.execution": "Выполнение:", + "explain.section.snapshotComparison": "Сравнение снимков", + "explain.section.indexRecommendations": "Рекомендации по индексам", + "explain.section.history": "История", + "explain.detail.estimated": "Оценочные", + "explain.detail.actual": "Фактические", + "explain.detail.error": "Ошибка", + "explain.detail.time": "Время", + "explain.detail.loops": "Циклы", + "explain.rerun": "Re-run", + "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", + "explain.running": "Running EXPLAIN…", + "explain.failed": "⚠ EXPLAIN failed", + "explain.noPlan": "No plan yet", + "explain.rerunHint": "Press Re-run to execute EXPLAIN", + "explain.header.operation": "ОПЕРАЦИЯ", + "explain.header.cost": "СТОИМОСТЬ", + "explain.header.rows": "СТРОКИ", + "explain.header.alert": "ПРЕДУПРЕЖДЕНИЕ", + "explain.mode.list": "Список", + "explain.mode.tree": "Дерево", + "explain.action.snapshot": "Сохранить снимок", + "explain.action.copyJson": "Копировать JSON", + "explain.action.copyText": "Копировать текст", + "explain.action.saveJson": "Сохранить .json", + "explain.action.openDalibo": "Открыть в Dalibo", + "explain.legend.seqscan": "SEQ SCAN — full table read, no index", + "explain.legend.sort": "SORT — in-memory sort", + "explain.legend.hash": "HASH — hash join", + "explain.escClose": "Esc to close", + "flowVersion.title": "Flow Version History", + "flowVersion.subtitle": "Create checkpoints, compare versions and restore", + "flowVersion.watermark": "Checkpoint label (optional)…", + "flowVersion.saveCheckpoint": "Save Checkpoint", + "flowVersion.compareMode": "Compare Mode", + "flowVersion.selectBase": "Select BASE version (from):", + "flowVersion.clickAny": "Then click any version in the list below to compare.", + "flowVersion.noCheckpoints": "No checkpoints yet", + "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", + "flowVersion.restore": "Restore", + "flowVersion.diffResults": "Diff Results", + "benchmark.title": "Query Benchmark", + "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", + "benchmark.sql": "SQL being benchmarked", + "benchmark.runLabel": "Run label", + "benchmark.runLabelWatermark": "Run 1", + "benchmark.iterations": "Iterations (1–100)", + "benchmark.warmup": "Warm-up passes (0–10)", + "benchmark.interval": "Interval between runs (ms)", + "benchmark.run": "Run Benchmark", + "benchmark.cancel": "Cancel", + "benchmark.clearHistory": "Clear History", + "benchmark.latest": "LATEST RESULT", + "benchmark.avg": "AVG", + "benchmark.median": "MEDIAN", + "benchmark.min": "MIN", + "benchmark.max": "MAX", + "benchmark.iterationsAt": " iterations · run at ", + "benchmark.history": "HISTORY", + "benchmark.header.label": "Метка", + "benchmark.header.avg": "Средн.", + "benchmark.header.median": "Медиана", + "benchmark.header.min": "Мин", + "benchmark.header.max": "Макс", + "benchmark.itersSuffix": " iters", + "diagnostics.title": "App Diagnostics", + "diagnostics.run": "Run", + "diagnostics.running": "Running checks…", + "diagnostics.ok": "OK", + "diagnostics.warning": "Warning", + "diagnostics.error": "Error", + "diagnostics.tooltip.rerun": "Re-run all checks", + "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", + "diagnostics.tooltip.close": "Close (Esc)", + "autoJoin.title": "Auto-Join Suggestions", + "autoJoin.titleForTable": "Auto-Join suggestions for {0}", + "autoJoin.acceptAll": "Accept All", + "autoJoin.accept": "Accept", + "autoJoin.skip": "Skip", + "autoJoin.allHandled": "All suggestions handled", + "autoJoin.joinKeyword": "JOIN", + "autoJoin.confidence.fkConstraint": "FK Constraint", + "autoJoin.confidence.fkReverse": "FK (Reverse)", + "autoJoin.confidence.namingMatch": "Naming Match", + "autoJoin.confidence.weakMatch": "Weak Match", + "autoJoin.runSelected": "Auto-Join Selected", + "autoJoin.noSimilarityTitle": "No automatic join found", + "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", + "autoJoin.appliedTitle": "Auto-join applied", + "autoJoin.manual.title": "Create Manual Join", + "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", + "autoJoin.manual.leftColumn": "Left column", + "autoJoin.manual.rightColumn": "Right column", + "autoJoin.manual.joinType": "Join type", + "autoJoin.manual.operator": "Operator", + "autoJoin.manual.confirm": "Create join", + "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", + "autoJoin.manualJoinCreatedTitle": "Manual join created", + "autoJoin.manualJoinFailedTitle": "Manual join could not be created", + "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", + "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", + "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", + "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", + "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", + "property.outputAlias": "OUTPUT ALIAS", + "property.sourceAlias": "SOURCE ALIAS", + "property.aliasWatermark": "e.g. MyColumn (optional)", + "property.parameters": "PARAMETERS", + "property.enabled": "Enabled", + "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", + "property.dateWatermark": "YYYY-MM-DD or leave empty", + "property.apply": "Apply", + "property.inputPins": "INPUT PINS", + "property.outputPins": "OUTPUT PINS", + "property.sqlTrace": "SQL TRACE", + "property.live": "live", + "node.numericValue": "Numeric Value", + "node.stringValue": "String Value", + "node.enterText": "Enter text", + "node.datetimeValue": "DateTime Value", + "node.valueLabel": "Value:", + "node.noInputs": "No inputs", + "node.loadingSample": "Loading sample…", + "node.previewFailed": "⚠ Preview failed", + "node.sampleRowsHint": "5 sample rows · demo data", + "sidebar.tab.nodes": "Узлы", + "sidebar.tab.connection": "Подключение", + "sidebar.tab.schema": "Схема", + "sidebar.tab.diagnostics": "Диагностика", + "sidebar.addNode": "+ Add Node (⇧A)", + "sidebar.previewF3": "Preview (F3)", + "nodesList.search": "Search nodes...", + "search.watermark": "Search nodes… (Esc to close)", + "search.snippets": "★ SNIPPETS", + "commandPalette.watermark": "Выполнить команду… (Esc для закрытия)", + "tooltip.newCanvas": "New canvas (Ctrl+N)", + "tooltip.openCanvas": "Open canvas (Ctrl+O)", + "tooltip.saveCanvas": "Save canvas (Ctrl+S)", + "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", + "tooltip.zoomOut": "Zoom out (Ctrl+-)", + "tooltip.zoomIn": "Zoom in (Ctrl++)", + "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", + "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", + "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", + "tooltip.dataPreview": "Data preview (F3)", + "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", + "tooltip.appDiagnostics": "App Diagnostics (self-check)", + "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", + "tooltip.cancelRunningQuery": "Cancel the running query", + "tooltip.closeEsc": "Close (Esc)", + "tooltip.recheckConnectionHealth": "Re-check connection health", + "tooltip.deleteConnection": "Delete connection", + "tooltip.testConnection": "Test connection", + "tooltip.saveConnection": "Save connection", + "tooltip.activateConnection": "Activate this connection", + "tooltip.toggleDataSamplePreview": "Toggle data sample preview", + "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", + "tooltip.copySql": "Copy SQL to clipboard", + "tooltip.formatSql": "Format SQL", + "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", + "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", + "tooltip.switchToQueryMode": "Switch to Query canvas", + "tooltip.switchToDdlMode": "Switch to DDL canvas", + "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", + "tooltip.pins.inputs": "Inputs", + "tooltip.pins.outputs": "Outputs", + "tooltip.pins.none": "None", + "tooltip.tableColumns": "Columns", + "tooltip.tableColumns.none": "No detailed columns", + "window.minimize": "Minimize window", + "window.maximizeRestore": "Maximize/restore window", + "window.close": "Close window", + "menu.newDiagram": "New diagram", + "menu.openFile": "Open file", + "menu.save": "Save", + "menu.fileHistory": "File history", + "menu.shortcuts": "Keyboard shortcuts", + "menu.settings": "Settings", + "menu.importDdlSchema": "Import DDL schema", + "menu.viewDdlSql": "View DDL SQL", + "menu.executeDdl": "Execute DDL", + "menu.backToStart": "Back to start", + "toast.ddlExecuteFailed": "Failed to execute DDL.", + "toast.ddlOpenFailed": "Failed to open DDL SQL.", + "toast.ddlImportFailed": "Failed to import schema into DDL.", + "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", + "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", + "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", + "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", + "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", + "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", + "toast.ddlTableImported": "Table imported into the DDL canvas.", + "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", + "toast.ddlNoActiveConnection": "No active connection to execute DDL.", + "toast.ddlExecutedSuccess": "DDL executed successfully.", + "toast.ddlExecutedWithIssues": "DDL executed with issues.", + "toast.switchToDdl": "Switch to DDL mode to generate SQL.", + "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", + "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", + "toast.previewOpenFailed": "Failed to open preview.", + "tab.switchFailed": "Failed to switch tab: {0}", + "settings.status.darkApplied": "Dark theme applied.", + "settings.status.lightApplied": "Light theme applied.", + "settings.status.snapUpdated": "Snap updated: {0}.", + "settings.status.languageToggled": "Language toggled.", + "settings.status.languageSelected": "Language selected: {0}.", + "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", + "settings.section.appearance.title": "Themes", + "settings.section.languageRegion.title": "Язык и регион", + "settings.section.dateTime.title": "Дата и время", + "settings.section.keyboard.title": "Горячие клавиши", + "settings.section.privacy.title": "Конфиденциальность", + "settings.section.notification.title": "Уведомления", + "settings.section.accessibility.title": "Доступность", + "settings.section.default.title": "Настройки", + "settings.section.appearance.subtitle": "Выберите стиль или настройте тему", + "settings.section.languageRegion.subtitle": "Управляйте языком и региональным форматированием", + "settings.section.keyboard.subtitle": "Настройте сочетания клавиш для command palette и выполнения на canvas.", + "settings.section.wip.subtitle": "В разработке.", + "settings.section.default.subtitle": "Настройки приложения", + "settings.general": "General", + "settings.nav.appearance": "Appearance", + "settings.theme.light": "Светлая тема", + "settings.theme.dark": "Тёмная тема", + "settings.theme.system": "Системная тема", + "settings.gridSnap.title": "Grid Snap", + "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", + "settings.language.subtitle": "Выберите язык приложения.", + "settings.language.toggle": "Сменить язык", + "settings.language.option.ptBR": "Португальский (Бразилия)", + "settings.language.option.enUS": "Английский (США)", + "settings.language.option.esES": "Испанский (Испания)", + "settings.language.option.ruRU": "Русский", + "settings.language.option.jaJP": "Японский", + "settings.language.option.zhTW": "Традиционный китайский", + "settings.themeJson.title": "JSON темы", + "settings.themeJson.subtitle": "Вставьте JSON темы, примените и сохраните сразу.", + "settings.themeJson.apply": "Применить JSON", + "settings.themeJson.restoreDefault": "Восстановить тему по умолчанию", + "mode.query": "Query", + "mode.ddl": "DDL", + "sidebar.left.close": "Close left sidebar", + "sidebar.left.open": "Reopen left sidebar", + "sidebar.right.close": "Close right sidebar", + "sidebar.right.open": "Reopen right sidebar", + "connection.completedTitle": "Connection completed", + "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", + "connection.close": "Close connection manager", + "connection.refreshHealth": "Refresh connection health", + "common.details": "Details", + "common.cancel": "Cancel", + "common.keep": "Keep", + "common.clear": "Clear", + "zoom.out": "Zoom out", + "zoom.in": "Zoom in", + "zoom.fit": "Fit zoom to screen", + "zoom.level": "Zoom level", + "settings.theme.mode": "Режим темы", + "diagnostics.category.canvas": "Canvas Integrity", + "diagnostics.category.output": "Output & Execution", + "diagnostics.category.session": "Session & Safety", + "diagnostics.category.notice": "Runtime Notices", + "diagnostics.summary.ok": "All systems OK", + "diagnostics.summary.warningCount": "{0} warning(s) detected", + "diagnostics.summary.errorCount": "{0} error(s) detected", + "diagnostics.canvasMigration": "Canvas Migration", + "diagnostics.recommendation.resaveFile": "Re-save the file to update it to the latest schema version.", + "diagnostics.canvasState.name": "Canvas State", + "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", + "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", + "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", + "diagnostics.validation.name": "Validation Errors", + "diagnostics.validation.recommendation": "Fix highlighted nodes before running output preview", + "diagnostics.validation.errorWithWarnings": "{0} error(s) and {1} warning(s) in the graph", + "diagnostics.validation.warningOnly": "{0} warning(s) in the graph", + "diagnostics.validation.none": "No validation issues", + "diagnostics.orphan.name": "Orphan Nodes", + "diagnostics.orphan.recommendation": "Use the orphan cleanup action to remove unused nodes", + "diagnostics.orphan.count": "{0} node(s) not connected to any output", + "diagnostics.orphan.none": "No orphan nodes detected", + "diagnostics.naming.name": "Naming Conventions", + "diagnostics.naming.recommendation": "Use auto-fix alias naming when conformance is below 100%", + "diagnostics.naming.conformance": "Naming conformance: {0}%", + "diagnostics.naming.ok": "All aliases follow naming conventions (100%)", + "diagnostics.queryCompilation.name": "Live SQL Compilation", + "diagnostics.queryCompilation.recommendation": "Review SQL diagnostics in output when errors/warnings are reported.", + "diagnostics.queryCompilation.errorFallback": "Live SQL compilation reported errors.", + "diagnostics.queryCompilation.warningCounts": "{0} diagnostic item(s), {1} guardrail warning(s).", + "diagnostics.queryCompilation.ok": "Live SQL compiled without diagnostics.", + "diagnostics.previewSafety.name": "Preview Safety", + "diagnostics.previewSafety.recommendation": "Preview executes read-only statements only.", + "diagnostics.previewSafety.blocked": "Current SQL is mutating and blocked by Safe Preview mode.", + "diagnostics.previewSafety.ok": "Preview safety checks passed.", + "diagnostics.previewExecution.name": "Preview Execution", + "diagnostics.previewExecution.recommendation": "Run preview and inspect diagnostics for execution/runtime errors.", + "diagnostics.previewExecution.failed": "Preview execution failed.", + "diagnostics.previewExecution.cancelled": "Preview execution was cancelled.", + "diagnostics.previewExecution.done": "{0} row(s) in {1}ms.", + "diagnostics.previewExecution.none": "No preview execution issues detected.", + "diagnostics.ddlCompilation.name": "DDL Compilation", + "diagnostics.ddlCompilation.recommendation": "Fix DDL compile diagnostics before execution.", + "diagnostics.ddlCompilation.failed": "DDL compilation failed.", + "diagnostics.ddlCompilation.warningCount": "{0} warning(s) reported by DDL compiler.", + "diagnostics.ddlCompilation.ok": "DDL compilation succeeded.", + "diagnostics.ddlOutput.name": "DDL Output", + "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", + "diagnostics.ddlOutput.none": "No DDL statements generated yet.", + "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", + "diagnostics.undo.name": "Undo History", + "diagnostics.undo.recommendation": "History is in-memory only; save your canvas regularly.", + "diagnostics.undo.saved": "Canvas is saved (no unsaved changes).", + "diagnostics.undo.unsavedDeep": "Unsaved changes with {0} undo steps.", + "diagnostics.undo.unsaved": "Unsaved changes - {0} undo step(s) available.", + "diagnostics.report.title": "AkkornStudio - Diagnostic Report", + "diagnostics.report.generated": "Generated", + "diagnostics.report.overall": "Overall", + "diagnostics.report.details": "Details", + "diagnostics.report.recommendation": "Recommendation", + "diagnostics.report.lastCheck": "Last Check", + "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", + "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", + "preview.providerLabel": "Provider", + "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", + "preview.schemaAnalysis.run": "Run Analysis", + "preview.schemaAnalysis.cancel": "Cancel", + "preview.schemaAnalysis.issues": "Issues", + "preview.schemaAnalysis.clearFilters": "Clear Filters", + "preview.schemaAnalysis.severity": "Severity", + "preview.schemaAnalysis.rule": "Rule", + "preview.schemaAnalysis.minConfidence": "Min Confidence", + "preview.schemaAnalysis.tableFilter": "Table Filter", + "preview.schemaAnalysis.tableFilterWatermark": "schema.table", + "preview.schemaAnalysis.details": "Details", + "preview.schemaAnalysis.evidence": "Evidence", + "preview.schemaAnalysis.suggestions": "Suggestions", + "preview.schemaAnalysis.ruleDiagnostics": "Rule Diagnostics", + "preview.schemaAnalysis.sqlCandidates": "SQL Candidates", + "preview.schemaAnalysis.copySql": "Copy SQL", + "preview.schemaAnalysis.applyToCanvas": "Apply to Canvas", + "preview.schemaAnalysis.summary.issues": "Issues:", + "preview.schemaAnalysis.summary.rawPrefix": "(raw:", + "preview.schemaAnalysis.summary.critical": "| Critical:", + "preview.schemaAnalysis.summary.warning": "| Warning:", + "preview.schemaAnalysis.summary.info": "| Info:", + "preview.schemaAnalysis.state.metadataUnavailable": "Metadata unavailable for structural analysis.", + "preview.schemaAnalysis.state.cancelled": "Analysis cancelled by the user.", + "preview.schemaAnalysis.state.partialTimeout": "Analysis finished partially due to timeout.", + "preview.schemaAnalysis.state.failed": "Structural analysis failed.", + "preview.schemaAnalysis.state.empty": "No inferable structural issue was detected.", + "preview.schemaAnalysis.state.noFilterMatch": "No issue matches the selected filters.", + "preview.schemaAnalysis.state.noIssueSelected": "No issue selected.", + "preview.schemaAnalysis.state.noSqlCandidate": "No SQL candidate available.", + "preview.schemaAnalysis.actionBlockedTooltip": "Action unavailable for the current risk level or capability.", + "common.navigate": "Navigate", + "common.close": "Close", + "common.esc": "Esc", + "common.ms": "ms", + "common.zero": "0", + "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", + "nodesList.empty": "No nodes found", + "nodesList.emptyHint": "Adjust the search term to explore available types", + "schema.emptyFiltered": "No objects found for the current filter", + "start.lastSnapshot": "Last snapshot", + "app.brandBadge": "VS", + "property.tab.properties": "Properties", + "property.tab.projectSettings": "Project Settings", + "property.nodeType": "NODE TYPE", + "property.selectNodeHint": "Select a node to edit its properties.", + "property.namingConventions": "Naming Conventions", + "property.aliasConvention": "Alias convention", + "property.enforceAliasNaming": "Enforce alias naming", + "property.warnReservedSql": "Warn on reserved SQL keywords", + "property.maxAliasLength": "Max alias length", + "property.maxAliasLengthDefault": "64", + "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", + "start.tips": "Tips", + "start.tips.quick": "Quick tips", + "start.tips.item1": "1. Click New Diagram to start from scratch.", + "start.tips.item2": "2. Use templates to speed up prototyping.", + "start.tips.item3": "3. Open saved connections to load real tables.", + "start.tips.shortcut": "Shortcut: CTRL+SHIFT+P opens the command palette.", + "start.workspace": "WORKSPACE", + "start.resumeTitle": "Continue where you left off", + "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", + "start.chip.quickFlow": "Quick flow", + "start.chip.templates": "Templates", + "start.chip.connections": "Connections", + "start.savedConnectionsTitle": "Saved Connections", + "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", + "start.noConnectionsTitle": "No connections configured yet", + "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", + "start.newConnection": "+ New Connection", + "start.recentProjectsTitle": "Recent Projects", + "start.searchRecent": "Search recent project...", + "start.quickActions": "Quick actions", + "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", + "start.noRecentTitle": "No recent projects yet", + "start.noRecentSubtitle": "Use the quick actions card above to get started.", + "start.exploreTemplates": "Explore templates", + "start.templatesFavoritesHint": "Favorites on top", + "start.favoriteTemplate": "Favorite template", + "node.columnSetPreview": "ColumnSet preview", + "node.view": "VIEW", + "node.tableDefinition": "Table Definition", + "node.join": "JOIN", + "node.window.addPartition": "Add PARTITION BY slot", + "node.window.removePartition": "Remove PARTITION BY slot", + "node.window.addOrder": "Add ORDER BY slot", + "node.window.removeOrder": "Remove ORDER BY slot", + "sql.keyword.select": "SELECT", + "sql.keyword.from": "FROM", + "sql.keyword.join": "JOIN", + "sql.keyword.where": "WHERE", + "sql.keyword.limit": "LIMIT", + "sqlImporter.close": "Close SQL importer", + "sqlImporter.report.imported": "Imported", + "sqlImporter.report.partial": "Partial", + "sqlImporter.report.skipped": "Skipped", + "benchmark.close": "Close benchmark", + "benchmark.p95": "P95", + "benchmark.n": "N", + "liveSql.safePreview": "SAFE PREVIEW MODE", + "liveSql.title": "LIVE SQL", + "liveSql.blocked": "BLOCKED", + "liveSql.copy": "Copy", + "liveSql.format": "Format", + "liveSql.benchmark": "Benchmark", + "liveSql.explain": "Explain", + "liveSql.actionsHint": "Performance tools", + "ddl.dialog.title": "Execute DDL", + "ddl.dialog.execute": "Execute", + "ddl.dialog.cancel": "Cancel", + "ddl.dialog.close": "Close", + "ddl.dialog.stopOnError": "Stop on first failure", + "ddl.dialog.confirmDestructive": "I confirm execution of destructive statements (DROP TABLE)", + "ddl.dialog.reviewBeforeRun": "Review the DDL script before confirming.", + "ddl.dialog.confirmQuestion": "Confirm DDL execution on the connected database?", + "ddl.dialog.irreversibleWarning": "This action can change the schema irreversibly.", + "ddl.dialog.mustConfirmDestructive": "Confirm destructive execution to continue.", + "ddl.dialog.executing": "Executing...", + "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", + "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", + "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", + "ddl.execute.result.failed": "Failed to execute DDL.", + "ddl.execute.result.cancelled": "Execution cancelled by the user.", + "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", + "context.deleteSingle": "Delete {0}", + "context.deleteMultiple": "Delete {0} nodes", + "context.bringForward": "Bring Forward (Ctrl+PgUp)", + "context.sendBackward": "Send Backward (Ctrl+PgDown)", + "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", + "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", + "context.normalizeLayers": "Normalize Layers", + "context.deleteWire": "Delete wire", + "context.addNode": "Add Node (Shift+A)", + "context.undoWithDescription": "Undo {0}", + "context.redo": "Redo", + "shortcuts.windowTitle": "Keyboard Shortcuts", + "shortcuts.headerTitle": "AkkornStudio - Shortcuts", + "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", + "shortcuts.filterWatermark": "Filter shortcuts by key or action...", + "shortcuts.resultCount": "{0} shortcuts", + "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", + "shortcuts.noneFound": "No shortcuts found.", + "shortcuts.section.fileGeneral": "Файл и общее", + "shortcuts.section.editing": "Редактирование", + "shortcuts.section.canvasNavigation": "Холст и навигация", + "shortcuts.section.zoomPanPrecision": "Масштаб, панорамирование и точность", + "shortcuts.section.previewInspection": "Предпросмотр и инспекция", + "shortcuts.key.deleteOrBackspace": "Del или Backspace", + "shortcuts.key.middleDrag": "Средняя кнопка + перетаскивание", + "shortcuts.key.rightDrag": "Правая кнопка + перетаскивание", + "shortcuts.key.spaceDrag": "Пробел + перетаскивание", + "shortcuts.key.altLeftDrag": "Alt + перетаскивание левой кнопкой", + "shortcuts.key.arrows": "Стрелки", + "shortcuts.key.shiftArrows": "Shift + Стрелки", + "shortcuts.action.openShortcutScreen": "Открыть этот экран горячих клавиш", + "shortcuts.action.newCanvas": "Новый холст", + "shortcuts.action.openFile": "Открыть файл", + "shortcuts.action.save": "Сохранить", + "shortcuts.action.saveAs": "Сохранить как", + "shortcuts.action.commandPalette": "Палитра команд", + "shortcuts.action.undo": "Отменить", + "shortcuts.action.redo": "Повторить", + "shortcuts.action.selectAll": "Выделить всё", + "shortcuts.action.deleteSelection": "Удалить выделение", + "shortcuts.action.closeOverlayCancel": "Закрыть оверлеи / отменить действия", + "shortcuts.action.openNodeSearch": "Открыть поиск узлов", + "shortcuts.action.resetViewport": "Сбросить viewport", + "shortcuts.action.centerSelection": "Центрировать выделение", + "shortcuts.action.fitSelection": "Подогнать выделение", + "shortcuts.action.autoLayout": "Авторазмещение", + "shortcuts.action.toggleSnapToGrid": "Переключить привязку к сетке", + "shortcuts.action.bringForward": "На слой выше", + "shortcuts.action.sendBackward": "На слой ниже", + "shortcuts.action.bringToFront": "На передний план", + "shortcuts.action.sendToBack": "На задний план", + "shortcuts.action.zoomInOut": "Увеличить / уменьшить", + "shortcuts.action.pan": "Панорамирование", + "shortcuts.action.temporaryPan": "Временное панорамирование", + "shortcuts.action.alternatePan": "Альтернативное панорамирование", + "shortcuts.action.fineNudge": "Точный сдвиг выделения", + "shortcuts.action.fastNudge": "Быстрый сдвиг", + "shortcuts.action.togglePreview": "Переключить предпросмотр данных", + "shortcuts.action.explainPlan": "План выполнения", + "shortcuts.action.runPreview": "Запустить предпросмотр", + "shortcuts.action.connectionManager": "Менеджер подключений", + "shortcuts.action.flowVersionHistory": "История версий потока", + "shortcuts.resetAll": "Сбросить всё", + "shortcuts.customized": "Изменено", + "shortcuts.default": "По умолчанию", + "shortcuts.apply": "Применить", + "shortcuts.reset": "Сбросить", + "shortcuts.status.resetAllSuccess": "Все сочетания сброшены к значениям по умолчанию.", + "shortcuts.status.updated": "Сочетание обновлено.", + "shortcuts.status.reset": "Сочетание сброшено по умолчанию.", + "shortcuts.status.updateFailed": "Не удалось обновить сочетание.", + "toast.severity.success": "Success", + "toast.severity.warning": "Warning", + "toast.severity.error": "Error", + "toast.details.success": "Success Details", + "toast.details.warning": "Warning Details", + "toast.details.error": "Error Details", + "diagnostics.area.cteEditor": "CTE Editor", + "diagnostics.area.viewEditor": "View Editor", + "diagnostics.area.subEditor": "Sub-editor", + "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", + "diagnostics.recommendation.reloadFileIfNeeded": "Reload the file if needed.", + "diagnostics.viewEditor.exitFailed": "Could not exit: {0}", + "diagnostics.viewEditor.canvasIncomplete": "the canvas is incomplete.", + "diagnostics.viewEditor.exitRecommendation": "Connect a valid ResultOutput or use the discard command.", + "diagnostics.viewEditor.restoreParentFailed": "Failed to restore the parent canvas. The subgraph was discarded.", + "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", + "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", + "diagnostics.canvasMigration.openWarning": "Open: {0}", + "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", + "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", + "diagnostics.recommendation.resaveLatestSchema": "Review diagnostics and re-save the canvas to persist the latest schema.", + "diagnostics.recommendation.saveMigratedSchema": "Review diagnostics and save the canvas to persist the migrated schema.", + "file.saveDialog.title": "Save Canvas", + "file.saveDialog.suggestedName": "Query1", + "file.save.success": "Canvas saved successfully.", + "file.save.failedWithReason": "Save failed: {0}", + "file.openDialog.title": "Open Canvas", + "file.open.failedWithReason": "Open failed: {0}", + "file.open.success": "Canvas opened successfully.", + "file.open.successWithWarnings": "Canvas opened with warnings.", + "session.restore.failedWithReason": "Restore failed: {0}", + "session.restore.successWithWarnings": "Session restored with warnings.", + "session.restore.success": "Session restored successfully.", + "export.documentation.dialogTitle": "Export Flow Documentation", + "export.documentation.success": "Documentation exported successfully.", + "export.documentation.failed": "Documentation export failed.", + "export.failed.pathPermissionsHint": "Check file path and permissions.", + "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", + "export.dialogTitleByExtension": "Export as {0}", + "export.success": "Export completed successfully.", + "export.failed": "Export failed.", + "fileHistory.currentFile.none": "No file selected", + "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", + "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", + "fileHistory.status.countAvailable": "{0} local version(s) available.", + "fileHistory.restore.failedWithReason": "Restore failed: {0}", + "fileHistory.restore.successFrom": "Restored version from {0}.", + "preview.status.cancelled": "Cancelled", + "preview.status.error": "Error", + "preview.status.ready": "Ready", + "preview.runningWithMs": "Running... {0}ms", + "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", + "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", + "explain.errorWithReason": "Explain plan error: {0}", + "explain.noSql": "No SQL to explain. Build a query on the canvas first.", + "ddl.compilationFailed": "Compilation failed", + "ddl.compileErrorWithReason": "DDL compile error: {0}", + "command.undo.name": "Undo", + "command.undo.description": "Undo last action", + "command.redo.name": "Redo", + "command.redo.description": "Redo last undone action", + "command.addNode.name": "Add Node", + "command.addNode.description": "Open node search menu to add a node", + "command.bringForward.name": "Bring Forward", + "command.bringForward.description": "Move selected nodes one layer forward", + "command.sendBackward.name": "Send Backward", + "command.sendBackward.description": "Move selected nodes one layer backward", + "command.bringToFront.name": "Bring to Front", + "command.bringToFront.description": "Move selected nodes to top layer", + "command.sendToBack.name": "Send to Back", + "command.sendToBack.description": "Move selected nodes to bottom layer", + "command.normalizeLayers.name": "Normalize Layers", + "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", + "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", + "main.orphanSuffix": "Orphan(s)", + "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", + "main.namingPrefix": "Naming", + "fileHistory.compressedLabel": "Compressed:", + "schema.itemsSuffix": "item(s)", + "property.panel.title": "Properties", + "property.panel.multiSelected": "{0} nodes selected", + "sqlImporter.status.pasteSelect": "Paste a SELECT statement above, then click Import.", + "sqlImporter.status.inputTooLarge": "SQL input is too large ({0:N0} chars). Limit is {1:N0}. Split the query or increase the import limit.", + "sqlImporter.status.parsing": "Parsing SQL...", + "sqlImporter.status.done": "Done - {0} imported, {1} partial, {2} skipped.", + "sqlImporter.status.cancelledByUser": "Import cancelled by user.", + "sqlImporter.status.timeout": "Import timed out after {0:0.#}s. Try a smaller query or increase timeout.", + "sqlImporter.status.parseError": "Parse error: {0}", + "diagnostics.area.undoRedoTransaction": "Undo/Redo Transaction", + "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", + "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", + "node.preview.noCatalog": "No catalog available", + "connection.error.searchMenuNotInitialized": "search menu not initialized", + "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", + "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", + "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", + "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", + "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", + "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", + "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", + "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", + "diagnostics.area.connection": "Connection", + "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", + "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", + "undoRedo.transaction.unnamed": "unnamed transaction", + "benchmark.runLabelDefault": "Run 1", + "benchmark.runLabelPattern": "Run {0}", + "benchmark.status.failedWithReason": "Benchmark failed: {0}", + "benchmark.status.noSql": "No SQL to benchmark - build a query first.", + "benchmark.status.warmupProgress": "Warm-up {0}/{1}...", + "benchmark.status.iterationProgress": "Iteration {0}/{1}...", + "benchmark.status.done": "Done - {0}", + "benchmark.status.cancelled": "Benchmark cancelled.", + "app.windowTitle": "AkkornStudio", + "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", + "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", + "sqlImporter.error.selectFromNotFound": "Could not find SELECT ... FROM in the query.", + "sqlImporter.error.fromClauseParseFailed": "Could not parse FROM clause.", + "sqlImporter.error.syntaxUnterminatedString": "Syntax error at line {0}, column {1}: unterminated string literal.", + "sqlImporter.error.missingClosingParenthesis": "missing closing ')'", + "sqlImporter.error.unexpectedClosingParenthesis": "unexpected ')'", + "sqlImporter.error.syntaxAtLineColumn": "Syntax error at line {0}, column {1}: {2}.", + "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", + "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", + "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", + "errorDiagnostics.connection.label": "Connection failed", + "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", + "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", + "errorDiagnostics.authorization.label": "Authorization error", + "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", + "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", + "errorDiagnostics.timeout.label": "Query timeout", + "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", + "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", + "errorDiagnostics.schema.label": "Schema error", + "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", + "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", + "errorDiagnostics.syntax.label": "SQL syntax error", + "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", + "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", + "errorDiagnostics.compatibility.label": "Compatibility error", + "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", + "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", + "errorDiagnostics.unknown.label": "Unexpected error", + "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", + "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", + "error.mainWindow.invalidDataContext": "DataContext MainWindow должен быть ShellViewModel.", + "error.mainWindow.canvasNotInitialized": "CanvasViewModel не был инициализирован.", + "error.mainWindow.ddlPreviewUnavailable": "DDL-предпросмотр недоступен для текущего canvas.", + "themeJson.editor.template": "{\n \"meta\": { \"name\": \"Пользовательская тема\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", + "themeJson.error.pasteBeforeApply": "Вставьте JSON темы перед применением.", + "themeJson.error.invalidJson": "Некорректный JSON: {0}", + "themeJson.error.emptyPayload": "Некорректный JSON: пустой payload.", + "themeJson.error.invalidTheme": "Некорректная тема: {0}", + "themeJson.error.appliedButSaveFailed": "Тема применена, но не удалось сохранить: {0}", + "themeJson.success.appliedAndSaved": "JSON-тема применена и сохранена.", + "themeJson.success.customRemoved": "Пользовательская тема удалена. Перезапустите приложение для полного возврата к теме по умолчанию.", + "themeJson.error.restoreDefaultFailed": "Не удалось восстановить тему по умолчанию: {0}", + "themeValidator.error.configNull": "Конфигурация темы пуста.", + "themeValidator.warning.noSections": "В теме нет разделов цветов или типографики; применять нечего.", + "themeValidator.warning.invalidColor": "{0} содержит некорректный цвет '{1}'. Этот ключ будет проигнорирован.", + "themeValidator.warning.sizeOutOfRange": "{0}={1} вне диапазона (8..48). Этот ключ будет проигнорирован.", + "queryExecutor.error.openConnectionMethodNotFound": "Не удалось найти метод OpenConnectionAsync в оркестраторе", + "queryExecutor.error.openConnectionInvokeFailed": "Не удалось вызвать OpenConnectionAsync", + "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}': SELECT представления нельзя визуально восстановить — отредактируйте вручную в subcanvas.", + "ddlImporter.error.tableNotFoundInMetadata": "Таблица '{0}' не найдена в текущих метаданных.", + "main.window.untitled": "Без названия", + "main.subEditor.noSeedProvided": "Для {0} не предоставлен seed подредактора.", + "main.layerOrder.bringToFront": "На передний план", + "main.layerOrder.sendToBack": "На задний план", + "main.layerOrder.bringForward": "Поднять слой", + "main.layerOrder.sendBackward": "Опустить слой", + "main.layerOrder.normalizeLayers": "Нормализовать слои", + "export.fileType.html": "Файлы HTML", + "export.fileType.json": "Файлы JSON", + "export.fileType.csv": "Файлы CSV", + "export.fileType.excel": "Файлы Excel", + "commandPalette.templatePrefix": "Шаблон: {0}", + "themeLoader.status.notFoundWithPath": "Файл темы не найден: {0}", + "themeLoader.status.deserializedNull": "JSON темы десериализован в null.", + "themeLoader.status.loaded": "JSON темы успешно загружен.", + "credential.error.ciphertextTooShort": "Блок шифртекста слишком короткий.", + "credential.error.dpapiWindowsOnly": "DPAPI доступен только в Windows.", + "credential.warning.loadVaultFailed": "Не удалось загрузить хранилище учетных данных {0}: {1}", + "credential.warning.persistVaultFailed": "Не удалось сохранить хранилище учетных данных {0}: {1}", + "snippetStore.warning.loadFailed": "Не удалось загрузить сниппеты из {0}: {1}", + "snippetStore.warning.saveFailed": "Не удалось сохранить сниппеты: {0}", + "flowVersionStore.warning.loadFailed": "Не удалось загрузить версии потока из {0}: {1}", + "flowVersionStore.warning.saveFailed": "Не удалось сохранить версии потока: {0}", + "queryExecutor.error.queryEmpty": "Запрос не может быть пустым", + "queryExecutor.error.providerNotSupported": "Провайдер {0} не поддерживается", + "queryExecutor.error.singleStatementOnly": "Предпросмотр принимает только один SQL-оператор.", + "queryExecutor.error.queryEmptyWithPeriod": "Запрос не может быть пустым.", + "queryExecutor.error.readOnlyOnly": "Режим предпросмотра поддерживает только SQL-запросы только для чтения.", + "queryExecutor.error.namedParametersNotSupported": "Режим предпросмотра не поддерживает именованные параметры в SQL. Используйте безопасные литералы inline или выполните запрос вне предпросмотра.", + "queryExecutor.error.positionalParametersNotSupported": "Режим предпросмотра не поддерживает позиционные плейсхолдеры (? или ).", + "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "Выровнять выбранные узлы по нижнему краю", + "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "Выровнять выбранные узлы по левому краю", + "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "Выровнять выбранные узлы по правому краю", + "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "Выровнять выбранные узлы по верхнему краю", + "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "Применить изменения подхолста CTE и вернуться на родительский холст", + "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "Автоматически расположить узлы по логическим колонкам", + "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "Центрировать выбранные узлы по горизонтали", + "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "Центрировать выбранные узлы по вертикали", + "commandPalette.description.clear_canvas_and_start_fresh": "Очистить холст и начать заново", + "commandPalette.description.clear_node_selection": "Снять выделение узлов", + "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "Преобразовать алиасы по правилу, заданному в настройках проекта", + "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "Создавать контрольные точки, сравнивать версии и восстанавливать предыдущее состояние холста", + "commandPalette.description.delete_the_selected_nodes": "Удалить выбранные узлы", + "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "Отменить изменения в подредакторе и вернуться на родительский холст", + "commandPalette.description.execute_the_current_query_in_preview": "Выполнить текущий запрос в предпросмотре", + "commandPalette.description.fit_all_nodes_into_the_visible_area": "Уместить все узлы в видимой области", + "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "Сгенерировать CSV из первого узла экспорта CSV", + "commandPalette.description.generate_html_file_from_the_first_html_export_node": "Сгенерировать HTML из первого узла экспорта HTML", + "commandPalette.description.generate_json_file_from_the_first_json_export_node": "Сгенерировать JSON из первого узла экспорта JSON", + "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "Сгенерировать книгу XLSX из первого узла экспорта Excel", + "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "Проверить план выполнения запроса: типы сканирования, стратегии join и оценки стоимости", + "commandPalette.description.load_a_vsaq_canvas_file": "Загрузить файл холста .vsaq", + "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "Измерить avg / median / p95 задержку текущего SQL за N итераций", + "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "Открыть изолированный редактор подхолста для выбранного узла определения CTE", + "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "Открыть локальную историю версий, создаваемую при сохранении, и восстановить предыдущие снимки", + "commandPalette.description.open_output_preview_modal_for_the_active_mode": "Открыть модальное окно предпросмотра вывода для активного режима", + "commandPalette.description.open_shortcut_reference_screen": "Открыть экран справки по горячим клавишам", + "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "Открыть менеджер подключений для добавления, редактирования и переключения БД-подключений", + "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "Вставить SELECT и автоматически сгенерировать узлы — поддерживаются FROM, JOIN, WHERE, LIMIT", + "commandPalette.description.remove_all_nodes_not_connected_to_output": "Удалить все узлы, не подключенные к выходу", + "commandPalette.description.reset_zoom_and_pan_to_default": "Сбросить масштаб и смещение к значениям по умолчанию", + "commandPalette.description.save_canvas_to_a_new_file": "Сохранить холст в новый файл", + "commandPalette.description.save_current_canvas": "Сохранить текущий холст", + "commandPalette.description.save_markdown_documentation_of_the_current_flow": "Сохранить Markdown-документацию текущего потока", + "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "Сохранить выбранные узлы как переиспользуемый сниппет и вставить позже через меню поиска узлов (⇧A)", + "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "Проверить все исходные табличные узлы на холсте на возможные связи join по FK-конвенциям и шаблонам имен", + "commandPalette.description.select_all_nodes_on_canvas": "Выбрать все узлы на холсте", + "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "Привязать позиции узлов к сетке 16px (Ctrl+G)", + "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "Распределить выбранные узлы с равным горизонтальным интервалом", + "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "Распределить выбранные узлы с равным вертикальным интервалом", + "commandPalette.description.zoom_into_the_canvas": "Увеличить холст", + "commandPalette.description.zoom_out_of_the_canvas": "Уменьшить холст", + "commandPalette.name.align_bottom": "Выровнять по низу", + "commandPalette.name.align_left": "Выровнять по левому краю", + "commandPalette.name.align_right": "Выровнять по правому краю", + "commandPalette.name.align_top": "Выровнять по верху", + "commandPalette.name.analyze_all_joins": "Анализировать все JOIN", + "commandPalette.name.auto_fix_naming": "Автоисправление имен", + "commandPalette.name.auto_layout": "Авторазмещение", + "commandPalette.name.center_horizontally": "Центрировать по горизонтали", + "commandPalette.name.center_vertically": "Центрировать по вертикали", + "commandPalette.name.cleanup_orphans": "Очистить сироты", + "commandPalette.name.delete_selected": "Удалить выделенные", + "commandPalette.name.deselect_all": "Снять выделение", + "commandPalette.name.discard_and_exit_editor": "Отменить и выйти из редактора", + "commandPalette.name.distribute_horizontally": "Распределить по горизонтали", + "commandPalette.name.distribute_vertically": "Распределить по вертикали", + "commandPalette.name.edit_selected_cte": "Редактировать выбранный CTE", + "commandPalette.name.exit_cte_editor": "Выйти из редактора CTE", + "commandPalette.name.explain_plan": "План выполнения", + "commandPalette.name.export_csv": "Экспорт CSV", + "commandPalette.name.export_documentation": "Экспорт документации", + "commandPalette.name.export_excel": "Экспорт Excel", + "commandPalette.name.export_html": "Экспорт HTML", + "commandPalette.name.export_json": "Экспорт JSON", + "commandPalette.name.file_save_load_history": "История сохранений/загрузок", + "commandPalette.name.fit_to_screen": "Уместить на экран", + "commandPalette.name.flow_version_history": "История версий потока", + "commandPalette.name.import_sql_to_graph": "Импорт SQL в граф", + "commandPalette.name.keyboard_shortcuts": "Горячие клавиши", + "commandPalette.name.manage_connections": "Управление подключениями", + "commandPalette.name.new_canvas": "Новый canvas", + "commandPalette.name.open_file": "Открыть файл", + "commandPalette.name.reset_viewport": "Сбросить область просмотра", + "commandPalette.name.run_preview": "Запустить предпросмотр", + "commandPalette.name.run_query_benchmark": "Запустить бенчмарк запроса", + "commandPalette.name.save": "Сохранить", + "commandPalette.name.save_as": "Сохранить как", + "commandPalette.name.save_selection_as_snippet": "Сохранить выделение как сниппет", + "commandPalette.name.select_all": "Выделить все", + "commandPalette.name.toggle_preview": "Переключить предпросмотр", + "commandPalette.name.toggle_snap_to_grid": "Переключить привязку к сетке", + "commandPalette.name.zoom_in": "Увеличить", + "commandPalette.name.zoom_out": "Уменьшить", + "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", + "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", + "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", + "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", + "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", + "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", + "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", + "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", + "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", + "commandPalette.tags.clear_selection": "clear selection", + "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", + "commandPalette.tags.create_insert_search_transform": "create insert search transform", + "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", + "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", + "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", + "commandPalette.tags.data_results_table_panel": "data results table panel", + "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", + "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", + "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", + "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", + "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", + "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", + "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", + "commandPalette.tags.export_json_file_output_save": "export json file output save", + "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", + "commandPalette.tags.export_persist_copy": "export persist copy", + "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", + "commandPalette.tags.forward_history": "forward history", + "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", + "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", + "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", + "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", + "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", + "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", + "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", + "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", + "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", + "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", + "commandPalette.tags.load_import_vsaq": "load import vsaq", + "commandPalette.tags.magnify_enlarge": "magnify enlarge", + "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", + "commandPalette.tags.persist_write_disk": "persist write disk", + "commandPalette.tags.remove_erase_nodes": "remove erase nodes", + "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", + "commandPalette.tags.reset_clear_blank": "reset clear blank", + "commandPalette.tags.revert_back_history": "revert back history", + "commandPalette.tags.shrink_reduce": "shrink reduce", + "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", + "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", + "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", + "sqlEditor.diffPreview.title": "Transactional Diff Preview", + "sqlEditor.mutation.confirmExecute": "Confirm Execute", + "sqlEditor.tab.closeAnyway": "Close Anyway", + "sqlEditor.tab.keepTab": "Keep Tab", + "sqlEditor.status.ready": "Ready.", + "sqlEditor.telemetry.none": "No execution telemetry yet.", + "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", + "sqlEditor.telemetry.errors.none": "No aggregated errors.", + "sqlEditor.diff.none": "No transactional diff preview available.", + "sqlEditor.mutation.estimate.none": "No mutation estimate available.", + "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", + "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", + "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", + "sqlEditor.tab.noPendingClose": "No tab close pending.", + "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", + "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", + "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", + "sqlEditor.message.empty": "Execute a statement to see messages.", + "sqlEditor.message.success": "Execution completed successfully.", + "sqlEditor.result.summary.empty": "Rows: - Time: -", + "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", + "sqlEditor.file.save.canceled": "Save canceled.", + "sqlEditor.file.save.noPath": "No target path selected.", + "sqlEditor.file.save.success": "SQL file saved.", + "sqlEditor.file.save.failed": "Save failed.", + "sqlEditor.file.open.failed": "Open failed.", + "sqlEditor.file.open.notFound": "Selected SQL file was not found.", + "sqlEditor.file.open.success": "SQL file opened.", + "sqlEditor.status.executing": "Executing SQL...", + "sqlEditor.status.executingScript": "Executing SQL script...", + "sqlEditor.status.executingStep": "Executing {0}/{1}...", + "sqlEditor.status.canceling": "Canceling execution...", + "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", + "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", + "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", + "sqlEditor.status.success": "Execution succeeded.", + "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", + "sqlEditor.status.canceled": "Execution canceled.", + "sqlEditor.status.failed": "Execution failed.", + "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", + "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", + "sqlEditor.result.tabTitle": "Result {0}", + "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", + "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", + "sqlEditor.tab.closed": "Tab closed.", + "sqlEditor.tab.closeCanceled": "Tab close canceled.", + "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", + "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", + "sqlEditor.error.noConnection": "No active database connection for SQL execution.", + "sqlEditor.error.executionCanceled": "SQL execution was canceled.", + "sqlEditor.tab.scriptTitle": "Script {0}", + "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", + "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", + "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", + "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", + "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", + "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", + "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", + "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", + "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", + "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", + "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", + "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", + "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", + "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", + "sqlEditor.results.title": "Results", + "sqlEditor.saveSql.fileType": "SQL Files", + "sqlEditor.saveSql.pickerTitle": "Save SQL File", + "sqlEditor.export.pickerTitle": "Export SQL Data", + "sqlEditor.export.status.noResultTitle": "No execution result available for export.", + "sqlEditor.export.status.noResultDetail": "Execute a query first.", + "sqlEditor.export.status.successTitle": "Report exported.", + "sqlEditor.export.status.failedTitle": "Failed to export report.", + "sqlEditor.export.fileType.html": "HTML File", + "sqlEditor.export.fileType.json": "JSON File", + "sqlEditor.export.fileType.csv": "CSV File", + "sqlEditor.export.fileType.xlsx": "Excel Workbook", + "sqlEditor.export.defaultFileBase": "report", + "sqlEditor.export.defaultTitle": "SQL Report", + "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", + "sqlEditor.export.type.html.title": "HTML full-feature report", + "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", + "sqlEditor.export.type.json.title": "JSON execution contract", + "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", + "sqlEditor.export.type.csv.title": "CSV data export", + "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", + "sqlEditor.export.type.xlsx.title": "Excel workbook export", + "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", + "sqlEditor.export.dialog.windowTitle": "Export SQL Data", + "sqlEditor.export.dialog.title": "Export SQL Data", + "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", + "sqlEditor.export.dialog.confirm": "Export", + "sqlEditor.export.dialog.fileNameWatermark": "report.html", + "sqlEditor.export.dialog.titleWatermark": "SQL Report", + "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", + "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", + "sqlEditor.export.dialog.section.fileName": "FILE NAME", + "sqlEditor.export.dialog.section.reportTitle": "TITLE", + "sqlEditor.export.dialog.section.description": "DESCRIPTION", + "sqlEditor.export.dialog.section.options": "OPTIONS", + "sqlEditor.export.option.includeSchema": "Include output schema", + "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", + "sqlEditor.export.option.includeMetadata": "Include optional metadata", + "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", + "sqlEditor.export.badge.offline": "OFFLINE READY", + "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", + "sqlEditor.export.badge.dataOnly": "DATA ONLY" +} diff --git a/src/DBWeaver.UI/Assets/Localization/zh-TW.json b/src/AkkornStudio.UI/Assets/Localization/zh-TW.json similarity index 98% rename from src/DBWeaver.UI/Assets/Localization/zh-TW.json rename to src/AkkornStudio.UI/Assets/Localization/zh-TW.json index a96e8267..8c4c410a 100644 --- a/src/DBWeaver.UI/Assets/Localization/zh-TW.json +++ b/src/AkkornStudio.UI/Assets/Localization/zh-TW.json @@ -1,1057 +1,1057 @@ -{ - "main.brand": "DBWeaver", - "main.tab.query1": "Query 1", - "main.new": "New", - "main.open": "Open", - "main.save": "Save", - "main.history": "History", - "main.layout": "Layout", - "main.preview": "Preview", - "main.undo": "Undo", - "main.redo": "Redo", - "main.cleanupOrphans": "Cleanup orphan nodes", - "main.autoFixAliasNaming": "Auto-fix alias naming", - "main.autoLayoutCanvas": "Auto layout canvas", - "main.toggleDataPreview": "Toggle data preview", - "main.language": "Language", - "main.restore.prompt": "Previous session found — restore the last canvas?", - "main.restore.button": "Restore session", - "main.cteEditor.editingPrefix": "Editing CTE: ", - "main.cteEditor.backToCanvas": "Back to Canvas", - "main.cteEditor.exitA11y": "Exit CTE editor", - "main.viewEditor.editingPrefix": "DDL > View: ", - "main.viewEditor.backToCanvas": "Back to DDL", - "main.viewEditor.exitA11y": "Exit view editor", - "connection.title": "Connection Manager", - "connection.subtitle": "不中斷流程即可設定、測試與啟用連線", - "connection.none": "No connection", - "connection.active": "ACTIVE", - "connection.health.online": "Online", - "connection.health.degraded": "Degraded", - "connection.health.offline": "Offline", - "connection.tooltip.none": "No active connection — click to manage", - "connection.ping": "Ping", - "connection.saved": "SAVED CONNECTIONS", - "connection.new": "New Connection", - "connection.selectOrCreate": "Select a connection or create a new one", - "connection.name": "Connection Name", - "connection.provider": "Provider", - "connection.host": "Host", - "connection.port": "Port", - "connection.database": "Database", - "connection.sqlitePath": "SQLite Path", - "connection.sqliteBrowse": "Browse", - "connection.sqliteCreate": "Create", - "connection.username": "Username", - "connection.password": "Password", - "connection.timeout": "Timeout (seconds)", - "connection.test": "Test", - "connection.save": "Save", - "connection.connect": "Connect", - "connection.action.testConnection": "Test connection", - "connection.action.saveConnection": "Save connection", - "connection.action.connectConnection": "Connect connection", - "connection.status.connecting": "連線中...", - "connection.status.connected": "已連線", - "connection.status.testing": "測試中...", - "connection.status.failedPrefix": "連線失敗", - "connection.status.metadataUnavailable": "連線失敗:中繼資料不可用。", - "connection.status.highLatency": "高延遲", - "connection.watermark.name": "我的正式環境資料庫", - "connection.watermark.host": "localhost", - "connection.watermark.port": "5432", - "connection.watermark.database": "database_name", - "connection.watermark.username": "user", - "connection.watermark.password": "••••••••", - "connection.watermark.timeout": "30", - "main.connectingDb": "Connecting to database...", - "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", - "status.nodesSeparator": " nodes · ", - "status.connectionsSuffix": " connections", - "status.undo": "Undo: ", - "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", - "connection.disconnect": "Disconnect", - "connection.action.disconnectConnection": "Disconnect connection", - "connectionTab.active": "ACTIVE CONNECTION", - "connectionTab.none": "No active connection", - "connectionTab.saved": "SAVED CONNECTIONS", - "connectionTab.new": "+ New Connection", - "schema.database": "DATABASE", - "schema.search": "Search tables, columns...", - "schema.loading": "Searching tables, columns...", - "schema.noConnection": "No Connection", - "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", - "schema.emptyNoTables": "No tables found", - "fileHistory.title": "Save/Load Version History", - "fileHistory.reload": "Reload", - "fileHistory.restoreSelected": "Restore selected", - "fileHistory.empty": "No local versions yet", - "fileHistory.emptyHint": "Save this file to generate version history.", - "preview.title": "Data Preview", - "preview.subtitle": "Review data and diagnostics before continuing", - "preview.run": "Run", - "preview.cancel": "Cancel", - "preview.tab.preview": "Preview", - "preview.tab.sql": "SQL", - "preview.close": "Close preview", - "preview.running": "Running preview query… ", - "preview.clickCancel": "Click Cancel to stop", - "preview.cancelled": "Query cancelled", - "preview.runAgain": "Press Run to execute again", - "preview.failed": "Preview execution failed", - "preview.technical": "TECHNICAL DETAILS", - "preview.noData": "No data yet", - "preview.f3Hint": "Press F3 or Space to run the current query", - "sqlImporter.title": "Import SQL to Graph", - "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", - "sqlImporter.sqlStatement": "SQL STATEMENT", - "sqlImporter.supported": "Supported: ", - "sqlImporter.import": "Import", - "sqlImporter.report": "CONVERSION REPORT", - "sqlEditor.mutation.dialogTitle": "變更確認", - "sqlEditor.mutation.dialogSubtitle": "在確認執行前請先檢視影響", - "search.empty": "No nodes found", - "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", - "search.shortcut": "⇧A", - "search.spawn": "Spawn", - "commandPalette.empty": "沒有符合搜尋的命令", - "commandPalette.search": "命令搜尋", - "commandPalette.shortcut": "CTRL+SHIFT+P", - "commandPalette.execute": "執行", - "context.editCte": "Edit Selected CTE", - "context.editViewSubcanvas": "Edit View Subcanvas", - "explain.title": "Explain Plan", - "explain.sql": "SQL", - "explain.option.analyze": "分析", - "explain.option.buffers": "緩衝區", - "explain.badge.simulated": "模擬", - "explain.timing.planning": "規劃:", - "explain.timing.execution": "執行:", - "explain.section.snapshotComparison": "快照比較", - "explain.section.indexRecommendations": "索引建議", - "explain.section.history": "歷程", - "explain.detail.estimated": "估計", - "explain.detail.actual": "實際", - "explain.detail.error": "誤差", - "explain.detail.time": "時間", - "explain.detail.loops": "迴圈", - "explain.rerun": "Re-run", - "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", - "explain.running": "Running EXPLAIN…", - "explain.failed": "⚠ EXPLAIN failed", - "explain.noPlan": "No plan yet", - "explain.rerunHint": "Press Re-run to execute EXPLAIN", - "explain.header.operation": "操作", - "explain.header.cost": "成本", - "explain.header.rows": "資料列", - "explain.header.alert": "警示", - "explain.mode.list": "清單", - "explain.mode.tree": "樹狀", - "explain.action.snapshot": "儲存快照", - "explain.action.copyJson": "複製 JSON", - "explain.action.copyText": "複製文字", - "explain.action.saveJson": "儲存 .json", - "explain.action.openDalibo": "在 Dalibo 開啟", - "explain.legend.seqscan": "SEQ SCAN — full table read, no index", - "explain.legend.sort": "SORT — in-memory sort", - "explain.legend.hash": "HASH — hash join", - "explain.escClose": "Esc to close", - "flowVersion.title": "Flow Version History", - "flowVersion.subtitle": "Create checkpoints, compare versions and restore", - "flowVersion.watermark": "Checkpoint label (optional)…", - "flowVersion.saveCheckpoint": "Save Checkpoint", - "flowVersion.compareMode": "Compare Mode", - "flowVersion.selectBase": "Select BASE version (from):", - "flowVersion.clickAny": "Then click any version in the list below to compare.", - "flowVersion.noCheckpoints": "No checkpoints yet", - "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", - "flowVersion.restore": "Restore", - "flowVersion.diffResults": "Diff Results", - "benchmark.title": "Query Benchmark", - "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", - "benchmark.sql": "SQL being benchmarked", - "benchmark.runLabel": "Run label", - "benchmark.runLabelWatermark": "Run 1", - "benchmark.iterations": "Iterations (1–100)", - "benchmark.warmup": "Warm-up passes (0–10)", - "benchmark.interval": "Interval between runs (ms)", - "benchmark.run": "Run Benchmark", - "benchmark.cancel": "Cancel", - "benchmark.clearHistory": "Clear History", - "benchmark.latest": "LATEST RESULT", - "benchmark.avg": "AVG", - "benchmark.median": "MEDIAN", - "benchmark.min": "MIN", - "benchmark.max": "MAX", - "benchmark.iterationsAt": " iterations · run at ", - "benchmark.history": "HISTORY", - "benchmark.header.label": "標籤", - "benchmark.header.avg": "平均", - "benchmark.header.median": "中位數", - "benchmark.header.min": "最小", - "benchmark.header.max": "最大", - "benchmark.itersSuffix": " iters", - "diagnostics.title": "App Diagnostics", - "diagnostics.run": "Run", - "diagnostics.running": "Running checks…", - "diagnostics.ok": "OK", - "diagnostics.warning": "Warning", - "diagnostics.error": "Error", - "diagnostics.tooltip.rerun": "Re-run all checks", - "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", - "diagnostics.tooltip.close": "Close (Esc)", - "autoJoin.title": "Auto-Join Suggestions", - "autoJoin.titleForTable": "Auto-Join suggestions for {0}", - "autoJoin.acceptAll": "Accept All", - "autoJoin.accept": "Accept", - "autoJoin.skip": "Skip", - "autoJoin.allHandled": "All suggestions handled", - "autoJoin.joinKeyword": "JOIN", - "autoJoin.confidence.fkConstraint": "FK Constraint", - "autoJoin.confidence.fkReverse": "FK (Reverse)", - "autoJoin.confidence.namingMatch": "Naming Match", - "autoJoin.confidence.weakMatch": "Weak Match", - "autoJoin.runSelected": "Auto-Join Selected", - "autoJoin.noSimilarityTitle": "No automatic join found", - "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", - "autoJoin.appliedTitle": "Auto-join applied", - "autoJoin.manual.title": "Create Manual Join", - "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", - "autoJoin.manual.leftColumn": "Left column", - "autoJoin.manual.rightColumn": "Right column", - "autoJoin.manual.joinType": "Join type", - "autoJoin.manual.operator": "Operator", - "autoJoin.manual.confirm": "Create join", - "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", - "autoJoin.manualJoinCreatedTitle": "Manual join created", - "autoJoin.manualJoinFailedTitle": "Manual join could not be created", - "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", - "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", - "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", - "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", - "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", - "property.outputAlias": "OUTPUT ALIAS", - "property.sourceAlias": "SOURCE ALIAS", - "property.aliasWatermark": "e.g. MyColumn (optional)", - "property.parameters": "PARAMETERS", - "property.enabled": "Enabled", - "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", - "property.dateWatermark": "YYYY-MM-DD or leave empty", - "property.apply": "Apply", - "property.inputPins": "INPUT PINS", - "property.outputPins": "OUTPUT PINS", - "property.sqlTrace": "SQL TRACE", - "property.live": "live", - "node.numericValue": "Numeric Value", - "node.stringValue": "String Value", - "node.enterText": "Enter text", - "node.datetimeValue": "DateTime Value", - "node.valueLabel": "Value:", - "node.noInputs": "No inputs", - "node.loadingSample": "Loading sample…", - "node.previewFailed": "⚠ Preview failed", - "node.sampleRowsHint": "5 sample rows · demo data", - "sidebar.tab.nodes": "節點", - "sidebar.tab.connection": "連線", - "sidebar.tab.schema": "結構", - "sidebar.tab.diagnostics": "診斷", - "sidebar.addNode": "+ Add Node (⇧A)", - "sidebar.previewF3": "Preview (F3)", - "nodesList.search": "Search nodes...", - "search.watermark": "Search nodes… (Esc to close)", - "search.snippets": "★ SNIPPETS", - "commandPalette.watermark": "執行命令…(Esc 關閉)", - "tooltip.newCanvas": "New canvas (Ctrl+N)", - "tooltip.openCanvas": "Open canvas (Ctrl+O)", - "tooltip.saveCanvas": "Save canvas (Ctrl+S)", - "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", - "tooltip.zoomOut": "Zoom out (Ctrl+-)", - "tooltip.zoomIn": "Zoom in (Ctrl++)", - "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", - "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", - "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", - "tooltip.dataPreview": "Data preview (F3)", - "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", - "tooltip.appDiagnostics": "App Diagnostics (self-check)", - "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", - "tooltip.cancelRunningQuery": "Cancel the running query", - "tooltip.closeEsc": "Close (Esc)", - "tooltip.recheckConnectionHealth": "Re-check connection health", - "tooltip.deleteConnection": "Delete connection", - "tooltip.testConnection": "Test connection", - "tooltip.saveConnection": "Save connection", - "tooltip.activateConnection": "Activate this connection", - "tooltip.toggleDataSamplePreview": "Toggle data sample preview", - "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", - "tooltip.copySql": "Copy SQL to clipboard", - "tooltip.formatSql": "Format SQL", - "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", - "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", - "tooltip.switchToQueryMode": "Switch to Query canvas", - "tooltip.switchToDdlMode": "Switch to DDL canvas", - "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", - "tooltip.pins.inputs": "Inputs", - "tooltip.pins.outputs": "Outputs", - "tooltip.pins.none": "None", - "tooltip.tableColumns": "Columns", - "tooltip.tableColumns.none": "No detailed columns", - "window.minimize": "Minimize window", - "window.maximizeRestore": "Maximize/restore window", - "window.close": "Close window", - "menu.newDiagram": "New diagram", - "menu.openFile": "Open file", - "menu.save": "Save", - "menu.fileHistory": "File history", - "menu.shortcuts": "Keyboard shortcuts", - "menu.settings": "Settings", - "menu.importDdlSchema": "Import DDL schema", - "menu.viewDdlSql": "View DDL SQL", - "menu.executeDdl": "Execute DDL", - "menu.backToStart": "Back to start", - "toast.ddlExecuteFailed": "Failed to execute DDL.", - "toast.ddlOpenFailed": "Failed to open DDL SQL.", - "toast.ddlImportFailed": "Failed to import schema into DDL.", - "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", - "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", - "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", - "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", - "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", - "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", - "toast.ddlTableImported": "Table imported into the DDL canvas.", - "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", - "toast.ddlNoActiveConnection": "No active connection to execute DDL.", - "toast.ddlExecutedSuccess": "DDL executed successfully.", - "toast.ddlExecutedWithIssues": "DDL executed with issues.", - "toast.switchToDdl": "Switch to DDL mode to generate SQL.", - "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", - "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", - "toast.previewOpenFailed": "Failed to open preview.", - "tab.switchFailed": "Failed to switch tab: {0}", - "settings.status.darkApplied": "Dark theme applied.", - "settings.status.lightApplied": "Light theme applied.", - "settings.status.snapUpdated": "Snap updated: {0}.", - "settings.status.languageToggled": "Language toggled.", - "settings.status.languageSelected": "Language selected: {0}.", - "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", - "settings.section.appearance.title": "Themes", - "settings.section.languageRegion.title": "語言與地區", - "settings.section.dateTime.title": "日期與時間", - "settings.section.keyboard.title": "鍵盤快速鍵", - "settings.section.privacy.title": "隱私", - "settings.section.notification.title": "通知", - "settings.section.accessibility.title": "無障礙", - "settings.section.default.title": "設定", - "settings.section.appearance.subtitle": "選擇你的風格或自訂主題", - "settings.section.languageRegion.subtitle": "管理語言與地區格式", - "settings.section.keyboard.subtitle": "自訂 command palette 與畫布執行所使用的鍵盤快速鍵。", - "settings.section.wip.subtitle": "開發中。", - "settings.section.default.subtitle": "應用程式設定", - "settings.general": "General", - "settings.nav.appearance": "Appearance", - "settings.theme.light": "淺色模式", - "settings.theme.dark": "深色模式", - "settings.theme.system": "系統偏好", - "settings.gridSnap.title": "Grid Snap", - "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", - "settings.language.subtitle": "請選擇應用程式語言。", - "settings.language.toggle": "切換語言", - "settings.language.option.ptBR": "葡萄牙語(巴西)", - "settings.language.option.enUS": "英語(美國)", - "settings.language.option.esES": "西班牙語(西班牙)", - "settings.language.option.ruRU": "俄語", - "settings.language.option.jaJP": "日語", - "settings.language.option.zhTW": "繁體中文", - "settings.themeJson.title": "主題 JSON", - "settings.themeJson.subtitle": "貼上你的主題 JSON,立即套用並保存。", - "settings.themeJson.apply": "套用 JSON", - "settings.themeJson.restoreDefault": "還原預設主題", - "mode.query": "Query", - "mode.ddl": "DDL", - "sidebar.left.close": "Close left sidebar", - "sidebar.left.open": "Reopen left sidebar", - "sidebar.right.close": "Close right sidebar", - "sidebar.right.open": "Reopen right sidebar", - "connection.completedTitle": "Connection completed", - "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", - "connection.close": "Close connection manager", - "connection.refreshHealth": "Refresh connection health", - "common.details": "Details", - "common.cancel": "Cancel", - "common.keep": "Keep", - "common.clear": "Clear", - "zoom.out": "Zoom out", - "zoom.in": "Zoom in", - "zoom.fit": "Fit zoom to screen", - "zoom.level": "Zoom level", - "settings.theme.mode": "主題模式", - "diagnostics.category.canvas": "Canvas Integrity", - "diagnostics.category.output": "Output & Execution", - "diagnostics.category.session": "Session & Safety", - "diagnostics.category.notice": "Runtime Notices", - "diagnostics.summary.ok": "All systems OK", - "diagnostics.summary.warningCount": "{0} warning(s) detected", - "diagnostics.summary.errorCount": "{0} error(s) detected", - "diagnostics.canvasMigration": "Canvas Migration", - "diagnostics.recommendation.resaveFile": "Re-save the file to update it to the latest schema version.", - "diagnostics.canvasState.name": "Canvas State", - "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", - "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", - "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", - "diagnostics.validation.name": "Validation Errors", - "diagnostics.validation.recommendation": "Fix highlighted nodes before running output preview", - "diagnostics.validation.errorWithWarnings": "{0} error(s) and {1} warning(s) in the graph", - "diagnostics.validation.warningOnly": "{0} warning(s) in the graph", - "diagnostics.validation.none": "No validation issues", - "diagnostics.orphan.name": "Orphan Nodes", - "diagnostics.orphan.recommendation": "Use the orphan cleanup action to remove unused nodes", - "diagnostics.orphan.count": "{0} node(s) not connected to any output", - "diagnostics.orphan.none": "No orphan nodes detected", - "diagnostics.naming.name": "Naming Conventions", - "diagnostics.naming.recommendation": "Use auto-fix alias naming when conformance is below 100%", - "diagnostics.naming.conformance": "Naming conformance: {0}%", - "diagnostics.naming.ok": "All aliases follow naming conventions (100%)", - "diagnostics.queryCompilation.name": "Live SQL Compilation", - "diagnostics.queryCompilation.recommendation": "Review SQL diagnostics in output when errors/warnings are reported.", - "diagnostics.queryCompilation.errorFallback": "Live SQL compilation reported errors.", - "diagnostics.queryCompilation.warningCounts": "{0} diagnostic item(s), {1} guardrail warning(s).", - "diagnostics.queryCompilation.ok": "Live SQL compiled without diagnostics.", - "diagnostics.previewSafety.name": "Preview Safety", - "diagnostics.previewSafety.recommendation": "Preview executes read-only statements only.", - "diagnostics.previewSafety.blocked": "Current SQL is mutating and blocked by Safe Preview mode.", - "diagnostics.previewSafety.ok": "Preview safety checks passed.", - "diagnostics.previewExecution.name": "Preview Execution", - "diagnostics.previewExecution.recommendation": "Run preview and inspect diagnostics for execution/runtime errors.", - "diagnostics.previewExecution.failed": "Preview execution failed.", - "diagnostics.previewExecution.cancelled": "Preview execution was cancelled.", - "diagnostics.previewExecution.done": "{0} row(s) in {1}ms.", - "diagnostics.previewExecution.none": "No preview execution issues detected.", - "diagnostics.ddlCompilation.name": "DDL Compilation", - "diagnostics.ddlCompilation.recommendation": "Fix DDL compile diagnostics before execution.", - "diagnostics.ddlCompilation.failed": "DDL compilation failed.", - "diagnostics.ddlCompilation.warningCount": "{0} warning(s) reported by DDL compiler.", - "diagnostics.ddlCompilation.ok": "DDL compilation succeeded.", - "diagnostics.ddlOutput.name": "DDL Output", - "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", - "diagnostics.ddlOutput.none": "No DDL statements generated yet.", - "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", - "diagnostics.undo.name": "Undo History", - "diagnostics.undo.recommendation": "History is in-memory only; save your canvas regularly.", - "diagnostics.undo.saved": "Canvas is saved (no unsaved changes).", - "diagnostics.undo.unsavedDeep": "Unsaved changes with {0} undo steps.", - "diagnostics.undo.unsaved": "Unsaved changes - {0} undo step(s) available.", - "diagnostics.report.title": "DBWeaver - Diagnostic Report", - "diagnostics.report.generated": "Generated", - "diagnostics.report.overall": "Overall", - "diagnostics.report.details": "Details", - "diagnostics.report.recommendation": "Recommendation", - "diagnostics.report.lastCheck": "Last Check", - "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", - "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", - "preview.providerLabel": "Provider", - "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", - "common.navigate": "Navigate", - "common.close": "Close", - "common.esc": "Esc", - "common.ms": "ms", - "common.zero": "0", - "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", - "nodesList.empty": "No nodes found", - "nodesList.emptyHint": "Adjust the search term to explore available types", - "schema.emptyFiltered": "No objects found for the current filter", - "start.lastSnapshot": "Last snapshot", - "app.brandBadge": "VS", - "property.tab.properties": "Properties", - "property.tab.projectSettings": "Project Settings", - "property.nodeType": "NODE TYPE", - "property.selectNodeHint": "Select a node to edit its properties.", - "property.namingConventions": "Naming Conventions", - "property.aliasConvention": "Alias convention", - "property.enforceAliasNaming": "Enforce alias naming", - "property.warnReservedSql": "Warn on reserved SQL keywords", - "property.maxAliasLength": "Max alias length", - "property.maxAliasLengthDefault": "64", - "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", - "start.tips": "Tips", - "start.tips.quick": "Quick tips", - "start.tips.item1": "1. Click New Diagram to start from scratch.", - "start.tips.item2": "2. Use templates to speed up prototyping.", - "start.tips.item3": "3. Open saved connections to load real tables.", - "start.tips.shortcut": "Shortcut: CTRL+SHIFT+P opens the command palette.", - "start.workspace": "WORKSPACE", - "start.resumeTitle": "Continue where you left off", - "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", - "start.chip.quickFlow": "Quick flow", - "start.chip.templates": "Templates", - "start.chip.connections": "Connections", - "start.savedConnectionsTitle": "Saved Connections", - "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", - "start.noConnectionsTitle": "No connections configured yet", - "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", - "start.newConnection": "+ New Connection", - "start.recentProjectsTitle": "Recent Projects", - "start.searchRecent": "Search recent project...", - "start.quickActions": "Quick actions", - "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", - "start.noRecentTitle": "No recent projects yet", - "start.noRecentSubtitle": "Use the quick actions card above to get started.", - "start.exploreTemplates": "Explore templates", - "start.templatesFavoritesHint": "Favorites on top", - "start.favoriteTemplate": "Favorite template", - "node.columnSetPreview": "ColumnSet preview", - "node.view": "VIEW", - "node.tableDefinition": "Table Definition", - "node.join": "JOIN", - "node.window.addPartition": "Add PARTITION BY slot", - "node.window.removePartition": "Remove PARTITION BY slot", - "node.window.addOrder": "Add ORDER BY slot", - "node.window.removeOrder": "Remove ORDER BY slot", - "sql.keyword.select": "SELECT", - "sql.keyword.from": "FROM", - "sql.keyword.join": "JOIN", - "sql.keyword.where": "WHERE", - "sql.keyword.limit": "LIMIT", - "sqlImporter.close": "Close SQL importer", - "sqlImporter.report.imported": "Imported", - "sqlImporter.report.partial": "Partial", - "sqlImporter.report.skipped": "Skipped", - "benchmark.close": "Close benchmark", - "benchmark.p95": "P95", - "benchmark.n": "N", - "liveSql.safePreview": "SAFE PREVIEW MODE", - "liveSql.title": "LIVE SQL", - "liveSql.blocked": "BLOCKED", - "liveSql.copy": "Copy", - "liveSql.format": "Format", - "liveSql.benchmark": "Benchmark", - "liveSql.explain": "Explain", - "liveSql.actionsHint": "Performance tools", - "ddl.dialog.title": "Execute DDL", - "ddl.dialog.execute": "Execute", - "ddl.dialog.cancel": "Cancel", - "ddl.dialog.close": "Close", - "ddl.dialog.stopOnError": "Stop on first failure", - "ddl.dialog.confirmDestructive": "I confirm execution of destructive statements (DROP TABLE)", - "ddl.dialog.reviewBeforeRun": "Review the DDL script before confirming.", - "ddl.dialog.confirmQuestion": "Confirm DDL execution on the connected database?", - "ddl.dialog.irreversibleWarning": "This action can change the schema irreversibly.", - "ddl.dialog.mustConfirmDestructive": "Confirm destructive execution to continue.", - "ddl.dialog.executing": "Executing...", - "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", - "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", - "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", - "ddl.execute.result.failed": "Failed to execute DDL.", - "ddl.execute.result.cancelled": "Execution cancelled by the user.", - "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", - "context.deleteSingle": "Delete {0}", - "context.deleteMultiple": "Delete {0} nodes", - "context.bringForward": "Bring Forward (Ctrl+PgUp)", - "context.sendBackward": "Send Backward (Ctrl+PgDown)", - "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", - "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", - "context.normalizeLayers": "Normalize Layers", - "context.deleteWire": "Delete wire", - "context.addNode": "Add Node (Shift+A)", - "context.undoWithDescription": "Undo {0}", - "context.redo": "Redo", - "shortcuts.windowTitle": "Keyboard Shortcuts", - "shortcuts.headerTitle": "DBWeaver - Shortcuts", - "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", - "shortcuts.filterWatermark": "Filter shortcuts by key or action...", - "shortcuts.resultCount": "{0} shortcuts", - "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", - "shortcuts.noneFound": "No shortcuts found.", - "shortcuts.section.fileGeneral": "檔案與一般", - "shortcuts.section.editing": "編輯", - "shortcuts.section.canvasNavigation": "畫布與導覽", - "shortcuts.section.zoomPanPrecision": "縮放、平移與精準控制", - "shortcuts.section.previewInspection": "預覽與檢視", - "shortcuts.key.deleteOrBackspace": "Del 或 Backspace", - "shortcuts.key.middleDrag": "中鍵 + 拖曳", - "shortcuts.key.rightDrag": "右鍵 + 拖曳", - "shortcuts.key.spaceDrag": "空白鍵 + 拖曳", - "shortcuts.key.altLeftDrag": "Alt + 左鍵拖曳", - "shortcuts.key.arrows": "方向鍵", - "shortcuts.key.shiftArrows": "Shift + 方向鍵", - "shortcuts.action.openShortcutScreen": "開啟此快捷鍵畫面", - "shortcuts.action.newCanvas": "新畫布", - "shortcuts.action.openFile": "開啟檔案", - "shortcuts.action.save": "儲存", - "shortcuts.action.saveAs": "另存新檔", - "shortcuts.action.commandPalette": "命令面板", - "shortcuts.action.undo": "復原", - "shortcuts.action.redo": "重做", - "shortcuts.action.selectAll": "全選", - "shortcuts.action.deleteSelection": "刪除選取", - "shortcuts.action.closeOverlayCancel": "關閉覆蓋層 / 取消操作", - "shortcuts.action.openNodeSearch": "開啟節點搜尋", - "shortcuts.action.resetViewport": "重設視窗", - "shortcuts.action.centerSelection": "置中選取", - "shortcuts.action.fitSelection": "適配選取", - "shortcuts.action.autoLayout": "自動佈局", - "shortcuts.action.toggleSnapToGrid": "切換對齊格線", - "shortcuts.action.bringForward": "往前一層", - "shortcuts.action.sendBackward": "往後一層", - "shortcuts.action.bringToFront": "移到最前", - "shortcuts.action.sendToBack": "移到最後", - "shortcuts.action.zoomInOut": "放大 / 縮小", - "shortcuts.action.pan": "平移", - "shortcuts.action.temporaryPan": "暫時平移", - "shortcuts.action.alternatePan": "替代平移", - "shortcuts.action.fineNudge": "精細移動選取", - "shortcuts.action.fastNudge": "快速移動", - "shortcuts.action.togglePreview": "切換資料預覽", - "shortcuts.action.explainPlan": "執行計畫", - "shortcuts.action.runPreview": "執行預覽", - "shortcuts.action.connectionManager": "連線管理器", - "shortcuts.action.flowVersionHistory": "流程版本歷史", - "shortcuts.resetAll": "全部重設", - "shortcuts.customized": "已自訂", - "shortcuts.default": "預設", - "shortcuts.apply": "套用", - "shortcuts.reset": "重設", - "shortcuts.status.resetAllSuccess": "所有快速鍵已重設為預設值。", - "shortcuts.status.updated": "快速鍵已更新。", - "shortcuts.status.reset": "快速鍵已重設為預設值。", - "shortcuts.status.updateFailed": "無法更新快速鍵。", - "toast.severity.success": "Success", - "toast.severity.warning": "Warning", - "toast.severity.error": "Error", - "toast.details.success": "Success Details", - "toast.details.warning": "Warning Details", - "toast.details.error": "Error Details", - "diagnostics.area.cteEditor": "CTE Editor", - "diagnostics.area.viewEditor": "View Editor", - "diagnostics.area.subEditor": "Sub-editor", - "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", - "diagnostics.recommendation.reloadFileIfNeeded": "Reload the file if needed.", - "diagnostics.viewEditor.exitFailed": "Could not exit: {0}", - "diagnostics.viewEditor.canvasIncomplete": "the canvas is incomplete.", - "diagnostics.viewEditor.exitRecommendation": "Connect a valid ResultOutput or use the discard command.", - "diagnostics.viewEditor.restoreParentFailed": "Failed to restore the parent canvas. The subgraph was discarded.", - "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", - "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", - "diagnostics.canvasMigration.openWarning": "Open: {0}", - "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", - "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", - "diagnostics.recommendation.resaveLatestSchema": "Review diagnostics and re-save the canvas to persist the latest schema.", - "diagnostics.recommendation.saveMigratedSchema": "Review diagnostics and save the canvas to persist the migrated schema.", - "file.saveDialog.title": "Save Canvas", - "file.saveDialog.suggestedName": "Query1", - "file.save.success": "Canvas saved successfully.", - "file.save.failedWithReason": "Save failed: {0}", - "file.openDialog.title": "Open Canvas", - "file.open.failedWithReason": "Open failed: {0}", - "file.open.success": "Canvas opened successfully.", - "file.open.successWithWarnings": "Canvas opened with warnings.", - "session.restore.failedWithReason": "Restore failed: {0}", - "session.restore.successWithWarnings": "Session restored with warnings.", - "session.restore.success": "Session restored successfully.", - "export.documentation.dialogTitle": "Export Flow Documentation", - "export.documentation.success": "Documentation exported successfully.", - "export.documentation.failed": "Documentation export failed.", - "export.failed.pathPermissionsHint": "Check file path and permissions.", - "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", - "export.dialogTitleByExtension": "Export as {0}", - "export.success": "Export completed successfully.", - "export.failed": "Export failed.", - "fileHistory.currentFile.none": "No file selected", - "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", - "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", - "fileHistory.status.countAvailable": "{0} local version(s) available.", - "fileHistory.restore.failedWithReason": "Restore failed: {0}", - "fileHistory.restore.successFrom": "Restored version from {0}.", - "preview.status.cancelled": "Cancelled", - "preview.status.error": "Error", - "preview.status.ready": "Ready", - "preview.runningWithMs": "Running... {0}ms", - "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", - "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", - "explain.errorWithReason": "Explain plan error: {0}", - "explain.noSql": "No SQL to explain. Build a query on the canvas first.", - "ddl.compilationFailed": "Compilation failed", - "ddl.compileErrorWithReason": "DDL compile error: {0}", - "command.undo.name": "Undo", - "command.undo.description": "Undo last action", - "command.redo.name": "Redo", - "command.redo.description": "Redo last undone action", - "command.addNode.name": "Add Node", - "command.addNode.description": "Open node search menu to add a node", - "command.bringForward.name": "Bring Forward", - "command.bringForward.description": "Move selected nodes one layer forward", - "command.sendBackward.name": "Send Backward", - "command.sendBackward.description": "Move selected nodes one layer backward", - "command.bringToFront.name": "Bring to Front", - "command.bringToFront.description": "Move selected nodes to top layer", - "command.sendToBack.name": "Send to Back", - "command.sendToBack.description": "Move selected nodes to bottom layer", - "command.normalizeLayers.name": "Normalize Layers", - "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", - "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", - "main.orphanSuffix": "Orphan(s)", - "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", - "main.namingPrefix": "Naming", - "fileHistory.compressedLabel": "Compressed:", - "schema.itemsSuffix": "item(s)", - "property.panel.title": "Properties", - "property.panel.multiSelected": "{0} nodes selected", - "sqlImporter.status.pasteSelect": "Paste a SELECT statement above, then click Import.", - "sqlImporter.status.inputTooLarge": "SQL input is too large ({0:N0} chars). Limit is {1:N0}. Split the query or increase the import limit.", - "sqlImporter.status.parsing": "Parsing SQL...", - "sqlImporter.status.done": "Done - {0} imported, {1} partial, {2} skipped.", - "sqlImporter.status.cancelledByUser": "Import cancelled by user.", - "sqlImporter.status.timeout": "Import timed out after {0:0.#}s. Try a smaller query or increase timeout.", - "sqlImporter.status.parseError": "Parse error: {0}", - "diagnostics.area.undoRedoTransaction": "Undo/Redo Transaction", - "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", - "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", - "node.preview.noCatalog": "No catalog available", - "connection.error.searchMenuNotInitialized": "search menu not initialized", - "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", - "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", - "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", - "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", - "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", - "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", - "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", - "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", - "diagnostics.area.connection": "Connection", - "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", - "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", - "undoRedo.transaction.unnamed": "unnamed transaction", - "benchmark.runLabelDefault": "Run 1", - "benchmark.runLabelPattern": "Run {0}", - "benchmark.status.failedWithReason": "Benchmark failed: {0}", - "benchmark.status.noSql": "No SQL to benchmark - build a query first.", - "benchmark.status.warmupProgress": "Warm-up {0}/{1}...", - "benchmark.status.iterationProgress": "Iteration {0}/{1}...", - "benchmark.status.done": "Done - {0}", - "benchmark.status.cancelled": "Benchmark cancelled.", - "app.windowTitle": "DBWeaver", - "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", - "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", - "sqlImporter.error.selectFromNotFound": "Could not find SELECT ... FROM in the query.", - "sqlImporter.error.fromClauseParseFailed": "Could not parse FROM clause.", - "sqlImporter.error.syntaxUnterminatedString": "Syntax error at line {0}, column {1}: unterminated string literal.", - "sqlImporter.error.missingClosingParenthesis": "missing closing ')'", - "sqlImporter.error.unexpectedClosingParenthesis": "unexpected ')'", - "sqlImporter.error.syntaxAtLineColumn": "Syntax error at line {0}, column {1}: {2}.", - "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", - "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", - "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", - "errorDiagnostics.connection.label": "Connection failed", - "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", - "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", - "errorDiagnostics.authorization.label": "Authorization error", - "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", - "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", - "errorDiagnostics.timeout.label": "Query timeout", - "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", - "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", - "errorDiagnostics.schema.label": "Schema error", - "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", - "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", - "errorDiagnostics.syntax.label": "SQL syntax error", - "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", - "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", - "errorDiagnostics.compatibility.label": "Compatibility error", - "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", - "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", - "errorDiagnostics.unknown.label": "Unexpected error", - "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", - "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", - "error.mainWindow.invalidDataContext": "MainWindow 的 DataContext 必須是 ShellViewModel。", - "error.mainWindow.canvasNotInitialized": "CanvasViewModel 尚未初始化。", - "error.mainWindow.ddlPreviewUnavailable": "目前畫布無法使用 DDL 預覽。", - "themeJson.editor.template": "{\n \"meta\": { \"name\": \"自訂主題\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", - "themeJson.error.pasteBeforeApply": "套用前請先貼上主題 JSON。", - "themeJson.error.invalidJson": "無效的 JSON:{0}", - "themeJson.error.emptyPayload": "無效的 JSON:內容為空。", - "themeJson.error.invalidTheme": "無效的主題:{0}", - "themeJson.error.appliedButSaveFailed": "主題已套用,但儲存失敗:{0}", - "themeJson.success.appliedAndSaved": "JSON 主題已套用並儲存。", - "themeJson.success.customRemoved": "已移除自訂主題。請重新啟動應用程式以完全回到預設主題。", - "themeJson.error.restoreDefaultFailed": "還原預設主題失敗:{0}", - "themeValidator.error.configNull": "主題設定為 null。", - "themeValidator.warning.noSections": "主題沒有色彩或字體設定區段;無可套用內容。", - "themeValidator.warning.invalidColor": "{0} 的色彩 '{1}' 無效。此鍵將被忽略。", - "themeValidator.warning.sizeOutOfRange": "{0}={1} 超出範圍 (8..48)。此鍵將被忽略。", - "queryExecutor.error.openConnectionMethodNotFound": "在 orchestrator 中找不到 OpenConnectionAsync 方法", - "queryExecutor.error.openConnectionInvokeFailed": "呼叫 OpenConnectionAsync 失敗", - "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}':此 view 的 SELECT 無法以視覺方式重建,請在子畫布手動編輯。", - "ddlImporter.error.tableNotFoundInMetadata": "在目前中繼資料中找不到資料表 '{0}'。", - "main.window.untitled": "未命名", - "main.subEditor.noSeedProvided": "未提供 {0} 的子編輯器 seed。", - "main.layerOrder.bringToFront": "移到最上層", - "main.layerOrder.sendToBack": "移到最下層", - "main.layerOrder.bringForward": "向前一層", - "main.layerOrder.sendBackward": "向後一層", - "main.layerOrder.normalizeLayers": "正規化圖層", - "export.fileType.html": "HTML 檔案", - "export.fileType.json": "JSON 檔案", - "export.fileType.csv": "CSV 檔案", - "export.fileType.excel": "Excel 檔案", - "commandPalette.templatePrefix": "範本:{0}", - "themeLoader.status.notFoundWithPath": "找不到主題檔案:{0}", - "themeLoader.status.deserializedNull": "主題 JSON 反序列化結果為 null。", - "themeLoader.status.loaded": "主題 JSON 載入成功。", - "credential.error.ciphertextTooShort": "密文資料太短。", - "credential.error.dpapiWindowsOnly": "DPAPI 僅支援 Windows。", - "credential.warning.loadVaultFailed": "載入憑證保險庫 {0} 失敗:{1}", - "credential.warning.persistVaultFailed": "儲存憑證保險庫 {0} 失敗:{1}", - "snippetStore.warning.loadFailed": "從 {0} 載入 snippet 失敗:{1}", - "snippetStore.warning.saveFailed": "儲存 snippet 失敗:{0}", - "flowVersionStore.warning.loadFailed": "從 {0} 載入流程版本失敗:{1}", - "flowVersionStore.warning.saveFailed": "儲存流程版本失敗:{0}", - "queryExecutor.error.queryEmpty": "查詢不可為空", - "queryExecutor.error.providerNotSupported": "不支援供應者 {0}", - "queryExecutor.error.singleStatementOnly": "預覽僅接受單一 SQL 語句。", - "queryExecutor.error.queryEmptyWithPeriod": "查詢不可為空。", - "queryExecutor.error.readOnlyOnly": "預覽模式僅支援唯讀 SQL。", - "queryExecutor.error.namedParametersNotSupported": "預覽模式不支援執行 SQL 的命名參數。請改用安全內嵌常值或在預覽外執行查詢。", - "queryExecutor.error.positionalParametersNotSupported": "預覽模式不支援位置參數佔位符(? 或 )。", - "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "將所選節點對齊到底部邊緣", - "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "將所選節點對齊到最左側邊緣", - "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "將所選節點對齊到最右側邊緣", - "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "將所選節點對齊到頂部邊緣", - "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "套用 CTE 子畫布編輯並返回父畫布", - "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "自動將節點排列為邏輯欄位", - "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "在水平軸上置中所選節點", - "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "在垂直軸上置中所選節點", - "commandPalette.description.clear_canvas_and_start_fresh": "清空畫布並重新開始", - "commandPalette.description.clear_node_selection": "清除節點選取", - "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "將別名轉換為專案設定中的命名慣例", - "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "建立檢查點、並排比較版本並還原先前畫布狀態", - "commandPalette.description.delete_the_selected_nodes": "刪除所選節點", - "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "捨棄目前子編輯器變更並返回父畫布", - "commandPalette.description.execute_the_current_query_in_preview": "在預覽中執行目前查詢", - "commandPalette.description.fit_all_nodes_into_the_visible_area": "將所有節點適配至可視區域", - "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "從第一個 CSV 匯出節點產生 CSV 檔案", - "commandPalette.description.generate_html_file_from_the_first_html_export_node": "從第一個 HTML 匯出節點產生 HTML 檔案", - "commandPalette.description.generate_json_file_from_the_first_json_export_node": "從第一個 JSON 匯出節點產生 JSON 檔案", - "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "從第一個 Excel 匯出節點產生 XLSX 活頁簿", - "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "檢視查詢執行計畫:掃描類型、Join 策略與成本估算", - "commandPalette.description.load_a_vsaq_canvas_file": "載入 .vsaq 畫布檔案", - "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "測量目前 SQL 在 N 次迭代下的平均/中位/p95 延遲", - "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "為所選 CTE 定義節點開啟獨立子畫布編輯器", - "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "開啟每次儲存產生的本機版本歷史並還原先前快照", - "commandPalette.description.open_output_preview_modal_for_the_active_mode": "開啟目前模式的輸出預覽對話框", - "commandPalette.description.open_shortcut_reference_screen": "開啟快捷鍵參考畫面", - "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "開啟連線管理器以新增、編輯或切換資料庫連線", - "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "貼上 SELECT 語句並自動產生節點(支援 FROM、JOIN、WHERE、LIMIT)", - "commandPalette.description.remove_all_nodes_not_connected_to_output": "移除所有未連接到輸出的節點", - "commandPalette.description.reset_zoom_and_pan_to_default": "將縮放與平移重設為預設值", - "commandPalette.description.save_canvas_to_a_new_file": "將畫布儲存為新檔案", - "commandPalette.description.save_current_canvas": "儲存目前畫布", - "commandPalette.description.save_markdown_documentation_of_the_current_flow": "儲存目前流程的 Markdown 文件", - "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "將所選節點儲存為可重用片段,稍後可透過節點搜尋選單(⇧A)插入", - "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "依據 FK 慣例與命名模式掃描畫布上的表來源節點,找出可能的 Join 關係", - "commandPalette.description.select_all_nodes_on_canvas": "選取畫布上的所有節點", - "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "將節點位置吸附到 16px 網格(Ctrl+G)", - "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "將所選節點以相等水平間距分佈", - "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "將所選節點以相等垂直間距分佈", - "commandPalette.description.zoom_into_the_canvas": "放大畫布", - "commandPalette.description.zoom_out_of_the_canvas": "縮小畫布", - "commandPalette.name.align_bottom": "底部對齊", - "commandPalette.name.align_left": "靠左對齊", - "commandPalette.name.align_right": "靠右對齊", - "commandPalette.name.align_top": "頂部對齊", - "commandPalette.name.analyze_all_joins": "分析所有 Join", - "commandPalette.name.auto_fix_naming": "自動修正命名", - "commandPalette.name.auto_layout": "自動排版", - "commandPalette.name.center_horizontally": "水平置中", - "commandPalette.name.center_vertically": "垂直置中", - "commandPalette.name.cleanup_orphans": "清理孤立節點", - "commandPalette.name.delete_selected": "刪除已選取", - "commandPalette.name.deselect_all": "取消全選", - "commandPalette.name.discard_and_exit_editor": "放棄並離開編輯器", - "commandPalette.name.distribute_horizontally": "水平平均分佈", - "commandPalette.name.distribute_vertically": "垂直平均分佈", - "commandPalette.name.edit_selected_cte": "編輯所選 CTE", - "commandPalette.name.exit_cte_editor": "離開 CTE 編輯器", - "commandPalette.name.explain_plan": "執行計畫", - "commandPalette.name.export_csv": "匯出 CSV", - "commandPalette.name.export_documentation": "匯出文件", - "commandPalette.name.export_excel": "匯出 Excel", - "commandPalette.name.export_html": "匯出 HTML", - "commandPalette.name.export_json": "匯出 JSON", - "commandPalette.name.file_save_load_history": "檔案儲存/載入歷史", - "commandPalette.name.fit_to_screen": "符合畫面", - "commandPalette.name.flow_version_history": "流程版本歷史", - "commandPalette.name.import_sql_to_graph": "將 SQL 匯入為圖形", - "commandPalette.name.keyboard_shortcuts": "鍵盤快捷鍵", - "commandPalette.name.manage_connections": "管理連線", - "commandPalette.name.new_canvas": "新增畫布", - "commandPalette.name.open_file": "開啟檔案", - "commandPalette.name.reset_viewport": "重設檢視區域", - "commandPalette.name.run_preview": "執行預覽", - "commandPalette.name.run_query_benchmark": "執行查詢效能測試", - "commandPalette.name.save": "儲存", - "commandPalette.name.save_as": "另存新檔", - "commandPalette.name.save_selection_as_snippet": "將選取儲存為片段", - "commandPalette.name.select_all": "全選", - "commandPalette.name.toggle_preview": "切換預覽", - "commandPalette.name.toggle_snap_to_grid": "切換格線吸附", - "commandPalette.name.zoom_in": "放大", - "commandPalette.name.zoom_out": "縮小", - "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", - "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", - "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", - "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", - "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", - "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", - "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", - "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", - "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", - "commandPalette.tags.clear_selection": "clear selection", - "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", - "commandPalette.tags.create_insert_search_transform": "create insert search transform", - "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", - "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", - "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", - "commandPalette.tags.data_results_table_panel": "data results table panel", - "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", - "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", - "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", - "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", - "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", - "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", - "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", - "commandPalette.tags.export_json_file_output_save": "export json file output save", - "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", - "commandPalette.tags.export_persist_copy": "export persist copy", - "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", - "commandPalette.tags.forward_history": "forward history", - "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", - "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", - "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", - "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", - "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", - "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", - "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", - "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", - "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", - "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", - "commandPalette.tags.load_import_vsaq": "load import vsaq", - "commandPalette.tags.magnify_enlarge": "magnify enlarge", - "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", - "commandPalette.tags.persist_write_disk": "persist write disk", - "commandPalette.tags.remove_erase_nodes": "remove erase nodes", - "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", - "commandPalette.tags.reset_clear_blank": "reset clear blank", - "commandPalette.tags.revert_back_history": "revert back history", - "commandPalette.tags.shrink_reduce": "shrink reduce", - "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", - "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", - "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", - "sqlEditor.diffPreview.title": "Transactional Diff Preview", - "sqlEditor.mutation.confirmExecute": "Confirm Execute", - "sqlEditor.tab.closeAnyway": "Close Anyway", - "sqlEditor.tab.keepTab": "Keep Tab", - "sqlEditor.status.ready": "Ready.", - "sqlEditor.telemetry.none": "No execution telemetry yet.", - "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", - "sqlEditor.telemetry.errors.none": "No aggregated errors.", - "sqlEditor.diff.none": "No transactional diff preview available.", - "sqlEditor.mutation.estimate.none": "No mutation estimate available.", - "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", - "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", - "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", - "sqlEditor.tab.noPendingClose": "No tab close pending.", - "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", - "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", - "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", - "sqlEditor.message.empty": "Execute a statement to see messages.", - "sqlEditor.message.success": "Execution completed successfully.", - "sqlEditor.result.summary.empty": "Rows: - Time: -", - "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", - "sqlEditor.file.save.canceled": "Save canceled.", - "sqlEditor.file.save.noPath": "No target path selected.", - "sqlEditor.file.save.success": "SQL file saved.", - "sqlEditor.file.save.failed": "Save failed.", - "sqlEditor.file.open.failed": "Open failed.", - "sqlEditor.file.open.notFound": "Selected SQL file was not found.", - "sqlEditor.file.open.success": "SQL file opened.", - "sqlEditor.status.executing": "Executing SQL...", - "sqlEditor.status.executingScript": "Executing SQL script...", - "sqlEditor.status.executingStep": "Executing {0}/{1}...", - "sqlEditor.status.canceling": "Canceling execution...", - "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", - "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", - "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", - "sqlEditor.status.success": "Execution succeeded.", - "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", - "sqlEditor.status.canceled": "Execution canceled.", - "sqlEditor.status.failed": "Execution failed.", - "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", - "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", - "sqlEditor.result.tabTitle": "Result {0}", - "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", - "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", - "sqlEditor.tab.closed": "Tab closed.", - "sqlEditor.tab.closeCanceled": "Tab close canceled.", - "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", - "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", - "sqlEditor.error.noConnection": "No active database connection for SQL execution.", - "sqlEditor.error.executionCanceled": "SQL execution was canceled.", - "sqlEditor.tab.scriptTitle": "Script {0}", - "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", - "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", - "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", - "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", - "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", - "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", - "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", - "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", - "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", - "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", - "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", - "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", - "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", - "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", - "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", - "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", - "sqlEditor.results.title": "Results", - "sqlEditor.saveSql.fileType": "SQL Files", - "sqlEditor.saveSql.pickerTitle": "Save SQL File", - "sqlEditor.export.pickerTitle": "Export SQL Data", - "sqlEditor.export.status.noResultTitle": "No execution result available for export.", - "sqlEditor.export.status.noResultDetail": "Execute a query first.", - "sqlEditor.export.status.successTitle": "Report exported.", - "sqlEditor.export.status.failedTitle": "Failed to export report.", - "sqlEditor.export.fileType.html": "HTML File", - "sqlEditor.export.fileType.json": "JSON File", - "sqlEditor.export.fileType.csv": "CSV File", - "sqlEditor.export.fileType.xlsx": "Excel Workbook", - "sqlEditor.export.defaultFileBase": "report", - "sqlEditor.export.defaultTitle": "SQL Report", - "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", - "sqlEditor.export.type.html.title": "HTML full-feature report", - "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", - "sqlEditor.export.type.json.title": "JSON execution contract", - "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", - "sqlEditor.export.type.csv.title": "CSV data export", - "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", - "sqlEditor.export.type.xlsx.title": "Excel workbook export", - "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", - "sqlEditor.export.dialog.windowTitle": "Export SQL Data", - "sqlEditor.export.dialog.title": "Export SQL Data", - "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", - "sqlEditor.export.dialog.confirm": "Export", - "sqlEditor.export.dialog.fileNameWatermark": "report.html", - "sqlEditor.export.dialog.titleWatermark": "SQL Report", - "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", - "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", - "sqlEditor.export.dialog.section.fileName": "FILE NAME", - "sqlEditor.export.dialog.section.reportTitle": "TITLE", - "sqlEditor.export.dialog.section.description": "DESCRIPTION", - "sqlEditor.export.dialog.section.options": "OPTIONS", - "sqlEditor.export.option.includeSchema": "Include output schema", - "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", - "sqlEditor.export.option.includeMetadata": "Include optional metadata", - "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", - "sqlEditor.export.badge.offline": "OFFLINE READY", - "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", - "sqlEditor.export.badge.dataOnly": "DATA ONLY" -} +{ + "main.brand": "AkkornStudio", + "main.tab.query1": "Query 1", + "main.new": "New", + "main.open": "Open", + "main.save": "Save", + "main.history": "History", + "main.layout": "Layout", + "main.preview": "Preview", + "main.undo": "Undo", + "main.redo": "Redo", + "main.cleanupOrphans": "Cleanup orphan nodes", + "main.autoFixAliasNaming": "Auto-fix alias naming", + "main.autoLayoutCanvas": "Auto layout canvas", + "main.toggleDataPreview": "Toggle data preview", + "main.language": "Language", + "main.restore.prompt": "Previous session found — restore the last canvas?", + "main.restore.button": "Restore session", + "main.cteEditor.editingPrefix": "Editing CTE: ", + "main.cteEditor.backToCanvas": "Back to Canvas", + "main.cteEditor.exitA11y": "Exit CTE editor", + "main.viewEditor.editingPrefix": "DDL > View: ", + "main.viewEditor.backToCanvas": "Back to DDL", + "main.viewEditor.exitA11y": "Exit view editor", + "connection.title": "Connection Manager", + "connection.subtitle": "不中斷流程即可設定、測試與啟用連線", + "connection.none": "No connection", + "connection.active": "ACTIVE", + "connection.health.online": "Online", + "connection.health.degraded": "Degraded", + "connection.health.offline": "Offline", + "connection.tooltip.none": "No active connection — click to manage", + "connection.ping": "Ping", + "connection.saved": "SAVED CONNECTIONS", + "connection.new": "New Connection", + "connection.selectOrCreate": "Select a connection or create a new one", + "connection.name": "Connection Name", + "connection.provider": "Provider", + "connection.host": "Host", + "connection.port": "Port", + "connection.database": "Database", + "connection.sqlitePath": "SQLite Path", + "connection.sqliteBrowse": "Browse", + "connection.sqliteCreate": "Create", + "connection.username": "Username", + "connection.password": "Password", + "connection.timeout": "Timeout (seconds)", + "connection.test": "Test", + "connection.save": "Save", + "connection.connect": "Connect", + "connection.action.testConnection": "Test connection", + "connection.action.saveConnection": "Save connection", + "connection.action.connectConnection": "Connect connection", + "connection.status.connecting": "連線中...", + "connection.status.connected": "已連線", + "connection.status.testing": "測試中...", + "connection.status.failedPrefix": "連線失敗", + "connection.status.metadataUnavailable": "連線失敗:中繼資料不可用。", + "connection.status.highLatency": "高延遲", + "connection.watermark.name": "我的正式環境資料庫", + "connection.watermark.host": "localhost", + "connection.watermark.port": "5432", + "connection.watermark.database": "database_name", + "connection.watermark.username": "user", + "connection.watermark.password": "••••••••", + "connection.watermark.timeout": "30", + "main.connectingDb": "Connecting to database...", + "main.emptyHint": "Press ⇧A to add your first node, or drag a table from the sidebar", + "status.nodesSeparator": " nodes · ", + "status.connectionsSuffix": " connections", + "status.undo": "Undo: ", + "status.shortcuts": "⇧A Nodes · F3 Preview · Ctrl+Z Undo · Del Remove · Alt+Drag Pan", + "connection.disconnect": "Disconnect", + "connection.action.disconnectConnection": "Disconnect connection", + "connectionTab.active": "ACTIVE CONNECTION", + "connectionTab.none": "No active connection", + "connectionTab.saved": "SAVED CONNECTIONS", + "connectionTab.new": "+ New Connection", + "schema.database": "DATABASE", + "schema.search": "Search tables, columns...", + "schema.loading": "Searching tables, columns...", + "schema.noConnection": "No Connection", + "schema.noConnectionHint": "Connect to a database to see tables, columns, and relationships", + "schema.emptyNoTables": "No tables found", + "fileHistory.title": "Save/Load Version History", + "fileHistory.reload": "Reload", + "fileHistory.restoreSelected": "Restore selected", + "fileHistory.empty": "No local versions yet", + "fileHistory.emptyHint": "Save this file to generate version history.", + "preview.title": "Data Preview", + "preview.subtitle": "Review data and diagnostics before continuing", + "preview.run": "Run", + "preview.cancel": "Cancel", + "preview.tab.preview": "Preview", + "preview.tab.sql": "SQL", + "preview.close": "Close preview", + "preview.running": "Running preview query… ", + "preview.clickCancel": "Click Cancel to stop", + "preview.cancelled": "Query cancelled", + "preview.runAgain": "Press Run to execute again", + "preview.failed": "Preview execution failed", + "preview.technical": "TECHNICAL DETAILS", + "preview.noData": "No data yet", + "preview.f3Hint": "Press F3 or Space to run the current query", + "sqlImporter.title": "Import SQL to Graph", + "sqlImporter.subtitle": "Paste a SELECT statement — nodes are created automatically", + "sqlImporter.sqlStatement": "SQL STATEMENT", + "sqlImporter.supported": "Supported: ", + "sqlImporter.import": "Import", + "sqlImporter.report": "CONVERSION REPORT", + "sqlEditor.mutation.dialogTitle": "變更確認", + "sqlEditor.mutation.dialogSubtitle": "在確認執行前請先檢視影響", + "search.empty": "No nodes found", + "search.emptyHint": "Try searching for UPPER, JSON, CAST, AND…", + "search.shortcut": "⇧A", + "search.spawn": "Spawn", + "commandPalette.empty": "沒有符合搜尋的命令", + "commandPalette.search": "命令搜尋", + "commandPalette.shortcut": "CTRL+SHIFT+P", + "commandPalette.execute": "執行", + "context.editCte": "Edit Selected CTE", + "context.editViewSubcanvas": "Edit View Subcanvas", + "explain.title": "Explain Plan", + "explain.sql": "SQL", + "explain.option.analyze": "分析", + "explain.option.buffers": "緩衝區", + "explain.badge.simulated": "模擬", + "explain.timing.planning": "規劃:", + "explain.timing.execution": "執行:", + "explain.section.snapshotComparison": "快照比較", + "explain.section.indexRecommendations": "索引建議", + "explain.section.history": "歷程", + "explain.detail.estimated": "估計", + "explain.detail.actual": "實際", + "explain.detail.error": "誤差", + "explain.detail.time": "時間", + "explain.detail.loops": "迴圈", + "explain.rerun": "Re-run", + "explain.alertSuffix": " expensive operation(s) detected — consider adding indexes", + "explain.running": "Running EXPLAIN…", + "explain.failed": "⚠ EXPLAIN failed", + "explain.noPlan": "No plan yet", + "explain.rerunHint": "Press Re-run to execute EXPLAIN", + "explain.header.operation": "操作", + "explain.header.cost": "成本", + "explain.header.rows": "資料列", + "explain.header.alert": "警示", + "explain.mode.list": "清單", + "explain.mode.tree": "樹狀", + "explain.action.snapshot": "儲存快照", + "explain.action.copyJson": "複製 JSON", + "explain.action.copyText": "複製文字", + "explain.action.saveJson": "儲存 .json", + "explain.action.openDalibo": "在 Dalibo 開啟", + "explain.legend.seqscan": "SEQ SCAN — full table read, no index", + "explain.legend.sort": "SORT — in-memory sort", + "explain.legend.hash": "HASH — hash join", + "explain.escClose": "Esc to close", + "flowVersion.title": "Flow Version History", + "flowVersion.subtitle": "Create checkpoints, compare versions and restore", + "flowVersion.watermark": "Checkpoint label (optional)…", + "flowVersion.saveCheckpoint": "Save Checkpoint", + "flowVersion.compareMode": "Compare Mode", + "flowVersion.selectBase": "Select BASE version (from):", + "flowVersion.clickAny": "Then click any version in the list below to compare.", + "flowVersion.noCheckpoints": "No checkpoints yet", + "flowVersion.noCheckpointsHint": "Save a checkpoint above to begin tracking versions.", + "flowVersion.restore": "Restore", + "flowVersion.diffResults": "Diff Results", + "benchmark.title": "Query Benchmark", + "benchmark.subtitle": "Measures avg / median / p95 latency over N iterations", + "benchmark.sql": "SQL being benchmarked", + "benchmark.runLabel": "Run label", + "benchmark.runLabelWatermark": "Run 1", + "benchmark.iterations": "Iterations (1–100)", + "benchmark.warmup": "Warm-up passes (0–10)", + "benchmark.interval": "Interval between runs (ms)", + "benchmark.run": "Run Benchmark", + "benchmark.cancel": "Cancel", + "benchmark.clearHistory": "Clear History", + "benchmark.latest": "LATEST RESULT", + "benchmark.avg": "AVG", + "benchmark.median": "MEDIAN", + "benchmark.min": "MIN", + "benchmark.max": "MAX", + "benchmark.iterationsAt": " iterations · run at ", + "benchmark.history": "HISTORY", + "benchmark.header.label": "標籤", + "benchmark.header.avg": "平均", + "benchmark.header.median": "中位數", + "benchmark.header.min": "最小", + "benchmark.header.max": "最大", + "benchmark.itersSuffix": " iters", + "diagnostics.title": "App Diagnostics", + "diagnostics.run": "Run", + "diagnostics.running": "Running checks…", + "diagnostics.ok": "OK", + "diagnostics.warning": "Warning", + "diagnostics.error": "Error", + "diagnostics.tooltip.rerun": "Re-run all checks", + "diagnostics.tooltip.copy": "Copy diagnostic report to clipboard", + "diagnostics.tooltip.close": "Close (Esc)", + "autoJoin.title": "Auto-Join Suggestions", + "autoJoin.titleForTable": "Auto-Join suggestions for {0}", + "autoJoin.acceptAll": "Accept All", + "autoJoin.accept": "Accept", + "autoJoin.skip": "Skip", + "autoJoin.allHandled": "All suggestions handled", + "autoJoin.joinKeyword": "JOIN", + "autoJoin.confidence.fkConstraint": "FK Constraint", + "autoJoin.confidence.fkReverse": "FK (Reverse)", + "autoJoin.confidence.namingMatch": "Naming Match", + "autoJoin.confidence.weakMatch": "Weak Match", + "autoJoin.runSelected": "Auto-Join Selected", + "autoJoin.noSimilarityTitle": "No automatic join found", + "autoJoin.noSimilarityDetails": "Choose the columns manually to create a simple join.", + "autoJoin.appliedTitle": "Auto-join applied", + "autoJoin.manual.title": "Create Manual Join", + "autoJoin.manual.subtitle": "No reliable similarity was found. Select one column from each table.", + "autoJoin.manual.leftColumn": "Left column", + "autoJoin.manual.rightColumn": "Right column", + "autoJoin.manual.joinType": "Join type", + "autoJoin.manual.operator": "Operator", + "autoJoin.manual.confirm": "Create join", + "autoJoin.manual.noCompatible": "No compatible columns for the selected left pin type.", + "autoJoin.manualJoinCreatedTitle": "Manual join created", + "autoJoin.manualJoinFailedTitle": "Manual join could not be created", + "autoJoin.manualJoinFailedDetails": "Check selected columns and existing joins, then try again.", + "autoJoin.multipleCandidatesTitle": "Multiple join candidates found", + "autoJoin.multipleCandidatesDetails": "{0} possible combinations were found. Confirm which columns should be used.", + "autoJoin.suggestionsFoundTitle": "Auto-join suggestions available", + "autoJoin.suggestionsFoundDetails": "{0} suggestion(s) found. Select two tables and run Auto-Join Selected.", + "property.outputAlias": "OUTPUT ALIAS", + "property.sourceAlias": "SOURCE ALIAS", + "property.aliasWatermark": "e.g. MyColumn (optional)", + "property.parameters": "PARAMETERS", + "property.enabled": "Enabled", + "property.datetimeWatermark": "YYYY-MM-DDTHH:mm:ss or leave empty", + "property.dateWatermark": "YYYY-MM-DD or leave empty", + "property.apply": "Apply", + "property.inputPins": "INPUT PINS", + "property.outputPins": "OUTPUT PINS", + "property.sqlTrace": "SQL TRACE", + "property.live": "live", + "node.numericValue": "Numeric Value", + "node.stringValue": "String Value", + "node.enterText": "Enter text", + "node.datetimeValue": "DateTime Value", + "node.valueLabel": "Value:", + "node.noInputs": "No inputs", + "node.loadingSample": "Loading sample…", + "node.previewFailed": "⚠ Preview failed", + "node.sampleRowsHint": "5 sample rows · demo data", + "sidebar.tab.nodes": "節點", + "sidebar.tab.connection": "連線", + "sidebar.tab.schema": "結構", + "sidebar.tab.diagnostics": "診斷", + "sidebar.addNode": "+ Add Node (⇧A)", + "sidebar.previewF3": "Preview (F3)", + "nodesList.search": "Search nodes...", + "search.watermark": "Search nodes… (Esc to close)", + "search.snippets": "★ SNIPPETS", + "commandPalette.watermark": "執行命令…(Esc 關閉)", + "tooltip.newCanvas": "New canvas (Ctrl+N)", + "tooltip.openCanvas": "Open canvas (Ctrl+O)", + "tooltip.saveCanvas": "Save canvas (Ctrl+S)", + "tooltip.fileHistory": "Local save/load history (Ctrl+Alt+H)", + "tooltip.zoomOut": "Zoom out (Ctrl+-)", + "tooltip.zoomIn": "Zoom in (Ctrl++)", + "tooltip.fitToScreen": "Fit to screen (Ctrl+0)", + "tooltip.autoLayout": "Auto Layout — arrange nodes into logical columns (Ctrl+L, Ctrl+Z to undo)", + "tooltip.snapToGrid": "Toggle snap to grid (Ctrl+G)", + "tooltip.dataPreview": "Data preview (F3)", + "tooltip.toggleLanguage": "Toggle language (pt-BR / en-US)", + "tooltip.appDiagnostics": "App Diagnostics (self-check)", + "tooltip.keyboardShortcuts": "Keyboard shortcuts (F1)", + "tooltip.cancelRunningQuery": "Cancel the running query", + "tooltip.closeEsc": "Close (Esc)", + "tooltip.recheckConnectionHealth": "Re-check connection health", + "tooltip.deleteConnection": "Delete connection", + "tooltip.testConnection": "Test connection", + "tooltip.saveConnection": "Save connection", + "tooltip.activateConnection": "Activate this connection", + "tooltip.toggleDataSamplePreview": "Toggle data sample preview", + "tooltip.liveSqlMutatingBlocked": "This SQL contains a data-mutating command and cannot be run in Safe Preview Mode", + "tooltip.copySql": "Copy SQL to clipboard", + "tooltip.formatSql": "Format SQL", + "tooltip.openBenchmark": "Open Query Benchmark (measure avg / median / p95 latency)", + "tooltip.openExplainPlan": "Open Explain Plan inspector — visualise the query execution plan", + "tooltip.switchToQueryMode": "Switch to Query canvas", + "tooltip.switchToDdlMode": "Switch to DDL canvas", + "tooltip.autoJoinSelected": "Try auto-join between the two selected tables", + "tooltip.pins.inputs": "Inputs", + "tooltip.pins.outputs": "Outputs", + "tooltip.pins.none": "None", + "tooltip.tableColumns": "Columns", + "tooltip.tableColumns.none": "No detailed columns", + "window.minimize": "Minimize window", + "window.maximizeRestore": "Maximize/restore window", + "window.close": "Close window", + "menu.newDiagram": "New diagram", + "menu.openFile": "Open file", + "menu.save": "Save", + "menu.fileHistory": "File history", + "menu.shortcuts": "Keyboard shortcuts", + "menu.settings": "Settings", + "menu.importDdlSchema": "Import DDL schema", + "menu.viewDdlSql": "View DDL SQL", + "menu.executeDdl": "Execute DDL", + "menu.backToStart": "Back to start", + "toast.ddlExecuteFailed": "Failed to execute DDL.", + "toast.ddlOpenFailed": "Failed to open DDL SQL.", + "toast.ddlImportFailed": "Failed to import schema into DDL.", + "toast.ddlConnectToImportSchema": "Connect to a database to import schema into the DDL canvas.", + "toast.ddlNoTablesFound": "No tables found to import in DDL mode.", + "toast.ddlSchemaImported": "Schema imported into the DDL canvas.", + "toast.ddlImportSummary": "{0} table(s), {1} column(s), {2} FK(s), {3} unique index(es).", + "toast.ddlConnectToImportTable": "Connect to a database to import tables into the DDL canvas.", + "toast.ddlTableAlreadyExists": "Table '{0}' already exists in the DDL canvas.", + "toast.ddlTableImported": "Table imported into the DDL canvas.", + "toast.ddlTableImportSummary": "Nodes: +{0}, connections: +{1}, FKs: +{2}.", + "toast.ddlNoActiveConnection": "No active connection to execute DDL.", + "toast.ddlExecutedSuccess": "DDL executed successfully.", + "toast.ddlExecutedWithIssues": "DDL executed with issues.", + "toast.switchToDdl": "Switch to DDL mode to generate SQL.", + "toast.ddlInvalid": "Invalid DDL. Fix errors before continuing.", + "toast.ddlNoStatements": "No DDL statements were generated in the canvas.", + "toast.previewOpenFailed": "Failed to open preview.", + "tab.switchFailed": "Failed to switch tab: {0}", + "settings.status.darkApplied": "Dark theme applied.", + "settings.status.lightApplied": "Light theme applied.", + "settings.status.snapUpdated": "Snap updated: {0}.", + "settings.status.languageToggled": "Language toggled.", + "settings.status.languageSelected": "Language selected: {0}.", + "settings.status.themeEditorReady": "Theme editor ready. Apply to save and use this theme.", + "settings.section.appearance.title": "Themes", + "settings.section.languageRegion.title": "語言與地區", + "settings.section.dateTime.title": "日期與時間", + "settings.section.keyboard.title": "鍵盤快速鍵", + "settings.section.privacy.title": "隱私", + "settings.section.notification.title": "通知", + "settings.section.accessibility.title": "無障礙", + "settings.section.default.title": "設定", + "settings.section.appearance.subtitle": "選擇你的風格或自訂主題", + "settings.section.languageRegion.subtitle": "管理語言與地區格式", + "settings.section.keyboard.subtitle": "自訂 command palette 與畫布執行所使用的鍵盤快速鍵。", + "settings.section.wip.subtitle": "開發中。", + "settings.section.default.subtitle": "應用程式設定", + "settings.general": "General", + "settings.nav.appearance": "Appearance", + "settings.theme.light": "淺色模式", + "settings.theme.dark": "深色模式", + "settings.theme.system": "系統偏好", + "settings.gridSnap.title": "Grid Snap", + "settings.gridSnap.subtitle": "Controls node snapping on the canvas.", + "settings.language.subtitle": "請選擇應用程式語言。", + "settings.language.toggle": "切換語言", + "settings.language.option.ptBR": "葡萄牙語(巴西)", + "settings.language.option.enUS": "英語(美國)", + "settings.language.option.esES": "西班牙語(西班牙)", + "settings.language.option.ruRU": "俄語", + "settings.language.option.jaJP": "日語", + "settings.language.option.zhTW": "繁體中文", + "settings.themeJson.title": "主題 JSON", + "settings.themeJson.subtitle": "貼上你的主題 JSON,立即套用並保存。", + "settings.themeJson.apply": "套用 JSON", + "settings.themeJson.restoreDefault": "還原預設主題", + "mode.query": "Query", + "mode.ddl": "DDL", + "sidebar.left.close": "Close left sidebar", + "sidebar.left.open": "Reopen left sidebar", + "sidebar.right.close": "Close right sidebar", + "sidebar.right.open": "Reopen right sidebar", + "connection.completedTitle": "Connection completed", + "connection.clearCanvasPrompt": "Do you want to clear the current canvas to start with the new connection?", + "connection.close": "Close connection manager", + "connection.refreshHealth": "Refresh connection health", + "common.details": "Details", + "common.cancel": "Cancel", + "common.keep": "Keep", + "common.clear": "Clear", + "zoom.out": "Zoom out", + "zoom.in": "Zoom in", + "zoom.fit": "Fit zoom to screen", + "zoom.level": "Zoom level", + "settings.theme.mode": "主題模式", + "diagnostics.category.canvas": "Canvas Integrity", + "diagnostics.category.output": "Output & Execution", + "diagnostics.category.session": "Session & Safety", + "diagnostics.category.notice": "Runtime Notices", + "diagnostics.summary.ok": "All systems OK", + "diagnostics.summary.warningCount": "{0} warning(s) detected", + "diagnostics.summary.errorCount": "{0} error(s) detected", + "diagnostics.canvasMigration": "Canvas Migration", + "diagnostics.recommendation.resaveFile": "Re-save the file to update it to the latest schema version.", + "diagnostics.canvasState.name": "Canvas State", + "diagnostics.canvasState.recommendation": "Add at least one {0} and one {1}", + "diagnostics.canvasState.empty": "Canvas is empty - no nodes present", + "diagnostics.canvasState.counts": "{0} node(s), {1} connection(s)", + "diagnostics.validation.name": "Validation Errors", + "diagnostics.validation.recommendation": "Fix highlighted nodes before running output preview", + "diagnostics.validation.errorWithWarnings": "{0} error(s) and {1} warning(s) in the graph", + "diagnostics.validation.warningOnly": "{0} warning(s) in the graph", + "diagnostics.validation.none": "No validation issues", + "diagnostics.orphan.name": "Orphan Nodes", + "diagnostics.orphan.recommendation": "Use the orphan cleanup action to remove unused nodes", + "diagnostics.orphan.count": "{0} node(s) not connected to any output", + "diagnostics.orphan.none": "No orphan nodes detected", + "diagnostics.naming.name": "Naming Conventions", + "diagnostics.naming.recommendation": "Use auto-fix alias naming when conformance is below 100%", + "diagnostics.naming.conformance": "Naming conformance: {0}%", + "diagnostics.naming.ok": "All aliases follow naming conventions (100%)", + "diagnostics.queryCompilation.name": "Live SQL Compilation", + "diagnostics.queryCompilation.recommendation": "Review SQL diagnostics in output when errors/warnings are reported.", + "diagnostics.queryCompilation.errorFallback": "Live SQL compilation reported errors.", + "diagnostics.queryCompilation.warningCounts": "{0} diagnostic item(s), {1} guardrail warning(s).", + "diagnostics.queryCompilation.ok": "Live SQL compiled without diagnostics.", + "diagnostics.previewSafety.name": "Preview Safety", + "diagnostics.previewSafety.recommendation": "Preview executes read-only statements only.", + "diagnostics.previewSafety.blocked": "Current SQL is mutating and blocked by Safe Preview mode.", + "diagnostics.previewSafety.ok": "Preview safety checks passed.", + "diagnostics.previewExecution.name": "Preview Execution", + "diagnostics.previewExecution.recommendation": "Run preview and inspect diagnostics for execution/runtime errors.", + "diagnostics.previewExecution.failed": "Preview execution failed.", + "diagnostics.previewExecution.cancelled": "Preview execution was cancelled.", + "diagnostics.previewExecution.done": "{0} row(s) in {1}ms.", + "diagnostics.previewExecution.none": "No preview execution issues detected.", + "diagnostics.ddlCompilation.name": "DDL Compilation", + "diagnostics.ddlCompilation.recommendation": "Fix DDL compile diagnostics before execution.", + "diagnostics.ddlCompilation.failed": "DDL compilation failed.", + "diagnostics.ddlCompilation.warningCount": "{0} warning(s) reported by DDL compiler.", + "diagnostics.ddlCompilation.ok": "DDL compilation succeeded.", + "diagnostics.ddlOutput.name": "DDL Output", + "diagnostics.ddlOutput.recommendation": "Add/complete DDL nodes until at least one statement is generated.", + "diagnostics.ddlOutput.none": "No DDL statements generated yet.", + "diagnostics.ddlOutput.lines": "{0} line(s) of DDL generated.", + "diagnostics.undo.name": "Undo History", + "diagnostics.undo.recommendation": "History is in-memory only; save your canvas regularly.", + "diagnostics.undo.saved": "Canvas is saved (no unsaved changes).", + "diagnostics.undo.unsavedDeep": "Unsaved changes with {0} undo steps.", + "diagnostics.undo.unsaved": "Unsaved changes - {0} undo step(s) available.", + "diagnostics.report.title": "AkkornStudio - Diagnostic Report", + "diagnostics.report.generated": "Generated", + "diagnostics.report.overall": "Overall", + "diagnostics.report.details": "Details", + "diagnostics.report.recommendation": "Recommendation", + "diagnostics.report.lastCheck": "Last Check", + "sqlImporter.watermark": "SELECT column_a, column_b FROM schema_name.table_name WHERE condition LIMIT 100", + "node.datetimeFormat": "YYYY-MM-DDTHH:mm:ss", + "preview.providerLabel": "Provider", + "preview.ddlDiagnosticsHint": "Error/warning details are available in Diagnostics.", + "common.navigate": "Navigate", + "common.close": "Close", + "common.esc": "Esc", + "common.ms": "ms", + "common.zero": "0", + "diagnostics.tip": "Tip: check Diagnostics whenever preview/output reports warnings or errors.", + "nodesList.empty": "No nodes found", + "nodesList.emptyHint": "Adjust the search term to explore available types", + "schema.emptyFiltered": "No objects found for the current filter", + "start.lastSnapshot": "Last snapshot", + "app.brandBadge": "VS", + "property.tab.properties": "Properties", + "property.tab.projectSettings": "Project Settings", + "property.nodeType": "NODE TYPE", + "property.selectNodeHint": "Select a node to edit its properties.", + "property.namingConventions": "Naming Conventions", + "property.aliasConvention": "Alias convention", + "property.enforceAliasNaming": "Enforce alias naming", + "property.warnReservedSql": "Warn on reserved SQL keywords", + "property.maxAliasLength": "Max alias length", + "property.maxAliasLengthDefault": "64", + "property.namingSettingsHint": "These settings are project-scoped and are used by naming and validation helpers.", + "start.tips": "Tips", + "start.tips.quick": "Quick tips", + "start.tips.item1": "1. Click New Diagram to start from scratch.", + "start.tips.item2": "2. Use templates to speed up prototyping.", + "start.tips.item3": "3. Open saved connections to load real tables.", + "start.tips.shortcut": "Shortcut: CTRL+SHIFT+P opens the command palette.", + "start.workspace": "WORKSPACE", + "start.resumeTitle": "Continue where you left off", + "start.resumeSubtitle": "Quickly resume a recent project or start a new diagram.", + "start.chip.quickFlow": "Quick flow", + "start.chip.templates": "Templates", + "start.chip.connections": "Connections", + "start.savedConnectionsTitle": "Saved Connections", + "start.savedConnectionsSubtitle": "Connect quickly to a database to load schema and tables.", + "start.noConnectionsTitle": "No connections configured yet", + "start.noConnectionsSubtitle": "Create a connection to explore real tables in the editor.", + "start.newConnection": "+ New Connection", + "start.recentProjectsTitle": "Recent Projects", + "start.searchRecent": "Search recent project...", + "start.quickActions": "Quick actions", + "start.quickActionsSubtitle": "Open an existing file or start a new diagram.", + "start.noRecentTitle": "No recent projects yet", + "start.noRecentSubtitle": "Use the quick actions card above to get started.", + "start.exploreTemplates": "Explore templates", + "start.templatesFavoritesHint": "Favorites on top", + "start.favoriteTemplate": "Favorite template", + "node.columnSetPreview": "ColumnSet preview", + "node.view": "VIEW", + "node.tableDefinition": "Table Definition", + "node.join": "JOIN", + "node.window.addPartition": "Add PARTITION BY slot", + "node.window.removePartition": "Remove PARTITION BY slot", + "node.window.addOrder": "Add ORDER BY slot", + "node.window.removeOrder": "Remove ORDER BY slot", + "sql.keyword.select": "SELECT", + "sql.keyword.from": "FROM", + "sql.keyword.join": "JOIN", + "sql.keyword.where": "WHERE", + "sql.keyword.limit": "LIMIT", + "sqlImporter.close": "Close SQL importer", + "sqlImporter.report.imported": "Imported", + "sqlImporter.report.partial": "Partial", + "sqlImporter.report.skipped": "Skipped", + "benchmark.close": "Close benchmark", + "benchmark.p95": "P95", + "benchmark.n": "N", + "liveSql.safePreview": "SAFE PREVIEW MODE", + "liveSql.title": "LIVE SQL", + "liveSql.blocked": "BLOCKED", + "liveSql.copy": "Copy", + "liveSql.format": "Format", + "liveSql.benchmark": "Benchmark", + "liveSql.explain": "Explain", + "liveSql.actionsHint": "Performance tools", + "ddl.dialog.title": "Execute DDL", + "ddl.dialog.execute": "Execute", + "ddl.dialog.cancel": "Cancel", + "ddl.dialog.close": "Close", + "ddl.dialog.stopOnError": "Stop on first failure", + "ddl.dialog.confirmDestructive": "I confirm execution of destructive statements (DROP TABLE)", + "ddl.dialog.reviewBeforeRun": "Review the DDL script before confirming.", + "ddl.dialog.confirmQuestion": "Confirm DDL execution on the connected database?", + "ddl.dialog.irreversibleWarning": "This action can change the schema irreversibly.", + "ddl.dialog.mustConfirmDestructive": "Confirm destructive execution to continue.", + "ddl.dialog.executing": "Executing...", + "ddl.execute.result.summary": "Statements: {0} | Success: {1} | Failures: {2} | Time: {3:0}ms", + "ddl.execute.result.okLine": "[{0}] OK | rows={1} | {2}", + "ddl.execute.result.failLine": "[{0}] FAIL | {1} | {2}", + "ddl.execute.result.failed": "Failed to execute DDL.", + "ddl.execute.result.cancelled": "Execution cancelled by the user.", + "ddl.execute.result.cancelledDetails": "DDL execution was interrupted before completion.", + "context.deleteSingle": "Delete {0}", + "context.deleteMultiple": "Delete {0} nodes", + "context.bringForward": "Bring Forward (Ctrl+PgUp)", + "context.sendBackward": "Send Backward (Ctrl+PgDown)", + "context.bringToFront": "Bring to Front (Ctrl+Shift+PgUp)", + "context.sendToBack": "Send to Back (Ctrl+Shift+PgDown)", + "context.normalizeLayers": "Normalize Layers", + "context.deleteWire": "Delete wire", + "context.addNode": "Add Node (Shift+A)", + "context.undoWithDescription": "Undo {0}", + "context.redo": "Redo", + "shortcuts.windowTitle": "Keyboard Shortcuts", + "shortcuts.headerTitle": "AkkornStudio - Shortcuts", + "shortcuts.headerHint": "Tip: use CTRL+SHIFT+P to open the Command Palette and search commands.", + "shortcuts.filterWatermark": "Filter shortcuts by key or action...", + "shortcuts.resultCount": "{0} shortcuts", + "shortcuts.resultFilter": "{0} result(s) for \"{1}\"", + "shortcuts.noneFound": "No shortcuts found.", + "shortcuts.section.fileGeneral": "檔案與一般", + "shortcuts.section.editing": "編輯", + "shortcuts.section.canvasNavigation": "畫布與導覽", + "shortcuts.section.zoomPanPrecision": "縮放、平移與精準控制", + "shortcuts.section.previewInspection": "預覽與檢視", + "shortcuts.key.deleteOrBackspace": "Del 或 Backspace", + "shortcuts.key.middleDrag": "中鍵 + 拖曳", + "shortcuts.key.rightDrag": "右鍵 + 拖曳", + "shortcuts.key.spaceDrag": "空白鍵 + 拖曳", + "shortcuts.key.altLeftDrag": "Alt + 左鍵拖曳", + "shortcuts.key.arrows": "方向鍵", + "shortcuts.key.shiftArrows": "Shift + 方向鍵", + "shortcuts.action.openShortcutScreen": "開啟此快捷鍵畫面", + "shortcuts.action.newCanvas": "新畫布", + "shortcuts.action.openFile": "開啟檔案", + "shortcuts.action.save": "儲存", + "shortcuts.action.saveAs": "另存新檔", + "shortcuts.action.commandPalette": "命令面板", + "shortcuts.action.undo": "復原", + "shortcuts.action.redo": "重做", + "shortcuts.action.selectAll": "全選", + "shortcuts.action.deleteSelection": "刪除選取", + "shortcuts.action.closeOverlayCancel": "關閉覆蓋層 / 取消操作", + "shortcuts.action.openNodeSearch": "開啟節點搜尋", + "shortcuts.action.resetViewport": "重設視窗", + "shortcuts.action.centerSelection": "置中選取", + "shortcuts.action.fitSelection": "適配選取", + "shortcuts.action.autoLayout": "自動佈局", + "shortcuts.action.toggleSnapToGrid": "切換對齊格線", + "shortcuts.action.bringForward": "往前一層", + "shortcuts.action.sendBackward": "往後一層", + "shortcuts.action.bringToFront": "移到最前", + "shortcuts.action.sendToBack": "移到最後", + "shortcuts.action.zoomInOut": "放大 / 縮小", + "shortcuts.action.pan": "平移", + "shortcuts.action.temporaryPan": "暫時平移", + "shortcuts.action.alternatePan": "替代平移", + "shortcuts.action.fineNudge": "精細移動選取", + "shortcuts.action.fastNudge": "快速移動", + "shortcuts.action.togglePreview": "切換資料預覽", + "shortcuts.action.explainPlan": "執行計畫", + "shortcuts.action.runPreview": "執行預覽", + "shortcuts.action.connectionManager": "連線管理器", + "shortcuts.action.flowVersionHistory": "流程版本歷史", + "shortcuts.resetAll": "全部重設", + "shortcuts.customized": "已自訂", + "shortcuts.default": "預設", + "shortcuts.apply": "套用", + "shortcuts.reset": "重設", + "shortcuts.status.resetAllSuccess": "所有快速鍵已重設為預設值。", + "shortcuts.status.updated": "快速鍵已更新。", + "shortcuts.status.reset": "快速鍵已重設為預設值。", + "shortcuts.status.updateFailed": "無法更新快速鍵。", + "toast.severity.success": "Success", + "toast.severity.warning": "Warning", + "toast.severity.error": "Error", + "toast.details.success": "Success Details", + "toast.details.warning": "Warning Details", + "toast.details.error": "Error Details", + "diagnostics.area.cteEditor": "CTE Editor", + "diagnostics.area.viewEditor": "View Editor", + "diagnostics.area.subEditor": "Sub-editor", + "diagnostics.cteEditor.restoreParentFailed": "Failed to restore the parent canvas. CTE edits were discarded.", + "diagnostics.recommendation.reloadFileIfNeeded": "Reload the file if needed.", + "diagnostics.viewEditor.exitFailed": "Could not exit: {0}", + "diagnostics.viewEditor.canvasIncomplete": "the canvas is incomplete.", + "diagnostics.viewEditor.exitRecommendation": "Connect a valid ResultOutput or use the discard command.", + "diagnostics.viewEditor.restoreParentFailed": "Failed to restore the parent canvas. The subgraph was discarded.", + "diagnostics.subEditor.executeFailed": "Failed to execute editor action: {0}", + "diagnostics.subEditor.executeRecommendation": "Try again. If it persists, reload the canvas.", + "diagnostics.canvasMigration.openWarning": "Open: {0}", + "diagnostics.canvasMigration.sessionRestoreWarning": "Session restore: {0}", + "diagnostics.canvasMigration.versionRestoreWarning": "Version restore: {0}", + "diagnostics.recommendation.resaveLatestSchema": "Review diagnostics and re-save the canvas to persist the latest schema.", + "diagnostics.recommendation.saveMigratedSchema": "Review diagnostics and save the canvas to persist the migrated schema.", + "file.saveDialog.title": "Save Canvas", + "file.saveDialog.suggestedName": "Query1", + "file.save.success": "Canvas saved successfully.", + "file.save.failedWithReason": "Save failed: {0}", + "file.openDialog.title": "Open Canvas", + "file.open.failedWithReason": "Open failed: {0}", + "file.open.success": "Canvas opened successfully.", + "file.open.successWithWarnings": "Canvas opened with warnings.", + "session.restore.failedWithReason": "Restore failed: {0}", + "session.restore.successWithWarnings": "Session restored with warnings.", + "session.restore.success": "Session restored successfully.", + "export.documentation.dialogTitle": "Export Flow Documentation", + "export.documentation.success": "Documentation exported successfully.", + "export.documentation.failed": "Documentation export failed.", + "export.failed.pathPermissionsHint": "Check file path and permissions.", + "export.nodeNotFound": "No {0} Export node found on the canvas. Add one via the node search menu.", + "export.dialogTitleByExtension": "Export as {0}", + "export.success": "Export completed successfully.", + "export.failed": "Export failed.", + "fileHistory.currentFile.none": "No file selected", + "fileHistory.status.saveFirst": "Save the canvas first to enable local history.", + "fileHistory.status.noneFound": "No local versions found yet. Save this file to create history entries.", + "fileHistory.status.countAvailable": "{0} local version(s) available.", + "fileHistory.restore.failedWithReason": "Restore failed: {0}", + "fileHistory.restore.successFrom": "Restored version from {0}.", + "preview.status.cancelled": "Cancelled", + "preview.status.error": "Error", + "preview.status.ready": "Ready", + "preview.runningWithMs": "Running... {0}ms", + "preview.runningWithTimeout": "Running... {0}ms (timeout: {1}s)", + "preview.runningSlowWithTimeout": "Running... {0}ms (timeout: {1}s) · Slow query, timeout in {2}s", + "explain.errorWithReason": "Explain plan error: {0}", + "explain.noSql": "No SQL to explain. Build a query on the canvas first.", + "ddl.compilationFailed": "Compilation failed", + "ddl.compileErrorWithReason": "DDL compile error: {0}", + "command.undo.name": "Undo", + "command.undo.description": "Undo last action", + "command.redo.name": "Redo", + "command.redo.description": "Redo last undone action", + "command.addNode.name": "Add Node", + "command.addNode.description": "Open node search menu to add a node", + "command.bringForward.name": "Bring Forward", + "command.bringForward.description": "Move selected nodes one layer forward", + "command.sendBackward.name": "Send Backward", + "command.sendBackward.description": "Move selected nodes one layer backward", + "command.bringToFront.name": "Bring to Front", + "command.bringToFront.description": "Move selected nodes to top layer", + "command.sendToBack.name": "Send to Back", + "command.sendToBack.description": "Move selected nodes to bottom layer", + "command.normalizeLayers.name": "Normalize Layers", + "command.normalizeLayers.description": "Compact node layer indices to a clean 0..N order", + "tooltip.cleanupOrphans": "Remove orphan nodes not connected to the output (Ctrl+Z to undo)", + "main.orphanSuffix": "Orphan(s)", + "tooltip.autoFixAliasNaming": "Fix alias naming to snake_case (Ctrl+Z to undo)", + "main.namingPrefix": "Naming", + "fileHistory.compressedLabel": "Compressed:", + "schema.itemsSuffix": "item(s)", + "property.panel.title": "Properties", + "property.panel.multiSelected": "{0} nodes selected", + "sqlImporter.status.pasteSelect": "Paste a SELECT statement above, then click Import.", + "sqlImporter.status.inputTooLarge": "SQL input is too large ({0:N0} chars). Limit is {1:N0}. Split the query or increase the import limit.", + "sqlImporter.status.parsing": "Parsing SQL...", + "sqlImporter.status.done": "Done - {0} imported, {1} partial, {2} skipped.", + "sqlImporter.status.cancelledByUser": "Import cancelled by user.", + "sqlImporter.status.timeout": "Import timed out after {0:0.#}s. Try a smaller query or increase timeout.", + "sqlImporter.status.parseError": "Parse error: {0}", + "diagnostics.area.undoRedoTransaction": "Undo/Redo Transaction", + "undoRedo.rollbackExecuted": "Rollback executed for '{0}' ({1} operation(s) reverted).", + "undoRedo.rollbackRecommendation": "Review the canvas state and retry the action if needed.", + "node.preview.noCatalog": "No catalog available", + "connection.error.searchMenuNotInitialized": "search menu not initialized", + "connection.error.timeoutReachability": "Connection timed out - check that the server is reachable and increase the timeout if needed.", + "connection.error.authenticationFailedForProvider": "Authentication failed - verify username and password for {0}.", + "connection.error.databaseNotFoundForProvider": "Database not found - confirm the database name exists on {0}.", + "connection.error.hostNotFound": "Host not found - check the server address and DNS resolution.", + "connection.error.portRefused": "Port connection refused - check the port number and that the server is running / firewall rules allow access.", + "connection.error.sslTls": "SSL/TLS error - check the server's SSL configuration or disable SSL for local connections.", + "connection.error.timeoutOverloaded": "Connection timed out - the server may be overloaded or unreachable. Try increasing the timeout.", + "connection.error.insufficientPrivileges": "Insufficient privileges - the user may lack permission to connect to this database.", + "diagnostics.area.connection": "Connection", + "connection.warning.canvasMayContainOldTables": "The canvas may still contain tables from a previous connection.", + "connection.warning.canvasMayContainOldTablesRecommendation": "Clear the canvas manually or reconnect and choose keep/clear again.", + "undoRedo.transaction.unnamed": "unnamed transaction", + "benchmark.runLabelDefault": "Run 1", + "benchmark.runLabelPattern": "Run {0}", + "benchmark.status.failedWithReason": "Benchmark failed: {0}", + "benchmark.status.noSql": "No SQL to benchmark - build a query first.", + "benchmark.status.warmupProgress": "Warm-up {0}/{1}...", + "benchmark.status.iterationProgress": "Iteration {0}/{1}...", + "benchmark.status.done": "Done - {0}", + "benchmark.status.cancelled": "Benchmark cancelled.", + "app.windowTitle": "AkkornStudio", + "preview.error.safePreviewBlocked": "Safe Preview Mode: data-mutating commands (INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE) cannot be executed in preview.", + "preview.error.noActiveConnection": "No active database connection. Please connect to a database first.", + "sqlImporter.error.selectFromNotFound": "Could not find SELECT ... FROM in the query.", + "sqlImporter.error.fromClauseParseFailed": "Could not parse FROM clause.", + "sqlImporter.error.syntaxUnterminatedString": "Syntax error at line {0}, column {1}: unterminated string literal.", + "sqlImporter.error.missingClosingParenthesis": "missing closing ')'", + "sqlImporter.error.unexpectedClosingParenthesis": "unexpected ')'", + "sqlImporter.error.syntaxAtLineColumn": "Syntax error at line {0}, column {1}: {2}.", + "errorDiagnostics.safePreview.label": "Blocked by Safe Preview Mode", + "errorDiagnostics.safePreview.friendly": "This SQL contains a data-mutating command and cannot be executed in preview.", + "errorDiagnostics.safePreview.suggestion": "Remove or replace the mutating command (INSERT / UPDATE / DELETE / DROP / ALTER / TRUNCATE) before running preview.", + "errorDiagnostics.connection.label": "Connection failed", + "errorDiagnostics.connection.friendly": "Could not reach the database server. The host may be down, unreachable, or blocking connections.", + "errorDiagnostics.connection.suggestion": "Verify the server address and port, ensure the database is running, and check firewall rules.", + "errorDiagnostics.authorization.label": "Authorization error", + "errorDiagnostics.authorization.friendly": "The current credentials do not have permission to perform this operation.", + "errorDiagnostics.authorization.suggestion": "Confirm the database user has SELECT privileges on the target table/schema, or contact your DBA.", + "errorDiagnostics.timeout.label": "Query timeout", + "errorDiagnostics.timeout.friendly": "The query took too long to complete and was cancelled by the server or client.", + "errorDiagnostics.timeout.suggestion": "Add a WHERE clause or LIMIT to reduce the result set, or increase the query timeout in connection settings.", + "errorDiagnostics.schema.label": "Schema error", + "errorDiagnostics.schema.friendly": "A referenced table, column, or object could not be found in the database.", + "errorDiagnostics.schema.suggestion": "Check that all table/column names are spelled correctly and that the schema matches the active connection.", + "errorDiagnostics.syntax.label": "SQL syntax error", + "errorDiagnostics.syntax.friendly": "The query contains a syntax error and could not be parsed by the database engine.", + "errorDiagnostics.syntax.suggestion": "Review the highlighted SQL for typos, mismatched parentheses, or unsupported clauses for the active provider.", + "errorDiagnostics.compatibility.label": "Compatibility error", + "errorDiagnostics.compatibility.friendly": "A function, operator, or syntax construct is not supported by the active database provider.", + "errorDiagnostics.compatibility.suggestion": "Switch to the correct provider in the SQL bar, or replace the unsupported construct with an equivalent.", + "errorDiagnostics.unknown.label": "Unexpected error", + "errorDiagnostics.unknown.friendly": "An error occurred while running the preview query.", + "errorDiagnostics.unknown.suggestion": "Check the technical details below and verify that your canvas is configured correctly.", + "error.mainWindow.invalidDataContext": "MainWindow 的 DataContext 必須是 ShellViewModel。", + "error.mainWindow.canvasNotInitialized": "CanvasViewModel 尚未初始化。", + "error.mainWindow.ddlPreviewUnavailable": "目前畫布無法使用 DDL 預覽。", + "themeJson.editor.template": "{\n \"meta\": { \"name\": \"自訂主題\" },\n \"colors\": {\n \"macroBg0\": \"#0B1020\",\n \"textPrimary\": \"#E8EAED\",\n \"textSecondary\": \"#8B95A8\"\n }\n}", + "themeJson.error.pasteBeforeApply": "套用前請先貼上主題 JSON。", + "themeJson.error.invalidJson": "無效的 JSON:{0}", + "themeJson.error.emptyPayload": "無效的 JSON:內容為空。", + "themeJson.error.invalidTheme": "無效的主題:{0}", + "themeJson.error.appliedButSaveFailed": "主題已套用,但儲存失敗:{0}", + "themeJson.success.appliedAndSaved": "JSON 主題已套用並儲存。", + "themeJson.success.customRemoved": "已移除自訂主題。請重新啟動應用程式以完全回到預設主題。", + "themeJson.error.restoreDefaultFailed": "還原預設主題失敗:{0}", + "themeValidator.error.configNull": "主題設定為 null。", + "themeValidator.warning.noSections": "主題沒有色彩或字體設定區段;無可套用內容。", + "themeValidator.warning.invalidColor": "{0} 的色彩 '{1}' 無效。此鍵將被忽略。", + "themeValidator.warning.sizeOutOfRange": "{0}={1} 超出範圍 (8..48)。此鍵將被忽略。", + "queryExecutor.error.openConnectionMethodNotFound": "在 orchestrator 中找不到 OpenConnectionAsync 方法", + "queryExecutor.error.openConnectionInvokeFailed": "呼叫 OpenConnectionAsync 失敗", + "ddlImporter.warning.viewSelectNotReconstructable": "View '{0}':此 view 的 SELECT 無法以視覺方式重建,請在子畫布手動編輯。", + "ddlImporter.error.tableNotFoundInMetadata": "在目前中繼資料中找不到資料表 '{0}'。", + "main.window.untitled": "未命名", + "main.subEditor.noSeedProvided": "未提供 {0} 的子編輯器 seed。", + "main.layerOrder.bringToFront": "移到最上層", + "main.layerOrder.sendToBack": "移到最下層", + "main.layerOrder.bringForward": "向前一層", + "main.layerOrder.sendBackward": "向後一層", + "main.layerOrder.normalizeLayers": "正規化圖層", + "export.fileType.html": "HTML 檔案", + "export.fileType.json": "JSON 檔案", + "export.fileType.csv": "CSV 檔案", + "export.fileType.excel": "Excel 檔案", + "commandPalette.templatePrefix": "範本:{0}", + "themeLoader.status.notFoundWithPath": "找不到主題檔案:{0}", + "themeLoader.status.deserializedNull": "主題 JSON 反序列化結果為 null。", + "themeLoader.status.loaded": "主題 JSON 載入成功。", + "credential.error.ciphertextTooShort": "密文資料太短。", + "credential.error.dpapiWindowsOnly": "DPAPI 僅支援 Windows。", + "credential.warning.loadVaultFailed": "載入憑證保險庫 {0} 失敗:{1}", + "credential.warning.persistVaultFailed": "儲存憑證保險庫 {0} 失敗:{1}", + "snippetStore.warning.loadFailed": "從 {0} 載入 snippet 失敗:{1}", + "snippetStore.warning.saveFailed": "儲存 snippet 失敗:{0}", + "flowVersionStore.warning.loadFailed": "從 {0} 載入流程版本失敗:{1}", + "flowVersionStore.warning.saveFailed": "儲存流程版本失敗:{0}", + "queryExecutor.error.queryEmpty": "查詢不可為空", + "queryExecutor.error.providerNotSupported": "不支援供應者 {0}", + "queryExecutor.error.singleStatementOnly": "預覽僅接受單一 SQL 語句。", + "queryExecutor.error.queryEmptyWithPeriod": "查詢不可為空。", + "queryExecutor.error.readOnlyOnly": "預覽模式僅支援唯讀 SQL。", + "queryExecutor.error.namedParametersNotSupported": "預覽模式不支援執行 SQL 的命名參數。請改用安全內嵌常值或在預覽外執行查詢。", + "queryExecutor.error.positionalParametersNotSupported": "預覽模式不支援位置參數佔位符(? 或 )。", + "commandPalette.description.align_selected_nodes_to_the_bottom_edge": "將所選節點對齊到底部邊緣", + "commandPalette.description.align_selected_nodes_to_the_leftmost_edge": "將所選節點對齊到最左側邊緣", + "commandPalette.description.align_selected_nodes_to_the_rightmost_edge": "將所選節點對齊到最右側邊緣", + "commandPalette.description.align_selected_nodes_to_the_topmost_edge": "將所選節點對齊到頂部邊緣", + "commandPalette.description.apply_cte_sub_canvas_edits_and_return_to_the_parent_canvas": "套用 CTE 子畫布編輯並返回父畫布", + "commandPalette.description.arrange_nodes_into_logical_columns_automatically": "自動將節點排列為邏輯欄位", + "commandPalette.description.centre_selected_nodes_on_a_horizontal_axis": "在水平軸上置中所選節點", + "commandPalette.description.centre_selected_nodes_on_a_vertical_axis": "在垂直軸上置中所選節點", + "commandPalette.description.clear_canvas_and_start_fresh": "清空畫布並重新開始", + "commandPalette.description.clear_node_selection": "清除節點選取", + "commandPalette.description.convert_aliases_to_the_convention_configured_in_project_settings": "將別名轉換為專案設定中的命名慣例", + "commandPalette.description.create_checkpoints_compare_versions_side_by_side_and_restore_a_previous_canvas_state": "建立檢查點、並排比較版本並還原先前畫布狀態", + "commandPalette.description.delete_the_selected_nodes": "刪除所選節點", + "commandPalette.description.discard_current_sub_editor_edits_and_return_to_the_parent_canvas": "捨棄目前子編輯器變更並返回父畫布", + "commandPalette.description.execute_the_current_query_in_preview": "在預覽中執行目前查詢", + "commandPalette.description.fit_all_nodes_into_the_visible_area": "將所有節點適配至可視區域", + "commandPalette.description.generate_csv_file_from_the_first_csv_export_node": "從第一個 CSV 匯出節點產生 CSV 檔案", + "commandPalette.description.generate_html_file_from_the_first_html_export_node": "從第一個 HTML 匯出節點產生 HTML 檔案", + "commandPalette.description.generate_json_file_from_the_first_json_export_node": "從第一個 JSON 匯出節點產生 JSON 檔案", + "commandPalette.description.generate_xlsx_workbook_from_the_first_excel_export_node": "從第一個 Excel 匯出節點產生 XLSX 活頁簿", + "commandPalette.description.inspect_the_query_execution_plan_see_scan_types_join_strategies_and_cost_estimates": "檢視查詢執行計畫:掃描類型、Join 策略與成本估算", + "commandPalette.description.load_a_vsaq_canvas_file": "載入 .vsaq 畫布檔案", + "commandPalette.description.measure_avg_median_p95_latency_of_the_current_sql_over_n_iterations": "測量目前 SQL 在 N 次迭代下的平均/中位/p95 延遲", + "commandPalette.description.open_isolated_sub_canvas_editor_for_the_selected_cte_definition_node": "為所選 CTE 定義節點開啟獨立子畫布編輯器", + "commandPalette.description.open_local_file_version_history_created_on_each_save_and_restore_previous_saved_snapshots": "開啟每次儲存產生的本機版本歷史並還原先前快照", + "commandPalette.description.open_output_preview_modal_for_the_active_mode": "開啟目前模式的輸出預覽對話框", + "commandPalette.description.open_shortcut_reference_screen": "開啟快捷鍵參考畫面", + "commandPalette.description.open_the_connection_manager_to_add_edit_or_switch_database_connections": "開啟連線管理器以新增、編輯或切換資料庫連線", + "commandPalette.description.paste_a_select_statement_and_generate_nodes_automatically_from_join_where_limit_are_supported": "貼上 SELECT 語句並自動產生節點(支援 FROM、JOIN、WHERE、LIMIT)", + "commandPalette.description.remove_all_nodes_not_connected_to_output": "移除所有未連接到輸出的節點", + "commandPalette.description.reset_zoom_and_pan_to_default": "將縮放與平移重設為預設值", + "commandPalette.description.save_canvas_to_a_new_file": "將畫布儲存為新檔案", + "commandPalette.description.save_current_canvas": "儲存目前畫布", + "commandPalette.description.save_markdown_documentation_of_the_current_flow": "儲存目前流程的 Markdown 文件", + "commandPalette.description.save_the_selected_nodes_as_a_reusable_snippet_insert_it_later_via_the_node_search_menu_a": "將所選節點儲存為可重用片段,稍後可透過節點搜尋選單(⇧A)插入", + "commandPalette.description.scan_all_table_source_nodes_on_the_canvas_for_possible_join_relationships_based_on_fk_conventions_and_naming_patterns": "依據 FK 慣例與命名模式掃描畫布上的表來源節點,找出可能的 Join 關係", + "commandPalette.description.select_all_nodes_on_canvas": "選取畫布上的所有節點", + "commandPalette.description.snap_node_positions_to_16px_grid_ctrl_g": "將節點位置吸附到 16px 網格(Ctrl+G)", + "commandPalette.description.spread_selected_nodes_with_equal_horizontal_spacing": "將所選節點以相等水平間距分佈", + "commandPalette.description.spread_selected_nodes_with_equal_vertical_spacing": "將所選節點以相等垂直間距分佈", + "commandPalette.description.zoom_into_the_canvas": "放大畫布", + "commandPalette.description.zoom_out_of_the_canvas": "縮小畫布", + "commandPalette.name.align_bottom": "底部對齊", + "commandPalette.name.align_left": "靠左對齊", + "commandPalette.name.align_right": "靠右對齊", + "commandPalette.name.align_top": "頂部對齊", + "commandPalette.name.analyze_all_joins": "分析所有 Join", + "commandPalette.name.auto_fix_naming": "自動修正命名", + "commandPalette.name.auto_layout": "自動排版", + "commandPalette.name.center_horizontally": "水平置中", + "commandPalette.name.center_vertically": "垂直置中", + "commandPalette.name.cleanup_orphans": "清理孤立節點", + "commandPalette.name.delete_selected": "刪除已選取", + "commandPalette.name.deselect_all": "取消全選", + "commandPalette.name.discard_and_exit_editor": "放棄並離開編輯器", + "commandPalette.name.distribute_horizontally": "水平平均分佈", + "commandPalette.name.distribute_vertically": "垂直平均分佈", + "commandPalette.name.edit_selected_cte": "編輯所選 CTE", + "commandPalette.name.exit_cte_editor": "離開 CTE 編輯器", + "commandPalette.name.explain_plan": "執行計畫", + "commandPalette.name.export_csv": "匯出 CSV", + "commandPalette.name.export_documentation": "匯出文件", + "commandPalette.name.export_excel": "匯出 Excel", + "commandPalette.name.export_html": "匯出 HTML", + "commandPalette.name.export_json": "匯出 JSON", + "commandPalette.name.file_save_load_history": "檔案儲存/載入歷史", + "commandPalette.name.fit_to_screen": "符合畫面", + "commandPalette.name.flow_version_history": "流程版本歷史", + "commandPalette.name.import_sql_to_graph": "將 SQL 匯入為圖形", + "commandPalette.name.keyboard_shortcuts": "鍵盤快捷鍵", + "commandPalette.name.manage_connections": "管理連線", + "commandPalette.name.new_canvas": "新增畫布", + "commandPalette.name.open_file": "開啟檔案", + "commandPalette.name.reset_viewport": "重設檢視區域", + "commandPalette.name.run_preview": "執行預覽", + "commandPalette.name.run_query_benchmark": "執行查詢效能測試", + "commandPalette.name.save": "儲存", + "commandPalette.name.save_as": "另存新檔", + "commandPalette.name.save_selection_as_snippet": "將選取儲存為片段", + "commandPalette.name.select_all": "全選", + "commandPalette.name.toggle_preview": "切換預覽", + "commandPalette.name.toggle_snap_to_grid": "切換格線吸附", + "commandPalette.name.zoom_in": "放大", + "commandPalette.name.zoom_out": "縮小", + "commandPalette.tags.100_percent_restore_zoom_pan_viewport": "100 percent restore zoom pan viewport", + "commandPalette.tags.align_bottom_edge_selection_nodes": "align bottom edge selection nodes", + "commandPalette.tags.align_center_middle_horizontal_nodes": "align center middle horizontal nodes", + "commandPalette.tags.align_center_middle_vertical_nodes": "align center middle vertical nodes", + "commandPalette.tags.align_left_edge_selection_nodes": "align left edge selection nodes", + "commandPalette.tags.align_right_edge_selection_nodes": "align right edge selection nodes", + "commandPalette.tags.align_top_edge_selection_nodes": "align top edge selection nodes", + "commandPalette.tags.auto_layout_view_reset_zoom": "auto layout view reset zoom", + "commandPalette.tags.benchmark_performance_latency_timing_profile_measure_speed": "benchmark performance latency timing profile measure speed", + "commandPalette.tags.clear_selection": "clear selection", + "commandPalette.tags.connection_database_server_host_provider_switch": "connection database server host provider switch", + "commandPalette.tags.create_insert_search_transform": "create insert search transform", + "commandPalette.tags.cte_subcanvas_exit_apply_back": "cte subcanvas exit apply back", + "commandPalette.tags.cte_view_subcanvas_discard_exit_force": "cte view subcanvas discard exit force", + "commandPalette.tags.cte_with_recursive_editor_subgraph_subcanvas_isolate": "cte with recursive editor subgraph subcanvas isolate", + "commandPalette.tags.data_results_table_panel": "data results table panel", + "commandPalette.tags.distribute_space_equal_horizontal_nodes": "distribute space equal horizontal nodes", + "commandPalette.tags.distribute_space_equal_vertical_nodes": "distribute space equal vertical nodes", + "commandPalette.tags.execute_run_sql_query_results": "execute run sql query results", + "commandPalette.tags.explain_plan_execution_cost_scan_index_join_performance": "explain plan execution cost scan index join performance", + "commandPalette.tags.export_csv_file_tabular_output_save": "export csv file tabular output save", + "commandPalette.tags.export_excel_xlsx_file_tabular_output_spreadsheet_save": "export excel xlsx file tabular output spreadsheet save", + "commandPalette.tags.export_html_file_output_report_save": "export html file output report save", + "commandPalette.tags.export_json_file_output_save": "export json file output save", + "commandPalette.tags.export_markdown_doc_documentation_flow_save_md": "export markdown doc documentation flow save md", + "commandPalette.tags.export_persist_copy": "export persist copy", + "commandPalette.tags.file_history_save_load_backup_versions_restore_local": "file history save load backup versions restore local", + "commandPalette.tags.forward_history": "forward history", + "commandPalette.tags.help_shortcuts_hotkeys_keyboard_reference": "help shortcuts hotkeys keyboard reference", + "commandPalette.tags.highlight_mark_all_nodes": "highlight mark all nodes", + "commandPalette.tags.import_sql_paste_convert_graph_reverse_engineer_query": "import sql paste convert graph reverse engineer query", + "commandPalette.tags.join_autojoin_analyze_suggest_detect_foreign_key_relationships_heuristic": "join autojoin analyze suggest detect foreign key relationships heuristic", + "commandPalette.tags.layer_z_order_back_selected_nodes": "layer z-order back selected nodes", + "commandPalette.tags.layer_z_order_backward_selected_nodes": "layer z-order backward selected nodes", + "commandPalette.tags.layer_z_order_forward_selected_nodes": "layer z-order forward selected nodes", + "commandPalette.tags.layer_z_order_front_selected_nodes": "layer z-order front selected nodes", + "commandPalette.tags.layer_z_order_normalize_compact": "layer z-order normalize compact", + "commandPalette.tags.layout_arrange_columns_auto_organize_readability": "layout arrange columns auto organize readability", + "commandPalette.tags.load_import_vsaq": "load import vsaq", + "commandPalette.tags.magnify_enlarge": "magnify enlarge", + "commandPalette.tags.orphan_unused_disconnected_clean_delete_nodes": "orphan unused disconnected clean delete nodes", + "commandPalette.tags.persist_write_disk": "persist write disk", + "commandPalette.tags.remove_erase_nodes": "remove erase nodes", + "commandPalette.tags.rename_alias_fix_naming_convention": "rename alias fix naming convention", + "commandPalette.tags.reset_clear_blank": "reset clear blank", + "commandPalette.tags.revert_back_history": "revert back history", + "commandPalette.tags.shrink_reduce": "shrink reduce", + "commandPalette.tags.snap_grid_align_precision_position": "snap grid align precision position", + "commandPalette.tags.snippet_save_selection_reuse_template_favorite_bookmark": "snippet save selection reuse template favorite bookmark", + "commandPalette.tags.version_history_checkpoint_diff_restore_snapshot_compare_undo_flow": "version history checkpoint diff restore snapshot compare undo flow", + "sqlEditor.diffPreview.title": "Transactional Diff Preview", + "sqlEditor.mutation.confirmExecute": "Confirm Execute", + "sqlEditor.tab.closeAnyway": "Close Anyway", + "sqlEditor.tab.keepTab": "Keep Tab", + "sqlEditor.status.ready": "Ready.", + "sqlEditor.telemetry.none": "No execution telemetry yet.", + "sqlEditor.telemetry.summary": "Statements: {0} Success: {1} Failed: {2} Total: {3} ms", + "sqlEditor.telemetry.errors.none": "No aggregated errors.", + "sqlEditor.diff.none": "No transactional diff preview available.", + "sqlEditor.mutation.estimate.none": "No mutation estimate available.", + "sqlEditor.mutation.estimate.value": "Estimated affected rows: {0}", + "sqlEditor.mutation.estimate.unavailable": "Could not estimate affected rows automatically.", + "sqlEditor.tab.closePending": "Unsaved changes detected. Confirm tab close.", + "sqlEditor.tab.noPendingClose": "No tab close pending.", + "sqlEditor.tab.manyWarning": "High tab count: {0} open tabs.", + "sqlEditor.mutation.pending.none": "No pending mutation confirmation.", + "sqlEditor.mutation.pending.required": "Mutation requires confirmation before execution.", + "sqlEditor.message.empty": "Execute a statement to see messages.", + "sqlEditor.message.success": "Execution completed successfully.", + "sqlEditor.result.summary.empty": "Rows: - Time: -", + "sqlEditor.result.summary": "Rows: {0} Time: {1} ms", + "sqlEditor.file.save.canceled": "Save canceled.", + "sqlEditor.file.save.noPath": "No target path selected.", + "sqlEditor.file.save.success": "SQL file saved.", + "sqlEditor.file.save.failed": "Save failed.", + "sqlEditor.file.open.failed": "Open failed.", + "sqlEditor.file.open.notFound": "Selected SQL file was not found.", + "sqlEditor.file.open.success": "SQL file opened.", + "sqlEditor.status.executing": "Executing SQL...", + "sqlEditor.status.executingScript": "Executing SQL script...", + "sqlEditor.status.executingStep": "Executing {0}/{1}...", + "sqlEditor.status.canceling": "Canceling execution...", + "sqlEditor.status.executingConfirmedMutation": "Executing confirmed mutation...", + "sqlEditor.status.mutationCanceled": "Mutation execution canceled.", + "sqlEditor.detail.statementNotExecuted": "Statement was not executed.", + "sqlEditor.status.success": "Execution succeeded.", + "sqlEditor.detail.rowsAndTime": "{0} row(s) in {1} ms.", + "sqlEditor.status.canceled": "Execution canceled.", + "sqlEditor.status.failed": "Execution failed.", + "sqlEditor.status.confirmationRequired": "Confirmation required before execution.", + "sqlEditor.error.mutationConfirmationRequired": "Mutation confirmation required.", + "sqlEditor.result.tabTitle": "Result {0}", + "sqlEditor.tab.closeRequiresConfirmation": "Tab close requires confirmation.", + "sqlEditor.tab.unsavedDetail": "This tab has unsaved changes.", + "sqlEditor.tab.closed": "Tab closed.", + "sqlEditor.tab.closeCanceled": "Tab close canceled.", + "sqlEditor.tab.closeCanceledDetail": "Unsaved tab kept open.", + "sqlEditor.error.noStatementSelected": "No SQL statement selected for execution.", + "sqlEditor.error.noConnection": "No active database connection for SQL execution.", + "sqlEditor.error.executionCanceled": "SQL execution was canceled.", + "sqlEditor.tab.scriptTitle": "Script {0}", + "sqlEditor.guard.delete.noWhere.message": "DELETE without WHERE can remove all rows.", + "sqlEditor.guard.delete.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.delete.trivialWhere.message": "DELETE has a trivially true WHERE clause.", + "sqlEditor.guard.delete.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.update.noWhere.message": "UPDATE without WHERE can affect all rows.", + "sqlEditor.guard.update.noWhere.recommendation": "Add a restrictive WHERE clause before executing.", + "sqlEditor.guard.update.trivialWhere.message": "UPDATE has a trivially true WHERE clause.", + "sqlEditor.guard.update.trivialWhere.recommendation": "Use a selective filter to target only intended rows.", + "sqlEditor.guard.insert.noColumnList.message": "INSERT without explicit column list is fragile against schema changes.", + "sqlEditor.guard.insert.noColumnList.recommendation": "Prefer INSERT INTO table(col1, col2, ...) VALUES (...).", + "sqlEditor.guard.ddl.message": "DDL statement may cause structural changes in the database.", + "sqlEditor.guard.ddl.recommendation": "Confirm execution only when schema changes are intended.", + "sqlEditor.diff.unavailable.noPreview": "No transactional diff preview available for this statement.", + "sqlEditor.diff.unavailable.parseError": "Could not parse mutation target for transactional diff preview.", + "sqlEditor.diff.unavailable.connection": "Transactional diff preview unavailable due to connection or query limitations.", + "sqlEditor.diff.deleteSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, affected {2}, total rows after {3}.", + "sqlEditor.diff.updateSummary": "Transactional diff preview (ROLLBACK guaranteed): table {0}, total rows before {1}, candidate rows affected {2}, total rows after {3}.", + "sqlEditor.diff.unavailable.unsupportedStatement": "Transactional diff preview currently supports UPDATE and DELETE only.", + "sqlEditor.results.title": "Results", + "sqlEditor.saveSql.fileType": "SQL Files", + "sqlEditor.saveSql.pickerTitle": "Save SQL File", + "sqlEditor.export.pickerTitle": "Export SQL Data", + "sqlEditor.export.status.noResultTitle": "No execution result available for export.", + "sqlEditor.export.status.noResultDetail": "Execute a query first.", + "sqlEditor.export.status.successTitle": "Report exported.", + "sqlEditor.export.status.failedTitle": "Failed to export report.", + "sqlEditor.export.fileType.html": "HTML File", + "sqlEditor.export.fileType.json": "JSON File", + "sqlEditor.export.fileType.csv": "CSV File", + "sqlEditor.export.fileType.xlsx": "Excel Workbook", + "sqlEditor.export.defaultFileBase": "report", + "sqlEditor.export.defaultTitle": "SQL Report", + "sqlEditor.export.error.typeRequired": "A report type must be selected before export.", + "sqlEditor.export.type.html.title": "HTML full-feature report", + "sqlEditor.export.type.html.description": "Standalone, SQL-first HTML artifact for offline audit.", + "sqlEditor.export.type.json.title": "JSON execution contract", + "sqlEditor.export.type.json.description": "Machine-readable payload with SQL, metadata and execution result.", + "sqlEditor.export.type.csv.title": "CSV data export", + "sqlEditor.export.type.csv.description": "Tabular result data only, suitable for spreadsheet tools.", + "sqlEditor.export.type.xlsx.title": "Excel workbook export", + "sqlEditor.export.type.xlsx.description": "Spreadsheet workbook with query result data only.", + "sqlEditor.export.dialog.windowTitle": "Export SQL Data", + "sqlEditor.export.dialog.title": "Export SQL Data", + "sqlEditor.export.dialog.subtitle": "Choose the artifact format and metadata before exporting.", + "sqlEditor.export.dialog.confirm": "Export", + "sqlEditor.export.dialog.fileNameWatermark": "report.html", + "sqlEditor.export.dialog.titleWatermark": "SQL Report", + "sqlEditor.export.dialog.descriptionWatermark": "Additional context for auditors and teammates.", + "sqlEditor.export.dialog.section.reportType": "REPORT TYPE", + "sqlEditor.export.dialog.section.fileName": "FILE NAME", + "sqlEditor.export.dialog.section.reportTitle": "TITLE", + "sqlEditor.export.dialog.section.description": "DESCRIPTION", + "sqlEditor.export.dialog.section.options": "OPTIONS", + "sqlEditor.export.option.includeSchema": "Include output schema", + "sqlEditor.export.option.includeNodeDetails": "Include node/connection placeholders in JSON", + "sqlEditor.export.option.includeMetadata": "Include optional metadata", + "sqlEditor.export.option.useDashForEmpty": "Use '-' for empty fields", + "sqlEditor.export.badge.offline": "OFFLINE READY", + "sqlEditor.export.badge.structured": "STRUCTURED PAYLOAD", + "sqlEditor.export.badge.dataOnly": "DATA ONLY" +} diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/dist/report-app.css b/src/AkkornStudio.UI/Assets/ReportFrontend/dist/report-app.css new file mode 100644 index 00000000..4b1aafc4 --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/dist/report-app.css @@ -0,0 +1 @@ +:root{color-scheme:dark;--bg: #0a101b;--surface: #0f1728;--surface-2: #141f35;--surface-3: #1b2942;--border: #2a3b5d;--border-strong: #385180;--text: #edf3ff;--text-soft: #9eb0d2;--text-strong: #f8fbff;--accent: #6ea8fe;--accent-strong: #88b4ff;--accent-soft: rgba(110, 168, 254, .14);--success: #72e2a1;--warning: #f4c26b;--danger: #ff8d94;--shadow: 0 20px 60px rgba(3, 10, 21, .28)}:root[data-theme=light]{color-scheme:light;--bg: #f4f7fb;--surface: #ffffff;--surface-2: #f6f8fc;--surface-3: #eef2f9;--border: #d4dceb;--border-strong: #bbc7dc;--text: #162136;--text-soft: #5e6d88;--text-strong: #0d1728;--accent: #356ef1;--accent-strong: #275ad7;--accent-soft: rgba(53, 110, 241, .12);--success: #2f8f57;--warning: #b26d00;--danger: #d14255;--shadow: 0 24px 54px rgba(23, 42, 79, .12)}*{box-sizing:border-box}body{margin:0;font-family:Segoe UI,system-ui,sans-serif;background:var(--bg);color:var(--text)}button,input,select{font:inherit;outline:none}button:focus,input:focus,select:focus,button:focus-visible,input:focus-visible,select:focus-visible{outline:none}.report-shell{max-width:1680px;margin:0 auto;padding:28px}.page-header,.panel,.mini-panel,.modal-card,.toast{box-shadow:var(--shadow)}.page-header,.panel,.mini-panel,.modal-card,.ghost-btn,.collapse-btn,.field-input,.chip,.pager,.pick-item,.icon-only{border:1px solid var(--border)}.page-header,.panel,.mini-panel,.modal-card{background:#0f1728db;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}:root[data-theme=light] .page-header,:root[data-theme=light] .panel,:root[data-theme=light] .mini-panel,:root[data-theme=light] .modal-card{background:#ffffffeb}.page-header{display:grid;grid-template-columns:260px minmax(320px,1fr) auto;gap:24px;align-items:center;border-radius:24px;padding:24px}.brand-wrap{display:flex;gap:14px;align-items:center}.brand-mark{width:48px;height:48px;display:grid;place-items:center;border-radius:16px;background:var(--surface-2);color:var(--accent);font-weight:800;font-size:1.15rem;border:1px solid var(--border)}.brand-name{font-size:.76rem;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:var(--text-soft)}.brand-sub{font-size:.95rem;font-weight:700;color:var(--text-strong)}.page-intro h1{margin:0 0 10px;font-size:clamp(1.4rem,2vw,2rem);line-height:1.1}.toolbar{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px;align-items:center}.main-content{display:grid;gap:18px;margin-top:20px}.panel{border-radius:24px;padding:18px}.section-head{display:flex;justify-content:space-between;gap:18px;align-items:center;margin-bottom:14px}.section-head h2{margin:0;font-size:1.05rem}.section-copy{margin-top:6px;font-size:.92rem;color:var(--text-soft)}.section-actions{display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;align-items:center}.overview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.overview-card{display:flex;gap:14px;align-items:start;padding:18px;border-radius:20px;background:var(--surface);border:1px solid var(--border)}.overview-card.is-wide{grid-column:1 / -1}.overview-card.is-warning{border-color:#f4c26b59}.overview-icon{width:42px;height:42px;display:grid;place-items:center;border-radius:14px;background:var(--accent-soft);color:var(--accent)}.overview-label{font-size:.78rem;letter-spacing:.08em;text-transform:uppercase;color:var(--text-soft)}.overview-value{margin-top:8px;font-size:1rem;font-weight:700;line-height:1.45}.dataset-body{display:grid;gap:14px}.summary-strip{display:grid;gap:12px}.mini-panel{border-radius:18px;padding:14px;background:var(--surface-2)}.mini-panel-head{display:flex;justify-content:space-between;gap:12px;align-items:center}.mini-panel-title{font-weight:700}.mini-panel-actions{display:flex;gap:8px}.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px}.chip{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;border-radius:999px;background:var(--surface);color:var(--text-soft)}.chip.empty{opacity:.8}.chip.removable{cursor:pointer}.table-shell{position:relative;display:grid;gap:12px}.table-shell.is-page-busy .table-scroll,.table-shell.is-page-busy .pager{opacity:.72}.table-loading{position:absolute;top:56px;right:0;bottom:72px;left:0;display:grid;place-items:center;pointer-events:none;z-index:8}.table-loading-card{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--border);background:#0f1728eb;color:var(--text);box-shadow:var(--shadow)}:root[data-theme=light] .table-loading-card{background:#fffffff2}.table-loading-spinner{width:14px;height:14px;border-radius:999px;border:2px solid var(--border-strong);border-top-color:var(--accent);animation:spin .8s linear infinite}.table-meta{display:flex;justify-content:space-between;gap:14px;align-items:flex-start;flex-wrap:wrap}.table-meta-main,.preset-toolbar,.preset-actions,.preset-secondary,.table-status,.table-quick-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}.table-meta-main{flex:1 1 420px}.preset-toolbar{gap:10px}.preset-segmented{display:inline-flex;flex-wrap:wrap;gap:4px;padding:4px;border:1px solid var(--border);border-radius:999px;background:var(--surface-2)}.segment-btn,.icon-chip{border:0;border-radius:999px;background:transparent;color:var(--text-soft);cursor:pointer}.segment-btn{padding:9px 12px;font-weight:700}.segment-btn:hover,.icon-chip:hover{background:var(--surface);color:var(--text)}.icon-chip{width:34px;height:34px;display:inline-grid;place-items:center;border:1px solid var(--border);background:var(--surface)}.quick-filters-wrap{overflow-x:auto;padding-bottom:2px}.quick-filters{display:flex;flex-wrap:nowrap;gap:8px;align-items:center;min-width:max-content}.quick-filters-label{color:var(--text-soft);font-size:.86rem;font-weight:700;position:sticky;left:0;padding-right:4px;background:var(--surface)}.quick-chip{background:var(--surface-2);border:1px solid var(--border);padding:6px 10px}.quick-chip small{opacity:.72;padding:2px 6px;border-radius:999px;background:var(--surface)}.result-count{font-weight:700;color:var(--text-soft);padding-inline:2px}.status-pill{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;border-radius:999px;background:var(--surface-2);color:var(--text-soft);border:1px solid var(--border);font-size:.88rem}.pager-wrap{display:flex;flex-wrap:wrap;gap:14px;align-items:center;justify-content:flex-end}.pager-wrap-bottom{padding-top:4px}.page-size-wrap{display:grid;gap:6px}.pager{display:flex;align-items:center;gap:8px;border-radius:16px;padding:8px;background:var(--surface-2)}.pager-readout{min-width:110px;text-align:center;color:var(--text-soft);font-weight:700}.pager-input{width:70px;padding:10px 12px;border-radius:12px;border:1px solid var(--border);background:var(--surface);color:var(--text)}.table-scroll{position:relative;overflow-x:auto;overflow-y:hidden;border-radius:18px;border:1px solid var(--border)}.table-scroll.has-left-shadow:before,.table-scroll.has-right-shadow:after{content:"";position:sticky;top:0;bottom:0;width:18px;pointer-events:none;z-index:6}.table-scroll.has-left-shadow:before{left:0;float:left;background:linear-gradient(90deg,rgba(10,16,27,.7),transparent)}.table-scroll.has-right-shadow:after{right:0;float:right;background:linear-gradient(270deg,rgba(10,16,27,.7),transparent)}:root[data-theme=light] .table-scroll.has-left-shadow:before{background:linear-gradient(90deg,rgba(244,247,251,.9),transparent)}:root[data-theme=light] .table-scroll.has-right-shadow:after{background:linear-gradient(270deg,rgba(244,247,251,.9),transparent)}.report-table{width:max-content;min-width:100%;border-collapse:separate;border-spacing:0;min-width:920px}.report-table thead th{background:var(--surface-2);border-bottom:1px solid var(--border);padding:0;z-index:1}.th-shell{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:stretch}.sort-head{width:100%;display:flex;justify-content:space-between;align-items:center;gap:10px;border:0;background:transparent;color:inherit;padding:14px;font-weight:700;cursor:pointer}.resize-handle{width:28px;min-width:28px;border:0;border-left:1px solid var(--border);background:transparent;color:var(--text-soft);cursor:col-resize;display:grid;place-items:center}.resize-handle:hover{color:var(--accent);background:var(--accent-soft)}.resize-handle:active{background:var(--surface-3)}.report-table tbody td{padding:0;border-bottom:1px solid rgba(255,255,255,.06)}:root[data-theme=light] .report-table tbody td{border-bottom-color:#1a233614}.cell-body{display:flex;align-items:start;gap:10px;padding:13px 14px}.cell{cursor:pointer;vertical-align:top}.cell-text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere}.cell-expand{flex:0 0 auto;border:0;background:transparent;color:var(--text-soft);cursor:pointer}.select-col{width:28px;min-width:28px;background:var(--surface-2)}.select-col-width{width:28px}.sticky-select{position:sticky;left:0;z-index:4}.sticky-main-col{position:sticky;left:28px;z-index:3;background:var(--surface);box-shadow:8px 0 18px #050a1224}.report-table thead .sticky-main-col{background:var(--surface-2)}.row-selector{width:100%;min-height:100%;min-width:28px;padding:0;border:0;background:transparent;cursor:pointer;color:var(--text-soft)}.row-selector:hover,.is-row-selected .row-selector{color:var(--accent)}.is-row-selected td{background:#6ea8fe14}.is-selected-cell{outline:2px solid var(--accent);outline-offset:-2px;background:#6ea8fe1f}.is-number .cell-text{color:#7cc7ff}.is-date .cell-text{color:#f4c26b}.is-boolean .cell-text{color:#78dba5;font-weight:700}.is-null .cell-text{color:var(--text-soft);font-style:italic}.empty-state{padding:28px;text-align:center;color:var(--text-soft)}.sql-box{padding:18px;border-radius:18px;background:#0a1120;border:1px solid var(--border);overflow:auto}.sql-box pre,.cell-modal-text{margin:0;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere}.page-footer{margin:18px 0 8px;text-align:center;color:var(--text-soft)}.modal-shell{position:fixed;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;padding:24px;background:#040a12a8;z-index:20}.modal-card{width:min(980px,100%);max-height:88vh;overflow:hidden;border-radius:22px;display:grid;grid-template-rows:auto 1fr auto}.modal-card.modal-wide{width:min(1180px,100%)}.modal-card.modal-cell{width:min(1080px,100%)}.modal-head,.modal-foot{display:flex;justify-content:space-between;gap:12px;align-items:center;padding:18px 20px;border-bottom:1px solid var(--border)}.modal-foot{border-top:1px solid var(--border);border-bottom:0;justify-content:flex-end}.modal-title{font-size:1.08rem;font-weight:800}.modal-body{overflow:auto;padding:20px}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.field-span{grid-column:1 / -1}.field-stack{display:grid;gap:8px}.field-label{display:inline-flex;align-items:center;gap:8px;font-size:.84rem;font-weight:700;color:var(--text-soft)}.field-input{width:100%;border-radius:14px;padding:12px 14px;background:var(--surface);color:var(--text)}.field-input.compact-search{min-width:220px;border:0;background:transparent;padding-left:0;padding-right:0}.preset-select{min-width:180px}.sort-rules{display:grid;gap:12px}.sort-rule{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,1fr) auto;gap:12px;align-items:end;padding:14px;border:1px solid var(--border);border-radius:16px;background:var(--surface-2)}.sort-remove{align-self:end}.field-input:focus,.field-input:focus-visible,.compact-select:focus,.compact-select:focus-visible,.pager-input:focus,.pager-input:focus-visible,.compact-search:focus,.compact-search:focus-visible{outline:none;box-shadow:none;border-color:var(--border)}.compact-select{width:84px}.search-field{display:inline-flex;align-items:center;gap:10px;min-width:260px;padding:0 12px;border:1px solid var(--border);border-radius:14px;background:var(--surface);color:var(--text-soft)}.ghost-btn,.collapse-btn,.icon-only{display:inline-flex;align-items:center;justify-content:center;gap:8px;border-radius:14px;padding:11px 14px;background:var(--surface);color:var(--text);cursor:pointer}.ghost-btn.small{padding:8px 10px}.icon-only{width:42px;height:42px;padding:0}.ghost-btn:disabled,.icon-only:disabled{opacity:.45;cursor:not-allowed}.ghost-btn:hover:not(:disabled),.collapse-btn:hover,.icon-only:hover:not(:disabled),.sort-head:hover,.pick-item:hover{border-color:var(--border-strong);background:var(--surface-3)}.picklist{display:grid;grid-template-columns:minmax(0,1fr) 96px minmax(0,1fr);gap:14px;align-items:center}.pick-pane{display:grid;gap:10px}.pick-list{display:grid;gap:8px;min-height:320px;max-height:440px;overflow:auto;padding:10px;border:1px solid var(--border);border-radius:18px;background:var(--surface-2)}.pick-item{text-align:left;border-radius:12px;padding:12px 14px;background:var(--surface);color:var(--text);cursor:pointer}.pick-item.active{border-color:var(--accent);background:var(--accent-soft)}.pick-actions{display:grid;gap:10px}.icon-glyph,.icon-glyph svg{width:16px;height:16px;display:inline-block}.toast{position:fixed;right:24px;bottom:24px;padding:14px 18px;border-radius:16px;background:var(--surface);color:var(--text-strong);z-index:30}@keyframes spin{to{transform:rotate(360deg)}}.context-menu{position:fixed;min-width:220px;display:grid;gap:4px;padding:8px;border-radius:16px;border:1px solid var(--border-strong);background:var(--surface);box-shadow:var(--shadow);z-index:40}.context-item{display:inline-flex;align-items:center;gap:8px;width:100%;padding:10px 12px;border:0;border-radius:12px;background:transparent;color:var(--text);cursor:pointer}.context-item:hover{background:var(--surface-3)}@media(max-width:1160px){.page-header{grid-template-columns:1fr}.toolbar{justify-content:flex-start}}@media(max-width:920px){.report-shell{padding:16px}.section-head,.table-meta,.modal-head,.modal-foot{flex-direction:column;align-items:stretch}.section-actions{justify-content:stretch}.form-grid,.picklist,.sort-rule{grid-template-columns:1fr}.field-span{grid-column:auto}} diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/dist/report-app.js b/src/AkkornStudio.UI/Assets/ReportFrontend/dist/report-app.js new file mode 100644 index 00000000..9a565aa0 --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/dist/report-app.js @@ -0,0 +1,214 @@ +(function(){"use strict";/** +* @vue/shared v3.5.33 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function yn(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const ce={},Ft=[],We=()=>{},uo=()=>!1,Ps=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Rs=e=>e.startsWith("onUpdate:"),ve=Object.assign,gn=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},vl=Object.prototype.hasOwnProperty,ee=(e,t)=>vl.call(e,t),W=Array.isArray,jt=e=>ss(e)==="[object Map]",Kt=e=>ss(e)==="[object Set]",fo=e=>ss(e)==="[object Date]",z=e=>typeof e=="function",pe=e=>typeof e=="string",Be=e=>typeof e=="symbol",oe=e=>e!==null&&typeof e=="object",po=e=>(oe(e)||z(e))&&z(e.then)&&z(e.catch),ho=Object.prototype.toString,ss=e=>ho.call(e),_l=e=>ss(e).slice(8,-1),mo=e=>ss(e)==="[object Object]",bn=e=>pe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ns=yn(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Es=e=>{const t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},xl=/-\w/g,Le=Es(e=>e.replace(xl,t=>t.slice(1).toUpperCase())),Cl=/\B([A-Z])/g,wt=Es(e=>e.replace(Cl,"-$1").toLowerCase()),yo=Es(e=>e.charAt(0).toUpperCase()+e.slice(1)),vn=Es(e=>e?`on${yo(e)}`:""),st=(e,t)=>!Object.is(e,t),Ds=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},Os=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let bo;const $s=()=>bo||(bo=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function gt(e){if(W(e)){const t={};for(let s=0;s{if(s){const n=s.split(wl);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function Ne(e){let t="";if(pe(e))t=e;else if(W(e))for(let s=0;sbt(s,t))}const _o=e=>!!(e&&e.__v_isRef===!0),x=e=>pe(e)?e:e==null?"":W(e)||oe(e)&&(e.toString===ho||!z(e.toString))?_o(e)?x(e.value):JSON.stringify(e,xo,2):String(e),xo=(e,t)=>_o(t)?xo(e,t.value):jt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[n,o],i)=>(s[xn(n,i)+" =>"]=o,s),{})}:Kt(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>xn(s))}:Be(t)?xn(t):oe(t)&&!W(t)&&!mo(t)?String(t):t,xn=(e,t="")=>{var s;return Be(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};/** +* @vue/reactivity v3.5.33 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let _e;class Pl{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.__v_skip=!0,this.parent=_e,!t&&_e&&(this.index=(_e.scopes||(_e.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,s;if(this.scopes)for(t=0,s=this.scopes.length;t0&&--this._on===0){if(_e===this)_e=this.prevScope;else{let t=_e;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let s,n;for(s=0,n=this.effects.length;s0)return;if(is){let t=is;for(is=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;os;){let t=os;for(os=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function So(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Mo(e){let t,s=e.depsTail,n=s;for(;n;){const o=n.prevDep;n.version===-1?(n===s&&(s=o),Mn(n),El(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=o}e.deps=t,e.depsTail=s}function Sn(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Ao(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Ao(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===ls)||(e.globalVersion=ls,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Sn(e))))return;e.flags|=2;const t=e.dep,s=ue,n=He;ue=e,He=!0;try{So(e);const o=e.fn(e._value);(t.version===0||st(o,e._value))&&(e.flags|=128,e._value=o,t.version++)}catch(o){throw t.version++,o}finally{ue=s,He=n,Mo(e),e.flags&=-3}}function Mn(e,t=!1){const{dep:s,prevSub:n,nextSub:o}=e;if(n&&(n.nextSub=o,e.prevSub=void 0),o&&(o.prevSub=n,e.nextSub=void 0),s.subs===e&&(s.subs=n,!n&&s.computed)){s.computed.flags&=-5;for(let i=s.computed.deps;i;i=i.nextDep)Mn(i,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function El(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}let He=!0;const To=[];function ze(){To.push(He),He=!1}function qe(){const e=To.pop();He=e===void 0?!0:e}function Po(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=ue;ue=void 0;try{t()}finally{ue=s}}}let ls=0;class Dl{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Ro{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!ue||!He||ue===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==ue)s=this.activeLink=new Dl(ue,this),ue.deps?(s.prevDep=ue.depsTail,ue.depsTail.nextDep=s,ue.depsTail=s):ue.deps=ue.depsTail=s,Eo(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=ue.depsTail,s.nextDep=void 0,ue.depsTail.nextDep=s,ue.depsTail=s,ue.deps===s&&(ue.deps=n)}return s}trigger(t){this.version++,ls++,this.notify(t)}notify(t){kn();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{wn()}}}function Eo(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)Eo(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const An=new WeakMap,St=Symbol(""),Tn=Symbol(""),rs=Symbol("");function xe(e,t,s){if(He&&ue){let n=An.get(e);n||An.set(e,n=new Map);let o=n.get(s);o||(n.set(s,o=new Ro),o.map=n,o.key=s),o.track()}}function nt(e,t,s,n,o,i){const r=An.get(e);if(!r){ls++;return}const c=u=>{u&&u.trigger()};if(kn(),t==="clear")r.forEach(c);else{const u=W(e),v=u&&bn(s);if(u&&s==="length"){const y=Number(n);r.forEach((C,$)=>{($==="length"||$===rs||!Be($)&&$>=y)&&c(C)})}else switch((s!==void 0||r.has(void 0))&&c(r.get(s)),v&&c(r.get(rs)),t){case"add":u?v&&c(r.get("length")):(c(r.get(St)),jt(e)&&c(r.get(Tn)));break;case"delete":u||(c(r.get(St)),jt(e)&&c(r.get(Tn)));break;case"set":jt(e)&&c(r.get(St));break}}wn()}function Lt(e){const t=te(e);return t===e?t:(xe(t,"iterate",rs),Ue(e)?t:t.map(it))}function Is(e){return xe(e=te(e),"iterate",rs),e}function Je(e,t){return vt(e)?Nt(Mt(e)?it(t):t):it(t)}const Ol={__proto__:null,[Symbol.iterator](){return Pn(this,Symbol.iterator,e=>Je(this,e))},concat(...e){return Lt(this).concat(...e.map(t=>W(t)?Lt(t):t))},entries(){return Pn(this,"entries",e=>(e[1]=Je(this,e[1]),e))},every(e,t){return ot(this,"every",e,t,void 0,arguments)},filter(e,t){return ot(this,"filter",e,t,s=>s.map(n=>Je(this,n)),arguments)},find(e,t){return ot(this,"find",e,t,s=>Je(this,s),arguments)},findIndex(e,t){return ot(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return ot(this,"findLast",e,t,s=>Je(this,s),arguments)},findLastIndex(e,t){return ot(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return ot(this,"forEach",e,t,void 0,arguments)},includes(...e){return Rn(this,"includes",e)},indexOf(...e){return Rn(this,"indexOf",e)},join(e){return Lt(this).join(e)},lastIndexOf(...e){return Rn(this,"lastIndexOf",e)},map(e,t){return ot(this,"map",e,t,void 0,arguments)},pop(){return cs(this,"pop")},push(...e){return cs(this,"push",e)},reduce(e,...t){return Do(this,"reduce",e,t)},reduceRight(e,...t){return Do(this,"reduceRight",e,t)},shift(){return cs(this,"shift")},some(e,t){return ot(this,"some",e,t,void 0,arguments)},splice(...e){return cs(this,"splice",e)},toReversed(){return Lt(this).toReversed()},toSorted(e){return Lt(this).toSorted(e)},toSpliced(...e){return Lt(this).toSpliced(...e)},unshift(...e){return cs(this,"unshift",e)},values(){return Pn(this,"values",e=>Je(this,e))}};function Pn(e,t,s){const n=Is(e),o=n[t]();return n!==e&&!Ue(e)&&(o._next=o.next,o.next=()=>{const i=o._next();return i.done||(i.value=s(i.value)),i}),o}const $l=Array.prototype;function ot(e,t,s,n,o,i){const r=Is(e),c=r!==e&&!Ue(e),u=r[t];if(u!==$l[t]){const C=u.apply(e,i);return c?it(C):C}let v=s;r!==e&&(c?v=function(C,$){return s.call(this,Je(e,C),$,e)}:s.length>2&&(v=function(C,$){return s.call(this,C,$,e)}));const y=u.call(r,v,n);return c&&o?o(y):y}function Do(e,t,s,n){const o=Is(e),i=o!==e&&!Ue(e);let r=s,c=!1;o!==e&&(i?(c=n.length===0,r=function(v,y,C){return c&&(c=!1,v=Je(e,v)),s.call(this,v,Je(e,y),C,e)}):s.length>3&&(r=function(v,y,C){return s.call(this,v,y,C,e)}));const u=o[t](r,...n);return c?Je(e,u):u}function Rn(e,t,s){const n=te(e);xe(n,"iterate",rs);const o=n[t](...s);return(o===-1||o===!1)&&On(s[0])?(s[0]=te(s[0]),n[t](...s)):o}function cs(e,t,s=[]){ze(),kn();const n=te(e)[t].apply(e,s);return wn(),qe(),n}const Il=yn("__proto__,__v_isRef,__isVue"),Oo=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Be));function Vl(e){Be(e)||(e=String(e));const t=te(this);return xe(t,"has",e),t.hasOwnProperty(e)}class $o{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){if(s==="__v_skip")return t.__v_skip;const o=this._isReadonly,i=this._isShallow;if(s==="__v_isReactive")return!o;if(s==="__v_isReadonly")return o;if(s==="__v_isShallow")return i;if(s==="__v_raw")return n===(o?i?Lo:Ko:i?jo:Fo).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const r=W(t);if(!o){let u;if(r&&(u=Ol[s]))return u;if(s==="hasOwnProperty")return Vl}const c=Reflect.get(t,s,Ce(t)?t:n);if((Be(s)?Oo.has(s):Il(s))||(o||xe(t,"get",s),i))return c;if(Ce(c)){const u=r&&bn(s)?c:c.value;return o&&oe(u)?Dn(u):u}return oe(c)?o?Dn(c):as(c):c}}class Io extends $o{constructor(t=!1){super(!1,t)}set(t,s,n,o){let i=t[s];const r=W(t)&&bn(s);if(!this._isShallow){const v=vt(i);if(!Ue(n)&&!vt(n)&&(i=te(i),n=te(n)),!r&&Ce(i)&&!Ce(n))return v||(i.value=n),!0}const c=r?Number(s)e,Vs=e=>Reflect.getPrototypeOf(e);function Nl(e,t,s){return function(...n){const o=this.__v_raw,i=te(o),r=jt(i),c=e==="entries"||e===Symbol.iterator&&r,u=e==="keys"&&r,v=o[e](...n),y=s?En:t?Nt:it;return!t&&xe(i,"iterate",u?Tn:St),ve(Object.create(v),{next(){const{value:C,done:$}=v.next();return $?{value:C,done:$}:{value:c?[y(C[0]),y(C[1])]:y(C),done:$}}})}}function Fs(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Hl(e,t){const s={get(o){const i=this.__v_raw,r=te(i),c=te(o);e||(st(o,c)&&xe(r,"get",o),xe(r,"get",c));const{has:u}=Vs(r),v=t?En:e?Nt:it;if(u.call(r,o))return v(i.get(o));if(u.call(r,c))return v(i.get(c));i!==r&&i.get(o)},get size(){const o=this.__v_raw;return!e&&xe(te(o),"iterate",St),o.size},has(o){const i=this.__v_raw,r=te(i),c=te(o);return e||(st(o,c)&&xe(r,"has",o),xe(r,"has",c)),o===c?i.has(o):i.has(o)||i.has(c)},forEach(o,i){const r=this,c=r.__v_raw,u=te(c),v=t?En:e?Nt:it;return!e&&xe(u,"iterate",St),c.forEach((y,C)=>o.call(i,v(y),v(C),r))}};return ve(s,e?{add:Fs("add"),set:Fs("set"),delete:Fs("delete"),clear:Fs("clear")}:{add(o){const i=te(this),r=Vs(i),c=te(o),u=!t&&!Ue(o)&&!vt(o)?c:o;return r.has.call(i,u)||st(o,u)&&r.has.call(i,o)||st(c,u)&&r.has.call(i,c)||(i.add(u),nt(i,"add",u,u)),this},set(o,i){!t&&!Ue(i)&&!vt(i)&&(i=te(i));const r=te(this),{has:c,get:u}=Vs(r);let v=c.call(r,o);v||(o=te(o),v=c.call(r,o));const y=u.call(r,o);return r.set(o,i),v?st(i,y)&&nt(r,"set",o,i):nt(r,"add",o,i),this},delete(o){const i=te(this),{has:r,get:c}=Vs(i);let u=r.call(i,o);u||(o=te(o),u=r.call(i,o)),c&&c.call(i,o);const v=i.delete(o);return u&&nt(i,"delete",o,void 0),v},clear(){const o=te(this),i=o.size!==0,r=o.clear();return i&&nt(o,"clear",void 0,void 0),r}}),["keys","values","entries",Symbol.iterator].forEach(o=>{s[o]=Nl(o,e,t)}),s}function js(e,t){const s=Hl(e,t);return(n,o,i)=>o==="__v_isReactive"?!e:o==="__v_isReadonly"?e:o==="__v_raw"?n:Reflect.get(ee(s,o)&&o in n?s:n,o,i)}const Ul={get:js(!1,!1)},Wl={get:js(!1,!0)},Bl={get:js(!0,!1)},zl={get:js(!0,!0)},Fo=new WeakMap,jo=new WeakMap,Ko=new WeakMap,Lo=new WeakMap;function ql(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Jl(e){return e.__v_skip||!Object.isExtensible(e)?0:ql(_l(e))}function as(e){return vt(e)?e:Ks(e,!1,Fl,Ul,Fo)}function Gl(e){return Ks(e,!1,Kl,Wl,jo)}function Dn(e){return Ks(e,!0,jl,Bl,Ko)}function Pp(e){return Ks(e,!0,Ll,zl,Lo)}function Ks(e,t,s,n,o){if(!oe(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=Jl(e);if(i===0)return e;const r=o.get(e);if(r)return r;const c=new Proxy(e,i===2?n:s);return o.set(e,c),c}function Mt(e){return vt(e)?Mt(e.__v_raw):!!(e&&e.__v_isReactive)}function vt(e){return!!(e&&e.__v_isReadonly)}function Ue(e){return!!(e&&e.__v_isShallow)}function On(e){return e?!!e.__v_raw:!1}function te(e){const t=e&&e.__v_raw;return t?te(t):e}function Yl(e){return!ee(e,"__v_skip")&&Object.isExtensible(e)&&go(e,"__v_skip",!0),e}const it=e=>oe(e)?as(e):e,Nt=e=>oe(e)?Dn(e):e;function Ce(e){return e?e.__v_isRef===!0:!1}function l(e){return Ce(e)?e.value:e}const Xl={get:(e,t,s)=>t==="__v_raw"?e:l(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const o=e[t];return Ce(o)&&!Ce(s)?(o.value=s,!0):Reflect.set(e,t,s,n)}};function $n(e){return Mt(e)?e:new Proxy(e,Xl)}class Ql{constructor(t,s,n){this.fn=t,this.setter=s,this._value=void 0,this.dep=new Ro(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=ls-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&ue!==this)return wo(this,!0),!0}get value(){const t=this.dep.track();return Ao(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Zl(e,t,s=!1){let n,o;return z(e)?n=e:(n=e.get,o=e.set),new Ql(n,o,s)}const Ls={},Ns=new WeakMap;let At;function er(e,t=!1,s=At){if(s){let n=Ns.get(s);n||Ns.set(s,n=[]),n.push(e)}}function tr(e,t,s=ce){const{immediate:n,deep:o,once:i,scheduler:r,augmentJob:c,call:u}=s,v=B=>o?B:Ue(B)||o===!1||o===0?lt(B,1):lt(B);let y,C,$,a,b=!1,T=!1;if(Ce(e)?(C=()=>e.value,b=Ue(e)):Mt(e)?(C=()=>v(e),b=!0):W(e)?(T=!0,b=e.some(B=>Mt(B)||Ue(B)),C=()=>e.map(B=>{if(Ce(B))return B.value;if(Mt(B))return v(B);if(z(B))return u?u(B,2):B()})):z(e)?t?C=u?()=>u(e,2):e:C=()=>{if($){ze();try{$()}finally{qe()}}const B=At;At=y;try{return u?u(e,3,[a]):e(a)}finally{At=B}}:C=We,t&&o){const B=C,me=o===!0?1/0:o;C=()=>lt(B(),me)}const q=Rl(),X=()=>{y.stop(),q&&q.active&&gn(q.effects,y)};if(i&&t){const B=t;t=(...me)=>{B(...me),X()}}let G=T?new Array(e.length).fill(Ls):Ls;const ae=B=>{if(!(!(y.flags&1)||!y.dirty&&!B))if(t){const me=y.run();if(o||b||(T?me.some((Ze,Ve)=>st(Ze,G[Ve])):st(me,G))){$&&$();const Ze=At;At=y;try{const Ve=[me,G===Ls?void 0:T&&G[0]===Ls?[]:G,a];G=me,u?u(t,3,Ve):t(...Ve)}finally{At=Ze}}}else y.run()};return c&&c(ae),y=new Co(C),y.scheduler=r?()=>r(ae,!1):ae,a=B=>er(B,!1,y),$=y.onStop=()=>{const B=Ns.get(y);if(B){if(u)u(B,4);else for(const me of B)me();Ns.delete(y)}},t?n?ae(!0):G=y.run():r?r(ae.bind(null,!0),!0):y.run(),X.pause=y.pause.bind(y),X.resume=y.resume.bind(y),X.stop=X,X}function lt(e,t=1/0,s){if(t<=0||!oe(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,Ce(e))lt(e.value,t,s);else if(W(e))for(let n=0;n{lt(n,t,s)});else if(mo(e)){for(const n in e)lt(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&<(e[n],t,s)}return e}/** +* @vue/runtime-core v3.5.33 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/const us=[];let In=!1;function Rp(e,...t){if(In)return;In=!0,ze();const s=us.length?us[us.length-1].component:null,n=s&&s.appContext.config.warnHandler,o=sr();if(n)Ht(n,s,11,[e+t.map(i=>{var r,c;return(c=(r=i.toString)==null?void 0:r.call(i))!=null?c:JSON.stringify(i)}).join(""),s&&s.proxy,o.map(({vnode:i})=>`at <${$i(s,i.type)}>`).join(` +`),o]);else{const i=[`[Vue warn]: ${e}`,...t];o.length&&i.push(` +`,...nr(o)),console.warn(...i)}qe(),In=!1}function sr(){let e=us[us.length-1];if(!e)return[];const t=[];for(;e;){const s=t[0];s&&s.vnode===e?s.recurseCount++:t.push({vnode:e,recurseCount:0});const n=e.component&&e.component.parent;e=n&&n.vnode}return t}function nr(e){const t=[];return e.forEach((s,n)=>{t.push(...n===0?[]:[` +`],...or(s))}),t}function or({vnode:e,recurseCount:t}){const s=t>0?`... (${t} recursive calls)`:"",n=e.component?e.component.parent==null:!1,o=` at <${$i(e.component,e.type,n)}`,i=">"+s;return e.props?[o,...ir(e.props),i]:[o+i]}function ir(e){const t=[],s=Object.keys(e);return s.slice(0,3).forEach(n=>{t.push(...No(n,e[n]))}),s.length>3&&t.push(" ..."),t}function No(e,t,s){return pe(t)?(t=JSON.stringify(t),s?t:[`${e}=${t}`]):typeof t=="number"||typeof t=="boolean"||t==null?s?t:[`${e}=${t}`]:Ce(t)?(t=No(e,te(t.value),!0),s?t:[`${e}=Ref<`,t,">"]):z(t)?[`${e}=fn${t.name?`<${t.name}>`:""}`]:(t=te(t),s?t:[`${e}=`,t])}function Ht(e,t,s,n){try{return n?e(...n):e()}catch(o){Hs(o,t,s)}}function Ge(e,t,s,n){if(z(e)){const o=Ht(e,t,s,n);return o&&po(o)&&o.catch(i=>{Hs(i,t,s)}),o}if(W(e)){const o=[];for(let i=0;i>>1,o=ke[n],i=ds(o);i=ds(s)?ke.push(e):ke.splice(rr(t),0,e),e.flags|=1,Wo()}}function Wo(){Us||(Us=Ho.then(qo))}function cr(e){W(e)?Ut.push(...e):_t&&e.id===-1?_t.splice(Wt+1,0,e):e.flags&1||(Ut.push(e),e.flags|=1),Wo()}function Bo(e,t,s=Ye+1){for(;sds(s)-ds(n));if(Ut.length=0,_t){_t.push(...t);return}for(_t=t,Wt=0;Wt<_t.length;Wt++){const s=_t[Wt];s.flags&4&&(s.flags&=-2),s.flags&8||s(),s.flags&=-2}_t=null,Wt=0}}const ds=e=>e.id==null?e.flags&2?-1:1/0:e.id;function qo(e){try{for(Ye=0;Ye{n._d&&Xs(-1);const i=Ws(t);let r;try{r=e(...o)}finally{Ws(i),n._d&&Xs(1)}return r};return n._n=!0,n._c=!0,n._d=!0,n}function rt(e,t){if(De===null)return e;const s=tn(De),n=e.dirs||(e.dirs=[]);for(let o=0;o1)return s&&z(t)?t.call(n&&n.proxy):t}}const dr=Symbol.for("v-scx"),fr=()=>Bs(dr);function fs(e,t,s){return Go(e,t,s)}function Go(e,t,s=ce){const{immediate:n,deep:o,flush:i,once:r}=s,c=ve({},s),u=t&&n||!t&&i!=="post";let v;if(xs){if(i==="sync"){const a=fr();v=a.__watcherHandles||(a.__watcherHandles=[])}else if(!u){const a=()=>{};return a.stop=We,a.resume=We,a.pause=We,a}}const y=Se;c.call=(a,b,T)=>Ge(a,y,b,T);let C=!1;i==="post"?c.scheduler=a=>{Ae(a,y&&y.suspense)}:i!=="sync"&&(C=!0,c.scheduler=(a,b)=>{b?a():Vn(a)}),c.augmentJob=a=>{t&&(a.flags|=4),C&&(a.flags|=2,y&&(a.id=y.uid,a.i=y))};const $=tr(e,t,c);return xs&&(v?v.push($):u&&$()),$}function pr(e,t,s){const n=this.proxy,o=pe(e)?e.includes(".")?Yo(n,e):()=>n[e]:e.bind(n,n);let i;z(t)?i=t:(i=t.handler,s=t);const r=_s(this),c=Go(o,i.bind(n),s);return r(),c}function Yo(e,t){const s=t.split(".");return()=>{let n=e;for(let o=0;oe.__isTeleport,yr=Symbol("_leaveCb");function Fn(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Fn(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Xo(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function Qo(e,t){let s;return!!((s=Object.getOwnPropertyDescriptor(e,t))&&!s.configurable)}const zs=new WeakMap;function ps(e,t,s,n,o=!1){if(W(e)){e.forEach((T,q)=>ps(T,t&&(W(t)?t[q]:t),s,n,o));return}if(hs(n)&&!o){n.shapeFlag&512&&n.type.__asyncResolved&&n.component.subTree.component&&ps(e,t,s,n.component.subTree);return}const i=n.shapeFlag&4?tn(n.component):n.el,r=o?null:i,{i:c,r:u}=e,v=t&&t.r,y=c.refs===ce?c.refs={}:c.refs,C=c.setupState,$=te(C),a=C===ce?uo:T=>Qo(y,T)?!1:ee($,T),b=(T,q)=>!(q&&Qo(y,q));if(v!=null&&v!==u){if(Zo(t),pe(v))y[v]=null,a(v)&&(C[v]=null);else if(Ce(v)){const T=t;b(v,T.k)&&(v.value=null),T.k&&(y[T.k]=null)}}if(z(u))Ht(u,c,12,[r,y]);else{const T=pe(u),q=Ce(u);if(T||q){const X=()=>{if(e.f){const G=T?a(u)?C[u]:y[u]:b()||!e.k?u.value:y[e.k];if(o)W(G)&&gn(G,i);else if(W(G))G.includes(i)||G.push(i);else if(T)y[u]=[i],a(u)&&(C[u]=y[u]);else{const ae=[i];b(u,e.k)&&(u.value=ae),e.k&&(y[e.k]=ae)}}else T?(y[u]=r,a(u)&&(C[u]=r)):q&&(b(u,e.k)&&(u.value=r),e.k&&(y[e.k]=r))};if(r){const G=()=>{X(),zs.delete(e)};G.id=-1,zs.set(e,G),Ae(G,s)}else Zo(e),X()}}}function Zo(e){const t=zs.get(e);t&&(t.flags|=8,zs.delete(e))}$s().requestIdleCallback,$s().cancelIdleCallback;const hs=e=>!!e.type.__asyncLoader,ei=e=>e.type.__isKeepAlive;function gr(e,t){ti(e,"a",t)}function br(e,t){ti(e,"da",t)}function ti(e,t,s=Se){const n=e.__wdc||(e.__wdc=()=>{let o=s;for(;o;){if(o.isDeactivated)return;o=o.parent}return e()});if(qs(t,n,s),s){let o=s.parent;for(;o&&o.parent;)ei(o.parent.vnode)&&vr(n,t,s,o),o=o.parent}}function vr(e,t,s,n){const o=qs(t,e,n,!0);si(()=>{gn(n[t],o)},s)}function qs(e,t,s=Se,n=!1){if(s){const o=s[e]||(s[e]=[]),i=t.__weh||(t.__weh=(...r)=>{ze();const c=_s(s),u=Ge(t,s,e,r);return c(),qe(),u});return n?o.unshift(i):o.push(i),i}}const ct=e=>(t,s=Se)=>{(!xs||e==="sp")&&qs(e,(...n)=>t(...n),s)},_r=ct("bm"),xr=ct("m"),Cr=ct("bu"),kr=ct("u"),wr=ct("bum"),si=ct("um"),Sr=ct("sp"),Mr=ct("rtg"),Ar=ct("rtc");function Tr(e,t=Se){qs("ec",e,t)}const Pr=Symbol.for("v-ndc");function ge(e,t,s,n){let o;const i=s,r=W(e);if(r||pe(e)){const c=r&&Mt(e);let u=!1,v=!1;c&&(u=!Ue(e),v=vt(e),e=Is(e)),o=new Array(e.length);for(let y=0,C=e.length;yt(c,u,void 0,i));else{const c=Object.keys(e);o=new Array(c.length);for(let u=0,v=c.length;ue?Ei(e)?tn(e):jn(e.parent):null,ms=ve(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>jn(e.parent),$root:e=>jn(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>li(e),$forceUpdate:e=>e.f||(e.f=()=>{Vn(e.update)}),$nextTick:e=>e.n||(e.n=Uo.bind(e.proxy)),$watch:e=>pr.bind(e)}),Kn=(e,t)=>e!==ce&&!e.__isScriptSetup&&ee(e,t),Rr={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:o,props:i,accessCache:r,type:c,appContext:u}=e;if(t[0]!=="$"){const $=r[t];if($!==void 0)switch($){case 1:return n[t];case 2:return o[t];case 4:return s[t];case 3:return i[t]}else{if(Kn(n,t))return r[t]=1,n[t];if(o!==ce&&ee(o,t))return r[t]=2,o[t];if(ee(i,t))return r[t]=3,i[t];if(s!==ce&&ee(s,t))return r[t]=4,s[t];Ln&&(r[t]=0)}}const v=ms[t];let y,C;if(v)return t==="$attrs"&&xe(e.attrs,"get",""),v(e);if((y=c.__cssModules)&&(y=y[t]))return y;if(s!==ce&&ee(s,t))return r[t]=4,s[t];if(C=u.config.globalProperties,ee(C,t))return C[t]},set({_:e},t,s){const{data:n,setupState:o,ctx:i}=e;return Kn(o,t)?(o[t]=s,!0):n!==ce&&ee(n,t)?(n[t]=s,!0):ee(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:o,props:i,type:r}},c){let u;return!!(s[c]||e!==ce&&c[0]!=="$"&&ee(e,c)||Kn(t,c)||ee(i,c)||ee(n,c)||ee(ms,c)||ee(o.config.globalProperties,c)||(u=r.__cssModules)&&u[c])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:ee(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}};function ni(e){return W(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}let Ln=!0;function Er(e){const t=li(e),s=e.proxy,n=e.ctx;Ln=!1,t.beforeCreate&&oi(t.beforeCreate,e,"bc");const{data:o,computed:i,methods:r,watch:c,provide:u,inject:v,created:y,beforeMount:C,mounted:$,beforeUpdate:a,updated:b,activated:T,deactivated:q,beforeDestroy:X,beforeUnmount:G,destroyed:ae,unmounted:B,render:me,renderTracked:Ze,renderTriggered:Ve,errorCaptured:et,serverPrefetch:Xt,expose:ht,inheritAttrs:Ot,components:Qt,directives:Zt,filters:Ms}=t;if(v&&Dr(v,n,null),r)for(const fe in r){const se=r[fe];z(se)&&(n[fe]=se.bind(s))}if(o){const fe=o.call(s,s);oe(fe)&&(e.data=as(fe))}if(Ln=!0,i)for(const fe in i){const se=i[fe],mt=z(se)?se.bind(s,s):z(se.get)?se.get.bind(s,s):We,es=!z(se)&&z(se.set)?se.set.bind(s):We,yt=qt({get:mt,set:es});Object.defineProperty(n,fe,{enumerable:!0,configurable:!0,get:()=>yt.value,set:Ee=>yt.value=Ee})}if(c)for(const fe in c)ii(c[fe],n,s,fe);if(u){const fe=z(u)?u.call(s):u;Reflect.ownKeys(fe).forEach(se=>{ur(se,fe[se])})}y&&oi(y,e,"c");function be(fe,se){W(se)?se.forEach(mt=>fe(mt.bind(s))):se&&fe(se.bind(s))}if(be(_r,C),be(xr,$),be(Cr,a),be(kr,b),be(gr,T),be(br,q),be(Tr,et),be(Ar,Ze),be(Mr,Ve),be(wr,G),be(si,B),be(Sr,Xt),W(ht))if(ht.length){const fe=e.exposed||(e.exposed={});ht.forEach(se=>{Object.defineProperty(fe,se,{get:()=>s[se],set:mt=>s[se]=mt,enumerable:!0})})}else e.exposed||(e.exposed={});me&&e.render===We&&(e.render=me),Ot!=null&&(e.inheritAttrs=Ot),Qt&&(e.components=Qt),Zt&&(e.directives=Zt),Xt&&Xo(e)}function Dr(e,t,s=We){W(e)&&(e=Nn(e));for(const n in e){const o=e[n];let i;oe(o)?"default"in o?i=Bs(o.from||n,o.default,!0):i=Bs(o.from||n):i=Bs(o),Ce(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:r=>i.value=r}):t[n]=i}}function oi(e,t,s){Ge(W(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function ii(e,t,s,n){let o=n.includes(".")?Yo(s,n):()=>s[n];if(pe(e)){const i=t[e];z(i)&&fs(o,i)}else if(z(e))fs(o,e.bind(s));else if(oe(e))if(W(e))e.forEach(i=>ii(i,t,s,n));else{const i=z(e.handler)?e.handler.bind(s):t[e.handler];z(i)&&fs(o,i,e)}}function li(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:o,optionsCache:i,config:{optionMergeStrategies:r}}=e.appContext,c=i.get(t);let u;return c?u=c:!o.length&&!s&&!n?u=t:(u={},o.length&&o.forEach(v=>Js(u,v,r,!0)),Js(u,t,r)),oe(t)&&i.set(t,u),u}function Js(e,t,s,n=!1){const{mixins:o,extends:i}=t;i&&Js(e,i,s,!0),o&&o.forEach(r=>Js(e,r,s,!0));for(const r in t)if(!(n&&r==="expose")){const c=Or[r]||s&&s[r];e[r]=c?c(e[r],t[r]):t[r]}return e}const Or={data:ri,props:ci,emits:ci,methods:ys,computed:ys,beforeCreate:we,created:we,beforeMount:we,mounted:we,beforeUpdate:we,updated:we,beforeDestroy:we,beforeUnmount:we,destroyed:we,unmounted:we,activated:we,deactivated:we,errorCaptured:we,serverPrefetch:we,components:ys,directives:ys,watch:Ir,provide:ri,inject:$r};function ri(e,t){return t?e?function(){return ve(z(e)?e.call(this,this):e,z(t)?t.call(this,this):t)}:t:e}function $r(e,t){return ys(Nn(e),Nn(t))}function Nn(e){if(W(e)){const t={};for(let s=0;st==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Le(t)}Modifiers`]||e[`${wt(t)}Modifiers`];function Kr(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||ce;let o=s;const i=t.startsWith("update:"),r=i&&jr(n,t.slice(7));r&&(r.trim&&(o=s.map(y=>pe(y)?y.trim():y)),r.number&&(o=s.map(Os)));let c,u=n[c=vn(t)]||n[c=vn(Le(t))];!u&&i&&(u=n[c=vn(wt(t))]),u&&Ge(u,e,6,o);const v=n[c+"Once"];if(v){if(!e.emitted)e.emitted={};else if(e.emitted[c])return;e.emitted[c]=!0,Ge(v,e,6,o)}}const Lr=new WeakMap;function ui(e,t,s=!1){const n=s?Lr:t.emitsCache,o=n.get(e);if(o!==void 0)return o;const i=e.emits;let r={},c=!1;if(!z(e)){const u=v=>{const y=ui(v,t,!0);y&&(c=!0,ve(r,y))};!s&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}return!i&&!c?(oe(e)&&n.set(e,null),null):(W(i)?i.forEach(u=>r[u]=null):ve(r,i),oe(e)&&n.set(e,r),r)}function Gs(e,t){return!e||!Ps(t)?!1:(t=t.slice(2).replace(/Once$/,""),ee(e,t[0].toLowerCase()+t.slice(1))||ee(e,wt(t))||ee(e,t))}function Ep(){}function di(e){const{type:t,vnode:s,proxy:n,withProxy:o,propsOptions:[i],slots:r,attrs:c,emit:u,render:v,renderCache:y,props:C,data:$,setupState:a,ctx:b,inheritAttrs:T}=e,q=Ws(e);let X,G;try{if(s.shapeFlag&4){const B=o||n,me=B;X=Xe(v.call(me,B,y,C,a,$,b)),G=c}else{const B=t;X=Xe(B.length>1?B(C,{attrs:c,slots:r,emit:u}):B(C,null)),G=t.props?c:Nr(c)}}catch(B){gs.length=0,Hs(B,e,1),X=O(xt)}let ae=X;if(G&&T!==!1){const B=Object.keys(G),{shapeFlag:me}=ae;B.length&&me&7&&(i&&B.some(Rs)&&(G=Hr(G,i)),ae=zt(ae,G,!1,!0))}return s.dirs&&(ae=zt(ae,null,!1,!0),ae.dirs=ae.dirs?ae.dirs.concat(s.dirs):s.dirs),s.transition&&Fn(ae,s.transition),X=ae,Ws(q),X}const Nr=e=>{let t;for(const s in e)(s==="class"||s==="style"||Ps(s))&&((t||(t={}))[s]=e[s]);return t},Hr=(e,t)=>{const s={};for(const n in e)(!Rs(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function Ur(e,t,s){const{props:n,children:o,component:i}=e,{props:r,children:c,patchFlag:u}=t,v=i.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&u>=0){if(u&1024)return!0;if(u&16)return n?fi(n,r,v):!!r;if(u&8){const y=t.dynamicProps;for(let C=0;CObject.create(hi),yi=e=>Object.getPrototypeOf(e)===hi;function Br(e,t,s,n=!1){const o={},i=mi();e.propsDefaults=Object.create(null),gi(e,t,o,i);for(const r in e.propsOptions[0])r in o||(o[r]=void 0);s?e.props=n?o:Gl(o):e.type.props?e.props=o:e.props=i,e.attrs=i}function zr(e,t,s,n){const{props:o,attrs:i,vnode:{patchFlag:r}}=e,c=te(o),[u]=e.propsOptions;let v=!1;if((n||r>0)&&!(r&16)){if(r&8){const y=e.vnode.dynamicProps;for(let C=0;C{u=!0;const[$,a]=bi(C,t,!0);ve(r,$),a&&c.push(...a)};!s&&t.mixins.length&&t.mixins.forEach(y),e.extends&&y(e.extends),e.mixins&&e.mixins.forEach(y)}if(!i&&!u)return oe(e)&&n.set(e,Ft),Ft;if(W(i))for(let y=0;ye==="_"||e==="_ctx"||e==="$stable",Wn=e=>W(e)?e.map(Xe):[Xe(e)],Jr=(e,t,s)=>{if(t._n)return t;const n=ar((...o)=>Wn(t(...o)),s);return n._c=!1,n},_i=(e,t,s)=>{const n=e._ctx;for(const o in e){if(Un(o))continue;const i=e[o];if(z(i))t[o]=Jr(o,i,n);else if(i!=null){const r=Wn(i);t[o]=()=>r}}},xi=(e,t)=>{const s=Wn(t);e.slots.default=()=>s},Ci=(e,t,s)=>{for(const n in t)(s||!Un(n))&&(e[n]=t[n])},Gr=(e,t,s)=>{const n=e.slots=mi();if(e.vnode.shapeFlag&32){const o=t._;o?(Ci(n,t,s),s&&go(n,"_",o,!0)):_i(t,n)}else t&&xi(e,t)},Yr=(e,t,s)=>{const{vnode:n,slots:o}=e;let i=!0,r=ce;if(n.shapeFlag&32){const c=t._;c?s&&c===1?i=!1:Ci(o,t,s):(i=!t.$stable,_i(t,o)),r=t}else t&&(xi(e,t),r={default:1});if(i)for(const c in o)!Un(c)&&r[c]==null&&delete o[c]},Ae=tc;function Xr(e){return Qr(e)}function Qr(e,t){const s=$s();s.__VUE__=!0;const{insert:n,remove:o,patchProp:i,createElement:r,createText:c,createComment:u,setText:v,setElementText:y,parentNode:C,nextSibling:$,setScopeId:a=We,insertStaticContent:b}=e,T=(f,m,_,A=null,k=null,w=null,D=void 0,E=null,P=!!m.dynamicChildren)=>{if(f===m)return;f&&!vs(f,m)&&(A=ts(f),Ee(f,k,w,!0),f=null),m.patchFlag===-2&&(P=!1,m.dynamicChildren=null);const{type:S,ref:H,shapeFlag:V}=m;switch(S){case Ys:q(f,m,_,A);break;case xt:X(f,m,_,A);break;case zn:f==null&&G(m,_,A,D);break;case ie:Qt(f,m,_,A,k,w,D,E,P);break;default:V&1?me(f,m,_,A,k,w,D,E,P):V&6?Zt(f,m,_,A,k,w,D,E,P):(V&64||V&128)&&S.process(f,m,_,A,k,w,D,E,P,It)}H!=null&&k?ps(H,f&&f.ref,w,m||f,!m):H==null&&f&&f.ref!=null&&ps(f.ref,null,w,f,!0)},q=(f,m,_,A)=>{if(f==null)n(m.el=c(m.children),_,A);else{const k=m.el=f.el;m.children!==f.children&&v(k,m.children)}},X=(f,m,_,A)=>{f==null?n(m.el=u(m.children||""),_,A):m.el=f.el},G=(f,m,_,A)=>{[f.el,f.anchor]=b(f.children,m,_,A,f.el,f.anchor)},ae=({el:f,anchor:m},_,A)=>{let k;for(;f&&f!==m;)k=$(f),n(f,_,A),f=k;n(m,_,A)},B=({el:f,anchor:m})=>{let _;for(;f&&f!==m;)_=$(f),o(f),f=_;o(m)},me=(f,m,_,A,k,w,D,E,P)=>{if(m.type==="svg"?D="svg":m.type==="math"&&(D="mathml"),f==null)Ze(m,_,A,k,w,D,E,P);else{const S=f.el&&f.el._isVueCE?f.el:null;try{S&&S._beginPatch(),Xt(f,m,k,w,D,E,P)}finally{S&&S._endPatch()}}},Ze=(f,m,_,A,k,w,D,E)=>{let P,S;const{props:H,shapeFlag:V,transition:N,dirs:U}=f;if(P=f.el=r(f.type,w,H&&H.is,H),V&8?y(P,f.children):V&16&&et(f.children,P,null,A,k,Bn(f,w),D,E),U&&Tt(f,null,A,"created"),Ve(P,f,f.scopeId,D,A),H){for(const Z in H)Z!=="value"&&!ns(Z)&&i(P,Z,null,H[Z],w,A);"value"in H&&i(P,"value",null,H.value,w),(S=H.onVnodeBeforeMount)&&Qe(S,A,f)}U&&Tt(f,null,A,"beforeMount");const Y=Zr(k,N);Y&&N.beforeEnter(P),n(P,m,_),((S=H&&H.onVnodeMounted)||Y||U)&&Ae(()=>{try{S&&Qe(S,A,f),Y&&N.enter(P),U&&Tt(f,null,A,"mounted")}finally{}},k)},Ve=(f,m,_,A,k)=>{if(_&&a(f,_),A)for(let w=0;w{for(let S=P;S{const E=m.el=f.el;let{patchFlag:P,dynamicChildren:S,dirs:H}=m;P|=f.patchFlag&16;const V=f.props||ce,N=m.props||ce;let U;if(_&&Pt(_,!1),(U=N.onVnodeBeforeUpdate)&&Qe(U,_,m,f),H&&Tt(m,f,_,"beforeUpdate"),_&&Pt(_,!0),(V.innerHTML&&N.innerHTML==null||V.textContent&&N.textContent==null)&&y(E,""),S?ht(f.dynamicChildren,S,E,_,A,Bn(m,k),w):D||se(f,m,E,null,_,A,Bn(m,k),w,!1),P>0){if(P&16)Ot(E,V,N,_,k);else if(P&2&&V.class!==N.class&&i(E,"class",null,N.class,k),P&4&&i(E,"style",V.style,N.style,k),P&8){const Y=m.dynamicProps;for(let Z=0;Z{U&&Qe(U,_,m,f),H&&Tt(m,f,_,"updated")},A)},ht=(f,m,_,A,k,w,D)=>{for(let E=0;E{if(m!==_){if(m!==ce)for(const w in m)!ns(w)&&!(w in _)&&i(f,w,m[w],null,k,A);for(const w in _){if(ns(w))continue;const D=_[w],E=m[w];D!==E&&w!=="value"&&i(f,w,E,D,k,A)}"value"in _&&i(f,"value",m.value,_.value,k)}},Qt=(f,m,_,A,k,w,D,E,P)=>{const S=m.el=f?f.el:c(""),H=m.anchor=f?f.anchor:c("");let{patchFlag:V,dynamicChildren:N,slotScopeIds:U}=m;U&&(E=E?E.concat(U):U),f==null?(n(S,_,A),n(H,_,A),et(m.children||[],_,H,k,w,D,E,P)):V>0&&V&64&&N&&f.dynamicChildren&&f.dynamicChildren.length===N.length?(ht(f.dynamicChildren,N,_,k,w,D,E),(m.key!=null||k&&m===k.subTree)&&ki(f,m,!0)):se(f,m,_,H,k,w,D,E,P)},Zt=(f,m,_,A,k,w,D,E,P)=>{m.slotScopeIds=E,f==null?m.shapeFlag&512?k.ctx.activate(m,_,A,D,P):Ms(m,_,A,k,w,D,P):un(f,m,P)},Ms=(f,m,_,A,k,w,D)=>{const E=f.component=ac(f,A,k);if(ei(f)&&(E.ctx.renderer=It),dc(E,!1,D),E.asyncDep){if(k&&k.registerDep(E,be,D),!f.el){const P=E.subTree=O(xt);X(null,P,m,_),f.placeholder=P.el}}else be(E,f,m,_,k,w,D)},un=(f,m,_)=>{const A=m.component=f.component;if(Ur(f,m,_))if(A.asyncDep&&!A.asyncResolved){fe(A,m,_);return}else A.next=m,A.update();else m.el=f.el,A.vnode=m},be=(f,m,_,A,k,w,D)=>{const E=()=>{if(f.isMounted){let{next:V,bu:N,u:U,parent:Y,vnode:Z}=f;{const je=wi(f);if(je){V&&(V.el=Z.el,fe(f,V,D)),je.asyncDep.then(()=>{Ae(()=>{f.isUnmounted||S()},k)});return}}let re=V,he;Pt(f,!1),V?(V.el=Z.el,fe(f,V,D)):V=Z,N&&Ds(N),(he=V.props&&V.props.onVnodeBeforeUpdate)&&Qe(he,Y,V,Z),Pt(f,!0);const ye=di(f),Fe=f.subTree;f.subTree=ye,T(Fe,ye,C(Fe.el),ts(Fe),f,k,w),V.el=ye.el,re===null&&Wr(f,ye.el),U&&Ae(U,k),(he=V.props&&V.props.onVnodeUpdated)&&Ae(()=>Qe(he,Y,V,Z),k)}else{let V;const{el:N,props:U}=m,{bm:Y,m:Z,parent:re,root:he,type:ye}=f,Fe=hs(m);Pt(f,!1),Y&&Ds(Y),!Fe&&(V=U&&U.onVnodeBeforeMount)&&Qe(V,re,m),Pt(f,!0);{he.ce&&he.ce._hasShadowRoot()&&he.ce._injectChildStyle(ye,f.parent?f.parent.type:void 0);const je=f.subTree=di(f);T(null,je,_,A,f,k,w),m.el=je.el}if(Z&&Ae(Z,k),!Fe&&(V=U&&U.onVnodeMounted)){const je=m;Ae(()=>Qe(V,re,je),k)}(m.shapeFlag&256||re&&hs(re.vnode)&&re.vnode.shapeFlag&256)&&f.a&&Ae(f.a,k),f.isMounted=!0,m=_=A=null}};f.scope.on();const P=f.effect=new Co(E);f.scope.off();const S=f.update=P.run.bind(P),H=f.job=P.runIfDirty.bind(P);H.i=f,H.id=f.uid,P.scheduler=()=>Vn(H),Pt(f,!0),S()},fe=(f,m,_)=>{m.component=f;const A=f.vnode.props;f.vnode=m,f.next=null,zr(f,m.props,A,_),Yr(f,m.children,_),ze(),Bo(f),qe()},se=(f,m,_,A,k,w,D,E,P=!1)=>{const S=f&&f.children,H=f?f.shapeFlag:0,V=m.children,{patchFlag:N,shapeFlag:U}=m;if(N>0){if(N&128){es(S,V,_,A,k,w,D,E,P);return}else if(N&256){mt(S,V,_,A,k,w,D,E,P);return}}U&8?(H&16&&$t(S,k,w),V!==S&&y(_,V)):H&16?U&16?es(S,V,_,A,k,w,D,E,P):$t(S,k,w,!0):(H&8&&y(_,""),U&16&&et(V,_,A,k,w,D,E,P))},mt=(f,m,_,A,k,w,D,E,P)=>{f=f||Ft,m=m||Ft;const S=f.length,H=m.length,V=Math.min(S,H);let N;for(N=0;NH?$t(f,k,w,!0,!1,V):et(m,_,A,k,w,D,E,P,V)},es=(f,m,_,A,k,w,D,E,P)=>{let S=0;const H=m.length;let V=f.length-1,N=H-1;for(;S<=V&&S<=N;){const U=f[S],Y=m[S]=P?ut(m[S]):Xe(m[S]);if(vs(U,Y))T(U,Y,_,null,k,w,D,E,P);else break;S++}for(;S<=V&&S<=N;){const U=f[V],Y=m[N]=P?ut(m[N]):Xe(m[N]);if(vs(U,Y))T(U,Y,_,null,k,w,D,E,P);else break;V--,N--}if(S>V){if(S<=N){const U=N+1,Y=UN)for(;S<=V;)Ee(f[S],k,w,!0),S++;else{const U=S,Y=S,Z=new Map;for(S=Y;S<=N;S++){const Me=m[S]=P?ut(m[S]):Xe(m[S]);Me.key!=null&&Z.set(Me.key,S)}let re,he=0;const ye=N-Y+1;let Fe=!1,je=0;const Vt=new Array(ye);for(S=0;S=ye){Ee(Me,k,w,!0);continue}let Ke;if(Me.key!=null)Ke=Z.get(Me.key);else for(re=Y;re<=N;re++)if(Vt[re-Y]===0&&vs(Me,m[re])){Ke=re;break}Ke===void 0?Ee(Me,k,w,!0):(Vt[Ke-Y]=S+1,Ke>=je?je=Ke:Fe=!0,T(Me,m[Ke],_,null,k,w,D,E,P),he++)}const pn=Fe?ec(Vt):Ft;for(re=pn.length-1,S=ye-1;S>=0;S--){const Me=Y+S,Ke=m[Me],hn=m[Me+1],mn=Me+1{const{el:w,type:D,transition:E,children:P,shapeFlag:S}=f;if(S&6){yt(f.component.subTree,m,_,A);return}if(S&128){f.suspense.move(m,_,A);return}if(S&64){D.move(f,m,_,It);return}if(D===ie){n(w,m,_);for(let V=0;VE.enter(w),k);else{const{leave:V,delayLeave:N,afterLeave:U}=E,Y=()=>{f.ctx.isUnmounted?o(w):n(w,m,_)},Z=()=>{w._isLeaving&&w[yr](!0),V(w,()=>{Y(),U&&U()})};N?N(w,Y,Z):Z()}else n(w,m,_)},Ee=(f,m,_,A=!1,k=!1)=>{const{type:w,props:D,ref:E,children:P,dynamicChildren:S,shapeFlag:H,patchFlag:V,dirs:N,cacheIndex:U,memo:Y}=f;if(V===-2&&(k=!1),E!=null&&(ze(),ps(E,null,_,f,!0),qe()),U!=null&&(m.renderCache[U]=void 0),H&256){m.ctx.deactivate(f);return}const Z=H&1&&N,re=!hs(f);let he;if(re&&(he=D&&D.onVnodeBeforeUnmount)&&Qe(he,m,f),H&6)ro(f.component,_,A);else{if(H&128){f.suspense.unmount(_,A);return}Z&&Tt(f,null,m,"beforeUnmount"),H&64?f.type.remove(f,m,_,It,A):S&&!S.hasOnce&&(w!==ie||V>0&&V&64)?$t(S,m,_,!1,!0):(w===ie&&V&384||!k&&H&16)&&$t(P,m,_),A&&dn(f)}const ye=Y!=null&&U==null;(re&&(he=D&&D.onVnodeUnmounted)||Z||ye)&&Ae(()=>{he&&Qe(he,m,f),Z&&Tt(f,null,m,"unmounted"),ye&&(f.el=null)},_)},dn=f=>{const{type:m,el:_,anchor:A,transition:k}=f;if(m===ie){lo(_,A);return}if(m===zn){B(f);return}const w=()=>{o(_),k&&!k.persisted&&k.afterLeave&&k.afterLeave()};if(f.shapeFlag&1&&k&&!k.persisted){const{leave:D,delayLeave:E}=k,P=()=>D(_,w);E?E(f.el,w,P):P()}else w()},lo=(f,m)=>{let _;for(;f!==m;)_=$(f),o(f),f=_;o(m)},ro=(f,m,_)=>{const{bum:A,scope:k,job:w,subTree:D,um:E,m:P,a:S}=f;Si(P),Si(S),A&&Ds(A),k.stop(),w&&(w.flags|=8,Ee(D,f,m,_)),E&&Ae(E,m),Ae(()=>{f.isUnmounted=!0},m)},$t=(f,m,_,A=!1,k=!1,w=0)=>{for(let D=w;D{if(f.shapeFlag&6)return ts(f.component.subTree);if(f.shapeFlag&128)return f.suspense.next();const m=$(f.anchor||f.el),_=m&&m[hr];return _?$(_):m};let As=!1;const fn=(f,m,_)=>{let A;f==null?m._vnode&&(Ee(m._vnode,null,null,!0),A=m._vnode.component):T(m._vnode||null,f,m,null,null,null,_),m._vnode=f,As||(As=!0,Bo(A),zo(),As=!1)},It={p:T,um:Ee,m:yt,r:dn,mt:Ms,mc:et,pc:se,pbc:ht,n:ts,o:e};return{render:fn,hydrate:void 0,createApp:Fr(fn)}}function Bn({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function Pt({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Zr(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function ki(e,t,s=!1){const n=e.children,o=t.children;if(W(n)&&W(o))for(let i=0;i>1,e[s[c]]0&&(t[n]=s[i-1]),s[i]=n)}}for(i=s.length,r=s[i-1];i-- >0;)s[i]=r,r=t[r];return s}function wi(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:wi(t)}function Si(e){if(e)for(let t=0;te.__isSuspense;function tc(e,t){t&&t.pendingBranch?W(e)?t.effects.push(...e):t.effects.push(e):cr(e)}const ie=Symbol.for("v-fgt"),Ys=Symbol.for("v-txt"),xt=Symbol.for("v-cmt"),zn=Symbol.for("v-stc"),gs=[];let Re=null;function I(e=!1){gs.push(Re=e?null:[])}function sc(){gs.pop(),Re=gs[gs.length-1]||null}let bs=1;function Xs(e,t=!1){bs+=e,e<0&&Re&&t&&(Re.hasOnce=!0)}function Ti(e){return e.dynamicChildren=bs>0?Re||Ft:null,sc(),bs>0&&Re&&Re.push(e),e}function F(e,t,s,n,o,i){return Ti(d(e,t,s,n,o,i,!0))}function nc(e,t,s,n,o){return Ti(O(e,t,s,n,o,!0))}function Qs(e){return e?e.__v_isVNode===!0:!1}function vs(e,t){return e.type===t.type&&e.key===t.key}const Pi=({key:e})=>e??null,Zs=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?pe(e)||Ce(e)||z(e)?{i:De,r:e,k:t,f:!!s}:e:null);function d(e,t=null,s=null,n=0,o=null,i=e===ie?0:1,r=!1,c=!1){const u={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Pi(t),ref:t&&Zs(t),scopeId:Jo,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:n,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:De};return c?(qn(u,s),i&128&&e.normalize(u)):s&&(u.shapeFlag|=pe(s)?8:16),bs>0&&!r&&Re&&(u.patchFlag>0||i&6)&&u.patchFlag!==32&&Re.push(u),u}const O=oc;function oc(e,t=null,s=null,n=0,o=null,i=!1){if((!e||e===Pr)&&(e=xt),Qs(e)){const c=zt(e,t,!0);return s&&qn(c,s),bs>0&&!i&&Re&&(c.shapeFlag&6?Re[Re.indexOf(e)]=c:Re.push(c)),c.patchFlag=-2,c}if(bc(e)&&(e=e.__vccOpts),t){t=ic(t);let{class:c,style:u}=t;c&&!pe(c)&&(t.class=Ne(c)),oe(u)&&(On(u)&&!W(u)&&(u=ve({},u)),t.style=gt(u))}const r=pe(e)?1:Ai(e)?128:mr(e)?64:oe(e)?4:z(e)?2:0;return d(e,t,s,n,o,r,i,!0)}function ic(e){return e?On(e)||yi(e)?ve({},e):e:null}function zt(e,t,s=!1,n=!1){const{props:o,ref:i,patchFlag:r,children:c,transition:u}=e,v=t?lc(o||{},t):o,y={__v_isVNode:!0,__v_skip:!0,type:e.type,props:v,key:v&&Pi(v),ref:t&&t.ref?s&&i?W(i)?i.concat(Zs(t)):[i,Zs(t)]:Zs(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:c,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ie?r===-1?16:r|16:r,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:u,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&zt(e.ssContent),ssFallback:e.ssFallback&&zt(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return u&&n&&Fn(y,u.clone(y)),y}function at(e=" ",t=0){return O(Ys,null,e,t)}function le(e="",t=!1){return t?(I(),nc(xt,null,e)):O(xt,null,e)}function Xe(e){return e==null||typeof e=="boolean"?O(xt):W(e)?O(ie,null,e.slice()):Qs(e)?ut(e):O(Ys,null,String(e))}function ut(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:zt(e)}function qn(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(W(t))s=16;else if(typeof t=="object")if(n&65){const o=t.default;o&&(o._c&&(o._d=!1),qn(e,o()),o._c&&(o._d=!0));return}else{s=32;const o=t._;!o&&!yi(t)?t._ctx=De:o===3&&De&&(De.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else z(t)?(t={default:t,_ctx:De},s=32):(t=String(t),n&64?(s=16,t=[at(t)]):s=8);e.children=t,e.shapeFlag|=s}function lc(...e){const t={};for(let s=0;sSe||De;let en,Jn;{const e=$s(),t=(s,n)=>{let o;return(o=e[s])||(o=e[s]=[]),o.push(n),i=>{o.length>1?o.forEach(r=>r(i)):o[0](i)}};en=t("__VUE_INSTANCE_SETTERS__",s=>Se=s),Jn=t("__VUE_SSR_SETTERS__",s=>xs=s)}const _s=e=>{const t=Se;return en(e),e.scope.on(),()=>{e.scope.off(),en(t)}},Ri=()=>{Se&&Se.scope.off(),en(null)};function Ei(e){return e.vnode.shapeFlag&4}let xs=!1;function dc(e,t=!1,s=!1){t&&Jn(t);const{props:n,children:o}=e.vnode,i=Ei(e);Br(e,n,i,t),Gr(e,o,s||t);const r=i?fc(e,t):void 0;return t&&Jn(!1),r}function fc(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Rr);const{setup:n}=s;if(n){ze();const o=e.setupContext=n.length>1?hc(e):null,i=_s(e),r=Ht(n,e,0,[e.props,o]),c=po(r);if(qe(),i(),(c||e.sp)&&!hs(e)&&Xo(e),c){if(r.then(Ri,Ri),t)return r.then(u=>{Di(e,u)}).catch(u=>{Hs(u,e,0)});e.asyncDep=r}else Di(e,r)}else Oi(e)}function Di(e,t,s){z(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:oe(t)&&(e.setupState=$n(t)),Oi(e)}function Oi(e,t,s){const n=e.type;e.render||(e.render=n.render||We);{const o=_s(e);ze();try{Er(e)}finally{qe(),o()}}}const pc={get(e,t){return xe(e,"get",""),e[t]}};function hc(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,pc),slots:e.slots,emit:e.emit,expose:t}}function tn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy($n(Yl(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in ms)return ms[s](e)},has(t,s){return s in t||s in ms}})):e.proxy}const mc=/(?:^|[-_])\w/g,yc=e=>e.replace(mc,t=>t.toUpperCase()).replace(/[-_]/g,"");function gc(e,t=!0){return z(e)?e.displayName||e.name:e.name||t&&e.__name}function $i(e,t,s=!1){let n=gc(t);if(!n&&t.__file){const o=t.__file.match(/([^/\\]+)\.\w+$/);o&&(n=o[1])}if(!n&&e){const o=i=>{for(const r in i)if(i[r]===t)return r};n=o(e.components)||e.parent&&o(e.parent.type.components)||o(e.appContext.components)}return n?yc(n):s?"App":"Anonymous"}function bc(e){return z(e)&&"__vccOpts"in e}const qt=(e,t)=>Zl(e,t,xs);function Cs(e,t,s){try{Xs(-1);const n=arguments.length;return n===2?oe(t)&&!W(t)?Qs(t)?O(e,null,[t]):O(e,t):O(e,null,t):(n>3?s=Array.prototype.slice.call(arguments,2):n===3&&Qs(s)&&(s=[s]),O(e,t,s))}finally{Xs(1)}}const vc="3.5.33";/** +* @vue/runtime-dom v3.5.33 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Gn;const Ii=typeof window<"u"&&window.trustedTypes;if(Ii)try{Gn=Ii.createPolicy("vue",{createHTML:e=>e})}catch{}const Vi=Gn?e=>Gn.createHTML(e):e=>e,_c="http://www.w3.org/2000/svg",xc="http://www.w3.org/1998/Math/MathML",dt=typeof document<"u"?document:null,Fi=dt&&dt.createElement("template"),Cc={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const o=t==="svg"?dt.createElementNS(_c,e):t==="mathml"?dt.createElementNS(xc,e):s?dt.createElement(e,{is:s}):dt.createElement(e);return e==="select"&&n&&n.multiple!=null&&o.setAttribute("multiple",n.multiple),o},createText:e=>dt.createTextNode(e),createComment:e=>dt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>dt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,o,i){const r=s?s.previousSibling:t.lastChild;if(o&&(o===i||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),s),!(o===i||!(o=o.nextSibling)););else{Fi.innerHTML=Vi(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const c=Fi.content;if(n==="svg"||n==="mathml"){const u=c.firstChild;for(;u.firstChild;)c.appendChild(u.firstChild);c.removeChild(u)}t.insertBefore(c,s)}return[r?r.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},kc=Symbol("_vtc");function wc(e,t,s){const n=e[kc];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const ji=Symbol("_vod"),Sc=Symbol("_vsh"),Mc=Symbol(""),Ac=/(?:^|;)\s*display\s*:/;function Tc(e,t,s){const n=e.style,o=pe(s);let i=!1;if(s&&!o){if(t)if(pe(t))for(const r of t.split(";")){const c=r.slice(0,r.indexOf(":")).trim();s[c]==null&&ks(n,c,"")}else for(const r in t)s[r]==null&&ks(n,r,"");for(const r in s){r==="display"&&(i=!0);const c=s[r];c!=null?Rc(e,r,!pe(t)&&t?t[r]:void 0,c)||ks(n,r,c):ks(n,r,"")}}else if(o){if(t!==s){const r=n[Mc];r&&(s+=";"+r),n.cssText=s,i=Ac.test(s)}}else t&&e.removeAttribute("style");ji in e&&(e[ji]=i?n.display:"",e[Sc]&&(n.display="none"))}const Ki=/\s*!important$/;function ks(e,t,s){if(W(s))s.forEach(n=>ks(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=Pc(e,t);Ki.test(s)?e.setProperty(wt(n),s.replace(Ki,""),"important"):e[n]=s}}const Li=["Webkit","Moz","ms"],Yn={};function Pc(e,t){const s=Yn[t];if(s)return s;let n=Le(t);if(n!=="filter"&&n in e)return Yn[t]=n;n=yo(n);for(let o=0;oXn||($c.then(()=>Xn=0),Xn=Date.now());function Vc(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;Ge(Fc(n,s.value),t,5,[n])};return s.value=e,s.attached=Ic(),s}function Fc(e,t){if(W(t)){const s=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{s.call(e),e._stopped=!0},t.map(n=>o=>!o._stopped&&n&&n(o))}else return t}const zi=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,jc=(e,t,s,n,o,i)=>{const r=o==="svg";t==="class"?wc(e,n,r):t==="style"?Tc(e,s,n):Ps(t)?Rs(t)||Dc(e,t,s,n,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Kc(e,t,n,r))?(Ui(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Hi(e,t,n,r,i,t!=="value")):e._isVueCE&&(Lc(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!pe(n)))?Ui(e,Le(t),n,i,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Hi(e,t,n,r))};function Kc(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&zi(t)&&z(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const o=e.tagName;if(o==="IMG"||o==="VIDEO"||o==="CANVAS"||o==="SOURCE")return!1}return zi(t)&&pe(s)?!1:t in e}function Lc(e,t){const s=e._def.props;if(!s)return!1;const n=Le(t);return Array.isArray(s)?s.some(o=>Le(o)===n):Object.keys(s).some(o=>Le(o)===n)}const Ct=e=>{const t=e.props["onUpdate:modelValue"]||!1;return W(t)?s=>Ds(t,s):t};function Nc(e){e.target.composing=!0}function qi(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Oe=Symbol("_assign");function Ji(e,t,s){return t&&(e=e.trim()),s&&(e=Os(e)),e}const sn={created(e,{modifiers:{lazy:t,trim:s,number:n}},o){e[Oe]=Ct(o);const i=n||o.props&&o.props.type==="number";ft(e,t?"change":"input",r=>{r.target.composing||e[Oe](Ji(e.value,s,i))}),(s||i)&&ft(e,"change",()=>{e.value=Ji(e.value,s,i)}),t||(ft(e,"compositionstart",Nc),ft(e,"compositionend",qi),ft(e,"change",qi))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:n,trim:o,number:i}},r){if(e[Oe]=Ct(r),e.composing)return;const c=(i||e.type==="number")&&!/^0\d/.test(e.value)?Os(e.value):e.value,u=t??"";if(c===u)return;const v=e.getRootNode();(v instanceof Document||v instanceof ShadowRoot)&&v.activeElement===e&&e.type!=="range"&&(n&&t===s||o&&e.value.trim()===u)||(e.value=u)}},Hc={deep:!0,created(e,t,s){e[Oe]=Ct(s),ft(e,"change",()=>{const n=e._modelValue,o=Jt(e),i=e.checked,r=e[Oe];if(W(n)){const c=_n(n,o),u=c!==-1;if(i&&!u)r(n.concat(o));else if(!i&&u){const v=[...n];v.splice(c,1),r(v)}}else if(Kt(n)){const c=new Set(n);i?c.add(o):c.delete(o),r(c)}else r(Xi(e,i))})},mounted:Gi,beforeUpdate(e,t,s){e[Oe]=Ct(s),Gi(e,t,s)}};function Gi(e,{value:t,oldValue:s},n){e._modelValue=t;let o;if(W(t))o=_n(t,n.props.value)>-1;else if(Kt(t))o=t.has(n.props.value);else{if(t===s)return;o=bt(t,Xi(e,!0))}e.checked!==o&&(e.checked=o)}const Uc={created(e,{value:t},s){e.checked=bt(t,s.props.value),e[Oe]=Ct(s),ft(e,"change",()=>{e[Oe](Jt(e))})},beforeUpdate(e,{value:t,oldValue:s},n){e[Oe]=Ct(n),t!==s&&(e.checked=bt(t,n.props.value))}},ws={deep:!0,created(e,{value:t,modifiers:{number:s}},n){const o=Kt(t);ft(e,"change",()=>{const i=Array.prototype.filter.call(e.options,r=>r.selected).map(r=>s?Os(Jt(r)):Jt(r));e[Oe](e.multiple?o?new Set(i):i:i[0]),e._assigning=!0,Uo(()=>{e._assigning=!1})}),e[Oe]=Ct(n)},mounted(e,{value:t}){Yi(e,t)},beforeUpdate(e,t,s){e[Oe]=Ct(s)},updated(e,{value:t}){e._assigning||Yi(e,t)}};function Yi(e,t){const s=e.multiple,n=W(t);if(!(s&&!n&&!Kt(t))){for(let o=0,i=e.options.length;oString(v)===String(c)):r.selected=_n(t,c)>-1}else r.selected=t.has(c);else if(bt(Jt(r),t)){e.selectedIndex!==o&&(e.selectedIndex=o);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Jt(e){return"_value"in e?e._value:e.value}function Xi(e,t){const s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}const Qn={created(e,t,s){nn(e,t,s,null,"created")},mounted(e,t,s){nn(e,t,s,null,"mounted")},beforeUpdate(e,t,s,n){nn(e,t,s,n,"beforeUpdate")},updated(e,t,s,n){nn(e,t,s,n,"updated")}};function Wc(e,t){switch(e){case"SELECT":return ws;case"TEXTAREA":return sn;default:switch(t){case"checkbox":return Hc;case"radio":return Uc;default:return sn}}}function nn(e,t,s,n,o){const r=Wc(e.tagName,s.props&&s.props.type)[o];r&&r(e,t,s,n)}const Bc=["ctrl","shift","alt","meta"],zc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Bc.some(s=>e[`${s}Key`]&&!t.includes(s))},kt=(e,t)=>{if(!e)return e;const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=((o,...i)=>{for(let r=0;r{const t=Jc().createApp(...e),{mount:s}=t;return t.mount=n=>{const o=Xc(n);if(!o)return;const i=t._component;!z(i)&&!i.render&&!i.template&&(i.template=o.innerHTML),o.nodeType===1&&(o.textContent="");const r=s(o,!1,Yc(o));return o instanceof Element&&(o.removeAttribute("v-cloak"),o.setAttribute("data-v-app","")),r},t});function Yc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Xc(e){return pe(e)?document.querySelector(e):e}/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Zi=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Qc=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,s,n)=>n?n.toUpperCase():s.toLowerCase()),Zc=e=>{const t=Qc(e);return t.charAt(0).toUpperCase()+t.slice(1)},ea=(...e)=>e.filter((t,s,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===s).join(" ").trim();/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var on={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"};/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ta=({size:e,strokeWidth:t=2,absoluteStrokeWidth:s,color:n,iconNode:o,name:i,class:r,...c},{slots:u})=>Cs("svg",{...on,width:e||on.width,height:e||on.height,stroke:n||on.stroke,"stroke-width":s?Number(t)*24/Number(e):t,class:ea("lucide",...i?[`lucide-${Zi(Zc(i))}-icon`,`lucide-${Zi(i)}`]:["lucide-icon"]),...c},[...o.map(v=>Cs(...v)),...u.default?[u.default()]:[]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const J=(e,t)=>(s,{slots:n})=>Cs(ta,{...s,iconNode:t,name:e},n);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const sa=J("arrow-down-a-z",[["path",{d:"m3 16 4 4 4-4",key:"1co6wj"}],["path",{d:"M7 20V4",key:"1yoxec"}],["path",{d:"M20 8h-5",key:"1vsyxs"}],["path",{d:"M15 10V6.5a2.5 2.5 0 0 1 5 0V10",key:"ag13bf"}],["path",{d:"M15 14h5l-5 6h5",key:"ur5jdg"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const na=J("arrow-down-wide-narrow",[["path",{d:"m3 16 4 4 4-4",key:"1co6wj"}],["path",{d:"M7 20V4",key:"1yoxec"}],["path",{d:"M11 4h10",key:"1w87gc"}],["path",{d:"M11 8h7",key:"djye34"}],["path",{d:"M11 12h4",key:"q8tih4"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const oa=J("arrow-up-a-z",[["path",{d:"m3 8 4-4 4 4",key:"11wl7u"}],["path",{d:"M7 4v16",key:"1glfcx"}],["path",{d:"M20 8h-5",key:"1vsyxs"}],["path",{d:"M15 10V6.5a2.5 2.5 0 0 1 5 0V10",key:"ag13bf"}],["path",{d:"M15 14h5l-5 6h5",key:"ur5jdg"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const el=J("arrow-up-down",[["path",{d:"m21 16-4 4-4-4",key:"f6ql7i"}],["path",{d:"M17 20V4",key:"1ejh1v"}],["path",{d:"m3 8 4-4 4 4",key:"11wl7u"}],["path",{d:"M7 4v16",key:"1glfcx"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ia=J("check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const la=J("chevron-down",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ra=J("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ca=J("chevron-up",[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const aa=J("chevron-right",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const tl=J("circle-dot",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"1",key:"41hilf"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ua=J("clipboard-list",[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1",key:"tgr4d6"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2",key:"116196"}],["path",{d:"M12 11h4",key:"1jrz19"}],["path",{d:"M12 16h4",key:"n85exb"}],["path",{d:"M8 11h.01",key:"1dfujw"}],["path",{d:"M8 16h.01",key:"18s6g9"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const da=J("clock-3",[["path",{d:"M12 6v6h4",key:"135r8i"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fa=J("code-xml",[["path",{d:"m18 16 4-4-4-4",key:"1inbqp"}],["path",{d:"m6 8-4 4 4 4",key:"15zrgr"}],["path",{d:"m14.5 4-5 16",key:"e7oirm"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const pa=J("columns-3",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M9 3v18",key:"fh3hqa"}],["path",{d:"M15 3v18",key:"14nvp0"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ha=J("database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ma=J("download",[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ya=J("expand",[["path",{d:"m15 15 6 6",key:"1s409w"}],["path",{d:"m15 9 6-6",key:"ko1vev"}],["path",{d:"M21 16v5h-5",key:"1ck2sf"}],["path",{d:"M21 8V3h-5",key:"1qoq8a"}],["path",{d:"M3 16v5h5",key:"1t08am"}],["path",{d:"m3 21 6-6",key:"wwnumi"}],["path",{d:"M3 8V3h5",key:"1ln10m"}],["path",{d:"M9 9 3 3",key:"v551iv"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const sl=J("file-json-2",[["path",{d:"M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4",key:"1pf5j1"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M4 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1",key:"fq0c9t"}],["path",{d:"M8 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1",key:"4gibmv"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ga=J("funnel",[["path",{d:"M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z",key:"sc7q7i"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ba=J("grip-vertical",[["circle",{cx:"9",cy:"12",r:"1",key:"1vctgf"}],["circle",{cx:"9",cy:"5",r:"1",key:"hp0tcf"}],["circle",{cx:"9",cy:"19",r:"1",key:"fkjjf6"}],["circle",{cx:"15",cy:"12",r:"1",key:"1tmaij"}],["circle",{cx:"15",cy:"5",r:"1",key:"19l28e"}],["circle",{cx:"15",cy:"19",r:"1",key:"f4zoj3"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const va=J("list-filter",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M7 12h10",key:"b7w52i"}],["path",{d:"M10 18h4",key:"1ulq68"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const _a=J("moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xa=J("pin",[["path",{d:"M12 17v5",key:"bb1du9"}],["path",{d:"M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z",key:"1nkz8b"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ca=J("plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ka=J("rotate-ccw",[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const wa=J("rows-3",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M21 9H3",key:"1338ky"}],["path",{d:"M21 15H3",key:"9uk58r"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Sa=J("save",[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z",key:"1c8476"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7",key:"1ydtos"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7",key:"t51u73"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ma=J("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Aa=J("sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ta=J("text-cursor-input",[["path",{d:"M12 20h-1a2 2 0 0 1-2-2 2 2 0 0 1-2 2H6",key:"1528k5"}],["path",{d:"M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7",key:"13ksps"}],["path",{d:"M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1",key:"1n9rhb"}],["path",{d:"M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1",key:"1mj8rg"}],["path",{d:"M9 6v12",key:"velyjx"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Pa=J("trash-2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ra=J("triangle-alert",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3",key:"wmoenq"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ea=J("upload",[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]]);/** + * @license lucide-vue-next v0.525.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Da=J("x",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]),Oa={moon:_a,sun:Aa,clipboard:ua,code:fa,download:ma,json:sl,filter:va,sort:na,sortAsc:oa,sortDesc:sa,sortAppend:el,columns:pa,rotate:ka,chevronDown:la,plus:Ca,x:Da,check:ia,text:Ta,funnel:ga,arrowUpDown:el,left:ra,right:aa,up:ca,expand:ya,rows:wa,clock:da,database:ha,alert:Ra,rowSelect:tl,search:Ma,resize:ba,pin:xa,save:Sa,trash:Pa,uploadPreset:Ea,downloadPreset:sl},K={name:"IconGlyph",props:{name:{type:String,required:!0},title:{type:String,default:""},size:{type:Number,default:16},strokeWidth:{type:Number,default:1.9}},render(){const e=Oa[this.name]??tl;return Cs("span",{class:"icon-glyph",title:this.title,"aria-hidden":"true"},[Cs(e,{size:this.size,strokeWidth:this.strokeWidth})])}},nl="akkorn-report-view-vue-v7",ol=[10,25,50,100],Rt="-",Zn=140;function $a(){const e=window.__AKKORN_REPORT__??{},t=e.labels??{},s=e.meta??{},n=(p,h)=>t[p]??t[h]??p,o=qt(()=>Ia(e,n)),i=as(Va(o.value)),r=as({activeModal:"",modalDatasetId:"",longCellValue:"",toast:"",toastTimer:0,resizing:null,contextMenu:null,scrollHints:{}}),c=new Map,u=qt(()=>{const p={};for(const h of o.value)p[h.id]=Na(h,i.datasets[h.id]);return p}),v=qt(()=>Ya(s,e,n)),y=qt(()=>i.theme==="dark"?"moon":"sun"),C=qt(()=>`${s.title??"Report"} • Vue 3 • v${e.version??"1.0"}`);fs(()=>i,()=>ja(i),{deep:!0}),fs(()=>i.theme,p=>{document.documentElement.setAttribute("data-theme",p)},{immediate:!0});function $(){return o.value.find(p=>p.id===r.modalDatasetId)??null}function a(){return r.modalDatasetId?i.datasets[r.modalDatasetId]:null}function b(p,h=""){r.activeModal=p,r.modalDatasetId=h}function T(){r.activeModal="",r.modalDatasetId=""}function q(){r.contextMenu=null}function X(){i.theme=i.theme==="dark"?"light":"dark"}function G(p){i.sections[p]=!i.sections[p]}function ae(p){const h=o.value.find(g=>g.id===p);h&&(i.datasets[p]=il(h,{}))}function B(p){i.datasets[p].filters=[],i.datasets[p].page=1}function me(p,h){i.datasets[p].filters.splice(h,1),i.datasets[p].page=1}function Ze(p){const h=o.value.find(g=>g.id===p);h&&(i.datasets[p].pendingFilter=eo(h,i.datasets[p].pendingFilter),b("filter",p))}function Ve(p){b("sort",p)}function et(p){b("columns",p)}function Xt(){const p=$(),h=a();if(!p||!h||!h.pendingFilter.key)return;h.filters.push({...h.pendingFilter}),h.page=1;const g=h.pendingFilter.key;h.pendingFilter=eo(p,{key:g})}function ht(){const p=a();p&&(p.page=1,T())}function Ot(){const p=a();p&&(p.sorts=[],pt(p),p.page=1)}function Qt(p,h){const g=i.datasets[p],M=g.sorts[0];(M==null?void 0:M.key)===h?g.sorts=[{key:h,dir:M.dir==="asc"?"desc":"asc"},...g.sorts.slice(1)]:g.sorts=[{key:h,dir:"asc"},...g.sorts.filter(R=>R.key!==h)],pt(g),g.page=1}function Zt(){var M,R;const p=$(),h=a();if(!p||!h)return;const g=((M=p.columns.find(L=>!h.sorts.some(j=>j.key===L.key)))==null?void 0:M.key)??((R=p.columns[0])==null?void 0:R.key)??"";g&&(h.sorts.push({key:g,dir:"asc"}),pt(h))}function Ms(p){const h=a();h&&(h.sorts.splice(p,1),pt(h))}function un(p,h){const g=a();!g||!g.sorts[p]||(g.sorts[p].key=h,g.sorts=ln(g.sorts),pt(g))}function be(p,h){const g=a();!g||!g.sorts[p]||(g.sorts[p].dir=h==="desc"?"desc":"asc",pt(g))}function fe(p,h,g=null){const M=i.datasets[p].selectedRows,R=i.datasets[p],L=u.value[p];if(g!=null&&g.shiftKey&&R.lastRowSelection&&L){const ne=L.rows.map(Pe=>Pe.rowId),de=ne.indexOf(R.lastRowSelection),Q=ne.indexOf(h);if(de>=0&&Q>=0){const[Pe,tt]=de=0?M.splice(j,1):M.push(h),R.lastRowSelection=h}function se(p,h,g){i.datasets[p].selectedCell={rowId:h,key:g},i.datasets[p].selectedCells=[`${h}::${g}`],i.datasets[p].anchorCell={rowId:h,key:g},q()}function mt(p,h,g,M=null){const R=i.datasets[p],L=u.value[p];if(M!=null&&M.shiftKey&&R.anchorCell&&L){const j=L.rows.map(Ts=>Ts.rowId),ne=L.visibleColumns.map(Ts=>Ts.key),de=j.indexOf(R.anchorCell.rowId),Q=j.indexOf(h),Pe=ne.indexOf(R.anchorCell.key),tt=ne.indexOf(g);if(de>=0&&Q>=0&&Pe>=0&&tt>=0){const[Ts,Sp]=de0)return M.selectedCells.includes(`${h}::${g}`);const R=M.selectedCell;return!!R&&R.rowId===h&&R.key===g}function yt(p,h){return i.datasets[p].selectedRows.includes(h)}function Ee(p,h){const g=u.value[p];if(!g)return;const M=Math.min(Math.max(1,Ss(h,1)),g.pageCount),R=i.datasets[p];if(R.page===M&&!R.pageBusy)return;c.has(p)&&window.clearTimeout(c.get(p)),R.pageBusy=!0;const L=window.setTimeout(()=>{R.page=M,window.requestAnimationFrame(()=>{R.pageBusy=!1,c.delete(p)})},0);c.set(p,L)}function dn(p,h){Ee(h,p.target.value)}function lo(p){r.longCellValue=Ie(p),q(),b("cell")}function ro(p){return $e(p).length>96}function $t(){Yt(e.sql??"",n("SQL copiado","SQL copied"),r)}function ts(){const p=[s.title??"",s.description??"",`${n("Linhas","Rows")}: ${(e.rows??[]).length}`,`${n("Colunas","Columns")}: ${(e.schema??[]).length}`,`${n("Gerado em","Generated at")}: ${s.generatedAt??""}`].filter(Boolean);Yt(p.join(` +`),n("Resumo copiado","Summary copied"),r)}function As(){const p=u.value.results;if(!p)return;const h=p.visibleColumns.map(ne=>ne.label),g=p.rows.map(ne=>p.visibleColumns.map(de=>hl(Te(ne.raw,de.key)))),M=[h.map(hl).join(","),...g.map(ne=>ne.join(","))].join(`\r +`),R=new Blob([M],{type:"text/csv;charset=utf-8;"}),L=URL.createObjectURL(R),j=document.createElement("a");j.href=L,j.download=`${(s.title??"report").replace(/[^\w\-]+/g,"_")}.csv`,j.click(),URL.revokeObjectURL(L)}function fn(){const p=`${yl(s.title??"report")||"report"}.json`,h=new Blob([JSON.stringify(e,null,2)],{type:"application/json;charset=utf-8;"});ml(h,p)}function It(p,h,g,M=!1){const R=i.datasets[p].pickSelection[h];if(!M){R.splice(0,R.length,g);return}const L=R.indexOf(g);L>=0?R.splice(L,1):R.push(g)}function gl(p,h){const g=i.datasets[p],M=u.value[p];if(M){if(h==="add"){for(const R of g.pickSelection.available)g.visibleKeys.includes(R)||g.visibleKeys.push(R);g.pickSelection.available=[];return}h==="remove"&&(g.visibleKeys=g.visibleKeys.filter(R=>!g.pickSelection.visible.includes(R)),g.visibleKeys.length===0&&(g.visibleKeys=M.dataset.columns.slice(0,1).map(R=>R.key)),g.pickSelection.visible=[])}}function f(p,h){const g=i.datasets[p],M=g.pickSelection.visible[0];if(!M)return;const R=g.visibleKeys.indexOf(M);if(R<0)return;const L=h==="up"?R-1:R+1;if(L<0||L>=g.visibleKeys.length)return;const j=[...g.visibleKeys];[j[R],j[L]]=[j[L],j[R]],g.visibleKeys=j}function m(p,h){const g=o.value.find(M=>M.id===p);if(g){if(h==="all"){i.datasets[p].visibleKeys=g.columns.map(M=>M.key);return}i.datasets[p].visibleKeys=g.columns.slice(0,1).map(M=>M.key)}}function _(p){var h;return((h=u.value[p])==null?void 0:h.shownColumns)??[]}function A(p){var h,g;return((g=(h=i.datasets[p])==null?void 0:h.selectedRows)==null?void 0:g.length)??0}function k(p){var L;const h=i.datasets[p],g=h==null?void 0:h.selectedCell,M=u.value[p];if(!g||!M)return;if(((L=h.selectedCells)==null?void 0:L.length)>1){const j=[...new Set(h.selectedCells.map(Q=>Q.split("::")[0]))],ne=M.visibleColumns.map(Q=>Q.key).filter(Q=>h.selectedCells.some(Pe=>Pe.endsWith(`::${Q}`))),de=j.map(Q=>{const Pe=M.rows.find(tt=>tt.rowId===Q);return ne.map(tt=>Ie(Te(Pe==null?void 0:Pe.raw,tt))).join(" ")});Yt(de.join(` +`),n("Selecao copiada","Selection copied"),r);return}const R=M.rows.find(j=>j.rowId===g.rowId);R&&Yt(Ie(Te(R.raw,g.key)),n("Celula copiada","Cell copied"),r)}function w(p){var j;const h=((j=i.datasets[p])==null?void 0:j.selectedRows)??[],g=u.value[p];if(!g||h.length===0)return;const M=g.rows.filter(ne=>h.includes(ne.rowId));if(M.length===0)return;const R=g.visibleColumns.map(ne=>ne.label),L=M.map(ne=>g.visibleColumns.map(de=>Ie(Te(ne.raw,de.key))).join(" "));Yt([R.join(" "),...L].join(` +`),n("Linhas copiadas","Rows copied"),r)}function D(p,h,g,M){p.preventDefault(),se(h,g,M);const R=u.value[h],L=R==null?void 0:R.rows.find(ne=>ne.rowId===g),j=L?Te(L.raw,M):"";r.contextMenu={type:"cell",datasetId:h,rowId:g,key:M,value:j,x:p.clientX,y:p.clientY}}function E(p,h,g){p.preventDefault(),q(),r.contextMenu={type:"header",datasetId:h,key:g,x:p.clientX,y:p.clientY}}function P(){const p=r.contextMenu;p&&(i.datasets[p.datasetId].filters.push({key:p.key,operator:"eq",value:Ie(p.value)===Rt?"":$e(p.value)}),i.datasets[p.datasetId].page=1,q())}function S(){const p=r.contextMenu;if(!p)return;const h=i.datasets[p.datasetId];h.visibleKeys=h.visibleKeys.filter(g=>g!==p.key),h.visibleKeys.length===0&&(h.visibleKeys=[p.key]),q()}function H(){const p=r.contextMenu;p&&(i.datasets[p.datasetId].visibleKeys=[p.key],q())}function V(){const p=r.contextMenu;p&&(Yt(Ie(p.value),n("Celula copiada","Cell copied"),r),q())}function N(){const p=r.contextMenu;if(!p)return;const h=i.datasets[p.datasetId];h.pinnedColumnKey=h.pinnedColumnKey===p.key?"":p.key,q()}function U(p,h){var g;return((g=i.datasets[p])==null?void 0:g.pinnedColumnKey)===h}function Y(p,h="primary"){const g=r.contextMenu;if(!g)return;const M=i.datasets[g.datasetId],R=p==="desc"?"desc":"asc",L={key:g.key,dir:R};M.sorts=ln(h==="secondary"?[...M.sorts.filter(j=>j.key!==g.key),L]:[L,...M.sorts.filter(j=>j.key!==g.key)]),pt(M),M.page=1,q()}function Z(p,h){var M,R;const g=(R=(M=i.datasets[p])==null?void 0:M.columnWidths)==null?void 0:R[h];return rn(g)}function re(p,h){return{width:`${Z(p,h)}px`,minWidth:`${Zn}px`}}function he(p,h){return{"sticky-main-col":U(p,h)}}function ye(p,h){return U(p,h)?{left:"28px"}:{}}function Fe(p,h,g){if(p.button!==0)return;const M=Z(h,g);r.resizing={id:h,key:g,startX:p.clientX,startWidth:M};const R=j=>{if(!r.resizing||r.resizing.id!==h||r.resizing.key!==g)return;const ne=r.resizing.startWidth+(j.clientX-r.resizing.startX);i.datasets[h].columnWidths[g]=rn(ne)},L=()=>{window.removeEventListener("mousemove",R),window.removeEventListener("mouseup",L),r.resizing=null};window.addEventListener("mousemove",R),window.addEventListener("mouseup",L),p.preventDefault(),p.stopPropagation()}function je(p,h){const g=u.value[p];if(!g)return;const M=g.visibleColumns.find(j=>j.key===h);if(!M)return;const R=[M.label,...g.rows.slice(0,120).map(j=>Ie(Te(j.raw,h)))],L=Math.max(...R.map(j=>Ha(j,M.kind)));i.datasets[p].columnWidths[h]=rn(L,dl(M.kind))}function Vt(p){const h=i.datasets[p],g=(window.prompt(n("Nome do preset","Preset name"),h.activeCustomPreset||n("Meu preset","My preset"))??"").trim();if(!g)return;const M={name:g,visibleKeys:[...h.visibleKeys],sort:{...h.sort},sorts:h.sorts.map(R=>({...R})),size:h.size,columnWidths:{...h.columnWidths},pinnedColumnKey:h.pinnedColumnKey||"",filters:[...h.filters]};h.customPresets=[...h.customPresets.filter(R=>R.name!==g),M],h.activeCustomPreset=g}function pn(p,h){const g=i.datasets[p],M=o.value.find(j=>j.id===p),R=g.customPresets.find(j=>j.name===h);if(!R||!M)return;const L=so(M,R);g.visibleKeys=[...L.visibleKeys],g.sorts=[...L.sorts],pt(g),g.size=L.size,g.columnWidths={...g.columnWidths,...L.columnWidths},g.pinnedColumnKey=L.pinnedColumnKey||"",g.filters=[...L.filters],g.page=1,g.activeCustomPreset=L.name}function Me(p,h){const g=i.datasets[p];g.customPresets=g.customPresets.filter(M=>M.name!==h),g.activeCustomPreset===h&&(g.activeCustomPreset="")}function Ke(p){const h=i.datasets[p];if(!h||h.customPresets.length===0)return;const g=new Blob([JSON.stringify(h.customPresets,null,2)],{type:"application/json;charset=utf-8;"});ml(g,`${yl(`${s.title??"report"}-${p}-presets`)||"report-presets"}.json`)}function hn(p){const h=document.createElement("input");h.type="file",h.accept="application/json,.json",h.onchange=async()=>{var M;const g=(M=h.files)==null?void 0:M[0];if(g)try{const R=await g.text(),L=JSON.parse(R);if(!Array.isArray(L))throw new Error("invalid");const j=o.value.find(Q=>Q.id===p);if(!j)return;const ne=L.filter(Q=>Q&&typeof Q.name=="string").map(Q=>so(j,Q)),de=i.datasets[p];de.customPresets=Ka([...de.customPresets,...ne]),an(n("Presets importados","Presets imported"),r)}catch{an(n("Falha ao importar presets","Could not import presets"),r)}},h.click()}function mn(p){var j,ne;const h=u.value[p],g=i.datasets[p];if(!h||h.rows.length===0)return[];const M=((j=g.selectedCell)==null?void 0:j.key)||g.pinnedColumnKey||((ne=h.visibleColumns[0])==null?void 0:ne.key),R=h.visibleColumns.find(de=>de.key===M);if(!R)return[];const L=new Map;for(const de of h.rows.slice(0,200)){const Q=$e(Te(de.raw,M)).trim();Q&&L.set(Q,(L.get(Q)??0)+1)}return[...L.entries()].sort((de,Q)=>Q[1]-de[1]).slice(0,5).map(([de,Q])=>({key:M,value:de,count:Q,label:R.label}))}function yp(p,h,g){i.datasets[p].filters.push({key:h,operator:"eq",value:g}),i.datasets[p].page=1}function gp(p,h){const g=o.value.find(j=>j.id===p),M=i.datasets[p];if(!g||!M)return;const R=g.columns.filter(j=>j.kind==="text"),L=g.columns.filter(j=>j.kind!=="text");if(h==="summary")M.visibleKeys=[...L.slice(0,4),...R.slice(0,3)].slice(0,7).map(j=>j.key),M.size=10;else if(h==="audit")M.visibleKeys=g.columns.map(j=>j.key),M.size=25;else if(h==="detailed"){M.visibleKeys=g.columns.map(j=>j.key),M.size=10;for(const j of R)M.columnWidths[j.key]=Math.max(M.columnWidths[j.key]??0,320)}M.page=1,M.activePreset=h}function bp(p,h){const g=(h==null?void 0:h.currentTarget)??(h==null?void 0:h.target);if(!g)return;const M=Math.max(0,g.scrollWidth-g.clientWidth);r.scrollHints[p]={left:g.scrollLeft>8,right:g.scrollLeft{const L=h.dataset.columns.find(j=>j.key===M.key);return L?{index:R,key:M.key,dir:M.dir,label:L.label}:null}).filter(Boolean)}function xp(p){var h;return!!((h=i.datasets[p])!=null&&h.pageBusy)}function Cp(){const p=$(),h=a();return!p||!h?null:p.columns.find(g=>g.key===h.pendingFilter.key)??null}function kp(p,h){return(p==="number"||p==="date")&&h==="between"}function wp(p){return p==="number"?"number":p==="date"?"date":"text"}return $n({payload:e,meta:s,txt:n,state:i,ui:r,datasets:o,datasetViews:u,overviewCards:v,themeIcon:y,footerText:C,activeDataset:$,activeDatasetState:a,toggleTheme:X,toggleSection:G,resetDataset:ae,clearFilters:B,removeFilter:me,openFilterModal:Ze,openSortModal:Ve,openColumnsModal:et,addFilter:Xt,applySort:ht,clearSort:Ot,setSortFromHeader:Qt,toggleRow:fe,selectCell:se,selectCellWithEvent:mt,isCellSelected:es,isRowSelected:yt,changePage:Ee,pageJump:dn,openCell:lo,isLongText:ro,copySql:$t,copySummary:ts,exportCsv:As,exportJson:fn,choosePick:It,moveColumns:gl,moveAllColumns:m,moveVisibleColumnOrder:f,visibleColumnSummary:_,selectedRowsCount:A,copySelectedCell:k,copySelectedRows:w,openCellMenu:D,openHeaderMenu:E,contextFilterByValue:P,contextHideColumn:S,contextShowOnlyColumn:H,copyContextValue:V,contextSort:Y,togglePinnedColumn:N,isPinnedColumn:U,getColumnWidth:Z,columnStyle:re,columnStickyClass:he,columnStickyStyle:ye,startResize:Fe,autoFitColumn:je,applyPreset:gp,saveCustomPreset:Vt,applyCustomPreset:pn,deleteCustomPreset:Me,exportCustomPresets:Ke,importCustomPresets:hn,quickFilterValues:mn,addQuickFilter:yp,updateScrollHint:bp,scrollClass:vp,activeSortSummary:_p,activePendingColumn:Cp,filterUsesRange:kp,filterInputType:wp,isPageBusy:xp,addSortRule:Zt,removeSortRule:Ms,updateSortRuleKey:un,updateSortRuleDirection:be,openModal:b,closeModal:T,closeContextMenu:q,summaryChipText:Qa,columnTone:Xa,allowedOperators:pl,operatorLabel:oo,getValue:Te,displayValue:Ie,stringifyValue:$e,PAGE_SIZES:ol,DEFAULT_EMPTY:Rt})}function Ia(e,t){const s=[],n=La(e.rows??[],e.schema??[]);s.push({id:"results",title:t("Resultados","Results"),searchPlaceholder:t("Buscar resultados","Search results"),columns:n,rows:e.rows??[],preparedRows:fl(e.rows??[],n),collapsed:!1}),(e.schema??[]).length>0&&s.push({id:"schema",title:t("Colunas e schema","Columns and schema"),searchPlaceholder:t("Buscar schema","Search schema"),columns:[{key:"name",label:t("Nome","Name"),kind:"text"},{key:"kind",label:t("Tipo","Type"),kind:"text"},{key:"nullCount",label:t("Nulos","Nulls"),kind:"number"},{key:"distinctCount",label:t("Distintos","Distinct"),kind:"number"},{key:"example",label:t("Exemplo","Example"),kind:"text"},{key:"minValue",label:t("Minimo","Minimum"),kind:"text"},{key:"maxValue",label:t("Maximo","Maximum"),kind:"text"}],rows:(e.schema??[]).map(o=>({name:o.name??o.Name??"",kind:o.kind??o.Kind??"",nullCount:o.nullCount??o.NullCount??0,distinctCount:o.distinctCount??o.DistinctCount??0,example:o.example??o.Example??"",minValue:o.minValue??o.MinValue??"",maxValue:o.maxValue??o.MaxValue??""})),preparedRows:[],collapsed:!0});for(const o of[["metadata",t("Metadados","Metadata"),t("Buscar metadados","Search metadata"),e.metadata??[]],["lineageNodes",t("Nos de linhagem","Lineage nodes"),t("Buscar nos","Search nodes"),e.lineageNodes??[]],["lineageConnections",t("Conexoes de linhagem","Lineage connections"),t("Buscar conexoes","Search connections"),e.lineageConnections??[]]])o[3].length>0&&s.push({id:o[0],title:o[1],searchPlaceholder:o[2],columns:no(o[3]),rows:o[3],preparedRows:[],collapsed:!0});return s}function Va(e){var n;const t=Fa(),s={theme:t.theme==="light"?"light":"dark",sections:{overview:!1,sql:!0},datasets:{}};for(const o of e){const i=((n=t.datasets)==null?void 0:n[o.id])??{};s.sections[o.id]=typeof i.collapsed=="boolean"?i.collapsed:!!o.collapsed,s.datasets[o.id]=il(o,i)}return s}function Fa(){try{return JSON.parse(window.localStorage.getItem(nl)??"{}")}catch{return{}}}function ja(e){const t={theme:e.theme,sections:JSON.parse(JSON.stringify(e.sections)),datasets:JSON.parse(JSON.stringify(e.datasets))};window.localStorage.setItem(nl,JSON.stringify(t))}function il(e,t){var r,c;const s=Gt(e.columns);e.columns=s;const n=s.map(u=>u.key),o=Array.isArray(t.visibleKeys)&&t.visibleKeys.length>0?t.visibleKeys.filter(u=>n.includes(u)):[...n],i={search:typeof t.search=="string"?t.search:"",filters:Array.isArray(t.filters)?t.filters.filter(u=>n.includes(u.key)):[],sort:rl(t.sort,n),sorts:cl(t.sorts,n,t.sort),visibleKeys:o.length>0?o:[...n],page:Ss(t.page,1),pageBusy:!1,size:to(t.size),inlineFiltersOpen:!!t.inlineFiltersOpen,inlineColumnsOpen:!!t.inlineColumnsOpen,selectedRows:Array.isArray(t.selectedRows)?t.selectedRows.map(String):[],selectedCell:t.selectedCell&&typeof t.selectedCell.key=="string"?{rowId:String(t.selectedCell.rowId??""),key:t.selectedCell.key}:null,selectedCells:Array.isArray(t.selectedCells)?t.selectedCells.map(String):[],anchorCell:t.anchorCell&&typeof t.anchorCell.key=="string"?{rowId:String(t.anchorCell.rowId??""),key:t.anchorCell.key}:null,columnWidths:ll(t.columnWidths,s),pendingFilter:eo(e,t.pendingFilter),columnSearch:typeof t.columnSearch=="string"?t.columnSearch:"",activePreset:typeof t.activePreset=="string"?t.activePreset:"",activeCustomPreset:typeof t.activeCustomPreset=="string"?t.activeCustomPreset:"",lastRowSelection:typeof t.lastRowSelection=="string"?t.lastRowSelection:"",pinnedColumnKey:typeof t.pinnedColumnKey=="string"?t.pinnedColumnKey:"",customPresets:Array.isArray(t.customPresets)?t.customPresets.filter(u=>u&&typeof u.name=="string").map(u=>so(e,u)):[],pickSelection:{available:Array.isArray((r=t.pickSelection)==null?void 0:r.available)?t.pickSelection.available.filter(u=>n.includes(u)):[],visible:Array.isArray((c=t.pickSelection)==null?void 0:c.visible)?t.pickSelection.visible.filter(u=>n.includes(u)):[]}};return pt(i),i}function eo(e,t){const s=e.columns[0],n=e.columns.some(r=>r.key===(t==null?void 0:t.key))?t.key:(s==null?void 0:s.key)??"",o=e.columns.find(r=>r.key===n)??s??{kind:"text"},i=pl(o.kind).includes(t==null?void 0:t.operator)?t.operator:qa(o.kind);return{key:n,operator:i,value:(t==null?void 0:t.value)??"",valueTo:(t==null?void 0:t.valueTo)??""}}function ll(e,t){const s={},n=e&&typeof e=="object"?e:{};for(const o of t)s[o.key]=rn(n[o.key],dl(o.kind));return s}function rl(e,t){return!e||!t.includes(e.key)?{key:"",dir:"asc"}:{key:e.key,dir:e.dir==="desc"?"desc":"asc"}}function cl(e,t,s=null){const n=Array.isArray(e)&&e.length>0?e:s?[s]:[];return ln(n.filter(o=>o&&t.includes(o.key)).map(o=>({key:o.key,dir:o.dir==="desc"?"desc":"asc"})))}function ln(e){const t=new Set,s=[];for(const n of e)!(n!=null&&n.key)||t.has(n.key)||(t.add(n.key),s.push({key:n.key,dir:n.dir==="desc"?"desc":"asc"}));return s}function pt(e){e.sort=e.sorts[0]?{...e.sorts[0]}:{key:"",dir:"asc"}}function Ss(e,t){const s=Number.parseInt(e,10);return Number.isFinite(s)&&s>0?s:t}function to(e){const t=Ss(e,10);return ol.includes(t)?t:10}function so(e,t){const s=Gt(e.columns).map(n=>n.key);return{name:String(t.name??"").trim()||"Preset",visibleKeys:Array.isArray(t.visibleKeys)?t.visibleKeys.filter(n=>s.includes(n)):[...s],sort:rl(t.sort,s),sorts:cl(t.sorts,s,t.sort),size:to(t.size),columnWidths:ll(t.columnWidths,e.columns),pinnedColumnKey:typeof t.pinnedColumnKey=="string"&&s.includes(t.pinnedColumnKey)?t.pinnedColumnKey:"",filters:Array.isArray(t.filters)?t.filters.filter(n=>s.includes(n.key)).map(n=>({...n,valueTo:n.valueTo??""})):[]}}function Ka(e){const t=new Map;for(const s of e)s!=null&&s.name&&t.set(s.name,s);return[...t.values()]}function La(e,t){const s=t.filter(n=>n&&typeof n=="object").map(n=>({key:n.name??n.Name??"",label:n.name??n.Name??"",kind:ul(n.kind??n.Kind??al(e,n.name??n.Name??""))}));return s.length>0?Gt(s):Gt(no(e))}function no(e){return e.length?Object.keys(e[0]??{}).map(t=>({key:t,label:t,kind:al(e,t)})):[]}function Gt(e){return(Array.isArray(e)?e:[]).filter(t=>t&&typeof t=="object").map(t=>{const s=String(t.key??t.name??t.Name??"").trim(),n=String(t.label??s).trim(),o=ul(t.kind??"text");return s?{key:s,label:n||s,kind:o}:null}).filter(Boolean)}function al(e,t){for(const s of e){const n=Te(s,t);if(n==null||n==="")continue;if(typeof n=="number")return"number";if(typeof n=="boolean")return"boolean";const o=String(n).trim();return/^-?\d+([.,]\d+)?$/.test(o)?"number":/\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/.test(o)?"date":/^(true|false|sim|nao|não)$/i.test(o)?"boolean":"text"}return"text"}function ul(e){const t=String(e??"").toLowerCase();return t.includes("date")||t.includes("time")?"date":t.includes("bool")?"boolean":t.includes("int")||t.includes("number")||t.includes("decimal")||t.includes("float")?"number":"text"}function Na(e,t){const s=Gt(e.columns);e.columns=s;const n=s.filter(b=>t.visibleKeys.includes(b.key)),o=n.length>0?n:s.slice(0,1),i=Array.isArray(e.preparedRows)&&e.preparedRows.length===e.rows.length?e.preparedRows:fl(e.rows,s),r=i.filter(b=>Wa(b,e,t)),c=Ba(r,e,t.sorts),u=to(t.size),v=Math.max(1,Math.ceil(c.length/u)),y=Math.min(Math.max(1,Ss(t.page,1)),v);t.page=y,t.size=u;const C=(y-1)*u,$=c.slice(C,C+u),a=s.find(b=>{var T;return b.key===((T=t.sorts[0])==null?void 0:T.key)})??null;return o.length>0&&!t.visibleKeys.some(b=>o.some(T=>T.key===b))&&(t.visibleKeys=o.map(b=>b.key)),{dataset:e,visibleColumns:o,rows:c,pageRows:$,page:y,pageCount:v,totalRows:i.length,filteredCount:c.length,size:u,sortColumn:a,sortRules:t.sorts,availableColumns:s.filter(b=>!t.visibleKeys.includes(b.key)&&(b.label||b.key).toLowerCase().includes(t.columnSearch.toLowerCase())),shownColumns:s.filter(b=>t.visibleKeys.includes(b.key)),hasRows:c.length>0}}function rn(e,t=Zn){const s=Ss(e,t);return Math.max(Zn,s)}function dl(e){switch(e){case"number":return 160;case"date":return 180;case"boolean":return 150;default:return 240}}function Ha(e,t){const s=$e(e),n=Math.min(Math.max(s.length,8),t==="text"?48:22);return Math.ceil(n*(t==="number"?9:8.2)+48)}function Ua(e,t){for(const s of["id","ID","Id","codigo","Codigo","key","Key"]){const n=Te(e,s);if(n!=null&&n!=="")return`${s}:${String(n)}`}return`row:${t}`}function fl(e,t){const s=Gt(t);return(Array.isArray(e)?e:[]).map((n,o)=>({raw:n,rowId:Ua(n,o),searchText:s.map(i=>$e(Te(n,i.key))).join(" ").toLowerCase()}))}function Wa(e,t,s){const n=s.search.trim().toLowerCase();if(n&&!e.searchText.includes(n))return!1;for(const o of s.filters){const i=t.columns.find(r=>r.key===o.key);if(i&&!Ja(Te(e.raw,i.key),i.kind,o))return!1}return!0}function Ba(e,t,s){const n=Array.isArray(s)?s:[];return n.length===0?e:[...e].sort((o,i)=>{for(const r of n){const c=t.columns.find(y=>y.key===r.key);if(!c)continue;const u=r.dir==="desc"?-1:1,v=za(Te(o.raw,c.key),Te(i.raw,c.key),c.kind)*u;if(v!==0)return v}return 0})}function za(e,t,s){return e===t?0:e==null||e===""?1:t==null||t===""?-1:s==="number"?Et(e)-Et(t):s==="date"?Dt(e)-Dt(t):s==="boolean"?Number(cn(e))-Number(cn(t)):$e(e).localeCompare($e(t),void 0,{numeric:!0,sensitivity:"base"})}function Et(e){if(typeof e=="number")return e;const t=String(e).replace(/\./g,"").replace(",","."),s=Number.parseFloat(t);return Number.isFinite(s)?s:0}function Dt(e){if(e instanceof Date)return e.getTime();const t=String(e).trim();if(/^\d{2}\/\d{2}\/\d{4}$/.test(t)){const[n,o,i]=t.split("/");return new Date(`${i}-${o}-${n}T00:00:00`).getTime()}const s=Date.parse(t);return Number.isFinite(s)?s:0}function cn(e){return typeof e=="boolean"?e:/^(true|sim|yes|1)$/i.test(String(e))}function Te(e,t){if(!e||typeof e!="object")return;if(Object.prototype.hasOwnProperty.call(e,t))return e[t];const s=Object.keys(e).find(n=>n.toLowerCase()===String(t).toLowerCase());return s?e[s]:void 0}function $e(e){return e==null||e===""?"":String(e)}function Ie(e){return $e(e)||Rt}function pl(e){return e==="number"||e==="date"?["between","eq","neq","gt","gte","lt","lte"]:e==="boolean"?["eq","neq"]:["contains","like","regex","eq","neq","starts","ends"]}function qa(e){return e==="text"?"contains":e==="number"||e==="date"?"between":"eq"}function oo(e){return{contains:"Contains",like:"Like",regex:"Regex",between:"Between",eq:"Equals",neq:"Different",gt:"Greater than",gte:"Greater or equal",lt:"Less than",lte:"Less or equal",starts:"Starts with",ends:"Ends with"}[e]??e}function Ja(e,t,s){const n=$e(e),o=$e(s.value),i=$e(s.valueTo);if(t==="number"){if(s.operator==="between"){const u=Et(n);return u>=Et(o)&&u<=Et(i)}return io(Et(n),Et(o),s.operator)}if(t==="date"){if(s.operator==="between"){const u=Dt(n);return u>=Dt(o)&&u<=Dt(i)}return io(Dt(n),Dt(o),s.operator)}if(t==="boolean")return io(cn(n),cn(o),s.operator);const r=n.toLowerCase(),c=o.toLowerCase();switch(s.operator){case"contains":return r.includes(c);case"starts":return r.startsWith(c);case"ends":return r.endsWith(c);case"like":return Ga(o).test(n);case"regex":try{return new RegExp(o,"i").test(n)}catch{return!1}case"eq":return r===c;case"neq":return r!==c;default:return!0}}function io(e,t,s){switch(s){case"eq":return e===t;case"neq":return e!==t;case"gt":return e>t;case"gte":return e>=t;case"lt":return ean(t,s)).catch(()=>an("Could not copy",s))}function an(e,t){t&&(window.clearTimeout(t.toastTimer),t.toast=e,t.toastTimer=window.setTimeout(()=>{t.toast=""},1800))}function yl(e){return String(e??"").normalize("NFD").replace(/[\u0300-\u036f]/g,"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}function Ya(e,t,s){const n=e.warnings??[];return[{label:s("Linhas","Rows"),value:new Intl.NumberFormat().format(Number((t.rows??[]).length)),icon:"rows"},{label:s("Colunas","Columns"),value:new Intl.NumberFormat().format(Number((t.schema??[]).length||no(t.rows??[]).length)),icon:"columns"},{label:s("Tempo","Duration"),value:e.duration??Rt,icon:"clock"},{label:s("Conexao","Connection"),value:e.connectionName??Rt,icon:"database"},{label:s("Gerado em","Generated at"),value:e.generatedAt??Rt,icon:"clock"},{label:s("Descricao","Description"),value:e.description||Rt,icon:"clipboard",span:"wide"},{label:"Warnings",value:n.length>0?n.join(" • "):s("Nenhum","None"),icon:"alert",tone:n.length>0?"warning":""}]}function Xa(e,t){return t==null||t===""?"is-null":e==="number"?"is-number":e==="date"?"is-date":e==="boolean"?"is-boolean":"is-text"}function Qa(e){return e.operator==="between"?`${e.key} • ${oo(e.operator)} • ${Ie(e.value)} → ${Ie(e.valueTo)}`:`${e.key} • ${oo(e.operator)} • ${Ie(e.value)}`}const Za={class:"page-header"},eu={class:"brand-wrap"},tu={class:"brand-copy"},su={class:"brand-sub"},nu={class:"page-intro"},ou={class:"toolbar"},iu=["disabled"],lu=["disabled"],ru={class:"main-content"},cu={class:"panel"},au={class:"section-head"},uu={class:"section-copy"},du={key:0,class:"overview-grid"},fu={class:"overview-icon"},pu={class:"overview-content"},hu={class:"overview-label"},mu={class:"overview-value"},yu={class:"section-head"},gu={class:"section-copy"},bu={class:"section-actions"},vu={class:"search-field"},_u=["onUpdate:modelValue","placeholder"],xu=["onClick"],Cu=["onClick"],ku=["onClick"],wu=["onClick"],Su=["onClick"],Mu={key:0,class:"dataset-body"},Au={class:"summary-strip"},Tu={class:"mini-panel"},Pu={class:"mini-panel-head"},Ru={class:"mini-panel-title"},Eu={class:"mini-panel-actions"},Du=["onClick"],Ou=["onClick"],$u={key:0,class:"chips"},Iu=["onClick"],Vu={key:0,class:"chip empty"},Fu={class:"mini-panel"},ju={class:"mini-panel-head"},Ku={class:"mini-panel-title"},Lu={class:"mini-panel-actions"},Nu=["onClick"],Hu=["onClick"],Uu={key:0,class:"chips"},Wu={class:"mini-panel"},Bu={class:"mini-panel-head"},zu={class:"mini-panel-title"},qu={class:"chips"},Ju={key:0,class:"chip empty"},Gu={class:"table-meta"},Yu={class:"table-meta-main"},Xu={class:"result-count"},Qu={key:0,class:"preset-toolbar"},Zu={class:"preset-segmented"},ed=["onClick"],td=["onClick"],sd=["onClick"],nd={class:"preset-actions"},od=["onClick","title"],id=["disabled","onClick","title"],ld=["onClick","title"],rd=["value","onChange"],cd={value:""},ad=["value"],ud={key:0,class:"preset-secondary"},dd=["disabled","onClick"],fd={class:"table-status"},pd={class:"status-pill"},hd={class:"status-pill"},md={key:0,class:"status-pill"},yd={key:1,class:"table-quick-actions"},gd=["disabled","onClick"],bd=["disabled","onClick"],vd={key:0,class:"quick-filters-wrap"},_d={class:"quick-filters"},xd={class:"quick-filters-label"},Cd=["onClick"],kd=["onScroll","onMouseenter"],wd={class:"report-table"},Sd=["onContextmenu"],Md={class:"th-shell"},Ad=["onClick"],Td={key:0,class:"sort-mark"},Pd=["title","onMousedown","onDblclick"],Rd={key:0},Ed={class:"select-col sticky-select"},Dd=["onClick"],Od=["onClick","onDblclick","onContextmenu"],$d={class:"cell-body"},Id={class:"cell-text"},Vd=["onClick"],Fd={key:1},jd=["colspan"],Kd={key:1,class:"table-loading"},Ld={class:"table-loading-card"},Nd={class:"pager-wrap pager-wrap-bottom"},Hd={class:"page-size-wrap"},Ud={class:"field-label"},Wd=["onUpdate:modelValue"],Bd=["value"],zd={class:"pager"},qd=["disabled","onClick"],Jd={class:"pager-readout"},Gd=["max","value","onChange"],Yd=["disabled","onClick"],Xd={key:0,class:"panel"},Qd={class:"section-head"},Zd={class:"section-copy"},ef={key:0,class:"sql-box"},tf={class:"page-footer"},sf={class:"modal-card"},nf={class:"modal-head"},of={class:"modal-title"},lf={class:"section-copy"},rf={key:0,class:"modal-body"},cf={class:"form-grid"},af={class:"field-stack"},uf={class:"field-label"},df=["value"],ff={class:"field-stack"},pf={class:"field-label"},hf=["value"],mf={key:0,class:"field-stack field-span"},yf={class:"field-label"},gf={class:"field-stack"},bf={class:"field-label"},vf=["type"],_f={class:"field-stack"},xf={class:"field-label"},Cf=["type"],kf={key:2,class:"field-stack field-span"},wf={class:"field-label"},Sf=["type"],Mf={class:"modal-foot"},Af={class:"modal-card"},Tf={class:"modal-head"},Pf={class:"modal-title"},Rf={class:"section-copy"},Ef={key:0,class:"modal-body"},Df={class:"sort-rules"},Of={class:"field-stack"},$f={class:"field-label"},If=["value","onChange"],Vf={value:""},Ff=["value"],jf={class:"field-stack"},Kf={class:"field-label"},Lf=["value","onChange"],Nf={value:"asc"},Hf={value:"desc"},Uf=["onClick"],Wf={key:0,class:"chip empty"},Bf={class:"modal-foot"},zf={class:"modal-card modal-wide"},qf={class:"modal-head"},Jf={class:"modal-title"},Gf={class:"section-copy"},Yf={key:0,class:"modal-body"},Xf={class:"picklist"},Qf={class:"pick-pane"},Zf={class:"field-label"},ep=["placeholder"],tp={class:"pick-list"},sp=["onClick"],np={class:"pick-actions"},op={class:"pick-pane"},ip={class:"field-label"},lp={class:"pick-list"},rp=["onClick"],cp={class:"modal-foot"},ap={class:"modal-card modal-cell"},up={class:"modal-head"},dp={class:"modal-title"},fp={class:"section-copy"},pp={class:"modal-body"},hp={class:"cell-modal-text"},mp={key:6,class:"toast"};Gc({__name:"App",setup(e){const t=$a();return(s,n)=>{var o,i,r,c,u,v,y,C,$;return I(),F("div",{class:"report-shell",onClick:n[47]||(n[47]=(...a)=>l(t).closeContextMenu&&l(t).closeContextMenu(...a))},[d("header",Za,[d("div",eu,[n[49]||(n[49]=d("div",{class:"brand-mark"},"A",-1)),d("div",tu,[n[48]||(n[48]=d("div",{class:"brand-name"},"AkkornStudio",-1)),d("div",su,x(l(t).meta.tabTitle||l(t).meta.title),1)])]),d("div",nu,[d("h1",null,x(l(t).meta.title),1)]),d("div",ou,[d("button",{class:"ghost-btn",type:"button",onClick:n[0]||(n[0]=(...a)=>l(t).toggleTheme&&l(t).toggleTheme(...a))},[O(l(K),{name:l(t).themeIcon},null,8,["name"]),d("span",null,x(l(t).txt("Tema","Theme")),1)]),d("button",{class:"ghost-btn",type:"button",onClick:n[1]||(n[1]=(...a)=>l(t).copySummary&&l(t).copySummary(...a))},[O(l(K),{name:"clipboard"}),d("span",null,x(l(t).txt("Copiar resumo","Copy summary")),1)]),d("button",{class:"ghost-btn",type:"button",onClick:n[2]||(n[2]=(...a)=>l(t).copySql&&l(t).copySql(...a)),disabled:!(l(t).payload.sql??"").trim()},[O(l(K),{name:"code"}),d("span",null,x(l(t).txt("Copiar SQL","Copy SQL")),1)],8,iu),d("button",{class:"ghost-btn",type:"button",onClick:n[3]||(n[3]=(...a)=>l(t).exportCsv&&l(t).exportCsv(...a)),disabled:!l(t).datasetViews.results},[O(l(K),{name:"download"}),d("span",null,x(l(t).txt("Exportar visivel CSV","Export visible CSV")),1)],8,lu),d("button",{class:"ghost-btn",type:"button",onClick:n[4]||(n[4]=(...a)=>l(t).exportJson&&l(t).exportJson(...a))},[O(l(K),{name:"json"}),d("span",null,x(l(t).txt("Exportar JSON","Export JSON")),1)])])]),d("main",ru,[d("section",cu,[d("div",au,[d("div",null,[d("h2",null,x(l(t).txt("Visao geral","Overview")),1),d("p",uu,x(l(t).txt("Panorama rapido da consulta e do resultado exportado.","Quick summary of the query and exported result.")),1)]),d("button",{class:"collapse-btn",type:"button",onClick:n[5]||(n[5]=a=>l(t).toggleSection("overview"))},[O(l(K),{name:"chevronDown"}),d("span",null,x(l(t).state.sections.overview?l(t).txt("Expandir","Expand"):l(t).txt("Recolher","Collapse")),1)])]),l(t).state.sections.overview?le("",!0):(I(),F("div",du,[(I(!0),F(ie,null,ge((l(t).overviewCards??[]).filter(Boolean),a=>(I(),F("article",{key:a.label,class:Ne(["overview-card",[a.span==="wide"?"is-wide":"",a.tone?`is-${a.tone}`:""]])},[d("div",fu,[O(l(K),{name:a.icon},null,8,["name"])]),d("div",pu,[d("div",hu,x(a.label),1),d("div",mu,x(a.value),1)])],2))),128))]))]),(I(!0),F(ie,null,ge(l(t).datasets,a=>(I(),F("section",{key:a.id,class:"panel"},[d("div",yu,[d("div",null,[d("h2",null,x(a.title),1),d("p",gu,x(a.searchPlaceholder),1)]),d("div",bu,[d("label",vu,[O(l(K),{name:"search"}),rt(d("input",{"onUpdate:modelValue":b=>l(t).state.datasets[a.id].search=b,class:"field-input compact-search",type:"search",placeholder:a.searchPlaceholder},null,8,_u),[[sn,l(t).state.datasets[a.id].search]])]),d("button",{class:"ghost-btn",type:"button",onClick:b=>l(t).openFilterModal(a.id)},[O(l(K),{name:"filter"}),d("span",null,x(l(t).txt("Filtros","Filters")),1)],8,xu),d("button",{class:"ghost-btn",type:"button",onClick:b=>l(t).openSortModal(a.id)},[O(l(K),{name:"sort"}),d("span",null,x(l(t).txt("Ordenacao","Sorting")),1)],8,Cu),d("button",{class:"ghost-btn",type:"button",onClick:b=>l(t).openColumnsModal(a.id)},[O(l(K),{name:"columns"}),d("span",null,x(l(t).txt("Colunas","Columns")),1)],8,ku),d("button",{class:"ghost-btn",type:"button",onClick:b=>l(t).resetDataset(a.id)},[O(l(K),{name:"rotate"}),d("span",null,x(l(t).txt("Resetar visao","Reset view")),1)],8,wu),d("button",{class:"collapse-btn",type:"button",onClick:b=>l(t).toggleSection(a.id)},[O(l(K),{name:"chevronDown"}),d("span",null,x(l(t).state.sections[a.id]?l(t).txt("Expandir","Expand"):l(t).txt("Recolher","Collapse")),1)],8,Su)])]),l(t).state.sections[a.id]?le("",!0):(I(),F("div",Mu,[d("div",Au,[d("article",Tu,[d("div",Pu,[d("div",Ru,x(l(t).txt("Filtros ativos","Active filters")),1),d("div",Eu,[d("button",{class:"icon-only",type:"button",onClick:b=>l(t).state.datasets[a.id].inlineFiltersOpen=!l(t).state.datasets[a.id].inlineFiltersOpen},[O(l(K),{name:"chevronDown"})],8,Du),d("button",{class:"ghost-btn small",type:"button",onClick:b=>l(t).clearFilters(a.id)},[O(l(K),{name:"rotate"}),d("span",null,x(l(t).txt("Limpar","Clear")),1)],8,Ou)])]),l(t).state.datasets[a.id].inlineFiltersOpen?(I(),F("div",$u,[(I(!0),F(ie,null,ge(l(t).state.datasets[a.id].filters,(b,T)=>(I(),F("button",{key:`${b.key}-${T}`,class:"chip removable",type:"button",onClick:q=>l(t).removeFilter(a.id,T)},[d("span",null,x(l(t).summaryChipText(b)),1),O(l(K),{name:"x"})],8,Iu))),128)),l(t).state.datasets[a.id].filters.length===0?(I(),F("div",Vu,x(l(t).txt("Nenhum filtro aplicado","No active filters")),1)):le("",!0)])):le("",!0)]),d("article",Fu,[d("div",ju,[d("div",Ku,x(l(t).txt("Colunas visiveis","Visible columns")),1),d("div",Lu,[d("button",{class:"icon-only",type:"button",onClick:b=>l(t).state.datasets[a.id].inlineColumnsOpen=!l(t).state.datasets[a.id].inlineColumnsOpen},[O(l(K),{name:"chevronDown"})],8,Nu),d("button",{class:"ghost-btn small",type:"button",onClick:b=>l(t).moveAllColumns(a.id,"all")},[O(l(K),{name:"rotate"}),d("span",null,x(l(t).txt("Resetar","Reset")),1)],8,Hu)])]),l(t).state.datasets[a.id].inlineColumnsOpen?(I(),F("div",Uu,[(I(!0),F(ie,null,ge((l(t).visibleColumnSummary(a.id)??[]).filter(Boolean),b=>(I(),F("div",{key:b.key,class:"chip"},x(b.label??b.key??l(t).DEFAULT_EMPTY),1))),128))])):le("",!0)]),d("article",Wu,[d("div",Bu,[d("div",zu,x(l(t).txt("Ordenacao ativa","Active sorting")),1)]),d("div",qu,[(I(!0),F(ie,null,ge(l(t).activeSortSummary(a.id),b=>(I(),F("div",{key:`${b.key}-${b.index}`,class:"chip"},x(b.index+1)+" • "+x(b.label??b.key??l(t).DEFAULT_EMPTY)+" • "+x(b.dir==="desc"?l(t).txt("Decrescente","Descending"):l(t).txt("Crescente","Ascending")),1))),128)),l(t).activeSortSummary(a.id).length===0?(I(),F("div",Ju,x(l(t).txt("Sem ordenacao ativa","No active sorting")),1)):le("",!0)])])]),d("div",{class:Ne(["table-shell",{"is-page-busy":l(t).isPageBusy(a.id)}])},[d("div",Gu,[d("div",Yu,[d("div",Xu,x(l(t).datasetViews[a.id].filteredCount)+" / "+x(l(t).datasetViews[a.id].totalRows),1),a.id==="results"?(I(),F("div",Qu,[d("div",Zu,[d("button",{class:"segment-btn",type:"button",onClick:b=>l(t).applyPreset(a.id,"summary")},x(l(t).txt("Resumo","Summary")),9,ed),d("button",{class:"segment-btn",type:"button",onClick:b=>l(t).applyPreset(a.id,"audit")},x(l(t).txt("Auditoria","Audit")),9,td),d("button",{class:"segment-btn",type:"button",onClick:b=>l(t).applyPreset(a.id,"detailed")},x(l(t).txt("Detalhado","Detailed")),9,sd)]),d("div",nd,[d("button",{class:"icon-chip",type:"button",onClick:b=>l(t).saveCustomPreset(a.id),title:l(t).txt("Salvar preset","Save preset")},[O(l(K),{name:"save"})],8,od),d("button",{class:"icon-chip",type:"button",disabled:l(t).state.datasets[a.id].customPresets.length===0,onClick:b=>l(t).exportCustomPresets(a.id),title:l(t).txt("Exportar presets","Export presets")},[O(l(K),{name:"downloadPreset"})],8,id),d("button",{class:"icon-chip",type:"button",onClick:b=>l(t).importCustomPresets(a.id),title:l(t).txt("Importar presets","Import presets")},[O(l(K),{name:"uploadPreset"})],8,ld)])])):le("",!0),a.id==="results"&&l(t).state.datasets[a.id].customPresets.length>0?(I(),F("select",{key:1,value:l(t).state.datasets[a.id].activeCustomPreset,class:"field-input preset-select",onChange:b=>l(t).applyCustomPreset(a.id,b.target.value)},[d("option",cd,x(l(t).txt("Presets salvos","Saved presets")),1),(I(!0),F(ie,null,ge(l(t).state.datasets[a.id].customPresets,b=>(I(),F("option",{key:b.name,value:b.name},x(b.name),9,ad))),128))],40,rd)):le("",!0)]),a.id==="results"&&l(t).state.datasets[a.id].customPresets.length>0?(I(),F("div",ud,[d("button",{class:"ghost-btn small",type:"button",disabled:!l(t).state.datasets[a.id].activeCustomPreset,onClick:b=>l(t).deleteCustomPreset(a.id,l(t).state.datasets[a.id].activeCustomPreset)},[O(l(K),{name:"trash"}),d("span",null,x(l(t).txt("Excluir preset","Delete preset")),1)],8,dd)])):le("",!0),d("div",fd,[d("span",pd,x(l(t).txt("Colunas visiveis","Visible columns"))+": "+x(l(t).datasetViews[a.id].visibleColumns.length),1),d("span",hd,x(l(t).txt("Linhas selecionadas","Selected rows"))+": "+x(l(t).selectedRowsCount(a.id)),1),l(t).state.datasets[a.id].selectedCell?(I(),F("span",md,x(l(t).txt("Celula selecionada","Selected cell")),1)):le("",!0)]),a.id==="results"?(I(),F("div",yd,[d("button",{class:"ghost-btn small",type:"button",disabled:!l(t).state.datasets[a.id].selectedCell,onClick:b=>l(t).copySelectedCell(a.id)},[O(l(K),{name:"clipboard"}),d("span",null,x(l(t).txt("Copiar celula","Copy cell")),1)],8,gd),d("button",{class:"ghost-btn small",type:"button",disabled:l(t).selectedRowsCount(a.id)===0,onClick:b=>l(t).copySelectedRows(a.id)},[O(l(K),{name:"rows"}),d("span",null,x(l(t).txt("Copiar linhas","Copy rows")),1)],8,bd)])):le("",!0)]),a.id==="results"&&l(t).quickFilterValues(a.id).length>0?(I(),F("div",vd,[d("div",_d,[d("span",xd,x(l(t).txt("Filtros rapidos","Quick filters")),1),(I(!0),F(ie,null,ge(l(t).quickFilterValues(a.id),b=>(I(),F("button",{key:`${b.key}-${b.value}`,class:"chip quick-chip",type:"button",onClick:T=>l(t).addQuickFilter(a.id,b.key,b.value)},[d("span",null,x(b.value),1),d("small",null,x(b.count),1)],8,Cd))),128))])])):le("",!0),d("div",{class:Ne(["table-scroll",l(t).scrollClass(a.id)]),onScroll:b=>l(t).updateScrollHint(a.id,b),onMouseenter:b=>l(t).updateScrollHint(a.id,b)},[d("table",wd,[d("colgroup",null,[n[50]||(n[50]=d("col",{class:"select-col-width"},null,-1)),(I(!0),F(ie,null,ge((l(t).datasetViews[a.id].visibleColumns??[]).filter(Boolean),b=>(I(),F("col",{key:`col-${b.key}`,style:gt(l(t).columnStyle(a.id,b.key))},null,4))),128))]),d("thead",null,[d("tr",null,[n[51]||(n[51]=d("th",{class:"select-col sticky-select"},null,-1)),(I(!0),F(ie,null,ge((l(t).datasetViews[a.id].visibleColumns??[]).filter(Boolean),b=>{var T,q;return I(),F("th",{key:b.key,style:gt([l(t).columnStyle(a.id,b.key),l(t).columnStickyStyle(a.id,b.key)]),class:Ne([l(t).columnStickyClass(a.id,b.key)]),onContextmenu:X=>l(t).openHeaderMenu(X,a.id,b.key)},[d("div",Md,[d("button",{class:"sort-head",type:"button",onClick:X=>l(t).setSortFromHeader(a.id,b.key)},[d("span",null,x(b.label??b.key??l(t).DEFAULT_EMPTY),1),((T=l(t).state.datasets[a.id].sorts[0])==null?void 0:T.key)===b.key?(I(),F("span",Td,x(((q=l(t).state.datasets[a.id].sorts[0])==null?void 0:q.dir)==="asc"?"▲":"▼"),1)):le("",!0)],8,Ad),d("button",{class:"resize-handle",type:"button",title:l(t).txt("Redimensionar coluna","Resize column"),onMousedown:X=>l(t).startResize(X,a.id,b.key),onDblclick:kt(X=>l(t).autoFitColumn(a.id,b.key),["stop"])},[O(l(K),{name:"resize",size:14})],40,Pd)])],46,Sd)}),128))])]),l(t).datasetViews[a.id].pageRows.length>0?(I(),F("tbody",Rd,[(I(!0),F(ie,null,ge(l(t).datasetViews[a.id].pageRows,b=>(I(),F("tr",{key:b.rowId,class:Ne({"is-row-selected":l(t).isRowSelected(a.id,b.rowId)})},[d("td",Ed,[d("button",{class:"row-selector",type:"button",onClick:T=>l(t).toggleRow(a.id,b.rowId,T)},[O(l(K),{name:"rowSelect",size:14,"stroke-width":2.1})],8,Dd)]),(I(!0),F(ie,null,ge((l(t).datasetViews[a.id].visibleColumns??[]).filter(Boolean),T=>(I(),F("td",{key:`${b.rowId}-${T.key}`,style:gt([l(t).columnStyle(a.id,T.key),l(t).columnStickyStyle(a.id,T.key)]),class:Ne(["cell",l(t).columnTone(T.kind,l(t).getValue(b.raw,T.key)),l(t).columnStickyClass(a.id,T.key),{"is-selected-cell":l(t).isCellSelected(a.id,b.rowId,T.key)}]),onClick:q=>l(t).selectCellWithEvent(a.id,b.rowId,T.key,q),onDblclick:q=>l(t).isLongText(l(t).getValue(b.raw,T.key))&&l(t).openCell(l(t).getValue(b.raw,T.key)),onContextmenu:q=>l(t).openCellMenu(q,a.id,b.rowId,T.key)},[d("div",$d,[d("span",Id,x(l(t).displayValue(l(t).getValue(b.raw,T.key))),1),l(t).isLongText(l(t).getValue(b.raw,T.key))?(I(),F("button",{key:0,class:"cell-expand",type:"button",onClick:kt(q=>l(t).openCell(l(t).getValue(b.raw,T.key)),["stop"])},[O(l(K),{name:"expand"})],8,Vd)):le("",!0)])],46,Od))),128))],2))),128))])):(I(),F("tbody",Fd,[d("tr",null,[d("td",{colspan:Math.max(1,l(t).datasetViews[a.id].visibleColumns.length+1),class:"empty-state"},x(l(t).txt("Nenhuma linha disponivel","No rows available")),9,jd)])]))])],42,kd),l(t).isPageBusy(a.id)?(I(),F("div",Kd,[d("div",Ld,[n[52]||(n[52]=d("span",{class:"table-loading-spinner"},null,-1)),d("span",null,x(l(t).txt("Atualizando pagina","Updating page")),1)])])):le("",!0),d("div",Nd,[d("div",Hd,[d("label",Ud,x(l(t).txt("Tamanho da pagina","Page size")),1),rt(d("select",{"onUpdate:modelValue":b=>l(t).state.datasets[a.id].size=b,class:"field-input compact-select"},[(I(!0),F(ie,null,ge(l(t).PAGE_SIZES,b=>(I(),F("option",{key:b,value:b},x(b),9,Bd))),128))],8,Wd),[[ws,l(t).state.datasets[a.id].size,void 0,{number:!0}]])]),d("div",zd,[d("button",{class:"icon-only",type:"button",disabled:l(t).datasetViews[a.id].page<=1,onClick:b=>l(t).changePage(a.id,l(t).datasetViews[a.id].page-1)},[O(l(K),{name:"left",title:l(t).txt("Pagina anterior","Previous page")},null,8,["title"])],8,qd),d("div",Jd,x(l(t).txt("Pagina","Page"))+" "+x(l(t).datasetViews[a.id].page)+" "+x(l(t).txt("de","of"))+" "+x(l(t).datasetViews[a.id].pageCount),1),d("input",{class:"pager-input",type:"number",min:"1",max:l(t).datasetViews[a.id].pageCount,value:l(t).datasetViews[a.id].page,onChange:b=>l(t).pageJump(b,a.id)},null,40,Gd),d("button",{class:"icon-only",type:"button",disabled:l(t).datasetViews[a.id].page>=l(t).datasetViews[a.id].pageCount,onClick:b=>l(t).changePage(a.id,l(t).datasetViews[a.id].page+1)},[O(l(K),{name:"right",title:l(t).txt("Proxima pagina","Next page")},null,8,["title"])],8,Yd)])])],2)]))]))),128)),(l(t).payload.sql??"").trim().length>0?(I(),F("section",Xd,[d("div",Qd,[d("div",null,[n[53]||(n[53]=d("h2",null,"SQL",-1)),d("p",Zd,x(l(t).txt("Consulta completa exportada no relatorio.","Full query exported in the report.")),1)]),d("button",{class:"collapse-btn",type:"button",onClick:n[6]||(n[6]=a=>l(t).toggleSection("sql"))},[O(l(K),{name:"chevronDown"}),d("span",null,x(l(t).state.sections.sql?l(t).txt("Expandir","Expand"):l(t).txt("Recolher","Collapse")),1)])]),l(t).state.sections.sql?le("",!0):(I(),F("div",ef,[d("pre",null,x(l(t).payload.sql),1)]))])):le("",!0)]),d("footer",tf,x(l(t).footerText),1),l(t).ui.activeModal==="filter"?(I(),F("div",{key:0,class:"modal-shell",onClick:n[16]||(n[16]=kt((...a)=>l(t).closeModal&&l(t).closeModal(...a),["self"]))},[d("div",sf,[d("div",nf,[d("div",null,[d("div",of,x(l(t).txt("Adicionar filtro","Add filter")),1),d("div",lf,x(l(t).txt("Escolha coluna, condicao e valor.","Choose column, condition and value.")),1)]),d("button",{class:"icon-only",type:"button",onClick:n[7]||(n[7]=(...a)=>l(t).closeModal&&l(t).closeModal(...a))},[O(l(K),{name:"x"})])]),l(t).activeDataset()&&l(t).activeDatasetState()?(I(),F("div",rf,[d("div",cf,[d("label",af,[d("span",uf,[O(l(K),{name:"columns"}),at(" "+x(l(t).txt("Coluna","Column")),1)]),rt(d("select",{"onUpdate:modelValue":n[8]||(n[8]=a=>l(t).activeDatasetState().pendingFilter.key=a),class:"field-input"},[(I(!0),F(ie,null,ge((l(t).activeDataset().columns??[]).filter(Boolean),a=>(I(),F("option",{key:a.key,value:a.key},x(a.label??a.key??l(t).DEFAULT_EMPTY),9,df))),128))],512),[[ws,l(t).activeDatasetState().pendingFilter.key]])]),d("label",ff,[d("span",pf,[O(l(K),{name:"funnel"}),at(" "+x(l(t).txt("Condicao","Condition")),1)]),rt(d("select",{"onUpdate:modelValue":n[9]||(n[9]=a=>l(t).activeDatasetState().pendingFilter.operator=a),class:"field-input"},[(I(!0),F(ie,null,ge(l(t).allowedOperators((l(t).activePendingColumn()??{kind:"text"}).kind),a=>(I(),F("option",{key:a,value:a},x(l(t).operatorLabel(a)),9,hf))),128))],512),[[ws,l(t).activeDatasetState().pendingFilter.operator]])]),(((o=l(t).activePendingColumn())==null?void 0:o.kind)??"text")==="boolean"?(I(),F("label",mf,[d("span",yf,[O(l(K),{name:"text"}),at(" "+x(l(t).txt("Valor","Value")),1)]),rt(d("select",{"onUpdate:modelValue":n[10]||(n[10]=a=>l(t).activeDatasetState().pendingFilter.value=a),class:"field-input"},[...n[54]||(n[54]=[d("option",{value:"true"},"True",-1),d("option",{value:"false"},"False",-1),d("option",{value:"sim"},"Sim",-1),d("option",{value:"nao"},"Não",-1)])],512),[[ws,l(t).activeDatasetState().pendingFilter.value]])])):l(t).filterUsesRange(((i=l(t).activePendingColumn())==null?void 0:i.kind)??"text",l(t).activeDatasetState().pendingFilter.operator)?(I(),F(ie,{key:1},[d("label",gf,[d("span",bf,[O(l(K),{name:"text"}),at(" "+x(l(t).txt("De","From")),1)]),rt(d("input",{"onUpdate:modelValue":n[11]||(n[11]=a=>l(t).activeDatasetState().pendingFilter.value=a),class:"field-input",type:l(t).filterInputType(((r=l(t).activePendingColumn())==null?void 0:r.kind)??"text")},null,8,vf),[[Qn,l(t).activeDatasetState().pendingFilter.value]])]),d("label",_f,[d("span",xf,[O(l(K),{name:"text"}),at(" "+x(l(t).txt("Ate","To")),1)]),rt(d("input",{"onUpdate:modelValue":n[12]||(n[12]=a=>l(t).activeDatasetState().pendingFilter.valueTo=a),class:"field-input",type:l(t).filterInputType(((c=l(t).activePendingColumn())==null?void 0:c.kind)??"text")},null,8,Cf),[[Qn,l(t).activeDatasetState().pendingFilter.valueTo]])])],64)):(I(),F("label",kf,[d("span",wf,[O(l(K),{name:"text"}),at(" "+x(l(t).txt("Valor","Value")),1)]),rt(d("input",{"onUpdate:modelValue":n[13]||(n[13]=a=>l(t).activeDatasetState().pendingFilter.value=a),class:"field-input",type:l(t).filterInputType(((u=l(t).activePendingColumn())==null?void 0:u.kind)??"text")},null,8,Sf),[[Qn,l(t).activeDatasetState().pendingFilter.value]])]))])])):le("",!0),d("div",Mf,[d("button",{class:"ghost-btn",type:"button",onClick:n[14]||(n[14]=(...a)=>l(t).addFilter&&l(t).addFilter(...a))},[O(l(K),{name:"plus"}),d("span",null,x(l(t).txt("Adicionar filtro","Add filter")),1)]),d("button",{class:"ghost-btn",type:"button",onClick:n[15]||(n[15]=(...a)=>l(t).closeModal&&l(t).closeModal(...a))},x(l(t).txt("Fechar","Close")),1)])])])):le("",!0),l(t).ui.activeModal==="sort"?(I(),F("div",{key:1,class:"modal-shell",onClick:n[21]||(n[21]=kt((...a)=>l(t).closeModal&&l(t).closeModal(...a),["self"]))},[d("div",Af,[d("div",Tf,[d("div",null,[d("div",Pf,x(l(t).txt("Gerenciar ordenacao","Manage sorting")),1),d("div",Rf,x(l(t).txt("Defina prioridade e direcao de cada criterio.","Set priority and direction for each sort rule.")),1)]),d("button",{class:"icon-only",type:"button",onClick:n[17]||(n[17]=(...a)=>l(t).closeModal&&l(t).closeModal(...a))},[O(l(K),{name:"x"})])]),l(t).activeDataset()&&l(t).activeDatasetState()?(I(),F("div",Ef,[d("div",Df,[(I(!0),F(ie,null,ge(l(t).activeDatasetState().sorts,(a,b)=>(I(),F("div",{key:`${a.key}-${b}`,class:"sort-rule"},[d("label",Of,[d("span",$f,[O(l(K),{name:"sort"}),at(" "+x(l(t).txt("Coluna de ordenacao","Sort column"))+" "+x(b+1),1)]),d("select",{value:a.key,class:"field-input",onChange:T=>l(t).updateSortRuleKey(b,T.target.value)},[d("option",Vf,x(l(t).txt("Nenhuma","None")),1),(I(!0),F(ie,null,ge((l(t).activeDataset().columns??[]).filter(Boolean),T=>(I(),F("option",{key:T.key,value:T.key},x(T.label??T.key??l(t).DEFAULT_EMPTY),9,Ff))),128))],40,If)]),d("label",jf,[d("span",Kf,[O(l(K),{name:"arrowUpDown"}),at(" "+x(l(t).txt("Direcao","Direction")),1)]),d("select",{value:a.dir,class:"field-input",onChange:T=>l(t).updateSortRuleDirection(b,T.target.value)},[d("option",Nf,x(l(t).txt("Crescente","Ascending")),1),d("option",Hf,x(l(t).txt("Decrescente","Descending")),1)],40,Lf)]),d("button",{class:"ghost-btn small sort-remove",type:"button",onClick:T=>l(t).removeSortRule(b)},[O(l(K),{name:"trash"}),d("span",null,x(l(t).txt("Remover","Remove")),1)],8,Uf)]))),128)),l(t).activeDatasetState().sorts.length===0?(I(),F("div",Wf,x(l(t).txt("Sem criterios de ordenacao","No sort rules yet")),1)):le("",!0)])])):le("",!0),d("div",Bf,[d("button",{class:"ghost-btn",type:"button",onClick:n[18]||(n[18]=(...a)=>l(t).addSortRule&&l(t).addSortRule(...a))},[O(l(K),{name:"plus"}),d("span",null,x(l(t).txt("Adicionar nivel","Add level")),1)]),d("button",{class:"ghost-btn",type:"button",onClick:n[19]||(n[19]=(...a)=>l(t).applySort&&l(t).applySort(...a))},[O(l(K),{name:"check"}),d("span",null,x(l(t).txt("Aplicar ordenacao","Apply sorting")),1)]),d("button",{class:"ghost-btn",type:"button",onClick:n[20]||(n[20]=(...a)=>l(t).clearSort&&l(t).clearSort(...a))},[O(l(K),{name:"rotate"}),d("span",null,x(l(t).txt("Resetar","Reset")),1)])])])])):le("",!0),l(t).ui.activeModal==="columns"?(I(),F("div",{key:2,class:"modal-shell",onClick:n[31]||(n[31]=kt((...a)=>l(t).closeModal&&l(t).closeModal(...a),["self"]))},[d("div",zf,[d("div",qf,[d("div",null,[d("div",Jf,x(l(t).txt("Colunas visiveis","Visible columns")),1),d("div",Gf,x(l(t).txt("Gerencie o que permanece na tabela.","Manage what stays visible in the table.")),1)]),d("button",{class:"icon-only",type:"button",onClick:n[22]||(n[22]=(...a)=>l(t).closeModal&&l(t).closeModal(...a))},[O(l(K),{name:"x"})])]),l(t).activeDataset()&&l(t).activeDatasetState()?(I(),F("div",Yf,[d("div",Xf,[d("div",Qf,[d("label",Zf,x(l(t).txt("Disponiveis","Available")),1),rt(d("input",{"onUpdate:modelValue":n[23]||(n[23]=a=>l(t).activeDatasetState().columnSearch=a),class:"field-input",type:"search",placeholder:l(t).txt("Buscar colunas","Search columns")},null,8,ep),[[sn,l(t).activeDatasetState().columnSearch]]),d("div",tp,[(I(!0),F(ie,null,ge((((v=l(t).datasetViews[l(t).ui.modalDatasetId])==null?void 0:v.availableColumns)??[]).filter(Boolean),a=>(I(),F("button",{key:a.key,class:Ne(["pick-item",{active:l(t).activeDatasetState().pickSelection.available.includes(a.key)}]),type:"button",onClick:b=>l(t).choosePick(l(t).ui.modalDatasetId,"available",a.key,!0)},x(a.label??a.key??l(t).DEFAULT_EMPTY),11,sp))),128))])]),d("div",np,[d("button",{class:"ghost-btn",type:"button",onClick:n[24]||(n[24]=a=>l(t).moveColumns(l(t).ui.modalDatasetId,"add"))},">"),d("button",{class:"ghost-btn",type:"button",onClick:n[25]||(n[25]=a=>l(t).moveColumns(l(t).ui.modalDatasetId,"remove"))},"<"),d("button",{class:"ghost-btn",type:"button",onClick:n[26]||(n[26]=a=>l(t).moveAllColumns(l(t).ui.modalDatasetId,"all"))},">>"),d("button",{class:"ghost-btn",type:"button",onClick:n[27]||(n[27]=a=>l(t).moveAllColumns(l(t).ui.modalDatasetId,"none"))},"<<"),d("button",{class:"ghost-btn",type:"button",onClick:n[28]||(n[28]=a=>l(t).moveVisibleColumnOrder(l(t).ui.modalDatasetId,"up"))},[O(l(K),{name:"up"})]),d("button",{class:"ghost-btn",type:"button",onClick:n[29]||(n[29]=a=>l(t).moveVisibleColumnOrder(l(t).ui.modalDatasetId,"down"))},[O(l(K),{name:"chevronDown"})])]),d("div",op,[d("label",ip,x(l(t).txt("Em exibicao","Showing")),1),d("div",lp,[(I(!0),F(ie,null,ge((((y=l(t).datasetViews[l(t).ui.modalDatasetId])==null?void 0:y.shownColumns)??[]).filter(Boolean),a=>(I(),F("button",{key:a.key,class:Ne(["pick-item",{active:l(t).activeDatasetState().pickSelection.visible.includes(a.key)}]),type:"button",onClick:b=>l(t).choosePick(l(t).ui.modalDatasetId,"visible",a.key,!0)},x(a.label??a.key??l(t).DEFAULT_EMPTY),11,rp))),128))])])])])):le("",!0),d("div",cp,[d("button",{class:"ghost-btn",type:"button",onClick:n[30]||(n[30]=(...a)=>l(t).closeModal&&l(t).closeModal(...a))},[O(l(K),{name:"check"}),d("span",null,x(l(t).txt("Concluir","Done")),1)])])])])):le("",!0),l(t).ui.activeModal==="cell"?(I(),F("div",{key:3,class:"modal-shell",onClick:n[33]||(n[33]=kt((...a)=>l(t).closeModal&&l(t).closeModal(...a),["self"]))},[d("div",ap,[d("div",up,[d("div",null,[d("div",dp,x(l(t).txt("Conteudo completo","Full content")),1),d("div",fp,x(l(t).txt("Visualizacao ampliada da celula selecionada.","Expanded view for the selected cell.")),1)]),d("button",{class:"icon-only",type:"button",onClick:n[32]||(n[32]=(...a)=>l(t).closeModal&&l(t).closeModal(...a))},[O(l(K),{name:"x"})])]),d("div",pp,[d("pre",hp,x(l(t).ui.longCellValue),1)])])])):le("",!0),((C=l(t).ui.contextMenu)==null?void 0:C.type)==="cell"?(I(),F("div",{key:4,class:"context-menu",style:gt({left:`${l(t).ui.contextMenu.x}px`,top:`${l(t).ui.contextMenu.y}px`}),onClick:n[38]||(n[38]=kt(()=>{},["stop"]))},[d("button",{class:"context-item",type:"button",onClick:n[34]||(n[34]=a=>l(t).copyContextValue())},[O(l(K),{name:"clipboard"}),d("span",null,x(l(t).txt("Copiar valor","Copy value")),1)]),d("button",{class:"context-item",type:"button",onClick:n[35]||(n[35]=a=>l(t).contextFilterByValue())},[O(l(K),{name:"filter"}),d("span",null,x(l(t).txt("Filtrar por este valor","Filter by this value")),1)]),d("button",{class:"context-item",type:"button",onClick:n[36]||(n[36]=a=>l(t).contextHideColumn())},[O(l(K),{name:"columns"}),d("span",null,x(l(t).txt("Ocultar coluna","Hide column")),1)]),d("button",{class:"context-item",type:"button",onClick:n[37]||(n[37]=a=>l(t).openCell(l(t).ui.contextMenu.value))},[O(l(K),{name:"expand"}),d("span",null,x(l(t).txt("Abrir valor completo","Open full value")),1)])],4)):le("",!0),(($=l(t).ui.contextMenu)==null?void 0:$.type)==="header"?(I(),F("div",{key:5,class:"context-menu",style:gt({left:`${l(t).ui.contextMenu.x}px`,top:`${l(t).ui.contextMenu.y}px`}),onClick:n[46]||(n[46]=kt(()=>{},["stop"]))},[d("button",{class:"context-item",type:"button",onClick:n[39]||(n[39]=a=>l(t).contextSort("asc","primary"))},[O(l(K),{name:"sortAsc"}),d("span",null,x(l(t).txt("Ordenar crescente","Sort ascending")),1)]),d("button",{class:"context-item",type:"button",onClick:n[40]||(n[40]=a=>l(t).contextSort("desc","primary"))},[O(l(K),{name:"sortDesc"}),d("span",null,x(l(t).txt("Ordenar decrescente","Sort descending")),1)]),d("button",{class:"context-item",type:"button",onClick:n[41]||(n[41]=a=>l(t).contextSort("asc","secondary"))},[O(l(K),{name:"sortAppend"}),d("span",null,x(l(t).txt("Adicionar como criterio","Add as sort level")),1)]),d("button",{class:"context-item",type:"button",onClick:n[42]||(n[42]=a=>l(t).autoFitColumn(l(t).ui.contextMenu.datasetId,l(t).ui.contextMenu.key))},[O(l(K),{name:"resize"}),d("span",null,x(l(t).txt("Auto ajustar coluna","Auto fit column")),1)]),d("button",{class:"context-item",type:"button",onClick:n[43]||(n[43]=a=>l(t).togglePinnedColumn())},[O(l(K),{name:"pin"}),d("span",null,x(l(t).isPinnedColumn(l(t).ui.contextMenu.datasetId,l(t).ui.contextMenu.key)?l(t).txt("Desafixar coluna","Unpin column"):l(t).txt("Fixar coluna","Pin column")),1)]),d("button",{class:"context-item",type:"button",onClick:n[44]||(n[44]=a=>l(t).contextHideColumn())},[O(l(K),{name:"columns"}),d("span",null,x(l(t).txt("Ocultar coluna","Hide column")),1)]),d("button",{class:"context-item",type:"button",onClick:n[45]||(n[45]=a=>l(t).contextShowOnlyColumn())},[O(l(K),{name:"check"}),d("span",null,x(l(t).txt("Mostrar apenas esta","Show only this")),1)])],4)):le("",!0),l(t).ui.toast?(I(),F("div",mp,x(l(t).ui.toast),1)):le("",!0)])}}}).mount("#app")})(); diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/package-lock.json b/src/AkkornStudio.UI/Assets/ReportFrontend/package-lock.json new file mode 100644 index 00000000..3bd40574 --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/package-lock.json @@ -0,0 +1,1373 @@ +{ + "name": "akkornstudio-report-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "akkornstudio-report-frontend", + "version": "1.0.0", + "dependencies": { + "lucide-vue-next": "^0.525.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.3.5" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz", + "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.33", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", + "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", + "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.33", + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.10", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", + "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz", + "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz", + "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", + "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/runtime-core": "3.5.33", + "@vue/shared": "3.5.33", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz", + "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "vue": "3.5.33" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz", + "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lucide-vue-next": { + "version": "0.525.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.525.0.tgz", + "integrity": "sha512-Xf8+x8B2DrnGDV/rxylS+KBp2FIe6ljwDn2JsGTZZvXIfhmm/q+nv8RuGO1OyoMjOVkkz7CqtUqJfwtFPRbB2w==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", + "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-sfc": "3.5.33", + "@vue/runtime-dom": "3.5.33", + "@vue/server-renderer": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/package.json b/src/AkkornStudio.UI/Assets/ReportFrontend/package.json new file mode 100644 index 00000000..36c67a2d --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/package.json @@ -0,0 +1,17 @@ +{ + "name": "akkornstudio-report-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "vite build" + }, + "dependencies": { + "lucide-vue-next": "^0.525.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.3.5" + } +} diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/src/App.vue b/src/AkkornStudio.UI/Assets/ReportFrontend/src/App.vue new file mode 100644 index 00000000..a21fd5a4 --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/src/App.vue @@ -0,0 +1,347 @@ + + + diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/src/icons.js b/src/AkkornStudio.UI/Assets/ReportFrontend/src/icons.js new file mode 100644 index 00000000..09221812 --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/src/icons.js @@ -0,0 +1,103 @@ +import { h } from "vue"; +import { + AlertTriangle, + ArrowDownWideNarrow, + ArrowDownAZ, + ArrowUpDown, + ArrowUpAZ, + Check, + ChevronDown, + ChevronLeft, + ChevronRight, + ChevronUp, + CircleDot, + ClipboardList, + Clock3, + Code2, + Columns3, + Database, + Download, + Expand, + FileJson2, + Funnel, + GripVertical, + ListFilter, + Moon, + Pin, + Plus, + Rows3, + RotateCcw, + Search, + Save, + Sun, + TextCursorInput, + Trash2, + Upload, + X +} from "lucide-vue-next"; + +const iconMap = { + moon: Moon, + sun: Sun, + clipboard: ClipboardList, + code: Code2, + download: Download, + json: FileJson2, + filter: ListFilter, + sort: ArrowDownWideNarrow, + sortAsc: ArrowUpAZ, + sortDesc: ArrowDownAZ, + sortAppend: ArrowUpDown, + columns: Columns3, + rotate: RotateCcw, + chevronDown: ChevronDown, + plus: Plus, + x: X, + check: Check, + text: TextCursorInput, + funnel: Funnel, + arrowUpDown: ArrowUpDown, + left: ChevronLeft, + right: ChevronRight, + up: ChevronUp, + expand: Expand, + rows: Rows3, + clock: Clock3, + database: Database, + alert: AlertTriangle, + rowSelect: CircleDot, + search: Search, + resize: GripVertical, + pin: Pin, + save: Save, + trash: Trash2, + uploadPreset: Upload, + downloadPreset: FileJson2 +}; + +export const IconGlyph = { + name: "IconGlyph", + props: { + name: { type: String, required: true }, + title: { type: String, default: "" }, + size: { type: Number, default: 16 }, + strokeWidth: { type: Number, default: 1.9 } + }, + render() { + const IconComponent = iconMap[this.name] ?? CircleDot; + return h( + "span", + { + class: "icon-glyph", + title: this.title, + "aria-hidden": "true" + }, + [ + h(IconComponent, { + size: this.size, + strokeWidth: this.strokeWidth + }) + ] + ); + } +}; diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/src/main.js b/src/AkkornStudio.UI/Assets/ReportFrontend/src/main.js new file mode 100644 index 00000000..b670de8b --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/src/main.js @@ -0,0 +1,4 @@ +import { createApp } from "vue"; +import App from "./App.vue"; + +createApp(App).mount("#app"); diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/src/report-data.js b/src/AkkornStudio.UI/Assets/ReportFrontend/src/report-data.js new file mode 100644 index 00000000..3b4826db --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/src/report-data.js @@ -0,0 +1,1731 @@ +import { computed, proxyRefs, reactive, watch } from "vue"; + +export const STORAGE_KEY = "akkorn-report-view-vue-v7"; +export const PAGE_SIZES = [10, 25, 50, 100]; +export const DEFAULT_EMPTY = "-"; +export const MIN_COLUMN_WIDTH = 140; + +export function createReportModel() { + const payload = window.__AKKORN_REPORT__ ?? {}; + const labels = payload.labels ?? {}; + const meta = payload.meta ?? {}; + const txt = (pt, en) => labels[pt] ?? labels[en] ?? pt; + + const datasets = computed(() => buildDatasets(payload, txt)); + const state = reactive(loadState(datasets.value)); + const ui = reactive({ + activeModal: "", + modalDatasetId: "", + longCellValue: "", + toast: "", + toastTimer: 0, + resizing: null, + contextMenu: null, + scrollHints: {} + }); + + const pageJobs = new Map(); + + const datasetViews = computed(() => { + const entries = {}; + for (const dataset of datasets.value) { + entries[dataset.id] = buildDatasetView(dataset, state.datasets[dataset.id]); + } + + return entries; + }); + + const overviewCards = computed(() => buildOverviewCards(meta, payload, txt)); + const themeIcon = computed(() => (state.theme === "dark" ? "moon" : "sun")); + const footerText = computed(() => `${meta.title ?? "Report"} • Vue 3 • v${payload.version ?? "1.0"}`); + + watch( + () => state, + () => saveState(state), + { deep: true } + ); + + watch( + () => state.theme, + (theme) => { + document.documentElement.setAttribute("data-theme", theme); + }, + { immediate: true } + ); + + function activeDataset() { + return datasets.value.find((dataset) => dataset.id === ui.modalDatasetId) ?? null; + } + + function activeDatasetState() { + return ui.modalDatasetId ? state.datasets[ui.modalDatasetId] : null; + } + + function openModal(name, datasetId = "") { + ui.activeModal = name; + ui.modalDatasetId = datasetId; + } + + function closeModal() { + ui.activeModal = ""; + ui.modalDatasetId = ""; + } + + function closeContextMenu() { + ui.contextMenu = null; + } + + function toggleTheme() { + state.theme = state.theme === "dark" ? "light" : "dark"; + } + + function toggleSection(id) { + state.sections[id] = !state.sections[id]; + } + + function resetDataset(id) { + const dataset = datasets.value.find((item) => item.id === id); + if (!dataset) { + return; + } + + state.datasets[id] = normalizeDatasetState(dataset, {}); + } + + function clearFilters(id) { + state.datasets[id].filters = []; + state.datasets[id].page = 1; + } + + function removeFilter(id, index) { + state.datasets[id].filters.splice(index, 1); + state.datasets[id].page = 1; + } + + function openFilterModal(id) { + const dataset = datasets.value.find((item) => item.id === id); + if (!dataset) { + return; + } + + state.datasets[id].pendingFilter = normalizePendingFilter(dataset, state.datasets[id].pendingFilter); + openModal("filter", id); + } + + function openSortModal(id) { + openModal("sort", id); + } + + function openColumnsModal(id) { + openModal("columns", id); + } + + function addFilter() { + const dataset = activeDataset(); + const datasetState = activeDatasetState(); + if (!dataset || !datasetState || !datasetState.pendingFilter.key) { + return; + } + + datasetState.filters.push({ ...datasetState.pendingFilter }); + datasetState.page = 1; + const currentKey = datasetState.pendingFilter.key; + datasetState.pendingFilter = normalizePendingFilter(dataset, { key: currentKey }); + } + + function applySort() { + const datasetState = activeDatasetState(); + if (!datasetState) { + return; + } + + datasetState.page = 1; + closeModal(); + } + + function clearSort() { + const datasetState = activeDatasetState(); + if (!datasetState) { + return; + } + + datasetState.sorts = []; + syncPrimarySort(datasetState); + datasetState.page = 1; + } + + function setSortFromHeader(id, key) { + const datasetState = state.datasets[id]; + const current = datasetState.sorts[0]; + if (current?.key === key) { + datasetState.sorts = [{ key, dir: current.dir === "asc" ? "desc" : "asc" }, ...datasetState.sorts.slice(1)]; + } else { + datasetState.sorts = [{ key, dir: "asc" }, ...datasetState.sorts.filter((item) => item.key !== key)]; + } + syncPrimarySort(datasetState); + datasetState.page = 1; + } + + function addSortRule() { + const dataset = activeDataset(); + const datasetState = activeDatasetState(); + if (!dataset || !datasetState) { + return; + } + + const key = dataset.columns.find((column) => !datasetState.sorts.some((item) => item.key === column.key))?.key ?? dataset.columns[0]?.key ?? ""; + if (!key) { + return; + } + + datasetState.sorts.push({ key, dir: "asc" }); + syncPrimarySort(datasetState); + } + + function removeSortRule(index) { + const datasetState = activeDatasetState(); + if (!datasetState) { + return; + } + + datasetState.sorts.splice(index, 1); + syncPrimarySort(datasetState); + } + + function updateSortRuleKey(index, key) { + const datasetState = activeDatasetState(); + if (!datasetState || !datasetState.sorts[index]) { + return; + } + + datasetState.sorts[index].key = key; + datasetState.sorts = dedupeSortRules(datasetState.sorts); + syncPrimarySort(datasetState); + } + + function updateSortRuleDirection(index, dir) { + const datasetState = activeDatasetState(); + if (!datasetState || !datasetState.sorts[index]) { + return; + } + + datasetState.sorts[index].dir = dir === "desc" ? "desc" : "asc"; + syncPrimarySort(datasetState); + } + + function toggleRow(id, rowId, event = null) { + const selected = state.datasets[id].selectedRows; + const datasetState = state.datasets[id]; + const view = datasetViews.value[id]; + if (event?.shiftKey && datasetState.lastRowSelection && view) { + const rowIds = view.rows.map((entry) => entry.rowId); + const start = rowIds.indexOf(datasetState.lastRowSelection); + const end = rowIds.indexOf(rowId); + if (start >= 0 && end >= 0) { + const [from, to] = start < end ? [start, end] : [end, start]; + datasetState.selectedRows = [...new Set([...selected, ...rowIds.slice(from, to + 1)])]; + datasetState.lastRowSelection = rowId; + return; + } + } + + const index = selected.indexOf(rowId); + if (index >= 0) { + selected.splice(index, 1); + } else { + selected.push(rowId); + } + datasetState.lastRowSelection = rowId; + } + + function selectCell(id, rowId, key) { + state.datasets[id].selectedCell = { rowId, key }; + state.datasets[id].selectedCells = [`${rowId}::${key}`]; + state.datasets[id].anchorCell = { rowId, key }; + closeContextMenu(); + } + + function selectCellWithEvent(id, rowId, key, event = null) { + const datasetState = state.datasets[id]; + const view = datasetViews.value[id]; + if (event?.shiftKey && datasetState.anchorCell && view) { + const rowIds = view.rows.map((entry) => entry.rowId); + const colKeys = view.visibleColumns.map((column) => column.key); + const startRow = rowIds.indexOf(datasetState.anchorCell.rowId); + const endRow = rowIds.indexOf(rowId); + const startCol = colKeys.indexOf(datasetState.anchorCell.key); + const endCol = colKeys.indexOf(key); + if (startRow >= 0 && endRow >= 0 && startCol >= 0 && endCol >= 0) { + const [rowFrom, rowTo] = startRow < endRow ? [startRow, endRow] : [endRow, startRow]; + const [colFrom, colTo] = startCol < endCol ? [startCol, endCol] : [endCol, startCol]; + const selectedCells = []; + for (let rowIndex = rowFrom; rowIndex <= rowTo; rowIndex += 1) { + for (let colIndex = colFrom; colIndex <= colTo; colIndex += 1) { + selectedCells.push(`${rowIds[rowIndex]}::${colKeys[colIndex]}`); + } + } + datasetState.selectedCell = { rowId, key }; + datasetState.selectedCells = selectedCells; + closeContextMenu(); + return; + } + } + + selectCell(id, rowId, key); + } + + function isCellSelected(id, rowId, key) { + const datasetState = state.datasets[id]; + if (datasetState.selectedCells?.length > 0) { + return datasetState.selectedCells.includes(`${rowId}::${key}`); + } + + const selected = datasetState.selectedCell; + return !!selected && selected.rowId === rowId && selected.key === key; + } + + function isRowSelected(id, rowId) { + return state.datasets[id].selectedRows.includes(rowId); + } + + function changePage(id, target) { + const view = datasetViews.value[id]; + if (!view) { + return; + } + + const safeTarget = Math.min(Math.max(1, normalizePositiveInt(target, 1)), view.pageCount); + const datasetState = state.datasets[id]; + if (datasetState.page === safeTarget && !datasetState.pageBusy) { + return; + } + + if (pageJobs.has(id)) { + window.clearTimeout(pageJobs.get(id)); + } + + datasetState.pageBusy = true; + const handle = window.setTimeout(() => { + datasetState.page = safeTarget; + window.requestAnimationFrame(() => { + datasetState.pageBusy = false; + pageJobs.delete(id); + }); + }, 0); + pageJobs.set(id, handle); + } + + function pageJump(event, id) { + changePage(id, event.target.value); + } + + function openCell(value) { + ui.longCellValue = displayValue(value); + closeContextMenu(); + openModal("cell"); + } + + function isLongText(value) { + return stringifyValue(value).length > 96; + } + + function copySql() { + copyText(payload.sql ?? "", txt("SQL copiado", "SQL copied"), ui); + } + + function copySummary() { + const lines = [ + meta.title ?? "", + meta.description ?? "", + `${txt("Linhas", "Rows")}: ${(payload.rows ?? []).length}`, + `${txt("Colunas", "Columns")}: ${(payload.schema ?? []).length}`, + `${txt("Gerado em", "Generated at")}: ${meta.generatedAt ?? ""}` + ].filter(Boolean); + copyText(lines.join("\n"), txt("Resumo copiado", "Summary copied"), ui); + } + + function exportCsv() { + const view = datasetViews.value.results; + if (!view) { + return; + } + + const headers = view.visibleColumns.map((column) => column.label); + const rows = view.rows.map((entry) => view.visibleColumns.map((column) => csvEscape(getValue(entry.raw, column.key)))); + const content = [headers.map(csvEscape).join(","), ...rows.map((row) => row.join(","))].join("\r\n"); + const blob = new Blob([content], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `${(meta.title ?? "report").replace(/[^\w\-]+/g, "_")}.csv`; + anchor.click(); + URL.revokeObjectURL(url); + } + + function exportJson() { + const fileName = `${slugify(meta.title ?? "report") || "report"}.json`; + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json;charset=utf-8;" }); + downloadBlob(blob, fileName); + } + + function choosePick(id, bucket, key, multi = false) { + const list = state.datasets[id].pickSelection[bucket]; + if (!multi) { + list.splice(0, list.length, key); + return; + } + + const index = list.indexOf(key); + if (index >= 0) { + list.splice(index, 1); + } else { + list.push(key); + } + } + + function moveColumns(id, direction) { + const datasetState = state.datasets[id]; + const view = datasetViews.value[id]; + if (!view) { + return; + } + + if (direction === "add") { + for (const key of datasetState.pickSelection.available) { + if (!datasetState.visibleKeys.includes(key)) { + datasetState.visibleKeys.push(key); + } + } + datasetState.pickSelection.available = []; + return; + } + + if (direction === "remove") { + datasetState.visibleKeys = datasetState.visibleKeys.filter((key) => !datasetState.pickSelection.visible.includes(key)); + if (datasetState.visibleKeys.length === 0) { + datasetState.visibleKeys = view.dataset.columns.slice(0, 1).map((column) => column.key); + } + datasetState.pickSelection.visible = []; + } + } + + function moveVisibleColumnOrder(id, direction) { + const datasetState = state.datasets[id]; + const selectedKey = datasetState.pickSelection.visible[0]; + if (!selectedKey) { + return; + } + + const currentIndex = datasetState.visibleKeys.indexOf(selectedKey); + if (currentIndex < 0) { + return; + } + + const targetIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1; + if (targetIndex < 0 || targetIndex >= datasetState.visibleKeys.length) { + return; + } + + const next = [...datasetState.visibleKeys]; + [next[currentIndex], next[targetIndex]] = [next[targetIndex], next[currentIndex]]; + datasetState.visibleKeys = next; + } + + function moveAllColumns(id, mode) { + const dataset = datasets.value.find((item) => item.id === id); + if (!dataset) { + return; + } + + if (mode === "all") { + state.datasets[id].visibleKeys = dataset.columns.map((column) => column.key); + return; + } + + state.datasets[id].visibleKeys = dataset.columns.slice(0, 1).map((column) => column.key); + } + + function visibleColumnSummary(id) { + return datasetViews.value[id]?.shownColumns ?? []; + } + + function selectedRowsCount(id) { + return state.datasets[id]?.selectedRows?.length ?? 0; + } + + function copySelectedCell(id) { + const datasetState = state.datasets[id]; + const selected = datasetState?.selectedCell; + const view = datasetViews.value[id]; + if (!selected || !view) { + return; + } + + if (datasetState.selectedCells?.length > 1) { + const rowIds = [...new Set(datasetState.selectedCells.map((item) => item.split("::")[0]))]; + const colKeys = view.visibleColumns + .map((column) => column.key) + .filter((key) => datasetState.selectedCells.some((item) => item.endsWith(`::${key}`))); + const body = rowIds.map((rowId) => { + const entry = view.rows.find((item) => item.rowId === rowId); + return colKeys.map((key) => displayValue(getValue(entry?.raw, key))).join("\t"); + }); + copyText(body.join("\n"), txt("Selecao copiada", "Selection copied"), ui); + return; + } + + const entry = view.rows.find((item) => item.rowId === selected.rowId); + if (!entry) { + return; + } + + copyText(displayValue(getValue(entry.raw, selected.key)), txt("Celula copiada", "Cell copied"), ui); + } + + function copySelectedRows(id) { + const selected = state.datasets[id]?.selectedRows ?? []; + const view = datasetViews.value[id]; + if (!view || selected.length === 0) { + return; + } + + const rows = view.rows.filter((entry) => selected.includes(entry.rowId)); + if (rows.length === 0) { + return; + } + + const headers = view.visibleColumns.map((column) => column.label); + const body = rows.map((entry) => view.visibleColumns.map((column) => displayValue(getValue(entry.raw, column.key))).join("\t")); + copyText([headers.join("\t"), ...body].join("\n"), txt("Linhas copiadas", "Rows copied"), ui); + } + + function openCellMenu(event, id, rowId, key) { + event.preventDefault(); + selectCell(id, rowId, key); + const view = datasetViews.value[id]; + const entry = view?.rows.find((item) => item.rowId === rowId); + const value = entry ? getValue(entry.raw, key) : ""; + ui.contextMenu = { + type: "cell", + datasetId: id, + rowId, + key, + value, + x: event.clientX, + y: event.clientY + }; + } + + function openHeaderMenu(event, id, key) { + event.preventDefault(); + closeContextMenu(); + ui.contextMenu = { + type: "header", + datasetId: id, + key, + x: event.clientX, + y: event.clientY + }; + } + + function contextFilterByValue() { + const menu = ui.contextMenu; + if (!menu) { + return; + } + + state.datasets[menu.datasetId].filters.push({ + key: menu.key, + operator: "eq", + value: displayValue(menu.value) === DEFAULT_EMPTY ? "" : stringifyValue(menu.value) + }); + state.datasets[menu.datasetId].page = 1; + closeContextMenu(); + } + + function contextHideColumn() { + const menu = ui.contextMenu; + if (!menu) { + return; + } + + const datasetState = state.datasets[menu.datasetId]; + datasetState.visibleKeys = datasetState.visibleKeys.filter((item) => item !== menu.key); + if (datasetState.visibleKeys.length === 0) { + datasetState.visibleKeys = [menu.key]; + } + closeContextMenu(); + } + + function contextShowOnlyColumn() { + const menu = ui.contextMenu; + if (!menu) { + return; + } + + state.datasets[menu.datasetId].visibleKeys = [menu.key]; + closeContextMenu(); + } + + function copyContextValue() { + const menu = ui.contextMenu; + if (!menu) { + return; + } + + copyText(displayValue(menu.value), txt("Celula copiada", "Cell copied"), ui); + closeContextMenu(); + } + + function togglePinnedColumn() { + const menu = ui.contextMenu; + if (!menu) { + return; + } + + const datasetState = state.datasets[menu.datasetId]; + datasetState.pinnedColumnKey = datasetState.pinnedColumnKey === menu.key ? "" : menu.key; + closeContextMenu(); + } + + function isPinnedColumn(id, key) { + return state.datasets[id]?.pinnedColumnKey === key; + } + + function contextSort(direction, mode = "primary") { + const menu = ui.contextMenu; + if (!menu) { + return; + } + + const datasetState = state.datasets[menu.datasetId]; + const dir = direction === "desc" ? "desc" : "asc"; + const nextRule = { key: menu.key, dir }; + datasetState.sorts = mode === "secondary" + ? dedupeSortRules([...datasetState.sorts.filter((item) => item.key !== menu.key), nextRule]) + : dedupeSortRules([nextRule, ...datasetState.sorts.filter((item) => item.key !== menu.key)]); + syncPrimarySort(datasetState); + datasetState.page = 1; + closeContextMenu(); + } + + function getColumnWidth(id, key) { + const width = state.datasets[id]?.columnWidths?.[key]; + return clampColumnWidth(width); + } + + function columnStyle(id, key) { + const width = getColumnWidth(id, key); + return { + width: `${width}px`, + minWidth: `${MIN_COLUMN_WIDTH}px` + }; + } + + function columnStickyClass(id, key) { + return { "sticky-main-col": isPinnedColumn(id, key) }; + } + + function columnStickyStyle(id, key) { + return isPinnedColumn(id, key) ? { left: "28px" } : {}; + } + + function startResize(event, id, key) { + if (event.button !== 0) { + return; + } + + const startWidth = getColumnWidth(id, key); + ui.resizing = { + id, + key, + startX: event.clientX, + startWidth + }; + + const onMove = (moveEvent) => { + if (!ui.resizing || ui.resizing.id !== id || ui.resizing.key !== key) { + return; + } + + const nextWidth = ui.resizing.startWidth + (moveEvent.clientX - ui.resizing.startX); + state.datasets[id].columnWidths[key] = clampColumnWidth(nextWidth); + }; + + const onUp = () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + ui.resizing = null; + }; + + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + event.preventDefault(); + event.stopPropagation(); + } + + function autoFitColumn(id, key) { + const view = datasetViews.value[id]; + if (!view) { + return; + } + + const column = view.visibleColumns.find((item) => item.key === key); + if (!column) { + return; + } + + const sample = [column.label, ...view.rows.slice(0, 120).map((entry) => displayValue(getValue(entry.raw, key)))]; + const contentWidth = Math.max(...sample.map((value) => estimateColumnWidth(value, column.kind))); + state.datasets[id].columnWidths[key] = clampColumnWidth(contentWidth, defaultColumnWidth(column.kind)); + } + + function saveCustomPreset(id) { + const datasetState = state.datasets[id]; + const name = (window.prompt(txt("Nome do preset", "Preset name"), datasetState.activeCustomPreset || txt("Meu preset", "My preset")) ?? "").trim(); + if (!name) { + return; + } + + const preset = { + name, + visibleKeys: [...datasetState.visibleKeys], + sort: { ...datasetState.sort }, + sorts: datasetState.sorts.map((item) => ({ ...item })), + size: datasetState.size, + columnWidths: { ...datasetState.columnWidths }, + pinnedColumnKey: datasetState.pinnedColumnKey || "", + filters: [...datasetState.filters] + }; + + datasetState.customPresets = [...datasetState.customPresets.filter((item) => item.name !== name), preset]; + datasetState.activeCustomPreset = name; + } + + function applyCustomPreset(id, name) { + const datasetState = state.datasets[id]; + const dataset = datasets.value.find((item) => item.id === id); + const preset = datasetState.customPresets.find((item) => item.name === name); + if (!preset || !dataset) { + return; + } + + const normalized = normalizePreset(dataset, preset); + datasetState.visibleKeys = [...normalized.visibleKeys]; + datasetState.sorts = [...normalized.sorts]; + syncPrimarySort(datasetState); + datasetState.size = normalized.size; + datasetState.columnWidths = { ...datasetState.columnWidths, ...normalized.columnWidths }; + datasetState.pinnedColumnKey = normalized.pinnedColumnKey || ""; + datasetState.filters = [...normalized.filters]; + datasetState.page = 1; + datasetState.activeCustomPreset = normalized.name; + } + + function deleteCustomPreset(id, name) { + const datasetState = state.datasets[id]; + datasetState.customPresets = datasetState.customPresets.filter((item) => item.name !== name); + if (datasetState.activeCustomPreset === name) { + datasetState.activeCustomPreset = ""; + } + } + + function exportCustomPresets(id) { + const datasetState = state.datasets[id]; + if (!datasetState || datasetState.customPresets.length === 0) { + return; + } + + const blob = new Blob([JSON.stringify(datasetState.customPresets, null, 2)], { type: "application/json;charset=utf-8;" }); + downloadBlob(blob, `${slugify(`${meta.title ?? "report"}-${id}-presets`) || "report-presets"}.json`); + } + + function importCustomPresets(id) { + const input = document.createElement("input"); + input.type = "file"; + input.accept = "application/json,.json"; + input.onchange = async () => { + const file = input.files?.[0]; + if (!file) { + return; + } + + try { + const text = await file.text(); + const imported = JSON.parse(text); + if (!Array.isArray(imported)) { + throw new Error("invalid"); + } + + const dataset = datasets.value.find((item) => item.id === id); + if (!dataset) { + return; + } + + const normalized = imported + .filter((item) => item && typeof item.name === "string") + .map((item) => normalizePreset(dataset, item)); + const datasetState = state.datasets[id]; + datasetState.customPresets = dedupePresets([...datasetState.customPresets, ...normalized]); + showToast(txt("Presets importados", "Presets imported"), ui); + } catch { + showToast(txt("Falha ao importar presets", "Could not import presets"), ui); + } + }; + input.click(); + } + + function quickFilterValues(id) { + const view = datasetViews.value[id]; + const datasetState = state.datasets[id]; + if (!view || view.rows.length === 0) { + return []; + } + + const activeKey = datasetState.selectedCell?.key || datasetState.pinnedColumnKey || view.visibleColumns[0]?.key; + const column = view.visibleColumns.find((item) => item.key === activeKey); + if (!column) { + return []; + } + + const counts = new Map(); + for (const entry of view.rows.slice(0, 200)) { + const value = stringifyValue(getValue(entry.raw, activeKey)).trim(); + if (!value) { + continue; + } + counts.set(value, (counts.get(value) ?? 0) + 1); + } + + return [...counts.entries()] + .sort((left, right) => right[1] - left[1]) + .slice(0, 5) + .map(([value, count]) => ({ key: activeKey, value, count, label: column.label })); + } + + function addQuickFilter(id, key, value) { + state.datasets[id].filters.push({ key, operator: "eq", value }); + state.datasets[id].page = 1; + } + + function applyPreset(id, preset) { + const dataset = datasets.value.find((item) => item.id === id); + const datasetState = state.datasets[id]; + if (!dataset || !datasetState) { + return; + } + + const textColumns = dataset.columns.filter((column) => column.kind === "text"); + const compactColumns = dataset.columns.filter((column) => column.kind !== "text"); + if (preset === "summary") { + datasetState.visibleKeys = [...compactColumns.slice(0, 4), ...textColumns.slice(0, 3)].slice(0, 7).map((column) => column.key); + datasetState.size = 10; + } else if (preset === "audit") { + datasetState.visibleKeys = dataset.columns.map((column) => column.key); + datasetState.size = 25; + } else if (preset === "detailed") { + datasetState.visibleKeys = dataset.columns.map((column) => column.key); + datasetState.size = 10; + for (const column of textColumns) { + datasetState.columnWidths[column.key] = Math.max(datasetState.columnWidths[column.key] ?? 0, 320); + } + } + datasetState.page = 1; + datasetState.activePreset = preset; + } + + function updateScrollHint(id, event) { + const element = event?.currentTarget ?? event?.target; + if (!element) { + return; + } + + const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth); + ui.scrollHints[id] = { + left: element.scrollLeft > 8, + right: element.scrollLeft < maxScrollLeft - 8 + }; + } + + function scrollClass(id) { + const hint = ui.scrollHints[id] ?? { left: false, right: true }; + return { + "has-left-shadow": !!hint.left, + "has-right-shadow": !!hint.right + }; + } + + function activeSortSummary(id) { + const view = datasetViews.value[id]; + const datasetState = state.datasets[id]; + if (!view || !datasetState) { + return []; + } + + return datasetState.sorts + .map((sortRule, index) => { + const column = view.dataset.columns.find((item) => item.key === sortRule.key); + return column + ? { + index, + key: sortRule.key, + dir: sortRule.dir, + label: column.label + } + : null; + }) + .filter(Boolean); + } + + function isPageBusy(id) { + return !!state.datasets[id]?.pageBusy; + } + + function activePendingColumn() { + const dataset = activeDataset(); + const datasetState = activeDatasetState(); + if (!dataset || !datasetState) { + return null; + } + + return dataset.columns.find((item) => item.key === datasetState.pendingFilter.key) ?? null; + } + + function filterUsesRange(kind, operator) { + return (kind === "number" || kind === "date") && operator === "between"; + } + + function filterInputType(kind) { + if (kind === "number") { + return "number"; + } + + if (kind === "date") { + return "date"; + } + + return "text"; + } + + return proxyRefs({ + payload, + meta, + txt, + state, + ui, + datasets, + datasetViews, + overviewCards, + themeIcon, + footerText, + activeDataset, + activeDatasetState, + toggleTheme, + toggleSection, + resetDataset, + clearFilters, + removeFilter, + openFilterModal, + openSortModal, + openColumnsModal, + addFilter, + applySort, + clearSort, + setSortFromHeader, + toggleRow, + selectCell, + selectCellWithEvent, + isCellSelected, + isRowSelected, + changePage, + pageJump, + openCell, + isLongText, + copySql, + copySummary, + exportCsv, + exportJson, + choosePick, + moveColumns, + moveAllColumns, + moveVisibleColumnOrder, + visibleColumnSummary, + selectedRowsCount, + copySelectedCell, + copySelectedRows, + openCellMenu, + openHeaderMenu, + contextFilterByValue, + contextHideColumn, + contextShowOnlyColumn, + copyContextValue, + contextSort, + togglePinnedColumn, + isPinnedColumn, + getColumnWidth, + columnStyle, + columnStickyClass, + columnStickyStyle, + startResize, + autoFitColumn, + applyPreset, + saveCustomPreset, + applyCustomPreset, + deleteCustomPreset, + exportCustomPresets, + importCustomPresets, + quickFilterValues, + addQuickFilter, + updateScrollHint, + scrollClass, + activeSortSummary, + activePendingColumn, + filterUsesRange, + filterInputType, + isPageBusy, + addSortRule, + removeSortRule, + updateSortRuleKey, + updateSortRuleDirection, + openModal, + closeModal, + closeContextMenu, + summaryChipText: formatSummaryChip, + columnTone, + allowedOperators, + operatorLabel, + getValue, + displayValue, + stringifyValue, + PAGE_SIZES, + DEFAULT_EMPTY + }); +} + +function buildDatasets(payload, txt) { + const sections = []; + const resultColumns = buildResultColumns(payload.rows ?? [], payload.schema ?? []); + sections.push({ + id: "results", + title: txt("Resultados", "Results"), + searchPlaceholder: txt("Buscar resultados", "Search results"), + columns: resultColumns, + rows: payload.rows ?? [], + preparedRows: prepareDatasetRows(payload.rows ?? [], resultColumns), + collapsed: false + }); + + if ((payload.schema ?? []).length > 0) { + sections.push({ + id: "schema", + title: txt("Colunas e schema", "Columns and schema"), + searchPlaceholder: txt("Buscar schema", "Search schema"), + columns: [ + { key: "name", label: txt("Nome", "Name"), kind: "text" }, + { key: "kind", label: txt("Tipo", "Type"), kind: "text" }, + { key: "nullCount", label: txt("Nulos", "Nulls"), kind: "number" }, + { key: "distinctCount", label: txt("Distintos", "Distinct"), kind: "number" }, + { key: "example", label: txt("Exemplo", "Example"), kind: "text" }, + { key: "minValue", label: txt("Minimo", "Minimum"), kind: "text" }, + { key: "maxValue", label: txt("Maximo", "Maximum"), kind: "text" } + ], + rows: (payload.schema ?? []).map((item) => ({ + name: item.name ?? item.Name ?? "", + kind: item.kind ?? item.Kind ?? "", + nullCount: item.nullCount ?? item.NullCount ?? 0, + distinctCount: item.distinctCount ?? item.DistinctCount ?? 0, + example: item.example ?? item.Example ?? "", + minValue: item.minValue ?? item.MinValue ?? "", + maxValue: item.maxValue ?? item.MaxValue ?? "" + })), + preparedRows: [], + collapsed: true + }); + } + + for (const entry of [ + ["metadata", txt("Metadados", "Metadata"), txt("Buscar metadados", "Search metadata"), payload.metadata ?? []], + ["lineageNodes", txt("Nos de linhagem", "Lineage nodes"), txt("Buscar nos", "Search nodes"), payload.lineageNodes ?? []], + ["lineageConnections", txt("Conexoes de linhagem", "Lineage connections"), txt("Buscar conexoes", "Search connections"), payload.lineageConnections ?? []] + ]) { + if (entry[3].length > 0) { + sections.push({ + id: entry[0], + title: entry[1], + searchPlaceholder: entry[2], + columns: buildColumnsFromRows(entry[3]), + rows: entry[3], + preparedRows: [], + collapsed: true + }); + } + } + + return sections; +} + +function loadState(datasetDefs) { + const stored = readStoredState(); + const base = { + theme: stored.theme === "light" ? "light" : "dark", + sections: { + overview: false, + sql: true + }, + datasets: {} + }; + + for (const dataset of datasetDefs) { + const previous = stored.datasets?.[dataset.id] ?? {}; + base.sections[dataset.id] = typeof previous.collapsed === "boolean" ? previous.collapsed : !!dataset.collapsed; + base.datasets[dataset.id] = normalizeDatasetState(dataset, previous); + } + + return base; +} + +function readStoredState() { + try { + return JSON.parse(window.localStorage.getItem(STORAGE_KEY) ?? "{}"); + } catch { + return {}; + } +} + +function saveState(appState) { + const serializable = { + theme: appState.theme, + sections: JSON.parse(JSON.stringify(appState.sections)), + datasets: JSON.parse(JSON.stringify(appState.datasets)) + }; + + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(serializable)); +} + +function normalizeDatasetState(dataset, input) { + const safeColumns = sanitizeColumns(dataset.columns); + dataset.columns = safeColumns; + const availableKeys = safeColumns.map((column) => column.key); + const visibleKeys = Array.isArray(input.visibleKeys) && input.visibleKeys.length > 0 + ? input.visibleKeys.filter((key) => availableKeys.includes(key)) + : [...availableKeys]; + + const normalized = { + search: typeof input.search === "string" ? input.search : "", + filters: Array.isArray(input.filters) ? input.filters.filter((item) => availableKeys.includes(item.key)) : [], + sort: normalizeSort(input.sort, availableKeys), + sorts: normalizeSorts(input.sorts, availableKeys, input.sort), + visibleKeys: visibleKeys.length > 0 ? visibleKeys : [...availableKeys], + page: normalizePositiveInt(input.page, 1), + pageBusy: false, + size: normalizePageSize(input.size), + inlineFiltersOpen: !!input.inlineFiltersOpen, + inlineColumnsOpen: !!input.inlineColumnsOpen, + selectedRows: Array.isArray(input.selectedRows) ? input.selectedRows.map(String) : [], + selectedCell: input.selectedCell && typeof input.selectedCell.key === "string" + ? { rowId: String(input.selectedCell.rowId ?? ""), key: input.selectedCell.key } + : null, + selectedCells: Array.isArray(input.selectedCells) ? input.selectedCells.map(String) : [], + anchorCell: input.anchorCell && typeof input.anchorCell.key === "string" + ? { rowId: String(input.anchorCell.rowId ?? ""), key: input.anchorCell.key } + : null, + columnWidths: normalizeColumnWidths(input.columnWidths, safeColumns), + pendingFilter: normalizePendingFilter(dataset, input.pendingFilter), + columnSearch: typeof input.columnSearch === "string" ? input.columnSearch : "", + activePreset: typeof input.activePreset === "string" ? input.activePreset : "", + activeCustomPreset: typeof input.activeCustomPreset === "string" ? input.activeCustomPreset : "", + lastRowSelection: typeof input.lastRowSelection === "string" ? input.lastRowSelection : "", + pinnedColumnKey: typeof input.pinnedColumnKey === "string" ? input.pinnedColumnKey : "", + customPresets: Array.isArray(input.customPresets) ? input.customPresets.filter((item) => item && typeof item.name === "string").map((item) => normalizePreset(dataset, item)) : [], + pickSelection: { + available: Array.isArray(input.pickSelection?.available) ? input.pickSelection.available.filter((key) => availableKeys.includes(key)) : [], + visible: Array.isArray(input.pickSelection?.visible) ? input.pickSelection.visible.filter((key) => availableKeys.includes(key)) : [] + } + }; + + syncPrimarySort(normalized); + return normalized; +} + +function normalizePendingFilter(dataset, pendingFilter) { + const firstColumn = dataset.columns[0]; + const key = dataset.columns.some((column) => column.key === pendingFilter?.key) ? pendingFilter.key : firstColumn?.key ?? ""; + const column = dataset.columns.find((item) => item.key === key) ?? firstColumn ?? { kind: "text" }; + const operator = allowedOperators(column.kind).includes(pendingFilter?.operator) ? pendingFilter.operator : defaultOperator(column.kind); + return { + key, + operator, + value: pendingFilter?.value ?? "", + valueTo: pendingFilter?.valueTo ?? "" + }; +} + +function normalizeColumnWidths(input, columns) { + const widths = {}; + const source = input && typeof input === "object" ? input : {}; + for (const column of columns) { + widths[column.key] = clampColumnWidth(source[column.key], defaultColumnWidth(column.kind)); + } + + return widths; +} + +function normalizeSort(sort, availableKeys) { + if (!sort || !availableKeys.includes(sort.key)) { + return { key: "", dir: "asc" }; + } + + return { + key: sort.key, + dir: sort.dir === "desc" ? "desc" : "asc" + }; +} + +function normalizeSorts(sorts, availableKeys, fallbackSort = null) { + const source = Array.isArray(sorts) && sorts.length > 0 ? sorts : (fallbackSort ? [fallbackSort] : []); + return dedupeSortRules( + source + .filter((item) => item && availableKeys.includes(item.key)) + .map((item) => ({ + key: item.key, + dir: item.dir === "desc" ? "desc" : "asc" + })) + ); +} + +function dedupeSortRules(sorts) { + const seen = new Set(); + const output = []; + for (const item of sorts) { + if (!item?.key || seen.has(item.key)) { + continue; + } + seen.add(item.key); + output.push({ key: item.key, dir: item.dir === "desc" ? "desc" : "asc" }); + } + return output; +} + +function syncPrimarySort(datasetState) { + datasetState.sort = datasetState.sorts[0] ? { ...datasetState.sorts[0] } : { key: "", dir: "asc" }; +} + +function normalizePositiveInt(value, fallback) { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function normalizePageSize(value) { + const size = normalizePositiveInt(value, 10); + return PAGE_SIZES.includes(size) ? size : 10; +} + +function normalizePreset(dataset, input) { + const availableKeys = sanitizeColumns(dataset.columns).map((column) => column.key); + return { + name: String(input.name ?? "").trim() || "Preset", + visibleKeys: Array.isArray(input.visibleKeys) ? input.visibleKeys.filter((key) => availableKeys.includes(key)) : [...availableKeys], + sort: normalizeSort(input.sort, availableKeys), + sorts: normalizeSorts(input.sorts, availableKeys, input.sort), + size: normalizePageSize(input.size), + columnWidths: normalizeColumnWidths(input.columnWidths, dataset.columns), + pinnedColumnKey: typeof input.pinnedColumnKey === "string" && availableKeys.includes(input.pinnedColumnKey) ? input.pinnedColumnKey : "", + filters: Array.isArray(input.filters) ? input.filters.filter((item) => availableKeys.includes(item.key)).map((item) => ({ ...item, valueTo: item.valueTo ?? "" })) : [] + }; +} + +function dedupePresets(presets) { + const map = new Map(); + for (const item of presets) { + if (item?.name) { + map.set(item.name, item); + } + } + return [...map.values()]; +} + +function buildResultColumns(rows, schema) { + const schemaDetails = schema + .filter((item) => item && typeof item === "object") + .map((item) => ({ + key: item.name ?? item.Name ?? "", + label: item.name ?? item.Name ?? "", + kind: normalizeKind(item.kind ?? item.Kind ?? inferKindFromRows(rows, item.name ?? item.Name ?? "")) + })); + + if (schemaDetails.length > 0) { + return sanitizeColumns(schemaDetails); + } + + return sanitizeColumns(buildColumnsFromRows(rows)); +} + +function buildColumnsFromRows(rows) { + if (!rows.length) { + return []; + } + + return Object.keys(rows[0] ?? {}).map((key) => ({ + key, + label: key, + kind: inferKindFromRows(rows, key) + })); +} + +function sanitizeColumns(columns) { + return (Array.isArray(columns) ? columns : []) + .filter((column) => column && typeof column === "object") + .map((column) => { + const key = String(column.key ?? column.name ?? column.Name ?? "").trim(); + const label = String(column.label ?? key).trim(); + const kind = normalizeKind(column.kind ?? "text"); + return key ? { key, label: label || key, kind } : null; + }) + .filter(Boolean); +} + +function inferKindFromRows(rows, key) { + for (const row of rows) { + const value = getValue(row, key); + if (value === null || value === undefined || value === "") { + continue; + } + + if (typeof value === "number") { + return "number"; + } + + if (typeof value === "boolean") { + return "boolean"; + } + + const text = String(value).trim(); + if (/^-?\d+([.,]\d+)?$/.test(text)) { + return "number"; + } + + if (/\d{4}-\d{2}-\d{2}|\d{2}\/\d{2}\/\d{4}/.test(text)) { + return "date"; + } + + if (/^(true|false|sim|nao|não)$/i.test(text)) { + return "boolean"; + } + + return "text"; + } + + return "text"; +} + +function normalizeKind(kind) { + const text = String(kind ?? "").toLowerCase(); + if (text.includes("date") || text.includes("time")) { + return "date"; + } + + if (text.includes("bool")) { + return "boolean"; + } + + if (text.includes("int") || text.includes("number") || text.includes("decimal") || text.includes("float")) { + return "number"; + } + + return "text"; +} + +function buildDatasetView(dataset, datasetState) { + const safeColumns = sanitizeColumns(dataset.columns); + dataset.columns = safeColumns; + const visibleColumns = safeColumns.filter((column) => datasetState.visibleKeys.includes(column.key)); + const effectiveVisibleColumns = visibleColumns.length > 0 ? visibleColumns : safeColumns.slice(0, 1); + const allRows = Array.isArray(dataset.preparedRows) && dataset.preparedRows.length === dataset.rows.length + ? dataset.preparedRows + : prepareDatasetRows(dataset.rows, safeColumns); + + const filteredRows = allRows.filter((entry) => matchesDataset(entry, dataset, datasetState)); + const sortedRows = sortRows(filteredRows, dataset, datasetState.sorts); + const size = normalizePageSize(datasetState.size); + const pageCount = Math.max(1, Math.ceil(sortedRows.length / size)); + const page = Math.min(Math.max(1, normalizePositiveInt(datasetState.page, 1)), pageCount); + datasetState.page = page; + datasetState.size = size; + const start = (page - 1) * size; + const pageRows = sortedRows.slice(start, start + size); + const sortColumn = safeColumns.find((column) => column.key === datasetState.sorts[0]?.key) ?? null; + if (effectiveVisibleColumns.length > 0 && !datasetState.visibleKeys.some((key) => effectiveVisibleColumns.some((column) => column.key === key))) { + datasetState.visibleKeys = effectiveVisibleColumns.map((column) => column.key); + } + + return { + dataset, + visibleColumns: effectiveVisibleColumns, + rows: sortedRows, + pageRows, + page, + pageCount, + totalRows: allRows.length, + filteredCount: sortedRows.length, + size, + sortColumn, + sortRules: datasetState.sorts, + availableColumns: safeColumns.filter((column) => !datasetState.visibleKeys.includes(column.key) && (column.label || column.key).toLowerCase().includes(datasetState.columnSearch.toLowerCase())), + shownColumns: safeColumns.filter((column) => datasetState.visibleKeys.includes(column.key)), + hasRows: sortedRows.length > 0 + }; +} + +function clampColumnWidth(value, fallback = MIN_COLUMN_WIDTH) { + const normalized = normalizePositiveInt(value, fallback); + return Math.max(MIN_COLUMN_WIDTH, normalized); +} + +function defaultColumnWidth(kind) { + switch (kind) { + case "number": + return 160; + case "date": + return 180; + case "boolean": + return 150; + default: + return 240; + } +} + +function estimateColumnWidth(value, kind) { + const text = stringifyValue(value); + const maxChars = Math.min(Math.max(text.length, 8), kind === "text" ? 48 : 22); + const charWidth = kind === "number" ? 9 : 8.2; + return Math.ceil((maxChars * charWidth) + 48); +} + +function buildRowId(row, index) { + for (const key of ["id", "ID", "Id", "codigo", "Codigo", "key", "Key"]) { + const value = getValue(row, key); + if (value !== undefined && value !== null && value !== "") { + return `${key}:${String(value)}`; + } + } + + return `row:${index}`; +} + +function prepareDatasetRows(rows, columns) { + const safeColumns = sanitizeColumns(columns); + return (Array.isArray(rows) ? rows : []).map((row, index) => ({ + raw: row, + rowId: buildRowId(row, index), + searchText: safeColumns.map((column) => stringifyValue(getValue(row, column.key))).join(" ").toLowerCase() + })); +} + +function matchesDataset(entry, dataset, datasetState) { + const search = datasetState.search.trim().toLowerCase(); + if (search && !entry.searchText.includes(search)) { + return false; + } + + for (const filter of datasetState.filters) { + const column = dataset.columns.find((item) => item.key === filter.key); + if (!column) { + continue; + } + + if (!matchesFilter(getValue(entry.raw, column.key), column.kind, filter)) { + return false; + } + } + + return true; +} + +function sortRows(rows, dataset, sorts) { + const rules = Array.isArray(sorts) ? sorts : []; + if (rules.length === 0) { + return rows; + } + + return [...rows].sort((left, right) => { + for (const sort of rules) { + const column = dataset.columns.find((item) => item.key === sort.key); + if (!column) { + continue; + } + + const direction = sort.dir === "desc" ? -1 : 1; + const comparison = compareValues(getValue(left.raw, column.key), getValue(right.raw, column.key), column.kind) * direction; + if (comparison !== 0) { + return comparison; + } + } + + return 0; + }); +} + +function compareValues(left, right, kind) { + if (left === right) { + return 0; + } + + if (left === null || left === undefined || left === "") { + return 1; + } + + if (right === null || right === undefined || right === "") { + return -1; + } + + if (kind === "number") { + return parseNumeric(left) - parseNumeric(right); + } + + if (kind === "date") { + return parseDate(left) - parseDate(right); + } + + if (kind === "boolean") { + return Number(parseBoolean(left)) - Number(parseBoolean(right)); + } + + return stringifyValue(left).localeCompare(stringifyValue(right), undefined, { numeric: true, sensitivity: "base" }); +} + +function parseNumeric(value) { + if (typeof value === "number") { + return value; + } + + const normalized = String(value).replace(/\./g, "").replace(",", "."); + const parsed = Number.parseFloat(normalized); + return Number.isFinite(parsed) ? parsed : 0; +} + +function parseDate(value) { + if (value instanceof Date) { + return value.getTime(); + } + + const text = String(value).trim(); + if (/^\d{2}\/\d{2}\/\d{4}$/.test(text)) { + const [day, month, year] = text.split("/"); + return new Date(`${year}-${month}-${day}T00:00:00`).getTime(); + } + + const time = Date.parse(text); + return Number.isFinite(time) ? time : 0; +} + +function parseBoolean(value) { + if (typeof value === "boolean") { + return value; + } + + return /^(true|sim|yes|1)$/i.test(String(value)); +} + +export function getValue(row, key) { + if (!row || typeof row !== "object") { + return undefined; + } + + if (Object.prototype.hasOwnProperty.call(row, key)) { + return row[key]; + } + + const match = Object.keys(row).find((candidate) => candidate.toLowerCase() === String(key).toLowerCase()); + return match ? row[match] : undefined; +} + +export function stringifyValue(value) { + if (value === null || value === undefined || value === "") { + return ""; + } + + return String(value); +} + +export function displayValue(value) { + const text = stringifyValue(value); + return text || DEFAULT_EMPTY; +} + +export function allowedOperators(kind) { + if (kind === "number" || kind === "date") { + return ["between", "eq", "neq", "gt", "gte", "lt", "lte"]; + } + + if (kind === "boolean") { + return ["eq", "neq"]; + } + + return ["contains", "like", "regex", "eq", "neq", "starts", "ends"]; +} + +function defaultOperator(kind) { + return kind === "text" ? "contains" : kind === "number" || kind === "date" ? "between" : "eq"; +} + +export function operatorLabel(operator) { + return { + contains: "Contains", + like: "Like", + regex: "Regex", + between: "Between", + eq: "Equals", + neq: "Different", + gt: "Greater than", + gte: "Greater or equal", + lt: "Less than", + lte: "Less or equal", + starts: "Starts with", + ends: "Ends with" + }[operator] ?? operator; +} + +function matchesFilter(value, kind, filter) { + const left = stringifyValue(value); + const right = stringifyValue(filter.value); + const rightTo = stringifyValue(filter.valueTo); + + if (kind === "number") { + if (filter.operator === "between") { + const leftValue = parseNumeric(left); + return leftValue >= parseNumeric(right) && leftValue <= parseNumeric(rightTo); + } + return compareByOperator(parseNumeric(left), parseNumeric(right), filter.operator); + } + + if (kind === "date") { + if (filter.operator === "between") { + const leftValue = parseDate(left); + return leftValue >= parseDate(right) && leftValue <= parseDate(rightTo); + } + return compareByOperator(parseDate(left), parseDate(right), filter.operator); + } + + if (kind === "boolean") { + return compareByOperator(parseBoolean(left), parseBoolean(right), filter.operator); + } + + const leftLower = left.toLowerCase(); + const rightLower = right.toLowerCase(); + switch (filter.operator) { + case "contains": return leftLower.includes(rightLower); + case "starts": return leftLower.startsWith(rightLower); + case "ends": return leftLower.endsWith(rightLower); + case "like": return buildLikeRegex(right).test(left); + case "regex": + try { + return new RegExp(right, "i").test(left); + } catch { + return false; + } + case "eq": return leftLower === rightLower; + case "neq": return leftLower !== rightLower; + default: return true; + } +} + +function compareByOperator(left, right, operator) { + switch (operator) { + case "eq": return left === right; + case "neq": return left !== right; + case "gt": return left > right; + case "gte": return left >= right; + case "lt": return left < right; + case "lte": return left <= right; + default: return true; + } +} + +function buildLikeRegex(pattern) { + const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, "."); + return new RegExp(`^${escaped}$`, "i"); +} + +function csvEscape(value) { + const text = displayValue(value); + return /[,"\r\n]/.test(text) ? `"${text.replaceAll("\"", "\"\"")}"` : text; +} + +function downloadBlob(blob, fileName) { + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = fileName; + anchor.click(); + URL.revokeObjectURL(url); +} + +function copyText(text, message, ui = null) { + navigator.clipboard?.writeText(text).then(() => showToast(message, ui)).catch(() => showToast("Could not copy", ui)); +} + +function showToast(message, ui) { + if (!ui) { + return; + } + + window.clearTimeout(ui.toastTimer); + ui.toast = message; + ui.toastTimer = window.setTimeout(() => { + ui.toast = ""; + }, 1800); +} + +function slugify(value) { + return String(value ?? "") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +function buildOverviewCards(meta, payload, txt) { + const warnings = meta.warnings ?? []; + return [ + { label: txt("Linhas", "Rows"), value: new Intl.NumberFormat().format(Number((payload.rows ?? []).length)), icon: "rows" }, + { label: txt("Colunas", "Columns"), value: new Intl.NumberFormat().format(Number((payload.schema ?? []).length || buildColumnsFromRows(payload.rows ?? []).length)), icon: "columns" }, + { label: txt("Tempo", "Duration"), value: meta.duration ?? DEFAULT_EMPTY, icon: "clock" }, + { label: txt("Conexao", "Connection"), value: meta.connectionName ?? DEFAULT_EMPTY, icon: "database" }, + { label: txt("Gerado em", "Generated at"), value: meta.generatedAt ?? DEFAULT_EMPTY, icon: "clock" }, + { label: txt("Descricao", "Description"), value: meta.description || DEFAULT_EMPTY, icon: "clipboard", span: "wide" }, + { label: "Warnings", value: warnings.length > 0 ? warnings.join(" • ") : txt("Nenhum", "None"), icon: "alert", tone: warnings.length > 0 ? "warning" : "" } + ]; +} + +export function summaryChipText(filter) { + return `${filter.key} • ${operatorLabel(filter.operator)} • ${displayValue(filter.value)}`; +} + +export function columnTone(kind, value) { + if (value === null || value === undefined || value === "") { + return "is-null"; + } + + if (kind === "number") { + return "is-number"; + } + + if (kind === "date") { + return "is-date"; + } + + if (kind === "boolean") { + return "is-boolean"; + } + + return "is-text"; +} + +function formatSummaryChip(filter) { + if (filter.operator === "between") { + return `${filter.key} • ${operatorLabel(filter.operator)} • ${displayValue(filter.value)} → ${displayValue(filter.valueTo)}`; + } + + return `${filter.key} • ${operatorLabel(filter.operator)} • ${displayValue(filter.value)}`; +} diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/src/styles.css b/src/AkkornStudio.UI/Assets/ReportFrontend/src/styles.css new file mode 100644 index 00000000..b59f9562 --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/src/styles.css @@ -0,0 +1,1073 @@ +:root { + color-scheme: dark; + --bg: #0a101b; + --surface: #0f1728; + --surface-2: #141f35; + --surface-3: #1b2942; + --border: #2a3b5d; + --border-strong: #385180; + --text: #edf3ff; + --text-soft: #9eb0d2; + --text-strong: #f8fbff; + --accent: #6ea8fe; + --accent-strong: #88b4ff; + --accent-soft: rgba(110, 168, 254, 0.14); + --success: #72e2a1; + --warning: #f4c26b; + --danger: #ff8d94; + --shadow: 0 20px 60px rgba(3, 10, 21, 0.28); +} + +:root[data-theme="light"] { + color-scheme: light; + --bg: #f4f7fb; + --surface: #ffffff; + --surface-2: #f6f8fc; + --surface-3: #eef2f9; + --border: #d4dceb; + --border-strong: #bbc7dc; + --text: #162136; + --text-soft: #5e6d88; + --text-strong: #0d1728; + --accent: #356ef1; + --accent-strong: #275ad7; + --accent-soft: rgba(53, 110, 241, 0.12); + --success: #2f8f57; + --warning: #b26d00; + --danger: #d14255; + --shadow: 0 24px 54px rgba(23, 42, 79, 0.12); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Segoe UI", system-ui, sans-serif; + background: var(--bg); + color: var(--text); +} + +button, +input, +select { + font: inherit; + outline: none; +} + +button:focus, +input:focus, +select:focus, +button:focus-visible, +input:focus-visible, +select:focus-visible { + outline: none; +} + +.report-shell { + max-width: 1680px; + margin: 0 auto; + padding: 28px; +} + +.page-header, +.panel, +.mini-panel, +.modal-card, +.toast { + box-shadow: var(--shadow); +} + +.page-header, +.panel, +.mini-panel, +.modal-card, +.ghost-btn, +.collapse-btn, +.field-input, +.chip, +.pager, +.pick-item, +.icon-only { + border: 1px solid var(--border); +} + +.page-header, +.panel, +.mini-panel, +.modal-card { + background: rgba(15, 23, 40, 0.86); + backdrop-filter: blur(10px); +} + +:root[data-theme="light"] .page-header, +:root[data-theme="light"] .panel, +:root[data-theme="light"] .mini-panel, +:root[data-theme="light"] .modal-card { + background: rgba(255, 255, 255, 0.92); +} + +.page-header { + display: grid; + grid-template-columns: 260px minmax(320px, 1fr) auto; + gap: 24px; + align-items: center; + border-radius: 24px; + padding: 24px; +} + +.brand-wrap { + display: flex; + gap: 14px; + align-items: center; +} + +.brand-mark { + width: 48px; + height: 48px; + display: grid; + place-items: center; + border-radius: 16px; + background: var(--surface-2); + color: var(--accent); + font-weight: 800; + font-size: 1.15rem; + border: 1px solid var(--border); +} + +.brand-name { + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--text-soft); +} + +.brand-sub { + font-size: 0.95rem; + font-weight: 700; + color: var(--text-strong); +} + +.page-intro h1 { + margin: 0 0 10px; + font-size: clamp(1.4rem, 2vw, 2rem); + line-height: 1.1; +} + +.toolbar { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 10px; + align-items: center; +} + +.main-content { + display: grid; + gap: 18px; + margin-top: 20px; +} + +.panel { + border-radius: 24px; + padding: 18px; +} + +.section-head { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: center; + margin-bottom: 14px; +} + +.section-head h2 { + margin: 0; + font-size: 1.05rem; +} + +.section-copy { + margin-top: 6px; + font-size: 0.92rem; + color: var(--text-soft); +} + +.section-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-end; + align-items: center; +} + +.overview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; +} + +.overview-card { + display: flex; + gap: 14px; + align-items: start; + padding: 18px; + border-radius: 20px; + background: var(--surface); + border: 1px solid var(--border); +} + +.overview-card.is-wide { + grid-column: 1 / -1; +} + +.overview-card.is-warning { + border-color: rgba(244, 194, 107, 0.35); +} + +.overview-icon { + width: 42px; + height: 42px; + display: grid; + place-items: center; + border-radius: 14px; + background: var(--accent-soft); + color: var(--accent); +} + +.overview-label { + font-size: 0.78rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-soft); +} + +.overview-value { + margin-top: 8px; + font-size: 1rem; + font-weight: 700; + line-height: 1.45; +} + +.dataset-body { + display: grid; + gap: 14px; +} + +.summary-strip { + display: grid; + gap: 12px; +} + +.mini-panel { + border-radius: 18px; + padding: 14px; + background: var(--surface-2); +} + +.mini-panel-head { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; +} + +.mini-panel-title { + font-weight: 700; +} + +.mini-panel-actions { + display: flex; + gap: 8px; +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border-radius: 999px; + background: var(--surface); + color: var(--text-soft); +} + +.chip.empty { + opacity: 0.8; +} + +.chip.removable { + cursor: pointer; +} + +.table-shell { + position: relative; + display: grid; + gap: 12px; +} + +.table-shell.is-page-busy .table-scroll, +.table-shell.is-page-busy .pager { + opacity: 0.72; +} + +.table-loading { + position: absolute; + inset: 56px 0 72px 0; + display: grid; + place-items: center; + pointer-events: none; + z-index: 8; +} + +.table-loading-card { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 999px; + border: 1px solid var(--border); + background: rgba(15, 23, 40, 0.92); + color: var(--text); + box-shadow: var(--shadow); +} + +:root[data-theme="light"] .table-loading-card { + background: rgba(255, 255, 255, 0.95); +} + +.table-loading-spinner { + width: 14px; + height: 14px; + border-radius: 999px; + border: 2px solid var(--border-strong); + border-top-color: var(--accent); + animation: spin 0.8s linear infinite; +} + +.table-meta { + display: flex; + justify-content: space-between; + gap: 14px; + align-items: flex-start; + flex-wrap: wrap; +} + +.table-meta-main, +.preset-toolbar, +.preset-actions, +.preset-secondary, +.table-status, +.table-quick-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.table-meta-main { + flex: 1 1 420px; +} + +.preset-toolbar { + gap: 10px; +} + +.preset-segmented { + display: inline-flex; + flex-wrap: wrap; + gap: 4px; + padding: 4px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface-2); +} + +.segment-btn, +.icon-chip { + border: 0; + border-radius: 999px; + background: transparent; + color: var(--text-soft); + cursor: pointer; +} + +.segment-btn { + padding: 9px 12px; + font-weight: 700; +} + +.segment-btn:hover, +.icon-chip:hover { + background: var(--surface); + color: var(--text); +} + +.icon-chip { + width: 34px; + height: 34px; + display: inline-grid; + place-items: center; + border: 1px solid var(--border); + background: var(--surface); +} + +.quick-filters-wrap { + overflow-x: auto; + padding-bottom: 2px; +} + +.quick-filters { + display: flex; + flex-wrap: nowrap; + gap: 8px; + align-items: center; + min-width: max-content; +} + +.quick-filters-label { + color: var(--text-soft); + font-size: 0.86rem; + font-weight: 700; + position: sticky; + left: 0; + padding-right: 4px; + background: var(--surface); +} + +.quick-chip { + background: var(--surface-2); + border: 1px solid var(--border); + padding: 6px 10px; +} + +.quick-chip small { + opacity: 0.72; + padding: 2px 6px; + border-radius: 999px; + background: var(--surface); +} + +.result-count { + font-weight: 700; + color: var(--text-soft); + padding-inline: 2px; +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border-radius: 999px; + background: var(--surface-2); + color: var(--text-soft); + border: 1px solid var(--border); + font-size: 0.88rem; +} + +.pager-wrap { + display: flex; + flex-wrap: wrap; + gap: 14px; + align-items: center; + justify-content: flex-end; +} + +.pager-wrap-bottom { + padding-top: 4px; +} + +.page-size-wrap { + display: grid; + gap: 6px; +} + +.pager { + display: flex; + align-items: center; + gap: 8px; + border-radius: 16px; + padding: 8px; + background: var(--surface-2); +} + +.pager-readout { + min-width: 110px; + text-align: center; + color: var(--text-soft); + font-weight: 700; +} + +.pager-input { + width: 70px; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); +} + +.table-scroll { + position: relative; + overflow-x: auto; + overflow-y: hidden; + border-radius: 18px; + border: 1px solid var(--border); +} + +.table-scroll.has-left-shadow::before, +.table-scroll.has-right-shadow::after { + content: ""; + position: sticky; + top: 0; + bottom: 0; + width: 18px; + pointer-events: none; + z-index: 6; +} + +.table-scroll.has-left-shadow::before { + left: 0; + float: left; + background: linear-gradient(90deg, rgba(10, 16, 27, 0.7), transparent); +} + +.table-scroll.has-right-shadow::after { + right: 0; + float: right; + background: linear-gradient(270deg, rgba(10, 16, 27, 0.7), transparent); +} + +:root[data-theme="light"] .table-scroll.has-left-shadow::before { + background: linear-gradient(90deg, rgba(244, 247, 251, 0.9), transparent); +} + +:root[data-theme="light"] .table-scroll.has-right-shadow::after { + background: linear-gradient(270deg, rgba(244, 247, 251, 0.9), transparent); +} + +.report-table { + width: max-content; + min-width: 100%; + border-collapse: separate; + border-spacing: 0; + min-width: 920px; +} + +.report-table thead th { + background: var(--surface-2); + border-bottom: 1px solid var(--border); + padding: 0; + z-index: 1; +} + +.th-shell { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: stretch; +} + +.sort-head { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + border: 0; + background: transparent; + color: inherit; + padding: 14px; + font-weight: 700; + cursor: pointer; +} + +.resize-handle { + width: 28px; + min-width: 28px; + border: 0; + border-left: 1px solid var(--border); + background: transparent; + color: var(--text-soft); + cursor: col-resize; + display: grid; + place-items: center; +} + +.resize-handle:hover { + color: var(--accent); + background: var(--accent-soft); +} + +.resize-handle:active { + background: var(--surface-3); +} + +.report-table tbody td { + padding: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +:root[data-theme="light"] .report-table tbody td { + border-bottom-color: rgba(26, 35, 54, 0.08); +} + +.cell-body { + display: flex; + align-items: start; + gap: 10px; + padding: 13px 14px; +} + +.cell { + cursor: pointer; + vertical-align: top; +} + +.cell-text { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: anywhere; +} + +.cell-expand { + flex: 0 0 auto; + border: 0; + background: transparent; + color: var(--text-soft); + cursor: pointer; +} + +.select-col { + width: 28px; + min-width: 28px; + background: var(--surface-2); +} + +.select-col-width { + width: 28px; +} + +.sticky-select { + position: sticky; + left: 0; + z-index: 4; +} + +.sticky-main-col { + position: sticky; + left: 28px; + z-index: 3; + background: var(--surface); + box-shadow: 8px 0 18px rgba(5, 10, 18, 0.14); +} + +.report-table thead .sticky-main-col { + background: var(--surface-2); +} + +.row-selector { + width: 100%; + min-height: 100%; + min-width: 28px; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + color: var(--text-soft); +} + +.row-selector:hover { + color: var(--accent); +} + +.is-row-selected .row-selector { + color: var(--accent); +} + +.is-row-selected td { + background: rgba(110, 168, 254, 0.08); +} + +.is-selected-cell { + outline: 2px solid var(--accent); + outline-offset: -2px; + background: rgba(110, 168, 254, 0.12); +} + +.is-number .cell-text { + color: #7cc7ff; +} + +.is-date .cell-text { + color: #f4c26b; +} + +.is-boolean .cell-text { + color: #78dba5; + font-weight: 700; +} + +.is-null .cell-text { + color: var(--text-soft); + font-style: italic; +} + +.empty-state { + padding: 28px; + text-align: center; + color: var(--text-soft); +} + +.sql-box { + padding: 18px; + border-radius: 18px; + background: #0a1120; + border: 1px solid var(--border); + overflow: auto; +} + +.sql-box pre, +.cell-modal-text { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: anywhere; +} + +.page-footer { + margin: 18px 0 8px; + text-align: center; + color: var(--text-soft); +} + +.modal-shell { + position: fixed; + inset: 0; + display: grid; + place-items: center; + padding: 24px; + background: rgba(4, 10, 18, 0.66); + z-index: 20; +} + +.modal-card { + width: min(980px, 100%); + max-height: 88vh; + overflow: hidden; + border-radius: 22px; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.modal-card.modal-wide { + width: min(1180px, 100%); +} + +.modal-card.modal-cell { + width: min(1080px, 100%); +} + +.modal-head, +.modal-foot { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: center; + padding: 18px 20px; + border-bottom: 1px solid var(--border); +} + +.modal-foot { + border-top: 1px solid var(--border); + border-bottom: 0; + justify-content: flex-end; +} + +.modal-title { + font-size: 1.08rem; + font-weight: 800; +} + +.modal-body { + overflow: auto; + padding: 20px; +} + +.form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.field-span { + grid-column: 1 / -1; +} + +.field-stack { + display: grid; + gap: 8px; +} + +.field-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.84rem; + font-weight: 700; + color: var(--text-soft); +} + +.field-input { + width: 100%; + border-radius: 14px; + padding: 12px 14px; + background: var(--surface); + color: var(--text); +} + +.field-input.compact-search { + min-width: 220px; + border: 0; + background: transparent; + padding-left: 0; + padding-right: 0; +} + +.preset-select { + min-width: 180px; +} + +.sort-rules { + display: grid; + gap: 12px; +} + +.sort-rule { + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) auto; + gap: 12px; + align-items: end; + padding: 14px; + border: 1px solid var(--border); + border-radius: 16px; + background: var(--surface-2); +} + +.sort-remove { + align-self: end; +} + +.field-input:focus, +.field-input:focus-visible, +.compact-select:focus, +.compact-select:focus-visible, +.pager-input:focus, +.pager-input:focus-visible, +.compact-search:focus, +.compact-search:focus-visible { + outline: none; + box-shadow: none; + border-color: var(--border); +} + +.compact-select { + width: 84px; +} + +.search-field { + display: inline-flex; + align-items: center; + gap: 10px; + min-width: 260px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface); + color: var(--text-soft); +} + +.ghost-btn, +.collapse-btn, +.icon-only { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 14px; + padding: 11px 14px; + background: var(--surface); + color: var(--text); + cursor: pointer; +} + +.ghost-btn.small { + padding: 8px 10px; +} + +.icon-only { + width: 42px; + height: 42px; + padding: 0; +} + +.ghost-btn:disabled, +.icon-only:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.ghost-btn:hover:not(:disabled), +.collapse-btn:hover, +.icon-only:hover:not(:disabled), +.sort-head:hover, +.pick-item:hover { + border-color: var(--border-strong); + background: var(--surface-3); +} + +.picklist { + display: grid; + grid-template-columns: minmax(0, 1fr) 96px minmax(0, 1fr); + gap: 14px; + align-items: center; +} + +.pick-pane { + display: grid; + gap: 10px; +} + +.pick-list { + display: grid; + gap: 8px; + min-height: 320px; + max-height: 440px; + overflow: auto; + padding: 10px; + border: 1px solid var(--border); + border-radius: 18px; + background: var(--surface-2); +} + +.pick-item { + text-align: left; + border-radius: 12px; + padding: 12px 14px; + background: var(--surface); + color: var(--text); + cursor: pointer; +} + +.pick-item.active { + border-color: var(--accent); + background: var(--accent-soft); +} + +.pick-actions { + display: grid; + gap: 10px; +} + +.icon-glyph, +.icon-glyph svg { + width: 16px; + height: 16px; + display: inline-block; +} + +.toast { + position: fixed; + right: 24px; + bottom: 24px; + padding: 14px 18px; + border-radius: 16px; + background: var(--surface); + color: var(--text-strong); + z-index: 30; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.context-menu { + position: fixed; + min-width: 220px; + display: grid; + gap: 4px; + padding: 8px; + border-radius: 16px; + border: 1px solid var(--border-strong); + background: var(--surface); + box-shadow: var(--shadow); + z-index: 40; +} + +.context-item { + display: inline-flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 12px; + border: 0; + border-radius: 12px; + background: transparent; + color: var(--text); + cursor: pointer; +} + +.context-item:hover { + background: var(--surface-3); +} + +@media (max-width: 1160px) { + .page-header { + grid-template-columns: 1fr; + } + + .toolbar { + justify-content: flex-start; + } +} + +@media (max-width: 920px) { + .report-shell { + padding: 16px; + } + + .section-head, + .table-meta, + .modal-head, + .modal-foot { + flex-direction: column; + align-items: stretch; + } + + .section-actions { + justify-content: stretch; + } + + .form-grid, + .picklist, + .sort-rule { + grid-template-columns: 1fr; + } + + .field-span { + grid-column: auto; + } +} diff --git a/src/AkkornStudio.UI/Assets/ReportFrontend/vite.config.js b/src/AkkornStudio.UI/Assets/ReportFrontend/vite.config.js new file mode 100644 index 00000000..fe817316 --- /dev/null +++ b/src/AkkornStudio.UI/Assets/ReportFrontend/vite.config.js @@ -0,0 +1,32 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + plugins: [vue()], + define: { + "process.env.NODE_ENV": JSON.stringify("production"), + "process.env": "{}" + }, + build: { + emptyOutDir: true, + outDir: "dist", + cssCodeSplit: false, + lib: { + entry: "src/main.js", + name: "AkkornReportApp", + formats: ["iife"], + fileName: () => "report-app.js" + }, + rollupOptions: { + output: { + assetFileNames: (assetInfo) => { + if (assetInfo.name?.endsWith(".css")) { + return "report-app.css"; + } + + return "report-app.[ext]"; + } + } + } + } +}); diff --git a/src/DBWeaver.UI/Assets/Syntax/Sql.xshd b/src/AkkornStudio.UI/Assets/Syntax/Sql.xshd similarity index 100% rename from src/DBWeaver.UI/Assets/Syntax/Sql.xshd rename to src/AkkornStudio.UI/Assets/Syntax/Sql.xshd diff --git a/src/DBWeaver.UI/Assets/Themes/AppStyles.axaml b/src/AkkornStudio.UI/Assets/Themes/AppStyles.axaml similarity index 61% rename from src/DBWeaver.UI/Assets/Themes/AppStyles.axaml rename to src/AkkornStudio.UI/Assets/Themes/AppStyles.axaml index 088583fd..44f06dae 100644 --- a/src/DBWeaver.UI/Assets/Themes/AppStyles.axaml +++ b/src/AkkornStudio.UI/Assets/Themes/AppStyles.axaml @@ -1,459 +1,761 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Assets/Themes/DesignTokens.axaml b/src/AkkornStudio.UI/Assets/Themes/DesignTokens.axaml similarity index 98% rename from src/DBWeaver.UI/Assets/Themes/DesignTokens.axaml rename to src/AkkornStudio.UI/Assets/Themes/DesignTokens.axaml index 96ccf492..770176e9 100644 --- a/src/DBWeaver.UI/Assets/Themes/DesignTokens.axaml +++ b/src/AkkornStudio.UI/Assets/Themes/DesignTokens.axaml @@ -1,319 +1,319 @@ - - - - - - #090B14 - #0F1220 - #151A2C - #1C2338 - #252F49 - - #090B14 - #2A3554 - #1D2740 - - #101629 - #171E34 - #1E2740 - #243150 - #202D48 - #263556 - #2A3B5F - #7C96FF22 - - #E7ECFF - #AEB9D9 - #7F8AAE - #66708F - #0B0F1D - #8FA7FF - #8FA7FF - - #B9C6EA - #7E8BB1 - #616A86 - - #2A3554 - #334164 - #42527B - #6B8CFF - #2C3348 - - #5B7CFA - #6C8CFF - #4E6EE8 - #8A63F6 - #9A76FF - #7752DD - - #2FBF84 - #D9A441 - #E16174 - #4D9BFF - - #1A2031 - #252C40 - #7D87A5 - - - #5B7CFA - #4E6EE8 - #666B8CFF - #6C8CFF - #5B7CFA - #4E6EE8 - - - #5B7CFA - #EAF0FF - #1C2338 - #D0DAF5 - #1D2E52 - #CAE1FF - #153A2C - #C5F6E5 - #3A2A12 - #F8DEB0 - #41202A - #FFD2DB - #151A2C - #C5D2F0 - - - #4F78F4 - #6B93FF - #7E66E8 - #9C89FF - #B38B5E - #D0AC7A - #946EF0 - #B094FF - #D36A7B - #E48C99 - #8E9BC2 - #A8B4D6 - #7A87BC - #97A3CF - #B38B5E - #C8A275 - #7B90C7 - #9AB0E0 - #A486E8 - #B8A1F1 - #6F86D9 - #7EA6FF - - - #8FB9FF - #B98BFF - #D36A7B - #7AB6FF - #7E8CC3 - #97A4C8 - #CDA35A - #E1B24F - #73A7FF - #CDA35A - #A878FF - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Manrope,Sora,Plus Jakarta Sans,Segoe UI,Arial,sans-serif - Space Grotesk,Manrope,Segoe UI,Arial,sans-serif - JetBrainsMono Nerd Font,JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace - - 24 - 18 - 15 - 14 - 13 - 12 - 11 - 12 - 11 - - Bold - Bold - SemiBold - SemiBold - Medium - Normal - Normal - Normal - Medium - SemiBold - Medium - - 1.4 - 1.3 - 1.2 - 1.45 - 0 - - - 4 - 6 - 10 - 14 - 18 - 999 - 14,14,0,0 - 0,0,14,14 - - - 12 - 12,12,0,0 - 5 - - - 200 - 10 - 0.85 - 1 - 1 - 0.95 - 0.7 - 0.4 - 0.8 - 0.4 - - 4 - 10 - 14 - 18 - 24 - - 14 - 18,10 - 10,6 - 18,12 - - 0,18,0,0 - 18,18,0,0 - 0,10,0,0 - 0,10,0,10 - 0,0,10,10 - 0,0,22,18 - - - 0 0 0 0 #00000000 - 0 2 8 0 #00000033 - 0 6 18 0 #00000040 - 0 10 28 0 #00000052 - 0 16 40 0 #00000066 - 0 0 7 2 #AAFFFFFF - - - 0 4 24 0 #40000000, 0 1 4 0 #50000000 - 0 0 0 2 #6B8CFF, 0 8 32 0 #556B8CFF - 0 10 28 0 #00000052 - - + + + + + + #090B14 + #0F1220 + #151A2C + #1C2338 + #252F49 + + #090B14 + #2A3554 + #1D2740 + + #101629 + #171E34 + #1E2740 + #243150 + #202D48 + #263556 + #2A3B5F + #7C96FF22 + + #E7ECFF + #AEB9D9 + #7F8AAE + #66708F + #0B0F1D + #8FA7FF + #8FA7FF + + #B9C6EA + #7E8BB1 + #616A86 + + #2A3554 + #334164 + #42527B + #6B8CFF + #2C3348 + + #5B7CFA + #6C8CFF + #4E6EE8 + #8A63F6 + #9A76FF + #7752DD + + #2FBF84 + #D9A441 + #E16174 + #4D9BFF + + #1A2031 + #252C40 + #7D87A5 + + + #5B7CFA + #4E6EE8 + #666B8CFF + #6C8CFF + #5B7CFA + #4E6EE8 + + + #5B7CFA + #EAF0FF + #1C2338 + #D0DAF5 + #1D2E52 + #CAE1FF + #153A2C + #C5F6E5 + #3A2A12 + #F8DEB0 + #41202A + #FFD2DB + #151A2C + #C5D2F0 + + + #4F78F4 + #6B93FF + #7E66E8 + #9C89FF + #B38B5E + #D0AC7A + #946EF0 + #B094FF + #D36A7B + #E48C99 + #8E9BC2 + #A8B4D6 + #7A87BC + #97A3CF + #B38B5E + #C8A275 + #7B90C7 + #9AB0E0 + #A486E8 + #B8A1F1 + #6F86D9 + #7EA6FF + + + #8FB9FF + #B98BFF + #D36A7B + #7AB6FF + #7E8CC3 + #97A4C8 + #CDA35A + #E1B24F + #73A7FF + #CDA35A + #A878FF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Manrope,Sora,Plus Jakarta Sans,Segoe UI,Arial,sans-serif + Space Grotesk,Manrope,Segoe UI,Arial,sans-serif + JetBrainsMono Nerd Font,JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace + + 24 + 18 + 15 + 14 + 13 + 12 + 11 + 12 + 11 + + Bold + Bold + SemiBold + SemiBold + Medium + Normal + Normal + Normal + Medium + SemiBold + Medium + + 1.4 + 1.3 + 1.2 + 1.45 + 0 + + + 4 + 6 + 10 + 14 + 18 + 999 + 14,14,0,0 + 0,0,14,14 + + + 12 + 12,12,0,0 + 5 + + + 200 + 10 + 0.85 + 1 + 1 + 0.95 + 0.7 + 0.4 + 0.8 + 0.4 + + 4 + 10 + 14 + 18 + 24 + + 14 + 18,10 + 10,6 + 18,12 + + 0,18,0,0 + 18,18,0,0 + 0,10,0,0 + 0,10,0,10 + 0,0,10,10 + 0,0,22,18 + + + 0 0 0 0 #00000000 + 0 2 8 0 #00000033 + 0 6 18 0 #00000040 + 0 10 28 0 #00000052 + 0 16 40 0 #00000066 + 0 0 7 2 #AAFFFFFF + + + 0 4 24 0 #40000000, 0 1 4 0 #50000000 + 0 0 0 2 #6B8CFF, 0 8 32 0 #556B8CFF + 0 10 28 0 #00000052 + + diff --git a/src/DBWeaver.UI/Assets/Themes/user-theme.high-contrast.sample.json b/src/AkkornStudio.UI/Assets/Themes/user-theme.high-contrast.sample.json similarity index 96% rename from src/DBWeaver.UI/Assets/Themes/user-theme.high-contrast.sample.json rename to src/AkkornStudio.UI/Assets/Themes/user-theme.high-contrast.sample.json index f6da959f..9fafeea7 100644 --- a/src/DBWeaver.UI/Assets/Themes/user-theme.high-contrast.sample.json +++ b/src/AkkornStudio.UI/Assets/Themes/user-theme.high-contrast.sample.json @@ -1,37 +1,37 @@ -{ - "meta": { - "name": "High Contrast Verification", - "version": "1.0" - }, - "colors": { - "bg0": "#05070D", - "bg1": "#0A1220", - "bg2": "#13213A", - "bg3": "#1D2D4D", - "bg4": "#29416E", - "textPrimary": "#F5FAFF", - "textSecondary": "#B4C5DC", - "textMuted": "#8FA3BF", - "textDisabled": "#6C7F9D", - "textInverse": "#03050A", - "textAccent": "#9BB8FF", - "btnPrimaryBg": "#1D4ED8", - "btnPrimaryFg": "#FFFFFF", - "btnWarningBg": "#9A3412", - "btnWarningFg": "#FFF1E8" - }, - "typography": { - "uiFont": "Manrope,Segoe UI,Arial,sans-serif", - "nodeFont": "Space Grotesk,Manrope,Segoe UI,Arial,sans-serif", - "monoFont": "JetBrainsMono Nerd Font,JetBrains Mono,Cascadia Code,Consolas,monospace", - "displaySize": 24, - "headingSize": 18, - "titleSize": 15, - "nodeTitleSize": 14, - "labelSize": 13, - "bodySize": 12, - "captionSize": 11, - "monoBodySize": 12, - "monoSmallSize": 11 - } -} +{ + "meta": { + "name": "High Contrast Verification", + "version": "1.0" + }, + "colors": { + "bg0": "#05070D", + "bg1": "#0A1220", + "bg2": "#13213A", + "bg3": "#1D2D4D", + "bg4": "#29416E", + "textPrimary": "#F5FAFF", + "textSecondary": "#B4C5DC", + "textMuted": "#8FA3BF", + "textDisabled": "#6C7F9D", + "textInverse": "#03050A", + "textAccent": "#9BB8FF", + "btnPrimaryBg": "#1D4ED8", + "btnPrimaryFg": "#FFFFFF", + "btnWarningBg": "#9A3412", + "btnWarningFg": "#FFF1E8" + }, + "typography": { + "uiFont": "Manrope,Segoe UI,Arial,sans-serif", + "nodeFont": "Space Grotesk,Manrope,Segoe UI,Arial,sans-serif", + "monoFont": "JetBrainsMono Nerd Font,JetBrains Mono,Cascadia Code,Consolas,monospace", + "displaySize": 24, + "headingSize": 18, + "titleSize": 15, + "nodeTitleSize": 14, + "labelSize": 13, + "bodySize": 12, + "captionSize": 11, + "monoBodySize": 12, + "monoSmallSize": 11 + } +} diff --git a/src/DBWeaver.UI/Assets/Themes/user-theme.json b/src/AkkornStudio.UI/Assets/Themes/user-theme.json similarity index 96% rename from src/DBWeaver.UI/Assets/Themes/user-theme.json rename to src/AkkornStudio.UI/Assets/Themes/user-theme.json index cfc3fb23..0193680a 100644 --- a/src/DBWeaver.UI/Assets/Themes/user-theme.json +++ b/src/AkkornStudio.UI/Assets/Themes/user-theme.json @@ -1,37 +1,37 @@ -{ - "meta": { - "name": "Studio Dark", - "version": "1.0" - }, - "colors": { - "bg0": "#090B14", - "bg1": "#0F1220", - "bg2": "#151A2C", - "bg3": "#1C2338", - "bg4": "#252F49", - "textPrimary": "#E7ECFF", - "textSecondary": "#AEB9D9", - "textMuted": "#7F8AAE", - "textDisabled": "#66708F", - "textInverse": "#0B0F1D", - "textAccent": "#8FA7FF", - "btnPrimaryBg": "#2563EB", - "btnPrimaryFg": "#F8FAFC", - "btnWarningBg": "#7C2D12", - "btnWarningFg": "#FED7AA" - }, - "typography": { - "uiFont": "Manrope,Sora,Plus Jakarta Sans,Segoe UI,Arial,sans-serif", - "nodeFont": "Space Grotesk,Manrope,Segoe UI,Arial,sans-serif", - "monoFont": "JetBrainsMono Nerd Font,JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace", - "displaySize": 24, - "headingSize": 18, - "titleSize": 15, - "nodeTitleSize": 14, - "labelSize": 13, - "bodySize": 12, - "captionSize": 11, - "monoBodySize": 12, - "monoSmallSize": 11 - } -} +{ + "meta": { + "name": "Studio Dark", + "version": "1.0" + }, + "colors": { + "bg0": "#090B14", + "bg1": "#0F1220", + "bg2": "#151A2C", + "bg3": "#1C2338", + "bg4": "#252F49", + "textPrimary": "#E7ECFF", + "textSecondary": "#AEB9D9", + "textMuted": "#7F8AAE", + "textDisabled": "#66708F", + "textInverse": "#0B0F1D", + "textAccent": "#8FA7FF", + "btnPrimaryBg": "#2563EB", + "btnPrimaryFg": "#F8FAFC", + "btnWarningBg": "#7C2D12", + "btnWarningFg": "#FED7AA" + }, + "typography": { + "uiFont": "Manrope,Sora,Plus Jakarta Sans,Segoe UI,Arial,sans-serif", + "nodeFont": "Space Grotesk,Manrope,Segoe UI,Arial,sans-serif", + "monoFont": "JetBrainsMono Nerd Font,JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace", + "displaySize": 24, + "headingSize": 18, + "titleSize": 15, + "nodeTitleSize": 14, + "labelSize": 13, + "bodySize": 12, + "captionSize": 11, + "monoBodySize": 12, + "monoSmallSize": 11 + } +} diff --git a/src/DBWeaver.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml b/src/AkkornStudio.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml similarity index 83% rename from src/DBWeaver.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml rename to src/AkkornStudio.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml index 249e2124..b728c8a6 100644 --- a/src/DBWeaver.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml +++ b/src/AkkornStudio.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml @@ -1,231 +1,215 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml.cs b/src/AkkornStudio.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml.cs similarity index 84% rename from src/DBWeaver.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml.cs rename to src/AkkornStudio.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml.cs index 1d260d70..d6e0499c 100644 --- a/src/DBWeaver.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/AppDiagnostics/AppDiagnosticsControl.axaml.cs @@ -1,23 +1,23 @@ -using Avalonia.Controls; -using Avalonia.Input; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Controls; - -public sealed partial class AppDiagnosticsControl : UserControl -{ - public AppDiagnosticsControl() - { - InitializeComponent(); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (e.Key == Key.Escape && DataContext is AppDiagnosticsViewModel vm) - { - vm.CloseCommand.Execute(null); - e.Handled = true; - } - } -} +using Avalonia.Controls; +using Avalonia.Input; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Controls; + +public sealed partial class AppDiagnosticsControl : UserControl +{ + public AppDiagnosticsControl() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (e.Key == Key.Escape && DataContext is AppDiagnosticsViewModel vm) + { + vm.CloseCommand.Execute(null); + e.Handled = true; + } + } +} diff --git a/src/DBWeaver.UI/Controls/AutoJoin/AutoJoinOverlay.axaml b/src/AkkornStudio.UI/Controls/AutoJoin/AutoJoinOverlay.axaml similarity index 87% rename from src/DBWeaver.UI/Controls/AutoJoin/AutoJoinOverlay.axaml rename to src/AkkornStudio.UI/Controls/AutoJoin/AutoJoinOverlay.axaml index 644fd002..e1a66ab2 100644 --- a/src/DBWeaver.UI/Controls/AutoJoin/AutoJoinOverlay.axaml +++ b/src/AkkornStudio.UI/Controls/AutoJoin/AutoJoinOverlay.axaml @@ -1,255 +1,241 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/Benchmark/BenchmarkOverlay.axaml.cs b/src/AkkornStudio.UI/Controls/Benchmark/BenchmarkOverlay.axaml.cs similarity index 83% rename from src/DBWeaver.UI/Controls/Benchmark/BenchmarkOverlay.axaml.cs rename to src/AkkornStudio.UI/Controls/Benchmark/BenchmarkOverlay.axaml.cs index 0ba89f20..c6f05169 100644 --- a/src/DBWeaver.UI/Controls/Benchmark/BenchmarkOverlay.axaml.cs +++ b/src/AkkornStudio.UI/Controls/Benchmark/BenchmarkOverlay.axaml.cs @@ -1,23 +1,23 @@ -using Avalonia.Controls; -using Avalonia.Input; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Controls; - -public sealed partial class BenchmarkOverlay : UserControl -{ - public BenchmarkOverlay() - { - InitializeComponent(); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (e.Key == Key.Escape && DataContext is BenchmarkViewModel vm) - { - vm.CloseCommand.Execute(null); - e.Handled = true; - } - } -} +using Avalonia.Controls; +using Avalonia.Input; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Controls; + +public sealed partial class BenchmarkOverlay : UserControl +{ + public BenchmarkOverlay() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (e.Key == Key.Escape && DataContext is BenchmarkViewModel vm) + { + vm.CloseCommand.Execute(null); + e.Handled = true; + } + } +} diff --git a/src/DBWeaver.UI/Controls/CommandPalette/CommandPaletteControl.axaml b/src/AkkornStudio.UI/Controls/CommandPalette/CommandPaletteControl.axaml similarity index 77% rename from src/DBWeaver.UI/Controls/CommandPalette/CommandPaletteControl.axaml rename to src/AkkornStudio.UI/Controls/CommandPalette/CommandPaletteControl.axaml index 6979431c..da17f2d2 100644 --- a/src/DBWeaver.UI/Controls/CommandPalette/CommandPaletteControl.axaml +++ b/src/AkkornStudio.UI/Controls/CommandPalette/CommandPaletteControl.axaml @@ -1,178 +1,156 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/CommandPalette/CommandPaletteControl.axaml.cs b/src/AkkornStudio.UI/Controls/CommandPalette/CommandPaletteControl.axaml.cs similarity index 94% rename from src/DBWeaver.UI/Controls/CommandPalette/CommandPaletteControl.axaml.cs rename to src/AkkornStudio.UI/Controls/CommandPalette/CommandPaletteControl.axaml.cs index e7158470..e03ffa30 100644 --- a/src/DBWeaver.UI/Controls/CommandPalette/CommandPaletteControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/CommandPalette/CommandPaletteControl.axaml.cs @@ -1,106 +1,106 @@ -using System.ComponentModel; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.LogicalTree; -using Avalonia.Threading; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Controls; - -public sealed partial class CommandPaletteControl : UserControl -{ - private CommandPaletteViewModel? _vm; - - public CommandPaletteControl() - { - InitializeComponent(); - DataContextChanged += (_, _) => BindViewModel(); - - ItemsControl? list = this.FindControl("ResultsList"); - list?.AddHandler( - PointerPressedEvent, - OnResultPointerPressed, - Avalonia.Interactivity.RoutingStrategies.Bubble - ); - } - - private void BindViewModel() - { - if (_vm is not null) - _vm.PropertyChanged -= OnVmPropertyChanged; - - _vm = DataContext as CommandPaletteViewModel; - - if (_vm is not null) - _vm.PropertyChanged += OnVmPropertyChanged; - } - - private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(CommandPaletteViewModel.IsVisible) && _vm?.IsVisible == true) - Dispatcher.UIThread.Post(FocusSearch, DispatcherPriority.Input); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (DataContext is not CommandPaletteViewModel vm) - return; - - switch (e.Key) - { - case Key.Down: - vm.SelectNext(); - e.Handled = true; - break; - - case Key.Up: - vm.SelectPrev(); - e.Handled = true; - break; - - case Key.Return - or Key.Enter: - vm.ExecuteSelected(); - e.Handled = true; - break; - - case Key.Escape: - vm.Close(); - e.Handled = true; - break; - } - } - - private void OnResultPointerPressed(object? sender, PointerPressedEventArgs e) - { - if (DataContext is not CommandPaletteViewModel vm) - return; - if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - return; - - PaletteCommandItem? item = - (e.Source as Avalonia.LogicalTree.ILogical) - ?.GetLogicalAncestors() - .OfType() - .Select(c => c.DataContext) - .OfType() - .FirstOrDefault() - ?? (e.Source as Control)?.DataContext as PaletteCommandItem; - - if (item is null) - return; - - int idx = vm.Results.IndexOf(item); - if (idx >= 0) - vm.SelectedIndex = idx; - vm.ExecuteSelected(); - e.Handled = true; - } - - private void FocusSearch() - { - TextBox? input = this.FindControl("SearchInput"); - input?.Focus(); - } -} +using System.ComponentModel; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.LogicalTree; +using Avalonia.Threading; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Controls; + +public sealed partial class CommandPaletteControl : UserControl +{ + private CommandPaletteViewModel? _vm; + + public CommandPaletteControl() + { + InitializeComponent(); + DataContextChanged += (_, _) => BindViewModel(); + + ItemsControl? list = this.FindControl("ResultsList"); + list?.AddHandler( + PointerPressedEvent, + OnResultPointerPressed, + Avalonia.Interactivity.RoutingStrategies.Bubble + ); + } + + private void BindViewModel() + { + if (_vm is not null) + _vm.PropertyChanged -= OnVmPropertyChanged; + + _vm = DataContext as CommandPaletteViewModel; + + if (_vm is not null) + _vm.PropertyChanged += OnVmPropertyChanged; + } + + private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(CommandPaletteViewModel.IsVisible) && _vm?.IsVisible == true) + Dispatcher.UIThread.Post(FocusSearch, DispatcherPriority.Input); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (DataContext is not CommandPaletteViewModel vm) + return; + + switch (e.Key) + { + case Key.Down: + vm.SelectNext(); + e.Handled = true; + break; + + case Key.Up: + vm.SelectPrev(); + e.Handled = true; + break; + + case Key.Return + or Key.Enter: + vm.ExecuteSelected(); + e.Handled = true; + break; + + case Key.Escape: + vm.Close(); + e.Handled = true; + break; + } + } + + private void OnResultPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (DataContext is not CommandPaletteViewModel vm) + return; + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + PaletteCommandItem? item = + (e.Source as Avalonia.LogicalTree.ILogical) + ?.GetLogicalAncestors() + .OfType() + .Select(c => c.DataContext) + .OfType() + .FirstOrDefault() + ?? (e.Source as Control)?.DataContext as PaletteCommandItem; + + if (item is null) + return; + + int idx = vm.Results.IndexOf(item); + if (idx >= 0) + vm.SelectedIndex = idx; + vm.ExecuteSelected(); + e.Handled = true; + } + + private void FocusSearch() + { + TextBox? input = this.FindControl("SearchInput"); + input?.Focus(); + } +} diff --git a/src/DBWeaver.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml b/src/AkkornStudio.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml similarity index 83% rename from src/DBWeaver.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml rename to src/AkkornStudio.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml index c7824208..089e1757 100644 --- a/src/DBWeaver.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml +++ b/src/AkkornStudio.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml @@ -1,409 +1,362 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml.cs b/src/AkkornStudio.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml.cs similarity index 94% rename from src/DBWeaver.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml.cs rename to src/AkkornStudio.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml.cs index 6a0909cf..faa14ab3 100644 --- a/src/DBWeaver.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/ConnectionManager/ConnectionManagerControl.axaml.cs @@ -1,99 +1,99 @@ -using System.Linq; -using System.IO; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Platform.Storage; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Controls; - -public sealed partial class ConnectionManagerControl : UserControl -{ - public ConnectionManagerControl() - { - InitializeComponent(); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (e.Key == Key.Escape && DataContext is ConnectionManagerViewModel vm) - { - vm.CloseCommand.Execute(null); - e.Handled = true; - } - } - - private async void BrowseSqliteFile_Click(object? sender, RoutedEventArgs e) - { - if (DataContext is not ConnectionManagerViewModel vm) - return; - - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel?.StorageProvider is null) - return; - - var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = "Select SQLite database", - AllowMultiple = false, - FileTypeFilter = - [ - new FilePickerFileType("SQLite") - { - Patterns = ["*.sqlite", "*.sqlite3", "*.db", "*.db3"], - }, - FilePickerFileTypes.All, - ], - }); - - IStorageFile? selected = files.FirstOrDefault(); - if (selected is null) - return; - - string? localPath = selected.TryGetLocalPath(); - vm.EditDatabase = string.IsNullOrWhiteSpace(localPath) - ? selected.Path.LocalPath - : localPath; - } - - private async void CreateSqliteFile_Click(object? sender, RoutedEventArgs e) - { - if (DataContext is not ConnectionManagerViewModel vm) - return; - - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel?.StorageProvider is null) - return; - - IStorageFile? file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = "Create SQLite database", - SuggestedFileName = "database.sqlite3", - FileTypeChoices = - [ - new FilePickerFileType("SQLite") - { - Patterns = ["*.sqlite", "*.sqlite3", "*.db", "*.db3"], - }, - ], - }); - - if (file is null) - return; - - string? localPath = file.TryGetLocalPath(); - string targetPath = string.IsNullOrWhiteSpace(localPath) - ? file.Path.LocalPath - : localPath; - - if (!string.IsNullOrWhiteSpace(targetPath) && !File.Exists(targetPath)) - { - Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); - File.WriteAllBytes(targetPath, []); - } - - vm.EditDatabase = targetPath; - } -} +using System.Linq; +using System.IO; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Platform.Storage; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Controls; + +public sealed partial class ConnectionManagerControl : UserControl +{ + public ConnectionManagerControl() + { + InitializeComponent(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (e.Key == Key.Escape && DataContext is ConnectionManagerViewModel vm) + { + vm.CloseCommand.Execute(null); + e.Handled = true; + } + } + + private async void BrowseSqliteFile_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is not ConnectionManagerViewModel vm) + return; + + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel?.StorageProvider is null) + return; + + var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = "Select SQLite database", + AllowMultiple = false, + FileTypeFilter = + [ + new FilePickerFileType("SQLite") + { + Patterns = ["*.sqlite", "*.sqlite3", "*.db", "*.db3"], + }, + FilePickerFileTypes.All, + ], + }); + + IStorageFile? selected = files.FirstOrDefault(); + if (selected is null) + return; + + string? localPath = selected.TryGetLocalPath(); + vm.EditDatabase = string.IsNullOrWhiteSpace(localPath) + ? selected.Path.LocalPath + : localPath; + } + + private async void CreateSqliteFile_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is not ConnectionManagerViewModel vm) + return; + + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel?.StorageProvider is null) + return; + + IStorageFile? file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + { + Title = "Create SQLite database", + SuggestedFileName = "database.sqlite3", + FileTypeChoices = + [ + new FilePickerFileType("SQLite") + { + Patterns = ["*.sqlite", "*.sqlite3", "*.db", "*.db3"], + }, + ], + }); + + if (file is null) + return; + + string? localPath = file.TryGetLocalPath(); + string targetPath = string.IsNullOrWhiteSpace(localPath) + ? file.Path.LocalPath + : localPath; + + if (!string.IsNullOrWhiteSpace(targetPath) && !File.Exists(targetPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); + File.WriteAllBytes(targetPath, []); + } + + vm.EditDatabase = targetPath; + } +} diff --git a/src/DBWeaver.UI/Controls/Ddl/DdlExecuteDialogWindow.cs b/src/AkkornStudio.UI/Controls/Ddl/DdlExecuteDialogWindow.cs similarity index 96% rename from src/DBWeaver.UI/Controls/Ddl/DdlExecuteDialogWindow.cs rename to src/AkkornStudio.UI/Controls/Ddl/DdlExecuteDialogWindow.cs index aaf02241..4fd743c5 100644 --- a/src/DBWeaver.UI/Controls/Ddl/DdlExecuteDialogWindow.cs +++ b/src/AkkornStudio.UI/Controls/Ddl/DdlExecuteDialogWindow.cs @@ -1,414 +1,414 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; -using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.Media; -using DBWeaver.Core; -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.Services.Theming; - -namespace DBWeaver.UI.Controls.Ddl; - -public sealed class DdlExecuteDialogWindow : Window -{ - private readonly DdlExecuteDialogViewModel _vm; - private readonly Func> _executor; - private Button? _runButton; - private Button? _cancelButton; - private CheckBox? _stopOnErrorCheck; - private CheckBox? _confirmDestructiveCheck; - private TextBox? _resultBox; - private TextBlock? _summaryText; - private CancellationTokenSource? _runCts; - - public DdlExecuteDialogWindow( - DdlExecuteDialogViewModel vm, - Func> executor) - { - _vm = vm; - _executor = executor; - - Title = L("ddl.dialog.title", "Execute DDL"); - Width = 920; - Height = 680; - MinWidth = 740; - MinHeight = 520; - WindowStartupLocation = WindowStartupLocation.CenterOwner; - SystemDecorations = SystemDecorations.None; - ExtendClientAreaToDecorationsHint = true; - ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.NoChrome; - ExtendClientAreaTitleBarHeightHint = -1; - Background = new SolidColorBrush(Color.Parse(UiColorConstants.C_0D0F14)); - - KeyDown += OnKeyDown; - Content = BuildContent(); - } - - private Control BuildContent() - { - _runButton = new Button - { - Content = L("ddl.dialog.execute", "Execute"), - Padding = new Thickness(12, 6), - HorizontalAlignment = HorizontalAlignment.Right, - MinWidth = 110, - IsEnabled = _vm.CanExecute, - }; - _runButton.Click += async (_, _) => await RunAsync(); - - _cancelButton = new Button - { - Content = L("ddl.dialog.cancel", "Cancel"), - Padding = new Thickness(12, 6), - HorizontalAlignment = HorizontalAlignment.Right, - MinWidth = 110, - IsEnabled = false, - }; - _cancelButton.Click += (_, _) => _runCts?.Cancel(); - - Button closeButton = new() - { - Content = L("ddl.dialog.close", "Close"), - Padding = new Thickness(12, 6), - HorizontalAlignment = HorizontalAlignment.Right, - MinWidth = 110, - }; - closeButton.Click += (_, _) => Close(); - - _stopOnErrorCheck = new CheckBox - { - Content = L("ddl.dialog.stopOnError", "Stop on first failure"), - IsChecked = _vm.StopOnError, - VerticalAlignment = VerticalAlignment.Center, - }; - _stopOnErrorCheck.IsCheckedChanged += (_, _) => _vm.StopOnError = _stopOnErrorCheck.IsChecked ?? false; - - _confirmDestructiveCheck = new CheckBox - { - Content = L( - "ddl.dialog.confirmDestructive", - "I confirm execution of destructive statements (DROP TABLE)" - ), - IsVisible = _vm.HasDestructiveStatements, - IsChecked = _vm.ConfirmDestructiveExecution, - VerticalAlignment = VerticalAlignment.Center, - Foreground = new SolidColorBrush(Color.Parse(UiColorConstants.C_FCA5A5)), - }; - _confirmDestructiveCheck.IsCheckedChanged += (_, _) => - { - _vm.ConfirmDestructiveExecution = _confirmDestructiveCheck.IsChecked ?? false; - if (_runButton is not null) - _runButton.IsEnabled = _vm.CanExecute; - }; - - TextBox sqlPreview = new() - { - Text = _vm.SqlPreview, - IsReadOnly = true, - AcceptsReturn = true, - TextWrapping = TextWrapping.Wrap, - FontFamily = new FontFamily("JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace"), - MinHeight = 260, - }; - - _summaryText = new TextBlock - { - Text = L("ddl.dialog.reviewBeforeRun", "Review the DDL script before confirming."), - Foreground = new SolidColorBrush(Color.Parse(UiColorConstants.C_C8D0DC)), - FontSize = ResolveFontSize("FontSizeBody", 12), - }; - - ItemsControl statementList = new() - { - ItemsSource = _vm.StatementPreviews, - ItemTemplate = new FuncDataTemplate((item, _) => - new TextBlock - { - Text = item.CompactSql, - FontFamily = new FontFamily("JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace"), - FontSize = ResolveFontSize("FontSizeCaption", 11), - Margin = new Thickness(0, 1, 0, 1), - Foreground = item.IsDestructive - ? new SolidColorBrush(Color.Parse(UiColorConstants.C_FCA5A5)) - : new SolidColorBrush(Color.Parse(UiColorConstants.C_9CA3AF)), - } - ), - Margin = new Thickness(0, 6, 0, 0), - }; - - Border statementListHost = new() - { - BorderBrush = new SolidColorBrush(Color.Parse(UiColorConstants.C_1F2937)), - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(6), - Padding = new Thickness(8), - MaxHeight = 120, - Child = new ScrollViewer - { - Content = statementList, - VerticalScrollBarVisibility = ScrollBarVisibility.Auto, - HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, - }, - }; - - Button headerCloseButton = new() - { - Content = "×", - Padding = new Thickness(8, 4), - MinWidth = 32, - }; - headerCloseButton.Click += (_, _) => Close(); - - _resultBox = new TextBox - { - IsReadOnly = true, - AcceptsReturn = true, - TextWrapping = TextWrapping.Wrap, - FontFamily = new FontFamily("JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace"), - MinHeight = 200, - }; - - return new Border - { - Background = new SolidColorBrush(Color.Parse(UiColorConstants.C_070A12)), - Child = new Grid - { - RowDefinitions = new RowDefinitions("Auto,*,Auto"), - Children = - { - new Border - { - Background = ResolveBrush("Bg1Brush", UiColorConstants.C_0F1220), - BorderBrush = ResolveBrush("BorderSubtleBrush", UiColorConstants.C_1E2A3F), - BorderThickness = new Thickness(0, 0, 0, 1), - Padding = new Thickness(16, 12), - Child = new Grid - { - ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"), - Children = - { - new Border - { - Width = 28, - Height = 28, - CornerRadius = ResolveCornerRadius("RadiusSM", 6), - Background = ResolveBrush("BtnWarningBgBrush", UiColorConstants.C_10291A), - BorderBrush = ResolveBrush("StatusWarningBrush", UiColorConstants.C_FBBF24), - BorderThickness = new Thickness(1), - Child = new TextBlock - { - Text = "!", - FontWeight = ResolveFontWeight("FontWeightTitle", FontWeight.SemiBold), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - Foreground = ResolveBrush("StatusWarningBrush", UiColorConstants.C_FBBF24), - }, - }, - PlaceAtCol(new StackPanel - { - Margin = new Thickness(10, 0, 0, 0), - Spacing = 2, - Children = - { - new TextBlock - { - Text = L("ddl.dialog.title", "Execute DDL"), - FontSize = ResolveFontSize("FontSizeNodeTitle", 14), - FontWeight = ResolveFontWeight("FontWeightTitle", FontWeight.SemiBold), - Foreground = ResolveBrush("TextPrimaryBrush", UiColorConstants.C_E8EAED), - }, - new TextBlock - { - Text = L("ddl.dialog.subtitle", "Review and execute DDL changes safely"), - FontSize = ResolveFontSize("FontSizeCaption", 11), - Foreground = ResolveBrush("TextMutedBrush", UiColorConstants.C_9CA3AF), - }, - }, - }, 1), - PlaceAtCol(headerCloseButton, 2), - }, - }, - }, - PlaceAtRow(new ScrollViewer - { - VerticalScrollBarVisibility = ScrollBarVisibility.Auto, - HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, - Content = new Border - { - Padding = new Thickness(16, 12), - Child = new StackPanel - { - Spacing = 10, - Children = - { - new TextBlock - { - Text = L("ddl.dialog.confirmQuestion", "Confirm DDL execution on the connected database?"), - FontSize = ResolveFontSize("FontSizeHeading", 18), - FontWeight = ResolveFontWeight("FontWeightTitle", FontWeight.SemiBold), - Foreground = new SolidColorBrush(Color.Parse(UiColorConstants.C_E8EAED)), - }, - new TextBlock - { - Text = L("ddl.dialog.irreversibleWarning", "This action can change the schema irreversibly."), - Foreground = new SolidColorBrush(Color.Parse(UiColorConstants.C_FBBF24)), - FontSize = ResolveFontSize("FontSizeBody", 12), - }, - sqlPreview, - _summaryText, - statementListHost, - _resultBox, - }, - }, - }, - }, 1), - PlaceAtRow(new Border - { - Background = ResolveBrush("Bg1Brush", UiColorConstants.C_0F1220), - BorderBrush = ResolveBrush("BorderSubtleBrush", UiColorConstants.C_1E2A3F), - BorderThickness = new Thickness(0, 1, 0, 0), - Padding = new Thickness(16, 10), - Child = new Grid - { - ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto"), - Children = - { - new StackPanel - { - Spacing = 4, - Children = - { - _stopOnErrorCheck, - _confirmDestructiveCheck, - }, - }, - PlaceAtCol(_runButton, 1), - PlaceAtCol(_cancelButton, 2), - PlaceAtCol(closeButton, 3), - }, - }, - }, 2), - }, - }, - }; - } - - private static Control PlaceAtRow(Control control, int row) - { - Grid.SetRow(control, row); - return control; - } - - private static Control PlaceAtCol(Control control, int col) - { - Grid.SetColumn(control, col); - return control; - } - - private async Task RunAsync() - { - if (_runButton is null || _cancelButton is null || _stopOnErrorCheck is null || _resultBox is null || _summaryText is null || _confirmDestructiveCheck is null) - return; - - if (_vm.HasDestructiveStatements && !_vm.ConfirmDestructiveExecution) - { - _summaryText.Text = L("ddl.dialog.mustConfirmDestructive", "Confirm destructive execution to continue."); - return; - } - - _vm.IsExecuting = true; - _runButton.IsEnabled = false; - _cancelButton.IsEnabled = true; - _stopOnErrorCheck.IsEnabled = false; - _confirmDestructiveCheck.IsEnabled = false; - _summaryText.Text = L("ddl.dialog.executing", "Executing..."); - _runCts = new CancellationTokenSource(); - - try - { - DdlExecutionResult result = await _executor(_vm.StopOnError, _runCts.Token); - _vm.ApplyResult(result); - } - catch (OperationCanceledException) - { - _vm.ApplyCancelled(); - } - catch (Exception ex) - { - _vm.ApplyError(ex); - } - finally - { - _runCts?.Dispose(); - _runCts = null; - _vm.IsExecuting = false; - _runButton.IsEnabled = _vm.CanExecute; - _cancelButton.IsEnabled = false; - _stopOnErrorCheck.IsEnabled = true; - _confirmDestructiveCheck.IsEnabled = true; - _summaryText.Text = _vm.ResultSummary; - _resultBox.Text = _vm.ResultDetails; - } - } - - private void OnKeyDown(object? sender, KeyEventArgs e) - { - if (e.Key != Key.Escape) - return; - - if (_vm.IsExecuting) - _runCts?.Cancel(); - else - Close(); - e.Handled = true; - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } - - private static IBrush ResolveBrush(string key, string fallbackHex) - { - if (Application.Current?.TryFindResource(key, out object? resource) == true && resource is IBrush brush) - return brush; - - return new SolidColorBrush(Color.Parse(fallbackHex)); - } - - private static CornerRadius ResolveCornerRadius(string key, double fallbackValue) - { - if (Application.Current?.Resources.TryGetResource(key, null, out object? resource) == true && resource is CornerRadius radius) - return radius; - - return new CornerRadius(fallbackValue); - } - - private static double ResolveFontSize(string key, double fallback) - { - if (Application.Current?.Resources.TryGetResource(key, null, out object? resource) == true) - { - if (resource is double size) - return size; - if (resource is int intSize) - return intSize; - } - - return fallback; - } - - private static FontWeight ResolveFontWeight(string key, FontWeight fallback) - { - if (Application.Current?.Resources.TryGetResource(key, null, out object? resource) == true - && resource is FontWeight fontWeight) - { - return fontWeight; - } - - return fallback; - } -} +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; +using AkkornStudio.Core; +using AkkornStudio.UI.Services.Localization; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.Services.Theming; + +namespace AkkornStudio.UI.Controls.Ddl; + +public sealed class DdlExecuteDialogWindow : Window +{ + private readonly DdlExecuteDialogViewModel _vm; + private readonly Func> _executor; + private Button? _runButton; + private Button? _cancelButton; + private CheckBox? _stopOnErrorCheck; + private CheckBox? _confirmDestructiveCheck; + private TextBox? _resultBox; + private TextBlock? _summaryText; + private CancellationTokenSource? _runCts; + + public DdlExecuteDialogWindow( + DdlExecuteDialogViewModel vm, + Func> executor) + { + _vm = vm; + _executor = executor; + + Title = L("ddl.dialog.title", "Execute DDL"); + Width = 920; + Height = 680; + MinWidth = 740; + MinHeight = 520; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + SystemDecorations = SystemDecorations.None; + ExtendClientAreaToDecorationsHint = true; + ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.NoChrome; + ExtendClientAreaTitleBarHeightHint = -1; + Background = new SolidColorBrush(Color.Parse(UiColorConstants.C_0D0F14)); + + KeyDown += OnKeyDown; + Content = BuildContent(); + } + + private Control BuildContent() + { + _runButton = new Button + { + Content = L("ddl.dialog.execute", "Execute"), + Padding = new Thickness(12, 6), + HorizontalAlignment = HorizontalAlignment.Right, + MinWidth = 110, + IsEnabled = _vm.CanExecute, + }; + _runButton.Click += async (_, _) => await RunAsync(); + + _cancelButton = new Button + { + Content = L("ddl.dialog.cancel", "Cancel"), + Padding = new Thickness(12, 6), + HorizontalAlignment = HorizontalAlignment.Right, + MinWidth = 110, + IsEnabled = false, + }; + _cancelButton.Click += (_, _) => _runCts?.Cancel(); + + Button closeButton = new() + { + Content = L("ddl.dialog.close", "Close"), + Padding = new Thickness(12, 6), + HorizontalAlignment = HorizontalAlignment.Right, + MinWidth = 110, + }; + closeButton.Click += (_, _) => Close(); + + _stopOnErrorCheck = new CheckBox + { + Content = L("ddl.dialog.stopOnError", "Stop on first failure"), + IsChecked = _vm.StopOnError, + VerticalAlignment = VerticalAlignment.Center, + }; + _stopOnErrorCheck.IsCheckedChanged += (_, _) => _vm.StopOnError = _stopOnErrorCheck.IsChecked ?? false; + + _confirmDestructiveCheck = new CheckBox + { + Content = L( + "ddl.dialog.confirmDestructive", + "I confirm execution of destructive statements (DROP TABLE)" + ), + IsVisible = _vm.HasDestructiveStatements, + IsChecked = _vm.ConfirmDestructiveExecution, + VerticalAlignment = VerticalAlignment.Center, + Foreground = new SolidColorBrush(Color.Parse(UiColorConstants.C_FCA5A5)), + }; + _confirmDestructiveCheck.IsCheckedChanged += (_, _) => + { + _vm.ConfirmDestructiveExecution = _confirmDestructiveCheck.IsChecked ?? false; + if (_runButton is not null) + _runButton.IsEnabled = _vm.CanExecute; + }; + + TextBox sqlPreview = new() + { + Text = _vm.SqlPreview, + IsReadOnly = true, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + FontFamily = new FontFamily("JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace"), + MinHeight = 260, + }; + + _summaryText = new TextBlock + { + Text = L("ddl.dialog.reviewBeforeRun", "Review the DDL script before confirming."), + Foreground = new SolidColorBrush(Color.Parse(UiColorConstants.C_C8D0DC)), + FontSize = ResolveFontSize("FontSizeBody", 12), + }; + + ItemsControl statementList = new() + { + ItemsSource = _vm.StatementPreviews, + ItemTemplate = new FuncDataTemplate((item, _) => + new TextBlock + { + Text = item.CompactSql, + FontFamily = new FontFamily("JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace"), + FontSize = ResolveFontSize("FontSizeCaption", 11), + Margin = new Thickness(0, 1, 0, 1), + Foreground = item.IsDestructive + ? new SolidColorBrush(Color.Parse(UiColorConstants.C_FCA5A5)) + : new SolidColorBrush(Color.Parse(UiColorConstants.C_9CA3AF)), + } + ), + Margin = new Thickness(0, 6, 0, 0), + }; + + Border statementListHost = new() + { + BorderBrush = new SolidColorBrush(Color.Parse(UiColorConstants.C_1F2937)), + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(6), + Padding = new Thickness(8), + MaxHeight = 120, + Child = new ScrollViewer + { + Content = statementList, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + }, + }; + + Button headerCloseButton = new() + { + Content = "×", + Padding = new Thickness(8, 4), + MinWidth = 32, + }; + headerCloseButton.Click += (_, _) => Close(); + + _resultBox = new TextBox + { + IsReadOnly = true, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + FontFamily = new FontFamily("JetBrains Mono,IBM Plex Mono,Cascadia Code,Consolas,monospace"), + MinHeight = 200, + }; + + return new Border + { + Background = new SolidColorBrush(Color.Parse(UiColorConstants.C_070A12)), + Child = new Grid + { + RowDefinitions = new RowDefinitions("Auto,*,Auto"), + Children = + { + new Border + { + Background = ResolveBrush("Bg1Brush", UiColorConstants.C_0F1220), + BorderBrush = ResolveBrush("BorderSubtleBrush", UiColorConstants.C_1E2A3F), + BorderThickness = new Thickness(0, 0, 0, 1), + Padding = new Thickness(16, 12), + Child = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"), + Children = + { + new Border + { + Width = 28, + Height = 28, + CornerRadius = ResolveCornerRadius("RadiusSM", 6), + Background = ResolveBrush("BtnWarningBgBrush", UiColorConstants.C_10291A), + BorderBrush = ResolveBrush("StatusWarningBrush", UiColorConstants.C_FBBF24), + BorderThickness = new Thickness(1), + Child = new TextBlock + { + Text = "!", + FontWeight = ResolveFontWeight("FontWeightTitle", FontWeight.SemiBold), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Foreground = ResolveBrush("StatusWarningBrush", UiColorConstants.C_FBBF24), + }, + }, + PlaceAtCol(new StackPanel + { + Margin = new Thickness(10, 0, 0, 0), + Spacing = 2, + Children = + { + new TextBlock + { + Text = L("ddl.dialog.title", "Execute DDL"), + FontSize = ResolveFontSize("FontSizeNodeTitle", 14), + FontWeight = ResolveFontWeight("FontWeightTitle", FontWeight.SemiBold), + Foreground = ResolveBrush("TextPrimaryBrush", UiColorConstants.C_E8EAED), + }, + new TextBlock + { + Text = L("ddl.dialog.subtitle", "Review and execute DDL changes safely"), + FontSize = ResolveFontSize("FontSizeCaption", 11), + Foreground = ResolveBrush("TextMutedBrush", UiColorConstants.C_9CA3AF), + }, + }, + }, 1), + PlaceAtCol(headerCloseButton, 2), + }, + }, + }, + PlaceAtRow(new ScrollViewer + { + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + Content = new Border + { + Padding = new Thickness(16, 12), + Child = new StackPanel + { + Spacing = 10, + Children = + { + new TextBlock + { + Text = L("ddl.dialog.confirmQuestion", "Confirm DDL execution on the connected database?"), + FontSize = ResolveFontSize("FontSizeHeading", 18), + FontWeight = ResolveFontWeight("FontWeightTitle", FontWeight.SemiBold), + Foreground = new SolidColorBrush(Color.Parse(UiColorConstants.C_E8EAED)), + }, + new TextBlock + { + Text = L("ddl.dialog.irreversibleWarning", "This action can change the schema irreversibly."), + Foreground = new SolidColorBrush(Color.Parse(UiColorConstants.C_FBBF24)), + FontSize = ResolveFontSize("FontSizeBody", 12), + }, + sqlPreview, + _summaryText, + statementListHost, + _resultBox, + }, + }, + }, + }, 1), + PlaceAtRow(new Border + { + Background = ResolveBrush("Bg1Brush", UiColorConstants.C_0F1220), + BorderBrush = ResolveBrush("BorderSubtleBrush", UiColorConstants.C_1E2A3F), + BorderThickness = new Thickness(0, 1, 0, 0), + Padding = new Thickness(16, 10), + Child = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto"), + Children = + { + new StackPanel + { + Spacing = 4, + Children = + { + _stopOnErrorCheck, + _confirmDestructiveCheck, + }, + }, + PlaceAtCol(_runButton, 1), + PlaceAtCol(_cancelButton, 2), + PlaceAtCol(closeButton, 3), + }, + }, + }, 2), + }, + }, + }; + } + + private static Control PlaceAtRow(Control control, int row) + { + Grid.SetRow(control, row); + return control; + } + + private static Control PlaceAtCol(Control control, int col) + { + Grid.SetColumn(control, col); + return control; + } + + private async Task RunAsync() + { + if (_runButton is null || _cancelButton is null || _stopOnErrorCheck is null || _resultBox is null || _summaryText is null || _confirmDestructiveCheck is null) + return; + + if (_vm.HasDestructiveStatements && !_vm.ConfirmDestructiveExecution) + { + _summaryText.Text = L("ddl.dialog.mustConfirmDestructive", "Confirm destructive execution to continue."); + return; + } + + _vm.IsExecuting = true; + _runButton.IsEnabled = false; + _cancelButton.IsEnabled = true; + _stopOnErrorCheck.IsEnabled = false; + _confirmDestructiveCheck.IsEnabled = false; + _summaryText.Text = L("ddl.dialog.executing", "Executing..."); + _runCts = new CancellationTokenSource(); + + try + { + DdlExecutionResult result = await _executor(_vm.StopOnError, _runCts.Token); + _vm.ApplyResult(result); + } + catch (OperationCanceledException) + { + _vm.ApplyCancelled(); + } + catch (Exception ex) + { + _vm.ApplyError(ex); + } + finally + { + _runCts?.Dispose(); + _runCts = null; + _vm.IsExecuting = false; + _runButton.IsEnabled = _vm.CanExecute; + _cancelButton.IsEnabled = false; + _stopOnErrorCheck.IsEnabled = true; + _confirmDestructiveCheck.IsEnabled = true; + _summaryText.Text = _vm.ResultSummary; + _resultBox.Text = _vm.ResultDetails; + } + } + + private void OnKeyDown(object? sender, KeyEventArgs e) + { + if (e.Key != Key.Escape) + return; + + if (_vm.IsExecuting) + _runCts?.Cancel(); + else + Close(); + e.Handled = true; + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } + + private static IBrush ResolveBrush(string key, string fallbackHex) + { + if (Application.Current?.TryFindResource(key, out object? resource) == true && resource is IBrush brush) + return brush; + + return new SolidColorBrush(Color.Parse(fallbackHex)); + } + + private static CornerRadius ResolveCornerRadius(string key, double fallbackValue) + { + if (Application.Current?.Resources.TryGetResource(key, null, out object? resource) == true && resource is CornerRadius radius) + return radius; + + return new CornerRadius(fallbackValue); + } + + private static double ResolveFontSize(string key, double fallback) + { + if (Application.Current?.Resources.TryGetResource(key, null, out object? resource) == true) + { + if (resource is double size) + return size; + if (resource is int intSize) + return intSize; + } + + return fallback; + } + + private static FontWeight ResolveFontWeight(string key, FontWeight fallback) + { + if (Application.Current?.Resources.TryGetResource(key, null, out object? resource) == true + && resource is FontWeight fontWeight) + { + return fontWeight; + } + + return fallback; + } +} diff --git a/src/AkkornStudio.UI/Controls/DragDrop/CanvasDragDropDataFormats.cs b/src/AkkornStudio.UI/Controls/DragDrop/CanvasDragDropDataFormats.cs new file mode 100644 index 00000000..9544763f --- /dev/null +++ b/src/AkkornStudio.UI/Controls/DragDrop/CanvasDragDropDataFormats.cs @@ -0,0 +1,7 @@ +namespace AkkornStudio.UI.Controls.DragDrop; + +public static class CanvasDragDropDataFormats +{ + public const string NodeType = "akkornstudio/node-type"; + public const string SchemaTableFullName = "akkornstudio/schema-table-full-name"; +} diff --git a/src/AkkornStudio.UI/Controls/ErDiagram/ErCanvasControl.axaml b/src/AkkornStudio.UI/Controls/ErDiagram/ErCanvasControl.axaml new file mode 100644 index 00000000..814bbeb3 --- /dev/null +++ b/src/AkkornStudio.UI/Controls/ErDiagram/ErCanvasControl.axaml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AkkornStudio.UI/Controls/ErDiagram/ErCanvasControl.axaml.cs b/src/AkkornStudio.UI/Controls/ErDiagram/ErCanvasControl.axaml.cs new file mode 100644 index 00000000..b57613a2 --- /dev/null +++ b/src/AkkornStudio.UI/Controls/ErDiagram/ErCanvasControl.axaml.cs @@ -0,0 +1,246 @@ +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia; +using AkkornStudio.UI.ViewModels.ErDiagram; +using System.ComponentModel; +using AkkornStudio.UI.Controls; + +namespace AkkornStudio.UI.Controls.ErDiagram; + +public sealed partial class ErCanvasControl : UserControl +{ + private readonly CanvasViewportInteractionHost _interactionHost = + new(CanvasViewportGesturePolicy.ErCanvasDefault); + private readonly CanvasViewportSelectionAdornerController _selectionAdornerController = new(); + private readonly CanvasViewportSelectionNavigationController _selectionNavigationController = new(); + private readonly CanvasViewportGesturePolicy _gesturePolicy = CanvasViewportGesturePolicy.ErCanvasDefault; + private ErCanvasViewModel? _observedCanvas; + + public ErCanvasControl() + { + InitializeComponent(); + DataContextChanged += OnDataContextChanged; + LayoutUpdated += (_, _) => UpdateViewportState(); + } + + private void Edge_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (DataContext is not ErCanvasViewModel canvas) + return; + + PointerPointProperties pointerProperties = e.GetCurrentPoint(this).Properties; + bool isPanGesture = CanvasViewportGestureDecisions.IsPanGesture( + _gesturePolicy, + pointerProperties, + e.KeyModifiers); + if (isPanGesture) + return; + + if (sender is not Control control || control.DataContext is not ErRelationEdgeViewModel edge) + return; + + canvas.SelectedEdge = edge; + e.Handled = true; + } + + private void CanvasBackground_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (e.Source is not Canvas) + return; + + PointerPointProperties pointerProperties = e.GetCurrentPoint(this).Properties; + bool isPanGesture = CanvasViewportGestureDecisions.IsPanGesture( + _gesturePolicy, + pointerProperties, + e.KeyModifiers); + if (isPanGesture) + return; + + // Deixa o evento subir para o ViewportSurface decidir entre pan/marquee. + } + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (_observedCanvas is not null) + _observedCanvas.PropertyChanged -= OnCanvasPropertyChanged; + + _observedCanvas = DataContext as ErCanvasViewModel; + + Canvas? sceneContentCanvas = this.FindControl("SceneContentCanvas"); + if (sceneContentCanvas is not null) + sceneContentCanvas.DataContext = _observedCanvas; + + Canvas? overlayCanvas = this.FindControl("SelectionMarquee")?.Parent as Canvas; + if (overlayCanvas is not null) + overlayCanvas.DataContext = _observedCanvas; + + ItemsControl? edgeItems = this.FindControl("EdgeItems"); + if (edgeItems is not null) + edgeItems.ItemsSource = _observedCanvas?.Edges; + + ItemsControl? entityItems = this.FindControl("EntityItems"); + if (entityItems is not null) + entityItems.ItemsSource = _observedCanvas?.Entities; + + if (_observedCanvas is not null) + _observedCanvas.PropertyChanged += OnCanvasPropertyChanged; + + SyncTransform(); + } + + private void OnCanvasPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ErCanvasViewModel.FocusRequestVersion)) + { + Dispatcher.UIThread.Post(CenterViewportOnFocusTarget, DispatcherPriority.Background); + return; + } + + if (e.PropertyName is nameof(ErCanvasViewModel.Zoom) + or nameof(ErCanvasViewModel.ViewportX) + or nameof(ErCanvasViewModel.ViewportY) + or nameof(ErCanvasViewModel.SelectedEntity) + or nameof(ErCanvasViewModel.SelectedEdge)) + { + SyncTransform(); + } + } + + private void CenterViewportOnFocusTarget() + { + if (_observedCanvas is null) + return; + + UpdateViewportState(); + if (!_selectionNavigationController.TryCenterSelection(_observedCanvas, Bounds.Size)) + _observedCanvas.CenterViewportOnCanvasPoint(_observedCanvas.FocusTargetX, _observedCanvas.FocusTargetY); + SyncTransform(); + } + + private void CenterSelection_Click(object? sender, RoutedEventArgs e) + { + if (_observedCanvas is null) + return; + + UpdateViewportState(); + if (_selectionNavigationController.TryCenterSelection(_observedCanvas, Bounds.Size)) + SyncTransform(); + } + + private void FitSelection_Click(object? sender, RoutedEventArgs e) + { + if (_observedCanvas is null) + return; + + UpdateViewportState(); + if (_selectionNavigationController.TryFitSelection( + _observedCanvas, + Bounds.Size, + padding: 40, + minZoom: 0.15, + maxZoom: 2.0)) + { + SyncTransform(); + } + } + + private void Viewport_PointerWheelChanged(object? sender, PointerWheelEventArgs e) + { + if (_observedCanvas is null || sender is not InfiniteCanvasCoreControl viewportHost) + return; + + _interactionHost.HandlePointerWheel(_observedCanvas, viewportHost.ViewportSurface, e); + viewportHost.SyncViewport(); + } + + private void Viewport_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (_observedCanvas is null || sender is not InfiniteCanvasCoreControl viewportHost) + return; + + PointerPointProperties props = e.GetCurrentPoint(viewportHost.ViewportSurface).Properties; + bool isPanGesture = CanvasViewportGestureDecisions.IsPanGesture(_gesturePolicy, props, e.KeyModifiers); + if (!isPanGesture && !props.IsLeftButtonPressed) + return; + + if (_interactionHost.HandlePointerPressed(_observedCanvas, viewportHost.ViewportSurface, e)) + UpdateMarqueeVisual(); + } + + private void Viewport_PointerMoved(object? sender, PointerEventArgs e) + { + if (_observedCanvas is null || sender is not InfiniteCanvasCoreControl viewportHost) + return; + + if (!_interactionHost.HandlePointerMoved(_observedCanvas, viewportHost.ViewportSurface, e, out bool marqueeChanged)) + return; + + if (marqueeChanged) + { + UpdateMarqueeVisual(); + return; + } + + viewportHost.SyncViewport(); + } + + private void Viewport_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + CanvasViewportPointerReleaseKind releaseKind = _interactionHost.HandlePointerReleased(e); + if (releaseKind == CanvasViewportPointerReleaseKind.PanEnded) + return; + + if (releaseKind != CanvasViewportPointerReleaseKind.MarqueeCompleted + || _observedCanvas is null + || sender is not InfiniteCanvasCoreControl viewportHost) + return; + + CanvasMarqueeAdorner? marquee = this.FindControl("SelectionMarquee"); + _selectionAdornerController.CompleteMarqueeSelection(_observedCanvas, _interactionHost, marquee); + viewportHost.SyncViewport(); + } + + private void UpdateViewportState() + { + if (_observedCanvas is null) + return; + + InfiniteCanvasCoreControl? viewportHost = this.FindControl("ViewportSurface"); + if (viewportHost is null) + return; + + _observedCanvas.SetViewportSize(viewportHost.Bounds.Width, viewportHost.Bounds.Height); + viewportHost.SyncViewport(); + } + + private void SyncTransform() + { + this.FindControl("ViewportSurface")?.SyncViewport(); + UpdateFocusAdorner(); + UpdateMarqueeVisual(); + } + + private void UpdateFocusAdorner() + { + if (_observedCanvas is null) + return; + + CanvasFocusAdorner? adorner = this.FindControl("FocusOverlay"); + _selectionAdornerController.SyncFocusAdorner(_observedCanvas, adorner, padding: 12); + } + + private void UpdateMarqueeVisual() + { + CanvasMarqueeAdorner? marquee = this.FindControl("SelectionMarquee"); + if (_observedCanvas is null) + { + if (marquee is not null) + marquee.SelectionRect = default; + return; + } + + _selectionAdornerController.SyncMarqueeAdorner(_observedCanvas, _interactionHost, marquee); + } +} diff --git a/src/AkkornStudio.UI/Controls/ErDiagram/ErEntityControl.axaml b/src/AkkornStudio.UI/Controls/ErDiagram/ErEntityControl.axaml new file mode 100644 index 00000000..6a410c3a --- /dev/null +++ b/src/AkkornStudio.UI/Controls/ErDiagram/ErEntityControl.axaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AkkornStudio.UI/Controls/ErDiagram/ErEntityControl.axaml.cs b/src/AkkornStudio.UI/Controls/ErDiagram/ErEntityControl.axaml.cs new file mode 100644 index 00000000..3bb3614b --- /dev/null +++ b/src/AkkornStudio.UI/Controls/ErDiagram/ErEntityControl.axaml.cs @@ -0,0 +1,40 @@ +using Avalonia.Input; +using Avalonia.VisualTree; +using Avalonia.Controls; +using AkkornStudio.UI.ViewModels.ErDiagram; + +namespace AkkornStudio.UI.Controls.ErDiagram; + +public sealed partial class ErEntityControl : UserControl +{ + private static readonly CanvasViewportGesturePolicy GesturePolicy = CanvasViewportGesturePolicy.ErCanvasDefault; + + public ErEntityControl() + { + InitializeComponent(); + } + + private void Root_PointerPressed(object? sender, PointerPressedEventArgs e) + { + PointerPointProperties pointerProperties = e.GetCurrentPoint(this).Properties; + bool isPanGesture = CanvasViewportGestureDecisions.IsPanGesture( + GesturePolicy, + pointerProperties, + e.KeyModifiers); + if (isPanGesture) + return; + + if (DataContext is not ErEntityNodeViewModel entity) + return; + + ErCanvasControl? canvasControl = this.FindAncestorOfType(); + if (canvasControl is null) + return; + + if (canvasControl.DataContext is not ErCanvasViewModel canvas) + return; + + canvas.SelectedEntity = entity; + e.Handled = true; + } +} diff --git a/src/DBWeaver.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml b/src/AkkornStudio.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml similarity index 95% rename from src/DBWeaver.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml rename to src/AkkornStudio.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml index afbe5be4..d4984ac9 100644 --- a/src/DBWeaver.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml +++ b/src/AkkornStudio.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml @@ -1,695 +1,680 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml.cs b/src/AkkornStudio.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml.cs similarity index 95% rename from src/DBWeaver.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml.cs rename to src/AkkornStudio.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml.cs index 7fe0dcbf..8adc2487 100644 --- a/src/DBWeaver.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml.cs +++ b/src/AkkornStudio.UI/Controls/ExplainPlan/ExplainPlanOverlay.axaml.cs @@ -1,293 +1,293 @@ -using System.Diagnostics; -using System.IO; -using System.ComponentModel; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Media; -using Avalonia.Controls.Shapes; -using Avalonia.Platform.Storage; -using DBWeaver.UI.Services.Explain; -using DBWeaver.UI.Services.Workspace.Models; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.UI.Services.Theming; - -namespace DBWeaver.UI.Controls; - -public sealed partial class ExplainPlanOverlay : UserControl -{ - private PropertyChangedEventHandler? _viewModelPropertyChangedHandler; - private ExplainPlanViewModel? _currentVm; - - public ExplainPlanOverlay() - { - InitializeComponent(); - - Button? closeBtn = this.FindControl - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/FileHistory/FileVersionHistoryOverlay.axaml.cs b/src/AkkornStudio.UI/Controls/FileHistory/FileVersionHistoryOverlay.axaml.cs similarity index 91% rename from src/DBWeaver.UI/Controls/FileHistory/FileVersionHistoryOverlay.axaml.cs rename to src/AkkornStudio.UI/Controls/FileHistory/FileVersionHistoryOverlay.axaml.cs index ea8e058d..d74e4466 100644 --- a/src/DBWeaver.UI/Controls/FileHistory/FileVersionHistoryOverlay.axaml.cs +++ b/src/AkkornStudio.UI/Controls/FileHistory/FileVersionHistoryOverlay.axaml.cs @@ -1,46 +1,46 @@ -using Avalonia.Controls; -using Avalonia.Input; -using DBWeaver.UI.ViewModels.Canvas; - -namespace DBWeaver.UI.Controls; - -public sealed partial class FileVersionHistoryOverlay : UserControl -{ - private FileVersionHistoryViewModel? Vm => DataContext as FileVersionHistoryViewModel; - - public FileVersionHistoryOverlay() - { - InitializeComponent(); - - Button? closeBtn = this.FindControl - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/FlowVersions/FlowVersionOverlay.axaml.cs b/src/AkkornStudio.UI/Controls/FlowVersions/FlowVersionOverlay.axaml.cs similarity index 94% rename from src/DBWeaver.UI/Controls/FlowVersions/FlowVersionOverlay.axaml.cs rename to src/AkkornStudio.UI/Controls/FlowVersions/FlowVersionOverlay.axaml.cs index 7e9e7444..aab35b32 100644 --- a/src/DBWeaver.UI/Controls/FlowVersions/FlowVersionOverlay.axaml.cs +++ b/src/AkkornStudio.UI/Controls/FlowVersions/FlowVersionOverlay.axaml.cs @@ -1,86 +1,86 @@ -using Avalonia.Controls; -using Avalonia.Input; -using DBWeaver.UI.ViewModels.Canvas; - -namespace DBWeaver.UI.Controls; - -public sealed partial class FlowVersionOverlay : UserControl -{ - private FlowVersionOverlayViewModel? Vm => DataContext as FlowVersionOverlayViewModel; - - public FlowVersionOverlay() - { - InitializeComponent(); - - Button? closeBtn = this.FindControl - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Postgres + MySql + SqlServer + SQLite + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/LiveSqlBar/LiveSqlBar.axaml.cs b/src/AkkornStudio.UI/Controls/LiveSqlBar/LiveSqlBar.axaml.cs similarity index 92% rename from src/DBWeaver.UI/Controls/LiveSqlBar/LiveSqlBar.axaml.cs rename to src/AkkornStudio.UI/Controls/LiveSqlBar/LiveSqlBar.axaml.cs index 0d978d4c..9dfd066d 100644 --- a/src/DBWeaver.UI/Controls/LiveSqlBar/LiveSqlBar.axaml.cs +++ b/src/AkkornStudio.UI/Controls/LiveSqlBar/LiveSqlBar.axaml.cs @@ -1,46 +1,46 @@ -using Avalonia.Controls; -using Avalonia; -using DBWeaver.UI.Converters; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Controls; - -public sealed partial class LiveSqlBar : UserControl -{ - public static readonly StyledProperty ShowPerformanceToolsProperty = - AvaloniaProperty.Register(nameof(ShowPerformanceTools), true); - - public bool ShowPerformanceTools - { - get => GetValue(ShowPerformanceToolsProperty); - set => SetValue(ShowPerformanceToolsProperty, value); - } - - public LiveSqlBar() - { - InitializeComponent(); - - Button? copyBtn = this.FindControl - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/Node/NodeControl.axaml.cs b/src/AkkornStudio.UI/Controls/Node/NodeControl.axaml.cs similarity index 65% rename from src/DBWeaver.UI/Controls/Node/NodeControl.axaml.cs rename to src/AkkornStudio.UI/Controls/Node/NodeControl.axaml.cs index 762af94b..ae526c10 100644 --- a/src/DBWeaver.UI/Controls/Node/NodeControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/Node/NodeControl.axaml.cs @@ -1,31 +1,31 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.LogicalTree; -using Avalonia.VisualTree; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Controls; - -public sealed partial class NodeControl : UserControl -{ - public event EventHandler<(NodeViewModel Node, bool ShiftHeld)>? NodeClicked; - public event EventHandler? NodeDoubleClicked; - public event EventHandler<(NodeViewModel Node, Point ScreenPos)>? NodeDragStarted; - public event EventHandler<(NodeViewModel Node, Point ScreenPos)>? NodeDragDelta; - public event EventHandler<(NodeViewModel Node, Point ScreenPos)>? NodeDragCompleted; - public event EventHandler<(PinViewModel Pin, Point CanvasPoint)>? PinPressed; - - private bool _dragging; - private Point _pressPos; - private bool _didDrag; - private bool _isDoubleClick; - - public NodeControl() - { - InitializeComponent(); - - Button? previewToggle = this.FindControl - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/Shell/AppHeaderBar.axaml.cs b/src/AkkornStudio.UI/Controls/Shell/AppHeaderBar.axaml.cs similarity index 95% rename from src/DBWeaver.UI/Controls/Shell/AppHeaderBar.axaml.cs rename to src/AkkornStudio.UI/Controls/Shell/AppHeaderBar.axaml.cs index d10a1b1e..629c372d 100644 --- a/src/DBWeaver.UI/Controls/Shell/AppHeaderBar.axaml.cs +++ b/src/AkkornStudio.UI/Controls/Shell/AppHeaderBar.axaml.cs @@ -1,124 +1,124 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Input; -using Avalonia.Markup.Xaml; - -namespace DBWeaver.UI.Controls.Shell; - -public partial class AppHeaderBar : UserControl -{ - public event EventHandler? TitleMenuRequested; - - public static readonly StyledProperty TitleProperty = AvaloniaProperty.Register( - nameof(Title), - string.Empty - ); - - public static readonly StyledProperty LeftContentProperty = AvaloniaProperty.Register( - nameof(LeftContent) - ); - - public static readonly StyledProperty CenterContentProperty = AvaloniaProperty.Register( - nameof(CenterContent) - ); - - public static readonly StyledProperty RightContentProperty = AvaloniaProperty.Register( - nameof(RightContent) - ); - - public static readonly StyledProperty ShowWindowControlsProperty = AvaloniaProperty.Register( - nameof(ShowWindowControls), - true - ); - - public static readonly StyledProperty ShowBrandBadgeProperty = AvaloniaProperty.Register( - nameof(ShowBrandBadge), - true - ); - - public AppHeaderBar() - { - InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - public string Title - { - get => GetValue(TitleProperty); - set => SetValue(TitleProperty, value); - } - - public object? LeftContent - { - get => GetValue(LeftContentProperty); - set => SetValue(LeftContentProperty, value); - } - - public object? CenterContent - { - get => GetValue(CenterContentProperty); - set => SetValue(CenterContentProperty, value); - } - - public object? RightContent - { - get => GetValue(RightContentProperty); - set => SetValue(RightContentProperty, value); - } - - public bool ShowWindowControls - { - get => GetValue(ShowWindowControlsProperty); - set => SetValue(ShowWindowControlsProperty, value); - } - - public bool ShowBrandBadge - { - get => GetValue(ShowBrandBadgeProperty); - set => SetValue(ShowBrandBadgeProperty, value); - } - - private void Header_PointerPressed(object? sender, PointerPressedEventArgs e) - { - if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - return; - - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel is Window window) - window.BeginMoveDrag(e); - } - - private void TitleButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - TitleMenuRequested?.Invoke(this, EventArgs.Empty); - e.Handled = true; - } - - private void Minimize_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel is Window window) - window.WindowState = WindowState.Minimized; - } - - private void Maximize_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel is not Window window) - return; - - window.WindowState = - window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } - - private void Close_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel is Window window) - window.Close(); - } -} +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; + +namespace AkkornStudio.UI.Controls.Shell; + +public partial class AppHeaderBar : UserControl +{ + public event EventHandler? TitleMenuRequested; + + public static readonly StyledProperty TitleProperty = AvaloniaProperty.Register( + nameof(Title), + string.Empty + ); + + public static readonly StyledProperty LeftContentProperty = AvaloniaProperty.Register( + nameof(LeftContent) + ); + + public static readonly StyledProperty CenterContentProperty = AvaloniaProperty.Register( + nameof(CenterContent) + ); + + public static readonly StyledProperty RightContentProperty = AvaloniaProperty.Register( + nameof(RightContent) + ); + + public static readonly StyledProperty ShowWindowControlsProperty = AvaloniaProperty.Register( + nameof(ShowWindowControls), + true + ); + + public static readonly StyledProperty ShowBrandBadgeProperty = AvaloniaProperty.Register( + nameof(ShowBrandBadge), + true + ); + + public AppHeaderBar() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public string Title + { + get => GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public object? LeftContent + { + get => GetValue(LeftContentProperty); + set => SetValue(LeftContentProperty, value); + } + + public object? CenterContent + { + get => GetValue(CenterContentProperty); + set => SetValue(CenterContentProperty, value); + } + + public object? RightContent + { + get => GetValue(RightContentProperty); + set => SetValue(RightContentProperty, value); + } + + public bool ShowWindowControls + { + get => GetValue(ShowWindowControlsProperty); + set => SetValue(ShowWindowControlsProperty, value); + } + + public bool ShowBrandBadge + { + get => GetValue(ShowBrandBadgeProperty); + set => SetValue(ShowBrandBadgeProperty, value); + } + + private void Header_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + return; + + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel is Window window) + window.BeginMoveDrag(e); + } + + private void TitleButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + TitleMenuRequested?.Invoke(this, EventArgs.Empty); + e.Handled = true; + } + + private void Minimize_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel is Window window) + window.WindowState = WindowState.Minimized; + } + + private void Maximize_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel is not Window window) + return; + + window.WindowState = + window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + + private void Close_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel is Window window) + window.Close(); + } +} diff --git a/src/DBWeaver.UI/Controls/Shell/DdlSchemaAnalysisWorkspaceControl.axaml b/src/AkkornStudio.UI/Controls/Shell/DdlSchemaAnalysisWorkspaceControl.axaml similarity index 78% rename from src/DBWeaver.UI/Controls/Shell/DdlSchemaAnalysisWorkspaceControl.axaml rename to src/AkkornStudio.UI/Controls/Shell/DdlSchemaAnalysisWorkspaceControl.axaml index a62be814..67c08d30 100644 --- a/src/DBWeaver.UI/Controls/Shell/DdlSchemaAnalysisWorkspaceControl.axaml +++ b/src/AkkornStudio.UI/Controls/Shell/DdlSchemaAnalysisWorkspaceControl.axaml @@ -1,272 +1,273 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/Shell/DiagramDocumentPageControl.axaml.cs b/src/AkkornStudio.UI/Controls/Shell/DiagramDocumentPageControl.axaml.cs similarity index 91% rename from src/DBWeaver.UI/Controls/Shell/DiagramDocumentPageControl.axaml.cs rename to src/AkkornStudio.UI/Controls/Shell/DiagramDocumentPageControl.axaml.cs index 4870c668..f5128676 100644 --- a/src/DBWeaver.UI/Controls/Shell/DiagramDocumentPageControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/Shell/DiagramDocumentPageControl.axaml.cs @@ -1,95 +1,95 @@ -using Avalonia.Controls; -using Avalonia.Input; -using DBWeaver.UI.Controls; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; - -namespace DBWeaver.UI.Controls.Shell; - -public partial class DiagramDocumentPageControl : UserControl -{ - private Panel? _overlayDismissBackdrop; - private CanvasViewModel? _viewModel; - - public DiagramDocumentPageControl() - { - InitializeComponent(); - DataContextChanged += OnDataContextChanged; - AddHandler(KeyDownEvent, OnHostKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel); - - _overlayDismissBackdrop = this.FindControl("OverlayDismissBackdrop"); - if (_overlayDismissBackdrop is not null) - _overlayDismissBackdrop.PointerPressed += (_, _) => TryDismissTopOverlay(); - } - - private void OnHostKeyDown(object? sender, KeyEventArgs e) - { - if (e.Key != Key.Escape) - return; - - if (TryDismissTopOverlay()) - e.Handled = true; - } - - public InfiniteCanvas? CanvasControl => this.FindControl("TheCanvas"); - - public SearchMenuControl? SearchOverlayControl => this.FindControl("SearchOverlay"); - - public void InvalidateCanvasWires() - { - CanvasControl?.InvalidateWires(); - } - - private void OnDataContextChanged(object? sender, EventArgs e) - { - _viewModel = DataContext as CanvasViewModel; - } - - private bool TryDismissTopOverlay() - { - if (_viewModel is null) - return false; - - if (_viewModel.FileHistory.IsVisible) - { - _viewModel.FileHistory.Close(); - return true; - } - - if (_viewModel.FlowVersions.IsVisible) - { - _viewModel.FlowVersions.Close(); - return true; - } - - if (_viewModel.SqlImporter.IsVisible) - { - if (_viewModel.SqlImporter.IsImporting) - _viewModel.SqlImporter.CancelImport(); - else - _viewModel.SqlImporter.Close(); - - return true; - } - - if (_viewModel.ExplainPlan.IsVisible) - { - _viewModel.ExplainPlan.Close(); - return true; - } - - if (_viewModel.Benchmark.IsVisible) - { - _viewModel.Benchmark.CloseCommand.Execute(null); - return true; - } - - if (VisualRoot is Window window && window.DataContext is ShellViewModel shellViewModel && shellViewModel.CommandPalette.IsVisible) - { - shellViewModel.CommandPalette.Close(); - return true; - } - - return false; - } -} +using Avalonia.Controls; +using Avalonia.Input; +using AkkornStudio.UI.Controls; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; + +namespace AkkornStudio.UI.Controls.Shell; + +public partial class DiagramDocumentPageControl : UserControl +{ + private Panel? _overlayDismissBackdrop; + private CanvasViewModel? _viewModel; + + public DiagramDocumentPageControl() + { + InitializeComponent(); + DataContextChanged += OnDataContextChanged; + AddHandler(KeyDownEvent, OnHostKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel); + + _overlayDismissBackdrop = this.FindControl("OverlayDismissBackdrop"); + if (_overlayDismissBackdrop is not null) + _overlayDismissBackdrop.PointerPressed += (_, _) => TryDismissTopOverlay(); + } + + private void OnHostKeyDown(object? sender, KeyEventArgs e) + { + if (e.Key != Key.Escape) + return; + + if (TryDismissTopOverlay()) + e.Handled = true; + } + + public InfiniteCanvas? CanvasControl => this.FindControl("TheCanvas"); + + public SearchMenuControl? SearchOverlayControl => this.FindControl("SearchOverlay"); + + public void InvalidateCanvasWires() + { + CanvasControl?.InvalidateWires(); + } + + private void OnDataContextChanged(object? sender, EventArgs e) + { + _viewModel = DataContext as CanvasViewModel; + } + + private bool TryDismissTopOverlay() + { + if (_viewModel is null) + return false; + + if (_viewModel.FileHistory.IsVisible) + { + _viewModel.FileHistory.Close(); + return true; + } + + if (_viewModel.FlowVersions.IsVisible) + { + _viewModel.FlowVersions.Close(); + return true; + } + + if (_viewModel.SqlImporter.IsVisible) + { + if (_viewModel.SqlImporter.IsImporting) + _viewModel.SqlImporter.CancelImport(); + else + _viewModel.SqlImporter.Close(); + + return true; + } + + if (_viewModel.ExplainPlan.IsVisible) + { + _viewModel.ExplainPlan.Close(); + return true; + } + + if (_viewModel.Benchmark.IsVisible) + { + _viewModel.Benchmark.CloseCommand.Execute(null); + return true; + } + + if (VisualRoot is Window window && window.DataContext is ShellViewModel shellViewModel && shellViewModel.CommandPalette.IsVisible) + { + shellViewModel.CommandPalette.Close(); + return true; + } + + return false; + } +} diff --git a/src/DBWeaver.UI/Controls/Shell/OutputPreviewModalControl.axaml b/src/AkkornStudio.UI/Controls/Shell/OutputPreviewModalControl.axaml similarity index 73% rename from src/DBWeaver.UI/Controls/Shell/OutputPreviewModalControl.axaml rename to src/AkkornStudio.UI/Controls/Shell/OutputPreviewModalControl.axaml index 11b82432..6a06a03c 100644 --- a/src/DBWeaver.UI/Controls/Shell/OutputPreviewModalControl.axaml +++ b/src/AkkornStudio.UI/Controls/Shell/OutputPreviewModalControl.axaml @@ -1,229 +1,192 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/SidebarLeft/ConnectionTabControl.axaml.cs b/src/AkkornStudio.UI/Controls/SidebarLeft/ConnectionTabControl.axaml.cs similarity index 79% rename from src/DBWeaver.UI/Controls/SidebarLeft/ConnectionTabControl.axaml.cs rename to src/AkkornStudio.UI/Controls/SidebarLeft/ConnectionTabControl.axaml.cs index f98b2f63..429a9879 100644 --- a/src/DBWeaver.UI/Controls/SidebarLeft/ConnectionTabControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/SidebarLeft/ConnectionTabControl.axaml.cs @@ -1,11 +1,11 @@ -using Avalonia.Controls; - -namespace DBWeaver.UI.Controls; - -public partial class ConnectionTabControl : UserControl -{ - public ConnectionTabControl() - { - InitializeComponent(); - } -} +using Avalonia.Controls; + +namespace AkkornStudio.UI.Controls; + +public partial class ConnectionTabControl : UserControl +{ + public ConnectionTabControl() + { + InitializeComponent(); + } +} diff --git a/src/DBWeaver.UI/Controls/SidebarLeft/NodesListControl.axaml b/src/AkkornStudio.UI/Controls/SidebarLeft/NodesListControl.axaml similarity index 95% rename from src/DBWeaver.UI/Controls/SidebarLeft/NodesListControl.axaml rename to src/AkkornStudio.UI/Controls/SidebarLeft/NodesListControl.axaml index 31aa1c99..ac4061b9 100644 --- a/src/DBWeaver.UI/Controls/SidebarLeft/NodesListControl.axaml +++ b/src/AkkornStudio.UI/Controls/SidebarLeft/NodesListControl.axaml @@ -1,419 +1,417 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/SidebarLeft/NodesListControl.axaml.cs b/src/AkkornStudio.UI/Controls/SidebarLeft/NodesListControl.axaml.cs similarity index 94% rename from src/DBWeaver.UI/Controls/SidebarLeft/NodesListControl.axaml.cs rename to src/AkkornStudio.UI/Controls/SidebarLeft/NodesListControl.axaml.cs index 315e5cc4..7b681cc8 100644 --- a/src/DBWeaver.UI/Controls/SidebarLeft/NodesListControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/SidebarLeft/NodesListControl.axaml.cs @@ -1,10 +1,10 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia; -using DBWeaver.UI.Controls.DragDrop; -using DBWeaver.UI.ViewModels; +using AkkornStudio.UI.Controls.DragDrop; +using AkkornStudio.UI.ViewModels; -namespace DBWeaver.UI.Controls; +namespace AkkornStudio.UI.Controls; public partial class NodesListControl : UserControl { diff --git a/src/DBWeaver.UI/Controls/SidebarLeft/SchemaControl.axaml b/src/AkkornStudio.UI/Controls/SidebarLeft/SchemaControl.axaml similarity index 95% rename from src/DBWeaver.UI/Controls/SidebarLeft/SchemaControl.axaml rename to src/AkkornStudio.UI/Controls/SidebarLeft/SchemaControl.axaml index 42ffbc66..736f04ae 100644 --- a/src/DBWeaver.UI/Controls/SidebarLeft/SchemaControl.axaml +++ b/src/AkkornStudio.UI/Controls/SidebarLeft/SchemaControl.axaml @@ -1,284 +1,284 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/SidebarLeft/SchemaControl.axaml.cs b/src/AkkornStudio.UI/Controls/SidebarLeft/SchemaControl.axaml.cs similarity index 93% rename from src/DBWeaver.UI/Controls/SidebarLeft/SchemaControl.axaml.cs rename to src/AkkornStudio.UI/Controls/SidebarLeft/SchemaControl.axaml.cs index c8dcbd58..ac6e1205 100644 --- a/src/DBWeaver.UI/Controls/SidebarLeft/SchemaControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/SidebarLeft/SchemaControl.axaml.cs @@ -1,11 +1,11 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia; -using DBWeaver.Metadata; -using DBWeaver.UI.Controls.DragDrop; -using DBWeaver.UI.ViewModels; +using AkkornStudio.Metadata; +using AkkornStudio.UI.Controls.DragDrop; +using AkkornStudio.UI.ViewModels; -namespace DBWeaver.UI.Controls; +namespace AkkornStudio.UI.Controls; public partial class SchemaControl : UserControl { diff --git a/src/AkkornStudio.UI/Controls/SidebarLeft/SidebarControl.axaml b/src/AkkornStudio.UI/Controls/SidebarLeft/SidebarControl.axaml new file mode 100644 index 00000000..e37d7911 --- /dev/null +++ b/src/AkkornStudio.UI/Controls/SidebarLeft/SidebarControl.axaml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/SidebarLeft/SidebarDiagnosticsControl.axaml.cs b/src/AkkornStudio.UI/Controls/SidebarLeft/SidebarDiagnosticsControl.axaml.cs similarity index 80% rename from src/DBWeaver.UI/Controls/SidebarLeft/SidebarDiagnosticsControl.axaml.cs rename to src/AkkornStudio.UI/Controls/SidebarLeft/SidebarDiagnosticsControl.axaml.cs index 37936b7a..8c2cafb7 100644 --- a/src/DBWeaver.UI/Controls/SidebarLeft/SidebarDiagnosticsControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/SidebarLeft/SidebarDiagnosticsControl.axaml.cs @@ -1,11 +1,11 @@ -using Avalonia.Controls; - -namespace DBWeaver.UI.Controls; - -public sealed partial class SidebarDiagnosticsControl : UserControl -{ - public SidebarDiagnosticsControl() - { - InitializeComponent(); - } -} +using Avalonia.Controls; + +namespace AkkornStudio.UI.Controls; + +public sealed partial class SidebarDiagnosticsControl : UserControl +{ + public SidebarDiagnosticsControl() + { + InitializeComponent(); + } +} diff --git a/src/DBWeaver.UI/Controls/SqlEditor/DiffPreviewControl.axaml b/src/AkkornStudio.UI/Controls/SqlEditor/DiffPreviewControl.axaml similarity index 75% rename from src/DBWeaver.UI/Controls/SqlEditor/DiffPreviewControl.axaml rename to src/AkkornStudio.UI/Controls/SqlEditor/DiffPreviewControl.axaml index ea4549a2..227c6b03 100644 --- a/src/DBWeaver.UI/Controls/SqlEditor/DiffPreviewControl.axaml +++ b/src/AkkornStudio.UI/Controls/SqlEditor/DiffPreviewControl.axaml @@ -1,15 +1,15 @@ - - - - - - + + + + + + diff --git a/src/DBWeaver.UI/Controls/SqlEditor/DiffPreviewControl.axaml.cs b/src/AkkornStudio.UI/Controls/SqlEditor/DiffPreviewControl.axaml.cs similarity index 75% rename from src/DBWeaver.UI/Controls/SqlEditor/DiffPreviewControl.axaml.cs rename to src/AkkornStudio.UI/Controls/SqlEditor/DiffPreviewControl.axaml.cs index a16143ed..e746db99 100644 --- a/src/DBWeaver.UI/Controls/SqlEditor/DiffPreviewControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/SqlEditor/DiffPreviewControl.axaml.cs @@ -1,11 +1,11 @@ -using Avalonia.Controls; - -namespace DBWeaver.UI.Controls.SqlEditor; - -public partial class DiffPreviewControl : UserControl -{ - public DiffPreviewControl() - { - InitializeComponent(); - } -} +using Avalonia.Controls; + +namespace AkkornStudio.UI.Controls.SqlEditor; + +public partial class DiffPreviewControl : UserControl +{ + public DiffPreviewControl() + { + InitializeComponent(); + } +} diff --git a/src/DBWeaver.UI/Controls/SqlEditor/MutationConfirmDialog.axaml b/src/AkkornStudio.UI/Controls/SqlEditor/MutationConfirmDialog.axaml similarity index 89% rename from src/DBWeaver.UI/Controls/SqlEditor/MutationConfirmDialog.axaml rename to src/AkkornStudio.UI/Controls/SqlEditor/MutationConfirmDialog.axaml index 72cf8bdd..cadf0ad8 100644 --- a/src/DBWeaver.UI/Controls/SqlEditor/MutationConfirmDialog.axaml +++ b/src/AkkornStudio.UI/Controls/SqlEditor/MutationConfirmDialog.axaml @@ -1,83 +1,88 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/SqlEditor/SqlEditorResultPanel.axaml.cs b/src/AkkornStudio.UI/Controls/SqlEditor/SqlEditorResultPanel.axaml.cs similarity index 96% rename from src/DBWeaver.UI/Controls/SqlEditor/SqlEditorResultPanel.axaml.cs rename to src/AkkornStudio.UI/Controls/SqlEditor/SqlEditorResultPanel.axaml.cs index a27f8f6a..2888a5d6 100644 --- a/src/DBWeaver.UI/Controls/SqlEditor/SqlEditorResultPanel.axaml.cs +++ b/src/AkkornStudio.UI/Controls/SqlEditor/SqlEditorResultPanel.axaml.cs @@ -1,852 +1,852 @@ -using System.ComponentModel; -using System.Collections.ObjectModel; -using System.Data; -using System.Globalization; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Platform.Storage; -using Material.Icons; -using Material.Icons.Avalonia; -using DBWeaver.UI.Services.SqlEditor; -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.Services.Search; -using DBWeaver.UI.Services.SqlEditor.Reports; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Controls.SqlEditor; - -public partial class SqlEditorResultPanel : UserControl -{ - private static readonly TextSearchService TextSearch = new(); - private SqlEditorViewModel? _viewModel; - private object?[]? _lastSelectedRow; - private int _lastSelectedColumnIndex = -1; - private IBrush? _nullCellForegroundBrush; - private readonly SqlEditorReportExportService _reportExportService = new(); - private SqlInlineEditEligibility _inlineEditEligibility = SqlInlineEditEligibility.NotEligible; - private readonly Dictionary _currentColumnIndexMap = new(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _pendingInlineEdits = new(ObjectArrayReferenceComparer.Instance); - - public SqlEditorResultPanel() - { - InitializeComponent(); - DataContextChanged += OnDataContextChanged; - ResultGrid.KeyDown += OnResultGridKeyDown; - ResultGrid.Sorting += OnResultGridSorting; - ResultGrid.CellEditEnding += OnResultGridCellEditEnding; - ResultGrid.CellEditEnded += OnResultGridCellEditEnded; - ResultGrid.ColumnReordered += OnResultGridColumnReordered; - ConfigureGridContextMenu(); - } - - private void OnDataContextChanged(object? sender, EventArgs e) - { - if (_viewModel is not null) - _viewModel.PropertyChanged -= OnViewModelPropertyChanged; - - _viewModel = DataContext as SqlEditorViewModel; - - if (_viewModel is not null) - { - _viewModel.PropertyChanged += OnViewModelPropertyChanged; - RefreshGrid(_viewModel.ResultRowsView); - } - else - { - RefreshGrid(null); - RowsCounterText.Text = "0 linhas"; - } - } - - private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName is nameof(SqlEditorViewModel.ResultRowsView) - or nameof(SqlEditorViewModel.SelectedResultTabIndex) - or nameof(SqlEditorViewModel.ResultTabs) - or nameof(SqlEditorViewModel.ResultGridFilterText) - or nameof(SqlEditorViewModel.ResultGridSortColumn) - or nameof(SqlEditorViewModel.ResultGridSortAscending) - or nameof(SqlEditorViewModel.SelectedOutputPane)) - { - RefreshGrid(_viewModel?.ResultRowsView); - } - - if (e.PropertyName is nameof(SqlEditorViewModel.OutputMessages) - or nameof(SqlEditorViewModel.SelectedOutputPane)) - { - UpdateCounters(); - } - } - - private void RefreshGrid(DataView? rowsView) - { - ResultGrid.Columns.Clear(); - ResultGrid.ItemsSource = null; - ResultGrid.IsReadOnly = true; - _inlineEditEligibility = SqlInlineEditEligibility.NotEligible; - _currentColumnIndexMap.Clear(); - _pendingInlineEdits.Clear(); - _lastSelectedRow = null; - _lastSelectedColumnIndex = -1; - - if (rowsView is null) - { - RowsCounterText.Text = L("sqlEditor.results.rows.countZero", "0 linhas"); - return; - } - - DataTable? table = rowsView.Table; - if (table is null || table.Columns.Count == 0) - { - RowsCounterText.Text = L("sqlEditor.results.rows.countZero", "0 linhas"); - return; - } - - _inlineEditEligibility = _viewModel?.EvaluateInlineEditEligibility(table) ?? SqlInlineEditEligibility.NotEligible; - ResultGrid.IsReadOnly = !_inlineEditEligibility.IsEligible; - - IReadOnlyList displayColumns = BuildDisplayColumns(table); - for (int displayIndex = 0; displayIndex < displayColumns.Count; displayIndex++) - { - DataColumn dataColumn = displayColumns[displayIndex]; - int capturedColumnIndex = table.Columns.IndexOf(dataColumn); - string columnName = dataColumn.ColumnName; - string columnTypeLabel = SqlEditorResultCellContentFormatter.GetColumnTypeLabel(dataColumn); - SqlEditorSchemaColumnItem? schemaColumn = _viewModel?.ResolveResultSchemaColumn(columnName); - _currentColumnIndexMap[columnName] = capturedColumnIndex; - - if (_viewModel?.IsResultColumnHidden(columnName) == true) - continue; - - ResultGrid.Columns.Add(new DataGridTemplateColumn - { - Header = BuildColumnHeader(columnName, columnTypeLabel, schemaColumn), - SortMemberPath = columnName, - IsReadOnly = !_inlineEditEligibility.IsEligible - || !_inlineEditEligibility.EditableColumns.Contains(columnName, StringComparer.OrdinalIgnoreCase), - CanUserSort = true, - CellTemplate = new FuncDataTemplate((row, _) => - { - string rawValue = SqlEditorResultCellContentFormatter.FormatCellValue(row, capturedColumnIndex); - bool canExpandCell = SqlEditorResultCellContentFormatter.ShouldOfferExpandedView(rawValue); - - var textBlock = new TextBlock - { - Text = rawValue, - Padding = new Avalonia.Thickness(12, 6), - VerticalAlignment = VerticalAlignment.Center, - TextTrimming = TextTrimming.CharacterEllipsis, - }; - if (IsDatabaseNull(row, capturedColumnIndex)) - { - textBlock.FontStyle = FontStyle.Italic; - textBlock.Foreground = ResolveNullCellForegroundBrush(); - } - - textBlock.PointerPressed += (_, _) => CaptureGridCellContext(row, capturedColumnIndex); - - var expandCellItem = new MenuItem { Header = L("sqlEditor.results.context.expandCell", "Expandir celula") }; - expandCellItem.IsEnabled = canExpandCell; - expandCellItem.Click += async (_, _) => - { - CaptureGridCellContext(row, capturedColumnIndex); - await ShowExpandedCellDialogAsync(row, capturedColumnIndex, columnName, columnTypeLabel); - }; - - var copyCellItem = new MenuItem { Header = L("sqlEditor.results.context.copyCell", "Copiar celula") }; - copyCellItem.Click += async (_, _) => - { - CaptureGridCellContext(row, capturedColumnIndex); - await CopyTextToClipboardAsync(SqlEditorResultCellContentFormatter.FormatCellValue(row, capturedColumnIndex)); - }; - - var copyRowItem = new MenuItem { Header = L("sqlEditor.results.context.copyRow", "Copiar linha") }; - copyRowItem.Click += async (_, _) => - { - CaptureGridCellContext(row, capturedColumnIndex); - await CopyTextToClipboardAsync(BuildRowClipboardText(row)); - }; - - var hideColumnItem = new MenuItem { Header = L("sqlEditor.results.context.hideColumn", "Ocultar coluna") }; - hideColumnItem.Click += (_, _) => - { - _viewModel?.HideResultColumn(columnName); - }; - - bool isPinned = _viewModel?.IsResultColumnPinned(columnName) == true; - var pinColumnItem = new MenuItem - { - Header = isPinned - ? L("sqlEditor.results.context.unpinColumn", "Desafixar coluna") - : L("sqlEditor.results.context.pinColumn", "Fixar coluna"), - }; - pinColumnItem.Click += (_, _) => - { - _viewModel?.SetResultColumnPinned(columnName, !isPinned); - }; - - textBlock.ContextMenu = new ContextMenu - { - ItemsSource = new object[] { expandCellItem, copyCellItem, copyRowItem, hideColumnItem, pinColumnItem }, - }; - - textBlock.DoubleTapped += async (_, _) => - { - if (!canExpandCell) - return; - - await ShowExpandedCellDialogAsync(row, capturedColumnIndex, columnName, columnTypeLabel); - }; - - return textBlock; - }), - CellEditingTemplate = new FuncDataTemplate((row, _) => - { - string text = SqlEditorResultCellContentFormatter.FormatCellValue(row, capturedColumnIndex); - return new TextBox - { - Text = string.Equals(text, "NULL", StringComparison.Ordinal) ? string.Empty : text, - MinWidth = 80, - HorizontalAlignment = HorizontalAlignment.Stretch, - VerticalAlignment = VerticalAlignment.Center, - }; - }), - }); - } - - int pinnedCount = _viewModel is null - ? 0 - : displayColumns.Count(column => _viewModel.IsResultColumnPinned(column.ColumnName)); - ResultGrid.FrozenColumnCount = Math.Clamp(pinnedCount, 0, ResultGrid.Columns.Count); - - var rows = new List(); - foreach (object? rowItem in rowsView) - { - if (rowItem is not DataRowView rowView) - continue; - - var values = new object?[table.Columns.Count]; - for (int i = 0; i < table.Columns.Count; i++) - values[i] = rowView.Row[i]; - - rows.Add(values); - } - - rows = ApplyFilter(rows, table); - rows = ApplySort(rows, table); - - int totalRows = table.Rows.Count; - RowsCounterText.Text = rows.Count == totalRows - ? string.Format(CultureInfo.InvariantCulture, L("sqlEditor.results.rows.countSingle", "{0} linhas"), totalRows) - : string.Format(CultureInfo.InvariantCulture, L("sqlEditor.results.rows.countFiltered", "{0} de {1} linhas"), rows.Count, totalRows); - - var observableRows = new ObservableCollection(rows); - - ResultGrid.ItemsSource = observableRows; - UpdateCounters(); - } - - private void ConfigureGridContextMenu() - { - var showAllColumnsItem = new MenuItem { Header = L("sqlEditor.results.context.showAllColumns", "Mostrar todas as colunas") }; - showAllColumnsItem.Click += (_, _) => _viewModel?.ShowAllResultColumns(); - - ResultGrid.ContextMenu = new ContextMenu - { - ItemsSource = new object[] { showAllColumnsItem }, - }; - } - - private void ShowAllColumnsButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (_viewModel is null) - return; - - _viewModel.ShowAllResultColumns(); - RefreshGrid(_viewModel.ResultRowsView); - } - - private void ClearResultFilterButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (_viewModel is null) - return; - - if (string.IsNullOrWhiteSpace(_viewModel.ResultGridFilterText)) - return; - - _viewModel.ResultGridFilterText = string.Empty; - RefreshGrid(_viewModel.ResultRowsView); - } - - private void UndoHiddenColumnButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (_viewModel is null) - return; - - if (!_viewModel.UndoLastHiddenResultColumn()) - return; - - RefreshGrid(_viewModel.ResultRowsView); - } - - private void ResultsPaneButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (_viewModel is null) - return; - - _viewModel.SelectedOutputPane = SqlEditorOutputPane.Results; - } - - private void MessagesPaneButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (_viewModel is null) - return; - - _viewModel.SelectedOutputPane = SqlEditorOutputPane.Messages; - } - - private void ClearMessagesButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (_viewModel is null) - return; - - _viewModel.ClearOutputMessages(); - } - - private async void ExportReportButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - if (_viewModel is null) - return; - - if (!_viewModel.TryBuildReportExportContext(out SqlEditorReportExportContext? context) || context is null) - { - _viewModel.PublishStatus( - L("sqlEditor.export.status.noResultTitle", "Nenhum resultado de execucao disponivel para exportacao."), - L("sqlEditor.export.status.noResultDetail", "Execute uma consulta primeiro."), - hasError: true); - return; - } - - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel is not Window owner || topLevel.StorageProvider is null) - return; - - var dialogVm = new SqlEditorReportExportDialogViewModel(context.TabTitle); - var dialog = new SqlEditorReportExportDialogWindow(dialogVm); - - await dialog.ShowDialog(owner); - if (!dialog.WasConfirmed) - return; - - string normalizedExtension = dialogVm.SuggestedExtension.TrimStart('.'); - var reportFileType = GetExportFileType(dialogVm.SelectedType?.Type ?? SqlEditorReportType.HtmlFullFeature); - - IStorageFile? file = await topLevel.StorageProvider.SaveFilePickerAsync( - new FilePickerSaveOptions - { - Title = L("sqlEditor.export.pickerTitle", "Exportar dados SQL"), - DefaultExtension = normalizedExtension, - SuggestedFileName = dialogVm.FileName, - FileTypeChoices = [reportFileType], - }); - - string? outputPath = file?.TryGetLocalPath(); - if (string.IsNullOrWhiteSpace(outputPath)) - return; - - try - { - SqlEditorReportExportRequest request = dialogVm.BuildRequest(outputPath); - string writtenPath = await _reportExportService.ExportAsync(context, request); - _viewModel.PublishStatus(L("sqlEditor.export.status.successTitle", "Relatorio exportado."), writtenPath); - } - catch (Exception ex) - { - _viewModel.PublishStatus(L("sqlEditor.export.status.failedTitle", "Falha ao exportar relatorio."), ex.Message, hasError: true); - } - } - - private void CaptureGridCellContext(object?[]? row, int columnIndex) - { - _lastSelectedRow = row; - _lastSelectedColumnIndex = columnIndex; - } - - private async void OnResultGridKeyDown(object? sender, KeyEventArgs e) - { - bool isCopy = e.Key == Key.C && e.KeyModifiers.HasFlag(KeyModifiers.Control); - if (!isCopy) - return; - - if (_lastSelectedRow is null) - return; - - bool copyRow = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - string text = copyRow - ? BuildRowClipboardText(_lastSelectedRow) - : SqlEditorResultCellContentFormatter.FormatCellValue(_lastSelectedRow, _lastSelectedColumnIndex); - - await CopyTextToClipboardAsync(text); - e.Handled = true; - } - - private string BuildRowClipboardText(object?[]? row) - { - if (row is null || row.Length == 0) - return string.Empty; - - var values = new string[row.Length]; - for (int i = 0; i < row.Length; i++) - values[i] = SqlEditorResultCellContentFormatter.FormatCellValue(row, i); - - return string.Join('\t', values); - } - - private async Task CopyTextToClipboardAsync(string text) - { - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel?.Clipboard is null) - return; - - await topLevel.Clipboard.SetTextAsync(text ?? string.Empty); - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } - - private void UpdateCounters() - { - if (_viewModel is null) - return; - - if (_viewModel.IsMessagesOutputPaneSelected) - { - RowsCounterText.Text = string.Format( - CultureInfo.InvariantCulture, - L("sqlEditor.messages.count", "{0} mensagens"), - _viewModel.OutputMessages.Count); - } - } - - private List ApplyFilter(List rows, DataTable table) - { - if (_viewModel is null || rows.Count == 0) - return rows; - - string filter = _viewModel.ResultGridFilterText; - if (string.IsNullOrWhiteSpace(filter)) - return rows; - - string needle = filter.Trim(); - return rows.Where(row => RowMatchesFilter(row, table, needle)).ToList(); - } - - private List ApplySort(List rows, DataTable table) - { - if (_viewModel is null || rows.Count == 0) - return rows; - - string? sortColumn = _viewModel.ResultGridSortColumn; - if (string.IsNullOrWhiteSpace(sortColumn)) - return rows; - - int sortIndex = table.Columns.IndexOf(sortColumn); - if (sortIndex < 0) - return rows; - - return _viewModel.ResultGridSortAscending - ? rows.OrderBy(row => ToSortKey(row, sortIndex), StringComparer.OrdinalIgnoreCase).ToList() - : rows.OrderByDescending(row => ToSortKey(row, sortIndex), StringComparer.OrdinalIgnoreCase).ToList(); - } - - private static bool RowMatchesFilter(object?[] row, DataTable table, string needle) - { - for (int i = 0; i < table.Columns.Count; i++) - { - if (TextSearch.Matches(needle, SqlEditorResultCellContentFormatter.FormatCellValue(row, i))) - return true; - } - - return false; - } - - private static string ToSortKey(object?[] row, int index) - { - string value = SqlEditorResultCellContentFormatter.FormatCellValue(row, index); - if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double numeric)) - return numeric.ToString("00000000000000000000.000000", CultureInfo.InvariantCulture); - - return value; - } - - private void OnResultGridCellEditEnding(object? sender, DataGridCellEditEndingEventArgs e) - { - if (_viewModel is null || !_inlineEditEligibility.IsEligible) - return; - - if (e.EditAction != DataGridEditAction.Commit) - return; - - if (e.Row.DataContext is not object?[] row) - return; - - string columnName = e.Column.SortMemberPath ?? string.Empty; - if (string.IsNullOrWhiteSpace(columnName)) - return; - - if (!_inlineEditEligibility.EditableColumns.Contains(columnName, StringComparer.OrdinalIgnoreCase)) - return; - - if (!_currentColumnIndexMap.TryGetValue(columnName, out int columnIndex)) - return; - - if (e.EditingElement is not TextBox textBox) - return; - - string originalText = SqlEditorResultCellContentFormatter.FormatCellValue(row, columnIndex); - string editedText = textBox.Text ?? string.Empty; - if (string.Equals(originalText, editedText, StringComparison.Ordinal)) - return; - - _pendingInlineEdits[row] = new PendingInlineEdit(columnName, columnIndex, editedText); - } - - private async void OnResultGridCellEditEnded(object? sender, DataGridCellEditEndedEventArgs e) - { - if (_viewModel is null || !_inlineEditEligibility.IsEligible) - return; - - if (e.EditAction != DataGridEditAction.Commit) - return; - - if (e.Row.DataContext is not object?[] row) - return; - - if (!_pendingInlineEdits.TryGetValue(row, out PendingInlineEdit? pending)) - return; - - _pendingInlineEdits.Remove(row); - try - { - string? tableFullName = _inlineEditEligibility.TableFullName; - if (string.IsNullOrWhiteSpace(tableFullName)) - return; - - var pkValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (string pkColumn in _inlineEditEligibility.PrimaryKeyColumns) - { - if (!_currentColumnIndexMap.TryGetValue(pkColumn, out int pkIndex)) - return; - - pkValues[pkColumn] = row[pkIndex]; - } - - object? editedValue = string.IsNullOrWhiteSpace(pending.EditedText) - ? DBNull.Value - : pending.EditedText; - string updateSql = SqlInlineUpdateStatementBuilder.Build( - _viewModel.ActiveTabProvider, - tableFullName, - pending.ColumnName, - editedValue, - pkValues); - - SqlEditorResultSet result = await _viewModel.ExecuteInlineUpdateAsync(updateSql); - if (!result.Success) - return; - - row[pending.ColumnIndex] = editedValue; - ResultGrid.InvalidateVisual(); - } - catch (Exception ex) - { - _viewModel.PublishStatus( - L("sqlEditor.inlineEdit.failed", "Falha ao atualizar celula."), - ex.Message, - hasError: true); - } - } - - private void OnResultGridSorting(object? sender, DataGridColumnEventArgs e) - { - if (_viewModel is null) - return; - - string column = e.Column.SortMemberPath ?? e.Column.Header?.ToString() ?? string.Empty; - if (string.IsNullOrWhiteSpace(column)) - return; - - bool ascending = true; - bool clearSorting = false; - if (string.Equals(_viewModel.ResultGridSortColumn, column, StringComparison.Ordinal)) - { - if (_viewModel.ResultGridSortAscending) - { - ascending = false; - } - else - { - clearSorting = true; - } - } - - _viewModel.SetResultGridSort(clearSorting ? null : column, ascending); - RefreshGrid(_viewModel.ResultRowsView); - e.Handled = true; - } - - private void OnResultGridColumnReordered(object? sender, DataGridColumnEventArgs e) - { - if (_viewModel is null || ResultGrid.Columns.Count == 0) - return; - - IReadOnlyList order = ResultGrid.Columns - .OrderBy(static column => column.DisplayIndex) - .Select(column => column.SortMemberPath ?? column.Header?.ToString() ?? string.Empty) - .Where(static name => !string.IsNullOrWhiteSpace(name)) - .ToList(); - if (order.Count == 0) - return; - - _viewModel.SetResultColumnOrder(order); - } - - private IReadOnlyList BuildDisplayColumns(DataTable table) - { - if (_viewModel is null || table.Columns.Count == 0) - return table.Columns.Cast().ToList(); - - IReadOnlyList order = _viewModel.ActiveTab.ResultColumnOrder; - Dictionary orderMap = order - .Select((name, index) => (name, index)) - .GroupBy(static x => x.name, StringComparer.OrdinalIgnoreCase) - .ToDictionary(static g => g.Key, static g => g.First().index, StringComparer.OrdinalIgnoreCase); - - List columns = table.Columns - .Cast() - .Where(column => !_viewModel.IsResultColumnHidden(column.ColumnName)) - .OrderByDescending(column => _viewModel.IsResultColumnPinned(column.ColumnName)) - .ThenBy(column => orderMap.TryGetValue(column.ColumnName, out int index) ? index : int.MaxValue) - .ThenBy(static column => column.Ordinal) - .ToList(); - - return columns; - } - - private object BuildColumnHeader( - string columnName, - string columnTypeLabel, - SqlEditorSchemaColumnItem? schemaColumn) - { - var titleRow = new StackPanel - { - Orientation = Orientation.Horizontal, - Spacing = 6, - }; - - titleRow.Children.Add(new MaterialIcon - { - Kind = schemaColumn?.TypeIcon ?? MaterialIconKind.TableColumn, - Width = 12, - Height = 12, - VerticalAlignment = VerticalAlignment.Center, - Foreground = ResolveResourceBrush("AccentPrimaryHoverBrush", Brushes.LightBlue), - }); - titleRow.Children.Add(new TextBlock - { - Text = columnName, - FontWeight = ResolveResourceFontWeight("FontWeightTitle", FontWeight.SemiBold), - }); - if (schemaColumn?.IsPrimaryKey == true) - { - titleRow.Children.Add(new MaterialIcon - { - Kind = MaterialIconKind.KeyVariant, - Width = 11, - Height = 11, - Foreground = new SolidColorBrush(Color.Parse("#5CE59D")), - }); - } - if (schemaColumn?.IsForeignKey == true) - { - var fkButton = new Button - { - Background = Brushes.Transparent, - BorderBrush = Brushes.Transparent, - BorderThickness = new Thickness(0), - Padding = new Thickness(0), - Width = 14, - Height = 14, - Content = new MaterialIcon - { - Kind = MaterialIconKind.SourceBranch, - Width = 11, - Height = 11, - Foreground = new SolidColorBrush(Color.Parse("#72B8FF")), - }, - }; - ToolTip.SetTip(fkButton, L("sqlEditor.results.relationship.quick", "Abrir relacionamento")); - fkButton.Click += async (_, _) => await ShowRelationshipDialogAsync(columnName); - titleRow.Children.Add(fkButton); - } - if (schemaColumn?.IsIndexed == true) - { - titleRow.Children.Add(new MaterialIcon - { - Kind = MaterialIconKind.Magnify, - Width = 11, - Height = 11, - Foreground = new SolidColorBrush(Color.Parse("#FFCF75")), - }); - } - - var header = new StackPanel { Spacing = 0 }; - header.Children.Add(titleRow); - header.Children.Add(new TextBlock - { - Text = columnTypeLabel, - FontSize = ResolveResourceFontSize("FontSizeCaption", 11), - Foreground = ResolveResourceBrush("TextMutedBrush", Brushes.Gray), - }); - - return header; - } - - private async Task ShowRelationshipDialogAsync(string columnName) - { - if (_viewModel is null || string.IsNullOrWhiteSpace(columnName)) - return; - - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel is not Window owner) - return; - - IReadOnlyList relations = _viewModel.ResolveColumnRelationships(columnName); - var dialog = new SqlEditorForeignKeyDialogWindow(columnName, relations); - await dialog.ShowDialog(owner); - } - - private async Task ShowExpandedCellDialogAsync(object?[]? row, int columnIndex, string columnName, string columnTypeLabel) - { - TopLevel? topLevel = TopLevel.GetTopLevel(this); - if (topLevel is not Window owner) - return; - - string rawValue = SqlEditorResultCellContentFormatter.FormatCellValue(row, columnIndex); - if (!SqlEditorResultCellContentFormatter.ShouldOfferExpandedView(rawValue)) - return; - - string expandedValue = SqlEditorResultCellContentFormatter.FormatExpandedCellValue(rawValue); - - var dialog = new SqlEditorCellExpandDialogWindow(columnName, columnTypeLabel, expandedValue); - await dialog.ShowDialog(owner); - } - - private static bool IsDatabaseNull(object?[]? row, int index) - { - if (row is null || index < 0 || index >= row.Length) - return false; - - return row[index] is DBNull; - } - - private IBrush ResolveNullCellForegroundBrush() - { - if (_nullCellForegroundBrush is not null) - return _nullCellForegroundBrush; - - if (Application.Current?.TryGetResource("TextSecondaryBrush", null, out object? resource) == true - && resource is IBrush brush) - { - _nullCellForegroundBrush = brush; - return brush; - } - - _nullCellForegroundBrush = Brushes.Gray; - return _nullCellForegroundBrush; - } - - private static IBrush ResolveResourceBrush(string resourceKey, IBrush fallback) - { - if (Application.Current?.TryGetResource(resourceKey, null, out object? resource) == true - && resource is IBrush brush) - { - return brush; - } - - return fallback; - } - - private static double ResolveResourceFontSize(string resourceKey, double fallback) - { - if (Application.Current?.TryGetResource(resourceKey, null, out object? resource) == true) - { - if (resource is double size) - return size; - if (resource is int intSize) - return intSize; - } - - return fallback; - } - - private static FontWeight ResolveResourceFontWeight(string resourceKey, FontWeight fallback) - { - if (Application.Current?.TryGetResource(resourceKey, null, out object? resource) == true - && resource is FontWeight fontWeight) - { - return fontWeight; - } - - return fallback; - } - - private static FilePickerFileType GetExportFileType(SqlEditorReportType reportType) - { - return reportType switch - { - SqlEditorReportType.JsonContract => new FilePickerFileType(L("sqlEditor.export.fileType.json", "Arquivo JSON")) - { - Patterns = ["*.json"], - MimeTypes = ["application/json", "text/plain"], - }, - SqlEditorReportType.CsvData => new FilePickerFileType(L("sqlEditor.export.fileType.csv", "Arquivo CSV")) - { - Patterns = ["*.csv"], - MimeTypes = ["text/csv", "text/plain"], - }, - SqlEditorReportType.ExcelWorkbook => new FilePickerFileType(L("sqlEditor.export.fileType.xlsx", "Pasta de trabalho Excel")) - { - Patterns = ["*.xlsx"], - MimeTypes = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], - }, - _ => new FilePickerFileType(L("sqlEditor.export.fileType.html", "Arquivo HTML")) - { - Patterns = ["*.html", "*.htm"], - MimeTypes = ["text/html", "text/plain"], - }, - }; - } - - private sealed record PendingInlineEdit( - string ColumnName, - int ColumnIndex, - string EditedText); - - private sealed class ObjectArrayReferenceComparer : IEqualityComparer - { - public static ObjectArrayReferenceComparer Instance { get; } = new(); - - public bool Equals(object?[]? x, object?[]? y) => ReferenceEquals(x, y); - - public int GetHashCode(object?[] obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); - } -} - +using System.ComponentModel; +using System.Collections.ObjectModel; +using System.Data; +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform.Storage; +using Material.Icons; +using Material.Icons.Avalonia; +using AkkornStudio.UI.Services.SqlEditor; +using AkkornStudio.UI.Services.Localization; +using AkkornStudio.UI.Services.Search; +using AkkornStudio.UI.Services.SqlEditor.Reports; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Controls.SqlEditor; + +public partial class SqlEditorResultPanel : UserControl +{ + private static readonly TextSearchService TextSearch = new(); + private SqlEditorViewModel? _viewModel; + private object?[]? _lastSelectedRow; + private int _lastSelectedColumnIndex = -1; + private IBrush? _nullCellForegroundBrush; + private readonly SqlEditorReportExportService _reportExportService = new(); + private SqlInlineEditEligibility _inlineEditEligibility = SqlInlineEditEligibility.NotEligible; + private readonly Dictionary _currentColumnIndexMap = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _pendingInlineEdits = new(ObjectArrayReferenceComparer.Instance); + + public SqlEditorResultPanel() + { + InitializeComponent(); + DataContextChanged += OnDataContextChanged; + ResultGrid.KeyDown += OnResultGridKeyDown; + ResultGrid.Sorting += OnResultGridSorting; + ResultGrid.CellEditEnding += OnResultGridCellEditEnding; + ResultGrid.CellEditEnded += OnResultGridCellEditEnded; + ResultGrid.ColumnReordered += OnResultGridColumnReordered; + ConfigureGridContextMenu(); + } + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (_viewModel is not null) + _viewModel.PropertyChanged -= OnViewModelPropertyChanged; + + _viewModel = DataContext as SqlEditorViewModel; + + if (_viewModel is not null) + { + _viewModel.PropertyChanged += OnViewModelPropertyChanged; + RefreshGrid(_viewModel.ResultRowsView); + } + else + { + RefreshGrid(null); + RowsCounterText.Text = "0 linhas"; + } + } + + private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(SqlEditorViewModel.ResultRowsView) + or nameof(SqlEditorViewModel.SelectedResultTabIndex) + or nameof(SqlEditorViewModel.ResultTabs) + or nameof(SqlEditorViewModel.ResultGridFilterText) + or nameof(SqlEditorViewModel.ResultGridSortColumn) + or nameof(SqlEditorViewModel.ResultGridSortAscending) + or nameof(SqlEditorViewModel.SelectedOutputPane)) + { + RefreshGrid(_viewModel?.ResultRowsView); + } + + if (e.PropertyName is nameof(SqlEditorViewModel.OutputMessages) + or nameof(SqlEditorViewModel.SelectedOutputPane)) + { + UpdateCounters(); + } + } + + private void RefreshGrid(DataView? rowsView) + { + ResultGrid.Columns.Clear(); + ResultGrid.ItemsSource = null; + ResultGrid.IsReadOnly = true; + _inlineEditEligibility = SqlInlineEditEligibility.NotEligible; + _currentColumnIndexMap.Clear(); + _pendingInlineEdits.Clear(); + _lastSelectedRow = null; + _lastSelectedColumnIndex = -1; + + if (rowsView is null) + { + RowsCounterText.Text = L("sqlEditor.results.rows.countZero", "0 linhas"); + return; + } + + DataTable? table = rowsView.Table; + if (table is null || table.Columns.Count == 0) + { + RowsCounterText.Text = L("sqlEditor.results.rows.countZero", "0 linhas"); + return; + } + + _inlineEditEligibility = _viewModel?.EvaluateInlineEditEligibility(table) ?? SqlInlineEditEligibility.NotEligible; + ResultGrid.IsReadOnly = !_inlineEditEligibility.IsEligible; + + IReadOnlyList displayColumns = BuildDisplayColumns(table); + for (int displayIndex = 0; displayIndex < displayColumns.Count; displayIndex++) + { + DataColumn dataColumn = displayColumns[displayIndex]; + int capturedColumnIndex = table.Columns.IndexOf(dataColumn); + string columnName = dataColumn.ColumnName; + string columnTypeLabel = SqlEditorResultCellContentFormatter.GetColumnTypeLabel(dataColumn); + SqlEditorSchemaColumnItem? schemaColumn = _viewModel?.ResolveResultSchemaColumn(columnName); + _currentColumnIndexMap[columnName] = capturedColumnIndex; + + if (_viewModel?.IsResultColumnHidden(columnName) == true) + continue; + + ResultGrid.Columns.Add(new DataGridTemplateColumn + { + Header = BuildColumnHeader(columnName, columnTypeLabel, schemaColumn), + SortMemberPath = columnName, + IsReadOnly = !_inlineEditEligibility.IsEligible + || !_inlineEditEligibility.EditableColumns.Contains(columnName, StringComparer.OrdinalIgnoreCase), + CanUserSort = true, + CellTemplate = new FuncDataTemplate((row, _) => + { + string rawValue = SqlEditorResultCellContentFormatter.FormatCellValue(row, capturedColumnIndex); + bool canExpandCell = SqlEditorResultCellContentFormatter.ShouldOfferExpandedView(rawValue); + + var textBlock = new TextBlock + { + Text = rawValue, + Padding = new Avalonia.Thickness(12, 6), + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + }; + if (IsDatabaseNull(row, capturedColumnIndex)) + { + textBlock.FontStyle = FontStyle.Italic; + textBlock.Foreground = ResolveNullCellForegroundBrush(); + } + + textBlock.PointerPressed += (_, _) => CaptureGridCellContext(row, capturedColumnIndex); + + var expandCellItem = new MenuItem { Header = L("sqlEditor.results.context.expandCell", "Expandir celula") }; + expandCellItem.IsEnabled = canExpandCell; + expandCellItem.Click += async (_, _) => + { + CaptureGridCellContext(row, capturedColumnIndex); + await ShowExpandedCellDialogAsync(row, capturedColumnIndex, columnName, columnTypeLabel); + }; + + var copyCellItem = new MenuItem { Header = L("sqlEditor.results.context.copyCell", "Copiar celula") }; + copyCellItem.Click += async (_, _) => + { + CaptureGridCellContext(row, capturedColumnIndex); + await CopyTextToClipboardAsync(SqlEditorResultCellContentFormatter.FormatCellValue(row, capturedColumnIndex)); + }; + + var copyRowItem = new MenuItem { Header = L("sqlEditor.results.context.copyRow", "Copiar linha") }; + copyRowItem.Click += async (_, _) => + { + CaptureGridCellContext(row, capturedColumnIndex); + await CopyTextToClipboardAsync(BuildRowClipboardText(row)); + }; + + var hideColumnItem = new MenuItem { Header = L("sqlEditor.results.context.hideColumn", "Ocultar coluna") }; + hideColumnItem.Click += (_, _) => + { + _viewModel?.HideResultColumn(columnName); + }; + + bool isPinned = _viewModel?.IsResultColumnPinned(columnName) == true; + var pinColumnItem = new MenuItem + { + Header = isPinned + ? L("sqlEditor.results.context.unpinColumn", "Desafixar coluna") + : L("sqlEditor.results.context.pinColumn", "Fixar coluna"), + }; + pinColumnItem.Click += (_, _) => + { + _viewModel?.SetResultColumnPinned(columnName, !isPinned); + }; + + textBlock.ContextMenu = new ContextMenu + { + ItemsSource = new object[] { expandCellItem, copyCellItem, copyRowItem, hideColumnItem, pinColumnItem }, + }; + + textBlock.DoubleTapped += async (_, _) => + { + if (!canExpandCell) + return; + + await ShowExpandedCellDialogAsync(row, capturedColumnIndex, columnName, columnTypeLabel); + }; + + return textBlock; + }), + CellEditingTemplate = new FuncDataTemplate((row, _) => + { + string text = SqlEditorResultCellContentFormatter.FormatCellValue(row, capturedColumnIndex); + return new TextBox + { + Text = string.Equals(text, "NULL", StringComparison.Ordinal) ? string.Empty : text, + MinWidth = 80, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Center, + }; + }), + }); + } + + int pinnedCount = _viewModel is null + ? 0 + : displayColumns.Count(column => _viewModel.IsResultColumnPinned(column.ColumnName)); + ResultGrid.FrozenColumnCount = Math.Clamp(pinnedCount, 0, ResultGrid.Columns.Count); + + var rows = new List(); + foreach (object? rowItem in rowsView) + { + if (rowItem is not DataRowView rowView) + continue; + + var values = new object?[table.Columns.Count]; + for (int i = 0; i < table.Columns.Count; i++) + values[i] = rowView.Row[i]; + + rows.Add(values); + } + + rows = ApplyFilter(rows, table); + rows = ApplySort(rows, table); + + int totalRows = table.Rows.Count; + RowsCounterText.Text = rows.Count == totalRows + ? string.Format(CultureInfo.InvariantCulture, L("sqlEditor.results.rows.countSingle", "{0} linhas"), totalRows) + : string.Format(CultureInfo.InvariantCulture, L("sqlEditor.results.rows.countFiltered", "{0} de {1} linhas"), rows.Count, totalRows); + + var observableRows = new ObservableCollection(rows); + + ResultGrid.ItemsSource = observableRows; + UpdateCounters(); + } + + private void ConfigureGridContextMenu() + { + var showAllColumnsItem = new MenuItem { Header = L("sqlEditor.results.context.showAllColumns", "Mostrar todas as colunas") }; + showAllColumnsItem.Click += (_, _) => _viewModel?.ShowAllResultColumns(); + + ResultGrid.ContextMenu = new ContextMenu + { + ItemsSource = new object[] { showAllColumnsItem }, + }; + } + + private void ShowAllColumnsButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_viewModel is null) + return; + + _viewModel.ShowAllResultColumns(); + RefreshGrid(_viewModel.ResultRowsView); + } + + private void ClearResultFilterButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_viewModel is null) + return; + + if (string.IsNullOrWhiteSpace(_viewModel.ResultGridFilterText)) + return; + + _viewModel.ResultGridFilterText = string.Empty; + RefreshGrid(_viewModel.ResultRowsView); + } + + private void UndoHiddenColumnButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_viewModel is null) + return; + + if (!_viewModel.UndoLastHiddenResultColumn()) + return; + + RefreshGrid(_viewModel.ResultRowsView); + } + + private void ResultsPaneButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_viewModel is null) + return; + + _viewModel.SelectedOutputPane = SqlEditorOutputPane.Results; + } + + private void MessagesPaneButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_viewModel is null) + return; + + _viewModel.SelectedOutputPane = SqlEditorOutputPane.Messages; + } + + private void ClearMessagesButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_viewModel is null) + return; + + _viewModel.ClearOutputMessages(); + } + + private async void ExportReportButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (_viewModel is null) + return; + + if (!_viewModel.TryBuildReportExportContext(out SqlEditorReportExportContext? context) || context is null) + { + _viewModel.PublishStatus( + L("sqlEditor.export.status.noResultTitle", "Nenhum resultado de execucao disponivel para exportacao."), + L("sqlEditor.export.status.noResultDetail", "Execute uma consulta primeiro."), + hasError: true); + return; + } + + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel is not Window owner || topLevel.StorageProvider is null) + return; + + var dialogVm = new SqlEditorReportExportDialogViewModel(context.TabTitle); + var dialog = new SqlEditorReportExportDialogWindow(dialogVm); + + await dialog.ShowDialog(owner); + if (!dialog.WasConfirmed) + return; + + string normalizedExtension = dialogVm.SuggestedExtension.TrimStart('.'); + var reportFileType = GetExportFileType(dialogVm.SelectedType?.Type ?? SqlEditorReportType.HtmlFullFeature); + + IStorageFile? file = await topLevel.StorageProvider.SaveFilePickerAsync( + new FilePickerSaveOptions + { + Title = L("sqlEditor.export.pickerTitle", "Exportar dados SQL"), + DefaultExtension = normalizedExtension, + SuggestedFileName = dialogVm.FileName, + FileTypeChoices = [reportFileType], + }); + + string? outputPath = file?.TryGetLocalPath(); + if (string.IsNullOrWhiteSpace(outputPath)) + return; + + try + { + SqlEditorReportExportRequest request = dialogVm.BuildRequest(outputPath); + string writtenPath = await _reportExportService.ExportAsync(context, request); + _viewModel.PublishStatus(L("sqlEditor.export.status.successTitle", "Relatorio exportado."), writtenPath); + } + catch (Exception ex) + { + _viewModel.PublishStatus(L("sqlEditor.export.status.failedTitle", "Falha ao exportar relatorio."), ex.Message, hasError: true); + } + } + + private void CaptureGridCellContext(object?[]? row, int columnIndex) + { + _lastSelectedRow = row; + _lastSelectedColumnIndex = columnIndex; + } + + private async void OnResultGridKeyDown(object? sender, KeyEventArgs e) + { + bool isCopy = e.Key == Key.C && e.KeyModifiers.HasFlag(KeyModifiers.Control); + if (!isCopy) + return; + + if (_lastSelectedRow is null) + return; + + bool copyRow = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + string text = copyRow + ? BuildRowClipboardText(_lastSelectedRow) + : SqlEditorResultCellContentFormatter.FormatCellValue(_lastSelectedRow, _lastSelectedColumnIndex); + + await CopyTextToClipboardAsync(text); + e.Handled = true; + } + + private string BuildRowClipboardText(object?[]? row) + { + if (row is null || row.Length == 0) + return string.Empty; + + var values = new string[row.Length]; + for (int i = 0; i < row.Length; i++) + values[i] = SqlEditorResultCellContentFormatter.FormatCellValue(row, i); + + return string.Join('\t', values); + } + + private async Task CopyTextToClipboardAsync(string text) + { + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel?.Clipboard is null) + return; + + await topLevel.Clipboard.SetTextAsync(text ?? string.Empty); + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } + + private void UpdateCounters() + { + if (_viewModel is null) + return; + + if (_viewModel.IsMessagesOutputPaneSelected) + { + RowsCounterText.Text = string.Format( + CultureInfo.InvariantCulture, + L("sqlEditor.messages.count", "{0} mensagens"), + _viewModel.OutputMessages.Count); + } + } + + private List ApplyFilter(List rows, DataTable table) + { + if (_viewModel is null || rows.Count == 0) + return rows; + + string filter = _viewModel.ResultGridFilterText; + if (string.IsNullOrWhiteSpace(filter)) + return rows; + + string needle = filter.Trim(); + return rows.Where(row => RowMatchesFilter(row, table, needle)).ToList(); + } + + private List ApplySort(List rows, DataTable table) + { + if (_viewModel is null || rows.Count == 0) + return rows; + + string? sortColumn = _viewModel.ResultGridSortColumn; + if (string.IsNullOrWhiteSpace(sortColumn)) + return rows; + + int sortIndex = table.Columns.IndexOf(sortColumn); + if (sortIndex < 0) + return rows; + + return _viewModel.ResultGridSortAscending + ? rows.OrderBy(row => ToSortKey(row, sortIndex), StringComparer.OrdinalIgnoreCase).ToList() + : rows.OrderByDescending(row => ToSortKey(row, sortIndex), StringComparer.OrdinalIgnoreCase).ToList(); + } + + private static bool RowMatchesFilter(object?[] row, DataTable table, string needle) + { + for (int i = 0; i < table.Columns.Count; i++) + { + if (TextSearch.Matches(needle, SqlEditorResultCellContentFormatter.FormatCellValue(row, i))) + return true; + } + + return false; + } + + private static string ToSortKey(object?[] row, int index) + { + string value = SqlEditorResultCellContentFormatter.FormatCellValue(row, index); + if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double numeric)) + return numeric.ToString("00000000000000000000.000000", CultureInfo.InvariantCulture); + + return value; + } + + private void OnResultGridCellEditEnding(object? sender, DataGridCellEditEndingEventArgs e) + { + if (_viewModel is null || !_inlineEditEligibility.IsEligible) + return; + + if (e.EditAction != DataGridEditAction.Commit) + return; + + if (e.Row.DataContext is not object?[] row) + return; + + string columnName = e.Column.SortMemberPath ?? string.Empty; + if (string.IsNullOrWhiteSpace(columnName)) + return; + + if (!_inlineEditEligibility.EditableColumns.Contains(columnName, StringComparer.OrdinalIgnoreCase)) + return; + + if (!_currentColumnIndexMap.TryGetValue(columnName, out int columnIndex)) + return; + + if (e.EditingElement is not TextBox textBox) + return; + + string originalText = SqlEditorResultCellContentFormatter.FormatCellValue(row, columnIndex); + string editedText = textBox.Text ?? string.Empty; + if (string.Equals(originalText, editedText, StringComparison.Ordinal)) + return; + + _pendingInlineEdits[row] = new PendingInlineEdit(columnName, columnIndex, editedText); + } + + private async void OnResultGridCellEditEnded(object? sender, DataGridCellEditEndedEventArgs e) + { + if (_viewModel is null || !_inlineEditEligibility.IsEligible) + return; + + if (e.EditAction != DataGridEditAction.Commit) + return; + + if (e.Row.DataContext is not object?[] row) + return; + + if (!_pendingInlineEdits.TryGetValue(row, out PendingInlineEdit? pending)) + return; + + _pendingInlineEdits.Remove(row); + try + { + string? tableFullName = _inlineEditEligibility.TableFullName; + if (string.IsNullOrWhiteSpace(tableFullName)) + return; + + var pkValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string pkColumn in _inlineEditEligibility.PrimaryKeyColumns) + { + if (!_currentColumnIndexMap.TryGetValue(pkColumn, out int pkIndex)) + return; + + pkValues[pkColumn] = row[pkIndex]; + } + + object? editedValue = string.IsNullOrWhiteSpace(pending.EditedText) + ? DBNull.Value + : pending.EditedText; + string updateSql = SqlInlineUpdateStatementBuilder.Build( + _viewModel.ActiveTabProvider, + tableFullName, + pending.ColumnName, + editedValue, + pkValues); + + SqlEditorResultSet result = await _viewModel.ExecuteInlineUpdateAsync(updateSql); + if (!result.Success) + return; + + row[pending.ColumnIndex] = editedValue; + ResultGrid.InvalidateVisual(); + } + catch (Exception ex) + { + _viewModel.PublishStatus( + L("sqlEditor.inlineEdit.failed", "Falha ao atualizar celula."), + ex.Message, + hasError: true); + } + } + + private void OnResultGridSorting(object? sender, DataGridColumnEventArgs e) + { + if (_viewModel is null) + return; + + string column = e.Column.SortMemberPath ?? e.Column.Header?.ToString() ?? string.Empty; + if (string.IsNullOrWhiteSpace(column)) + return; + + bool ascending = true; + bool clearSorting = false; + if (string.Equals(_viewModel.ResultGridSortColumn, column, StringComparison.Ordinal)) + { + if (_viewModel.ResultGridSortAscending) + { + ascending = false; + } + else + { + clearSorting = true; + } + } + + _viewModel.SetResultGridSort(clearSorting ? null : column, ascending); + RefreshGrid(_viewModel.ResultRowsView); + e.Handled = true; + } + + private void OnResultGridColumnReordered(object? sender, DataGridColumnEventArgs e) + { + if (_viewModel is null || ResultGrid.Columns.Count == 0) + return; + + IReadOnlyList order = ResultGrid.Columns + .OrderBy(static column => column.DisplayIndex) + .Select(column => column.SortMemberPath ?? column.Header?.ToString() ?? string.Empty) + .Where(static name => !string.IsNullOrWhiteSpace(name)) + .ToList(); + if (order.Count == 0) + return; + + _viewModel.SetResultColumnOrder(order); + } + + private IReadOnlyList BuildDisplayColumns(DataTable table) + { + if (_viewModel is null || table.Columns.Count == 0) + return table.Columns.Cast().ToList(); + + IReadOnlyList order = _viewModel.ActiveTab.ResultColumnOrder; + Dictionary orderMap = order + .Select((name, index) => (name, index)) + .GroupBy(static x => x.name, StringComparer.OrdinalIgnoreCase) + .ToDictionary(static g => g.Key, static g => g.First().index, StringComparer.OrdinalIgnoreCase); + + List columns = table.Columns + .Cast() + .Where(column => !_viewModel.IsResultColumnHidden(column.ColumnName)) + .OrderByDescending(column => _viewModel.IsResultColumnPinned(column.ColumnName)) + .ThenBy(column => orderMap.TryGetValue(column.ColumnName, out int index) ? index : int.MaxValue) + .ThenBy(static column => column.Ordinal) + .ToList(); + + return columns; + } + + private object BuildColumnHeader( + string columnName, + string columnTypeLabel, + SqlEditorSchemaColumnItem? schemaColumn) + { + var titleRow = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 6, + }; + + titleRow.Children.Add(new MaterialIcon + { + Kind = schemaColumn?.TypeIcon ?? MaterialIconKind.TableColumn, + Width = 12, + Height = 12, + VerticalAlignment = VerticalAlignment.Center, + Foreground = ResolveResourceBrush("AccentPrimaryHoverBrush", Brushes.LightBlue), + }); + titleRow.Children.Add(new TextBlock + { + Text = columnName, + FontWeight = ResolveResourceFontWeight("FontWeightTitle", FontWeight.SemiBold), + }); + if (schemaColumn?.IsPrimaryKey == true) + { + titleRow.Children.Add(new MaterialIcon + { + Kind = MaterialIconKind.KeyVariant, + Width = 11, + Height = 11, + Foreground = new SolidColorBrush(Color.Parse("#5CE59D")), + }); + } + if (schemaColumn?.IsForeignKey == true) + { + var fkButton = new Button + { + Background = Brushes.Transparent, + BorderBrush = Brushes.Transparent, + BorderThickness = new Thickness(0), + Padding = new Thickness(0), + Width = 14, + Height = 14, + Content = new MaterialIcon + { + Kind = MaterialIconKind.SourceBranch, + Width = 11, + Height = 11, + Foreground = new SolidColorBrush(Color.Parse("#72B8FF")), + }, + }; + ToolTip.SetTip(fkButton, L("sqlEditor.results.relationship.quick", "Abrir relacionamento")); + fkButton.Click += async (_, _) => await ShowRelationshipDialogAsync(columnName); + titleRow.Children.Add(fkButton); + } + if (schemaColumn?.IsIndexed == true) + { + titleRow.Children.Add(new MaterialIcon + { + Kind = MaterialIconKind.Magnify, + Width = 11, + Height = 11, + Foreground = new SolidColorBrush(Color.Parse("#FFCF75")), + }); + } + + var header = new StackPanel { Spacing = 0 }; + header.Children.Add(titleRow); + header.Children.Add(new TextBlock + { + Text = columnTypeLabel, + FontSize = ResolveResourceFontSize("FontSizeCaption", 11), + Foreground = ResolveResourceBrush("TextMutedBrush", Brushes.Gray), + }); + + return header; + } + + private async Task ShowRelationshipDialogAsync(string columnName) + { + if (_viewModel is null || string.IsNullOrWhiteSpace(columnName)) + return; + + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel is not Window owner) + return; + + IReadOnlyList relations = _viewModel.ResolveColumnRelationships(columnName); + var dialog = new SqlEditorForeignKeyDialogWindow(columnName, relations); + await dialog.ShowDialog(owner); + } + + private async Task ShowExpandedCellDialogAsync(object?[]? row, int columnIndex, string columnName, string columnTypeLabel) + { + TopLevel? topLevel = TopLevel.GetTopLevel(this); + if (topLevel is not Window owner) + return; + + string rawValue = SqlEditorResultCellContentFormatter.FormatCellValue(row, columnIndex); + if (!SqlEditorResultCellContentFormatter.ShouldOfferExpandedView(rawValue)) + return; + + string expandedValue = SqlEditorResultCellContentFormatter.FormatExpandedCellValue(rawValue); + + var dialog = new SqlEditorCellExpandDialogWindow(columnName, columnTypeLabel, expandedValue); + await dialog.ShowDialog(owner); + } + + private static bool IsDatabaseNull(object?[]? row, int index) + { + if (row is null || index < 0 || index >= row.Length) + return false; + + return row[index] is DBNull; + } + + private IBrush ResolveNullCellForegroundBrush() + { + if (_nullCellForegroundBrush is not null) + return _nullCellForegroundBrush; + + if (Application.Current?.TryGetResource("TextSecondaryBrush", null, out object? resource) == true + && resource is IBrush brush) + { + _nullCellForegroundBrush = brush; + return brush; + } + + _nullCellForegroundBrush = Brushes.Gray; + return _nullCellForegroundBrush; + } + + private static IBrush ResolveResourceBrush(string resourceKey, IBrush fallback) + { + if (Application.Current?.TryGetResource(resourceKey, null, out object? resource) == true + && resource is IBrush brush) + { + return brush; + } + + return fallback; + } + + private static double ResolveResourceFontSize(string resourceKey, double fallback) + { + if (Application.Current?.TryGetResource(resourceKey, null, out object? resource) == true) + { + if (resource is double size) + return size; + if (resource is int intSize) + return intSize; + } + + return fallback; + } + + private static FontWeight ResolveResourceFontWeight(string resourceKey, FontWeight fallback) + { + if (Application.Current?.TryGetResource(resourceKey, null, out object? resource) == true + && resource is FontWeight fontWeight) + { + return fontWeight; + } + + return fallback; + } + + private static FilePickerFileType GetExportFileType(SqlEditorReportType reportType) + { + return reportType switch + { + SqlEditorReportType.JsonContract => new FilePickerFileType(L("sqlEditor.export.fileType.json", "Arquivo JSON")) + { + Patterns = ["*.json"], + MimeTypes = ["application/json", "text/plain"], + }, + SqlEditorReportType.CsvData => new FilePickerFileType(L("sqlEditor.export.fileType.csv", "Arquivo CSV")) + { + Patterns = ["*.csv"], + MimeTypes = ["text/csv", "text/plain"], + }, + SqlEditorReportType.ExcelWorkbook => new FilePickerFileType(L("sqlEditor.export.fileType.xlsx", "Pasta de trabalho Excel")) + { + Patterns = ["*.xlsx"], + MimeTypes = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], + }, + _ => new FilePickerFileType(L("sqlEditor.export.fileType.html", "Arquivo HTML")) + { + Patterns = ["*.html", "*.htm"], + MimeTypes = ["text/html", "text/plain"], + }, + }; + } + + private sealed record PendingInlineEdit( + string ColumnName, + int ColumnIndex, + string EditedText); + + private sealed class ObjectArrayReferenceComparer : IEqualityComparer + { + public static ObjectArrayReferenceComparer Instance { get; } = new(); + + public bool Equals(object?[]? x, object?[]? y) => ReferenceEquals(x, y); + + public int GetHashCode(object?[] obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } +} + diff --git a/src/DBWeaver.UI/Controls/SqlEditor/SqlEditorRightSidebarControl.axaml b/src/AkkornStudio.UI/Controls/SqlEditor/SqlEditorRightSidebarControl.axaml similarity index 81% rename from src/DBWeaver.UI/Controls/SqlEditor/SqlEditorRightSidebarControl.axaml rename to src/AkkornStudio.UI/Controls/SqlEditor/SqlEditorRightSidebarControl.axaml index f24ad27f..7bd06914 100644 --- a/src/DBWeaver.UI/Controls/SqlEditor/SqlEditorRightSidebarControl.axaml +++ b/src/AkkornStudio.UI/Controls/SqlEditor/SqlEditorRightSidebarControl.axaml @@ -1,9 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DBWeaver.UI/Controls/Start/StartMenuControl.axaml.cs b/src/AkkornStudio.UI/Controls/Start/StartMenuControl.axaml.cs similarity index 94% rename from src/DBWeaver.UI/Controls/Start/StartMenuControl.axaml.cs rename to src/AkkornStudio.UI/Controls/Start/StartMenuControl.axaml.cs index 3a485a50..f96b9eed 100644 --- a/src/DBWeaver.UI/Controls/Start/StartMenuControl.axaml.cs +++ b/src/AkkornStudio.UI/Controls/Start/StartMenuControl.axaml.cs @@ -1,111 +1,111 @@ -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.Layout; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Controls.Start; - -public partial class StartMenuControl : UserControl -{ - private const double CompactBreakpoint = 1400; - private const double LargeBreakpoint = 1850; - private bool _entryAnimationPlayed; - - public StartMenuControl() - { - InitializeComponent(); - - AttachedToVisualTree += (_, _) => - { - ApplySizeClasses(Bounds.Width); - if (_entryAnimationPlayed) - return; - - _entryAnimationPlayed = true; - _ = RunEntryAnimationAsync(); - }; - SizeChanged += (_, e) => ApplySizeClasses(e.NewSize.Width); - } - - private StartMenuViewModel? Vm => DataContext as StartMenuViewModel; - - private void RecentProjectCard_Click(object? sender, RoutedEventArgs e) - { - if (Vm is null || sender is not Button { Tag: StartRecentProjectItem item }) - return; - - Vm.OpenRecentProjectCommand.Execute(item); - } - - private void TemplateCard_Click(object? sender, RoutedEventArgs e) - { - if (Vm is null || sender is not Button { Tag: StartTemplateItem item }) - return; - - Vm.OpenTemplateCommand.Execute(item); - } - - private void TemplateFavorite_Click(object? sender, RoutedEventArgs e) - { - if (Vm is null || sender is not Button { Tag: StartTemplateItem item }) - return; - - Vm.ToggleTemplateFavoriteCommand.Execute(item); - e.Handled = true; - } - - private void SavedConnectionCard_Click(object? sender, RoutedEventArgs e) - { - if (Vm is null || sender is not Button { Tag: StartSavedConnectionItem item }) - return; - - Vm.OpenSavedConnectionCommand.Execute(item); - } - - private void ApplySizeClasses(double width) - { - bool compact = width > 0 && width <= CompactBreakpoint; - bool large = width >= LargeBreakpoint; - - Classes.Set("start-compact", compact); - Classes.Set("start-large", large); - } - - private async Task RunEntryAnimationAsync() - { - var targets = new Control?[] - { - this.FindControl("HeroBlock"), - this.FindControl("RecentSection"), - this.FindControl("ConnectionsSection"), - this.FindControl("TemplatesSection"), - }; - - foreach (Control? target in targets) - { - if (target is null) - continue; - - target.Opacity = 0; - } - - foreach (Control? target in targets) - { - if (target is null) - continue; - - await Task.Delay(45); - await FadeInAsync(target); - } - } - - private static async Task FadeInAsync(Control target) - { - const int steps = 6; - for (int i = 1; i <= steps; i++) - { - target.Opacity = i / (double)steps; - await Task.Delay(24); - } - } -} +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Layout; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Controls.Start; + +public partial class StartMenuControl : UserControl +{ + private const double CompactBreakpoint = 1400; + private const double LargeBreakpoint = 1850; + private bool _entryAnimationPlayed; + + public StartMenuControl() + { + InitializeComponent(); + + AttachedToVisualTree += (_, _) => + { + ApplySizeClasses(Bounds.Width); + if (_entryAnimationPlayed) + return; + + _entryAnimationPlayed = true; + _ = RunEntryAnimationAsync(); + }; + SizeChanged += (_, e) => ApplySizeClasses(e.NewSize.Width); + } + + private StartMenuViewModel? Vm => DataContext as StartMenuViewModel; + + private void RecentProjectCard_Click(object? sender, RoutedEventArgs e) + { + if (Vm is null || sender is not Button { Tag: StartRecentProjectItem item }) + return; + + Vm.OpenRecentProjectCommand.Execute(item); + } + + private void TemplateCard_Click(object? sender, RoutedEventArgs e) + { + if (Vm is null || sender is not Button { Tag: StartTemplateItem item }) + return; + + Vm.OpenTemplateCommand.Execute(item); + } + + private void TemplateFavorite_Click(object? sender, RoutedEventArgs e) + { + if (Vm is null || sender is not Button { Tag: StartTemplateItem item }) + return; + + Vm.ToggleTemplateFavoriteCommand.Execute(item); + e.Handled = true; + } + + private void SavedConnectionCard_Click(object? sender, RoutedEventArgs e) + { + if (Vm is null || sender is not Button { Tag: StartSavedConnectionItem item }) + return; + + Vm.OpenSavedConnectionCommand.Execute(item); + } + + private void ApplySizeClasses(double width) + { + bool compact = width > 0 && width <= CompactBreakpoint; + bool large = width >= LargeBreakpoint; + + Classes.Set("start-compact", compact); + Classes.Set("start-large", large); + } + + private async Task RunEntryAnimationAsync() + { + var targets = new Control?[] + { + this.FindControl("HeroBlock"), + this.FindControl("RecentSection"), + this.FindControl("ConnectionsSection"), + this.FindControl("TemplatesSection"), + }; + + foreach (Control? target in targets) + { + if (target is null) + continue; + + target.Opacity = 0; + } + + foreach (Control? target in targets) + { + if (target is null) + continue; + + await Task.Delay(45); + await FadeInAsync(target); + } + } + + private static async Task FadeInAsync(Control target) + { + const int steps = 6; + for (int i = 1; i <= steps; i++) + { + target.Opacity = i / (double)steps; + await Task.Delay(24); + } + } +} diff --git a/src/DBWeaver.UI/Controls/Start/TemplateCard.axaml b/src/AkkornStudio.UI/Controls/Start/TemplateCard.axaml similarity index 89% rename from src/DBWeaver.UI/Controls/Start/TemplateCard.axaml rename to src/AkkornStudio.UI/Controls/Start/TemplateCard.axaml index ddfb9754..d3e3139e 100644 --- a/src/DBWeaver.UI/Controls/Start/TemplateCard.axaml +++ b/src/AkkornStudio.UI/Controls/Start/TemplateCard.axaml @@ -1,30 +1,30 @@ - - - - - + + + + + - - - - + + + + diff --git a/src/DBWeaver.UI/Controls/Start/TemplateCard.axaml.cs b/src/AkkornStudio.UI/Controls/Start/TemplateCard.axaml.cs similarity index 75% rename from src/DBWeaver.UI/Controls/Start/TemplateCard.axaml.cs rename to src/AkkornStudio.UI/Controls/Start/TemplateCard.axaml.cs index 50e96188..6e6aaa81 100644 --- a/src/DBWeaver.UI/Controls/Start/TemplateCard.axaml.cs +++ b/src/AkkornStudio.UI/Controls/Start/TemplateCard.axaml.cs @@ -1,11 +1,11 @@ -using Avalonia.Controls; - -namespace DBWeaver.UI.Controls.Start; - -public partial class TemplateCard : UserControl -{ - public TemplateCard() - { - InitializeComponent(); - } -} +using Avalonia.Controls; + +namespace AkkornStudio.UI.Controls.Start; + +public partial class TemplateCard : UserControl +{ + public TemplateCard() + { + InitializeComponent(); + } +} diff --git a/src/DBWeaver.UI/Converters/Data/DataTableConverter.cs b/src/AkkornStudio.UI/Converters/Data/DataTableConverter.cs similarity index 95% rename from src/DBWeaver.UI/Converters/Data/DataTableConverter.cs rename to src/AkkornStudio.UI/Converters/Data/DataTableConverter.cs index 846c47a5..c891bb26 100644 --- a/src/DBWeaver.UI/Converters/Data/DataTableConverter.cs +++ b/src/AkkornStudio.UI/Converters/Data/DataTableConverter.cs @@ -1,38 +1,38 @@ -using System.Data; -using Avalonia.Data.Converters; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace DBWeaver.UI.Converters; - -/// -/// Converts a DataTable to its DefaultView for binding to Avalonia DataGrid. -/// Avalonia DataGrid doesn't work well with raw DataTable binding; it needs -/// a DataView which properly implements IEnumerable with change notification. -/// -public class DataTableConverter : IValueConverter -{ - private static readonly ILogger _logger = NullLogger.Instance; - - public object? Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo? culture) - { - _logger.LogDebug("Convert called with value type: {ValueType}", value?.GetType().Name ?? "null"); - - if (value is DataTable dt) - { - // Return the DefaultView which supports proper binding and change notifications - _logger.LogDebug("Converting DataTable with {RowCount} rows to DefaultView", dt.Rows.Count); - var view = dt.DefaultView; - _logger.LogDebug("DefaultView created, RowFilter='{RowFilter}', Count={Count}", view.RowFilter, view.Count); - return view; - } - - _logger.LogDebug("Value is not DataTable, returning null"); - return null; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo? culture) - { - throw new NotSupportedException(); - } -} +using System.Data; +using Avalonia.Data.Converters; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AkkornStudio.UI.Converters; + +/// +/// Converts a DataTable to its DefaultView for binding to Avalonia DataGrid. +/// Avalonia DataGrid doesn't work well with raw DataTable binding; it needs +/// a DataView which properly implements IEnumerable with change notification. +/// +public class DataTableConverter : IValueConverter +{ + private static readonly ILogger _logger = NullLogger.Instance; + + public object? Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo? culture) + { + _logger.LogDebug("Convert called with value type: {ValueType}", value?.GetType().Name ?? "null"); + + if (value is DataTable dt) + { + // Return the DefaultView which supports proper binding and change notifications + _logger.LogDebug("Converting DataTable with {RowCount} rows to DefaultView", dt.Rows.Count); + var view = dt.DefaultView; + _logger.LogDebug("DefaultView created, RowFilter='{RowFilter}', Count={Count}", view.RowFilter, view.Count); + return view; + } + + _logger.LogDebug("Value is not DataTable, returning null"); + return null; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo? culture) + { + throw new NotSupportedException(); + } +} diff --git a/src/DBWeaver.UI/Converters/Icons/StringToMaterialIconConverter.cs b/src/AkkornStudio.UI/Converters/Icons/StringToMaterialIconConverter.cs similarity index 91% rename from src/DBWeaver.UI/Converters/Icons/StringToMaterialIconConverter.cs rename to src/AkkornStudio.UI/Converters/Icons/StringToMaterialIconConverter.cs index 30118d7c..e8708aea 100644 --- a/src/DBWeaver.UI/Converters/Icons/StringToMaterialIconConverter.cs +++ b/src/AkkornStudio.UI/Converters/Icons/StringToMaterialIconConverter.cs @@ -1,22 +1,22 @@ -using System.Globalization; -using Avalonia.Data.Converters; -using Material.Icons; - -namespace DBWeaver.UI.Converters; - -public class StringToMaterialIconConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo? culture) - { - if (value is string iconName && Enum.TryParse(iconName, out var kind)) - { - return kind; - } - return MaterialIconKind.HelpCircleOutline; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo? culture) - { - throw new NotImplementedException(); - } -} +using System.Globalization; +using Avalonia.Data.Converters; +using Material.Icons; + +namespace AkkornStudio.UI.Converters; + +public class StringToMaterialIconConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo? culture) + { + if (value is string iconName && Enum.TryParse(iconName, out var kind)) + { + return kind; + } + return MaterialIconKind.HelpCircleOutline; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo? culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/DBWeaver.UI/Converters/Metadata/DbMetadataTreeViewConverter.cs b/src/AkkornStudio.UI/Converters/Metadata/DbMetadataTreeViewConverter.cs similarity index 94% rename from src/DBWeaver.UI/Converters/Metadata/DbMetadataTreeViewConverter.cs rename to src/AkkornStudio.UI/Converters/Metadata/DbMetadataTreeViewConverter.cs index 26325f6b..c8b77ba4 100644 --- a/src/DBWeaver.UI/Converters/Metadata/DbMetadataTreeViewConverter.cs +++ b/src/AkkornStudio.UI/Converters/Metadata/DbMetadataTreeViewConverter.cs @@ -1,74 +1,74 @@ -using System.Collections.ObjectModel; -using DBWeaver.Metadata; - -namespace DBWeaver.UI.Converters; - -/// -/// Converts DbMetadata into a TreeView-friendly structure. -/// Groups tables by schema and displays columns within each table. -/// -public static class DbMetadataTreeViewConverter -{ - public class SchemaTreeNode - { - public string SchemaName { get; set; } = ""; - public ObservableCollection Tables { get; } = []; - } - - public class TableTreeNode - { - public string TableName { get; set; } = ""; - public string FullName { get; set; } = ""; - public ObservableCollection Columns { get; } = []; - } - - public class ColumnTreeNode - { - public string ColumnName { get; set; } = ""; - public string DataType { get; set; } = ""; - public bool IsPrimaryKey { get; set; } - public bool IsForeignKey { get; set; } - } - - /// - /// Converts DbMetadata to a hierarchical structure for TreeView binding. - /// - public static ObservableCollection ToTreeViewItems(DbMetadata? metadata) - { - var result = new ObservableCollection(); - - if (metadata is null) - return result; - - foreach (var schema in metadata.Schemas) - { - var schemaNode = new SchemaTreeNode { SchemaName = schema.Name }; - - foreach (var table in schema.Tables.OrderBy(t => t.Name)) - { - var tableNode = new TableTreeNode - { - TableName = table.Name, - FullName = $"{table.Schema}.{table.Name}" - }; - - foreach (var column in table.Columns.OrderBy(c => c.OrdinalPosition)) - { - tableNode.Columns.Add(new ColumnTreeNode - { - ColumnName = column.Name, - DataType = column.NativeType, - IsPrimaryKey = column.IsPrimaryKey, - IsForeignKey = column.IsForeignKey - }); - } - - schemaNode.Tables.Add(tableNode); - } - - result.Add(schemaNode); - } - - return result; - } -} +using System.Collections.ObjectModel; +using AkkornStudio.Metadata; + +namespace AkkornStudio.UI.Converters; + +/// +/// Converts DbMetadata into a TreeView-friendly structure. +/// Groups tables by schema and displays columns within each table. +/// +public static class DbMetadataTreeViewConverter +{ + public class SchemaTreeNode + { + public string SchemaName { get; set; } = ""; + public ObservableCollection Tables { get; } = []; + } + + public class TableTreeNode + { + public string TableName { get; set; } = ""; + public string FullName { get; set; } = ""; + public ObservableCollection Columns { get; } = []; + } + + public class ColumnTreeNode + { + public string ColumnName { get; set; } = ""; + public string DataType { get; set; } = ""; + public bool IsPrimaryKey { get; set; } + public bool IsForeignKey { get; set; } + } + + /// + /// Converts DbMetadata to a hierarchical structure for TreeView binding. + /// + public static ObservableCollection ToTreeViewItems(DbMetadata? metadata) + { + var result = new ObservableCollection(); + + if (metadata is null) + return result; + + foreach (var schema in metadata.Schemas) + { + var schemaNode = new SchemaTreeNode { SchemaName = schema.Name }; + + foreach (var table in schema.Tables.OrderBy(t => t.Name)) + { + var tableNode = new TableTreeNode + { + TableName = table.Name, + FullName = $"{table.Schema}.{table.Name}" + }; + + foreach (var column in table.Columns.OrderBy(c => c.OrdinalPosition)) + { + tableNode.Columns.Add(new ColumnTreeNode + { + ColumnName = column.Name, + DataType = column.NativeType, + IsPrimaryKey = column.IsPrimaryKey, + IsForeignKey = column.IsForeignKey + }); + } + + schemaNode.Tables.Add(tableNode); + } + + result.Add(schemaNode); + } + + return result; + } +} diff --git a/src/DBWeaver.UI/Converters/Sql/SqlTokenKindConverter.cs b/src/AkkornStudio.UI/Converters/Sql/SqlTokenKindConverter.cs similarity index 88% rename from src/DBWeaver.UI/Converters/Sql/SqlTokenKindConverter.cs rename to src/AkkornStudio.UI/Converters/Sql/SqlTokenKindConverter.cs index 1d51f90e..1f63621c 100644 --- a/src/DBWeaver.UI/Converters/Sql/SqlTokenKindConverter.cs +++ b/src/AkkornStudio.UI/Converters/Sql/SqlTokenKindConverter.cs @@ -1,25 +1,25 @@ -using System.Globalization; -using Avalonia.Data.Converters; -using DBWeaver.UI.Services.LiveSqlBar; - -namespace DBWeaver.UI.Converters; - -/// Converts a SqlTokenKind enum value to bool for CSS class binding. -public sealed class SqlTokenKindConverter : IValueConverter -{ - public static readonly SqlTokenKindConverter Instance = new(); - - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is SqlTokenKind kind && parameter is string param) - return kind.ToString().Equals(param, StringComparison.Ordinal); - return false; - } - - public object? ConvertBack( - object? value, - Type targetType, - object? parameter, - CultureInfo culture - ) => throw new NotSupportedException(); -} +using System.Globalization; +using Avalonia.Data.Converters; +using AkkornStudio.UI.Services.LiveSqlBar; + +namespace AkkornStudio.UI.Converters; + +/// Converts a SqlTokenKind enum value to bool for CSS class binding. +public sealed class SqlTokenKindConverter : IValueConverter +{ + public static readonly SqlTokenKindConverter Instance = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is SqlTokenKind kind && parameter is string param) + return kind.ToString().Equals(param, StringComparison.Ordinal); + return false; + } + + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture + ) => throw new NotSupportedException(); +} diff --git a/src/AkkornStudio.UI/Extensions/FileNameExtensions.cs b/src/AkkornStudio.UI/Extensions/FileNameExtensions.cs new file mode 100644 index 00000000..9d5db321 --- /dev/null +++ b/src/AkkornStudio.UI/Extensions/FileNameExtensions.cs @@ -0,0 +1,33 @@ +using System.IO; +using System.Linq; + +namespace AkkornStudio.UI.Extensions; + +public static class FileNameExtensions +{ + public static string ToSafeFileBase(this string? value, string fallback = "report") + { + string safeBase = string.Concat((value ?? string.Empty).Select(ch => + char.IsLetterOrDigit(ch) || ch is '-' or '_' ? ch : '_')); + + return string.IsNullOrWhiteSpace(safeBase) ? fallback : safeBase; + } + + public static string EnsureExtension(this string? fileName, string extension) + => fileName.EnsureExtension(extension, extension); + + public static string EnsureExtension(this string? fileName, string previousExtension, string nextExtension) + { + string normalizedFileName = string.IsNullOrWhiteSpace(fileName) ? "report" : fileName.Trim(); + string normalizedPreviousExtension = previousExtension.TrimStart('.'); + string normalizedExtension = nextExtension.TrimStart('.'); + + if (normalizedFileName.EndsWith($".{normalizedExtension}", StringComparison.OrdinalIgnoreCase)) + return normalizedFileName; + + if (normalizedFileName.EndsWith($".{normalizedPreviousExtension}", StringComparison.OrdinalIgnoreCase)) + return normalizedFileName[..^(normalizedPreviousExtension.Length)] + normalizedExtension; + + return Path.GetFileNameWithoutExtension(normalizedFileName) + "." + normalizedExtension; + } +} diff --git a/src/AkkornStudio.UI/Extensions/ReportBuilderExtensions.cs b/src/AkkornStudio.UI/Extensions/ReportBuilderExtensions.cs new file mode 100644 index 00000000..5e1e8d1b --- /dev/null +++ b/src/AkkornStudio.UI/Extensions/ReportBuilderExtensions.cs @@ -0,0 +1,66 @@ +using System.Globalization; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.RegularExpressions; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Extensions; + +public static class ReportBuilderExtensions +{ + public static string ResolveReportLanguageTag(this CultureInfo culture) => + culture.Name.StartsWith("pt", StringComparison.OrdinalIgnoreCase) ? "pt-BR" : "en"; + + public static string PickReportText(this string language, string pt, string en) => + language.StartsWith("pt", StringComparison.OrdinalIgnoreCase) ? pt : en; + + public static string ToEmptyValueMode(this SqlEditorReportEmptyValueDisplayMode mode) => mode switch + { + SqlEditorReportEmptyValueDisplayMode.Dash => "dash", + SqlEditorReportEmptyValueDisplayMode.NullLiteral => "null", + _ => "blank" + }; + + public static string ToReportDialect(this DatabaseProvider? provider) => provider switch + { + DatabaseProvider.Postgres => "postgresql", + DatabaseProvider.SqlServer => "sqlserver", + DatabaseProvider.MySql => "mysql", + DatabaseProvider.SQLite => "sqlite", + _ => "unknown" + }; + + public static string NormalizeReportStatus(this string? status) => + string.IsNullOrWhiteSpace(status) ? "success" : status.Trim().ToLowerInvariant() switch + { + "error" => "error", + "warning" => "warning", + _ => "success" + }; + + public static int CountRegexMatches(this string? input, string pattern) => + string.IsNullOrWhiteSpace(input) ? 0 : Regex.Matches(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Count; + + public static bool ContainsSqlSubquery(this string? sql) => + !string.IsNullOrWhiteSpace(sql) + && (Regex.IsMatch(sql, @"\(\s*select\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) + || Regex.IsMatch(sql, @"\bexists\s*\(", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)); + + public static string ToHtmlEncoded(this string? value) => + System.Net.WebUtility.HtmlEncode(value ?? string.Empty); + + public static string ToInlineScriptJson(this object value) + { + string json = JsonSerializer.Serialize(value, new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + return Regex.Replace(json, " value switch + { + bool => "bool", + byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal => "number", + DateTime or DateTimeOffset => "date", + _ => "text" + }; +} diff --git a/src/AkkornStudio.UI/Extensions/StringTransformExtensions.cs b/src/AkkornStudio.UI/Extensions/StringTransformExtensions.cs new file mode 100644 index 00000000..183885ae --- /dev/null +++ b/src/AkkornStudio.UI/Extensions/StringTransformExtensions.cs @@ -0,0 +1,75 @@ +using System.Globalization; +using System.Text; + +namespace AkkornStudio.UI.Extensions; + +public static class StringTransformExtensions +{ + public static string ToSlugCase(this string? value) + => value.ToSlugToken('-', string.Empty); + + public static string ToSlugToken(this string? value, char separator = '-', string emptyFallback = "") + { + if (string.IsNullOrWhiteSpace(value)) + return emptyFallback; + + string normalized = value.Normalize(NormalizationForm.FormD); + StringBuilder builder = new(normalized.Length); + bool lastWasSeparator = false; + + foreach (char ch in normalized) + { + UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(ch); + if (category == UnicodeCategory.NonSpacingMark) + continue; + + if (char.IsLetterOrDigit(ch)) + { + builder.Append(char.ToLowerInvariant(ch)); + lastWasSeparator = false; + continue; + } + + if (lastWasSeparator) + continue; + + builder.Append(separator); + lastWasSeparator = true; + } + + string result = builder.ToString().Trim(separator); + return string.IsNullOrWhiteSpace(result) ? emptyFallback : result; + } + + public static string ToTitleCaseInvariant(this string? value) + { + string normalized = (value ?? string.Empty).Trim().ToLowerInvariant(); + return string.IsNullOrWhiteSpace(normalized) + ? string.Empty + : CultureInfo.InvariantCulture.TextInfo.ToTitleCase(normalized); + } + + public static string ToSentenceCaseInvariant(this string? value) + { + string trimmed = (value ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(trimmed)) + return string.Empty; + + string lower = trimmed.ToLowerInvariant(); + return char.ToUpperInvariant(lower[0]) + lower[1..]; + } + + public static string TransformText(this string? value, string? mode) + { + string text = value ?? string.Empty; + return (mode ?? string.Empty).Trim().ToLowerInvariant() switch + { + "upper" => text.ToUpperInvariant(), + "lower" => text.ToLowerInvariant(), + "title" => text.ToTitleCaseInvariant(), + "sentence" => text.ToSentenceCaseInvariant(), + "slug" => text.ToSlugCase(), + _ => text, + }; + } +} diff --git a/src/AkkornStudio.UI/GlobalUsings.ConnectionManager.cs b/src/AkkornStudio.UI/GlobalUsings.ConnectionManager.cs new file mode 100644 index 00000000..184b84d2 --- /dev/null +++ b/src/AkkornStudio.UI/GlobalUsings.ConnectionManager.cs @@ -0,0 +1 @@ +global using AkkornStudio.UI.Services.ConnectionManager.Models; diff --git a/src/AkkornStudio.UI/GlobalUsings.Node.cs b/src/AkkornStudio.UI/GlobalUsings.Node.cs new file mode 100644 index 00000000..e40d1bba --- /dev/null +++ b/src/AkkornStudio.UI/GlobalUsings.Node.cs @@ -0,0 +1 @@ +global using AkkornStudio.UI.Services.Node; diff --git a/src/AkkornStudio.UI/GlobalUsings.Shell.cs b/src/AkkornStudio.UI/GlobalUsings.Shell.cs new file mode 100644 index 00000000..c6fa51a1 --- /dev/null +++ b/src/AkkornStudio.UI/GlobalUsings.Shell.cs @@ -0,0 +1 @@ +global using AkkornStudio.UI.Services.Shell.Models; diff --git a/src/AkkornStudio.UI/GlobalUsings.Start.cs b/src/AkkornStudio.UI/GlobalUsings.Start.cs new file mode 100644 index 00000000..be9b98f6 --- /dev/null +++ b/src/AkkornStudio.UI/GlobalUsings.Start.cs @@ -0,0 +1 @@ +global using AkkornStudio.UI.Services.Start.Models; diff --git a/src/AkkornStudio.UI/GlobalUsings.Validation.cs b/src/AkkornStudio.UI/GlobalUsings.Validation.cs new file mode 100644 index 00000000..3a6d34a0 --- /dev/null +++ b/src/AkkornStudio.UI/GlobalUsings.Validation.cs @@ -0,0 +1 @@ +global using AkkornStudio.UI.Services.Validation; diff --git a/src/AkkornStudio.UI/Properties/AssemblyInfo.cs b/src/AkkornStudio.UI/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..7f35c7ec --- /dev/null +++ b/src/AkkornStudio.UI/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AkkornStudio.Tests")] diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasLoadResult.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasLoadResult.cs similarity index 86% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasLoadResult.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasLoadResult.cs index 95c69486..b6bd63a2 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasLoadResult.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasLoadResult.cs @@ -1,23 +1,23 @@ -using DBWeaver.UI.Services.Workspace.Models; - -namespace DBWeaver.UI.Serialization; - -/// -/// Describes the outcome of deserialising a canvas file. -/// -public sealed record CanvasLoadResult( - bool Success, - string? Error = null, - IReadOnlyList? Warnings = null, - WorkspaceDocumentType? ActiveDocumentType = null, - IReadOnlyList? SqlEditorSeedScripts = null -) -{ - public static CanvasLoadResult Ok( - IReadOnlyList? warnings = null, - WorkspaceDocumentType? activeDocumentType = null, - IReadOnlyList? sqlEditorSeedScripts = null) => - new(true, null, warnings, activeDocumentType, sqlEditorSeedScripts); - - public static CanvasLoadResult Fail(string error) => new(false, error, null); -} +using AkkornStudio.UI.Services.Workspace.Models; + +namespace AkkornStudio.UI.Serialization; + +/// +/// Describes the outcome of deserialising a canvas file. +/// +public sealed record CanvasLoadResult( + bool Success, + string? Error = null, + IReadOnlyList? Warnings = null, + WorkspaceDocumentType? ActiveDocumentType = null, + IReadOnlyList? SqlEditorSeedScripts = null +) +{ + public static CanvasLoadResult Ok( + IReadOnlyList? warnings = null, + WorkspaceDocumentType? activeDocumentType = null, + IReadOnlyList? sqlEditorSeedScripts = null) => + new(true, null, warnings, activeDocumentType, sqlEditorSeedScripts); + + public static CanvasLoadResult Fail(string error) => new(false, error, null); +} diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializationModels.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializationModels.cs similarity index 96% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasSerializationModels.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializationModels.cs index 52361c86..93d1ecdf 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializationModels.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializationModels.cs @@ -1,28 +1,29 @@ -namespace DBWeaver.UI.Serialization; - -/// -/// Top-level canvas save file. -/// -public record SavedCanvas( - int Version, - string? DatabaseProvider, - string? ConnectionName, - double Zoom, - double PanX, - double PanY, - List Nodes, - List Connections, - List SelectBindings, - List WhereBindings, - string? AppVersion = null, - string? CreatedAt = null, - string? Description = null -); - -/// -/// Top-level workspace save file (schema v4+). -/// Contains independent query and DDL canvases. -/// +namespace AkkornStudio.UI.Serialization; + +/// +/// Top-level canvas save file. +/// +public record SavedCanvas( + int Version, + string? DatabaseProvider, + string? ConnectionName, + double Zoom, + double PanX, + double PanY, + List Nodes, + List Connections, + List SelectBindings, + List WhereBindings, + Dictionary? PreviewParameterInputs = null, + string? AppVersion = null, + string? CreatedAt = null, + string? Description = null +); + +/// +/// Top-level workspace save file (schema v4+). +/// Contains independent query and DDL canvases. +/// public record SavedWorkspaceCanvas( int Version, SavedCanvas QueryCanvas, @@ -55,27 +56,27 @@ public record SavedWorkspaceDocument( string PersistenceSchemaVersion, SavedCanvas? CanvasPayload = null ); - -public record SavedColumn( - string Name, - string Type -); - -public record SavedNode( - string NodeId, - string NodeType, - double X, - double Y, - int? ZOrder, - string? Alias, - string? TableFullName, - Dictionary Parameters, - Dictionary PinLiterals, - List? Columns = null, - SavedCteSubgraph? CteSubgraph = null, - SavedViewSubgraph? ViewSubgraph = null -); - + +public record SavedColumn( + string Name, + string Type +); + +public record SavedNode( + string NodeId, + string NodeType, + double X, + double Y, + int? ZOrder, + string? Alias, + string? TableFullName, + Dictionary Parameters, + Dictionary PinLiterals, + List? Columns = null, + SavedCteSubgraph? CteSubgraph = null, + SavedViewSubgraph? ViewSubgraph = null +); + public record SavedCteSubgraph( List Nodes, List Connections, @@ -97,13 +98,13 @@ public record SavedSubquerySubgraph( string? BridgeNodeId, List? InputBindings = null ); - -public record SavedViewSubgraph( - string? GraphJson, - string? FromTable, - string? EditorCanvasJson = null -); - + +public record SavedViewSubgraph( + string? GraphJson, + string? FromTable, + string? EditorCanvasJson = null +); + public record SavedConnection( string FromNodeId, string FromPinName, @@ -117,19 +118,19 @@ public readonly record struct SavedWireBreakpoint( double X, double Y ); - -public sealed record LocalFileVersionInfo( - string VersionId, - string VersionPath, - DateTimeOffset CreatedAt, - long SizeBytes, - bool IsCompressed -) -{ - public string CreatedAtLocalLabel => CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); - public string SizeLabel => SizeBytes < 1024 - ? $"{SizeBytes} B" - : SizeBytes < 1024 * 1024 - ? $"{SizeBytes / 1024.0:F1} KB" - : $"{SizeBytes / (1024.0 * 1024.0):F1} MB"; -} + +public sealed record LocalFileVersionInfo( + string VersionId, + string VersionPath, + DateTimeOffset CreatedAt, + long SizeBytes, + bool IsCompressed +) +{ + public string CreatedAtLocalLabel => CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + public string SizeLabel => SizeBytes < 1024 + ? $"{SizeBytes} B" + : SizeBytes < 1024 * 1024 + ? $"{SizeBytes / 1024.0:F1} KB" + : $"{SizeBytes / (1024.0 * 1024.0):F1} MB"; +} diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Connections.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Connections.cs similarity index 96% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Connections.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Connections.cs index 6507ee4e..3749414b 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Connections.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Connections.cs @@ -1,235 +1,235 @@ -using Avalonia; -using DBWeaver.Nodes; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Serialization; - -public static partial class CanvasSerializer -{ - private readonly record struct ConnectionRebuildStats( - int Malformed, - int Unresolved, - int Incompatible, - int MigratedLegacyProjectionPins - ); - - private static ConnectionRebuildStats RebuildConnections( - IEnumerable savedConnections, - IReadOnlyDictionary nodeMap, - ICollection targetConnections - ) - { - int malformed = 0; - int incompatible = 0; - int migratedLegacyProjectionPins = 0; - List pending = []; - - foreach (SavedConnection sc in savedConnections) - { - if (string.IsNullOrWhiteSpace(sc.FromNodeId) - || string.IsNullOrWhiteSpace(sc.ToNodeId) - || string.IsNullOrWhiteSpace(sc.FromPinName) - || string.IsNullOrWhiteSpace(sc.ToPinName)) - { - malformed++; - continue; - } - - if (!nodeMap.TryGetValue(sc.FromNodeId, out NodeViewModel? fromNode) - || !nodeMap.TryGetValue(sc.ToNodeId, out NodeViewModel? toNode)) - { - pending.Add(sc); - continue; - } - - PinViewModel? fromPin = - fromNode.OutputPins.FirstOrDefault(p => p.Name == sc.FromPinName) - ?? fromNode.InputPins.FirstOrDefault(p => p.Name == sc.FromPinName); - PinViewModel? toPin = - toNode.InputPins.FirstOrDefault(p => p.Name == sc.ToPinName) - ?? toNode.OutputPins.FirstOrDefault(p => p.Name == sc.ToPinName); - - // ColumnList/ColumnSetBuilder: redirect old dynamic pins (col_N) - // to the canonical fixed "columns" pin. - if ( - toPin is null - && (toNode.IsColumnList || toNode.Type == NodeType.ColumnSetBuilder) - && sc.ToPinName.StartsWith("col_", StringComparison.OrdinalIgnoreCase) - ) - { - toPin = toNode.InputPins.FirstOrDefault(p => p.Name == "columns"); - if (toPin is not null) - migratedLegacyProjectionPins++; - } - - // AND/OR migration: legacy cond_N pins are now normalized to the - // single variadic "conditions" input pin. - if ( - toPin is null - && toNode.IsLogicGate - && sc.ToPinName.StartsWith("cond_", StringComparison.OrdinalIgnoreCase) - ) - { - toPin = toNode.InputPins.FirstOrDefault(p => p.Name == "conditions"); - } - - if ( - toPin is null - && toNode.Type == NodeType.CompileWhere - && string.Equals(sc.ToPinName, "condition", StringComparison.OrdinalIgnoreCase) - ) - { - toPin = toNode.InputPins.FirstOrDefault(p => p.Name == "conditions"); - } - - // WindowFunction dynamic pins: create partition_N/order_N on-the-fly if missing. - if ( - toPin is null - && toNode.IsWindowFunction - && ( - sc.ToPinName.StartsWith("partition_", StringComparison.OrdinalIgnoreCase) - || sc.ToPinName.StartsWith("order_", StringComparison.OrdinalIgnoreCase) - ) - ) - { - var dynPin = new PinViewModel( - new PinDescriptor( - sc.ToPinName, - PinDirection.Input, - PinDataType.ColumnRef, - IsRequired: false, - Description: "Connect a column or expression" - ), - toNode - ); - toNode.InputPins.Add(dynPin); - toPin = dynPin; - } - - if (fromPin is null || toPin is null) - { - pending.Add(sc); - continue; - } - - if (!TryConnect(targetConnections, fromPin, toPin, sc)) - { - incompatible++; - continue; - } - } - - foreach (NodeViewModel node in nodeMap.Values.Where(n => n.Type == NodeType.CteSource)) - node.SyncCteSourceColumns(targetConnections); - - int unresolved = 0; - foreach (SavedConnection sc in pending) - { - if (!nodeMap.TryGetValue(sc.FromNodeId, out NodeViewModel? fromNode) - || !nodeMap.TryGetValue(sc.ToNodeId, out NodeViewModel? toNode)) - { - unresolved++; - continue; - } - - if (!TryResolvePins(fromNode, sc.FromPinName, toNode, sc.ToPinName, out PinViewModel? fromPin, out PinViewModel? toPin)) - { - unresolved++; - continue; - } - - if (!TryConnect(targetConnections, fromPin!, toPin!, sc)) - { - incompatible++; - continue; - } - } - - return new ConnectionRebuildStats(malformed, unresolved, incompatible, migratedLegacyProjectionPins); - } - - private static void AddConnectionRebuildWarnings(ConnectionRebuildStats stats, List warnings) - { - if (stats.Malformed > 0) - warnings.Add( - $"Skipped {stats.Malformed} malformed connection(s) with missing endpoint data." - ); - - if (stats.Unresolved > 0) - warnings.Add( - $"Skipped {stats.Unresolved} connection(s) that reference missing nodes or pins." - ); - - if (stats.Incompatible > 0) - warnings.Add( - $"Skipped {stats.Incompatible} incompatible connection(s) due to type mismatch." - ); - - if (stats.MigratedLegacyProjectionPins > 0) - warnings.Add( - $"Migrated {stats.MigratedLegacyProjectionPins} legacy projection connection(s) from dynamic 'col_*' pins to canonical 'columns' pin." - ); - } - - private static bool TryResolvePins( - NodeViewModel fromNode, - string fromPinName, - NodeViewModel toNode, - string toPinName, - out PinViewModel? fromPin, - out PinViewModel? toPin - ) - { - fromPin = fromNode.OutputPins.FirstOrDefault(p => p.Name == fromPinName) - ?? fromNode.InputPins.FirstOrDefault(p => p.Name == fromPinName); - toPin = toNode.InputPins.FirstOrDefault(p => p.Name == toPinName) - ?? toNode.OutputPins.FirstOrDefault(p => p.Name == toPinName); - - return fromPin is not null && toPin is not null; - } - - private static bool TryConnect( - ICollection connections, - PinViewModel fromPin, - PinViewModel toPin, - SavedConnection savedConnection - ) - { - if (!IsConnectionCompatible(fromPin, toPin)) - return false; - - var conn = new ConnectionViewModel(fromPin, default, default) { ToPin = toPin }; - ApplyWireMetadata(conn, savedConnection); - fromPin.IsConnected = true; - toPin.IsConnected = true; - connections.Add(conn); - return true; - } - - private static bool TryConnect( - ICollection connections, - PinViewModel fromPin, - PinViewModel toPin) - { - SavedConnection savedConnection = new( - FromNodeId: fromPin.Owner.Id, - FromPinName: fromPin.Name, - ToNodeId: toPin.Owner.Id, - ToPinName: toPin.Name); - return TryConnect(connections, fromPin, toPin, savedConnection); - } - - private static void ApplyWireMetadata(ConnectionViewModel connection, SavedConnection savedConnection) - { - if (Enum.TryParse(savedConnection.RoutingMode, true, out CanvasWireRoutingMode routingMode)) - connection.RoutingMode = routingMode; - - if (savedConnection.Breakpoints is not { Count: > 0 }) - return; - - connection.SetBreakpoints([.. savedConnection.Breakpoints.Select(b => new WireBreakpoint(new(b.X, b.Y)))]); - } - - private static bool IsConnectionCompatible(PinViewModel fromPin, PinViewModel toPin) => - toPin.EvaluateConnection(fromPin).IsAllowed; -} +using Avalonia; +using AkkornStudio.Nodes; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Serialization; + +public static partial class CanvasSerializer +{ + private readonly record struct ConnectionRebuildStats( + int Malformed, + int Unresolved, + int Incompatible, + int MigratedLegacyProjectionPins + ); + + private static ConnectionRebuildStats RebuildConnections( + IEnumerable savedConnections, + IReadOnlyDictionary nodeMap, + ICollection targetConnections + ) + { + int malformed = 0; + int incompatible = 0; + int migratedLegacyProjectionPins = 0; + List pending = []; + + foreach (SavedConnection sc in savedConnections) + { + if (string.IsNullOrWhiteSpace(sc.FromNodeId) + || string.IsNullOrWhiteSpace(sc.ToNodeId) + || string.IsNullOrWhiteSpace(sc.FromPinName) + || string.IsNullOrWhiteSpace(sc.ToPinName)) + { + malformed++; + continue; + } + + if (!nodeMap.TryGetValue(sc.FromNodeId, out NodeViewModel? fromNode) + || !nodeMap.TryGetValue(sc.ToNodeId, out NodeViewModel? toNode)) + { + pending.Add(sc); + continue; + } + + PinViewModel? fromPin = + fromNode.OutputPins.FirstOrDefault(p => p.Name == sc.FromPinName) + ?? fromNode.InputPins.FirstOrDefault(p => p.Name == sc.FromPinName); + PinViewModel? toPin = + toNode.InputPins.FirstOrDefault(p => p.Name == sc.ToPinName) + ?? toNode.OutputPins.FirstOrDefault(p => p.Name == sc.ToPinName); + + // ColumnList/ColumnSetBuilder: redirect old dynamic pins (col_N) + // to the canonical fixed "columns" pin. + if ( + toPin is null + && (toNode.IsColumnList || toNode.Type == NodeType.ColumnSetBuilder) + && sc.ToPinName.StartsWith("col_", StringComparison.OrdinalIgnoreCase) + ) + { + toPin = toNode.InputPins.FirstOrDefault(p => p.Name == "columns"); + if (toPin is not null) + migratedLegacyProjectionPins++; + } + + // AND/OR migration: legacy cond_N pins are now normalized to the + // single variadic "conditions" input pin. + if ( + toPin is null + && toNode.IsLogicGate + && sc.ToPinName.StartsWith("cond_", StringComparison.OrdinalIgnoreCase) + ) + { + toPin = toNode.InputPins.FirstOrDefault(p => p.Name == "conditions"); + } + + if ( + toPin is null + && toNode.Type == NodeType.CompileWhere + && string.Equals(sc.ToPinName, "condition", StringComparison.OrdinalIgnoreCase) + ) + { + toPin = toNode.InputPins.FirstOrDefault(p => p.Name == "conditions"); + } + + // WindowFunction dynamic pins: create partition_N/order_N on-the-fly if missing. + if ( + toPin is null + && toNode.IsWindowFunction + && ( + sc.ToPinName.StartsWith("partition_", StringComparison.OrdinalIgnoreCase) + || sc.ToPinName.StartsWith("order_", StringComparison.OrdinalIgnoreCase) + ) + ) + { + var dynPin = new PinViewModel( + new PinDescriptor( + sc.ToPinName, + PinDirection.Input, + PinDataType.ColumnRef, + IsRequired: false, + Description: "Connect a column or expression" + ), + toNode + ); + toNode.InputPins.Add(dynPin); + toPin = dynPin; + } + + if (fromPin is null || toPin is null) + { + pending.Add(sc); + continue; + } + + if (!TryConnect(targetConnections, fromPin, toPin, sc)) + { + incompatible++; + continue; + } + } + + foreach (NodeViewModel node in nodeMap.Values.Where(n => n.Type == NodeType.CteSource)) + node.SyncCteSourceColumns(targetConnections); + + int unresolved = 0; + foreach (SavedConnection sc in pending) + { + if (!nodeMap.TryGetValue(sc.FromNodeId, out NodeViewModel? fromNode) + || !nodeMap.TryGetValue(sc.ToNodeId, out NodeViewModel? toNode)) + { + unresolved++; + continue; + } + + if (!TryResolvePins(fromNode, sc.FromPinName, toNode, sc.ToPinName, out PinViewModel? fromPin, out PinViewModel? toPin)) + { + unresolved++; + continue; + } + + if (!TryConnect(targetConnections, fromPin!, toPin!, sc)) + { + incompatible++; + continue; + } + } + + return new ConnectionRebuildStats(malformed, unresolved, incompatible, migratedLegacyProjectionPins); + } + + private static void AddConnectionRebuildWarnings(ConnectionRebuildStats stats, List warnings) + { + if (stats.Malformed > 0) + warnings.Add( + $"Skipped {stats.Malformed} malformed connection(s) with missing endpoint data." + ); + + if (stats.Unresolved > 0) + warnings.Add( + $"Skipped {stats.Unresolved} connection(s) that reference missing nodes or pins." + ); + + if (stats.Incompatible > 0) + warnings.Add( + $"Skipped {stats.Incompatible} incompatible connection(s) due to type mismatch." + ); + + if (stats.MigratedLegacyProjectionPins > 0) + warnings.Add( + $"Migrated {stats.MigratedLegacyProjectionPins} legacy projection connection(s) from dynamic 'col_*' pins to canonical 'columns' pin." + ); + } + + private static bool TryResolvePins( + NodeViewModel fromNode, + string fromPinName, + NodeViewModel toNode, + string toPinName, + out PinViewModel? fromPin, + out PinViewModel? toPin + ) + { + fromPin = fromNode.OutputPins.FirstOrDefault(p => p.Name == fromPinName) + ?? fromNode.InputPins.FirstOrDefault(p => p.Name == fromPinName); + toPin = toNode.InputPins.FirstOrDefault(p => p.Name == toPinName) + ?? toNode.OutputPins.FirstOrDefault(p => p.Name == toPinName); + + return fromPin is not null && toPin is not null; + } + + private static bool TryConnect( + ICollection connections, + PinViewModel fromPin, + PinViewModel toPin, + SavedConnection savedConnection + ) + { + if (!IsConnectionCompatible(fromPin, toPin)) + return false; + + var conn = new ConnectionViewModel(fromPin, default, default) { ToPin = toPin }; + ApplyWireMetadata(conn, savedConnection); + fromPin.IsConnected = true; + toPin.IsConnected = true; + connections.Add(conn); + return true; + } + + private static bool TryConnect( + ICollection connections, + PinViewModel fromPin, + PinViewModel toPin) + { + SavedConnection savedConnection = new( + FromNodeId: fromPin.Owner.Id, + FromPinName: fromPin.Name, + ToNodeId: toPin.Owner.Id, + ToPinName: toPin.Name); + return TryConnect(connections, fromPin, toPin, savedConnection); + } + + private static void ApplyWireMetadata(ConnectionViewModel connection, SavedConnection savedConnection) + { + if (Enum.TryParse(savedConnection.RoutingMode, true, out CanvasWireRoutingMode routingMode)) + connection.RoutingMode = routingMode; + + if (savedConnection.Breakpoints is not { Count: > 0 }) + return; + + connection.SetBreakpoints([.. savedConnection.Breakpoints.Select(b => new WireBreakpoint(new(b.X, b.Y)))]); + } + + private static bool IsConnectionCompatible(PinViewModel fromPin, PinViewModel toPin) => + toPin.EvaluateConnection(fromPin).IsAllowed; +} diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Load.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Load.cs similarity index 96% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Load.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Load.cs index 76d8f294..4380d2a2 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Load.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Load.cs @@ -1,418 +1,419 @@ -using System.Text.Json; -using Avalonia; -using DBWeaver.Core; -using DBWeaver.Nodes; -using DBWeaver.UI.Services.Workspace.Models; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Serialization; - -public static partial class CanvasSerializer -{ - /// - /// Rebuilds a from JSON. - /// Clears the existing canvas before loading. - /// Returns a . - /// - /// Optional catalog to restore TableSource column pins. - public static CanvasLoadResult Deserialize( - string json, - CanvasViewModel vm, - IReadOnlyDictionary>? columnLookup = - null - ) - { - if (LooksLikeDocumentWorkspaceEnvelope(json) || LooksLikeLegacyWorkspaceEnvelope(json)) - return DeserializeWorkspace(json, vm, null, columnLookup); - - SavedCanvas? raw; - try - { - raw = JsonSerializer.Deserialize(json, _opts); - } - catch (JsonException ex) - { - return CanvasLoadResult.Fail($"Invalid JSON: {ex.Message}"); - } - - if (raw is null) - return CanvasLoadResult.Fail("Canvas file is empty or could not be parsed."); - - return DeserializeSavedCanvas(raw, vm, columnLookup); - } - - public static CanvasLoadResult DeserializeWorkspace( - string json, - CanvasViewModel queryVm, - CanvasViewModel? ddlVm, - IReadOnlyDictionary>? columnLookup = - null - ) - { - if (!LooksLikeDocumentWorkspaceEnvelope(json) && !LooksLikeLegacyWorkspaceEnvelope(json)) - return CanvasLoadResult.Fail("Unsupported file format. Expected workspace canvas envelope."); - - if (LooksLikeDocumentWorkspaceEnvelope(json)) - return DeserializeDocumentWorkspace(json, queryVm, ddlVm, columnLookup); - - return DeserializeLegacyWorkspace(json, queryVm, ddlVm, columnLookup); - } - - private static CanvasLoadResult DeserializeDocumentWorkspace( - string json, - CanvasViewModel queryVm, - CanvasViewModel? ddlVm, - IReadOnlyDictionary>? columnLookup) - { - SavedWorkspaceDocumentsCanvas? workspace; - try - { - workspace = JsonSerializer.Deserialize(json, _opts); - } - catch (JsonException ex) - { - return CanvasLoadResult.Fail($"Invalid workspace JSON: {ex.Message}"); - } - - if (workspace is null) - return CanvasLoadResult.Fail("Workspace file is empty or could not be parsed."); - - if (workspace.Version < 5 || workspace.Version > CurrentSchemaVersion) - return CanvasLoadResult.Fail( - $"Unsupported workspace schema version {workspace.Version}. " - + $"This build supports versions 5–{CurrentSchemaVersion}." - ); - - List documents = workspace.Documents ?? []; - if (documents.Count == 0) - return CanvasLoadResult.Fail("Workspace does not contain any document."); - - SavedWorkspaceDocument? queryDocument = documents.FirstOrDefault(document => - IsDocumentType(document, WorkspaceDocumentType.QueryCanvas)); - if (queryDocument?.CanvasPayload is null) - return CanvasLoadResult.Fail("Workspace query document payload is missing."); - - CanvasLoadResult queryLoad = DeserializeSavedCanvas( - queryDocument.CanvasPayload, - queryVm, - columnLookup, - CanvasNodeFamily.Query - ); - if (!queryLoad.Success) - return queryLoad; - - var warnings = queryLoad.Warnings?.ToList() ?? []; - - SavedWorkspaceDocument? ddlDocument = documents.FirstOrDefault(document => - IsDocumentType(document, WorkspaceDocumentType.DdlCanvas)); - if (ddlDocument?.CanvasPayload is not null && ddlVm is not null) - { - CanvasLoadResult ddlLoad = ApplyDdlFromSavedCanvas(ddlDocument.CanvasPayload, ddlVm); - if (!ddlLoad.Success) - warnings.Add($"DDL canvas restore skipped: {ddlLoad.Error}"); - else if (ddlLoad.Warnings is { Count: > 0 }) - warnings.AddRange(ddlLoad.Warnings.Select(w => $"DDL: {w}")); - } - else if (ddlDocument?.CanvasPayload is not null) - { - warnings.Add("File contains a DDL document snapshot, but no DDL target canvas was provided during load."); - } - - WorkspaceDocumentType activeDocumentType = ResolveActiveDocumentType(workspace, documents); - return CanvasLoadResult.Ok( - warnings.Count > 0 ? warnings : null, - activeDocumentType, - queryLoad.SqlEditorSeedScripts); - } - - private static CanvasLoadResult DeserializeLegacyWorkspace( - string json, - CanvasViewModel queryVm, - CanvasViewModel? ddlVm, - IReadOnlyDictionary>? columnLookup) - { - SavedWorkspaceCanvas? workspace; - try - { - workspace = JsonSerializer.Deserialize(json, _opts); - } - catch (JsonException ex) - { - return CanvasLoadResult.Fail($"Invalid workspace JSON: {ex.Message}"); - } - - if (workspace is null) - return CanvasLoadResult.Fail("Workspace file is empty or could not be parsed."); - - if (workspace.Version != 4) - return CanvasLoadResult.Fail( - $"Unsupported workspace schema version {workspace.Version}. " - + "This build supports legacy version 4 and document schema v5+." - ); - - CanvasLoadResult queryLoad = DeserializeSavedCanvas( - workspace.QueryCanvas, - queryVm, - columnLookup, - CanvasNodeFamily.Query - ); - if (!queryLoad.Success) - return queryLoad; - - var warnings = queryLoad.Warnings?.ToList() ?? []; - - if (ddlVm is not null) - { - CanvasLoadResult ddlLoad = ApplyDdlFromSavedCanvas(workspace.DdlCanvas, ddlVm); - if (!ddlLoad.Success) - warnings.Add($"DDL canvas restore skipped: {ddlLoad.Error}"); - else if (ddlLoad.Warnings is { Count: > 0 }) - warnings.AddRange(ddlLoad.Warnings.Select(w => $"DDL: {w}")); - } - else if (workspace.DdlCanvas.Nodes.Count > 0 || workspace.DdlCanvas.Connections.Count > 0) - { - warnings.Add("File contains a DDL canvas snapshot, but no DDL target canvas was provided during load."); - } - - warnings.Add("Legacy workspace envelope migrated to document-oriented workspace model during load."); - WorkspaceDocumentType activeType = ResolveLegacyActiveDocumentType(workspace); - return CanvasLoadResult.Ok( - warnings.Count > 0 ? warnings : null, - activeType, - queryLoad.SqlEditorSeedScripts); - } - - private static CanvasLoadResult ApplyDdlFromSavedCanvas( - SavedCanvas saved, - CanvasViewModel ddlVm - ) - { - var scratch = new CanvasViewModel(); - scratch.Nodes.Clear(); - scratch.Connections.Clear(); - - CanvasLoadResult load = DeserializeSavedCanvas(saved, scratch, null, CanvasNodeFamily.Ddl); - if (!load.Success) - return load; - - ddlVm.ReplaceGraph(scratch.Nodes.ToList(), scratch.Connections.ToList()); - if (Enum.TryParse(saved.DatabaseProvider, true, out DatabaseProvider provider)) - ddlVm.Provider = provider; - - return load; - } - - private static CanvasLoadResult DeserializeSavedCanvas( - SavedCanvas raw, - CanvasViewModel vm, - IReadOnlyDictionary>? columnLookup, - CanvasNodeFamily allowedFamily = CanvasNodeFamily.Any - ) - { - if (raw.Version != CurrentCanvasSchemaVersion) - return CanvasLoadResult.Fail( - $"Unsupported canvas schema version {raw.Version}. " - + $"This build supports only version {CurrentCanvasSchemaVersion}." - ); - - SavedCanvas saved = raw; - var warnings = new List(); - IReadOnlyList sqlEditorSeedScripts = allowedFamily == CanvasNodeFamily.Query - ? ExtractLegacyReportSqlScripts(saved) - : []; - if (sqlEditorSeedScripts.Count > 0) - { - warnings.Add( - $"Migrated {sqlEditorSeedScripts.Count} legacy report SQL script(s) to SQL Editor seed drafts during load."); - } - - vm.Connections.Clear(); - vm.Nodes.Clear(); - vm.UndoRedo.Clear(); - - vm.Zoom = saved.Zoom; - vm.PanOffset = new Point(saved.PanX, saved.PanY); - - var nodeMap = new Dictionary(StringComparer.Ordinal); - var skippedNodes = new List<(string NodeId, string NodeType, string Reason)>(); - - foreach (SavedNode sn in saved.Nodes) - { - (NodeViewModel? nodeVm, string? skipReason) = BuildNodeVm(sn, columnLookup, allowedFamily); - if (nodeVm is null) - { - skippedNodes.Add((sn.NodeId, sn.NodeType, skipReason ?? "Unknown error")); - continue; - } - nodeMap[sn.NodeId] = nodeVm; - vm.Nodes.Add(nodeVm); - } - - var loadOrderById = saved.Nodes - .Select((n, i) => (n.NodeId, i)) - .ToDictionary(x => x.NodeId, x => x.i, StringComparer.Ordinal); - - int z = 0; - foreach (NodeViewModel n in vm.Nodes - .OrderBy(n => n.ZOrder) - .ThenBy(n => loadOrderById.TryGetValue(n.Id, out int idx) ? idx : int.MaxValue)) - n.ZOrder = z++; - - if (skippedNodes.Count > 0) - { - var skippedSummary = string.Join(", ", - skippedNodes.GroupBy(s => s.NodeType) - .Select(g => $"{g.Count()} {g.Key}")); - warnings.Add( - $"Warning: {skippedNodes.Count} node(s) could not be loaded and were skipped: {skippedSummary}. " + - "This may indicate the file was created with a newer version or has unsupported node types." - ); - - int familyFiltered = skippedNodes.Count(s => - s.Reason.Contains("canvas family mismatch", StringComparison.OrdinalIgnoreCase) - ); - if (familyFiltered > 0) - { - string target = allowedFamily == CanvasNodeFamily.Ddl ? "DDL" : "Query"; - warnings.Add( - $"Skipped {familyFiltered} legacy node(s) that belong to the opposite canvas family while loading {target} canvas." - ); - } - } - - ConnectionRebuildStats connectionStats = RebuildConnections(saved.Connections, nodeMap, vm.Connections); - AddConnectionRebuildWarnings(connectionStats, warnings); - - foreach (SavedNode sn in saved.Nodes) - { - if (!nodeMap.TryGetValue(sn.NodeId, out NodeViewModel? nodeVm)) - continue; - if (nodeVm.Type == NodeType.ResultOutput) - { - nodeVm.SyncOutputColumns(vm.Connections); - - if (!sn.Parameters.TryGetValue("__colOrder", out string? colOrderStr)) - continue; - - string[] savedOrder = colOrderStr.Split('|', StringSplitOptions.RemoveEmptyEntries); - ApplySavedColumnOrder(nodeVm, savedOrder); - continue; - } - - if (nodeVm.Type == NodeType.CteSource) - nodeVm.SyncCteSourceColumns(vm.Connections); - } - - return CanvasLoadResult.Ok( - warnings.Count > 0 ? warnings : null, - activeDocumentType: null, - sqlEditorSeedScripts: sqlEditorSeedScripts.Count > 0 ? sqlEditorSeedScripts : null); - } - - private static IReadOnlyList ExtractLegacyReportSqlScripts(SavedCanvas saved) - { - Dictionary nodesById = saved.Nodes - .ToDictionary(node => node.NodeId, StringComparer.Ordinal); - - HashSet scripts = []; - foreach (SavedNode reportOutput in saved.Nodes.Where(node => - string.Equals(node.NodeType, "ReportOutput", StringComparison.OrdinalIgnoreCase))) - { - foreach (SavedConnection wire in saved.Connections.Where(connection => - string.Equals(connection.ToNodeId, reportOutput.NodeId, StringComparison.Ordinal) - && string.Equals(connection.ToPinName, "query", StringComparison.OrdinalIgnoreCase))) - { - if (!nodesById.TryGetValue(wire.FromNodeId, out SavedNode? sourceNode)) - continue; - if (!string.Equals(sourceNode.NodeType, "RawSqlQuery", StringComparison.OrdinalIgnoreCase)) - continue; - - string? sql = ResolveLegacyReportSql(sourceNode); - if (!string.IsNullOrWhiteSpace(sql)) - scripts.Add(sql.Trim()); - } - } - - return scripts.ToList(); - } - - private static string? ResolveLegacyReportSql(SavedNode rawSqlNode) - { - if (rawSqlNode.Parameters.TryGetValue("sql_text", out string? sqlText) - && !string.IsNullOrWhiteSpace(sqlText)) - { - return sqlText; - } - - if (rawSqlNode.Parameters.TryGetValue("sql", out string? sql) - && !string.IsNullOrWhiteSpace(sql)) - { - return sql; - } - - return null; - } - - private static bool LooksLikeLegacyWorkspaceEnvelope(string json) - { - try - { - using JsonDocument doc = JsonDocument.Parse(json); - JsonElement root = doc.RootElement; - return root.ValueKind == JsonValueKind.Object - && root.TryGetProperty(nameof(SavedWorkspaceCanvas.QueryCanvas), out _) - && root.TryGetProperty(nameof(SavedWorkspaceCanvas.DdlCanvas), out _); - } - catch - { - return false; - } - } - - private static bool LooksLikeDocumentWorkspaceEnvelope(string json) - { - try - { - using JsonDocument doc = JsonDocument.Parse(json); - JsonElement root = doc.RootElement; - return root.ValueKind == JsonValueKind.Object - && root.TryGetProperty(nameof(SavedWorkspaceDocumentsCanvas.Documents), out _); - } - catch - { - return false; - } - } - - private static bool IsDocumentType(SavedWorkspaceDocument document, WorkspaceDocumentType documentType) - { - return string.Equals(document.DocumentType, documentType.ToString(), StringComparison.OrdinalIgnoreCase); - } - - private static WorkspaceDocumentType ResolveActiveDocumentType( - SavedWorkspaceDocumentsCanvas workspace, - IReadOnlyList documents) - { - if (workspace.ActiveDocumentId is Guid activeId) - { - SavedWorkspaceDocument? active = documents.FirstOrDefault(document => document.DocumentId == activeId); - if (active is not null && Enum.TryParse(active.DocumentType, true, out WorkspaceDocumentType activeType)) - return activeType; - } - - return documents - .Select(document => Enum.TryParse(document.DocumentType, true, out WorkspaceDocumentType type) ? type : (WorkspaceDocumentType?)null) - .FirstOrDefault(type => type.HasValue) ?? WorkspaceDocumentType.QueryCanvas; - } - - private static WorkspaceDocumentType ResolveLegacyActiveDocumentType(SavedWorkspaceCanvas workspace) - { - bool hasQueryContent = workspace.QueryCanvas.Nodes.Count > 0 || workspace.QueryCanvas.Connections.Count > 0; - bool hasDdlContent = workspace.DdlCanvas.Nodes.Count > 0 || workspace.DdlCanvas.Connections.Count > 0; - - if (!hasQueryContent && hasDdlContent) - return WorkspaceDocumentType.DdlCanvas; - - return WorkspaceDocumentType.QueryCanvas; - } -} +using System.Text.Json; +using Avalonia; +using AkkornStudio.Core; +using AkkornStudio.Nodes; +using AkkornStudio.UI.Services.Workspace.Models; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Serialization; + +public static partial class CanvasSerializer +{ + /// + /// Rebuilds a from JSON. + /// Clears the existing canvas before loading. + /// Returns a . + /// + /// Optional catalog to restore TableSource column pins. + public static CanvasLoadResult Deserialize( + string json, + CanvasViewModel vm, + IReadOnlyDictionary>? columnLookup = + null + ) + { + if (LooksLikeDocumentWorkspaceEnvelope(json) || LooksLikeLegacyWorkspaceEnvelope(json)) + return DeserializeWorkspace(json, vm, null, columnLookup); + + SavedCanvas? raw; + try + { + raw = JsonSerializer.Deserialize(json, _opts); + } + catch (JsonException ex) + { + return CanvasLoadResult.Fail($"Invalid JSON: {ex.Message}"); + } + + if (raw is null) + return CanvasLoadResult.Fail("Canvas file is empty or could not be parsed."); + + return DeserializeSavedCanvas(raw, vm, columnLookup); + } + + public static CanvasLoadResult DeserializeWorkspace( + string json, + CanvasViewModel queryVm, + CanvasViewModel? ddlVm, + IReadOnlyDictionary>? columnLookup = + null + ) + { + if (!LooksLikeDocumentWorkspaceEnvelope(json) && !LooksLikeLegacyWorkspaceEnvelope(json)) + return CanvasLoadResult.Fail("Unsupported file format. Expected workspace canvas envelope."); + + if (LooksLikeDocumentWorkspaceEnvelope(json)) + return DeserializeDocumentWorkspace(json, queryVm, ddlVm, columnLookup); + + return DeserializeLegacyWorkspace(json, queryVm, ddlVm, columnLookup); + } + + private static CanvasLoadResult DeserializeDocumentWorkspace( + string json, + CanvasViewModel queryVm, + CanvasViewModel? ddlVm, + IReadOnlyDictionary>? columnLookup) + { + SavedWorkspaceDocumentsCanvas? workspace; + try + { + workspace = JsonSerializer.Deserialize(json, _opts); + } + catch (JsonException ex) + { + return CanvasLoadResult.Fail($"Invalid workspace JSON: {ex.Message}"); + } + + if (workspace is null) + return CanvasLoadResult.Fail("Workspace file is empty or could not be parsed."); + + if (workspace.Version < 5 || workspace.Version > CurrentSchemaVersion) + return CanvasLoadResult.Fail( + $"Unsupported workspace schema version {workspace.Version}. " + + $"This build supports versions 5–{CurrentSchemaVersion}." + ); + + List documents = workspace.Documents ?? []; + if (documents.Count == 0) + return CanvasLoadResult.Fail("Workspace does not contain any document."); + + SavedWorkspaceDocument? queryDocument = documents.FirstOrDefault(document => + IsDocumentType(document, WorkspaceDocumentType.QueryCanvas)); + if (queryDocument?.CanvasPayload is null) + return CanvasLoadResult.Fail("Workspace query document payload is missing."); + + CanvasLoadResult queryLoad = DeserializeSavedCanvas( + queryDocument.CanvasPayload, + queryVm, + columnLookup, + CanvasNodeFamily.Query + ); + if (!queryLoad.Success) + return queryLoad; + + var warnings = queryLoad.Warnings?.ToList() ?? []; + + SavedWorkspaceDocument? ddlDocument = documents.FirstOrDefault(document => + IsDocumentType(document, WorkspaceDocumentType.DdlCanvas)); + if (ddlDocument?.CanvasPayload is not null && ddlVm is not null) + { + CanvasLoadResult ddlLoad = ApplyDdlFromSavedCanvas(ddlDocument.CanvasPayload, ddlVm); + if (!ddlLoad.Success) + warnings.Add($"DDL canvas restore skipped: {ddlLoad.Error}"); + else if (ddlLoad.Warnings is { Count: > 0 }) + warnings.AddRange(ddlLoad.Warnings.Select(w => $"DDL: {w}")); + } + else if (ddlDocument?.CanvasPayload is not null) + { + warnings.Add("File contains a DDL document snapshot, but no DDL target canvas was provided during load."); + } + + WorkspaceDocumentType activeDocumentType = ResolveActiveDocumentType(workspace, documents); + return CanvasLoadResult.Ok( + warnings.Count > 0 ? warnings : null, + activeDocumentType, + queryLoad.SqlEditorSeedScripts); + } + + private static CanvasLoadResult DeserializeLegacyWorkspace( + string json, + CanvasViewModel queryVm, + CanvasViewModel? ddlVm, + IReadOnlyDictionary>? columnLookup) + { + SavedWorkspaceCanvas? workspace; + try + { + workspace = JsonSerializer.Deserialize(json, _opts); + } + catch (JsonException ex) + { + return CanvasLoadResult.Fail($"Invalid workspace JSON: {ex.Message}"); + } + + if (workspace is null) + return CanvasLoadResult.Fail("Workspace file is empty or could not be parsed."); + + if (workspace.Version != 4) + return CanvasLoadResult.Fail( + $"Unsupported workspace schema version {workspace.Version}. " + + "This build supports legacy version 4 and document schema v5+." + ); + + CanvasLoadResult queryLoad = DeserializeSavedCanvas( + workspace.QueryCanvas, + queryVm, + columnLookup, + CanvasNodeFamily.Query + ); + if (!queryLoad.Success) + return queryLoad; + + var warnings = queryLoad.Warnings?.ToList() ?? []; + + if (ddlVm is not null) + { + CanvasLoadResult ddlLoad = ApplyDdlFromSavedCanvas(workspace.DdlCanvas, ddlVm); + if (!ddlLoad.Success) + warnings.Add($"DDL canvas restore skipped: {ddlLoad.Error}"); + else if (ddlLoad.Warnings is { Count: > 0 }) + warnings.AddRange(ddlLoad.Warnings.Select(w => $"DDL: {w}")); + } + else if (workspace.DdlCanvas.Nodes.Count > 0 || workspace.DdlCanvas.Connections.Count > 0) + { + warnings.Add("File contains a DDL canvas snapshot, but no DDL target canvas was provided during load."); + } + + warnings.Add("Legacy workspace envelope migrated to document-oriented workspace model during load."); + WorkspaceDocumentType activeType = ResolveLegacyActiveDocumentType(workspace); + return CanvasLoadResult.Ok( + warnings.Count > 0 ? warnings : null, + activeType, + queryLoad.SqlEditorSeedScripts); + } + + private static CanvasLoadResult ApplyDdlFromSavedCanvas( + SavedCanvas saved, + CanvasViewModel ddlVm + ) + { + var scratch = new CanvasViewModel(); + scratch.Nodes.Clear(); + scratch.Connections.Clear(); + + CanvasLoadResult load = DeserializeSavedCanvas(saved, scratch, null, CanvasNodeFamily.Ddl); + if (!load.Success) + return load; + + ddlVm.ReplaceGraph(scratch.Nodes.ToList(), scratch.Connections.ToList()); + if (Enum.TryParse(saved.DatabaseProvider, true, out DatabaseProvider provider)) + ddlVm.Provider = provider; + + return load; + } + + private static CanvasLoadResult DeserializeSavedCanvas( + SavedCanvas raw, + CanvasViewModel vm, + IReadOnlyDictionary>? columnLookup, + CanvasNodeFamily allowedFamily = CanvasNodeFamily.Any + ) + { + if (raw.Version != CurrentCanvasSchemaVersion) + return CanvasLoadResult.Fail( + $"Unsupported canvas schema version {raw.Version}. " + + $"This build supports only version {CurrentCanvasSchemaVersion}." + ); + + SavedCanvas saved = raw; + var warnings = new List(); + IReadOnlyList sqlEditorSeedScripts = allowedFamily == CanvasNodeFamily.Query + ? ExtractLegacyReportSqlScripts(saved) + : []; + if (sqlEditorSeedScripts.Count > 0) + { + warnings.Add( + $"Migrated {sqlEditorSeedScripts.Count} legacy report SQL script(s) to SQL Editor seed drafts during load."); + } + + vm.Connections.Clear(); + vm.Nodes.Clear(); + vm.UndoRedo.Clear(); + + vm.Zoom = saved.Zoom; + vm.PanOffset = new Point(saved.PanX, saved.PanY); + vm.ReplacePreviewParameterInputs(saved.PreviewParameterInputs); + + var nodeMap = new Dictionary(StringComparer.Ordinal); + var skippedNodes = new List<(string NodeId, string NodeType, string Reason)>(); + + foreach (SavedNode sn in saved.Nodes) + { + (NodeViewModel? nodeVm, string? skipReason) = BuildNodeVm(sn, columnLookup, allowedFamily); + if (nodeVm is null) + { + skippedNodes.Add((sn.NodeId, sn.NodeType, skipReason ?? "Unknown error")); + continue; + } + nodeMap[sn.NodeId] = nodeVm; + vm.Nodes.Add(nodeVm); + } + + var loadOrderById = saved.Nodes + .Select((n, i) => (n.NodeId, i)) + .ToDictionary(x => x.NodeId, x => x.i, StringComparer.Ordinal); + + int z = 0; + foreach (NodeViewModel n in vm.Nodes + .OrderBy(n => n.ZOrder) + .ThenBy(n => loadOrderById.TryGetValue(n.Id, out int idx) ? idx : int.MaxValue)) + n.ZOrder = z++; + + if (skippedNodes.Count > 0) + { + var skippedSummary = string.Join(", ", + skippedNodes.GroupBy(s => s.NodeType) + .Select(g => $"{g.Count()} {g.Key}")); + warnings.Add( + $"Warning: {skippedNodes.Count} node(s) could not be loaded and were skipped: {skippedSummary}. " + + "This may indicate the file was created with a newer version or has unsupported node types." + ); + + int familyFiltered = skippedNodes.Count(s => + s.Reason.Contains("canvas family mismatch", StringComparison.OrdinalIgnoreCase) + ); + if (familyFiltered > 0) + { + string target = allowedFamily == CanvasNodeFamily.Ddl ? "DDL" : "Query"; + warnings.Add( + $"Skipped {familyFiltered} legacy node(s) that belong to the opposite canvas family while loading {target} canvas." + ); + } + } + + ConnectionRebuildStats connectionStats = RebuildConnections(saved.Connections, nodeMap, vm.Connections); + AddConnectionRebuildWarnings(connectionStats, warnings); + + foreach (SavedNode sn in saved.Nodes) + { + if (!nodeMap.TryGetValue(sn.NodeId, out NodeViewModel? nodeVm)) + continue; + if (nodeVm.Type == NodeType.ResultOutput) + { + nodeVm.SyncOutputColumns(vm.Connections); + + if (!sn.Parameters.TryGetValue("__colOrder", out string? colOrderStr)) + continue; + + string[] savedOrder = colOrderStr.Split('|', StringSplitOptions.RemoveEmptyEntries); + ApplySavedColumnOrder(nodeVm, savedOrder); + continue; + } + + if (nodeVm.Type == NodeType.CteSource) + nodeVm.SyncCteSourceColumns(vm.Connections); + } + + return CanvasLoadResult.Ok( + warnings.Count > 0 ? warnings : null, + activeDocumentType: null, + sqlEditorSeedScripts: sqlEditorSeedScripts.Count > 0 ? sqlEditorSeedScripts : null); + } + + private static IReadOnlyList ExtractLegacyReportSqlScripts(SavedCanvas saved) + { + Dictionary nodesById = saved.Nodes + .ToDictionary(node => node.NodeId, StringComparer.Ordinal); + + HashSet scripts = []; + foreach (SavedNode reportOutput in saved.Nodes.Where(node => + string.Equals(node.NodeType, "ReportOutput", StringComparison.OrdinalIgnoreCase))) + { + foreach (SavedConnection wire in saved.Connections.Where(connection => + string.Equals(connection.ToNodeId, reportOutput.NodeId, StringComparison.Ordinal) + && string.Equals(connection.ToPinName, "query", StringComparison.OrdinalIgnoreCase))) + { + if (!nodesById.TryGetValue(wire.FromNodeId, out SavedNode? sourceNode)) + continue; + if (!string.Equals(sourceNode.NodeType, "RawSqlQuery", StringComparison.OrdinalIgnoreCase)) + continue; + + string? sql = ResolveLegacyReportSql(sourceNode); + if (!string.IsNullOrWhiteSpace(sql)) + scripts.Add(sql.Trim()); + } + } + + return scripts.ToList(); + } + + private static string? ResolveLegacyReportSql(SavedNode rawSqlNode) + { + if (rawSqlNode.Parameters.TryGetValue("sql_text", out string? sqlText) + && !string.IsNullOrWhiteSpace(sqlText)) + { + return sqlText; + } + + if (rawSqlNode.Parameters.TryGetValue("sql", out string? sql) + && !string.IsNullOrWhiteSpace(sql)) + { + return sql; + } + + return null; + } + + private static bool LooksLikeLegacyWorkspaceEnvelope(string json) + { + try + { + using JsonDocument doc = JsonDocument.Parse(json); + JsonElement root = doc.RootElement; + return root.ValueKind == JsonValueKind.Object + && root.TryGetProperty(nameof(SavedWorkspaceCanvas.QueryCanvas), out _) + && root.TryGetProperty(nameof(SavedWorkspaceCanvas.DdlCanvas), out _); + } + catch + { + return false; + } + } + + private static bool LooksLikeDocumentWorkspaceEnvelope(string json) + { + try + { + using JsonDocument doc = JsonDocument.Parse(json); + JsonElement root = doc.RootElement; + return root.ValueKind == JsonValueKind.Object + && root.TryGetProperty(nameof(SavedWorkspaceDocumentsCanvas.Documents), out _); + } + catch + { + return false; + } + } + + private static bool IsDocumentType(SavedWorkspaceDocument document, WorkspaceDocumentType documentType) + { + return string.Equals(document.DocumentType, documentType.ToString(), StringComparison.OrdinalIgnoreCase); + } + + private static WorkspaceDocumentType ResolveActiveDocumentType( + SavedWorkspaceDocumentsCanvas workspace, + IReadOnlyList documents) + { + if (workspace.ActiveDocumentId is Guid activeId) + { + SavedWorkspaceDocument? active = documents.FirstOrDefault(document => document.DocumentId == activeId); + if (active is not null && Enum.TryParse(active.DocumentType, true, out WorkspaceDocumentType activeType)) + return activeType; + } + + return documents + .Select(document => Enum.TryParse(document.DocumentType, true, out WorkspaceDocumentType type) ? type : (WorkspaceDocumentType?)null) + .FirstOrDefault(type => type.HasValue) ?? WorkspaceDocumentType.QueryCanvas; + } + + private static WorkspaceDocumentType ResolveLegacyActiveDocumentType(SavedWorkspaceCanvas workspace) + { + bool hasQueryContent = workspace.QueryCanvas.Nodes.Count > 0 || workspace.QueryCanvas.Connections.Count > 0; + bool hasDdlContent = workspace.DdlCanvas.Nodes.Count > 0 || workspace.DdlCanvas.Connections.Count > 0; + + if (!hasQueryContent && hasDdlContent) + return WorkspaceDocumentType.DdlCanvas; + + return WorkspaceDocumentType.QueryCanvas; + } +} diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Nodes.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Nodes.cs similarity index 96% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Nodes.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Nodes.cs index c371e368..9f876c41 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Nodes.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Nodes.cs @@ -1,261 +1,261 @@ -using System.Text.Json; -using Avalonia; -using DBWeaver.Nodes; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Serialization; - -public static partial class CanvasSerializer -{ - private static (NodeViewModel?, string? SkipReason) BuildNodeVm( - SavedNode sn, - IReadOnlyDictionary>? columnLookup, - CanvasNodeFamily allowedFamily = CanvasNodeFamily.Any - ) - { - if (string.Equals(sn.NodeType, "RawSqlQuery", StringComparison.OrdinalIgnoreCase) - || string.Equals(sn.NodeType, "ReportOutput", StringComparison.OrdinalIgnoreCase)) - { - return ( - null, - $"Node '{sn.NodeType}' migrated to SQL Editor script flow during load" - ); - } - - string normalizedNodeType = sn.NodeType; - if (string.Equals(sn.NodeType, "SelectOutput", StringComparison.OrdinalIgnoreCase)) - normalizedNodeType = nameof(NodeType.ResultOutput); - else if (string.Equals(sn.NodeType, "WhereOutput", StringComparison.OrdinalIgnoreCase)) - normalizedNodeType = nameof(NodeType.CompileWhere); - - if (!Enum.TryParse(normalizedNodeType, out NodeType nodeType)) - return (null, $"Unknown node type '{sn.NodeType}' (not supported in this version)"); - - if (!IsNodeTypeAllowed(nodeType, allowedFamily)) - { - string familyName = allowedFamily == CanvasNodeFamily.Ddl ? "DDL" : "Query"; - return ( - null, - $"Node '{sn.NodeType}' skipped due to canvas family mismatch (target: {familyName})" - ); - } - - NodeViewModel vm; - - if (nodeType == NodeType.TableSource && sn.TableFullName is not null) - { - // Restore columns — prefer persisted columns, fall back to lookup catalog - IEnumerable<(string, PinDataType)> cols = []; - - IReadOnlyDictionary? lookupColumnTypes = null; - if ( - columnLookup is not null - && columnLookup.TryGetValue( - sn.TableFullName, - out IReadOnlyList<(string Name, PinDataType Type)>? foundLookup - ) - ) - { - lookupColumnTypes = foundLookup.ToDictionary( - c => c.Name, - c => c.Type, - StringComparer.OrdinalIgnoreCase - ); - } - - // First: use persisted columns if available - if (sn.Columns is { Count: > 0 }) - { - cols = sn.Columns - .Select(c => ( - c.Name, - ResolveSavedColumnType(c, lookupColumnTypes) - )) - .ToList(); - } - // Fallback: use lookup catalog - else if ( - columnLookup is not null - && columnLookup.TryGetValue( - sn.TableFullName, - out IReadOnlyList<(string Name, PinDataType Type)>? found - ) - ) - { - cols = found; - } - - vm = new NodeViewModel(sn.TableFullName, cols, new Point(sn.X, sn.Y)); - } - else - { - NodeDefinition def; - try - { - def = NodeDefinitionRegistry.Get(nodeType); - } - catch (Exception ex) - { - return (null, $"NodeDefinition not found for type '{nodeType}': {ex.Message}"); - } - - vm = new NodeViewModel(def, new Point(sn.X, sn.Y)); - } - - // Override ID to match saved ID (for connection mapping) - // Since Id is init-only we use a workaround via reflection - System.Reflection.PropertyInfo? idProp = typeof(NodeViewModel).GetProperty( - nameof(NodeViewModel.Id) - ); - if (idProp is null || !idProp.CanWrite) - return (null, "Could not restore node ID (Id property is not writable)."); - - try - { - idProp.SetValue(vm, sn.NodeId); - } - catch (Exception ex) - { - return (null, $"Could not restore node ID '{sn.NodeId}': {ex.Message}"); - } - - // Old files may not contain layer info; defer normalization in Deserialize. - vm.ZOrder = sn.ZOrder ?? 0; - - vm.Alias = sn.Alias; - - foreach (KeyValuePair kv in sn.Parameters) - vm.Parameters[kv.Key] = kv.Value; - - NormalizeDeserializedNodeParameters(vm); - - foreach (KeyValuePair kv in sn.PinLiterals) - vm.PinLiterals[kv.Key] = kv.Value; - - if (sn.CteSubgraph is not null) - vm.Parameters[CteSubgraphParameterKey] = JsonSerializer.Serialize(sn.CteSubgraph); - - if (sn.ViewSubgraph is not null) - { - if (!string.IsNullOrWhiteSpace(sn.ViewSubgraph.GraphJson)) - vm.Parameters[ViewSubgraphParameterKey] = sn.ViewSubgraph.GraphJson!; - - if (!string.IsNullOrWhiteSpace(sn.ViewSubgraph.EditorCanvasJson)) - vm.Parameters[ViewEditorCanvasParameterKey] = sn.ViewSubgraph.EditorCanvasJson!; - - bool hasFromTable = vm.Parameters.TryGetValue(ViewFromTableParameterKey, out string? existingFrom) - && !string.IsNullOrWhiteSpace(existingFrom); - - if (!string.IsNullOrWhiteSpace(sn.ViewSubgraph.FromTable) - && !hasFromTable) - { - vm.Parameters[ViewFromTableParameterKey] = sn.ViewSubgraph.FromTable!; - } - } - - return (vm, null); - } - - private static void NormalizeDeserializedNodeParameters(NodeViewModel nodeVm) - { - nodeVm.Parameters.Remove("set_operator"); - nodeVm.Parameters.Remove("set_query"); - nodeVm.Parameters.Remove("import_order_terms"); - nodeVm.Parameters.Remove("import_group_terms"); - - if (nodeVm.Type is NodeType.SubqueryExists or NodeType.SubqueryIn or NodeType.SubqueryScalar or NodeType.SubqueryReference) - nodeVm.Parameters.Remove("query"); - } - - private static PinDataType ResolveSavedColumnType( - SavedColumn column, - IReadOnlyDictionary? lookupColumnTypes - ) - { - if (Enum.TryParse(column.Type, out PinDataType parsedType)) - { - if ( - parsedType != PinDataType.ColumnRef - || lookupColumnTypes is null - || !lookupColumnTypes.TryGetValue(column.Name, out PinDataType resolved) - ) - { - return parsedType; - } - - return resolved; - } - - if ( - lookupColumnTypes is not null - && lookupColumnTypes.TryGetValue(column.Name, out PinDataType fromLookup) - ) - { - return fromLookup; - } - - return PinDataType.ColumnRef; - } - - private static void ApplySavedColumnOrder(NodeViewModel nodeVm, IReadOnlyList savedOrder) - { - for (int i = 0; i < savedOrder.Count; i++) - { - if (i >= nodeVm.OutputColumnOrder.Count) - break; - - string key = savedOrder[i]; - int currentIndex = -1; - for (int idx = 0; idx < nodeVm.OutputColumnOrder.Count; idx++) - { - if (nodeVm.OutputColumnOrder[idx].Key != key) - continue; - - currentIndex = idx; - break; - } - - if (currentIndex < 0 || currentIndex == i) - continue; - - nodeVm.OutputColumnOrder.Move(currentIndex, i); - } - } - - private static bool IsNodeTypeAllowed(NodeType nodeType, CanvasNodeFamily family) - { - if (family == CanvasNodeFamily.Any) - return true; - - bool isDdlNode = IsDdlNodeType(nodeType); - return family == CanvasNodeFamily.Ddl ? isDdlNode : !isDdlNode; - } - - private static bool IsDdlNodeType(NodeType nodeType) => - nodeType - is NodeType.TableDefinition - or NodeType.ColumnDefinition - or NodeType.PrimaryKeyConstraint - or NodeType.ForeignKeyConstraint - or NodeType.UniqueConstraint - or NodeType.CheckConstraint - or NodeType.DefaultConstraint - or NodeType.IndexDefinition - or NodeType.ViewDefinition - or NodeType.CreateTableOutput - or NodeType.EnumTypeDefinition - or NodeType.CreateTypeOutput - or NodeType.SequenceDefinition - or NodeType.CreateSequenceOutput - or NodeType.CreateTableAsOutput - or NodeType.CreateViewOutput - or NodeType.AlterViewOutput - or NodeType.AlterTableOutput - or NodeType.CreateIndexOutput - or NodeType.AddColumnOp - or NodeType.DropColumnOp - or NodeType.RenameColumnOp - or NodeType.RenameTableOp - or NodeType.DropTableOp - or NodeType.AlterColumnTypeOp; -} +using System.Text.Json; +using Avalonia; +using AkkornStudio.Nodes; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Serialization; + +public static partial class CanvasSerializer +{ + private static (NodeViewModel?, string? SkipReason) BuildNodeVm( + SavedNode sn, + IReadOnlyDictionary>? columnLookup, + CanvasNodeFamily allowedFamily = CanvasNodeFamily.Any + ) + { + if (string.Equals(sn.NodeType, "RawSqlQuery", StringComparison.OrdinalIgnoreCase) + || string.Equals(sn.NodeType, "ReportOutput", StringComparison.OrdinalIgnoreCase)) + { + return ( + null, + $"Node '{sn.NodeType}' migrated to SQL Editor script flow during load" + ); + } + + string normalizedNodeType = sn.NodeType; + if (string.Equals(sn.NodeType, "SelectOutput", StringComparison.OrdinalIgnoreCase)) + normalizedNodeType = nameof(NodeType.ResultOutput); + else if (string.Equals(sn.NodeType, "WhereOutput", StringComparison.OrdinalIgnoreCase)) + normalizedNodeType = nameof(NodeType.CompileWhere); + + if (!Enum.TryParse(normalizedNodeType, out NodeType nodeType)) + return (null, $"Unknown node type '{sn.NodeType}' (not supported in this version)"); + + if (!IsNodeTypeAllowed(nodeType, allowedFamily)) + { + string familyName = allowedFamily == CanvasNodeFamily.Ddl ? "DDL" : "Query"; + return ( + null, + $"Node '{sn.NodeType}' skipped due to canvas family mismatch (target: {familyName})" + ); + } + + NodeViewModel vm; + + if (nodeType == NodeType.TableSource && sn.TableFullName is not null) + { + // Restore columns — prefer persisted columns, fall back to lookup catalog + IEnumerable<(string, PinDataType)> cols = []; + + IReadOnlyDictionary? lookupColumnTypes = null; + if ( + columnLookup is not null + && columnLookup.TryGetValue( + sn.TableFullName, + out IReadOnlyList<(string Name, PinDataType Type)>? foundLookup + ) + ) + { + lookupColumnTypes = foundLookup.ToDictionary( + c => c.Name, + c => c.Type, + StringComparer.OrdinalIgnoreCase + ); + } + + // First: use persisted columns if available + if (sn.Columns is { Count: > 0 }) + { + cols = sn.Columns + .Select(c => ( + c.Name, + ResolveSavedColumnType(c, lookupColumnTypes) + )) + .ToList(); + } + // Fallback: use lookup catalog + else if ( + columnLookup is not null + && columnLookup.TryGetValue( + sn.TableFullName, + out IReadOnlyList<(string Name, PinDataType Type)>? found + ) + ) + { + cols = found; + } + + vm = new NodeViewModel(sn.TableFullName, cols, new Point(sn.X, sn.Y)); + } + else + { + NodeDefinition def; + try + { + def = NodeDefinitionRegistry.Get(nodeType); + } + catch (Exception ex) + { + return (null, $"NodeDefinition not found for type '{nodeType}': {ex.Message}"); + } + + vm = new NodeViewModel(def, new Point(sn.X, sn.Y)); + } + + // Override ID to match saved ID (for connection mapping) + // Since Id is init-only we use a workaround via reflection + System.Reflection.PropertyInfo? idProp = typeof(NodeViewModel).GetProperty( + nameof(NodeViewModel.Id) + ); + if (idProp is null || !idProp.CanWrite) + return (null, "Could not restore node ID (Id property is not writable)."); + + try + { + idProp.SetValue(vm, sn.NodeId); + } + catch (Exception ex) + { + return (null, $"Could not restore node ID '{sn.NodeId}': {ex.Message}"); + } + + // Old files may not contain layer info; defer normalization in Deserialize. + vm.ZOrder = sn.ZOrder ?? 0; + + vm.Alias = sn.Alias; + + foreach (KeyValuePair kv in sn.Parameters) + vm.Parameters[kv.Key] = kv.Value; + + NormalizeDeserializedNodeParameters(vm); + + foreach (KeyValuePair kv in sn.PinLiterals) + vm.PinLiterals[kv.Key] = kv.Value; + + if (sn.CteSubgraph is not null) + vm.Parameters[CteSubgraphParameterKey] = JsonSerializer.Serialize(sn.CteSubgraph); + + if (sn.ViewSubgraph is not null) + { + if (!string.IsNullOrWhiteSpace(sn.ViewSubgraph.GraphJson)) + vm.Parameters[ViewSubgraphParameterKey] = sn.ViewSubgraph.GraphJson!; + + if (!string.IsNullOrWhiteSpace(sn.ViewSubgraph.EditorCanvasJson)) + vm.Parameters[ViewEditorCanvasParameterKey] = sn.ViewSubgraph.EditorCanvasJson!; + + bool hasFromTable = vm.Parameters.TryGetValue(ViewFromTableParameterKey, out string? existingFrom) + && !string.IsNullOrWhiteSpace(existingFrom); + + if (!string.IsNullOrWhiteSpace(sn.ViewSubgraph.FromTable) + && !hasFromTable) + { + vm.Parameters[ViewFromTableParameterKey] = sn.ViewSubgraph.FromTable!; + } + } + + return (vm, null); + } + + private static void NormalizeDeserializedNodeParameters(NodeViewModel nodeVm) + { + nodeVm.Parameters.Remove("set_operator"); + nodeVm.Parameters.Remove("set_query"); + nodeVm.Parameters.Remove("import_order_terms"); + nodeVm.Parameters.Remove("import_group_terms"); + + if (nodeVm.Type is NodeType.SubqueryExists or NodeType.SubqueryIn or NodeType.SubqueryScalar or NodeType.SubqueryReference) + nodeVm.Parameters.Remove("query"); + } + + private static PinDataType ResolveSavedColumnType( + SavedColumn column, + IReadOnlyDictionary? lookupColumnTypes + ) + { + if (Enum.TryParse(column.Type, out PinDataType parsedType)) + { + if ( + parsedType != PinDataType.ColumnRef + || lookupColumnTypes is null + || !lookupColumnTypes.TryGetValue(column.Name, out PinDataType resolved) + ) + { + return parsedType; + } + + return resolved; + } + + if ( + lookupColumnTypes is not null + && lookupColumnTypes.TryGetValue(column.Name, out PinDataType fromLookup) + ) + { + return fromLookup; + } + + return PinDataType.ColumnRef; + } + + private static void ApplySavedColumnOrder(NodeViewModel nodeVm, IReadOnlyList savedOrder) + { + for (int i = 0; i < savedOrder.Count; i++) + { + if (i >= nodeVm.OutputColumnOrder.Count) + break; + + string key = savedOrder[i]; + int currentIndex = -1; + for (int idx = 0; idx < nodeVm.OutputColumnOrder.Count; idx++) + { + if (nodeVm.OutputColumnOrder[idx].Key != key) + continue; + + currentIndex = idx; + break; + } + + if (currentIndex < 0 || currentIndex == i) + continue; + + nodeVm.OutputColumnOrder.Move(currentIndex, i); + } + } + + private static bool IsNodeTypeAllowed(NodeType nodeType, CanvasNodeFamily family) + { + if (family == CanvasNodeFamily.Any) + return true; + + bool isDdlNode = IsDdlNodeType(nodeType); + return family == CanvasNodeFamily.Ddl ? isDdlNode : !isDdlNode; + } + + private static bool IsDdlNodeType(NodeType nodeType) => + nodeType + is NodeType.TableDefinition + or NodeType.ColumnDefinition + or NodeType.PrimaryKeyConstraint + or NodeType.ForeignKeyConstraint + or NodeType.UniqueConstraint + or NodeType.CheckConstraint + or NodeType.DefaultConstraint + or NodeType.IndexDefinition + or NodeType.ViewDefinition + or NodeType.CreateTableOutput + or NodeType.EnumTypeDefinition + or NodeType.CreateTypeOutput + or NodeType.SequenceDefinition + or NodeType.CreateSequenceOutput + or NodeType.CreateTableAsOutput + or NodeType.CreateViewOutput + or NodeType.AlterViewOutput + or NodeType.AlterTableOutput + or NodeType.CreateIndexOutput + or NodeType.AddColumnOp + or NodeType.DropColumnOp + or NodeType.RenameColumnOp + or NodeType.RenameTableOp + or NodeType.DropTableOp + or NodeType.AlterColumnTypeOp; +} diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Save.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Save.cs similarity index 95% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Save.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Save.cs index 4962481c..6cce4c20 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Save.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Save.cs @@ -1,12 +1,12 @@ using Avalonia; -using DBWeaver.Nodes; -using DBWeaver.UI.Services.Workspace.Models; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Serialization; - -public static partial class CanvasSerializer -{ +using AkkornStudio.Nodes; +using AkkornStudio.UI.Services.Workspace.Models; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Serialization; + +public static partial class CanvasSerializer +{ public static string Serialize( CanvasViewModel vm, string provider = "Postgres", @@ -22,7 +22,7 @@ public static string Serialize( persistProviderMetadata: true); return System.Text.Json.JsonSerializer.Serialize(saved, _opts); } - + public static string SerializeWorkspace( CanvasViewModel queryVm, CanvasViewModel? ddlVm, @@ -44,6 +44,7 @@ public static string SerializeWorkspace( { WorkspaceDocumentType.DdlCanvas => ddlDocumentId, WorkspaceDocumentType.SqlEditor => sqlEditorDocumentId, + WorkspaceDocumentType.ErDiagram => queryDocumentId, _ => queryDocumentId, }; @@ -138,22 +139,22 @@ public static string SerializeWorkspaceDocuments( return System.Text.Json.JsonSerializer.Serialize(workspace, _opts); } - - private static SavedCanvas? TryDeserializeSavedCanvas(string? json) - { - if (string.IsNullOrWhiteSpace(json)) - return null; - - try - { - return System.Text.Json.JsonSerializer.Deserialize(json, _opts); - } - catch - { - return null; - } - } - + + private static SavedCanvas? TryDeserializeSavedCanvas(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return null; + + try + { + return System.Text.Json.JsonSerializer.Deserialize(json, _opts); + } + catch + { + return null; + } + } + private static SavedCanvas BuildSavedCanvas( CanvasViewModel vm, string provider, @@ -166,145 +167,152 @@ bool persistProviderMetadata Version: CurrentCanvasSchemaVersion, DatabaseProvider: persistProviderMetadata ? provider : null, ConnectionName: persistProviderMetadata ? connectionName : null, - Zoom: vm.Zoom, - PanX: vm.PanOffset.X, - PanY: vm.PanOffset.Y, - Nodes: [.. vm.Nodes.Select(n => SerialiseNode(n, vm.Nodes, vm.Connections))], - Connections: [.. vm.Connections - .Select(SerialiseConnection) - .Where(c => c is not null) - .Select(c => c!)], - SelectBindings: [], - WhereBindings: [], - AppVersion: AppVersion, - CreatedAt: DateTime.UtcNow.ToString("o"), - Description: description - ); - } - - private static SavedCanvas BuildSavedDdlCanvas( - CanvasViewModel? ddlVm, - string provider, - string connectionName - ) - { - if (ddlVm is null) - { - return new SavedCanvas( - Version: CurrentCanvasSchemaVersion, - DatabaseProvider: null, - ConnectionName: null, - Zoom: 1, - PanX: 0, - PanY: 0, - Nodes: [], - Connections: [], - SelectBindings: [], - WhereBindings: [], - AppVersion: AppVersion, - CreatedAt: DateTime.UtcNow.ToString("o") - ); - } - - return new SavedCanvas( - Version: CurrentCanvasSchemaVersion, - DatabaseProvider: null, - ConnectionName: null, - Zoom: 1, - PanX: 0, - PanY: 0, - Nodes: [.. ddlVm.Nodes.Select(n => SerialiseNode(n, ddlVm.Nodes, ddlVm.Connections))], - Connections: [.. ddlVm.Connections - .Select(SerialiseConnection) - .Where(c => c is not null) - .Select(c => c!)], - SelectBindings: [], - WhereBindings: [], - AppVersion: AppVersion, - CreatedAt: DateTime.UtcNow.ToString("o") - ); - } - - /// - /// Serialises a subset of nodes and the connections that are entirely - /// internal to that subset (both endpoints in ). - /// - public static (List Nodes, List Connections) SerialiseSubgraph( - IEnumerable nodes, - IEnumerable connections - ) - { - List nodeList = [.. nodes]; - HashSet ids = nodeList.Select(n => n.Id).ToHashSet(StringComparer.Ordinal); - return ( - [.. nodeList.Select(n => SerialiseNode(n, nodeList, connections, includeCteSubgraph: false))], - [.. connections.Where(c => - ids.Contains(c.FromPin.Owner.Id) && ids.Contains(c.ToPin?.Owner.Id ?? "") - ) - .Select(SerialiseConnection) - .Where(c => c is not null) - .Select(c => c!)] - ); - } - - /// - /// Inserts a saved subgraph into the canvas, generating fresh node IDs and - /// centering the pasted content at . - /// - public static void InsertSubgraph( - List nodes, - List conns, - CanvasViewModel vm, - Point canvasPos, - IReadOnlyDictionary< - string, - IReadOnlyList<(string Name, PinDataType Type)> - >? columnLookup = null - ) - { - if (nodes.Count == 0) - return; - - double cx = nodes.Average(n => n.X); - double cy = nodes.Average(n => n.Y); - - var idMap = new Dictionary(StringComparer.Ordinal); - foreach (SavedNode sn in nodes) - { - SavedNode positioned = sn with - { - NodeId = Guid.NewGuid().ToString(), - X = sn.X - cx + canvasPos.X, - Y = sn.Y - cy + canvasPos.Y, - }; - (NodeViewModel? nvm, _) = BuildNodeVm(positioned, columnLookup); - if (nvm is null) - continue; - idMap[sn.NodeId] = nvm; - vm.Nodes.Add(nvm); - } - - foreach (SavedConnection sc in conns) - { - if (!idMap.TryGetValue(sc.FromNodeId, out NodeViewModel? fromNode)) - continue; - if (!idMap.TryGetValue(sc.ToNodeId, out NodeViewModel? toNode)) - continue; - - if (!TryResolvePins(fromNode, sc.FromPinName, toNode, sc.ToPinName, out PinViewModel? fromPin, out PinViewModel? toPin)) - continue; - - TryConnect(vm.Connections, fromPin!, toPin!); - } - } - - private static SavedNode SerialiseNode( - NodeViewModel n, - IEnumerable allNodes, - IEnumerable allConnections, - bool includeCteSubgraph = true - ) - { + Zoom: vm.Zoom, + PanX: vm.PanOffset.X, + PanY: vm.PanOffset.Y, + Nodes: [.. vm.Nodes.Select(n => SerialiseNode(n, vm.Nodes, vm.Connections))], + Connections: [.. vm.Connections + .Select(SerialiseConnection) + .Where(c => c is not null) + .Select(c => c!)], + SelectBindings: [], + WhereBindings: [], + PreviewParameterInputs: vm.PreviewParameterInputs.Count > 0 + ? new Dictionary(vm.PreviewParameterInputs, StringComparer.OrdinalIgnoreCase) + : null, + AppVersion: AppVersion, + CreatedAt: DateTime.UtcNow.ToString("o"), + Description: description + ); + } + + private static SavedCanvas BuildSavedDdlCanvas( + CanvasViewModel? ddlVm, + string provider, + string connectionName + ) + { + if (ddlVm is null) + { + return new SavedCanvas( + Version: CurrentCanvasSchemaVersion, + DatabaseProvider: null, + ConnectionName: null, + Zoom: 1, + PanX: 0, + PanY: 0, + Nodes: [], + Connections: [], + SelectBindings: [], + WhereBindings: [], + PreviewParameterInputs: null, + AppVersion: AppVersion, + CreatedAt: DateTime.UtcNow.ToString("o") + ); + } + + return new SavedCanvas( + Version: CurrentCanvasSchemaVersion, + DatabaseProvider: null, + ConnectionName: null, + Zoom: 1, + PanX: 0, + PanY: 0, + Nodes: [.. ddlVm.Nodes.Select(n => SerialiseNode(n, ddlVm.Nodes, ddlVm.Connections))], + Connections: [.. ddlVm.Connections + .Select(SerialiseConnection) + .Where(c => c is not null) + .Select(c => c!)], + SelectBindings: [], + WhereBindings: [], + PreviewParameterInputs: ddlVm.PreviewParameterInputs.Count > 0 + ? new Dictionary(ddlVm.PreviewParameterInputs, StringComparer.OrdinalIgnoreCase) + : null, + AppVersion: AppVersion, + CreatedAt: DateTime.UtcNow.ToString("o") + ); + } + + /// + /// Serialises a subset of nodes and the connections that are entirely + /// internal to that subset (both endpoints in ). + /// + public static (List Nodes, List Connections) SerialiseSubgraph( + IEnumerable nodes, + IEnumerable connections + ) + { + List nodeList = [.. nodes]; + HashSet ids = nodeList.Select(n => n.Id).ToHashSet(StringComparer.Ordinal); + return ( + [.. nodeList.Select(n => SerialiseNode(n, nodeList, connections, includeCteSubgraph: false))], + [.. connections.Where(c => + ids.Contains(c.FromPin.Owner.Id) && ids.Contains(c.ToPin?.Owner.Id ?? "") + ) + .Select(SerialiseConnection) + .Where(c => c is not null) + .Select(c => c!)] + ); + } + + /// + /// Inserts a saved subgraph into the canvas, generating fresh node IDs and + /// centering the pasted content at . + /// + public static void InsertSubgraph( + List nodes, + List conns, + CanvasViewModel vm, + Point canvasPos, + IReadOnlyDictionary< + string, + IReadOnlyList<(string Name, PinDataType Type)> + >? columnLookup = null + ) + { + if (nodes.Count == 0) + return; + + double cx = nodes.Average(n => n.X); + double cy = nodes.Average(n => n.Y); + + var idMap = new Dictionary(StringComparer.Ordinal); + foreach (SavedNode sn in nodes) + { + SavedNode positioned = sn with + { + NodeId = Guid.NewGuid().ToString(), + X = sn.X - cx + canvasPos.X, + Y = sn.Y - cy + canvasPos.Y, + }; + (NodeViewModel? nvm, _) = BuildNodeVm(positioned, columnLookup); + if (nvm is null) + continue; + idMap[sn.NodeId] = nvm; + vm.Nodes.Add(nvm); + } + + foreach (SavedConnection sc in conns) + { + if (!idMap.TryGetValue(sc.FromNodeId, out NodeViewModel? fromNode)) + continue; + if (!idMap.TryGetValue(sc.ToNodeId, out NodeViewModel? toNode)) + continue; + + if (!TryResolvePins(fromNode, sc.FromPinName, toNode, sc.ToPinName, out PinViewModel? fromPin, out PinViewModel? toPin)) + continue; + + TryConnect(vm.Connections, fromPin!, toPin!); + } + } + + private static SavedNode SerialiseNode( + NodeViewModel n, + IEnumerable allNodes, + IEnumerable allConnections, + bool includeCteSubgraph = true + ) + { var parameters = new Dictionary(n.Parameters); parameters.Remove(CteSubgraphParameterKey); parameters.Remove("set_operator"); @@ -313,11 +321,11 @@ private static SavedNode SerialiseNode( parameters.Remove("import_group_terms"); if (n.Type is NodeType.SubqueryExists or NodeType.SubqueryIn or NodeType.SubqueryScalar or NodeType.SubqueryReference) parameters.Remove("query"); - SavedViewSubgraph? viewSubgraph = BuildViewSubgraph(n, parameters); - if (n.Type == NodeType.ResultOutput && n.OutputColumnOrder.Count > 0) - parameters["__colOrder"] = string.Join("|", n.OutputColumnOrder.Select(e => e.Key)); - - List? columns = null; + SavedViewSubgraph? viewSubgraph = BuildViewSubgraph(n, parameters); + if (n.Type == NodeType.ResultOutput && n.OutputColumnOrder.Count > 0) + parameters["__colOrder"] = string.Join("|", n.OutputColumnOrder.Select(e => e.Key)); + + List? columns = null; if (n.Type == NodeType.TableSource) { columns = n.OutputPins @@ -326,32 +334,32 @@ private static SavedNode SerialiseNode( (p.ColumnRefMeta?.ScalarType ?? p.EffectiveDataType).ToString())) .ToList(); } - - SavedCteSubgraph? cteSubgraph = includeCteSubgraph - ? BuildCteSubgraph(n, allNodes, allConnections) - : null; - - return new( - NodeId: n.Id, - NodeType: n.Type.ToString(), - X: n.Position.X, - Y: n.Position.Y, - ZOrder: n.ZOrder, - Alias: n.Alias, - TableFullName: n.Type == NodeType.TableSource ? n.Subtitle : null, - Parameters: parameters, - PinLiterals: new Dictionary(n.PinLiterals), - Columns: columns, - CteSubgraph: cteSubgraph, - ViewSubgraph: viewSubgraph - ); - } - - private static SavedConnection? SerialiseConnection(ConnectionViewModel c) - { - if (c.ToPin is null) - return null; - + + SavedCteSubgraph? cteSubgraph = includeCteSubgraph + ? BuildCteSubgraph(n, allNodes, allConnections) + : null; + + return new( + NodeId: n.Id, + NodeType: n.Type.ToString(), + X: n.Position.X, + Y: n.Position.Y, + ZOrder: n.ZOrder, + Alias: n.Alias, + TableFullName: n.Type == NodeType.TableSource ? n.Subtitle : null, + Parameters: parameters, + PinLiterals: new Dictionary(n.PinLiterals), + Columns: columns, + CteSubgraph: cteSubgraph, + ViewSubgraph: viewSubgraph + ); + } + + private static SavedConnection? SerialiseConnection(ConnectionViewModel c) + { + if (c.ToPin is null) + return null; + return new SavedConnection( FromNodeId: c.FromPin.Owner.Id, FromPinName: c.FromPin.Name, diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Storage.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Storage.cs similarity index 96% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Storage.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Storage.cs index 8e2be373..a768c1cb 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Storage.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Storage.cs @@ -1,14 +1,14 @@ -using System.IO.Compression; +using System.IO.Compression; using System.Text; using System.Text.Json; -using DBWeaver.Core; -using DBWeaver.UI.Services.Workspace.Models; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Serialization; - -public static partial class CanvasSerializer -{ +using AkkornStudio.Core; +using AkkornStudio.UI.Services.Workspace.Models; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Serialization; + +public static partial class CanvasSerializer +{ public static async Task SaveToFileAsync( string path, CanvasViewModel vm, @@ -20,7 +20,7 @@ public static async Task SaveToFileAsync( { await SaveToFileAsync(path, vm, null, provider, connection, description, null, activeDocumentType); } - + public static async Task SaveToFileAsync( string path, CanvasViewModel queryVm, @@ -45,73 +45,73 @@ public static async Task SaveToFileAsync( queryCanvasOverrideJson, activeDocumentType); byte[] utf8 = Encoding.UTF8.GetBytes(json); - bool useCompression = utf8.Length >= CompressionThresholdBytes; - byte[] payload = useCompression ? CompressBytes(utf8) : utf8; - - string? parentDir = Path.GetDirectoryName(path); - if (!string.IsNullOrWhiteSpace(parentDir)) - Directory.CreateDirectory(parentDir); - - await CreateAutomaticBackupAsync(path); - - await File.WriteAllBytesAsync(path, payload); - await AddLocalFileVersionAsync(path, payload); - } - - /// - /// Loads a canvas file and returns a . - /// Check before using the canvas. - /// - public static async Task LoadFromFileAsync( - string path, - CanvasViewModel vm, - IReadOnlyDictionary>? columnLookup = - null - ) - { - return await LoadFromFileAsync(path, vm, null, columnLookup); - } - - public static async Task LoadFromFileAsync( - string path, - CanvasViewModel queryVm, - CanvasViewModel? ddlVm, - IReadOnlyDictionary>? columnLookup = - null - ) - { - byte[] bytes; - try - { - bytes = await File.ReadAllBytesAsync(path); - } - catch (Exception ex) - { - return CanvasLoadResult.Fail($"Could not read file: {ex.Message}"); - } - - string json; - try - { - json = DecodeCanvasJson(bytes); - } - catch (Exception ex) - { - return CanvasLoadResult.Fail($"Could not decode canvas file: {ex.Message}"); - } - - return DeserializeWorkspace(json, queryVm, ddlVm, columnLookup); - } - - /// - /// Returns true if the file is a readable canvas file with a supported schema version. - /// - public static bool IsValidFile(string path) - { - try - { - byte[] bytes = File.ReadAllBytes(path); - string json = DecodeCanvasJson(bytes); + bool useCompression = utf8.Length >= CompressionThresholdBytes; + byte[] payload = useCompression ? CompressBytes(utf8) : utf8; + + string? parentDir = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(parentDir)) + Directory.CreateDirectory(parentDir); + + await CreateAutomaticBackupAsync(path); + + await File.WriteAllBytesAsync(path, payload); + await AddLocalFileVersionAsync(path, payload); + } + + /// + /// Loads a canvas file and returns a . + /// Check before using the canvas. + /// + public static async Task LoadFromFileAsync( + string path, + CanvasViewModel vm, + IReadOnlyDictionary>? columnLookup = + null + ) + { + return await LoadFromFileAsync(path, vm, null, columnLookup); + } + + public static async Task LoadFromFileAsync( + string path, + CanvasViewModel queryVm, + CanvasViewModel? ddlVm, + IReadOnlyDictionary>? columnLookup = + null + ) + { + byte[] bytes; + try + { + bytes = await File.ReadAllBytesAsync(path); + } + catch (Exception ex) + { + return CanvasLoadResult.Fail($"Could not read file: {ex.Message}"); + } + + string json; + try + { + json = DecodeCanvasJson(bytes); + } + catch (Exception ex) + { + return CanvasLoadResult.Fail($"Could not decode canvas file: {ex.Message}"); + } + + return DeserializeWorkspace(json, queryVm, ddlVm, columnLookup); + } + + /// + /// Returns true if the file is a readable canvas file with a supported schema version. + /// + public static bool IsValidFile(string path) + { + try + { + byte[] bytes = File.ReadAllBytes(path); + string json = DecodeCanvasJson(bytes); if (LooksLikeDocumentWorkspaceEnvelope(json)) { SavedWorkspaceDocumentsCanvas? workspace = JsonSerializer.Deserialize(json, _opts); @@ -131,31 +131,31 @@ public static bool IsValidFile(string path) && workspace.QueryCanvas.Version == CurrentCanvasSchemaVersion && workspace.DdlCanvas.Version == CurrentCanvasSchemaVersion; } - - SavedCanvas? saved = JsonSerializer.Deserialize(json, _opts); - return saved?.Version == CurrentCanvasSchemaVersion; - } - catch - { - return false; - } - } - - /// - /// Reads just the metadata fields from a file without fully loading the canvas. - /// Returns null if the file cannot be parsed. - /// + + SavedCanvas? saved = JsonSerializer.Deserialize(json, _opts); + return saved?.Version == CurrentCanvasSchemaVersion; + } + catch + { + return false; + } + } + + /// + /// Reads just the metadata fields from a file without fully loading the canvas. + /// Returns null if the file cannot be parsed. + /// public static ( int Version, string? AppVersion, string? CreatedAt, string? Description - )? ReadMeta(string path) - { - try - { - byte[] bytes = File.ReadAllBytes(path); - string json = DecodeCanvasJson(bytes); + )? ReadMeta(string path) + { + try + { + byte[] bytes = File.ReadAllBytes(path); + string json = DecodeCanvasJson(bytes); if (LooksLikeDocumentWorkspaceEnvelope(json)) { SavedWorkspaceDocumentsCanvas? workspace = JsonSerializer.Deserialize(json, _opts); @@ -174,25 +174,25 @@ public static ( { SavedWorkspaceCanvas? workspace = JsonSerializer.Deserialize(json, _opts); if (workspace is null) - return null; - - return ( - workspace.Version, - workspace.AppVersion ?? workspace.QueryCanvas.AppVersion, - workspace.CreatedAt ?? workspace.QueryCanvas.CreatedAt, - workspace.Description ?? workspace.QueryCanvas.Description - ); - } - - SavedCanvas? saved = JsonSerializer.Deserialize(json, _opts); - if (saved is null) - return null; - - return (saved.Version, saved.AppVersion, saved.CreatedAt, saved.Description); - } - catch - { - return null; + return null; + + return ( + workspace.Version, + workspace.AppVersion ?? workspace.QueryCanvas.AppVersion, + workspace.CreatedAt ?? workspace.QueryCanvas.CreatedAt, + workspace.Description ?? workspace.QueryCanvas.Description + ); + } + + SavedCanvas? saved = JsonSerializer.Deserialize(json, _opts); + if (saved is null) + return null; + + return (saved.Version, saved.AppVersion, saved.CreatedAt, saved.Description); + } + catch + { + return null; } } @@ -212,145 +212,145 @@ public static ( return null; } } - - public static IReadOnlyList GetLocalFileVersions(string targetFilePath) - { - string historyDir = GetHistoryDirectory(targetFilePath); - if (!Directory.Exists(historyDir)) - return []; - - return Directory - .EnumerateFiles(historyDir, "*.vsaq*") - .Select(path => - { - var fi = new FileInfo(path); - DateTimeOffset createdAt = fi.LastWriteTimeUtc; - string fileName = Path.GetFileNameWithoutExtension(path); - int sep = fileName.IndexOf('_'); - if (sep > 0 - && DateTimeOffset.TryParseExact( - fileName[..sep], - "yyyyMMddHHmmssfff", - null, - System.Globalization.DateTimeStyles.AssumeUniversal, - out DateTimeOffset parsed - )) - { - createdAt = parsed; - } - - return new LocalFileVersionInfo( - VersionId: fileName, - VersionPath: path, - CreatedAt: createdAt, - SizeBytes: fi.Length, - IsCompressed: path.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) - ); - }) - .OrderByDescending(v => v.CreatedAt) - .ToList(); - } - - public static async Task RestoreLocalVersionAsync(string targetFilePath, string versionFilePath) - { - await CreateAutomaticBackupAsync(targetFilePath); - byte[] bytes = await File.ReadAllBytesAsync(versionFilePath); - - string? parentDir = Path.GetDirectoryName(targetFilePath); - if (!string.IsNullOrWhiteSpace(parentDir)) - Directory.CreateDirectory(parentDir); - - await File.WriteAllBytesAsync(targetFilePath, bytes); - } - - private static bool IsGZipPayload(ReadOnlySpan bytes) => - bytes.Length >= 2 && bytes[0] == 0x1F && bytes[1] == 0x8B; - - private static string DecodeCanvasJson(byte[] bytes) - { - if (!IsGZipPayload(bytes)) - return Encoding.UTF8.GetString(bytes); - - using var input = new MemoryStream(bytes); - using var gzip = new GZipStream(input, CompressionMode.Decompress); - using var output = new MemoryStream(); - gzip.CopyTo(output); - return Encoding.UTF8.GetString(output.ToArray()); - } - - private static byte[] CompressBytes(byte[] utf8) - { - using var output = new MemoryStream(); - using (var gzip = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true)) - gzip.Write(utf8, 0, utf8.Length); - return output.ToArray(); - } - - private static async Task CreateAutomaticBackupAsync(string targetFilePath) - { - if (!File.Exists(targetFilePath)) - return; - - string backupDir = GetBackupDirectory(targetFilePath); - Directory.CreateDirectory(backupDir); - - string stamp = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff"); - string backupPath = Path.Combine( - backupDir, - $"{stamp}_{Path.GetFileName(targetFilePath)}.bak" - ); - - await using (FileStream src = File.OpenRead(targetFilePath)) - await using (FileStream dst = File.Create(backupPath)) - await src.CopyToAsync(dst); - - PruneOldFiles(backupDir, MaxAutomaticBackups); - } - - private static async Task AddLocalFileVersionAsync(string targetFilePath, byte[] payload) - { - string historyDir = GetHistoryDirectory(targetFilePath); - Directory.CreateDirectory(historyDir); - - string stamp = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff"); - bool compressed = IsGZipPayload(payload); - string ext = compressed ? ".vsaq.gz" : ".vsaq"; - string versionPath = Path.Combine(historyDir, $"{stamp}_{Path.GetFileNameWithoutExtension(targetFilePath)}{ext}"); - - await File.WriteAllBytesAsync(versionPath, payload); - PruneOldFiles(historyDir, MaxLocalFileVersions); - } - - private static void PruneOldFiles(string dir, int keep) - { - FileInfo[] files = new DirectoryInfo(dir) - .EnumerateFiles() - .OrderByDescending(f => f.LastWriteTimeUtc) - .ToArray(); - - foreach (FileInfo stale in files.Skip(keep)) - { - try - { - stale.Delete(); - } - catch - { - // Best-effort cleanup. - } - } - } - - private static string GetHistoryDirectory(string targetFilePath) - { - string parent = Path.GetDirectoryName(targetFilePath) ?? "."; - string baseName = Path.GetFileNameWithoutExtension(targetFilePath); - return Path.Combine(parent, ".vsaq_history", baseName); - } - - private static string GetBackupDirectory(string targetFilePath) - { - string parent = Path.GetDirectoryName(targetFilePath) ?? "."; - return Path.Combine(parent, ".vsaq_backups"); - } -} + + public static IReadOnlyList GetLocalFileVersions(string targetFilePath) + { + string historyDir = GetHistoryDirectory(targetFilePath); + if (!Directory.Exists(historyDir)) + return []; + + return Directory + .EnumerateFiles(historyDir, "*.vsaq*") + .Select(path => + { + var fi = new FileInfo(path); + DateTimeOffset createdAt = fi.LastWriteTimeUtc; + string fileName = Path.GetFileNameWithoutExtension(path); + int sep = fileName.IndexOf('_'); + if (sep > 0 + && DateTimeOffset.TryParseExact( + fileName[..sep], + "yyyyMMddHHmmssfff", + null, + System.Globalization.DateTimeStyles.AssumeUniversal, + out DateTimeOffset parsed + )) + { + createdAt = parsed; + } + + return new LocalFileVersionInfo( + VersionId: fileName, + VersionPath: path, + CreatedAt: createdAt, + SizeBytes: fi.Length, + IsCompressed: path.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) + ); + }) + .OrderByDescending(v => v.CreatedAt) + .ToList(); + } + + public static async Task RestoreLocalVersionAsync(string targetFilePath, string versionFilePath) + { + await CreateAutomaticBackupAsync(targetFilePath); + byte[] bytes = await File.ReadAllBytesAsync(versionFilePath); + + string? parentDir = Path.GetDirectoryName(targetFilePath); + if (!string.IsNullOrWhiteSpace(parentDir)) + Directory.CreateDirectory(parentDir); + + await File.WriteAllBytesAsync(targetFilePath, bytes); + } + + private static bool IsGZipPayload(ReadOnlySpan bytes) => + bytes.Length >= 2 && bytes[0] == 0x1F && bytes[1] == 0x8B; + + private static string DecodeCanvasJson(byte[] bytes) + { + if (!IsGZipPayload(bytes)) + return Encoding.UTF8.GetString(bytes); + + using var input = new MemoryStream(bytes); + using var gzip = new GZipStream(input, CompressionMode.Decompress); + using var output = new MemoryStream(); + gzip.CopyTo(output); + return Encoding.UTF8.GetString(output.ToArray()); + } + + private static byte[] CompressBytes(byte[] utf8) + { + using var output = new MemoryStream(); + using (var gzip = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true)) + gzip.Write(utf8, 0, utf8.Length); + return output.ToArray(); + } + + private static async Task CreateAutomaticBackupAsync(string targetFilePath) + { + if (!File.Exists(targetFilePath)) + return; + + string backupDir = GetBackupDirectory(targetFilePath); + Directory.CreateDirectory(backupDir); + + string stamp = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff"); + string backupPath = Path.Combine( + backupDir, + $"{stamp}_{Path.GetFileName(targetFilePath)}.bak" + ); + + await using (FileStream src = File.OpenRead(targetFilePath)) + await using (FileStream dst = File.Create(backupPath)) + await src.CopyToAsync(dst); + + PruneOldFiles(backupDir, MaxAutomaticBackups); + } + + private static async Task AddLocalFileVersionAsync(string targetFilePath, byte[] payload) + { + string historyDir = GetHistoryDirectory(targetFilePath); + Directory.CreateDirectory(historyDir); + + string stamp = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff"); + bool compressed = IsGZipPayload(payload); + string ext = compressed ? ".vsaq.gz" : ".vsaq"; + string versionPath = Path.Combine(historyDir, $"{stamp}_{Path.GetFileNameWithoutExtension(targetFilePath)}{ext}"); + + await File.WriteAllBytesAsync(versionPath, payload); + PruneOldFiles(historyDir, MaxLocalFileVersions); + } + + private static void PruneOldFiles(string dir, int keep) + { + FileInfo[] files = new DirectoryInfo(dir) + .EnumerateFiles() + .OrderByDescending(f => f.LastWriteTimeUtc) + .ToArray(); + + foreach (FileInfo stale in files.Skip(keep)) + { + try + { + stale.Delete(); + } + catch + { + // Best-effort cleanup. + } + } + } + + private static string GetHistoryDirectory(string targetFilePath) + { + string parent = Path.GetDirectoryName(targetFilePath) ?? "."; + string baseName = Path.GetFileNameWithoutExtension(targetFilePath); + return Path.Combine(parent, ".vsaq_history", baseName); + } + + private static string GetBackupDirectory(string targetFilePath) + { + string parent = Path.GetDirectoryName(targetFilePath) ?? "."; + return Path.Combine(parent, ".vsaq_backups"); + } +} diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Subgraphs.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Subgraphs.cs similarity index 96% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Subgraphs.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Subgraphs.cs index a20caa69..b05eb003 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.Subgraphs.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.Subgraphs.cs @@ -1,207 +1,207 @@ -using System.Text.Json; -using DBWeaver.Nodes; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Serialization; - -public static partial class CanvasSerializer -{ - private static SavedViewSubgraph? BuildViewSubgraph( - NodeViewModel node, - Dictionary parameters - ) - { - if (node.Type != NodeType.ViewDefinition) - return null; - - parameters.TryGetValue(ViewSubgraphParameterKey, out string? payload); - parameters.TryGetValue(ViewEditorCanvasParameterKey, out string? editorCanvasJson); - - bool hasCompiledGraph = !string.IsNullOrWhiteSpace(payload); - bool hasEditorCanvas = !string.IsNullOrWhiteSpace(editorCanvasJson); - if (!hasCompiledGraph && !hasEditorCanvas) - return null; - - // Keep malformed payloads in Parameters for diagnostics/fallback compatibility. - if (hasCompiledGraph) - { - try - { - using JsonDocument _ = JsonDocument.Parse(payload!); - } - catch - { - if (!hasEditorCanvas) - return null; - - payload = null; - } - } - - parameters.Remove(ViewSubgraphParameterKey); - parameters.Remove(ViewEditorCanvasParameterKey); - parameters.TryGetValue(ViewFromTableParameterKey, out string? fromTable); - return new SavedViewSubgraph( - string.IsNullOrWhiteSpace(payload) ? null : payload, - string.IsNullOrWhiteSpace(fromTable) ? null : fromTable.Trim(), - string.IsNullOrWhiteSpace(editorCanvasJson) ? null : editorCanvasJson); - } - - private static SavedCteSubgraph? BuildCteSubgraph( - NodeViewModel node, - IEnumerable allNodes, - IEnumerable allConnections - ) - { - if (node.Type != NodeType.CteDefinition) - return null; - - if (node.Parameters.TryGetValue(CteSubgraphParameterKey, out string? payload) - && !string.IsNullOrWhiteSpace(payload)) - { - try - { - SavedCteSubgraph? persisted = JsonSerializer.Deserialize(payload); - if (persisted is not null) - return persisted; - } - catch - { - // Fall back to graph extraction when payload is malformed. - } - } - - ConnectionViewModel? queryWire = allConnections.FirstOrDefault(c => - c.ToPin?.Owner == node - && c.ToPin.Name == "query" - && c.FromPin.Owner.Type == NodeType.ResultOutput - ); - if (queryWire?.FromPin.Owner is not NodeViewModel resultOutput) - return null; - - HashSet upstream = CollectUpstreamNodeIds(resultOutput, allConnections, includeCteDefinitions: false); - - var scopedNodes = allNodes.Where(n => upstream.Contains(n.Id)).ToList(); - var scopedIds = scopedNodes.Select(n => n.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); - var scopedConnections = allConnections.Where(c => - c.ToPin is not null - && scopedIds.Contains(c.FromPin.Owner.Id) - && scopedIds.Contains(c.ToPin!.Owner.Id) - ); - - return new SavedCteSubgraph( - Nodes: [.. scopedNodes.Select(n => SerialiseNode(n, scopedNodes, scopedConnections, includeCteSubgraph: false))], - Connections: [.. scopedConnections - .Select(SerialiseConnection) - .Where(c => c is not null) - .Select(c => c!)], - ResultOutputNodeId: resultOutput.Id - ); - } - - private static HashSet CollectUpstreamNodeIds( - NodeViewModel sinkNode, - IEnumerable allConnections, - bool includeCteDefinitions - ) - { - var visited = new HashSet(StringComparer.OrdinalIgnoreCase) { sinkNode.Id }; - var queue = new Queue(); - queue.Enqueue(sinkNode.Id); - - while (queue.Count > 0) - { - string current = queue.Dequeue(); - foreach (ConnectionViewModel conn in allConnections.Where(c => c.ToPin?.Owner.Id == current)) - { - NodeViewModel fromOwner = conn.FromPin.Owner; - if (!includeCteDefinitions && fromOwner.Type == NodeType.CteDefinition) - continue; - - if (visited.Add(fromOwner.Id)) - queue.Enqueue(fromOwner.Id); - } - } - - return visited; - } - - private static void MaterializeCteSubgraphs( - SavedCanvas saved, - CanvasViewModel vm, - Dictionary nodeMap, - List warnings, - IReadOnlyDictionary>? columnLookup - ) - { - foreach (SavedNode cteNode in saved.Nodes.Where(n => n.CteSubgraph is not null)) - { - if (!nodeMap.TryGetValue(cteNode.NodeId, out NodeViewModel? cteVm)) - continue; - - if (cteVm.Type != NodeType.CteDefinition) - continue; - - bool hasQueryWire = vm.Connections.Any(c => - c.ToPin?.Owner == cteVm - && c.ToPin.Name == "query" - && c.FromPin.Owner.Type == NodeType.ResultOutput - ); - if (hasQueryWire) - continue; - - SavedCteSubgraph subgraph = cteNode.CteSubgraph!; - if (subgraph.Nodes.Count == 0) - continue; - - var localIdMap = new Dictionary(StringComparer.Ordinal); - foreach (SavedNode subNode in subgraph.Nodes) - { - (NodeViewModel? subVm, string? skipReason) = BuildNodeVm(subNode, columnLookup); - if (subVm is null) - { - warnings.Add($"Skipped CTE subgraph node '{subNode.NodeType}' for '{cteNode.NodeId}': {skipReason ?? "Unknown error"}."); - continue; - } - - while (nodeMap.ContainsKey(subVm.Id)) - subVm.Id = Guid.NewGuid().ToString("N")[..8]; - - nodeMap[subVm.Id] = subVm; - localIdMap[subNode.NodeId] = subVm; - vm.Nodes.Add(subVm); - } - - foreach (SavedConnection sc in subgraph.Connections) - { - if (!localIdMap.TryGetValue(sc.FromNodeId, out NodeViewModel? fromNode)) - continue; - if (!localIdMap.TryGetValue(sc.ToNodeId, out NodeViewModel? toNode)) - continue; - - if (!TryResolvePins(fromNode, sc.FromPinName, toNode, sc.ToPinName, out PinViewModel? fromPin, out PinViewModel? toPin)) - continue; - - TryConnect(vm.Connections, fromPin!, toPin!); - } - - NodeViewModel? resultVm = null; - if (!string.IsNullOrWhiteSpace(subgraph.ResultOutputNodeId)) - localIdMap.TryGetValue(subgraph.ResultOutputNodeId, out resultVm); - - resultVm ??= localIdMap.Values.FirstOrDefault(n => n.Type == NodeType.ResultOutput); - if (resultVm is null) - continue; - - PinViewModel? resultPin = resultVm.OutputPins.FirstOrDefault(p => p.Name == "result"); - PinViewModel? queryPin = cteVm.InputPins.FirstOrDefault(p => p.Name == "query"); - if (resultPin is null || queryPin is null) - continue; - - if (!TryConnect(vm.Connections, resultPin, queryPin)) - continue; - - warnings.Add($"Materialized persisted CTE subgraph for node '{cteVm.Id}'."); - } - } -} +using System.Text.Json; +using AkkornStudio.Nodes; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Serialization; + +public static partial class CanvasSerializer +{ + private static SavedViewSubgraph? BuildViewSubgraph( + NodeViewModel node, + Dictionary parameters + ) + { + if (node.Type != NodeType.ViewDefinition) + return null; + + parameters.TryGetValue(ViewSubgraphParameterKey, out string? payload); + parameters.TryGetValue(ViewEditorCanvasParameterKey, out string? editorCanvasJson); + + bool hasCompiledGraph = !string.IsNullOrWhiteSpace(payload); + bool hasEditorCanvas = !string.IsNullOrWhiteSpace(editorCanvasJson); + if (!hasCompiledGraph && !hasEditorCanvas) + return null; + + // Keep malformed payloads in Parameters for diagnostics/fallback compatibility. + if (hasCompiledGraph) + { + try + { + using JsonDocument _ = JsonDocument.Parse(payload!); + } + catch + { + if (!hasEditorCanvas) + return null; + + payload = null; + } + } + + parameters.Remove(ViewSubgraphParameterKey); + parameters.Remove(ViewEditorCanvasParameterKey); + parameters.TryGetValue(ViewFromTableParameterKey, out string? fromTable); + return new SavedViewSubgraph( + string.IsNullOrWhiteSpace(payload) ? null : payload, + string.IsNullOrWhiteSpace(fromTable) ? null : fromTable.Trim(), + string.IsNullOrWhiteSpace(editorCanvasJson) ? null : editorCanvasJson); + } + + private static SavedCteSubgraph? BuildCteSubgraph( + NodeViewModel node, + IEnumerable allNodes, + IEnumerable allConnections + ) + { + if (node.Type != NodeType.CteDefinition) + return null; + + if (node.Parameters.TryGetValue(CteSubgraphParameterKey, out string? payload) + && !string.IsNullOrWhiteSpace(payload)) + { + try + { + SavedCteSubgraph? persisted = JsonSerializer.Deserialize(payload); + if (persisted is not null) + return persisted; + } + catch + { + // Fall back to graph extraction when payload is malformed. + } + } + + ConnectionViewModel? queryWire = allConnections.FirstOrDefault(c => + c.ToPin?.Owner == node + && c.ToPin.Name == "query" + && c.FromPin.Owner.Type == NodeType.ResultOutput + ); + if (queryWire?.FromPin.Owner is not NodeViewModel resultOutput) + return null; + + HashSet upstream = CollectUpstreamNodeIds(resultOutput, allConnections, includeCteDefinitions: false); + + var scopedNodes = allNodes.Where(n => upstream.Contains(n.Id)).ToList(); + var scopedIds = scopedNodes.Select(n => n.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); + var scopedConnections = allConnections.Where(c => + c.ToPin is not null + && scopedIds.Contains(c.FromPin.Owner.Id) + && scopedIds.Contains(c.ToPin!.Owner.Id) + ); + + return new SavedCteSubgraph( + Nodes: [.. scopedNodes.Select(n => SerialiseNode(n, scopedNodes, scopedConnections, includeCteSubgraph: false))], + Connections: [.. scopedConnections + .Select(SerialiseConnection) + .Where(c => c is not null) + .Select(c => c!)], + ResultOutputNodeId: resultOutput.Id + ); + } + + private static HashSet CollectUpstreamNodeIds( + NodeViewModel sinkNode, + IEnumerable allConnections, + bool includeCteDefinitions + ) + { + var visited = new HashSet(StringComparer.OrdinalIgnoreCase) { sinkNode.Id }; + var queue = new Queue(); + queue.Enqueue(sinkNode.Id); + + while (queue.Count > 0) + { + string current = queue.Dequeue(); + foreach (ConnectionViewModel conn in allConnections.Where(c => c.ToPin?.Owner.Id == current)) + { + NodeViewModel fromOwner = conn.FromPin.Owner; + if (!includeCteDefinitions && fromOwner.Type == NodeType.CteDefinition) + continue; + + if (visited.Add(fromOwner.Id)) + queue.Enqueue(fromOwner.Id); + } + } + + return visited; + } + + private static void MaterializeCteSubgraphs( + SavedCanvas saved, + CanvasViewModel vm, + Dictionary nodeMap, + List warnings, + IReadOnlyDictionary>? columnLookup + ) + { + foreach (SavedNode cteNode in saved.Nodes.Where(n => n.CteSubgraph is not null)) + { + if (!nodeMap.TryGetValue(cteNode.NodeId, out NodeViewModel? cteVm)) + continue; + + if (cteVm.Type != NodeType.CteDefinition) + continue; + + bool hasQueryWire = vm.Connections.Any(c => + c.ToPin?.Owner == cteVm + && c.ToPin.Name == "query" + && c.FromPin.Owner.Type == NodeType.ResultOutput + ); + if (hasQueryWire) + continue; + + SavedCteSubgraph subgraph = cteNode.CteSubgraph!; + if (subgraph.Nodes.Count == 0) + continue; + + var localIdMap = new Dictionary(StringComparer.Ordinal); + foreach (SavedNode subNode in subgraph.Nodes) + { + (NodeViewModel? subVm, string? skipReason) = BuildNodeVm(subNode, columnLookup); + if (subVm is null) + { + warnings.Add($"Skipped CTE subgraph node '{subNode.NodeType}' for '{cteNode.NodeId}': {skipReason ?? "Unknown error"}."); + continue; + } + + while (nodeMap.ContainsKey(subVm.Id)) + subVm.Id = Guid.NewGuid().ToString("N")[..8]; + + nodeMap[subVm.Id] = subVm; + localIdMap[subNode.NodeId] = subVm; + vm.Nodes.Add(subVm); + } + + foreach (SavedConnection sc in subgraph.Connections) + { + if (!localIdMap.TryGetValue(sc.FromNodeId, out NodeViewModel? fromNode)) + continue; + if (!localIdMap.TryGetValue(sc.ToNodeId, out NodeViewModel? toNode)) + continue; + + if (!TryResolvePins(fromNode, sc.FromPinName, toNode, sc.ToPinName, out PinViewModel? fromPin, out PinViewModel? toPin)) + continue; + + TryConnect(vm.Connections, fromPin!, toPin!); + } + + NodeViewModel? resultVm = null; + if (!string.IsNullOrWhiteSpace(subgraph.ResultOutputNodeId)) + localIdMap.TryGetValue(subgraph.ResultOutputNodeId, out resultVm); + + resultVm ??= localIdMap.Values.FirstOrDefault(n => n.Type == NodeType.ResultOutput); + if (resultVm is null) + continue; + + PinViewModel? resultPin = resultVm.OutputPins.FirstOrDefault(p => p.Name == "result"); + PinViewModel? queryPin = cteVm.InputPins.FirstOrDefault(p => p.Name == "query"); + if (resultPin is null || queryPin is null) + continue; + + if (!TryConnect(vm.Connections, resultPin, queryPin)) + continue; + + warnings.Add($"Materialized persisted CTE subgraph for node '{cteVm.Id}'."); + } + } +} diff --git a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.cs b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.cs similarity index 90% rename from src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.cs rename to src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.cs index 5a243647..7bc8892e 100644 --- a/src/DBWeaver.UI/Serialization/Canvas/CanvasSerializer.cs +++ b/src/AkkornStudio.UI/Serialization/Canvas/CanvasSerializer.cs @@ -1,41 +1,41 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Avalonia; -using DBWeaver.Core; -using DBWeaver.Nodes; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Serialization; - -public static partial class CanvasSerializer -{ - private enum CanvasNodeFamily - { - Any, - Query, - Ddl, - } - - /// Current workspace schema version written by this build. +using System.Text.Json; +using System.Text.Json.Serialization; +using Avalonia; +using AkkornStudio.Core; +using AkkornStudio.Nodes; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Serialization; + +public static partial class CanvasSerializer +{ + private enum CanvasNodeFamily + { + Any, + Query, + Ddl, + } + + /// Current workspace schema version written by this build. public const int CurrentSchemaVersion = 5; public const int CurrentCanvasSchemaVersion = 3; public const string CteSubgraphParameterKey = "__cteSubgraphJson"; public const string SubquerySubgraphParameterKey = "__subquerySubgraphJson"; public const string SubqueryInputBridgeNodeId = "__subquery_outer_inputs_bridge"; public const string ViewSubgraphParameterKey = "ViewSubgraphGraphJson"; - public const string ViewEditorCanvasParameterKey = "__viewSubgraphCanvasJson"; - public const string ViewFromTableParameterKey = "ViewFromTable"; - - /// Semantic version of the application (bumped per release). - public const string AppVersion = AppConstants.AppVersion; - public const int CompressionThresholdBytes = 64 * 1024; - public const int MaxLocalFileVersions = 30; - public const int MaxAutomaticBackups = 20; - - private static readonly JsonSerializerOptions _opts = new() - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }; - -} + public const string ViewEditorCanvasParameterKey = "__viewSubgraphCanvasJson"; + public const string ViewFromTableParameterKey = "ViewFromTable"; + + /// Semantic version of the application (bumped per release). + public const string AppVersion = AppConstants.AppVersion; + public const int CompressionThresholdBytes = 64 * 1024; + public const int MaxLocalFileVersions = 30; + public const int MaxAutomaticBackups = 20; + + private static readonly JsonSerializerOptions _opts = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + +} diff --git a/src/DBWeaver.UI/Serialization/FlowVersions/FlowVersionStore.cs b/src/AkkornStudio.UI/Serialization/FlowVersions/FlowVersionStore.cs similarity index 66% rename from src/DBWeaver.UI/Serialization/FlowVersions/FlowVersionStore.cs rename to src/AkkornStudio.UI/Serialization/FlowVersions/FlowVersionStore.cs index 39ac4020..5706c0fc 100644 --- a/src/DBWeaver.UI/Serialization/FlowVersions/FlowVersionStore.cs +++ b/src/AkkornStudio.UI/Serialization/FlowVersions/FlowVersionStore.cs @@ -1,132 +1,175 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using DBWeaver.UI.Services.Localization; - -namespace DBWeaver.UI.Serialization; - -// ═════════════════════════════════════════════════════════════════════════════ -// MODEL -// ═════════════════════════════════════════════════════════════════════════════ - -/// -/// A single versioned snapshot of the canvas state. -/// -public record FlowVersion( - string Id, - string Label, - string CreatedAt, - int NodeCount, - int ConnectionCount, - /// Full serialized canvas JSON (SavedCanvas). - string CanvasJson -); - -// ═════════════════════════════════════════════════════════════════════════════ -// STORE -// ═════════════════════════════════════════════════════════════════════════════ - -/// -/// Persists flow version checkpoints to -/// ~/.config/DBWeaver/flow_versions.json (Linux) or -/// %APPDATA%\DBWeaver\flow_versions.json (Windows). -/// -/// Automatically caps history at entries to avoid -/// unbounded growth — oldest entries are pruned when the cap is reached. -/// -public static class FlowVersionStore -{ - public const int MaxVersions = 50; - - public static event Action? WarningRaised; - private static readonly ILogger _logger = NullLogger.Instance; - - private static readonly JsonSerializerOptions _opts = new() - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }; - - private static string StorePath() - { - string dir = global::DBWeaver.UI.AppConstants.AppDataDirectory; - Directory.CreateDirectory(dir); - return Path.Combine(dir, "flow_versions.json"); - } - - /// Loads all versions from disk (newest first). Returns empty list on first run. - public static List Load() - { - string path = StorePath(); - if (!File.Exists(path)) - return []; - try - { - string json = File.ReadAllText(path); - return JsonSerializer.Deserialize>(json, _opts) ?? []; - } - catch (Exception ex) - { - RaiseWarning( - string.Format( - L("flowVersionStore.warning.loadFailed", "Failed to load flow versions from '{0}': {1}"), - path, - ex.Message - ) - ); - return []; - } - } - - /// Overwrites the store with the given list. - public static void Save(List versions) - { - try - { - File.WriteAllText(StorePath(), JsonSerializer.Serialize(versions, _opts)); - } - catch (Exception ex) - { - RaiseWarning( - string.Format( - L("flowVersionStore.warning.saveFailed", "Failed to save flow versions: {0}"), - ex.Message - ) - ); - } - } - - private static void RaiseWarning(string message) - { - _logger.LogWarning("[FlowVersionStore] {Message}", message); - WarningRaised?.Invoke(message); - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } - - /// - /// Prepends a new version to the history and persists. - /// Prunes oldest entries beyond . - /// - public static void Add(FlowVersion version) - { - List all = Load(); - all.Insert(0, version); - if (all.Count > MaxVersions) - all.RemoveRange(MaxVersions, all.Count - MaxVersions); - Save(all); - } - - /// Removes a version by ID. - public static void Remove(string id) - { - List all = Load(); - all.RemoveAll(v => v.Id == id); - Save(all); - } -} +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using AkkornStudio.UI.Services.Localization; + +namespace AkkornStudio.UI.Serialization; + +// ═════════════════════════════════════════════════════════════════════════════ +// MODEL +// ═════════════════════════════════════════════════════════════════════════════ + +/// +/// A single versioned snapshot of the canvas state. +/// +public record FlowVersion( + string Id, + string Label, + string CreatedAt, + int NodeCount, + int ConnectionCount, + /// Full serialized canvas JSON (SavedCanvas). + string CanvasJson, + string ProjectKey = FlowVersionStore.DefaultProjectKey +); + +// ═════════════════════════════════════════════════════════════════════════════ +// STORE +// ═════════════════════════════════════════════════════════════════════════════ + +/// +/// Persists flow version checkpoints to +/// ~/.config/AkkornStudio/flow_versions.json (Linux) or +/// %APPDATA%\AkkornStudio\flow_versions.json (Windows). +/// +/// Automatically caps history at entries to avoid +/// unbounded growth — oldest entries are pruned when the cap is reached. +/// +public static class FlowVersionStore +{ + public const int MaxVersions = 50; + public const string DefaultProjectKey = "global"; + + public static event Action? WarningRaised; + private static readonly ILogger _logger = NullLogger.Instance; + + private static readonly JsonSerializerOptions _opts = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + private static string StorePath() + { + string dir = global::AkkornStudio.UI.AppConstants.AppDataDirectory; + Directory.CreateDirectory(dir); + return Path.Combine(dir, "flow_versions.json"); + } + + /// Loads all versions from disk (newest first). Returns empty list on first run. + public static List Load() + { + string path = StorePath(); + if (!File.Exists(path)) + return []; + try + { + string json = File.ReadAllText(path); + return JsonSerializer.Deserialize>(json, _opts) ?? []; + } + catch (Exception ex) + { + RaiseWarning( + string.Format( + L("flowVersionStore.warning.loadFailed", "Failed to load flow versions from '{0}': {1}"), + path, + ex.Message + ) + ); + return []; + } + } + + public static List Load(string projectKey) + { + string normalizedProjectKey = NormalizeProjectKey(projectKey); + return Load() + .Where(version => string.Equals(NormalizeProjectKey(version.ProjectKey), normalizedProjectKey, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + /// Overwrites the store with the given list. + public static void Save(List versions) + { + try + { + File.WriteAllText(StorePath(), JsonSerializer.Serialize(versions, _opts)); + } + catch (Exception ex) + { + RaiseWarning( + string.Format( + L("flowVersionStore.warning.saveFailed", "Failed to save flow versions: {0}"), + ex.Message + ) + ); + } + } + + private static void RaiseWarning(string message) + { + _logger.LogWarning("[FlowVersionStore] {Message}", message); + WarningRaised?.Invoke(message); + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } + + /// + /// Prepends a new version to the history and persists. + /// Prunes oldest entries beyond . + /// + public static void Add(FlowVersion version) + { + Add(version, version.ProjectKey); + } + + public static void Add(FlowVersion version, string projectKey) + { + string normalizedProjectKey = NormalizeProjectKey(projectKey); + List all = Load(); + FlowVersion scopedVersion = version with { ProjectKey = normalizedProjectKey }; + all.RemoveAll(v => v.Id == scopedVersion.Id); + all.Insert(0, scopedVersion); + + HashSet pruneIds = all + .Where(v => string.Equals(NormalizeProjectKey(v.ProjectKey), normalizedProjectKey, StringComparison.OrdinalIgnoreCase)) + .Skip(MaxVersions) + .Select(v => v.Id) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + if (pruneIds.Count > 0) + all.RemoveAll(v => pruneIds.Contains(v.Id)); + + Save(all); + } + + /// Removes a version by ID. + public static void Remove(string id) + { + List all = Load(); + all.RemoveAll(v => v.Id == id); + Save(all); + } + + public static void Remove(string id, string projectKey) + { + string normalizedProjectKey = NormalizeProjectKey(projectKey); + List all = Load(); + all.RemoveAll(v => + v.Id == id + && string.Equals(NormalizeProjectKey(v.ProjectKey), normalizedProjectKey, StringComparison.OrdinalIgnoreCase)); + Save(all); + } + + public static string NormalizeProjectKey(string? projectKey) + { + if (string.IsNullOrWhiteSpace(projectKey)) + return DefaultProjectKey; + + return string.Join(" ", projectKey.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).Trim(); + } +} diff --git a/src/DBWeaver.UI/Serialization/Snippets/SnippetStore.cs b/src/AkkornStudio.UI/Serialization/Snippets/SnippetStore.cs similarity index 91% rename from src/DBWeaver.UI/Serialization/Snippets/SnippetStore.cs rename to src/AkkornStudio.UI/Serialization/Snippets/SnippetStore.cs index 119711f6..6063fe45 100644 --- a/src/DBWeaver.UI/Serialization/Snippets/SnippetStore.cs +++ b/src/AkkornStudio.UI/Serialization/Snippets/SnippetStore.cs @@ -1,123 +1,123 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using DBWeaver.UI.Services.Localization; - -namespace DBWeaver.UI.Serialization; - -// ═════════════════════════════════════════════════════════════════════════════ -// SNIPPET MODEL -// ═════════════════════════════════════════════════════════════════════════════ - -/// -/// A saved subgraph snippet — a named, reusable group of nodes and their -/// internal connections that can be inserted into any canvas. -/// -public record SavedSnippet( - string Id, - string Name, - string? Tags, - string? Description, - string CreatedAt, - List Nodes, - List Connections -); - -// ═════════════════════════════════════════════════════════════════════════════ -// SNIPPET STORE (JSON persistence in the user's app data folder) -// ═════════════════════════════════════════════════════════════════════════════ - -/// -/// Persists snippets to a JSON file in the OS-appropriate application data -/// directory (~/.config/DBWeaver/snippets.json on Linux, -/// %APPDATA%\DBWeaver\snippets.json on Windows). -/// -public static class SnippetStore -{ - public static event Action? WarningRaised; - private static readonly ILogger _logger = NullLogger.Instance; - - private static readonly JsonSerializerOptions _opts = new() - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - }; - - private static string StoreFilePath() - { - string dir = global::DBWeaver.UI.AppConstants.AppDataDirectory; - Directory.CreateDirectory(dir); - return Path.Combine(dir, "snippets.json"); - } - - /// Loads all snippets from disk. Returns an empty list on first run or error. - public static List Load() - { - string path = StoreFilePath(); - if (!File.Exists(path)) - return []; - try - { - string json = File.ReadAllText(path); - return JsonSerializer.Deserialize>(json, _opts) ?? []; - } - catch (Exception ex) - { - RaiseWarning( - string.Format( - L("snippetStore.warning.loadFailed", "Failed to load snippets from '{0}': {1}"), - path, - ex.Message - ) - ); - return []; - } - } - - /// Overwrites the snippet store with the given list. - public static void Save(List snippets) - { - try - { - File.WriteAllText(StoreFilePath(), JsonSerializer.Serialize(snippets, _opts)); - } - catch (Exception ex) - { - RaiseWarning( - string.Format( - L("snippetStore.warning.saveFailed", "Failed to save snippets: {0}"), - ex.Message - ) - ); - } - } - - private static void RaiseWarning(string message) - { - _logger.LogWarning("[SnippetStore] {Message}", message); - WarningRaised?.Invoke(message); - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } - - /// Appends a new snippet and persists to disk. - public static void Add(SavedSnippet snippet) - { - List all = Load(); - all.Add(snippet); - Save(all); - } - - /// Removes a snippet by ID and persists to disk. - public static void Remove(string id) - { - List all = Load(); - all.RemoveAll(s => s.Id == id); - Save(all); - } -} +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using AkkornStudio.UI.Services.Localization; + +namespace AkkornStudio.UI.Serialization; + +// ═════════════════════════════════════════════════════════════════════════════ +// SNIPPET MODEL +// ═════════════════════════════════════════════════════════════════════════════ + +/// +/// A saved subgraph snippet — a named, reusable group of nodes and their +/// internal connections that can be inserted into any canvas. +/// +public record SavedSnippet( + string Id, + string Name, + string? Tags, + string? Description, + string CreatedAt, + List Nodes, + List Connections +); + +// ═════════════════════════════════════════════════════════════════════════════ +// SNIPPET STORE (JSON persistence in the user's app data folder) +// ═════════════════════════════════════════════════════════════════════════════ + +/// +/// Persists snippets to a JSON file in the OS-appropriate application data +/// directory (~/.config/AkkornStudio/snippets.json on Linux, +/// %APPDATA%\AkkornStudio\snippets.json on Windows). +/// +public static class SnippetStore +{ + public static event Action? WarningRaised; + private static readonly ILogger _logger = NullLogger.Instance; + + private static readonly JsonSerializerOptions _opts = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + private static string StoreFilePath() + { + string dir = global::AkkornStudio.UI.AppConstants.AppDataDirectory; + Directory.CreateDirectory(dir); + return Path.Combine(dir, "snippets.json"); + } + + /// Loads all snippets from disk. Returns an empty list on first run or error. + public static List Load() + { + string path = StoreFilePath(); + if (!File.Exists(path)) + return []; + try + { + string json = File.ReadAllText(path); + return JsonSerializer.Deserialize>(json, _opts) ?? []; + } + catch (Exception ex) + { + RaiseWarning( + string.Format( + L("snippetStore.warning.loadFailed", "Failed to load snippets from '{0}': {1}"), + path, + ex.Message + ) + ); + return []; + } + } + + /// Overwrites the snippet store with the given list. + public static void Save(List snippets) + { + try + { + File.WriteAllText(StoreFilePath(), JsonSerializer.Serialize(snippets, _opts)); + } + catch (Exception ex) + { + RaiseWarning( + string.Format( + L("snippetStore.warning.saveFailed", "Failed to save snippets: {0}"), + ex.Message + ) + ); + } + } + + private static void RaiseWarning(string message) + { + _logger.LogWarning("[SnippetStore] {Message}", message); + WarningRaised?.Invoke(message); + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } + + /// Appends a new snippet and persists to disk. + public static void Add(SavedSnippet snippet) + { + List all = Load(); + all.Add(snippet); + Save(all); + } + + /// Removes a snippet by ID and persists to disk. + public static void Remove(string id) + { + List all = Load(); + all.RemoveAll(s => s.Id == id); + Save(all); + } +} diff --git a/src/DBWeaver.UI/Services/AppDiagnostics/Contracts/IAppDiagnosticsReportBuilder.cs b/src/AkkornStudio.UI/Services/AppDiagnostics/Contracts/IAppDiagnosticsReportBuilder.cs similarity index 57% rename from src/DBWeaver.UI/Services/AppDiagnostics/Contracts/IAppDiagnosticsReportBuilder.cs rename to src/AkkornStudio.UI/Services/AppDiagnostics/Contracts/IAppDiagnosticsReportBuilder.cs index 4b9ba41c..4f39fe7d 100644 --- a/src/DBWeaver.UI/Services/AppDiagnostics/Contracts/IAppDiagnosticsReportBuilder.cs +++ b/src/AkkornStudio.UI/Services/AppDiagnostics/Contracts/IAppDiagnosticsReportBuilder.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.Services.AppDiagnostics.Models; +using AkkornStudio.UI.Services.AppDiagnostics.Models; -namespace DBWeaver.UI.Services.AppDiagnostics.Contracts; +namespace AkkornStudio.UI.Services.AppDiagnostics.Contracts; public interface IAppDiagnosticsReportBuilder { diff --git a/src/DBWeaver.UI/Services/AppDiagnostics/Models/AppDiagnosticCategoryViewModel.cs b/src/AkkornStudio.UI/Services/AppDiagnostics/Models/AppDiagnosticCategoryViewModel.cs similarity index 95% rename from src/DBWeaver.UI/Services/AppDiagnostics/Models/AppDiagnosticCategoryViewModel.cs rename to src/AkkornStudio.UI/Services/AppDiagnostics/Models/AppDiagnosticCategoryViewModel.cs index a585d3a8..e0feb4ab 100644 --- a/src/DBWeaver.UI/Services/AppDiagnostics/Models/AppDiagnosticCategoryViewModel.cs +++ b/src/AkkornStudio.UI/Services/AppDiagnostics/Models/AppDiagnosticCategoryViewModel.cs @@ -1,8 +1,8 @@ using Material.Icons; using System.Collections.ObjectModel; -using DBWeaver.UI.ViewModels; +using AkkornStudio.UI.ViewModels; -namespace DBWeaver.UI.Services.AppDiagnostics.Models; +namespace AkkornStudio.UI.Services.AppDiagnostics.Models; public sealed class AppDiagnosticCategoryViewModel : ViewModelBase { diff --git a/src/DBWeaver.UI/Services/AppDiagnostics/Models/AppDiagnosticEntry.cs b/src/AkkornStudio.UI/Services/AppDiagnostics/Models/AppDiagnosticEntry.cs similarity index 94% rename from src/DBWeaver.UI/Services/AppDiagnostics/Models/AppDiagnosticEntry.cs rename to src/AkkornStudio.UI/Services/AppDiagnostics/Models/AppDiagnosticEntry.cs index b7ba2344..183c22ba 100644 --- a/src/DBWeaver.UI/Services/AppDiagnostics/Models/AppDiagnosticEntry.cs +++ b/src/AkkornStudio.UI/Services/AppDiagnostics/Models/AppDiagnosticEntry.cs @@ -1,8 +1,8 @@ using Material.Icons; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.Services.Theming; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.Services.Theming; -namespace DBWeaver.UI.Services.AppDiagnostics.Models; +namespace AkkornStudio.UI.Services.AppDiagnostics.Models; public sealed class AppDiagnosticEntry : ViewModelBase { diff --git a/src/DBWeaver.UI/Services/AppDiagnostics/Models/DiagnosticStatus.cs b/src/AkkornStudio.UI/Services/AppDiagnostics/Models/DiagnosticStatus.cs similarity index 53% rename from src/DBWeaver.UI/Services/AppDiagnostics/Models/DiagnosticStatus.cs rename to src/AkkornStudio.UI/Services/AppDiagnostics/Models/DiagnosticStatus.cs index 8a3dc62b..e92586ce 100644 --- a/src/DBWeaver.UI/Services/AppDiagnostics/Models/DiagnosticStatus.cs +++ b/src/AkkornStudio.UI/Services/AppDiagnostics/Models/DiagnosticStatus.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.AppDiagnostics.Models; +namespace AkkornStudio.UI.Services.AppDiagnostics.Models; public enum DiagnosticStatus { diff --git a/src/DBWeaver.UI/Services/AppDiagnostics/Presentation/AppDiagnosticsReportBuilder.cs b/src/AkkornStudio.UI/Services/AppDiagnostics/Presentation/AppDiagnosticsReportBuilder.cs similarity index 85% rename from src/DBWeaver.UI/Services/AppDiagnostics/Presentation/AppDiagnosticsReportBuilder.cs rename to src/AkkornStudio.UI/Services/AppDiagnostics/Presentation/AppDiagnosticsReportBuilder.cs index ee0470f7..f22fe3b6 100644 --- a/src/DBWeaver.UI/Services/AppDiagnostics/Presentation/AppDiagnosticsReportBuilder.cs +++ b/src/AkkornStudio.UI/Services/AppDiagnostics/Presentation/AppDiagnosticsReportBuilder.cs @@ -1,29 +1,29 @@ -using System.Text; -using DBWeaver.UI.Services.AppDiagnostics.Contracts; -using DBWeaver.UI.Services.AppDiagnostics.Models; -using DBWeaver.UI.Services.Localization; - -namespace DBWeaver.UI.Services.AppDiagnostics.Presentation; - -public sealed class AppDiagnosticsReportBuilder(ILocalizationService localization) : IAppDiagnosticsReportBuilder -{ - private readonly ILocalizationService _localization = localization; - - public string BuildReport(string overallLabel, IEnumerable categories) - { - var sb = new StringBuilder(); - sb.AppendLine(L("diagnostics.report.title", "DBWeaver - Diagnostic Report")); - sb.AppendLine($"{L("diagnostics.report.generated", "Generated")}: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); - sb.AppendLine($"{L("diagnostics.report.overall", "Overall")}: {overallLabel}"); - sb.AppendLine(new string('─', 50)); - - foreach (AppDiagnosticCategoryViewModel category in categories) - { - IReadOnlyList snapshot = category.SnapshotItems(); - if (snapshot.Count == 0) - continue; - - sb.AppendLine($"[{category.Title}]"); +using System.Text; +using AkkornStudio.UI.Services.AppDiagnostics.Contracts; +using AkkornStudio.UI.Services.AppDiagnostics.Models; +using AkkornStudio.UI.Services.Localization; + +namespace AkkornStudio.UI.Services.AppDiagnostics.Presentation; + +public sealed class AppDiagnosticsReportBuilder(ILocalizationService localization) : IAppDiagnosticsReportBuilder +{ + private readonly ILocalizationService _localization = localization; + + public string BuildReport(string overallLabel, IEnumerable categories) + { + var sb = new StringBuilder(); + sb.AppendLine(L("diagnostics.report.title", "AkkornStudio - Diagnostic Report")); + sb.AppendLine($"{L("diagnostics.report.generated", "Generated")}: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + sb.AppendLine($"{L("diagnostics.report.overall", "Overall")}: {overallLabel}"); + sb.AppendLine(new string('─', 50)); + + foreach (AppDiagnosticCategoryViewModel category in categories) + { + IReadOnlyList snapshot = category.SnapshotItems(); + if (snapshot.Count == 0) + continue; + + sb.AppendLine($"[{category.Title}]"); foreach (AppDiagnosticEntry entry in snapshot) { sb.AppendLine($"- {entry.Name} [{entry.Status}]"); @@ -35,14 +35,14 @@ public string BuildReport(string overallLabel, IEnumerable /// Executes real benchmark iterations when a connection + SQL are available. diff --git a/src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkExecutionService.cs b/src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkExecutionService.cs similarity index 94% rename from src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkExecutionService.cs rename to src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkExecutionService.cs index fb0d03d2..1bb06a0f 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkExecutionService.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkExecutionService.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class BenchmarkExecutionService(IBenchmarkRunner benchmarkRunner) : IBenchmarkExecutionService { diff --git a/src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkLatencyProfileSampler.cs b/src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkLatencyProfileSampler.cs similarity index 92% rename from src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkLatencyProfileSampler.cs rename to src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkLatencyProfileSampler.cs index 1e40b506..6b5d3f3e 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkLatencyProfileSampler.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkLatencyProfileSampler.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class BenchmarkLatencyProfileSampler : IBenchmarkLatencyProfileSampler { diff --git a/src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkRunner.cs b/src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkRunner.cs similarity index 97% rename from src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkRunner.cs rename to src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkRunner.cs index 4bf8eeb8..88f7514b 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkRunner.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkRunner.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class BenchmarkRunner( IBenchmarkIterationExecutor iterationExecutor, diff --git a/src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkStatisticsCalculator.cs b/src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkStatisticsCalculator.cs similarity index 96% rename from src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkStatisticsCalculator.cs rename to src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkStatisticsCalculator.cs index f4dbda40..ac322136 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Execution/BenchmarkStatisticsCalculator.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Execution/BenchmarkStatisticsCalculator.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public static class BenchmarkStatisticsCalculator { diff --git a/src/DBWeaver.UI/Services/Benchmark/Execution/SimulatedBenchmarkIterationExecutor.cs b/src/AkkornStudio.UI/Services/Benchmark/Execution/SimulatedBenchmarkIterationExecutor.cs similarity index 96% rename from src/DBWeaver.UI/Services/Benchmark/Execution/SimulatedBenchmarkIterationExecutor.cs rename to src/AkkornStudio.UI/Services/Benchmark/Execution/SimulatedBenchmarkIterationExecutor.cs index 40a876d6..f27e367d 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Execution/SimulatedBenchmarkIterationExecutor.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Execution/SimulatedBenchmarkIterationExecutor.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class SimulatedBenchmarkIterationExecutor : IBenchmarkIterationExecutor { diff --git a/src/DBWeaver.UI/Services/Benchmark/Execution/TaskDelayScheduler.cs b/src/AkkornStudio.UI/Services/Benchmark/Execution/TaskDelayScheduler.cs similarity index 82% rename from src/DBWeaver.UI/Services/Benchmark/Execution/TaskDelayScheduler.cs rename to src/AkkornStudio.UI/Services/Benchmark/Execution/TaskDelayScheduler.cs index b0b04b98..8ddb0bf7 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Execution/TaskDelayScheduler.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Execution/TaskDelayScheduler.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class TaskDelayScheduler : IBenchmarkDelayScheduler { diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkInitialState.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkInitialState.cs similarity index 75% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkInitialState.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkInitialState.cs index 20b4eadb..9762802f 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkInitialState.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkInitialState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkInitialState( int Iterations, diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkProgressViewState.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkProgressViewState.cs similarity index 66% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkProgressViewState.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkProgressViewState.cs index df5b0007..fe494c98 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkProgressViewState.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkProgressViewState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkProgressViewState(string Message, double Fraction); diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkResultApplicationState.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkResultApplicationState.cs similarity index 76% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkResultApplicationState.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkResultApplicationState.cs index 4fd58969..52c019b3 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkResultApplicationState.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkResultApplicationState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkResultApplicationState( BenchmarkRunResult LatestResult, diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunConfiguration.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunConfiguration.cs similarity index 92% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunConfiguration.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunConfiguration.cs index f1fd56c8..b3363082 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunConfiguration.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunConfiguration.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkRunConfiguration(int Iterations, int WarmupIterations, int IntervalMs) { diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunContext.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunContext.cs similarity index 78% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunContext.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunContext.cs index 8e2ff108..4af062cd 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunContext.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunContext.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkRunContext( string Sql, diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunContextCreationResult.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunContextCreationResult.cs similarity index 79% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunContextCreationResult.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunContextCreationResult.cs index 00a4f452..ef75f181 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunContextCreationResult.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunContextCreationResult.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkRunContextCreationResult( BenchmarkRunContext? Context, diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunProgress.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunProgress.cs similarity index 76% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunProgress.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunProgress.cs index aa9d437c..a70ca248 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunProgress.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunProgress.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkRunProgress( BenchmarkRunStage Stage, diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunResult.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunResult.cs similarity index 87% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunResult.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunResult.cs index 9d12ab5a..6a0b3312 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunResult.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunResult.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed record BenchmarkRunResult( string Label, diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunStage.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunStage.cs similarity index 57% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunStage.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunStage.cs index ff5f5c52..37bc4e1a 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunStage.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunStage.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public enum BenchmarkRunStage { diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunSuccessState.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunSuccessState.cs similarity index 69% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunSuccessState.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunSuccessState.cs index 1307423d..b050f18c 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunSuccessState.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunSuccessState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkRunSuccessState( string Progress, diff --git a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunUiState.cs b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunUiState.cs similarity index 72% rename from src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunUiState.cs rename to src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunUiState.cs index e5f7ffad..e61aca41 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Models/BenchmarkRunUiState.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Models/BenchmarkRunUiState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public readonly record struct BenchmarkRunUiState( bool IsRunning, diff --git a/src/DBWeaver.UI/Services/Benchmark/Presentation/BenchmarkProgressPresenter.cs b/src/AkkornStudio.UI/Services/Benchmark/Presentation/BenchmarkProgressPresenter.cs similarity index 94% rename from src/DBWeaver.UI/Services/Benchmark/Presentation/BenchmarkProgressPresenter.cs rename to src/AkkornStudio.UI/Services/Benchmark/Presentation/BenchmarkProgressPresenter.cs index abbfacc2..2e11376a 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Presentation/BenchmarkProgressPresenter.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Presentation/BenchmarkProgressPresenter.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class BenchmarkProgressPresenter(IBenchmarkTextProvider textProvider) : IBenchmarkProgressPresenter { diff --git a/src/DBWeaver.UI/Services/Benchmark/Presentation/LocalizedBenchmarkTextProvider.cs b/src/AkkornStudio.UI/Services/Benchmark/Presentation/LocalizedBenchmarkTextProvider.cs similarity index 93% rename from src/DBWeaver.UI/Services/Benchmark/Presentation/LocalizedBenchmarkTextProvider.cs rename to src/AkkornStudio.UI/Services/Benchmark/Presentation/LocalizedBenchmarkTextProvider.cs index 37e9f41c..484c27f0 100644 --- a/src/DBWeaver.UI/Services/Benchmark/Presentation/LocalizedBenchmarkTextProvider.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/Presentation/LocalizedBenchmarkTextProvider.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.Services.Localization; +using AkkornStudio.UI.Services.Localization; -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class LocalizedBenchmarkTextProvider : IBenchmarkTextProvider { diff --git a/src/DBWeaver.UI/Services/Benchmark/State/BenchmarkResultCoordinator.cs b/src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkResultCoordinator.cs similarity index 93% rename from src/DBWeaver.UI/Services/Benchmark/State/BenchmarkResultCoordinator.cs rename to src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkResultCoordinator.cs index c4e29f73..2c08db89 100644 --- a/src/DBWeaver.UI/Services/Benchmark/State/BenchmarkResultCoordinator.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkResultCoordinator.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class BenchmarkResultCoordinator(IBenchmarkTextProvider textProvider) : IBenchmarkResultCoordinator { diff --git a/src/DBWeaver.UI/Services/Benchmark/State/BenchmarkRunContextFactory.cs b/src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkRunContextFactory.cs similarity index 94% rename from src/DBWeaver.UI/Services/Benchmark/State/BenchmarkRunContextFactory.cs rename to src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkRunContextFactory.cs index fe60ca97..38966fc5 100644 --- a/src/DBWeaver.UI/Services/Benchmark/State/BenchmarkRunContextFactory.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkRunContextFactory.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class BenchmarkRunContextFactory(IBenchmarkTextProvider textProvider) : IBenchmarkRunContextFactory { diff --git a/src/DBWeaver.UI/Services/Benchmark/State/BenchmarkRunStateCoordinator.cs b/src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkRunStateCoordinator.cs similarity index 93% rename from src/DBWeaver.UI/Services/Benchmark/State/BenchmarkRunStateCoordinator.cs rename to src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkRunStateCoordinator.cs index 6e6c6c42..75cd0860 100644 --- a/src/DBWeaver.UI/Services/Benchmark/State/BenchmarkRunStateCoordinator.cs +++ b/src/AkkornStudio.UI/Services/Benchmark/State/BenchmarkRunStateCoordinator.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Benchmark; +namespace AkkornStudio.UI.Services.Benchmark; public sealed class BenchmarkRunStateCoordinator(IBenchmarkTextProvider textProvider) : IBenchmarkRunStateCoordinator { diff --git a/src/AkkornStudio.UI/Services/Canvas/AutoJoin/AutoJoinToastMessage.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/AutoJoinToastMessage.cs new file mode 100644 index 00000000..17e02d71 --- /dev/null +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/AutoJoinToastMessage.cs @@ -0,0 +1,11 @@ +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; + +internal readonly record struct AutoJoinToastMessage(string Message, string? Details = null); + + + + + diff --git a/src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinApplicationService.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinApplicationService.cs similarity index 97% rename from src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinApplicationService.cs rename to src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinApplicationService.cs index d485056d..e3aa1f3c 100644 --- a/src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinApplicationService.cs +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinApplicationService.cs @@ -1,11 +1,11 @@ using Avalonia; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.CanvasKit; -using DBWeaver.Metadata; -using DBWeaver.Nodes; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.CanvasKit; +using AkkornStudio.Metadata; +using AkkornStudio.Nodes; -namespace DBWeaver.UI.Services.Canvas.AutoJoin; +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; internal sealed class CanvasAutoJoinApplicationService : ICanvasAutoJoinApplicationService { diff --git a/src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinMessagePresenter.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinMessagePresenter.cs similarity index 93% rename from src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinMessagePresenter.cs rename to src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinMessagePresenter.cs index 9ac61d67..71dfd08f 100644 --- a/src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinMessagePresenter.cs +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinMessagePresenter.cs @@ -1,8 +1,8 @@ -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.Services.Localization; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Canvas.AutoJoin; +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; internal sealed class CanvasAutoJoinMessagePresenter(ILocalizationService localization) : ICanvasAutoJoinMessagePresenter { diff --git a/src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinNotifier.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinNotifier.cs similarity index 93% rename from src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinNotifier.cs rename to src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinNotifier.cs index abb6bb6a..64b546e2 100644 --- a/src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinNotifier.cs +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinNotifier.cs @@ -1,7 +1,7 @@ -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels; -namespace DBWeaver.UI.Services.Canvas.AutoJoin; +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; internal sealed class CanvasAutoJoinNotifier( ToastCenterViewModel toasts, diff --git a/src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinSuggestionService.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinSuggestionService.cs similarity index 96% rename from src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinSuggestionService.cs rename to src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinSuggestionService.cs index 4da23a12..2485cfd6 100644 --- a/src/DBWeaver.UI/Services/Canvas/AutoJoin/CanvasAutoJoinSuggestionService.cs +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/CanvasAutoJoinSuggestionService.cs @@ -1,11 +1,11 @@ -using DBWeaver.Core; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.CanvasKit; -using DBWeaver.Metadata; -using DBWeaver.Nodes; - -namespace DBWeaver.UI.Services.Canvas.AutoJoin; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.CanvasKit; +using AkkornStudio.Metadata; +using AkkornStudio.Nodes; + +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; internal sealed class CanvasAutoJoinSuggestionService : ICanvasAutoJoinSuggestionService { diff --git a/src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinApplicationService.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinApplicationService.cs similarity index 81% rename from src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinApplicationService.cs rename to src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinApplicationService.cs index 90150ace..89a24930 100644 --- a/src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinApplicationService.cs +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinApplicationService.cs @@ -1,10 +1,10 @@ using Avalonia; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.Metadata; -using DBWeaver.Nodes; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.Metadata; +using AkkornStudio.Nodes; -namespace DBWeaver.UI.Services.Canvas.AutoJoin; +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; internal interface ICanvasAutoJoinApplicationService { diff --git a/src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinMessagePresenter.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinMessagePresenter.cs similarity index 78% rename from src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinMessagePresenter.cs rename to src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinMessagePresenter.cs index 9e3d2a21..088d3753 100644 --- a/src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinMessagePresenter.cs +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinMessagePresenter.cs @@ -1,7 +1,7 @@ -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels; -namespace DBWeaver.UI.Services.Canvas.AutoJoin; +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; internal interface ICanvasAutoJoinMessagePresenter { diff --git a/src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinNotifier.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinNotifier.cs similarity index 76% rename from src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinNotifier.cs rename to src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinNotifier.cs index d8212916..a60cb291 100644 --- a/src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinNotifier.cs +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinNotifier.cs @@ -1,7 +1,7 @@ -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels; -namespace DBWeaver.UI.Services.Canvas.AutoJoin; +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; internal interface ICanvasAutoJoinNotifier { diff --git a/src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinSuggestionService.cs b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinSuggestionService.cs similarity index 72% rename from src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinSuggestionService.cs rename to src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinSuggestionService.cs index 95b962b7..9f0fb9fc 100644 --- a/src/DBWeaver.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinSuggestionService.cs +++ b/src/AkkornStudio.UI/Services/Canvas/AutoJoin/ICanvasAutoJoinSuggestionService.cs @@ -1,8 +1,8 @@ -using DBWeaver.Metadata; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Metadata; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Canvas.AutoJoin; +namespace AkkornStudio.UI.Services.Canvas.AutoJoin; internal interface ICanvasAutoJoinSuggestionService { diff --git a/src/DBWeaver.UI/Services/CommandPalette/CommandPaletteFactory.cs b/src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteFactory.cs similarity index 90% rename from src/DBWeaver.UI/Services/CommandPalette/CommandPaletteFactory.cs rename to src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteFactory.cs index 682a5e1e..7624bd28 100644 --- a/src/DBWeaver.UI/Services/CommandPalette/CommandPaletteFactory.cs +++ b/src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteFactory.cs @@ -1,706 +1,735 @@ -using Avalonia; -using Avalonia.Controls; -using Material.Icons; -using DBWeaver.Nodes; -using DBWeaver.UI.Controls; -using DBWeaver.UI.Controls.Shell; -using DBWeaver.UI.Services.Input.ShortcutRegistry; -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.Services.Workspace.Preview; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Shortcuts; - -namespace DBWeaver.UI.Services.CommandPalette; - -/// -/// Factory for creating command palette commands. -/// Centralizes 30+ command definitions with their shortcuts and metadata. -/// -public class CommandPaletteFactory( - Window window, - Func activeCanvasAccessor, - Func shellAccessor, - FileOperationsService fileOps, - ExportService export, - PreviewService preview, - Action? onCreateNewCanvas = null, - IShortcutRegistry? shortcutRegistry = null -) -{ - private static readonly IShortcutRegistry VolatileRegistry = - new global::DBWeaver.UI.Services.Input.ShortcutRegistry.ShortcutRegistry( - customizationStore: new NoOpShortcutCustomizationStore()); - - private readonly Window _window = window; - private readonly Func _activeCanvasAccessor = activeCanvasAccessor; - private readonly Func _shellAccessor = shellAccessor; - private readonly FileOperationsService _fileOps = fileOps; - private readonly ExportService _export = export; - private readonly PreviewService _preview = preview; - private readonly Action? _onCreateNewCanvas = onCreateNewCanvas; - private readonly IShortcutRegistry _shortcutRegistry = shortcutRegistry ?? VolatileRegistry; - private CanvasViewModel CurrentCanvas => _activeCanvasAccessor(); - private ShellViewModel CurrentShell => _shellAccessor(); - - public IReadOnlyList CreateAllCommands() => - [..CreateBasicCommands(), ..CreateTemplateCommands()]; - - private List CreateBasicCommands() => - [ - // ── Flow Version History ────────────────────────────────────────── - new() - { - Name = LN("Flow Version History"), - Description = LD("Create checkpoints, compare versions side-by-side and restore a previous canvas state"), - Shortcut = ShortcutText(ShortcutActionIds.OpenFlowVersions, "Ctrl+Shift+H"), - Icon = MaterialIconKind.ClipboardTextHistory, - Tags = LTg("version history checkpoint diff restore snapshot compare undo flow"), - Execute = () => CurrentCanvas.FlowVersions.Open(), - }, - new() - { - Name = LN("File Save/Load History"), - Description = LD("Open local file version history created on each save and restore previous saved snapshots"), - Shortcut = ShortcutText(ShortcutActionIds.OpenFileHistory, "Ctrl+Alt+H"), - Icon = MaterialIconKind.History, - Tags = LTg("file history save load backup versions restore local"), - Execute = () => CurrentCanvas.FileHistory.Open(), - }, - new() - { - Name = LN("Keyboard Shortcuts"), - Description = LD("Open shortcut reference screen"), - Shortcut = ShortcutText(ShortcutActionIds.OpenShortcutsReference, "F1"), - Icon = MaterialIconKind.Keyboard, - Tags = LTg("help shortcuts hotkeys keyboard reference"), - Execute = () => new KeyboardShortcutsWindow(new KeyboardShortcutsViewModel(_shortcutRegistry)).Show(_window), - }, - // ── Auto-Join ───────────────────────────────────────────────────── - new() - { - Name = LN("Analyze All Joins"), - Description = LD("Scan all table-source nodes on the canvas for possible join relationships based on FK conventions and naming patterns"), - Shortcut = "", - Icon = MaterialIconKind.AutoFix, - Tags = LTg("join autojoin analyze suggest detect foreign key relationships heuristic"), - Execute = () => CurrentCanvas.AnalyzeAllCanvasJoins(), - }, - // ── SQL Importer ────────────────────────────────────────────────── - new() - { - ActionId = "canvas.importSqlToGraph", - Name = LN("Import SQL to Graph"), - Description = LD("Paste a SELECT statement and generate nodes automatically — FROM, JOIN, WHERE, LIMIT are supported"), - Shortcut = "", - Icon = MaterialIconKind.CodeBrackets, - Tags = LTg("import sql paste convert graph reverse engineer query"), - Execute = () => - { - if (!CurrentShell.IsQueryDocumentPageActive) - { - CurrentShell.Toasts.ShowWarning( - L("toast.switchToQueryForSqlImport", "Alterne para o modo Query para importar SQL em grafo.")); - return; - } - - CurrentCanvas.SqlImporter.Open(); - }, - }, - // ── Snippets ────────────────────────────────────────────────────── - new() - { - Name = LN("Save Selection as Snippet"), - Description = LD("Save the selected nodes as a reusable snippet — insert it later via the node search menu (⇧A)"), - Shortcut = "", - Icon = MaterialIconKind.BookmarkPlus, - Tags = LTg("snippet save selection reuse template favorite bookmark"), - Execute = () => - { - var selected = CurrentCanvas.Nodes.Where(n => n.IsSelected).ToList(); - if (selected.Count == 0) - return; - // Auto-name: first node title + count - string baseName = selected[0].Title; - string name = selected.Count == 1 - ? baseName - : $"{baseName} +{selected.Count - 1}"; - CurrentCanvas.SaveSelectionAsSnippet(name); - }, - }, - new() - { - Name = LN("Edit Selected CTE"), - Description = LD("Open isolated sub-canvas editor for the selected CTE Definition node"), - Shortcut = ShortcutText(ShortcutActionIds.ToggleCteEditor, "Ctrl+Alt+Enter"), - Icon = MaterialIconKind.FileTree, - Tags = LTg("cte with recursive editor subgraph subcanvas isolate"), - Execute = () => CurrentCanvas.EnterCteEditorCommand.Execute(null), - }, - new() - { - Name = LN("Exit CTE Editor"), - Description = LD("Apply CTE sub-canvas edits and return to the parent canvas"), - Shortcut = "Esc", - Icon = MaterialIconKind.ExitToApp, - Tags = LTg("cte subcanvas exit apply back"), - Execute = () => CurrentCanvas.ExitCteEditorCommand.Execute(null), - }, - new() - { - Name = LN("Discard and Exit Editor"), - Description = LD("Discard current sub-editor edits and return to the parent canvas"), - Shortcut = "", - Icon = MaterialIconKind.ExitRun, - Tags = LTg("cte view subcanvas discard exit force"), - Execute = () => CurrentCanvas.DiscardAndExitSubEditorCommand.Execute(null), - }, - // ── Explain Plan ────────────────────────────────────────────────── - new() - { - Name = LN("Explain Plan"), - Description = LD("Inspect the query execution plan — see scan types, join strategies, and cost estimates"), - Shortcut = ShortcutText(ShortcutActionIds.ExplainPlan, "F4"), - Icon = MaterialIconKind.TableSearch, - Tags = LTg("explain plan execution cost scan index join performance"), - Execute = () => CurrentCanvas.ExplainPlan.Open(), - }, - // ── Benchmark ───────────────────────────────────────────────────── - new() - { - Name = LN("Run Query Benchmark"), - Description = LD("Measure avg / median / p95 latency of the current SQL over N iterations"), - Shortcut = "", - Icon = MaterialIconKind.TimerOutline, - Tags = LTg("benchmark performance latency timing profile measure speed"), - Execute = () => CurrentCanvas.Benchmark.Open(), - }, - // ── Connection ──────────────────────────────────────────────────── - new() - { - Name = LN("Manage Connections"), - Description = LD("Open the connection manager to add, edit or switch database connections"), - Shortcut = ShortcutText(ShortcutActionIds.OpenConnectionManager, "Ctrl+Shift+C"), - Icon = MaterialIconKind.DatabaseSettings, - Tags = LTg("connection database server host provider switch"), - Execute = () => CurrentCanvas.ConnectionManager.Open(), - }, - // ── File ────────────────────────────────────────────────────────── - new() - { - Name = "New Canvas", - Description = LD("Clear canvas and start fresh"), - Shortcut = ShortcutText(ShortcutActionIds.NewCanvas, "Ctrl+N"), - Icon = MaterialIconKind.FileOutline, - Tags = LTg("reset clear blank"), - Execute = () => - { - if (_onCreateNewCanvas is not null) - { - _onCreateNewCanvas(); - return; - } - - _window.DataContext = new CanvasViewModel(); - if (_window.DataContext is CanvasViewModel currentVm) - _window.Title = currentVm.WindowTitle; - }, - }, - new() - { - Name = LN("Open File"), - Description = LD("Load a .vsaq canvas file"), - Shortcut = ShortcutText(ShortcutActionIds.OpenFile, "Ctrl+O"), - Icon = MaterialIconKind.FolderOpenOutline, - Tags = LTg("load import vsaq"), - Execute = async () => await _fileOps.OpenAsync(), - }, - new() - { - Name = LN("Save"), - Description = LD("Save current canvas"), - Shortcut = ShortcutText(ShortcutActionIds.Save, "Ctrl+S"), - Icon = MaterialIconKind.ContentSave, - Tags = LTg("persist write disk"), - Execute = async () => await _fileOps.SaveAsync(saveAs: false), - }, - new() - { - Name = LN("Save As"), - Description = LD("Save canvas to a new file"), - Shortcut = ShortcutText(ShortcutActionIds.SaveAs, "Ctrl+Shift+S"), - Icon = MaterialIconKind.ContentSaveEdit, - Tags = LTg("export persist copy"), - Execute = async () => await _fileOps.SaveAsync(saveAs: true), - }, - // ── Edit ────────────────────────────────────────────────────────── - new() - { - Name = L("command.undo.name", "Undo"), - Description = L("command.undo.description", "Undo last action"), - Shortcut = ShortcutText(ShortcutActionIds.Undo, "Ctrl+Z"), - Icon = MaterialIconKind.Undo, - Tags = LTg("revert back history"), - Execute = () => CurrentCanvas.UndoRedo.Undo(), - }, - new() - { - Name = L("command.redo.name", "Redo"), - Description = L("command.redo.description", "Redo last undone action"), - Shortcut = ShortcutText(ShortcutActionIds.Redo, "Ctrl+Y"), - Icon = MaterialIconKind.Redo, - Tags = LTg("forward history"), - Execute = () => CurrentCanvas.UndoRedo.Redo(), - }, - new() - { - Name = LN("Select All"), - Description = LD("Select all nodes on canvas"), - Shortcut = ShortcutText(ShortcutActionIds.SelectAll, "Ctrl+A"), - Icon = MaterialIconKind.SelectAll, - Tags = LTg("highlight mark all nodes"), - Execute = () => CurrentCanvas.SelectAllCommand.Execute(null), - }, - new() - { - Name = LN("Deselect All"), - Description = LD("Clear node selection"), - Shortcut = "Esc", - Icon = MaterialIconKind.SelectOff, - Tags = LTg("clear selection"), - Execute = () => CurrentCanvas.DeselectAllCommand.Execute(null), - }, - new() - { - Name = LN("Delete Selected"), - Description = LD("Delete the selected nodes"), - Shortcut = ShortcutText(ShortcutActionIds.DeleteSelection, "Del"), - Icon = MaterialIconKind.Delete, - Tags = LTg("remove erase nodes"), - Execute = () => CurrentCanvas.DeleteSelectedCommand.Execute(null), - }, - // ── Canvas ──────────────────────────────────────────────────────── - new() - { - Name = L("command.addNode.name", "Add Node"), - Description = L("command.addNode.description", "Open node search menu to add a node"), - Shortcut = ShortcutText(ShortcutActionIds.OpenNodeSearch, "Shift+A"), - Icon = MaterialIconKind.Plus, - Tags = LTg("create insert search transform"), - Execute = () => OpenSearch(), - }, - new() - { - Name = L("command.bringForward.name", "Bring Forward"), - Description = L("command.bringForward.description", "Move selected nodes one layer forward"), - Shortcut = ShortcutText(ShortcutActionIds.BringForward, "Ctrl+PgUp"), - Icon = MaterialIconKind.ArrangeBringForward, - Tags = LTg("layer z-order forward selected nodes"), - Execute = () => CurrentCanvas.BringSelectionForwardCommand.Execute(null), - }, - new() - { - Name = L("command.sendBackward.name", "Send Backward"), - Description = L("command.sendBackward.description", "Move selected nodes one layer backward"), - Shortcut = ShortcutText(ShortcutActionIds.SendBackward, "Ctrl+PgDown"), - Icon = MaterialIconKind.ArrangeSendBackward, - Tags = LTg("layer z-order backward selected nodes"), - Execute = () => CurrentCanvas.SendSelectionBackwardCommand.Execute(null), - }, - new() - { - Name = L("command.bringToFront.name", "Bring to Front"), - Description = L("command.bringToFront.description", "Move selected nodes to top layer"), - Shortcut = ShortcutText(ShortcutActionIds.BringToFront, "Ctrl+Shift+PgUp"), - Icon = MaterialIconKind.ArrangeBringToFront, - Tags = LTg("layer z-order front selected nodes"), - Execute = () => CurrentCanvas.BringSelectionToFrontCommand.Execute(null), - }, - new() - { - Name = L("command.sendToBack.name", "Send to Back"), - Description = L("command.sendToBack.description", "Move selected nodes to bottom layer"), - Shortcut = ShortcutText(ShortcutActionIds.SendToBack, "Ctrl+Shift+PgDown"), - Icon = MaterialIconKind.ArrangeSendToBack, - Tags = LTg("layer z-order back selected nodes"), - Execute = () => CurrentCanvas.SendSelectionToBackCommand.Execute(null), - }, - new() - { - Name = L("command.normalizeLayers.name", "Normalize Layers"), - Description = L("command.normalizeLayers.description", "Compact node layer indices to a clean 0..N order"), - Shortcut = "", - Icon = MaterialIconKind.LayersTriple, - Tags = LTg("layer z-order normalize compact"), - Execute = () => CurrentCanvas.NormalizeLayersCommand.Execute(null), - }, - new() - { - Name = LN("Zoom In"), - Description = LD("Zoom into the canvas"), - Shortcut = ShortcutText(ShortcutActionIds.ZoomIn, "Ctrl++"), - Icon = MaterialIconKind.MagnifyPlus, - Tags = LTg("magnify enlarge"), - Execute = () => CurrentCanvas.ZoomInCommand.Execute(null), - }, - new() - { - Name = LN("Zoom Out"), - Description = LD("Zoom out of the canvas"), - Shortcut = ShortcutText(ShortcutActionIds.ZoomOut, "Ctrl+-"), - Icon = MaterialIconKind.MagnifyMinus, - Tags = LTg("shrink reduce"), - Execute = () => CurrentCanvas.ZoomOutCommand.Execute(null), - }, - new() - { - Name = LN("Fit to Screen"), - Description = LD("Fit all nodes into the visible area"), - Shortcut = "", - Icon = MaterialIconKind.FitToPage, - Tags = LTg("auto layout view reset zoom"), - Execute = () => CurrentCanvas.FitToScreenCommand.Execute(null), - }, - new() - { - Name = LN("Reset Viewport"), - Description = LD("Reset zoom and pan to default"), - Shortcut = ShortcutText(ShortcutActionIds.ZoomReset, "Ctrl+0"), - Icon = MaterialIconKind.MagnifyRemoveOutline, - Tags = LTg("100 percent restore zoom pan viewport"), - Execute = () => CurrentCanvas.ResetZoomCommand.Execute(null), - }, - // ── Query / Preview ─────────────────────────────────────────────── - new() - { - ActionId = ShortcutActionIds.TogglePreview, - Name = LN("Toggle Preview"), - Description = LD("Open output preview modal for the active mode"), - Shortcut = ShortcutText(ShortcutActionIds.TogglePreview, "F3"), - Icon = MaterialIconKind.TableEye, - Tags = LTg("data results table panel"), - Execute = OpenOutputPreviewModal, - }, - new() - { - ActionId = ShortcutActionIds.RunPreview, - Name = LN("Run Preview"), - Description = LD("Execute the current query in preview"), - Shortcut = ShortcutText(ShortcutActionIds.RunPreview, "F5"), - Icon = MaterialIconKind.Play, - Tags = LTg("execute run sql query results"), - Execute = async () => - { - if (!CurrentCanvas.HasErrors && !CurrentCanvas.LiveSql.IsMutatingCommand) - await _preview.RunPreviewAsync(); - }, - }, - // ── Cleanup / Quality ───────────────────────────────────────────── - new() - { - Name = LN("Auto Layout"), - Description = LD("Arrange nodes into logical columns automatically"), - Shortcut = ShortcutText(ShortcutActionIds.AutoLayout, "Ctrl+L"), - Icon = MaterialIconKind.FormatHorizontalAlignCenter, - Tags = LTg("layout arrange columns auto organize readability"), - Execute = () => - { - CurrentCanvas.AutoLayoutCommand.Execute(null); - GetActiveCanvasControl()?.InvalidateWires(); - }, - }, - new() - { - Name = LN("Cleanup Orphans"), - Description = LD("Remove all nodes not connected to output"), - Shortcut = "", - Icon = MaterialIconKind.VectorUnion, - Tags = LTg("orphan unused disconnected clean delete nodes"), - Execute = () => CurrentCanvas.CleanupOrphansCommand.Execute(null), - }, - new() - { - Name = LN("Auto-Fix Naming"), - Description = LD("Convert aliases to the convention configured in project settings"), - Shortcut = "", - Icon = MaterialIconKind.AutoFix, - Tags = LTg("rename alias fix naming convention"), - Execute = () => CurrentCanvas.AutoFixNamingCommand.Execute(null), - }, - // ── Snap / Alignment ────────────────────────────────────────────── - new() - { - Name = LN("Toggle Snap to Grid"), - Description = LD("Snap node positions to 16px grid (Ctrl+G)"), - Shortcut = ShortcutText(ShortcutActionIds.ToggleSnapToGrid, "Ctrl+G"), - Icon = MaterialIconKind.GridLarge, - Tags = LTg("snap grid align precision position"), - Execute = () => CurrentCanvas.ToggleSnapCommand.Execute(null), - }, - new() - { - Name = LN("Align Left"), - Description = LD("Align selected nodes to the leftmost edge"), - Shortcut = "", - Icon = MaterialIconKind.AlignHorizontalLeft, - Tags = LTg("align left edge selection nodes"), - Execute = () => CurrentCanvas.AlignLeftCommand.Execute(null), - }, - new() - { - Name = LN("Align Right"), - Description = LD("Align selected nodes to the rightmost edge"), - Shortcut = "", - Icon = MaterialIconKind.AlignHorizontalRight, - Tags = LTg("align right edge selection nodes"), - Execute = () => CurrentCanvas.AlignRightCommand.Execute(null), - }, - new() - { - Name = LN("Align Top"), - Description = LD("Align selected nodes to the topmost edge"), - Shortcut = "", - Icon = MaterialIconKind.AlignVerticalTop, - Tags = LTg("align top edge selection nodes"), - Execute = () => CurrentCanvas.AlignTopCommand.Execute(null), - }, - new() - { - Name = LN("Align Bottom"), - Description = LD("Align selected nodes to the bottom edge"), - Shortcut = "", - Icon = MaterialIconKind.AlignVerticalBottom, - Tags = LTg("align bottom edge selection nodes"), - Execute = () => CurrentCanvas.AlignBottomCommand.Execute(null), - }, - new() - { - Name = LN("Center Horizontally"), - Description = LD("Centre selected nodes on a horizontal axis"), - Shortcut = "", - Icon = MaterialIconKind.AlignHorizontalCenter, - Tags = LTg("align center middle horizontal nodes"), - Execute = () => CurrentCanvas.AlignCenterHCommand.Execute(null), - }, - new() - { - Name = LN("Center Vertically"), - Description = LD("Centre selected nodes on a vertical axis"), - Shortcut = "", - Icon = MaterialIconKind.AlignVerticalCenter, - Tags = LTg("align center middle vertical nodes"), - Execute = () => CurrentCanvas.AlignCenterVCommand.Execute(null), - }, - new() - { - Name = LN("Distribute Horizontally"), - Description = LD("Spread selected nodes with equal horizontal spacing"), - Shortcut = "", - Icon = MaterialIconKind.DistributeHorizontalCenter, - Tags = LTg("distribute space equal horizontal nodes"), - Execute = () => CurrentCanvas.DistributeHCommand.Execute(null), - }, - new() - { - Name = LN("Distribute Vertically"), - Description = LD("Spread selected nodes with equal vertical spacing"), - Shortcut = "", - Icon = MaterialIconKind.DistributeVerticalCenter, - Tags = LTg("distribute space equal vertical nodes"), - Execute = () => CurrentCanvas.DistributeVCommand.Execute(null), - }, - // ── Export ──────────────────────────────────────────────────────── - new() - { - Name = LN("Export Documentation"), - Description = LD("Save Markdown documentation of the current flow"), - Shortcut = "", - Icon = MaterialIconKind.FileDocument, - Tags = LTg("export markdown doc documentation flow save md"), - Execute = async () => await _export.RunExportDocumentationAsync(), - }, - new() - { - Name = LN("Export HTML"), - Description = LD("Generate HTML file from the first HTML Export node"), - Shortcut = "", - Icon = MaterialIconKind.LanguageHtml5, - Tags = LTg("export html file output report save"), - Execute = async () => - await _export.RunExportWithDialogAsync( - NodeType.HtmlExport, - L("export.fileType.html", "HTML Files"), - "html" - ), - }, - new() - { - Name = LN("Export JSON"), - Description = LD("Generate JSON file from the first JSON Export node"), - Shortcut = "", - Icon = MaterialIconKind.CodeJson, - Tags = LTg("export json file output save"), - Execute = async () => - await _export.RunExportWithDialogAsync( - NodeType.JsonExport, - L("export.fileType.json", "JSON Files"), - "json" - ), - }, - new() - { - Name = LN("Export CSV"), - Description = LD("Generate CSV file from the first CSV Export node"), - Shortcut = "", - Icon = MaterialIconKind.FileDelimited, - Tags = LTg("export csv file tabular output save"), - Execute = async () => - await _export.RunExportWithDialogAsync( - NodeType.CsvExport, - L("export.fileType.csv", "CSV Files"), - "csv" - ), - }, - new() - { - Name = LN("Export Excel"), - Description = LD("Generate XLSX workbook from the first Excel Export node"), - Shortcut = "", - Icon = MaterialIconKind.MicrosoftExcel, - Tags = LTg("export excel xlsx file tabular output spreadsheet save"), - Execute = async () => - await _export.RunExportWithDialogAsync( - NodeType.ExcelExport, - L("export.fileType.excel", "Excel Files"), - "xlsx" - ), - }, - ]; - - private void OpenOutputPreviewModal() - { - ShellViewModel shell = CurrentShell; - - switch (shell.ActivePreviewContract.Kind) - { - case WorkspaceDocumentPreviewKind.Query: - { - CanvasViewModel queryCanvas = shell.ActiveQueryCanvasDocument ?? shell.Canvas - ?? throw new InvalidOperationException("Preview SQL indisponivel para o canvas Query atual."); - LiveSqlBarViewModel liveSql = queryCanvas.LiveSql - ?? throw new InvalidOperationException("Preview SQL indisponivel para o canvas Query atual."); - liveSql.Recompile(); - shell.OutputPreview.OpenForQuery(queryCanvas, liveSql, liveSql.ProviderLabel); - return; - } - - case WorkspaceDocumentPreviewKind.Ddl: - { - CanvasViewModel ddlCanvas = shell.EnsureDdlCanvas(); - LiveDdlBarViewModel liveDdl = ddlCanvas.LiveDdl - ?? throw new InvalidOperationException("Preview DDL indisponivel para o canvas DDL atual."); - liveDdl.Recompile(); - shell.OutputPreview.OpenForDdl(ddlCanvas, liveDdl, ddlCanvas.Provider.ToString()); - return; - } - - default: - { - var contract = shell.ActivePreviewContract; - shell.OutputPreview.OpenUnavailable(contract.Title, contract.PrimaryTabLabel, contract.UnavailableMessage); - return; - } - } - } - - private List CreateTemplateCommands() => - [ - .. QueryTemplateLibrary.All.Select(t => new PaletteCommandItem - { - Name = string.Format(L("commandPalette.templatePrefix", "Template: {0}"), t.Name), - Description = t.Description, - Icon = MaterialIconKind.ViewGrid, - Tags = $"template starter query {t.Category.ToLowerInvariant()} {t.Tags}", - Execute = () => - { - CurrentCanvas.LoadTemplate(t); - GetActiveCanvasControl()?.InvalidateWires(); - }, - }), - ]; - - private void OpenSearch() - { - InfiniteCanvas? canvas = GetActiveCanvasControl(); - Point ctr = canvas is not null - ? CurrentCanvas.ScreenToCanvas(new Point(canvas.Bounds.Width / 2, canvas.Bounds.Height / 2)) - : new Point(400, 300); - CurrentCanvas.SearchMenu.Open(ctr); - } - - private InfiniteCanvas? GetActiveCanvasControl() - { - if (CurrentShell.IsDdlDocumentPageActive) - return _window.FindControl("DdlDocumentPage")?.CanvasControl; - - return _window.FindControl("QueryDocumentPage")?.CanvasControl; - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } - - private static string LN(string fallback) => - L($"commandPalette.name.{Slugify(fallback)}", fallback); - - private static string LD(string fallback) => - L($"commandPalette.description.{Slugify(fallback)}", fallback); - - private static string LTg(string fallback) => - L($"commandPalette.tags.{Slugify(fallback)}", fallback); - - private string ShortcutText(string actionId, string fallback) - { - ShortcutDefinition? definition = _shortcutRegistry.FindByActionId(actionId); - if (definition?.EffectiveGesture is null) - return fallback; - - return string.IsNullOrWhiteSpace(definition.EffectiveGesture.DisplayText) - ? fallback - : definition.EffectiveGesture.DisplayText; - } - - private static bool HasDdlOutput(LiveDdlBarViewModel? liveDdl) => - liveDdl is not null - && liveDdl.IsValid - && !string.IsNullOrWhiteSpace(liveDdl.RawSql); - - private static string Slugify(string text) - { - if (string.IsNullOrWhiteSpace(text)) - return "empty"; - - char[] buffer = new char[text.Length]; - int idx = 0; - bool lastWasUnderscore = false; - foreach (char ch in text.ToLowerInvariant()) - { - if (char.IsLetterOrDigit(ch)) - { - buffer[idx++] = ch; - lastWasUnderscore = false; - continue; - } - - if (!lastWasUnderscore) - { - buffer[idx++] = '_'; - lastWasUnderscore = true; - } - } - - string raw = new string(buffer, 0, idx).Trim('_'); - return string.IsNullOrWhiteSpace(raw) ? "value" : raw; - } -} +using Avalonia; +using Avalonia.Controls; +using Material.Icons; +using AkkornStudio.Nodes; +using AkkornStudio.UI.Controls; +using AkkornStudio.UI.Controls.Shell; +using AkkornStudio.UI.Services.Input.ShortcutRegistry; +using AkkornStudio.UI.Services.Localization; +using AkkornStudio.UI.Extensions; +using AkkornStudio.UI.Services.Workspace.Models; +using AkkornStudio.UI.Services.Workspace.Preview; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Shortcuts; + +namespace AkkornStudio.UI.Services.CommandPalette; + +/// +/// Factory for creating command palette commands. +/// Centralizes 30+ command definitions with their shortcuts and metadata. +/// +public class CommandPaletteFactory( + Window window, + Func activeCanvasAccessor, + Func shellAccessor, + FileOperationsService fileOps, + ExportService export, + PreviewService preview, + Action? onCreateNewCanvas = null, + IShortcutRegistry? shortcutRegistry = null +) +{ + private static readonly IShortcutRegistry VolatileRegistry = + new global::AkkornStudio.UI.Services.Input.ShortcutRegistry.ShortcutRegistry( + customizationStore: new NoOpShortcutCustomizationStore()); + + private readonly Window _window = window; + private readonly Func _activeCanvasAccessor = activeCanvasAccessor; + private readonly Func _shellAccessor = shellAccessor; + private readonly FileOperationsService _fileOps = fileOps; + private readonly ExportService _export = export; + private readonly PreviewService _preview = preview; + private readonly Action? _onCreateNewCanvas = onCreateNewCanvas; + private readonly IShortcutRegistry _shortcutRegistry = shortcutRegistry ?? VolatileRegistry; + private CanvasViewModel CurrentCanvas => _activeCanvasAccessor(); + private ShellViewModel CurrentShell => _shellAccessor(); + + public IReadOnlyList CreateAllCommands() => + [..CreateBasicCommands(), ..CreateTemplateCommands()]; + + private List CreateBasicCommands() => + [ + // ── Flow Version History ────────────────────────────────────────── + new() + { + Name = LN("Flow Version History"), + Description = LD("Create checkpoints, compare versions side-by-side and restore a previous canvas state"), + Shortcut = ShortcutText(ShortcutActionIds.OpenFlowVersions, "Ctrl+Shift+H"), + Icon = MaterialIconKind.ClipboardTextHistory, + Tags = LTg("version history checkpoint diff restore snapshot compare undo flow"), + Execute = () => CurrentCanvas.FlowVersions.Open(), + }, + new() + { + Name = LN("File Save/Load History"), + Description = LD("Open local file version history created on each save and restore previous saved snapshots"), + Shortcut = ShortcutText(ShortcutActionIds.OpenFileHistory, "Ctrl+Alt+H"), + Icon = MaterialIconKind.History, + Tags = LTg("file history save load backup versions restore local"), + Execute = () => CurrentCanvas.FileHistory.Open(), + }, + new() + { + Name = LN("Keyboard Shortcuts"), + Description = LD("Open shortcut reference screen"), + Shortcut = ShortcutText(ShortcutActionIds.OpenShortcutsReference, "F1"), + Icon = MaterialIconKind.Keyboard, + Tags = LTg("help shortcuts hotkeys keyboard reference"), + Execute = () => new KeyboardShortcutsWindow(new KeyboardShortcutsViewModel(_shortcutRegistry)).Show(_window), + }, + // ── Auto-Join ───────────────────────────────────────────────────── + new() + { + Name = LN("Analyze All Joins"), + Description = LD("Scan all table-source nodes on the canvas for possible join relationships based on FK conventions and naming patterns"), + Shortcut = "", + Icon = MaterialIconKind.AutoFix, + Tags = LTg("join autojoin analyze suggest detect foreign key relationships heuristic"), + Execute = () => CurrentCanvas.AnalyzeAllCanvasJoins(), + }, + // ── SQL Importer ────────────────────────────────────────────────── + new() + { + ActionId = "canvas.importSqlToGraph", + Name = LN("Import SQL to Graph"), + Description = LD("Paste a SELECT statement and generate nodes automatically — FROM, JOIN, WHERE, LIMIT are supported"), + Shortcut = "", + Icon = MaterialIconKind.CodeBrackets, + Tags = LTg("import sql paste convert graph reverse engineer query"), + Execute = () => + { + if (!CurrentShell.IsQueryDocumentPageActive) + { + CurrentShell.Toasts.ShowWarning( + L("toast.switchToQueryForSqlImport", "Alterne para o modo Query para importar SQL em grafo.")); + return; + } + + CurrentCanvas.SqlImporter.Open(); + }, + }, + // ── Snippets ────────────────────────────────────────────────────── + new() + { + Name = LN("Save Canvas as Template"), + Description = LD("Persist the current canvas as a reusable query template available from Start and Command Palette"), + Shortcut = "", + Icon = MaterialIconKind.ContentSaveCogOutline, + Tags = LTg("template save current canvas user custom reusable starter"), + Execute = () => + { + if (CurrentCanvas.Nodes.Count == 0) + { + CurrentShell.Toasts.ShowWarning( + L("toast.templateSaveEmptyCanvas", "Adicione ao menos um node antes de salvar um template.")); + return; + } + + string name = CreateUserTemplateName(CurrentCanvas); + UserQueryTemplate template = QueryTemplateCatalog.SaveFromCanvas(CurrentCanvas, name); + CurrentShell.Toasts.ShowSuccess( + L("toast.templateSaved", "Template salvo."), + template.Name); + }, + }, + new() + { + Name = LN("Save Selection as Snippet"), + Description = LD("Save the selected nodes as a reusable snippet — insert it later via the node search menu (⇧A)"), + Shortcut = "", + Icon = MaterialIconKind.BookmarkPlus, + Tags = LTg("snippet save selection reuse template favorite bookmark"), + Execute = () => + { + var selected = CurrentCanvas.Nodes.Where(n => n.IsSelected).ToList(); + if (selected.Count == 0) + return; + // Auto-name: first node title + count + string baseName = selected[0].Title; + string name = selected.Count == 1 + ? baseName + : $"{baseName} +{selected.Count - 1}"; + CurrentCanvas.SaveSelectionAsSnippet(name); + }, + }, + new() + { + Name = LN("Edit Selected CTE"), + Description = LD("Open isolated sub-canvas editor for the selected CTE Definition node"), + Shortcut = ShortcutText(ShortcutActionIds.ToggleCteEditor, "Ctrl+Alt+Enter"), + Icon = MaterialIconKind.FileTree, + Tags = LTg("cte with recursive editor subgraph subcanvas isolate"), + Execute = () => CurrentCanvas.EnterCteEditorCommand.Execute(null), + }, + new() + { + Name = LN("Exit CTE Editor"), + Description = LD("Apply CTE sub-canvas edits and return to the parent canvas"), + Shortcut = "Esc", + Icon = MaterialIconKind.ExitToApp, + Tags = LTg("cte subcanvas exit apply back"), + Execute = () => CurrentCanvas.ExitCteEditorCommand.Execute(null), + }, + new() + { + Name = LN("Discard and Exit Editor"), + Description = LD("Discard current sub-editor edits and return to the parent canvas"), + Shortcut = "", + Icon = MaterialIconKind.ExitRun, + Tags = LTg("cte view subcanvas discard exit force"), + Execute = () => CurrentCanvas.DiscardAndExitSubEditorCommand.Execute(null), + }, + // ── Explain Plan ────────────────────────────────────────────────── + new() + { + Name = LN("Explain Plan"), + Description = LD("Inspect the query execution plan — see scan types, join strategies, and cost estimates"), + Shortcut = ShortcutText(ShortcutActionIds.ExplainPlan, "F4"), + Icon = MaterialIconKind.TableSearch, + Tags = LTg("explain plan execution cost scan index join performance"), + Execute = () => CurrentCanvas.ExplainPlan.Open(), + }, + // ── Benchmark ───────────────────────────────────────────────────── + new() + { + Name = LN("Run Query Benchmark"), + Description = LD("Measure avg / median / p95 latency of the current SQL over N iterations"), + Shortcut = "", + Icon = MaterialIconKind.TimerOutline, + Tags = LTg("benchmark performance latency timing profile measure speed"), + Execute = () => CurrentCanvas.Benchmark.Open(), + }, + // ── Connection ──────────────────────────────────────────────────── + new() + { + Name = "Refresh ER Diagram", + Description = "Rebuild the ER document from the active metadata snapshot", + Shortcut = "", + Icon = MaterialIconKind.Reload, + Tags = "er diagram entity relationship refresh metadata schema rebuild", + Execute = () => CurrentShell.ActivateDocument(WorkspaceDocumentType.ErDiagram), + }, + new() + { + Name = "Open Selected Join in ER Diagram", + Description = "Jump from the selected Query join node to the matching ER relationship", + Shortcut = "", + Icon = MaterialIconKind.RelationManyToMany, + Tags = "er diagram open selected join relationship query canvas focus", + Execute = () => + { + if (!CurrentShell.TryOpenSelectedQueryJoinInErDiagram()) + { + CurrentShell.Toasts.ShowWarning( + "Selecione um node JOIN valido no Query Canvas para abrir a relacao correspondente no ER."); + } + }, + }, + new() + { + Name = LN("Manage Connections"), + Description = LD("Open the connection manager to add, edit or switch database connections"), + Shortcut = ShortcutText(ShortcutActionIds.OpenConnectionManager, "Ctrl+Shift+C"), + Icon = MaterialIconKind.DatabaseSettings, + Tags = LTg("connection database server host provider switch"), + Execute = () => CurrentCanvas.ConnectionManager.Open(), + }, + // ── File ────────────────────────────────────────────────────────── + new() + { + Name = "New Canvas", + Description = LD("Clear canvas and start fresh"), + Shortcut = ShortcutText(ShortcutActionIds.NewCanvas, "Ctrl+N"), + Icon = MaterialIconKind.FileOutline, + Tags = LTg("reset clear blank"), + Execute = () => + { + if (_onCreateNewCanvas is not null) + { + _onCreateNewCanvas(); + return; + } + + _window.DataContext = new CanvasViewModel(); + if (_window.DataContext is CanvasViewModel currentVm) + _window.Title = currentVm.WindowTitle; + }, + }, + new() + { + Name = LN("Open File"), + Description = LD("Load a .vsaq canvas file"), + Shortcut = ShortcutText(ShortcutActionIds.OpenFile, "Ctrl+O"), + Icon = MaterialIconKind.FolderOpenOutline, + Tags = LTg("load import vsaq"), + Execute = async () => await _fileOps.OpenAsync(), + }, + new() + { + Name = LN("Save"), + Description = LD("Save current canvas"), + Shortcut = ShortcutText(ShortcutActionIds.Save, "Ctrl+S"), + Icon = MaterialIconKind.ContentSave, + Tags = LTg("persist write disk"), + Execute = async () => await _fileOps.SaveAsync(saveAs: false), + }, + new() + { + Name = LN("Save As"), + Description = LD("Save canvas to a new file"), + Shortcut = ShortcutText(ShortcutActionIds.SaveAs, "Ctrl+Shift+S"), + Icon = MaterialIconKind.ContentSaveEdit, + Tags = LTg("export persist copy"), + Execute = async () => await _fileOps.SaveAsync(saveAs: true), + }, + // ── Edit ────────────────────────────────────────────────────────── + new() + { + Name = L("command.undo.name", "Undo"), + Description = L("command.undo.description", "Undo last action"), + Shortcut = ShortcutText(ShortcutActionIds.Undo, "Ctrl+Z"), + Icon = MaterialIconKind.Undo, + Tags = LTg("revert back history"), + Execute = () => CurrentCanvas.UndoRedo.Undo(), + }, + new() + { + Name = L("command.redo.name", "Redo"), + Description = L("command.redo.description", "Redo last undone action"), + Shortcut = ShortcutText(ShortcutActionIds.Redo, "Ctrl+Y"), + Icon = MaterialIconKind.Redo, + Tags = LTg("forward history"), + Execute = () => CurrentCanvas.UndoRedo.Redo(), + }, + new() + { + Name = LN("Select All"), + Description = LD("Select all nodes on canvas"), + Shortcut = ShortcutText(ShortcutActionIds.SelectAll, "Ctrl+A"), + Icon = MaterialIconKind.SelectAll, + Tags = LTg("highlight mark all nodes"), + Execute = () => CurrentCanvas.SelectAllCommand.Execute(null), + }, + new() + { + Name = LN("Deselect All"), + Description = LD("Clear node selection"), + Shortcut = "Esc", + Icon = MaterialIconKind.SelectOff, + Tags = LTg("clear selection"), + Execute = () => CurrentCanvas.DeselectAllCommand.Execute(null), + }, + new() + { + Name = LN("Delete Selected"), + Description = LD("Delete the selected nodes"), + Shortcut = ShortcutText(ShortcutActionIds.DeleteSelection, "Del"), + Icon = MaterialIconKind.Delete, + Tags = LTg("remove erase nodes"), + Execute = () => CurrentCanvas.DeleteSelectedCommand.Execute(null), + }, + // ── Canvas ──────────────────────────────────────────────────────── + new() + { + Name = L("command.addNode.name", "Add Node"), + Description = L("command.addNode.description", "Open node search menu to add a node"), + Shortcut = ShortcutText(ShortcutActionIds.OpenNodeSearch, "Shift+A"), + Icon = MaterialIconKind.Plus, + Tags = LTg("create insert search transform"), + Execute = () => OpenSearch(), + }, + new() + { + Name = L("command.bringForward.name", "Bring Forward"), + Description = L("command.bringForward.description", "Move selected nodes one layer forward"), + Shortcut = ShortcutText(ShortcutActionIds.BringForward, "Ctrl+PgUp"), + Icon = MaterialIconKind.ArrangeBringForward, + Tags = LTg("layer z-order forward selected nodes"), + Execute = () => CurrentCanvas.BringSelectionForwardCommand.Execute(null), + }, + new() + { + Name = L("command.sendBackward.name", "Send Backward"), + Description = L("command.sendBackward.description", "Move selected nodes one layer backward"), + Shortcut = ShortcutText(ShortcutActionIds.SendBackward, "Ctrl+PgDown"), + Icon = MaterialIconKind.ArrangeSendBackward, + Tags = LTg("layer z-order backward selected nodes"), + Execute = () => CurrentCanvas.SendSelectionBackwardCommand.Execute(null), + }, + new() + { + Name = L("command.bringToFront.name", "Bring to Front"), + Description = L("command.bringToFront.description", "Move selected nodes to top layer"), + Shortcut = ShortcutText(ShortcutActionIds.BringToFront, "Ctrl+Shift+PgUp"), + Icon = MaterialIconKind.ArrangeBringToFront, + Tags = LTg("layer z-order front selected nodes"), + Execute = () => CurrentCanvas.BringSelectionToFrontCommand.Execute(null), + }, + new() + { + Name = L("command.sendToBack.name", "Send to Back"), + Description = L("command.sendToBack.description", "Move selected nodes to bottom layer"), + Shortcut = ShortcutText(ShortcutActionIds.SendToBack, "Ctrl+Shift+PgDown"), + Icon = MaterialIconKind.ArrangeSendToBack, + Tags = LTg("layer z-order back selected nodes"), + Execute = () => CurrentCanvas.SendSelectionToBackCommand.Execute(null), + }, + new() + { + Name = L("command.normalizeLayers.name", "Normalize Layers"), + Description = L("command.normalizeLayers.description", "Compact node layer indices to a clean 0..N order"), + Shortcut = "", + Icon = MaterialIconKind.LayersTriple, + Tags = LTg("layer z-order normalize compact"), + Execute = () => CurrentCanvas.NormalizeLayersCommand.Execute(null), + }, + new() + { + Name = LN("Zoom In"), + Description = LD("Zoom into the canvas"), + Shortcut = ShortcutText(ShortcutActionIds.ZoomIn, "Ctrl++"), + Icon = MaterialIconKind.MagnifyPlus, + Tags = LTg("magnify enlarge"), + Execute = () => CurrentCanvas.ZoomInCommand.Execute(null), + }, + new() + { + Name = LN("Zoom Out"), + Description = LD("Zoom out of the canvas"), + Shortcut = ShortcutText(ShortcutActionIds.ZoomOut, "Ctrl+-"), + Icon = MaterialIconKind.MagnifyMinus, + Tags = LTg("shrink reduce"), + Execute = () => CurrentCanvas.ZoomOutCommand.Execute(null), + }, + new() + { + Name = LN("Fit to Screen"), + Description = LD("Fit all nodes into the visible area"), + Shortcut = "", + Icon = MaterialIconKind.FitToPage, + Tags = LTg("auto layout view reset zoom"), + Execute = () => CurrentCanvas.FitToScreenCommand.Execute(null), + }, + new() + { + Name = LN("Reset Viewport"), + Description = LD("Reset zoom and pan to default"), + Shortcut = ShortcutText(ShortcutActionIds.ZoomReset, "Ctrl+0"), + Icon = MaterialIconKind.MagnifyRemoveOutline, + Tags = LTg("100 percent restore zoom pan viewport"), + Execute = () => CurrentCanvas.ResetZoomCommand.Execute(null), + }, + // ── Query / Preview ─────────────────────────────────────────────── + new() + { + ActionId = ShortcutActionIds.TogglePreview, + Name = LN("Toggle Preview"), + Description = LD("Open output preview modal for the active mode"), + Shortcut = ShortcutText(ShortcutActionIds.TogglePreview, "F3"), + Icon = MaterialIconKind.TableEye, + Tags = LTg("data results table panel"), + Execute = OpenOutputPreviewModal, + }, + new() + { + ActionId = ShortcutActionIds.RunPreview, + Name = LN("Run Preview"), + Description = LD("Execute the current query in preview"), + Shortcut = ShortcutText(ShortcutActionIds.RunPreview, "F5"), + Icon = MaterialIconKind.Play, + Tags = LTg("execute run sql query results"), + Execute = async () => + { + if (!CurrentCanvas.HasErrors && !CurrentCanvas.LiveSql.IsMutatingCommand) + await _preview.RunPreviewAsync(); + }, + }, + // ── Cleanup / Quality ───────────────────────────────────────────── + new() + { + Name = LN("Auto Layout"), + Description = LD("Arrange nodes into logical columns automatically"), + Shortcut = ShortcutText(ShortcutActionIds.AutoLayout, "Ctrl+L"), + Icon = MaterialIconKind.FormatHorizontalAlignCenter, + Tags = LTg("layout arrange columns auto organize readability"), + Execute = () => + { + CurrentCanvas.AutoLayoutCommand.Execute(null); + GetActiveCanvasControl()?.InvalidateWires(); + }, + }, + new() + { + Name = LN("Cleanup Orphans"), + Description = LD("Remove all nodes not connected to output"), + Shortcut = "", + Icon = MaterialIconKind.VectorUnion, + Tags = LTg("orphan unused disconnected clean delete nodes"), + Execute = () => CurrentCanvas.CleanupOrphansCommand.Execute(null), + }, + new() + { + Name = LN("Auto-Fix Naming"), + Description = LD("Convert aliases to the convention configured in project settings"), + Shortcut = "", + Icon = MaterialIconKind.AutoFix, + Tags = LTg("rename alias fix naming convention"), + Execute = () => CurrentCanvas.AutoFixNamingCommand.Execute(null), + }, + // ── Snap / Alignment ────────────────────────────────────────────── + new() + { + Name = LN("Toggle Snap to Grid"), + Description = LD("Snap node positions to 16px grid (Ctrl+G)"), + Shortcut = ShortcutText(ShortcutActionIds.ToggleSnapToGrid, "Ctrl+G"), + Icon = MaterialIconKind.GridLarge, + Tags = LTg("snap grid align precision position"), + Execute = () => CurrentCanvas.ToggleSnapCommand.Execute(null), + }, + new() + { + Name = LN("Align Left"), + Description = LD("Align selected nodes to the leftmost edge"), + Shortcut = "", + Icon = MaterialIconKind.AlignHorizontalLeft, + Tags = LTg("align left edge selection nodes"), + Execute = () => CurrentCanvas.AlignLeftCommand.Execute(null), + }, + new() + { + Name = LN("Align Right"), + Description = LD("Align selected nodes to the rightmost edge"), + Shortcut = "", + Icon = MaterialIconKind.AlignHorizontalRight, + Tags = LTg("align right edge selection nodes"), + Execute = () => CurrentCanvas.AlignRightCommand.Execute(null), + }, + new() + { + Name = LN("Align Top"), + Description = LD("Align selected nodes to the topmost edge"), + Shortcut = "", + Icon = MaterialIconKind.AlignVerticalTop, + Tags = LTg("align top edge selection nodes"), + Execute = () => CurrentCanvas.AlignTopCommand.Execute(null), + }, + new() + { + Name = LN("Align Bottom"), + Description = LD("Align selected nodes to the bottom edge"), + Shortcut = "", + Icon = MaterialIconKind.AlignVerticalBottom, + Tags = LTg("align bottom edge selection nodes"), + Execute = () => CurrentCanvas.AlignBottomCommand.Execute(null), + }, + new() + { + Name = LN("Center Horizontally"), + Description = LD("Centre selected nodes on a horizontal axis"), + Shortcut = "", + Icon = MaterialIconKind.AlignHorizontalCenter, + Tags = LTg("align center middle horizontal nodes"), + Execute = () => CurrentCanvas.AlignCenterHCommand.Execute(null), + }, + new() + { + Name = LN("Center Vertically"), + Description = LD("Centre selected nodes on a vertical axis"), + Shortcut = "", + Icon = MaterialIconKind.AlignVerticalCenter, + Tags = LTg("align center middle vertical nodes"), + Execute = () => CurrentCanvas.AlignCenterVCommand.Execute(null), + }, + new() + { + Name = LN("Distribute Horizontally"), + Description = LD("Spread selected nodes with equal horizontal spacing"), + Shortcut = "", + Icon = MaterialIconKind.DistributeHorizontalCenter, + Tags = LTg("distribute space equal horizontal nodes"), + Execute = () => CurrentCanvas.DistributeHCommand.Execute(null), + }, + new() + { + Name = LN("Distribute Vertically"), + Description = LD("Spread selected nodes with equal vertical spacing"), + Shortcut = "", + Icon = MaterialIconKind.DistributeVerticalCenter, + Tags = LTg("distribute space equal vertical nodes"), + Execute = () => CurrentCanvas.DistributeVCommand.Execute(null), + }, + // ── Export ──────────────────────────────────────────────────────── + new() + { + Name = LN("Export Documentation"), + Description = LD("Save Markdown documentation of the current flow"), + Shortcut = "", + Icon = MaterialIconKind.FileDocument, + Tags = LTg("export markdown doc documentation flow save md"), + Execute = async () => await _export.RunExportDocumentationAsync(), + }, + new() + { + Name = LN("Export HTML"), + Description = LD("Generate HTML file from the first HTML Export node"), + Shortcut = "", + Icon = MaterialIconKind.LanguageHtml5, + Tags = LTg("export html file output report save"), + Execute = async () => + await _export.RunExportWithDialogAsync( + NodeType.HtmlExport, + L("export.fileType.html", "HTML Files"), + "html" + ), + }, + new() + { + Name = LN("Export JSON"), + Description = LD("Generate JSON file from the first JSON Export node"), + Shortcut = "", + Icon = MaterialIconKind.CodeJson, + Tags = LTg("export json file output save"), + Execute = async () => + await _export.RunExportWithDialogAsync( + NodeType.JsonExport, + L("export.fileType.json", "JSON Files"), + "json" + ), + }, + new() + { + Name = LN("Export CSV"), + Description = LD("Generate CSV file from the first CSV Export node"), + Shortcut = "", + Icon = MaterialIconKind.FileDelimited, + Tags = LTg("export csv file tabular output save"), + Execute = async () => + await _export.RunExportWithDialogAsync( + NodeType.CsvExport, + L("export.fileType.csv", "CSV Files"), + "csv" + ), + }, + new() + { + Name = LN("Export Excel"), + Description = LD("Generate XLSX workbook from the first Excel Export node"), + Shortcut = "", + Icon = MaterialIconKind.MicrosoftExcel, + Tags = LTg("export excel xlsx file tabular output spreadsheet save"), + Execute = async () => + await _export.RunExportWithDialogAsync( + NodeType.ExcelExport, + L("export.fileType.excel", "Excel Files"), + "xlsx" + ), + }, + ]; + + private void OpenOutputPreviewModal() + { + ShellViewModel shell = CurrentShell; + + switch (shell.ActivePreviewContract.Kind) + { + case WorkspaceDocumentPreviewKind.Query: + { + CanvasViewModel queryCanvas = shell.ActiveQueryCanvasDocument ?? shell.Canvas + ?? throw new InvalidOperationException("Preview SQL indisponivel para o canvas Query atual."); + LiveSqlBarViewModel liveSql = queryCanvas.LiveSql + ?? throw new InvalidOperationException("Preview SQL indisponivel para o canvas Query atual."); + liveSql.Recompile(); + shell.OutputPreview.OpenForQuery(queryCanvas, liveSql, liveSql.ProviderLabel); + return; + } + + case WorkspaceDocumentPreviewKind.Ddl: + { + CanvasViewModel ddlCanvas = shell.EnsureDdlCanvas(); + LiveDdlBarViewModel liveDdl = ddlCanvas.LiveDdl + ?? throw new InvalidOperationException("Preview DDL indisponivel para o canvas DDL atual."); + liveDdl.Recompile(); + shell.OutputPreview.OpenForDdl(ddlCanvas, liveDdl, ddlCanvas.Provider.ToString()); + return; + } + + default: + { + var contract = shell.ActivePreviewContract; + shell.OutputPreview.OpenUnavailable(contract.Title, contract.PrimaryTabLabel, contract.UnavailableMessage); + return; + } + } + } + + private List CreateTemplateCommands() => + [ + .. QueryTemplateCatalog.LoadAll().Select(t => new PaletteCommandItem + { + Name = string.Format(L("commandPalette.templatePrefix", "Template: {0}"), t.Name), + Description = t.Description, + Icon = MaterialIconKind.ViewGrid, + Tags = $"template starter query {t.Category.ToLowerInvariant()} {t.Tags}", + Execute = () => + { + CurrentCanvas.LoadTemplate(t); + GetActiveCanvasControl()?.InvalidateWires(); + }, + }), + ]; + + private static string CreateUserTemplateName(CanvasViewModel canvas) + { + string prefix = canvas.Nodes.FirstOrDefault()?.Title ?? "Canvas"; + return $"{prefix} template {DateTime.Now:yyyyMMdd-HHmmss}"; + } + + private void OpenSearch() + { + InfiniteCanvas? canvas = GetActiveCanvasControl(); + Point ctr = canvas is not null + ? CurrentCanvas.ScreenToCanvas(new Point(canvas.Bounds.Width / 2, canvas.Bounds.Height / 2)) + : new Point(400, 300); + CurrentCanvas.SearchMenu.Open(ctr); + } + + private InfiniteCanvas? GetActiveCanvasControl() + { + if (CurrentShell.IsDdlDocumentPageActive) + return _window.FindControl("DdlDocumentPage")?.CanvasControl; + + return _window.FindControl("QueryDocumentPage")?.CanvasControl; + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } + + private static string LN(string fallback) => + L($"commandPalette.name.{fallback.ToSlugToken('_', "empty")}", fallback); + + private static string LD(string fallback) => + L($"commandPalette.description.{fallback.ToSlugToken('_', "empty")}", fallback); + + private static string LTg(string fallback) => + L($"commandPalette.tags.{fallback.ToSlugToken('_', "empty")}", fallback); + + private string ShortcutText(string actionId, string fallback) + { + ShortcutDefinition? definition = _shortcutRegistry.FindByActionId(actionId); + if (definition?.EffectiveGesture is null) + return fallback; + + return string.IsNullOrWhiteSpace(definition.EffectiveGesture.DisplayText) + ? fallback + : definition.EffectiveGesture.DisplayText; + } + + private static bool HasDdlOutput(LiveDdlBarViewModel? liveDdl) => + liveDdl is not null + && liveDdl.IsValid + && !string.IsNullOrWhiteSpace(liveDdl.RawSql); + +} diff --git a/src/DBWeaver.UI/Services/CommandPalette/CommandPaletteFilterService.cs b/src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteFilterService.cs similarity index 88% rename from src/DBWeaver.UI/Services/CommandPalette/CommandPaletteFilterService.cs rename to src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteFilterService.cs index 37d6c5b6..1c03672f 100644 --- a/src/DBWeaver.UI/Services/CommandPalette/CommandPaletteFilterService.cs +++ b/src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteFilterService.cs @@ -1,20 +1,20 @@ -namespace DBWeaver.UI.Services.CommandPalette; - -public sealed class CommandPaletteFilterService : ICommandPaletteFilterService -{ - public IReadOnlyList FilterAndSort( - IEnumerable commands, - string query) - { - string q = query.Trim(); - - return commands - .Select(c => (Command: c, Score: FuzzyScorer.Score(c, q))) - .Where(x => x.Score > 0) - .OrderByDescending(x => x.Score) - .ThenBy(x => x.Command.Name) - .Select(x => x.Command) - .ToList(); - } -} - +namespace AkkornStudio.UI.Services.CommandPalette; + +public sealed class CommandPaletteFilterService : ICommandPaletteFilterService +{ + public IReadOnlyList FilterAndSort( + IEnumerable commands, + string query) + { + string q = query.Trim(); + + return commands + .Select(c => (Command: c, Score: FuzzyScorer.Score(c, q))) + .Where(x => x.Score > 0) + .OrderByDescending(x => x.Score) + .ThenBy(x => x.Command.Name) + .Select(x => x.Command) + .ToList(); + } +} + diff --git a/src/DBWeaver.UI/Services/CommandPalette/CommandPaletteService.cs b/src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteService.cs similarity index 83% rename from src/DBWeaver.UI/Services/CommandPalette/CommandPaletteService.cs rename to src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteService.cs index 9e05de41..35c21065 100644 --- a/src/DBWeaver.UI/Services/CommandPalette/CommandPaletteService.cs +++ b/src/AkkornStudio.UI/Services/CommandPalette/CommandPaletteService.cs @@ -1,21 +1,21 @@ -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services.CommandPalette; - -public sealed class CommandPaletteService : ICommandPaletteService -{ - private readonly CommandPaletteFactory _factory; - - public CommandPaletteService(CommandPaletteFactory factory, CommandPaletteViewModel? viewModel = null) - { - _factory = factory; - ViewModel = viewModel ?? new CommandPaletteViewModel(); - } - - public CommandPaletteViewModel ViewModel { get; } - - public void Refresh() - { - ViewModel.SetCommands(_factory.CreateAllCommands()); - } -} +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services.CommandPalette; + +public sealed class CommandPaletteService : ICommandPaletteService +{ + private readonly CommandPaletteFactory _factory; + + public CommandPaletteService(CommandPaletteFactory factory, CommandPaletteViewModel? viewModel = null) + { + _factory = factory; + ViewModel = viewModel ?? new CommandPaletteViewModel(); + } + + public CommandPaletteViewModel ViewModel { get; } + + public void Refresh() + { + ViewModel.SetCommands(_factory.CreateAllCommands()); + } +} diff --git a/src/DBWeaver.UI/Services/CommandPalette/FuzzyScorer.cs b/src/AkkornStudio.UI/Services/CommandPalette/FuzzyScorer.cs similarity index 76% rename from src/DBWeaver.UI/Services/CommandPalette/FuzzyScorer.cs rename to src/AkkornStudio.UI/Services/CommandPalette/FuzzyScorer.cs index 7790e02f..cb9bc4c2 100644 --- a/src/DBWeaver.UI/Services/CommandPalette/FuzzyScorer.cs +++ b/src/AkkornStudio.UI/Services/CommandPalette/FuzzyScorer.cs @@ -1,17 +1,17 @@ -using DBWeaver.UI.Services.Search; - -namespace DBWeaver.UI.Services.CommandPalette; - -internal static class FuzzyScorer -{ - private static readonly TextSearchService Search = new(); - - public static int Score(PaletteCommandItem item, string query) - { - if (string.IsNullOrEmpty(query)) - return 1; - - return Search.Score(query, item.Name, item.Description, item.Tags, item.Shortcut); - } -} - +using AkkornStudio.UI.Services.Search; + +namespace AkkornStudio.UI.Services.CommandPalette; + +internal static class FuzzyScorer +{ + private static readonly TextSearchService Search = new(); + + public static int Score(PaletteCommandItem item, string query) + { + if (string.IsNullOrEmpty(query)) + return 1; + + return Search.Score(query, item.Name, item.Description, item.Tags, item.Shortcut); + } +} + diff --git a/src/DBWeaver.UI/Services/CommandPalette/ICommandPaletteFilterService.cs b/src/AkkornStudio.UI/Services/CommandPalette/ICommandPaletteFilterService.cs similarity index 75% rename from src/DBWeaver.UI/Services/CommandPalette/ICommandPaletteFilterService.cs rename to src/AkkornStudio.UI/Services/CommandPalette/ICommandPaletteFilterService.cs index 196632b4..433edd65 100644 --- a/src/DBWeaver.UI/Services/CommandPalette/ICommandPaletteFilterService.cs +++ b/src/AkkornStudio.UI/Services/CommandPalette/ICommandPaletteFilterService.cs @@ -1,9 +1,9 @@ -namespace DBWeaver.UI.Services.CommandPalette; - -public interface ICommandPaletteFilterService -{ - IReadOnlyList FilterAndSort( - IEnumerable commands, - string query); -} - +namespace AkkornStudio.UI.Services.CommandPalette; + +public interface ICommandPaletteFilterService +{ + IReadOnlyList FilterAndSort( + IEnumerable commands, + string query); +} + diff --git a/src/DBWeaver.UI/Services/CommandPalette/ICommandPaletteService.cs b/src/AkkornStudio.UI/Services/CommandPalette/ICommandPaletteService.cs similarity index 56% rename from src/DBWeaver.UI/Services/CommandPalette/ICommandPaletteService.cs rename to src/AkkornStudio.UI/Services/CommandPalette/ICommandPaletteService.cs index b46e5f77..c61b25e1 100644 --- a/src/DBWeaver.UI/Services/CommandPalette/ICommandPaletteService.cs +++ b/src/AkkornStudio.UI/Services/CommandPalette/ICommandPaletteService.cs @@ -1,9 +1,9 @@ -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services.CommandPalette; - -public interface ICommandPaletteService -{ - CommandPaletteViewModel ViewModel { get; } - void Refresh(); -} +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services.CommandPalette; + +public interface ICommandPaletteService +{ + CommandPaletteViewModel ViewModel { get; } + void Refresh(); +} diff --git a/src/DBWeaver.UI/Services/Connection/ConnectionWorkspaceModule.cs b/src/AkkornStudio.UI/Services/Connection/ConnectionWorkspaceModule.cs similarity index 93% rename from src/DBWeaver.UI/Services/Connection/ConnectionWorkspaceModule.cs rename to src/AkkornStudio.UI/Services/Connection/ConnectionWorkspaceModule.cs index 0c47d24e..faca0736 100644 --- a/src/DBWeaver.UI/Services/Connection/ConnectionWorkspaceModule.cs +++ b/src/AkkornStudio.UI/Services/Connection/ConnectionWorkspaceModule.cs @@ -1,53 +1,53 @@ -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services.Connection; - -/// -/// Reusable coordinator for opening and activating connection workflows from different app entry points. -/// -public sealed class ConnectionWorkspaceModule -{ - private readonly Func _getConnectionManager; - private readonly Action _activateConnectionSidebar; - private readonly Action _enterCanvas; - - public ConnectionWorkspaceModule( - Func getConnectionManager, - Action activateConnectionSidebar, - Action enterCanvas) - { - _getConnectionManager = getConnectionManager; - _activateConnectionSidebar = activateConnectionSidebar; - _enterCanvas = enterCanvas; - } - - public void OpenManager(bool beginNewProfile, bool keepStartVisible) - { - ConnectionManagerViewModel manager = _getConnectionManager(); - - _activateConnectionSidebar(); - manager.Open(); - - if (beginNewProfile) - manager.NewProfileCommand.Execute(null); - - if (!keepStartVisible) - _enterCanvas(); - } - - public bool ConnectFromStartItem(string profileId) - { - if (string.IsNullOrWhiteSpace(profileId)) - return false; - - ConnectionManagerViewModel manager = _getConnectionManager(); - ConnectionProfile? profile = manager.Profiles.FirstOrDefault(p => - string.Equals(p.Id, profileId, StringComparison.OrdinalIgnoreCase) - ); - - if (profile is null) - return false; - +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services.Connection; + +/// +/// Reusable coordinator for opening and activating connection workflows from different app entry points. +/// +public sealed class ConnectionWorkspaceModule +{ + private readonly Func _getConnectionManager; + private readonly Action _activateConnectionSidebar; + private readonly Action _enterCanvas; + + public ConnectionWorkspaceModule( + Func getConnectionManager, + Action activateConnectionSidebar, + Action enterCanvas) + { + _getConnectionManager = getConnectionManager; + _activateConnectionSidebar = activateConnectionSidebar; + _enterCanvas = enterCanvas; + } + + public void OpenManager(bool beginNewProfile, bool keepStartVisible) + { + ConnectionManagerViewModel manager = _getConnectionManager(); + + _activateConnectionSidebar(); + manager.Open(); + + if (beginNewProfile) + manager.NewProfileCommand.Execute(null); + + if (!keepStartVisible) + _enterCanvas(); + } + + public bool ConnectFromStartItem(string profileId) + { + if (string.IsNullOrWhiteSpace(profileId)) + return false; + + ConnectionManagerViewModel manager = _getConnectionManager(); + ConnectionProfile? profile = manager.Profiles.FirstOrDefault(p => + string.Equals(p.Id, profileId, StringComparison.OrdinalIgnoreCase) + ); + + if (profile is null) + return false; + _activateConnectionSidebar(); manager.Open(); manager.SelectedProfile = profile; diff --git a/src/DBWeaver.UI/Services/Connection/CredentialProtector.cs b/src/AkkornStudio.UI/Services/Connection/CredentialProtector.cs similarity index 96% rename from src/DBWeaver.UI/Services/Connection/CredentialProtector.cs rename to src/AkkornStudio.UI/Services/Connection/CredentialProtector.cs index 6284075d..5e8a8f0f 100644 --- a/src/DBWeaver.UI/Services/Connection/CredentialProtector.cs +++ b/src/AkkornStudio.UI/Services/Connection/CredentialProtector.cs @@ -1,344 +1,344 @@ -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using DBWeaver.UI.Services.Localization; - -namespace DBWeaver.UI.Services.Connection; - -/// -/// Cross-platform credential protection. -/// Encrypted values are stored as "enc:<base64>" so that legacy plaintext -/// profiles are detected and transparently migrated on the next save. -/// -/// Encryption: AES-256-GCM with a key derived from the current machine name -/// and OS user name via HKDF-SHA-256. This ties the ciphertext to this -/// user account on this machine — the same security boundary as OS-level -/// user-scope DPAPI on Windows — without requiring platform-specific APIs. -/// -public static class CredentialProtector -{ - private static readonly ILogger _logger = NullLogger.Instance; - private const string Prefix = "enc:"; - private const string DpapiPrefix = "dpapi:"; - - // AES-GCM constants (fixed by the NIST standard; hardcoding for clarity) - private const int NonceLength = 12; - private const int TagLength = 16; - private const int KeyLength = 32; // AES-256 - private const int InstallationSecretLength = 32; - - private static readonly Lazy _installationSecret = new(LoadOrCreateInstallationSecret); - private static readonly Lazy _activeProtector = new(CreateDefaultProtector); - - /// - /// Encrypts and returns an "enc:…" string. - /// Returns unchanged if it is null/empty. - /// Throws if encryption fails — callers must not silently store plaintext as fallback. - /// - public static string Protect(string plaintext) - { - if (string.IsNullOrEmpty(plaintext)) - return plaintext; - return _activeProtector.Value.Protect(plaintext); - } - - /// - /// Decrypts an "enc:…" value produced by . - /// If does not start with the prefix it is returned - /// as-is (backward-compatible with legacy plaintext profiles). - /// Returns empty string if the ciphertext is corrupt. - /// - public static string Unprotect(string value) - { - if (string.IsNullOrEmpty(value)) - return value; - - // Legacy plaintext — no prefix means it was saved before this feature - if (!value.StartsWith(Prefix, StringComparison.Ordinal) && !value.StartsWith(DpapiPrefix, StringComparison.Ordinal)) - return value; - - try - { - if (value.StartsWith(DpapiPrefix, StringComparison.Ordinal)) - { - byte[] cipherBlob = Convert.FromBase64String(value[DpapiPrefix.Length..]); - return WindowsDpapiSecretProtector.UnprotectBytes(cipherBlob); - } - - byte[] aesBlob = Convert.FromBase64String(value[Prefix.Length..]); - return Encoding.UTF8.GetString(AesDecrypt(aesBlob)); - } - catch - { - // Corrupt or tampered ciphertext — return empty rather than a garbage string - return string.Empty; - } - } - - // ── AES-256-GCM ────────────────────────────────────────────────────────── - - private static byte[] AesEncrypt(byte[] plaintext) - { - byte[] key = DeriveKey(); - byte[] nonce = new byte[NonceLength]; - RandomNumberGenerator.Fill(nonce); - - byte[] ciphertext = new byte[plaintext.Length]; - byte[] tag = new byte[TagLength]; - - using var aes = new AesGcm(key, TagLength); - aes.Encrypt(nonce, plaintext, ciphertext, tag); - - // Wire format: [nonce (12 B)] | [tag (16 B)] | [ciphertext (n B)] - byte[] blob = new byte[NonceLength + TagLength + ciphertext.Length]; - nonce .CopyTo(blob, 0); - tag .CopyTo(blob, NonceLength); - ciphertext.CopyTo(blob, NonceLength + TagLength); - return blob; - } - - private static byte[] AesDecrypt(byte[] blob) - { - if (blob.Length < NonceLength + TagLength) - throw new CryptographicException( - L("credential.error.ciphertextTooShort", "Ciphertext blob is too short.") - ); - - byte[] key = DeriveKey(); - byte[] nonce = blob[..NonceLength]; - byte[] tag = blob[NonceLength..(NonceLength + TagLength)]; - byte[] ciphertext = blob[(NonceLength + TagLength)..]; - byte[] plaintext = new byte[ciphertext.Length]; - - using var aes = new AesGcm(key, TagLength); - aes.Decrypt(nonce, ciphertext, tag, plaintext); - return plaintext; - } - - private static ISecretProtector CreateDefaultProtector() - { - if (OperatingSystem.IsWindows()) - return new WindowsDpapiSecretProtector(); - return new PortableAesSecretProtector(); - } - - // ── Key derivation ──────────────────────────────────────────────────────── - - /// - /// Derives a 256-bit AES key from the current OS user identity (machine name - /// + user name). This means the encrypted value is only decryptable by the - /// same OS user on the same machine — equivalent to user-scope DPAPI. - /// - private static byte[] DeriveKey() - { - string material = $"{Environment.MachineName}\0{Environment.UserName}\0{AppConstants.AppName}"; - byte[] materialBytes = Encoding.UTF8.GetBytes(material); - byte[] ikmInput = new byte[materialBytes.Length + _installationSecret.Value.Length]; - Buffer.BlockCopy(materialBytes, 0, ikmInput, 0, materialBytes.Length); - Buffer.BlockCopy(_installationSecret.Value, 0, ikmInput, materialBytes.Length, _installationSecret.Value.Length); - - byte[] ikm = SHA256.HashData(ikmInput); - byte[] salt = "VSA.CredentialStore.v1"u8.ToArray(); - return HKDF.DeriveKey(HashAlgorithmName.SHA256, ikm, KeyLength, salt); - } - - private static byte[] LoadOrCreateInstallationSecret() - { - string dir = AppConstants.AppDataDirectory; - string path = Path.Combine(dir, ".credential_salt.bin"); - - // Existing file: reading must succeed — returning random bytes here would silently - // invalidate every credential previously encrypted with the persisted key. - if (File.Exists(path)) - { - byte[] existing = File.ReadAllBytes(path); - if (existing.Length == InstallationSecretLength) - return existing; - } - - // No valid file yet — this is a fresh install or the file was removed. - // If the write fails, an ephemeral key is acceptable: there are no prior - // encrypted credentials to invalidate on a first-run scenario. - try - { - Directory.CreateDirectory(dir); - byte[] secret = RandomNumberGenerator.GetBytes(InstallationSecretLength); - File.WriteAllBytes(path, secret); - return secret; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "[CredentialProtector] Failed to persist installation secret. Encrypted credentials will not survive restarts."); - return RandomNumberGenerator.GetBytes(InstallationSecretLength); - } - } - - private interface ISecretProtector - { - string Protect(string plaintext); - } - - private sealed class WindowsDpapiSecretProtector : ISecretProtector - { - public string Protect(string plaintext) - { - byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext); - byte[] protectedBytes = ProtectBytes(plaintextBytes); - return DpapiPrefix + Convert.ToBase64String(protectedBytes); - } - - private static byte[] ProtectBytes(byte[] plaintextBytes) - { - if (!OperatingSystem.IsWindows()) - throw new PlatformNotSupportedException( - L("credential.error.dpapiWindowsOnly", "DPAPI is only available on Windows.") - ); - - return ProtectedData.Protect(plaintextBytes, optionalEntropy: null, DataProtectionScope.CurrentUser); - } - - public static string UnprotectBytes(byte[] protectedBytes) - { - if (!OperatingSystem.IsWindows()) - throw new PlatformNotSupportedException( - L("credential.error.dpapiWindowsOnly", "DPAPI is only available on Windows.") - ); - - byte[] plaintext = ProtectedData.Unprotect(protectedBytes, optionalEntropy: null, DataProtectionScope.CurrentUser); - return Encoding.UTF8.GetString(plaintext); - } - } - - private sealed class PortableAesSecretProtector : ISecretProtector - { - public string Protect(string plaintext) - { - byte[] cipherBlob = AesEncrypt(Encoding.UTF8.GetBytes(plaintext)); - return Prefix + Convert.ToBase64String(cipherBlob); - } - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } -} - -/// -/// Local credential vault that stores per-profile secrets outside connections.json. -/// Payload is protected using and can be migrated -/// transparently between protection schemes. -/// -public sealed class CredentialVaultStore -{ - private readonly string _vaultPath; - private static readonly ILogger _logger = NullLogger.Instance; - public static event Action? WarningRaised; - - public CredentialVaultStore(string? appDataRoot = null) - { - string baseDir = string.IsNullOrWhiteSpace(appDataRoot) - ? AppConstants.AppDataDirectory - : Path.Combine(appDataRoot, AppConstants.AppName); - - _vaultPath = Path.Combine(baseDir, "credentials.vault.json"); - } - - public void SaveSecret(string profileId, string? secret) - { - if (string.IsNullOrWhiteSpace(profileId)) - return; - - Dictionary entries = LoadRawEntries(); - if (string.IsNullOrEmpty(secret)) - { - entries.Remove(profileId); - } - else - { - entries[profileId] = CredentialProtector.Protect(secret); - } - - PersistRawEntries(entries); - } - - public string? TryGetSecret(string profileId) - { - if (string.IsNullOrWhiteSpace(profileId)) - return null; - - Dictionary entries = LoadRawEntries(); - if (!entries.TryGetValue(profileId, out string? protectedSecret) || string.IsNullOrWhiteSpace(protectedSecret)) - return null; - - string unprotected = CredentialProtector.Unprotect(protectedSecret); - return string.IsNullOrEmpty(unprotected) ? null : unprotected; - } - - public void RemoveSecret(string profileId) - { - if (string.IsNullOrWhiteSpace(profileId)) - return; - - Dictionary entries = LoadRawEntries(); - if (!entries.Remove(profileId)) - return; - - PersistRawEntries(entries); - } - - internal Dictionary LoadRawEntries() - { - try - { - if (!File.Exists(_vaultPath)) - return new Dictionary(StringComparer.Ordinal); - - string json = File.ReadAllText(_vaultPath); - return JsonSerializer.Deserialize>(json) - ?? new Dictionary(StringComparer.Ordinal); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to load credential vault from {VaultPath}", _vaultPath); - WarningRaised?.Invoke( - string.Format( - L("credential.warning.loadVaultFailed", "Failed to load credential vault '{0}': {1}"), - _vaultPath, - ex.Message - ) - ); - return new Dictionary(StringComparer.Ordinal); - } - } - - private void PersistRawEntries(Dictionary entries) - { - try - { - Directory.CreateDirectory(Path.GetDirectoryName(_vaultPath)!); - string json = JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(_vaultPath, json); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to persist credential vault to {VaultPath}", _vaultPath); - WarningRaised?.Invoke( - string.Format( - L("credential.warning.persistVaultFailed", "Failed to persist credential vault '{0}': {1}"), - _vaultPath, - ex.Message - ) - ); - } - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } -} +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using AkkornStudio.UI.Services.Localization; + +namespace AkkornStudio.UI.Services.Connection; + +/// +/// Cross-platform credential protection. +/// Encrypted values are stored as "enc:<base64>" so that legacy plaintext +/// profiles are detected and transparently migrated on the next save. +/// +/// Encryption: AES-256-GCM with a key derived from the current machine name +/// and OS user name via HKDF-SHA-256. This ties the ciphertext to this +/// user account on this machine — the same security boundary as OS-level +/// user-scope DPAPI on Windows — without requiring platform-specific APIs. +/// +public static class CredentialProtector +{ + private static readonly ILogger _logger = NullLogger.Instance; + private const string Prefix = "enc:"; + private const string DpapiPrefix = "dpapi:"; + + // AES-GCM constants (fixed by the NIST standard; hardcoding for clarity) + private const int NonceLength = 12; + private const int TagLength = 16; + private const int KeyLength = 32; // AES-256 + private const int InstallationSecretLength = 32; + + private static readonly Lazy _installationSecret = new(LoadOrCreateInstallationSecret); + private static readonly Lazy _activeProtector = new(CreateDefaultProtector); + + /// + /// Encrypts and returns an "enc:…" string. + /// Returns unchanged if it is null/empty. + /// Throws if encryption fails — callers must not silently store plaintext as fallback. + /// + public static string Protect(string plaintext) + { + if (string.IsNullOrEmpty(plaintext)) + return plaintext; + return _activeProtector.Value.Protect(plaintext); + } + + /// + /// Decrypts an "enc:…" value produced by . + /// If does not start with the prefix it is returned + /// as-is (backward-compatible with legacy plaintext profiles). + /// Returns empty string if the ciphertext is corrupt. + /// + public static string Unprotect(string value) + { + if (string.IsNullOrEmpty(value)) + return value; + + // Legacy plaintext — no prefix means it was saved before this feature + if (!value.StartsWith(Prefix, StringComparison.Ordinal) && !value.StartsWith(DpapiPrefix, StringComparison.Ordinal)) + return value; + + try + { + if (value.StartsWith(DpapiPrefix, StringComparison.Ordinal)) + { + byte[] cipherBlob = Convert.FromBase64String(value[DpapiPrefix.Length..]); + return WindowsDpapiSecretProtector.UnprotectBytes(cipherBlob); + } + + byte[] aesBlob = Convert.FromBase64String(value[Prefix.Length..]); + return Encoding.UTF8.GetString(AesDecrypt(aesBlob)); + } + catch + { + // Corrupt or tampered ciphertext — return empty rather than a garbage string + return string.Empty; + } + } + + // ── AES-256-GCM ────────────────────────────────────────────────────────── + + private static byte[] AesEncrypt(byte[] plaintext) + { + byte[] key = DeriveKey(); + byte[] nonce = new byte[NonceLength]; + RandomNumberGenerator.Fill(nonce); + + byte[] ciphertext = new byte[plaintext.Length]; + byte[] tag = new byte[TagLength]; + + using var aes = new AesGcm(key, TagLength); + aes.Encrypt(nonce, plaintext, ciphertext, tag); + + // Wire format: [nonce (12 B)] | [tag (16 B)] | [ciphertext (n B)] + byte[] blob = new byte[NonceLength + TagLength + ciphertext.Length]; + nonce .CopyTo(blob, 0); + tag .CopyTo(blob, NonceLength); + ciphertext.CopyTo(blob, NonceLength + TagLength); + return blob; + } + + private static byte[] AesDecrypt(byte[] blob) + { + if (blob.Length < NonceLength + TagLength) + throw new CryptographicException( + L("credential.error.ciphertextTooShort", "Ciphertext blob is too short.") + ); + + byte[] key = DeriveKey(); + byte[] nonce = blob[..NonceLength]; + byte[] tag = blob[NonceLength..(NonceLength + TagLength)]; + byte[] ciphertext = blob[(NonceLength + TagLength)..]; + byte[] plaintext = new byte[ciphertext.Length]; + + using var aes = new AesGcm(key, TagLength); + aes.Decrypt(nonce, ciphertext, tag, plaintext); + return plaintext; + } + + private static ISecretProtector CreateDefaultProtector() + { + if (OperatingSystem.IsWindows()) + return new WindowsDpapiSecretProtector(); + return new PortableAesSecretProtector(); + } + + // ── Key derivation ──────────────────────────────────────────────────────── + + /// + /// Derives a 256-bit AES key from the current OS user identity (machine name + /// + user name). This means the encrypted value is only decryptable by the + /// same OS user on the same machine — equivalent to user-scope DPAPI. + /// + private static byte[] DeriveKey() + { + string material = $"{Environment.MachineName}\0{Environment.UserName}\0{AppConstants.AppName}"; + byte[] materialBytes = Encoding.UTF8.GetBytes(material); + byte[] ikmInput = new byte[materialBytes.Length + _installationSecret.Value.Length]; + Buffer.BlockCopy(materialBytes, 0, ikmInput, 0, materialBytes.Length); + Buffer.BlockCopy(_installationSecret.Value, 0, ikmInput, materialBytes.Length, _installationSecret.Value.Length); + + byte[] ikm = SHA256.HashData(ikmInput); + byte[] salt = "VSA.CredentialStore.v1"u8.ToArray(); + return HKDF.DeriveKey(HashAlgorithmName.SHA256, ikm, KeyLength, salt); + } + + private static byte[] LoadOrCreateInstallationSecret() + { + string dir = AppConstants.AppDataDirectory; + string path = Path.Combine(dir, ".credential_salt.bin"); + + // Existing file: reading must succeed — returning random bytes here would silently + // invalidate every credential previously encrypted with the persisted key. + if (File.Exists(path)) + { + byte[] existing = File.ReadAllBytes(path); + if (existing.Length == InstallationSecretLength) + return existing; + } + + // No valid file yet — this is a fresh install or the file was removed. + // If the write fails, an ephemeral key is acceptable: there are no prior + // encrypted credentials to invalidate on a first-run scenario. + try + { + Directory.CreateDirectory(dir); + byte[] secret = RandomNumberGenerator.GetBytes(InstallationSecretLength); + File.WriteAllBytes(path, secret); + return secret; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "[CredentialProtector] Failed to persist installation secret. Encrypted credentials will not survive restarts."); + return RandomNumberGenerator.GetBytes(InstallationSecretLength); + } + } + + private interface ISecretProtector + { + string Protect(string plaintext); + } + + private sealed class WindowsDpapiSecretProtector : ISecretProtector + { + public string Protect(string plaintext) + { + byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext); + byte[] protectedBytes = ProtectBytes(plaintextBytes); + return DpapiPrefix + Convert.ToBase64String(protectedBytes); + } + + private static byte[] ProtectBytes(byte[] plaintextBytes) + { + if (!OperatingSystem.IsWindows()) + throw new PlatformNotSupportedException( + L("credential.error.dpapiWindowsOnly", "DPAPI is only available on Windows.") + ); + + return ProtectedData.Protect(plaintextBytes, optionalEntropy: null, DataProtectionScope.CurrentUser); + } + + public static string UnprotectBytes(byte[] protectedBytes) + { + if (!OperatingSystem.IsWindows()) + throw new PlatformNotSupportedException( + L("credential.error.dpapiWindowsOnly", "DPAPI is only available on Windows.") + ); + + byte[] plaintext = ProtectedData.Unprotect(protectedBytes, optionalEntropy: null, DataProtectionScope.CurrentUser); + return Encoding.UTF8.GetString(plaintext); + } + } + + private sealed class PortableAesSecretProtector : ISecretProtector + { + public string Protect(string plaintext) + { + byte[] cipherBlob = AesEncrypt(Encoding.UTF8.GetBytes(plaintext)); + return Prefix + Convert.ToBase64String(cipherBlob); + } + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } +} + +/// +/// Local credential vault that stores per-profile secrets outside connections.json. +/// Payload is protected using and can be migrated +/// transparently between protection schemes. +/// +public sealed class CredentialVaultStore +{ + private readonly string _vaultPath; + private static readonly ILogger _logger = NullLogger.Instance; + public static event Action? WarningRaised; + + public CredentialVaultStore(string? appDataRoot = null) + { + string baseDir = string.IsNullOrWhiteSpace(appDataRoot) + ? AppConstants.AppDataDirectory + : Path.Combine(appDataRoot, AppConstants.AppName); + + _vaultPath = Path.Combine(baseDir, "credentials.vault.json"); + } + + public void SaveSecret(string profileId, string? secret) + { + if (string.IsNullOrWhiteSpace(profileId)) + return; + + Dictionary entries = LoadRawEntries(); + if (string.IsNullOrEmpty(secret)) + { + entries.Remove(profileId); + } + else + { + entries[profileId] = CredentialProtector.Protect(secret); + } + + PersistRawEntries(entries); + } + + public string? TryGetSecret(string profileId) + { + if (string.IsNullOrWhiteSpace(profileId)) + return null; + + Dictionary entries = LoadRawEntries(); + if (!entries.TryGetValue(profileId, out string? protectedSecret) || string.IsNullOrWhiteSpace(protectedSecret)) + return null; + + string unprotected = CredentialProtector.Unprotect(protectedSecret); + return string.IsNullOrEmpty(unprotected) ? null : unprotected; + } + + public void RemoveSecret(string profileId) + { + if (string.IsNullOrWhiteSpace(profileId)) + return; + + Dictionary entries = LoadRawEntries(); + if (!entries.Remove(profileId)) + return; + + PersistRawEntries(entries); + } + + internal Dictionary LoadRawEntries() + { + try + { + if (!File.Exists(_vaultPath)) + return new Dictionary(StringComparer.Ordinal); + + string json = File.ReadAllText(_vaultPath); + return JsonSerializer.Deserialize>(json) + ?? new Dictionary(StringComparer.Ordinal); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load credential vault from {VaultPath}", _vaultPath); + WarningRaised?.Invoke( + string.Format( + L("credential.warning.loadVaultFailed", "Failed to load credential vault '{0}': {1}"), + _vaultPath, + ex.Message + ) + ); + return new Dictionary(StringComparer.Ordinal); + } + } + + private void PersistRawEntries(Dictionary entries) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(_vaultPath)!); + string json = JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(_vaultPath, json); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to persist credential vault to {VaultPath}", _vaultPath); + WarningRaised?.Invoke( + string.Format( + L("credential.warning.persistVaultFailed", "Failed to persist credential vault '{0}': {1}"), + _vaultPath, + ex.Message + ) + ); + } + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } +} diff --git a/src/DBWeaver.UI/Services/Connection/DatabaseConnectionService.cs b/src/AkkornStudio.UI/Services/Connection/DatabaseConnectionService.cs similarity index 95% rename from src/DBWeaver.UI/Services/Connection/DatabaseConnectionService.cs rename to src/AkkornStudio.UI/Services/Connection/DatabaseConnectionService.cs index eba37ed6..77069925 100644 --- a/src/DBWeaver.UI/Services/Connection/DatabaseConnectionService.cs +++ b/src/AkkornStudio.UI/Services/Connection/DatabaseConnectionService.cs @@ -1,328 +1,328 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using System.Data; -using DBWeaver; -using DBWeaver.Core; -using DBWeaver.Metadata; -using DBWeaver.Nodes; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; - -namespace DBWeaver.UI.Services.Connection; - -/// -/// Orchestrates the database connection workflow: -/// 1. Establishes connection to the database -/// 2. Fetches and introspects database tables -/// 3. Loads tables into the search menu for quick access -/// 4. Initializes metadata service for auto-join suggestions -/// -/// This service ensures that after a successful database connection, -/// the normal flow of operations is triggered automatically. -/// -public sealed class DatabaseConnectionService : IDisposable -{ - private readonly ILogger _logger; - private MetadataService? _metadataService; - private CancellationTokenSource? _operationCts; - private DbMetadata? _loadedMetadata; - private ConnectionConfig? _activeConfig; - private bool _disposed; - - /// - /// The most recently loaded database metadata. - /// Null if no database has been connected or loading failed. - /// - public DbMetadata? LoadedMetadata => _loadedMetadata; - - public DatabaseConnectionService(ILogger? logger = null) - { - _logger = logger ?? NullLogger.Instance; - } - - /// - /// Executes the complete database connection workflow: - /// - Tests connection - /// - Loads schema metadata - /// - Populates search menu with tables - /// - Initializes metadata service for auto-join detection - /// - public async Task ConnectAndLoadAsync( - ConnectionConfig config, - SearchMenuViewModel searchMenu, - CancellationToken ct = default - ) - { - // Cancel any previous operation - _operationCts?.Cancel(); - _operationCts = CancellationTokenSource.CreateLinkedTokenSource(ct); - CancellationToken linked = _operationCts.Token; - - try - { - _logger.LogInformation( - "[DatabaseConnectionService] Starting connection workflow for {Provider} @ {Host}:{Port}/{Database}", - config.Provider, - config.Host, - config.Port, - config.Database - ); - - // Step 1: Initialize the metadata service - _metadataService = MetadataService.Create(config); - _activeConfig = config; - - // Step 2: Fetch complete database schema - _logger.LogInformation("[DatabaseConnectionService] Fetching database schema..."); - var metadata = await _metadataService.GetMetadataAsync(forceRefresh: true, ct: linked); - - // Store the loaded metadata for later access - _loadedMetadata = metadata; - - _logger.LogInformation( - "[DatabaseConnectionService] Schema loaded: {Tables} tables, {Views} views, {FKs} foreign keys", - metadata.TotalTables, - metadata.TotalViews, - metadata.TotalForeignKeys - ); - - // Step 3: Convert metadata to SearchMenu format and load - var tables = ConvertMetadataToTableList(metadata); - _logger.LogInformation( - "[DatabaseConnectionService] Loading {Count} tables into search menu", - tables.Count - ); - - searchMenu.LoadTables(tables); - - _logger.LogInformation( - "[DatabaseConnectionService] Database connection workflow completed successfully" - ); - } - catch (OperationCanceledException) - { - _logger.LogInformation("[DatabaseConnectionService] Connection workflow cancelled"); - throw; - } - catch (Exception ex) - { - _logger.LogError( - ex, - "[DatabaseConnectionService] Error during connection workflow: {Message}", - ex.Message - ); - throw; - } - } - - /// - /// Converts DbMetadata to the tuple format expected by SearchMenuViewModel.LoadTables(). - /// Flattens all schemas and returns (FullName, Columns) pairs. - /// - private static List<(string FullName, IReadOnlyList<(string Name, PinDataType Type)> Cols)> - ConvertMetadataToTableList(DbMetadata metadata) - { - var result = new List<(string, IReadOnlyList<(string, PinDataType)>)>(); - - foreach (var schema in metadata.Schemas) - { - foreach (var table in schema.Tables) - { - // Build full name: schema.tablename - string fullName = $"{table.Schema}.{table.Name}"; - - // Convert columns to (Name, PinDataType) tuples - var columns = table.Columns - .OrderBy(c => c.OrdinalPosition) - .Select(c => ( - Name: c.Name, - Type: MapDataTypeToPinDataType(c) - )) - .ToList(); - - result.Add((fullName, columns.AsReadOnly())); - } - } - - return result; - } - - /// - /// Maps database column semantics to UI pin data types. - /// Used for proper icon and color rendering in the canvas. - /// - private static PinDataType MapDataTypeToPinDataType(ColumnMetadata column) - { - return column.SemanticType switch - { - ColumnSemanticType.Numeric => PinDataType.Number, - ColumnSemanticType.Text => PinDataType.Text, - ColumnSemanticType.DateTime => PinDataType.DateTime, - ColumnSemanticType.Boolean => PinDataType.Boolean, - ColumnSemanticType.Guid => PinDataType.Text, // GUIDs rendered as text in pins - ColumnSemanticType.Document => PinDataType.Json, - ColumnSemanticType.Binary => PinDataType.Text, // Binary data rendered as text - ColumnSemanticType.Spatial => PinDataType.Json, // Spatial types as JSON - ColumnSemanticType.Other => PinDataType.Text, - _ => PinDataType.Text, - }; - } - - /// - /// Returns the active metadata service if connected, or null if not. - /// Used for features like auto-join detection that require database schema. - /// - public MetadataService? GetActiveMetadataService() => _metadataService; - - /// - /// Lists available databases/schemas for the active connection when provider supports it. - /// - public async Task ListDatabasesAsync(CancellationToken ct = default) - { - if (_activeConfig is null) - return []; - - if (_activeConfig.Provider == DatabaseProvider.SQLite) - return [Path.GetFileNameWithoutExtension(_activeConfig.Database)]; - - string sql = _activeConfig.Provider switch - { - DatabaseProvider.Postgres => "SELECT datname FROM pg_database WHERE datallowconn = TRUE ORDER BY datname;", - DatabaseProvider.MySql => "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY SCHEMA_NAME;", - DatabaseProvider.SqlServer => "SELECT name FROM sys.databases WHERE state = 0 ORDER BY name;", - _ => string.Empty, - }; - - if (string.IsNullOrWhiteSpace(sql)) - return []; - - try - { - DataTable? table = await ExecutePreviewAsTableAsync(_activeConfig, sql, 10000, ct); - if (table is null || table.Columns.Count == 0) - return []; - - string[] names = table.Rows - .Cast() - .Select(r => r[0]?.ToString()) - .Where(v => !string.IsNullOrWhiteSpace(v)) - .Select(v => v!.Trim()) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - return names; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to list databases for provider {Provider}", _activeConfig.Provider); - return []; - } - } - - /// - /// Switches the active database by updating the active connection config and validating connectivity. - /// - public async Task SwitchDatabaseAsync(string databaseName, CancellationToken ct = default) - { - if (_activeConfig is null || string.IsNullOrWhiteSpace(databaseName)) - return; - - if (string.Equals(_activeConfig.Database, databaseName, StringComparison.OrdinalIgnoreCase)) - return; - - ConnectionConfig switched = _activeConfig with { Database = databaseName }; - var factory = DbOrchestratorFactory.CreateDefault(); - await using IDbOrchestrator orchestrator = factory.Create(switched); - ConnectionTestResult result = await orchestrator.TestConnectionAsync(ct); - if (!result.Success) - throw new InvalidOperationException(result.ErrorMessage ?? $"Could not switch database to '{databaseName}'."); - - (_metadataService as IDisposable)?.Dispose(); - _metadataService = null; - _loadedMetadata = null; - _activeConfig = switched; - } - - /// - /// Returns a human-friendly server version string for the active connection. - /// - public async Task GetServerVersionAsync(CancellationToken ct = default) - { - if (!string.IsNullOrWhiteSpace(_loadedMetadata?.ServerVersion)) - return _loadedMetadata.ServerVersion; - - if (_activeConfig is null) - return null; - - string sql = _activeConfig.Provider switch - { - DatabaseProvider.Postgres => "SELECT version();", - DatabaseProvider.MySql => "SELECT VERSION();", - DatabaseProvider.SqlServer => "SELECT @@VERSION;", - DatabaseProvider.SQLite => "SELECT sqlite_version();", - _ => string.Empty, - }; - - if (string.IsNullOrWhiteSpace(sql)) - return null; - - try - { - DataTable? table = await ExecutePreviewAsTableAsync(_activeConfig, sql, 1, ct); - if (table is null || table.Rows.Count == 0 || table.Columns.Count == 0) - return null; - - return table.Rows[0][0]?.ToString()?.Trim(); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Could not fetch server version for provider {Provider}", _activeConfig.Provider); - return null; - } - } - - private static async Task ExecutePreviewAsTableAsync( - ConnectionConfig config, - string sql, - int maxRows, - CancellationToken ct) - { - var factory = DbOrchestratorFactory.CreateDefault(); - await using IDbOrchestrator orchestrator = factory.Create(config); - PreviewResult preview = await orchestrator.ExecutePreviewAsync(sql, maxRows, ct); - return preview.Success ? preview.Data : null; - } - - /// - /// Cancels any in-progress connection operations. - /// - public void Cancel() - { - _operationCts?.Cancel(); - } - - /// - /// Cleans up resources. Call when switching connections or closing the app. - /// Properly disposes CancellationTokenSource and MetadataService if they are IDisposable. - /// - public void Dispose() - { - if (_disposed) - return; - - // Dispose the CancellationTokenSource - _operationCts?.Dispose(); - _operationCts = null; - - // Dispose the MetadataService if not null - if (_metadataService is not null) - { - (_metadataService as IDisposable)?.Dispose(); - } - _metadataService = null; - _activeConfig = null; - - _loadedMetadata = null; - _disposed = true; - } -} +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.Data; +using AkkornStudio; +using AkkornStudio.Core; +using AkkornStudio.Metadata; +using AkkornStudio.Nodes; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; + +namespace AkkornStudio.UI.Services.Connection; + +/// +/// Orchestrates the database connection workflow: +/// 1. Establishes connection to the database +/// 2. Fetches and introspects database tables +/// 3. Loads tables into the search menu for quick access +/// 4. Initializes metadata service for auto-join suggestions +/// +/// This service ensures that after a successful database connection, +/// the normal flow of operations is triggered automatically. +/// +public sealed class DatabaseConnectionService : IDisposable +{ + private readonly ILogger _logger; + private MetadataService? _metadataService; + private CancellationTokenSource? _operationCts; + private DbMetadata? _loadedMetadata; + private ConnectionConfig? _activeConfig; + private bool _disposed; + + /// + /// The most recently loaded database metadata. + /// Null if no database has been connected or loading failed. + /// + public DbMetadata? LoadedMetadata => _loadedMetadata; + + public DatabaseConnectionService(ILogger? logger = null) + { + _logger = logger ?? NullLogger.Instance; + } + + /// + /// Executes the complete database connection workflow: + /// - Tests connection + /// - Loads schema metadata + /// - Populates search menu with tables + /// - Initializes metadata service for auto-join detection + /// + public async Task ConnectAndLoadAsync( + ConnectionConfig config, + SearchMenuViewModel searchMenu, + CancellationToken ct = default + ) + { + // Cancel any previous operation + _operationCts?.Cancel(); + _operationCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + CancellationToken linked = _operationCts.Token; + + try + { + _logger.LogInformation( + "[DatabaseConnectionService] Starting connection workflow for {Provider} @ {Host}:{Port}/{Database}", + config.Provider, + config.Host, + config.Port, + config.Database + ); + + // Step 1: Initialize the metadata service + _metadataService = MetadataService.Create(config); + _activeConfig = config; + + // Step 2: Fetch complete database schema + _logger.LogInformation("[DatabaseConnectionService] Fetching database schema..."); + var metadata = await _metadataService.GetMetadataAsync(forceRefresh: true, ct: linked); + + // Store the loaded metadata for later access + _loadedMetadata = metadata; + + _logger.LogInformation( + "[DatabaseConnectionService] Schema loaded: {Tables} tables, {Views} views, {FKs} foreign keys", + metadata.TotalTables, + metadata.TotalViews, + metadata.TotalForeignKeys + ); + + // Step 3: Convert metadata to SearchMenu format and load + var tables = ConvertMetadataToTableList(metadata); + _logger.LogInformation( + "[DatabaseConnectionService] Loading {Count} tables into search menu", + tables.Count + ); + + searchMenu.LoadTables(tables); + + _logger.LogInformation( + "[DatabaseConnectionService] Database connection workflow completed successfully" + ); + } + catch (OperationCanceledException) + { + _logger.LogInformation("[DatabaseConnectionService] Connection workflow cancelled"); + throw; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "[DatabaseConnectionService] Error during connection workflow: {Message}", + ex.Message + ); + throw; + } + } + + /// + /// Converts DbMetadata to the tuple format expected by SearchMenuViewModel.LoadTables(). + /// Flattens all schemas and returns (FullName, Columns) pairs. + /// + private static List<(string FullName, IReadOnlyList<(string Name, PinDataType Type)> Cols)> + ConvertMetadataToTableList(DbMetadata metadata) + { + var result = new List<(string, IReadOnlyList<(string, PinDataType)>)>(); + + foreach (var schema in metadata.Schemas) + { + foreach (var table in schema.Tables) + { + // Build full name: schema.tablename + string fullName = $"{table.Schema}.{table.Name}"; + + // Convert columns to (Name, PinDataType) tuples + var columns = table.Columns + .OrderBy(c => c.OrdinalPosition) + .Select(c => ( + Name: c.Name, + Type: MapDataTypeToPinDataType(c) + )) + .ToList(); + + result.Add((fullName, columns.AsReadOnly())); + } + } + + return result; + } + + /// + /// Maps database column semantics to UI pin data types. + /// Used for proper icon and color rendering in the canvas. + /// + private static PinDataType MapDataTypeToPinDataType(ColumnMetadata column) + { + return column.SemanticType switch + { + ColumnSemanticType.Numeric => PinDataType.Number, + ColumnSemanticType.Text => PinDataType.Text, + ColumnSemanticType.DateTime => PinDataType.DateTime, + ColumnSemanticType.Boolean => PinDataType.Boolean, + ColumnSemanticType.Guid => PinDataType.Text, // GUIDs rendered as text in pins + ColumnSemanticType.Document => PinDataType.Json, + ColumnSemanticType.Binary => PinDataType.Text, // Binary data rendered as text + ColumnSemanticType.Spatial => PinDataType.Json, // Spatial types as JSON + ColumnSemanticType.Other => PinDataType.Text, + _ => PinDataType.Text, + }; + } + + /// + /// Returns the active metadata service if connected, or null if not. + /// Used for features like auto-join detection that require database schema. + /// + public MetadataService? GetActiveMetadataService() => _metadataService; + + /// + /// Lists available databases/schemas for the active connection when provider supports it. + /// + public async Task ListDatabasesAsync(CancellationToken ct = default) + { + if (_activeConfig is null) + return []; + + if (_activeConfig.Provider == DatabaseProvider.SQLite) + return [Path.GetFileNameWithoutExtension(_activeConfig.Database)]; + + string sql = _activeConfig.Provider switch + { + DatabaseProvider.Postgres => "SELECT datname FROM pg_database WHERE datallowconn = TRUE ORDER BY datname;", + DatabaseProvider.MySql => "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY SCHEMA_NAME;", + DatabaseProvider.SqlServer => "SELECT name FROM sys.databases WHERE state = 0 ORDER BY name;", + _ => string.Empty, + }; + + if (string.IsNullOrWhiteSpace(sql)) + return []; + + try + { + DataTable? table = await ExecutePreviewAsTableAsync(_activeConfig, sql, 10000, ct); + if (table is null || table.Columns.Count == 0) + return []; + + string[] names = table.Rows + .Cast() + .Select(r => r[0]?.ToString()) + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Select(v => v!.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + return names; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to list databases for provider {Provider}", _activeConfig.Provider); + return []; + } + } + + /// + /// Switches the active database by updating the active connection config and validating connectivity. + /// + public async Task SwitchDatabaseAsync(string databaseName, CancellationToken ct = default) + { + if (_activeConfig is null || string.IsNullOrWhiteSpace(databaseName)) + return; + + if (string.Equals(_activeConfig.Database, databaseName, StringComparison.OrdinalIgnoreCase)) + return; + + ConnectionConfig switched = _activeConfig with { Database = databaseName }; + var factory = DbOrchestratorFactory.CreateDefault(); + await using IDbOrchestrator orchestrator = factory.Create(switched); + ConnectionTestResult result = await orchestrator.TestConnectionAsync(ct); + if (!result.Success) + throw new InvalidOperationException(result.ErrorMessage ?? $"Could not switch database to '{databaseName}'."); + + (_metadataService as IDisposable)?.Dispose(); + _metadataService = null; + _loadedMetadata = null; + _activeConfig = switched; + } + + /// + /// Returns a human-friendly server version string for the active connection. + /// + public async Task GetServerVersionAsync(CancellationToken ct = default) + { + if (!string.IsNullOrWhiteSpace(_loadedMetadata?.ServerVersion)) + return _loadedMetadata.ServerVersion; + + if (_activeConfig is null) + return null; + + string sql = _activeConfig.Provider switch + { + DatabaseProvider.Postgres => "SELECT version();", + DatabaseProvider.MySql => "SELECT VERSION();", + DatabaseProvider.SqlServer => "SELECT @@VERSION;", + DatabaseProvider.SQLite => "SELECT sqlite_version();", + _ => string.Empty, + }; + + if (string.IsNullOrWhiteSpace(sql)) + return null; + + try + { + DataTable? table = await ExecutePreviewAsTableAsync(_activeConfig, sql, 1, ct); + if (table is null || table.Rows.Count == 0 || table.Columns.Count == 0) + return null; + + return table.Rows[0][0]?.ToString()?.Trim(); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Could not fetch server version for provider {Provider}", _activeConfig.Provider); + return null; + } + } + + private static async Task ExecutePreviewAsTableAsync( + ConnectionConfig config, + string sql, + int maxRows, + CancellationToken ct) + { + var factory = DbOrchestratorFactory.CreateDefault(); + await using IDbOrchestrator orchestrator = factory.Create(config); + PreviewResult preview = await orchestrator.ExecutePreviewAsync(sql, maxRows, ct); + return preview.Success ? preview.Data : null; + } + + /// + /// Cancels any in-progress connection operations. + /// + public void Cancel() + { + _operationCts?.Cancel(); + } + + /// + /// Cleans up resources. Call when switching connections or closing the app. + /// Properly disposes CancellationTokenSource and MetadataService if they are IDisposable. + /// + public void Dispose() + { + if (_disposed) + return; + + // Dispose the CancellationTokenSource + _operationCts?.Dispose(); + _operationCts = null; + + // Dispose the MetadataService if not null + if (_metadataService is not null) + { + (_metadataService as IDisposable)?.Dispose(); + } + _metadataService = null; + _activeConfig = null; + + _loadedMetadata = null; + _disposed = true; + } +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/ConnectionContextLevel.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/ConnectionContextLevel.cs similarity index 60% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/ConnectionContextLevel.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/ConnectionContextLevel.cs index ba10dfdf..22418c57 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/ConnectionContextLevel.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/ConnectionContextLevel.cs @@ -1,8 +1,8 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public enum ConnectionContextLevel -{ - Connection = 0, - DatabaseOrCatalog = 1, - Schema = 2, -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public enum ConnectionContextLevel +{ + Connection = 0, + DatabaseOrCatalog = 1, + Schema = 2, +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ActiveConnectionSessionDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ActiveConnectionSessionDto.cs similarity index 72% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ActiveConnectionSessionDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ActiveConnectionSessionDto.cs index b402e190..1e1ad0be 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ActiveConnectionSessionDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ActiveConnectionSessionDto.cs @@ -1,7 +1,7 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ActiveConnectionSessionDto( - string? ConnectionId, - ConnectionSessionStateDto SessionState, - DateTimeOffset? StartedAt, - string? SessionLabel); +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ActiveConnectionSessionDto( + string? ConnectionId, + ConnectionSessionStateDto SessionState, + DateTimeOffset? StartedAt, + string? SessionLabel); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionCatalogCapabilityDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionCatalogCapabilityDto.cs similarity index 62% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionCatalogCapabilityDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionCatalogCapabilityDto.cs index 977170c6..25b4086d 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionCatalogCapabilityDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionCatalogCapabilityDto.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; - -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ConnectionCatalogCapabilityDto( - DatabaseProvider Provider, - IReadOnlyList SupportedLevels); +using AkkornStudio.Core; + +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ConnectionCatalogCapabilityDto( + DatabaseProvider Provider, + IReadOnlyList SupportedLevels); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionDetailsDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionDetailsDto.cs similarity index 82% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionDetailsDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionDetailsDto.cs index 90738833..6fb38f2d 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionDetailsDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionDetailsDto.cs @@ -1,14 +1,14 @@ -using System.Collections.Generic; - -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ConnectionDetailsDto( - string Id, - string Name, - string Provider, - ConnectionProviderModeDto Mode, - IReadOnlyDictionary FieldValues, - string? UrlValue, - string? Tag, - bool IsFavorite, - IReadOnlyDictionary AdvancedOptions); +using System.Collections.Generic; + +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ConnectionDetailsDto( + string Id, + string Name, + string Provider, + ConnectionProviderModeDto Mode, + IReadOnlyDictionary FieldValues, + string? UrlValue, + string? Tag, + bool IsFavorite, + IReadOnlyDictionary AdvancedOptions); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionSummaryDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionSummaryDto.cs similarity index 78% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionSummaryDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionSummaryDto.cs index 2412436b..10b30259 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionSummaryDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionSummaryDto.cs @@ -1,12 +1,12 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ConnectionSummaryDto( - string Id, - string Name, - string Provider, - string SummaryText, - bool IsFavorite, - bool IsActive, - DateTimeOffset? LastUsedAt, - ConnectionTestStatusDto LastTestStatus, - DateTimeOffset? LastTestAt); +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ConnectionSummaryDto( + string Id, + string Name, + string Provider, + string SummaryText, + bool IsFavorite, + bool IsActive, + DateTimeOffset? LastUsedAt, + ConnectionTestStatusDto LastTestStatus, + DateTimeOffset? LastTestAt); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionTestResultDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionTestResultDto.cs similarity index 76% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionTestResultDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionTestResultDto.cs index 5075fc6a..fae815a3 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionTestResultDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionTestResultDto.cs @@ -1,9 +1,9 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ConnectionTestResultDto( - ConnectionTestStatusDto Status, - string SummaryMessage, - string? TechnicalDetails, - int? LatencyMs, - string? ProviderErrorCode, - DateTimeOffset TestedAt); +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ConnectionTestResultDto( + ConnectionTestStatusDto Status, + string SummaryMessage, + string? TechnicalDetails, + int? LatencyMs, + string? ProviderErrorCode, + DateTimeOffset TestedAt); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionUrlParseResultDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionUrlParseResultDto.cs similarity index 84% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionUrlParseResultDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionUrlParseResultDto.cs index c2b888f7..a6647b17 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionUrlParseResultDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionUrlParseResultDto.cs @@ -1,13 +1,13 @@ -using System.Collections.Generic; - -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ConnectionUrlParseResultDto( - ConnectionUrlParseStatusDto ParseStatus, - IReadOnlyList RecognizedFields, - IReadOnlyList UnrecognizedTokens, - string? SuggestedProvider, - bool ConflictWithSelectedProvider, - string? NormalizedUrl, - string UserMessage, - string? TechnicalDetails); +using System.Collections.Generic; + +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ConnectionUrlParseResultDto( + ConnectionUrlParseStatusDto ParseStatus, + IReadOnlyList RecognizedFields, + IReadOnlyList UnrecognizedTokens, + string? SuggestedProvider, + bool ConflictWithSelectedProvider, + string? NormalizedUrl, + string UserMessage, + string? TechnicalDetails); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationMessageDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationMessageDto.cs similarity index 63% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationMessageDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationMessageDto.cs index 6701f3a4..8d752c8f 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationMessageDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationMessageDto.cs @@ -1,6 +1,6 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ConnectionValidationMessageDto( - string FieldKey, - string Code, - string Message); +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ConnectionValidationMessageDto( + string FieldKey, + string Code, + string Message); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationResultDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationResultDto.cs similarity index 76% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationResultDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationResultDto.cs index 9f84f163..bc5ef94b 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationResultDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ConnectionValidationResultDto.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; - -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ConnectionValidationResultDto( - bool IsValid, - IReadOnlyList Errors, - IReadOnlyList Warnings); +using System.Collections.Generic; + +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ConnectionValidationResultDto( + bool IsValid, + IReadOnlyList Errors, + IReadOnlyList Warnings); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/OperationResultDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/OperationResultDto.cs similarity index 76% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/OperationResultDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/OperationResultDto.cs index 65a28223..bed8f7d0 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/OperationResultDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/OperationResultDto.cs @@ -1,9 +1,9 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record OperationResultDto( - bool Success, - ConnectionOperationSemanticErrorCode SemanticErrorCode, - string UserMessage, - T? Payload, - string? TechnicalError, - string? CorrelationId); +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record OperationResultDto( + bool Success, + ConnectionOperationSemanticErrorCode SemanticErrorCode, + string UserMessage, + T? Payload, + string? TechnicalError, + string? CorrelationId); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ProviderCapabilityDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ProviderCapabilityDto.cs similarity index 80% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ProviderCapabilityDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ProviderCapabilityDto.cs index a0ba8c6d..4d654622 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/ProviderCapabilityDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/ProviderCapabilityDto.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; - -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record ProviderCapabilityDto( - string Provider, - bool SupportsUrlMode, - bool SupportsSsl, - bool SupportsIntegratedSecurity, - bool RequiresDatabase, - IReadOnlyList SupportedUrlSchemes, - IReadOnlyList RequiredFieldKeys); +using System.Collections.Generic; + +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record ProviderCapabilityDto( + string Provider, + bool SupportsUrlMode, + bool SupportsSsl, + bool SupportsIntegratedSecurity, + bool RequiresDatabase, + IReadOnlyList SupportedUrlSchemes, + IReadOnlyList RequiredFieldKeys); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/UrlParseFieldTokenDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/UrlParseFieldTokenDto.cs similarity index 55% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/UrlParseFieldTokenDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/UrlParseFieldTokenDto.cs index feefa96e..edae3ab5 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Dtos/UrlParseFieldTokenDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Dtos/UrlParseFieldTokenDto.cs @@ -1,5 +1,5 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public sealed record UrlParseFieldTokenDto( - string Key, - string? Value); +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public sealed record UrlParseFieldTokenDto( + string Key, + string? Value); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionOperationSemanticErrorCode.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionOperationSemanticErrorCode.cs similarity index 76% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionOperationSemanticErrorCode.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionOperationSemanticErrorCode.cs index cb8eafa3..96ca709d 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionOperationSemanticErrorCode.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionOperationSemanticErrorCode.cs @@ -1,16 +1,16 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public enum ConnectionOperationSemanticErrorCode -{ - None = 0, - ValidationFailed, - NotFound, - Conflict, - ProviderMismatch, - ParseFailed, - ParsePartial, - AuthenticationFailed, - Timeout, - OperationBlocked, - Unknown -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public enum ConnectionOperationSemanticErrorCode +{ + None = 0, + ValidationFailed, + NotFound, + Conflict, + ProviderMismatch, + ParseFailed, + ParsePartial, + AuthenticationFailed, + Timeout, + OperationBlocked, + Unknown +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionProviderModeDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionProviderModeDto.cs similarity index 50% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionProviderModeDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionProviderModeDto.cs index 5c193a5f..219731d2 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionProviderModeDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionProviderModeDto.cs @@ -1,7 +1,7 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public enum ConnectionProviderModeDto -{ - Fields = 0, - Url -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public enum ConnectionProviderModeDto +{ + Fields = 0, + Url +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionSessionStateDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionSessionStateDto.cs similarity index 62% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionSessionStateDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionSessionStateDto.cs index 5c2251a0..9127f363 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionSessionStateDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionSessionStateDto.cs @@ -1,10 +1,10 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public enum ConnectionSessionStateDto -{ - Inactive = 0, - Connecting, - Active, - Disconnecting, - Failed -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public enum ConnectionSessionStateDto +{ + Inactive = 0, + Connecting, + Active, + Disconnecting, + Failed +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionTestStatusDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionTestStatusDto.cs similarity index 68% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionTestStatusDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionTestStatusDto.cs index 6165dfe4..49166e1e 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionTestStatusDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionTestStatusDto.cs @@ -1,12 +1,12 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public enum ConnectionTestStatusDto -{ - NotTested = 0, - Testing, - Success, - Failure, - AuthenticationFailure, - Timeout, - Unavailable -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public enum ConnectionTestStatusDto +{ + NotTested = 0, + Testing, + Success, + Failure, + AuthenticationFailure, + Timeout, + Unavailable +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionUrlParseStatusDto.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionUrlParseStatusDto.cs similarity index 60% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionUrlParseStatusDto.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionUrlParseStatusDto.cs index a38032b6..f386c7d8 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/Enums/ConnectionUrlParseStatusDto.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/Enums/ConnectionUrlParseStatusDto.cs @@ -1,10 +1,10 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public enum ConnectionUrlParseStatusDto -{ - Idle = 0, - Parsing, - Success, - Partial, - Failed -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public enum ConnectionUrlParseStatusDto +{ + Idle = 0, + Parsing, + Success, + Partial, + Failed +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionActivationWorkflow.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionActivationWorkflow.cs similarity index 65% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionActivationWorkflow.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionActivationWorkflow.cs index 01f320ee..b42ac287 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionActivationWorkflow.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionActivationWorkflow.cs @@ -1,9 +1,9 @@ -using DBWeaver.Core; -using DBWeaver.Metadata; -using DBWeaver.UI.Services; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.Metadata; +using AkkornStudio.UI.Services; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionActivationWorkflow { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCanvasPromptCoordinator.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCanvasPromptCoordinator.cs similarity index 72% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCanvasPromptCoordinator.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCanvasPromptCoordinator.cs index 79f002f2..97b071e7 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCanvasPromptCoordinator.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCanvasPromptCoordinator.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.Metadata; +using AkkornStudio.Core; +using AkkornStudio.Metadata; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionCanvasPromptCoordinator { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCatalogCapabilityProvider.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCatalogCapabilityProvider.cs similarity index 62% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCatalogCapabilityProvider.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCatalogCapabilityProvider.cs index 63b6bef9..77c3f383 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCatalogCapabilityProvider.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCatalogCapabilityProvider.cs @@ -1,8 +1,8 @@ -using DBWeaver.Core; - -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public interface IConnectionCatalogCapabilityProvider -{ - IReadOnlyList GetSupportedLevels(DatabaseProvider provider); -} +using AkkornStudio.Core; + +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public interface IConnectionCatalogCapabilityProvider +{ + IReadOnlyList GetSupportedLevels(DatabaseProvider provider); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCatalogService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCatalogService.cs similarity index 90% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCatalogService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCatalogService.cs index 31e6dd35..9094970d 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionCatalogService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionCatalogService.cs @@ -1,23 +1,23 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public interface IConnectionCatalogService -{ - Task> ListSummariesAsync(CancellationToken cancellationToken = default); - - Task> GetDetailsAsync( - string connectionId, - CancellationToken cancellationToken = default); - - Task> SaveAsync( - ConnectionDetailsDto details, - CancellationToken cancellationToken = default); - - Task> DuplicateAsync( - string connectionId, - string? newName = null, - CancellationToken cancellationToken = default); - - Task> DeleteAsync( - string connectionId, - CancellationToken cancellationToken = default); -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public interface IConnectionCatalogService +{ + Task> ListSummariesAsync(CancellationToken cancellationToken = default); + + Task> GetDetailsAsync( + string connectionId, + CancellationToken cancellationToken = default); + + Task> SaveAsync( + ConnectionDetailsDto details, + CancellationToken cancellationToken = default); + + Task> DuplicateAsync( + string connectionId, + string? newName = null, + CancellationToken cancellationToken = default); + + Task> DeleteAsync( + string connectionId, + CancellationToken cancellationToken = default); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionErrorMessageMapper.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionErrorMessageMapper.cs similarity index 57% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionErrorMessageMapper.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionErrorMessageMapper.cs index 7e156047..b6b10eef 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionErrorMessageMapper.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionErrorMessageMapper.cs @@ -1,6 +1,6 @@ -using DBWeaver.Core; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionErrorMessageMapper { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionHealthLifecycleCoordinator.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionHealthLifecycleCoordinator.cs similarity index 87% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionHealthLifecycleCoordinator.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionHealthLifecycleCoordinator.cs index 6523332e..738ca624 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionHealthLifecycleCoordinator.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionHealthLifecycleCoordinator.cs @@ -1,6 +1,6 @@ -using DBWeaver.Core; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionHealthLifecycleCoordinator { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionHealthMonitorService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionHealthMonitorService.cs similarity index 88% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionHealthMonitorService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionHealthMonitorService.cs index 8edd2be1..a2bb1dd2 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionHealthMonitorService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionHealthMonitorService.cs @@ -1,6 +1,6 @@ -using DBWeaver.Core; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionHealthMonitorService { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionManagerViewModelFactory.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionManagerViewModelFactory.cs similarity index 63% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionManagerViewModelFactory.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionManagerViewModelFactory.cs index 7aa49d6b..dd2143de 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionManagerViewModelFactory.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionManagerViewModelFactory.cs @@ -1,6 +1,6 @@ -namespace DBWeaver.UI.Services.ConnectionManager; - -public interface IConnectionManagerViewModelFactory -{ - ConnectionManagerViewModel Create(); -} +namespace AkkornStudio.UI.Services.ConnectionManager; + +public interface IConnectionManagerViewModelFactory +{ + ConnectionManagerViewModel Create(); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileFormMapper.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileFormMapper.cs similarity index 80% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileFormMapper.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileFormMapper.cs index bfab3bae..4a3df444 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileFormMapper.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileFormMapper.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionProfileFormMapper { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileLifecycleService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileLifecycleService.cs similarity index 89% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileLifecycleService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileLifecycleService.cs index 708dbeb1..b8c4a0bb 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileLifecycleService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileLifecycleService.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionProfileLifecycleService { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileStore.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileStore.cs similarity index 70% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileStore.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileStore.cs index 582bddf1..e8734ff6 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionProfileStore.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionProfileStore.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.Services.Connection; +using AkkornStudio.UI.Services.Connection; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionProfileStore { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionSessionOrchestrator.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionSessionOrchestrator.cs similarity index 82% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionSessionOrchestrator.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionSessionOrchestrator.cs index d6180b7f..85ac1b84 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionSessionOrchestrator.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionSessionOrchestrator.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionSessionOrchestrator { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionSessionService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionSessionService.cs similarity index 86% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionSessionService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionSessionService.cs index 3f3b30e3..a5b8d133 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionSessionService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionSessionService.cs @@ -1,14 +1,14 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public interface IConnectionSessionService -{ - Task> ConnectAsync( - ConnectionDetailsDto details, - CancellationToken cancellationToken = default); - - Task> DisconnectAsync( - string connectionId, - CancellationToken cancellationToken = default); - - Task GetActiveSessionAsync(CancellationToken cancellationToken = default); -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public interface IConnectionSessionService +{ + Task> ConnectAsync( + ConnectionDetailsDto details, + CancellationToken cancellationToken = default); + + Task> DisconnectAsync( + string connectionId, + CancellationToken cancellationToken = default); + + Task GetActiveSessionAsync(CancellationToken cancellationToken = default); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionStatusPresenter.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionStatusPresenter.cs similarity index 89% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionStatusPresenter.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionStatusPresenter.cs index 2423b1e5..d89b0838 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionStatusPresenter.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionStatusPresenter.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionStatusPresenter { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTelemetryService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTelemetryService.cs similarity index 77% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTelemetryService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTelemetryService.cs index 0b03e27c..c36aed3e 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTelemetryService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTelemetryService.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; - -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public interface IConnectionTelemetryService -{ - Task TrackAsync( - string eventName, - IReadOnlyDictionary properties, - CancellationToken cancellationToken = default); -} +using System.Collections.Generic; + +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public interface IConnectionTelemetryService +{ + Task TrackAsync( + string eventName, + IReadOnlyDictionary properties, + CancellationToken cancellationToken = default); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTestExecutor.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTestExecutor.cs similarity index 73% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTestExecutor.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTestExecutor.cs index 67eb0dae..1a38d8b2 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTestExecutor.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTestExecutor.cs @@ -1,6 +1,6 @@ -using DBWeaver.Core; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public interface IConnectionTestExecutor { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTestService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTestService.cs similarity index 75% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTestService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTestService.cs index 94e22377..4d65d0ee 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionTestService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionTestService.cs @@ -1,8 +1,8 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public interface IConnectionTestService -{ - Task> TestAsync( - ConnectionDetailsDto details, - CancellationToken cancellationToken = default); -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public interface IConnectionTestService +{ + Task> TestAsync( + ConnectionDetailsDto details, + CancellationToken cancellationToken = default); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionUrlParserService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionUrlParserService.cs similarity index 75% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionUrlParserService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionUrlParserService.cs index 3de10651..9efd5104 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionUrlParserService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionUrlParserService.cs @@ -1,9 +1,9 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public interface IConnectionUrlParserService -{ - Task ParseAsync( - string rawUrl, - string? selectedProvider, - CancellationToken cancellationToken = default); -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public interface IConnectionUrlParserService +{ + Task ParseAsync( + string rawUrl, + string? selectedProvider, + CancellationToken cancellationToken = default); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionValidationService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionValidationService.cs similarity index 72% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionValidationService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionValidationService.cs index 9fa2d5b6..b2a526c0 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IConnectionValidationService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IConnectionValidationService.cs @@ -1,8 +1,8 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public interface IConnectionValidationService -{ - ConnectionValidationResultDto Validate( - ConnectionDetailsDto details, - ProviderCapabilityDto capability); -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public interface IConnectionValidationService +{ + ConnectionValidationResultDto Validate( + ConnectionDetailsDto details, + ProviderCapabilityDto capability); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IProviderCapabilityService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IProviderCapabilityService.cs similarity index 81% rename from src/DBWeaver.UI/Services/ConnectionManager/Contracts/IProviderCapabilityService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IProviderCapabilityService.cs index 47bfbf4a..5058c026 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Contracts/IProviderCapabilityService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Contracts/IProviderCapabilityService.cs @@ -1,10 +1,10 @@ -namespace DBWeaver.UI.Services.ConnectionManager.Contracts; - -public interface IProviderCapabilityService -{ - Task> ListCapabilitiesAsync(CancellationToken cancellationToken = default); - - Task GetCapabilityAsync( - string provider, - CancellationToken cancellationToken = default); -} +namespace AkkornStudio.UI.Services.ConnectionManager.Contracts; + +public interface IProviderCapabilityService +{ + Task> ListCapabilitiesAsync(CancellationToken cancellationToken = default); + + Task GetCapabilityAsync( + string provider, + CancellationToken cancellationToken = default); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionActivationOutcome.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionActivationOutcome.cs similarity index 71% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionActivationOutcome.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionActivationOutcome.cs index f0481262..95488d6d 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionActivationOutcome.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionActivationOutcome.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public enum ConnectionActivationOutcome { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionActivationResult.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionActivationResult.cs similarity index 72% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionActivationResult.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionActivationResult.cs index f66b20ca..9bf03bbf 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionActivationResult.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionActivationResult.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.Metadata; +using AkkornStudio.Core; +using AkkornStudio.Metadata; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public readonly record struct ConnectionActivationResult( ConnectionActivationOutcome Outcome, diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionCanvasPromptState.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionCanvasPromptState.cs similarity index 57% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionCanvasPromptState.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionCanvasPromptState.cs index 09315c23..ca92d8c5 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionCanvasPromptState.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionCanvasPromptState.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.Metadata; +using AkkornStudio.Core; +using AkkornStudio.Metadata; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public readonly record struct ConnectionCanvasPromptState( bool IsVisible, diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionConnectState.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionConnectState.cs similarity index 77% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionConnectState.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionConnectState.cs index 3702ae5b..2edbc4f5 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionConnectState.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionConnectState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public readonly record struct ConnectionConnectState( bool Started, diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionDisconnectState.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionDisconnectState.cs similarity index 72% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionDisconnectState.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionDisconnectState.cs index e745238a..96074256 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionDisconnectState.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionDisconnectState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public readonly record struct ConnectionDisconnectState( CancellationTokenSource? ConnectCts, diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfile.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfile.cs similarity index 94% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfile.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfile.cs index 71779238..072114f7 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfile.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfile.cs @@ -1,8 +1,8 @@ -using DBWeaver.Core; -using DBWeaver.UI.Services; -using DBWeaver.UI.Services.Connection; +using AkkornStudio.Core; +using AkkornStudio.UI.Services; +using AkkornStudio.UI.Services.Connection; -namespace DBWeaver.UI.Services.ConnectionManager.Models; +namespace AkkornStudio.UI.Services.ConnectionManager.Models; public sealed class ConnectionProfile { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileDeleteResult.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileDeleteResult.cs similarity index 76% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileDeleteResult.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileDeleteResult.cs index 79f776df..7cf36732 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileDeleteResult.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileDeleteResult.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public readonly record struct ConnectionProfileDeleteResult( bool Deleted, diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileFormData.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileFormData.cs similarity index 82% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileFormData.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileFormData.cs index d5a4f7e3..06a4ce18 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileFormData.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileFormData.cs @@ -1,6 +1,6 @@ -using DBWeaver.Core; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public readonly record struct ConnectionProfileFormData( string Id, diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileSaveResult.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileSaveResult.cs similarity index 70% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileSaveResult.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileSaveResult.cs index 4bcd85ec..fbdaf77f 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionProfileSaveResult.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionProfileSaveResult.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public readonly record struct ConnectionProfileSaveResult( ConnectionProfile SelectedProfile, diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionStatusViewState.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionStatusViewState.cs similarity index 60% rename from src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionStatusViewState.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionStatusViewState.cs index f5f37556..97842ace 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Models/ConnectionStatusViewState.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Models/ConnectionStatusViewState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public readonly record struct ConnectionStatusViewState(string Message, string Color); diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Persistence/ConnectionProfileStore.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Persistence/ConnectionProfileStore.cs similarity index 94% rename from src/DBWeaver.UI/Services/ConnectionManager/Persistence/ConnectionProfileStore.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Persistence/ConnectionProfileStore.cs index ca9382e7..083607eb 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Persistence/ConnectionProfileStore.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Persistence/ConnectionProfileStore.cs @@ -1,7 +1,7 @@ using System.Text.Json; -using DBWeaver.UI.Services.Connection; +using AkkornStudio.UI.Services.Connection; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionProfileStore(string? profilesFilePath = null) : IConnectionProfileStore { @@ -9,7 +9,7 @@ public sealed class ConnectionProfileStore(string? profilesFilePath = null) : IC private readonly string _profilesFilePath = string.IsNullOrWhiteSpace(profilesFilePath) - ? Path.Combine(global::DBWeaver.UI.AppConstants.AppDataDirectory, "connections.json") + ? Path.Combine(global::AkkornStudio.UI.AppConstants.AppDataDirectory, "connections.json") : profilesFilePath; public IReadOnlyList LoadProfiles(CredentialVaultStore credentialVault) diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionErrorMessageMapper.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionErrorMessageMapper.cs similarity index 95% rename from src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionErrorMessageMapper.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionErrorMessageMapper.cs index 9a3a2a20..6871599c 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionErrorMessageMapper.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionErrorMessageMapper.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.UI.Services.Localization; +using AkkornStudio.Core; +using AkkornStudio.UI.Services.Localization; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionErrorMessageMapper(ILocalizationService localization) : IConnectionErrorMessageMapper { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionProfileFormMapper.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionProfileFormMapper.cs similarity index 93% rename from src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionProfileFormMapper.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionProfileFormMapper.cs index 80fe3912..3d12ed2b 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionProfileFormMapper.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionProfileFormMapper.cs @@ -1,8 +1,8 @@ -using DBWeaver.Core; -using DBWeaver.UI.Services; -using DBWeaver.UI.Services.Localization; +using AkkornStudio.Core; +using AkkornStudio.UI.Services; +using AkkornStudio.UI.Services.Localization; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionProfileFormMapper(ILocalizationService localization) : IConnectionProfileFormMapper { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionStatusPresenter.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionStatusPresenter.cs similarity index 93% rename from src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionStatusPresenter.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionStatusPresenter.cs index 0962f9a3..9f26d65b 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Presentation/ConnectionStatusPresenter.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Presentation/ConnectionStatusPresenter.cs @@ -1,7 +1,7 @@ -using DBWeaver.UI.Services.Theming; -using DBWeaver.UI.Services.Localization; +using AkkornStudio.UI.Services.Theming; +using AkkornStudio.UI.Services.Localization; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionStatusPresenter(ILocalizationService localization) : IConnectionStatusPresenter { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionActivationWorkflow.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionActivationWorkflow.cs similarity index 92% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionActivationWorkflow.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionActivationWorkflow.cs index 866a6033..42405d4d 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionActivationWorkflow.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionActivationWorkflow.cs @@ -1,8 +1,8 @@ -using DBWeaver.Core; -using DBWeaver.Metadata; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.Metadata; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionActivationWorkflow : IConnectionActivationWorkflow { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCanvasPromptCoordinator.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCanvasPromptCoordinator.cs similarity index 84% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCanvasPromptCoordinator.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCanvasPromptCoordinator.cs index 161f88cf..a2c06c42 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCanvasPromptCoordinator.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCanvasPromptCoordinator.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.Metadata; +using AkkornStudio.Core; +using AkkornStudio.Metadata; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionCanvasPromptCoordinator : IConnectionCanvasPromptCoordinator { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCatalogCapabilityProvider.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCatalogCapabilityProvider.cs similarity index 90% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCatalogCapabilityProvider.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCatalogCapabilityProvider.cs index d7ae69ce..21411713 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCatalogCapabilityProvider.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCatalogCapabilityProvider.cs @@ -1,32 +1,32 @@ -using DBWeaver.Core; -using DBWeaver.UI.Services.ConnectionManager.Contracts; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ConnectionCatalogCapabilityProvider : IConnectionCatalogCapabilityProvider -{ - private static readonly IReadOnlyDictionary Capabilities = - new Dictionary - { - [DatabaseProvider.Postgres] = new( - DatabaseProvider.Postgres, - [ConnectionContextLevel.Connection, ConnectionContextLevel.DatabaseOrCatalog, ConnectionContextLevel.Schema]), - [DatabaseProvider.MySql] = new( - DatabaseProvider.MySql, - [ConnectionContextLevel.Connection, ConnectionContextLevel.DatabaseOrCatalog, ConnectionContextLevel.Schema]), - [DatabaseProvider.SqlServer] = new( - DatabaseProvider.SqlServer, - [ConnectionContextLevel.Connection, ConnectionContextLevel.DatabaseOrCatalog, ConnectionContextLevel.Schema]), - [DatabaseProvider.SQLite] = new( - DatabaseProvider.SQLite, - [ConnectionContextLevel.Connection, ConnectionContextLevel.Schema]), - }; - - public IReadOnlyList GetSupportedLevels(DatabaseProvider provider) - { - if (Capabilities.TryGetValue(provider, out ConnectionCatalogCapabilityDto? capability)) - return capability.SupportedLevels; - - return [ConnectionContextLevel.Connection, ConnectionContextLevel.Schema]; - } -} +using AkkornStudio.Core; +using AkkornStudio.UI.Services.ConnectionManager.Contracts; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ConnectionCatalogCapabilityProvider : IConnectionCatalogCapabilityProvider +{ + private static readonly IReadOnlyDictionary Capabilities = + new Dictionary + { + [DatabaseProvider.Postgres] = new( + DatabaseProvider.Postgres, + [ConnectionContextLevel.Connection, ConnectionContextLevel.DatabaseOrCatalog, ConnectionContextLevel.Schema]), + [DatabaseProvider.MySql] = new( + DatabaseProvider.MySql, + [ConnectionContextLevel.Connection, ConnectionContextLevel.DatabaseOrCatalog, ConnectionContextLevel.Schema]), + [DatabaseProvider.SqlServer] = new( + DatabaseProvider.SqlServer, + [ConnectionContextLevel.Connection, ConnectionContextLevel.DatabaseOrCatalog, ConnectionContextLevel.Schema]), + [DatabaseProvider.SQLite] = new( + DatabaseProvider.SQLite, + [ConnectionContextLevel.Connection, ConnectionContextLevel.Schema]), + }; + + public IReadOnlyList GetSupportedLevels(DatabaseProvider provider) + { + if (Capabilities.TryGetValue(provider, out ConnectionCatalogCapabilityDto? capability)) + return capability.SupportedLevels; + + return [ConnectionContextLevel.Connection, ConnectionContextLevel.Schema]; + } +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCatalogService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCatalogService.cs similarity index 95% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCatalogService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCatalogService.cs index fabef829..f32cfdd8 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionCatalogService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionCatalogService.cs @@ -1,207 +1,207 @@ -using DBWeaver.UI.Services.Connection; -using DBWeaver.UI.Services.ConnectionManager.Contracts; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ConnectionCatalogService : IConnectionCatalogService -{ - private readonly IConnectionProfileStore _profileStore; - private readonly CredentialVaultStore _credentialVault; - private readonly SemaphoreSlim _gate = new(1, 1); - - public ConnectionCatalogService(IConnectionProfileStore profileStore) - { - _profileStore = profileStore; - _credentialVault = new CredentialVaultStore(); - } - - public async Task> ListSummariesAsync(CancellationToken cancellationToken = default) - { - await _gate.WaitAsync(cancellationToken); - try - { - IReadOnlyList profiles = _profileStore.LoadProfiles(_credentialVault); - return profiles - .OrderBy(static p => p.Name, StringComparer.OrdinalIgnoreCase) - .Select(static p => ConnectionContractMapper.ToSummary(p, isActive: false)) - .ToArray(); - } - finally - { - _gate.Release(); - } - } - - public async Task> GetDetailsAsync( - string connectionId, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(connectionId)) - return Fail(ConnectionOperationSemanticErrorCode.ValidationFailed, "Connection id is required."); - - await _gate.WaitAsync(cancellationToken); - try - { - ConnectionProfile? profile = _profileStore - .LoadProfiles(_credentialVault) - .FirstOrDefault(p => string.Equals(p.Id, connectionId, StringComparison.Ordinal)); - - if (profile is null) - return Fail(ConnectionOperationSemanticErrorCode.NotFound, "Connection not found."); - - return Ok(ConnectionContractMapper.ToDetails(profile)); - } - finally - { - _gate.Release(); - } - } - - public async Task> SaveAsync( - ConnectionDetailsDto details, - CancellationToken cancellationToken = default) - { - await _gate.WaitAsync(cancellationToken); - try - { - List profiles = _profileStore.LoadProfiles(_credentialVault).ToList(); - ConnectionProfile profile = ConnectionContractMapper.ToProfile(details); - - int existingIndex = profiles.FindIndex(p => string.Equals(p.Id, profile.Id, StringComparison.Ordinal)); - if (existingIndex >= 0) - profiles[existingIndex] = profile; - else - profiles.Add(profile); - - _profileStore.PersistProfiles(profiles, _credentialVault); - return Ok(ConnectionContractMapper.ToDetails(profile)); - } - catch (Exception ex) - { - return Fail(ConnectionOperationSemanticErrorCode.Unknown, "Could not save connection.", ex.Message); - } - finally - { - _gate.Release(); - } - } - - public async Task> DuplicateAsync( - string connectionId, - string? newName = null, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(connectionId)) - return Fail(ConnectionOperationSemanticErrorCode.ValidationFailed, "Connection id is required."); - - await _gate.WaitAsync(cancellationToken); - try - { - List profiles = _profileStore.LoadProfiles(_credentialVault).ToList(); - ConnectionProfile? source = profiles.FirstOrDefault(p => string.Equals(p.Id, connectionId, StringComparison.Ordinal)); - if (source is null) - return Fail(ConnectionOperationSemanticErrorCode.NotFound, "Connection not found."); - - string candidateName = string.IsNullOrWhiteSpace(newName) - ? $"{source.Name} Copy" - : newName.Trim(); - - string uniqueName = EnsureUniqueName(candidateName, profiles); - var duplicate = new ConnectionProfile - { - Id = Guid.NewGuid().ToString(), - Name = uniqueName, - Provider = source.Provider, - Host = source.Host, - Port = source.Port, - Database = source.Database, - Username = source.Username, - Password = source.Password, - RememberPassword = source.RememberPassword, - UseSsl = source.UseSsl, - TrustServerCertificate = source.TrustServerCertificate, - UseIntegratedSecurity = source.UseIntegratedSecurity, - TimeoutSeconds = source.TimeoutSeconds, - }; - - profiles.Add(duplicate); - _profileStore.PersistProfiles(profiles, _credentialVault); - - return Ok(ConnectionContractMapper.ToDetails(duplicate)); - } - catch (Exception ex) - { - return Fail(ConnectionOperationSemanticErrorCode.Unknown, "Could not duplicate connection.", ex.Message); - } - finally - { - _gate.Release(); - } - } - - public async Task> DeleteAsync( - string connectionId, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(connectionId)) - return Fail(ConnectionOperationSemanticErrorCode.ValidationFailed, "Connection id is required."); - - await _gate.WaitAsync(cancellationToken); - try - { - List profiles = _profileStore.LoadProfiles(_credentialVault).ToList(); - ConnectionProfile? profile = profiles.FirstOrDefault(p => string.Equals(p.Id, connectionId, StringComparison.Ordinal)); - if (profile is null) - return Fail(ConnectionOperationSemanticErrorCode.NotFound, "Connection not found."); - - profiles.Remove(profile); - _profileStore.PersistProfiles(profiles, _credentialVault); - _credentialVault.RemoveSecret(connectionId); - - return Ok(true); - } - catch (Exception ex) - { - return Fail(ConnectionOperationSemanticErrorCode.Unknown, "Could not delete connection.", ex.Message); - } - finally - { - _gate.Release(); - } - } - - private static string EnsureUniqueName(string initialName, IReadOnlyList profiles) - { - string candidate = initialName; - int suffix = 2; - - while (profiles.Any(p => string.Equals(p.Name, candidate, StringComparison.OrdinalIgnoreCase))) - { - candidate = $"{initialName} ({suffix})"; - suffix++; - } - - return candidate; - } - - private static OperationResultDto Ok(T payload) => - new( - Success: true, - SemanticErrorCode: ConnectionOperationSemanticErrorCode.None, - UserMessage: string.Empty, - Payload: payload, - TechnicalError: null, - CorrelationId: null); - - private static OperationResultDto Fail( - ConnectionOperationSemanticErrorCode code, - string message, - string? technicalError = null) => - new( - Success: false, - SemanticErrorCode: code, - UserMessage: message, - Payload: default, - TechnicalError: technicalError, - CorrelationId: null); -} +using AkkornStudio.UI.Services.Connection; +using AkkornStudio.UI.Services.ConnectionManager.Contracts; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ConnectionCatalogService : IConnectionCatalogService +{ + private readonly IConnectionProfileStore _profileStore; + private readonly CredentialVaultStore _credentialVault; + private readonly SemaphoreSlim _gate = new(1, 1); + + public ConnectionCatalogService(IConnectionProfileStore profileStore) + { + _profileStore = profileStore; + _credentialVault = new CredentialVaultStore(); + } + + public async Task> ListSummariesAsync(CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken); + try + { + IReadOnlyList profiles = _profileStore.LoadProfiles(_credentialVault); + return profiles + .OrderBy(static p => p.Name, StringComparer.OrdinalIgnoreCase) + .Select(static p => ConnectionContractMapper.ToSummary(p, isActive: false)) + .ToArray(); + } + finally + { + _gate.Release(); + } + } + + public async Task> GetDetailsAsync( + string connectionId, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(connectionId)) + return Fail(ConnectionOperationSemanticErrorCode.ValidationFailed, "Connection id is required."); + + await _gate.WaitAsync(cancellationToken); + try + { + ConnectionProfile? profile = _profileStore + .LoadProfiles(_credentialVault) + .FirstOrDefault(p => string.Equals(p.Id, connectionId, StringComparison.Ordinal)); + + if (profile is null) + return Fail(ConnectionOperationSemanticErrorCode.NotFound, "Connection not found."); + + return Ok(ConnectionContractMapper.ToDetails(profile)); + } + finally + { + _gate.Release(); + } + } + + public async Task> SaveAsync( + ConnectionDetailsDto details, + CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken); + try + { + List profiles = _profileStore.LoadProfiles(_credentialVault).ToList(); + ConnectionProfile profile = ConnectionContractMapper.ToProfile(details); + + int existingIndex = profiles.FindIndex(p => string.Equals(p.Id, profile.Id, StringComparison.Ordinal)); + if (existingIndex >= 0) + profiles[existingIndex] = profile; + else + profiles.Add(profile); + + _profileStore.PersistProfiles(profiles, _credentialVault); + return Ok(ConnectionContractMapper.ToDetails(profile)); + } + catch (Exception ex) + { + return Fail(ConnectionOperationSemanticErrorCode.Unknown, "Could not save connection.", ex.Message); + } + finally + { + _gate.Release(); + } + } + + public async Task> DuplicateAsync( + string connectionId, + string? newName = null, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(connectionId)) + return Fail(ConnectionOperationSemanticErrorCode.ValidationFailed, "Connection id is required."); + + await _gate.WaitAsync(cancellationToken); + try + { + List profiles = _profileStore.LoadProfiles(_credentialVault).ToList(); + ConnectionProfile? source = profiles.FirstOrDefault(p => string.Equals(p.Id, connectionId, StringComparison.Ordinal)); + if (source is null) + return Fail(ConnectionOperationSemanticErrorCode.NotFound, "Connection not found."); + + string candidateName = string.IsNullOrWhiteSpace(newName) + ? $"{source.Name} Copy" + : newName.Trim(); + + string uniqueName = EnsureUniqueName(candidateName, profiles); + var duplicate = new ConnectionProfile + { + Id = Guid.NewGuid().ToString(), + Name = uniqueName, + Provider = source.Provider, + Host = source.Host, + Port = source.Port, + Database = source.Database, + Username = source.Username, + Password = source.Password, + RememberPassword = source.RememberPassword, + UseSsl = source.UseSsl, + TrustServerCertificate = source.TrustServerCertificate, + UseIntegratedSecurity = source.UseIntegratedSecurity, + TimeoutSeconds = source.TimeoutSeconds, + }; + + profiles.Add(duplicate); + _profileStore.PersistProfiles(profiles, _credentialVault); + + return Ok(ConnectionContractMapper.ToDetails(duplicate)); + } + catch (Exception ex) + { + return Fail(ConnectionOperationSemanticErrorCode.Unknown, "Could not duplicate connection.", ex.Message); + } + finally + { + _gate.Release(); + } + } + + public async Task> DeleteAsync( + string connectionId, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(connectionId)) + return Fail(ConnectionOperationSemanticErrorCode.ValidationFailed, "Connection id is required."); + + await _gate.WaitAsync(cancellationToken); + try + { + List profiles = _profileStore.LoadProfiles(_credentialVault).ToList(); + ConnectionProfile? profile = profiles.FirstOrDefault(p => string.Equals(p.Id, connectionId, StringComparison.Ordinal)); + if (profile is null) + return Fail(ConnectionOperationSemanticErrorCode.NotFound, "Connection not found."); + + profiles.Remove(profile); + _profileStore.PersistProfiles(profiles, _credentialVault); + _credentialVault.RemoveSecret(connectionId); + + return Ok(true); + } + catch (Exception ex) + { + return Fail(ConnectionOperationSemanticErrorCode.Unknown, "Could not delete connection.", ex.Message); + } + finally + { + _gate.Release(); + } + } + + private static string EnsureUniqueName(string initialName, IReadOnlyList profiles) + { + string candidate = initialName; + int suffix = 2; + + while (profiles.Any(p => string.Equals(p.Name, candidate, StringComparison.OrdinalIgnoreCase))) + { + candidate = $"{initialName} ({suffix})"; + suffix++; + } + + return candidate; + } + + private static OperationResultDto Ok(T payload) => + new( + Success: true, + SemanticErrorCode: ConnectionOperationSemanticErrorCode.None, + UserMessage: string.Empty, + Payload: payload, + TechnicalError: null, + CorrelationId: null); + + private static OperationResultDto Fail( + ConnectionOperationSemanticErrorCode code, + string message, + string? technicalError = null) => + new( + Success: false, + SemanticErrorCode: code, + UserMessage: message, + Payload: default, + TechnicalError: technicalError, + CorrelationId: null); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionContractMapper.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionContractMapper.cs similarity index 95% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionContractMapper.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionContractMapper.cs index 579e3880..74e6abcf 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionContractMapper.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionContractMapper.cs @@ -1,158 +1,158 @@ -using DBWeaver.Core; -using DBWeaver.UI.Services.ConnectionManager.Contracts; - -namespace DBWeaver.UI.Services.ConnectionManager; - -internal static class ConnectionContractMapper -{ - internal const string HostKey = "host"; - internal const string PortKey = "port"; - internal const string DatabaseKey = "database"; - internal const string UsernameKey = "username"; - internal const string PasswordKey = "password"; - internal const string RememberPasswordKey = "rememberPassword"; - internal const string UseSslKey = "useSsl"; - internal const string TrustServerCertificateKey = "trustServerCertificate"; - internal const string UseIntegratedSecurityKey = "useIntegratedSecurity"; - internal const string TimeoutSecondsKey = "timeoutSeconds"; - - public static ConnectionSummaryDto ToSummary(ConnectionProfile profile, bool isActive) - { - string summary = profile.Provider == DatabaseProvider.SQLite - ? profile.Database - : $"{profile.Host}:{profile.Port}/{profile.Database}"; - - return new ConnectionSummaryDto( - Id: profile.Id, - Name: profile.Name, - Provider: profile.Provider.ToString(), - SummaryText: summary, - IsFavorite: false, - IsActive: isActive, - LastUsedAt: null, - LastTestStatus: ConnectionTestStatusDto.NotTested, - LastTestAt: null); - } - - public static ConnectionDetailsDto ToDetails(ConnectionProfile profile) - { - var fields = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - [HostKey] = profile.Host, - [PortKey] = profile.Port.ToString(), - [DatabaseKey] = profile.Database, - [UsernameKey] = profile.Username, - [PasswordKey] = profile.Password, - [RememberPasswordKey] = profile.RememberPassword.ToString(), - [UseSslKey] = profile.UseSsl.ToString(), - [TrustServerCertificateKey] = profile.TrustServerCertificate.ToString(), - [UseIntegratedSecurityKey] = profile.UseIntegratedSecurity.ToString(), - [TimeoutSecondsKey] = profile.TimeoutSeconds.ToString(), - }; - - return new ConnectionDetailsDto( - Id: profile.Id, - Name: profile.Name, - Provider: profile.Provider.ToString(), - Mode: ConnectionProviderModeDto.Fields, - FieldValues: fields, - UrlValue: null, - Tag: null, - IsFavorite: false, - AdvancedOptions: new Dictionary()); - } - - public static ConnectionProfile ToProfile( - ConnectionDetailsDto details, - IReadOnlyDictionary? fieldsOverride = null, - string? providerOverride = null) - { - DatabaseProvider provider = ParseProvider(providerOverride ?? details.Provider); - IReadOnlyDictionary fields = fieldsOverride ?? details.FieldValues; - - int defaultPort = ConnectionProfile.DefaultPort(provider); - int port = provider == DatabaseProvider.SQLite - ? 0 - : ParseInt(fields, PortKey, defaultPort); - - bool useIntegratedSecurity = ParseBool(fields, UseIntegratedSecurityKey, false) - && provider == DatabaseProvider.SqlServer - && OperatingSystem.IsWindows(); - - return new ConnectionProfile - { - Id = string.IsNullOrWhiteSpace(details.Id) ? Guid.NewGuid().ToString() : details.Id, - Name = string.IsNullOrWhiteSpace(details.Name) ? "New Connection" : details.Name.Trim(), - Provider = provider, - Host = provider == DatabaseProvider.SQLite - ? AppConstants.DefaultHost - : ParseString(fields, HostKey, AppConstants.DefaultHost), - Port = port, - Database = ParseString(fields, DatabaseKey, string.Empty), - Username = ParseString(fields, UsernameKey, string.Empty), - Password = ParseString(fields, PasswordKey, string.Empty), - RememberPassword = ParseBool(fields, RememberPasswordKey, true), - UseSsl = ParseBool(fields, UseSslKey, false), - TrustServerCertificate = ParseBool(fields, TrustServerCertificateKey, true), - UseIntegratedSecurity = useIntegratedSecurity, - TimeoutSeconds = Math.Max(1, ParseInt(fields, TimeoutSecondsKey, 30)), - }; - } - - public static bool TryParseProvider(string provider, out DatabaseProvider parsed) - { - parsed = provider.Trim().ToLowerInvariant() switch - { - "postgres" or "postgresql" => DatabaseProvider.Postgres, - "mysql" => DatabaseProvider.MySql, - "sqlserver" or "mssql" => DatabaseProvider.SqlServer, - "sqlite" or "file" => DatabaseProvider.SQLite, - _ => default, - }; - - return provider.Equals("postgres", StringComparison.OrdinalIgnoreCase) - || provider.Equals("postgresql", StringComparison.OrdinalIgnoreCase) - || provider.Equals("mysql", StringComparison.OrdinalIgnoreCase) - || provider.Equals("sqlserver", StringComparison.OrdinalIgnoreCase) - || provider.Equals("mssql", StringComparison.OrdinalIgnoreCase) - || provider.Equals("sqlite", StringComparison.OrdinalIgnoreCase) - || provider.Equals("file", StringComparison.OrdinalIgnoreCase); - } - - private static DatabaseProvider ParseProvider(string provider) => - TryParseProvider(provider, out DatabaseProvider parsed) - ? parsed - : DatabaseProvider.Postgres; - - private static string ParseString(IReadOnlyDictionary fields, string key, string fallback) - { - if (!fields.TryGetValue(key, out string? raw) || string.IsNullOrWhiteSpace(raw)) - return fallback; - - return raw.Trim(); - } - - private static int ParseInt(IReadOnlyDictionary fields, string key, int fallback) - { - if (!fields.TryGetValue(key, out string? raw) || !int.TryParse(raw, out int parsed)) - return fallback; - - return parsed; - } - - private static bool ParseBool(IReadOnlyDictionary fields, string key, bool fallback) - { - if (!fields.TryGetValue(key, out string? raw) || string.IsNullOrWhiteSpace(raw)) - return fallback; - - if (bool.TryParse(raw, out bool parsed)) - return parsed; - - return raw.Equals("1", StringComparison.OrdinalIgnoreCase) - || raw.Equals("yes", StringComparison.OrdinalIgnoreCase) - || raw.Equals("on", StringComparison.OrdinalIgnoreCase) - || raw.Equals("required", StringComparison.OrdinalIgnoreCase) - || raw.Equals("require", StringComparison.OrdinalIgnoreCase) - || raw.Equals("true", StringComparison.OrdinalIgnoreCase); - } -} +using AkkornStudio.Core; +using AkkornStudio.UI.Services.ConnectionManager.Contracts; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +internal static class ConnectionContractMapper +{ + internal const string HostKey = "host"; + internal const string PortKey = "port"; + internal const string DatabaseKey = "database"; + internal const string UsernameKey = "username"; + internal const string PasswordKey = "password"; + internal const string RememberPasswordKey = "rememberPassword"; + internal const string UseSslKey = "useSsl"; + internal const string TrustServerCertificateKey = "trustServerCertificate"; + internal const string UseIntegratedSecurityKey = "useIntegratedSecurity"; + internal const string TimeoutSecondsKey = "timeoutSeconds"; + + public static ConnectionSummaryDto ToSummary(ConnectionProfile profile, bool isActive) + { + string summary = profile.Provider == DatabaseProvider.SQLite + ? profile.Database + : $"{profile.Host}:{profile.Port}/{profile.Database}"; + + return new ConnectionSummaryDto( + Id: profile.Id, + Name: profile.Name, + Provider: profile.Provider.ToString(), + SummaryText: summary, + IsFavorite: false, + IsActive: isActive, + LastUsedAt: null, + LastTestStatus: ConnectionTestStatusDto.NotTested, + LastTestAt: null); + } + + public static ConnectionDetailsDto ToDetails(ConnectionProfile profile) + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [HostKey] = profile.Host, + [PortKey] = profile.Port.ToString(), + [DatabaseKey] = profile.Database, + [UsernameKey] = profile.Username, + [PasswordKey] = profile.Password, + [RememberPasswordKey] = profile.RememberPassword.ToString(), + [UseSslKey] = profile.UseSsl.ToString(), + [TrustServerCertificateKey] = profile.TrustServerCertificate.ToString(), + [UseIntegratedSecurityKey] = profile.UseIntegratedSecurity.ToString(), + [TimeoutSecondsKey] = profile.TimeoutSeconds.ToString(), + }; + + return new ConnectionDetailsDto( + Id: profile.Id, + Name: profile.Name, + Provider: profile.Provider.ToString(), + Mode: ConnectionProviderModeDto.Fields, + FieldValues: fields, + UrlValue: null, + Tag: null, + IsFavorite: false, + AdvancedOptions: new Dictionary()); + } + + public static ConnectionProfile ToProfile( + ConnectionDetailsDto details, + IReadOnlyDictionary? fieldsOverride = null, + string? providerOverride = null) + { + DatabaseProvider provider = ParseProvider(providerOverride ?? details.Provider); + IReadOnlyDictionary fields = fieldsOverride ?? details.FieldValues; + + int defaultPort = ConnectionProfile.DefaultPort(provider); + int port = provider == DatabaseProvider.SQLite + ? 0 + : ParseInt(fields, PortKey, defaultPort); + + bool useIntegratedSecurity = ParseBool(fields, UseIntegratedSecurityKey, false) + && provider == DatabaseProvider.SqlServer + && OperatingSystem.IsWindows(); + + return new ConnectionProfile + { + Id = string.IsNullOrWhiteSpace(details.Id) ? Guid.NewGuid().ToString() : details.Id, + Name = string.IsNullOrWhiteSpace(details.Name) ? "New Connection" : details.Name.Trim(), + Provider = provider, + Host = provider == DatabaseProvider.SQLite + ? AppConstants.DefaultHost + : ParseString(fields, HostKey, AppConstants.DefaultHost), + Port = port, + Database = ParseString(fields, DatabaseKey, string.Empty), + Username = ParseString(fields, UsernameKey, string.Empty), + Password = ParseString(fields, PasswordKey, string.Empty), + RememberPassword = ParseBool(fields, RememberPasswordKey, true), + UseSsl = ParseBool(fields, UseSslKey, false), + TrustServerCertificate = ParseBool(fields, TrustServerCertificateKey, true), + UseIntegratedSecurity = useIntegratedSecurity, + TimeoutSeconds = Math.Max(1, ParseInt(fields, TimeoutSecondsKey, 30)), + }; + } + + public static bool TryParseProvider(string provider, out DatabaseProvider parsed) + { + parsed = provider.Trim().ToLowerInvariant() switch + { + "postgres" or "postgresql" => DatabaseProvider.Postgres, + "mysql" => DatabaseProvider.MySql, + "sqlserver" or "mssql" => DatabaseProvider.SqlServer, + "sqlite" or "file" => DatabaseProvider.SQLite, + _ => default, + }; + + return provider.Equals("postgres", StringComparison.OrdinalIgnoreCase) + || provider.Equals("postgresql", StringComparison.OrdinalIgnoreCase) + || provider.Equals("mysql", StringComparison.OrdinalIgnoreCase) + || provider.Equals("sqlserver", StringComparison.OrdinalIgnoreCase) + || provider.Equals("mssql", StringComparison.OrdinalIgnoreCase) + || provider.Equals("sqlite", StringComparison.OrdinalIgnoreCase) + || provider.Equals("file", StringComparison.OrdinalIgnoreCase); + } + + private static DatabaseProvider ParseProvider(string provider) => + TryParseProvider(provider, out DatabaseProvider parsed) + ? parsed + : DatabaseProvider.Postgres; + + private static string ParseString(IReadOnlyDictionary fields, string key, string fallback) + { + if (!fields.TryGetValue(key, out string? raw) || string.IsNullOrWhiteSpace(raw)) + return fallback; + + return raw.Trim(); + } + + private static int ParseInt(IReadOnlyDictionary fields, string key, int fallback) + { + if (!fields.TryGetValue(key, out string? raw) || !int.TryParse(raw, out int parsed)) + return fallback; + + return parsed; + } + + private static bool ParseBool(IReadOnlyDictionary fields, string key, bool fallback) + { + if (!fields.TryGetValue(key, out string? raw) || string.IsNullOrWhiteSpace(raw)) + return fallback; + + if (bool.TryParse(raw, out bool parsed)) + return parsed; + + return raw.Equals("1", StringComparison.OrdinalIgnoreCase) + || raw.Equals("yes", StringComparison.OrdinalIgnoreCase) + || raw.Equals("on", StringComparison.OrdinalIgnoreCase) + || raw.Equals("required", StringComparison.OrdinalIgnoreCase) + || raw.Equals("require", StringComparison.OrdinalIgnoreCase) + || raw.Equals("true", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionHealthLifecycleCoordinator.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionHealthLifecycleCoordinator.cs similarity index 93% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionHealthLifecycleCoordinator.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionHealthLifecycleCoordinator.cs index c178a128..7c6ad568 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionHealthLifecycleCoordinator.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionHealthLifecycleCoordinator.cs @@ -1,6 +1,6 @@ -using DBWeaver.Core; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionHealthLifecycleCoordinator( IConnectionHealthMonitorService healthMonitorService) : IConnectionHealthLifecycleCoordinator diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionHealthMonitorService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionHealthMonitorService.cs similarity index 96% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionHealthMonitorService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionHealthMonitorService.cs index ff541fe1..f2a47217 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionHealthMonitorService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionHealthMonitorService.cs @@ -1,6 +1,6 @@ -using DBWeaver.Core; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionHealthMonitorService : IConnectionHealthMonitorService { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionManagerViewModelFactory.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionManagerViewModelFactory.cs similarity index 95% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionManagerViewModelFactory.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionManagerViewModelFactory.cs index 0021d566..8832ad28 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionManagerViewModelFactory.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionManagerViewModelFactory.cs @@ -1,133 +1,133 @@ -using DBWeaver.UI.Services.ConnectionManager.Contracts; -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.Services.Modal; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ConnectionManagerViewModelFactory : IConnectionManagerViewModelFactory -{ - private readonly IConnectionErrorMessageMapper _errorMessageMapper; - private readonly IConnectionStatusPresenter _statusPresenter; - private readonly IConnectionCanvasPromptCoordinator _canvasPromptCoordinator; - private readonly IConnectionHealthMonitorService _healthMonitorService; - private readonly IConnectionSessionOrchestrator _sessionOrchestrator; - private readonly IConnectionProfileStore _profileStore; - private readonly IConnectionTestExecutor _connectionTestExecutor; - private readonly IConnectionCatalogService _connectionCatalogService; - private readonly IConnectionTestService _connectionTestService; - private readonly IConnectionSessionService _connectionSessionService; - private readonly IConnectionUrlParserService _connectionUrlParserService; - private readonly IConnectionProfileFormMapper _formMapper; - private readonly IConnectionActivationWorkflow _activationWorkflow; - private readonly IFireAndForgetSafetyExecutor _fireAndForgetSafetyExecutor; - private readonly IConnectionHealthLifecycleCoordinator _healthLifecycleCoordinator; - private readonly IGlobalModalManager _globalModalManager; - - public static IConnectionManagerViewModelFactory CreateDefault( - ILocalizationService? localizationService = null, - ILogger? connectionManagerLogger = null) - { - ILocalizationService localization = localizationService ?? LocalizationService.Instance; - - var errorMessageMapper = new ConnectionErrorMessageMapper(localization); - var statusPresenter = new ConnectionStatusPresenter(localization); - var canvasPromptCoordinator = new ConnectionCanvasPromptCoordinator(); - var healthMonitorService = new ConnectionHealthMonitorService(); - var sessionOrchestrator = new ConnectionSessionOrchestrator(); - var profileStore = new ConnectionProfileStore(); - var connectionTestExecutor = new DbOrchestratorConnectionTestExecutor(); - var connectionCatalogService = new ConnectionCatalogService(profileStore); - var connectionValidationService = new ConnectionValidationService(); - var providerCapabilityService = new ProviderCapabilityService(); - var connectionUrlParserService = new ConnectionUrlParserService(); - var connectionTestService = new ConnectionTestService( - connectionTestExecutor, - connectionValidationService, - providerCapabilityService, - connectionUrlParserService); - var connectionTelemetryService = new ConnectionTelemetryService(NullLogger.Instance); - var connectionSessionService = new ConnectionSessionService(connectionTestService, connectionTelemetryService); - var formMapper = new ConnectionProfileFormMapper(localization); - var activationWorkflow = new ConnectionActivationWorkflow(); - var fireAndForgetSafetyExecutor = new FireAndForgetSafetyExecutor( - connectionManagerLogger ?? NullLogger.Instance); - var healthLifecycleCoordinator = new ConnectionHealthLifecycleCoordinator(healthMonitorService); - - return new ConnectionManagerViewModelFactory( - errorMessageMapper, - statusPresenter, - canvasPromptCoordinator, - healthMonitorService, - sessionOrchestrator, - profileStore, - connectionTestExecutor, - connectionCatalogService, - connectionTestService, - connectionSessionService, - connectionUrlParserService, - formMapper, - activationWorkflow, - fireAndForgetSafetyExecutor, - healthLifecycleCoordinator, - GlobalModalManager.Instance); - } - - public ConnectionManagerViewModelFactory( - IConnectionErrorMessageMapper errorMessageMapper, - IConnectionStatusPresenter statusPresenter, - IConnectionCanvasPromptCoordinator canvasPromptCoordinator, - IConnectionHealthMonitorService healthMonitorService, - IConnectionSessionOrchestrator sessionOrchestrator, - IConnectionProfileStore profileStore, - IConnectionTestExecutor connectionTestExecutor, - IConnectionCatalogService connectionCatalogService, - IConnectionTestService connectionTestService, - IConnectionSessionService connectionSessionService, - IConnectionUrlParserService connectionUrlParserService, - IConnectionProfileFormMapper formMapper, - IConnectionActivationWorkflow activationWorkflow, - IFireAndForgetSafetyExecutor fireAndForgetSafetyExecutor, - IConnectionHealthLifecycleCoordinator healthLifecycleCoordinator, - IGlobalModalManager globalModalManager) - { - _errorMessageMapper = errorMessageMapper; - _statusPresenter = statusPresenter; - _canvasPromptCoordinator = canvasPromptCoordinator; - _healthMonitorService = healthMonitorService; - _sessionOrchestrator = sessionOrchestrator; - _profileStore = profileStore; - _connectionTestExecutor = connectionTestExecutor; - _connectionCatalogService = connectionCatalogService; - _connectionTestService = connectionTestService; - _connectionSessionService = connectionSessionService; - _connectionUrlParserService = connectionUrlParserService; - _formMapper = formMapper; - _activationWorkflow = activationWorkflow; - _fireAndForgetSafetyExecutor = fireAndForgetSafetyExecutor; - _healthLifecycleCoordinator = healthLifecycleCoordinator; - _globalModalManager = globalModalManager; - } - - public ConnectionManagerViewModel Create() - { - return new ConnectionManagerViewModel( - errorMessageMapper: _errorMessageMapper, - statusPresenter: _statusPresenter, - canvasPromptCoordinator: _canvasPromptCoordinator, - healthMonitorService: _healthMonitorService, - sessionOrchestrator: _sessionOrchestrator, - profileStore: _profileStore, - connectionTestExecutor: _connectionTestExecutor, - connectionCatalogService: _connectionCatalogService, - connectionTestService: _connectionTestService, - connectionSessionService: _connectionSessionService, - connectionUrlParserService: _connectionUrlParserService, - formMapper: _formMapper, - activationWorkflow: _activationWorkflow, - fireAndForgetSafetyExecutor: _fireAndForgetSafetyExecutor, - healthLifecycleCoordinator: _healthLifecycleCoordinator, - globalModalManager: _globalModalManager); - } -} +using AkkornStudio.UI.Services.ConnectionManager.Contracts; +using AkkornStudio.UI.Services.Localization; +using AkkornStudio.UI.Services.Modal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ConnectionManagerViewModelFactory : IConnectionManagerViewModelFactory +{ + private readonly IConnectionErrorMessageMapper _errorMessageMapper; + private readonly IConnectionStatusPresenter _statusPresenter; + private readonly IConnectionCanvasPromptCoordinator _canvasPromptCoordinator; + private readonly IConnectionHealthMonitorService _healthMonitorService; + private readonly IConnectionSessionOrchestrator _sessionOrchestrator; + private readonly IConnectionProfileStore _profileStore; + private readonly IConnectionTestExecutor _connectionTestExecutor; + private readonly IConnectionCatalogService _connectionCatalogService; + private readonly IConnectionTestService _connectionTestService; + private readonly IConnectionSessionService _connectionSessionService; + private readonly IConnectionUrlParserService _connectionUrlParserService; + private readonly IConnectionProfileFormMapper _formMapper; + private readonly IConnectionActivationWorkflow _activationWorkflow; + private readonly IFireAndForgetSafetyExecutor _fireAndForgetSafetyExecutor; + private readonly IConnectionHealthLifecycleCoordinator _healthLifecycleCoordinator; + private readonly IGlobalModalManager _globalModalManager; + + public static IConnectionManagerViewModelFactory CreateDefault( + ILocalizationService? localizationService = null, + ILogger? connectionManagerLogger = null) + { + ILocalizationService localization = localizationService ?? LocalizationService.Instance; + + var errorMessageMapper = new ConnectionErrorMessageMapper(localization); + var statusPresenter = new ConnectionStatusPresenter(localization); + var canvasPromptCoordinator = new ConnectionCanvasPromptCoordinator(); + var healthMonitorService = new ConnectionHealthMonitorService(); + var sessionOrchestrator = new ConnectionSessionOrchestrator(); + var profileStore = new ConnectionProfileStore(); + var connectionTestExecutor = new DbOrchestratorConnectionTestExecutor(); + var connectionCatalogService = new ConnectionCatalogService(profileStore); + var connectionValidationService = new ConnectionValidationService(); + var providerCapabilityService = new ProviderCapabilityService(); + var connectionUrlParserService = new ConnectionUrlParserService(); + var connectionTestService = new ConnectionTestService( + connectionTestExecutor, + connectionValidationService, + providerCapabilityService, + connectionUrlParserService); + var connectionTelemetryService = new ConnectionTelemetryService(NullLogger.Instance); + var connectionSessionService = new ConnectionSessionService(connectionTestService, connectionTelemetryService); + var formMapper = new ConnectionProfileFormMapper(localization); + var activationWorkflow = new ConnectionActivationWorkflow(); + var fireAndForgetSafetyExecutor = new FireAndForgetSafetyExecutor( + connectionManagerLogger ?? NullLogger.Instance); + var healthLifecycleCoordinator = new ConnectionHealthLifecycleCoordinator(healthMonitorService); + + return new ConnectionManagerViewModelFactory( + errorMessageMapper, + statusPresenter, + canvasPromptCoordinator, + healthMonitorService, + sessionOrchestrator, + profileStore, + connectionTestExecutor, + connectionCatalogService, + connectionTestService, + connectionSessionService, + connectionUrlParserService, + formMapper, + activationWorkflow, + fireAndForgetSafetyExecutor, + healthLifecycleCoordinator, + GlobalModalManager.Instance); + } + + public ConnectionManagerViewModelFactory( + IConnectionErrorMessageMapper errorMessageMapper, + IConnectionStatusPresenter statusPresenter, + IConnectionCanvasPromptCoordinator canvasPromptCoordinator, + IConnectionHealthMonitorService healthMonitorService, + IConnectionSessionOrchestrator sessionOrchestrator, + IConnectionProfileStore profileStore, + IConnectionTestExecutor connectionTestExecutor, + IConnectionCatalogService connectionCatalogService, + IConnectionTestService connectionTestService, + IConnectionSessionService connectionSessionService, + IConnectionUrlParserService connectionUrlParserService, + IConnectionProfileFormMapper formMapper, + IConnectionActivationWorkflow activationWorkflow, + IFireAndForgetSafetyExecutor fireAndForgetSafetyExecutor, + IConnectionHealthLifecycleCoordinator healthLifecycleCoordinator, + IGlobalModalManager globalModalManager) + { + _errorMessageMapper = errorMessageMapper; + _statusPresenter = statusPresenter; + _canvasPromptCoordinator = canvasPromptCoordinator; + _healthMonitorService = healthMonitorService; + _sessionOrchestrator = sessionOrchestrator; + _profileStore = profileStore; + _connectionTestExecutor = connectionTestExecutor; + _connectionCatalogService = connectionCatalogService; + _connectionTestService = connectionTestService; + _connectionSessionService = connectionSessionService; + _connectionUrlParserService = connectionUrlParserService; + _formMapper = formMapper; + _activationWorkflow = activationWorkflow; + _fireAndForgetSafetyExecutor = fireAndForgetSafetyExecutor; + _healthLifecycleCoordinator = healthLifecycleCoordinator; + _globalModalManager = globalModalManager; + } + + public ConnectionManagerViewModel Create() + { + return new ConnectionManagerViewModel( + errorMessageMapper: _errorMessageMapper, + statusPresenter: _statusPresenter, + canvasPromptCoordinator: _canvasPromptCoordinator, + healthMonitorService: _healthMonitorService, + sessionOrchestrator: _sessionOrchestrator, + profileStore: _profileStore, + connectionTestExecutor: _connectionTestExecutor, + connectionCatalogService: _connectionCatalogService, + connectionTestService: _connectionTestService, + connectionSessionService: _connectionSessionService, + connectionUrlParserService: _connectionUrlParserService, + formMapper: _formMapper, + activationWorkflow: _activationWorkflow, + fireAndForgetSafetyExecutor: _fireAndForgetSafetyExecutor, + healthLifecycleCoordinator: _healthLifecycleCoordinator, + globalModalManager: _globalModalManager); + } +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionProfileLifecycleService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionProfileLifecycleService.cs similarity index 96% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionProfileLifecycleService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionProfileLifecycleService.cs index c422c56c..f3d7e987 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionProfileLifecycleService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionProfileLifecycleService.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionProfileLifecycleService : IConnectionProfileLifecycleService { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionSessionOrchestrator.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionSessionOrchestrator.cs similarity index 95% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionSessionOrchestrator.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionSessionOrchestrator.cs index dd7efe7a..02499377 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionSessionOrchestrator.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionSessionOrchestrator.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class ConnectionSessionOrchestrator : IConnectionSessionOrchestrator { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionSessionService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionSessionService.cs similarity index 95% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionSessionService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionSessionService.cs index 5f58c567..53383619 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionSessionService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionSessionService.cs @@ -1,189 +1,189 @@ -using DBWeaver.UI.Services.ConnectionManager.Contracts; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ConnectionSessionService : IConnectionSessionService -{ - private readonly IConnectionTestService _connectionTestService; - private readonly IConnectionTelemetryService _connectionTelemetryService; - private readonly SemaphoreSlim _gate = new(1, 1); - private ActiveConnectionSessionDto _activeSession = new( - ConnectionId: null, - SessionState: ConnectionSessionStateDto.Inactive, - StartedAt: null, - SessionLabel: null); - - public ConnectionSessionService( - IConnectionTestService connectionTestService, - IConnectionTelemetryService connectionTelemetryService) - { - _connectionTestService = connectionTestService; - _connectionTelemetryService = connectionTelemetryService; - } - - public async Task> ConnectAsync( - ConnectionDetailsDto details, - CancellationToken cancellationToken = default) - { - string connectionId = string.IsNullOrWhiteSpace(details.Id) - ? Guid.NewGuid().ToString() - : details.Id; - - await _gate.WaitAsync(cancellationToken); - try - { - _activeSession = new ActiveConnectionSessionDto( - ConnectionId: connectionId, - SessionState: ConnectionSessionStateDto.Connecting, - StartedAt: null, - SessionLabel: details.Name); - } - finally - { - _gate.Release(); - } - - OperationResultDto testResult = await _connectionTestService.TestAsync(details, cancellationToken); - if (!testResult.Success) - { - await _gate.WaitAsync(cancellationToken); - try - { - _activeSession = new ActiveConnectionSessionDto( - ConnectionId: connectionId, - SessionState: ConnectionSessionStateDto.Failed, - StartedAt: null, - SessionLabel: details.Name); - } - finally - { - _gate.Release(); - } - - await _connectionTelemetryService.TrackAsync( - "connection.session.connect.failed", - new Dictionary - { - ["connectionId"] = connectionId, - ["provider"] = details.Provider, - ["errorCode"] = testResult.SemanticErrorCode.ToString(), - }, - cancellationToken); - - return new OperationResultDto( - Success: false, - SemanticErrorCode: testResult.SemanticErrorCode, - UserMessage: testResult.UserMessage, - Payload: _activeSession, - TechnicalError: testResult.TechnicalError, - CorrelationId: null); - } - - DateTimeOffset startedAt = DateTimeOffset.UtcNow; - - await _gate.WaitAsync(cancellationToken); - try - { - _activeSession = new ActiveConnectionSessionDto( - ConnectionId: connectionId, - SessionState: ConnectionSessionStateDto.Active, - StartedAt: startedAt, - SessionLabel: details.Name); - } - finally - { - _gate.Release(); - } - - await _connectionTelemetryService.TrackAsync( - "connection.session.connect.succeeded", - new Dictionary - { - ["connectionId"] = connectionId, - ["provider"] = details.Provider, - }, - cancellationToken); - - return new OperationResultDto( - Success: true, - SemanticErrorCode: ConnectionOperationSemanticErrorCode.None, - UserMessage: string.Empty, - Payload: _activeSession, - TechnicalError: null, - CorrelationId: null); - } - - public async Task> DisconnectAsync( - string connectionId, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(connectionId)) - { - return new OperationResultDto( - Success: false, - SemanticErrorCode: ConnectionOperationSemanticErrorCode.ValidationFailed, - UserMessage: "Connection id is required.", - Payload: _activeSession, - TechnicalError: null, - CorrelationId: null); - } - - await _gate.WaitAsync(cancellationToken); - try - { - if (_activeSession.ConnectionId is null) - { - return new OperationResultDto( - Success: false, - SemanticErrorCode: ConnectionOperationSemanticErrorCode.NotFound, - UserMessage: "There is no active connection to disconnect.", - Payload: _activeSession, - TechnicalError: null, - CorrelationId: null); - } - - if (!string.Equals(_activeSession.ConnectionId, connectionId, StringComparison.Ordinal)) - { - return new OperationResultDto( - Success: false, - SemanticErrorCode: ConnectionOperationSemanticErrorCode.Conflict, - UserMessage: "A different connection is active.", - Payload: _activeSession, - TechnicalError: null, - CorrelationId: null); - } - - _activeSession = _activeSession with { SessionState = ConnectionSessionStateDto.Disconnecting }; - _activeSession = new ActiveConnectionSessionDto( - ConnectionId: null, - SessionState: ConnectionSessionStateDto.Inactive, - StartedAt: null, - SessionLabel: null); - } - finally - { - _gate.Release(); - } - - await _connectionTelemetryService.TrackAsync( - "connection.session.disconnect", - new Dictionary - { - ["connectionId"] = connectionId, - }, - cancellationToken); - - return new OperationResultDto( - Success: true, - SemanticErrorCode: ConnectionOperationSemanticErrorCode.None, - UserMessage: string.Empty, - Payload: _activeSession, - TechnicalError: null, - CorrelationId: null); - } - - public Task GetActiveSessionAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult(_activeSession); - } -} +using AkkornStudio.UI.Services.ConnectionManager.Contracts; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ConnectionSessionService : IConnectionSessionService +{ + private readonly IConnectionTestService _connectionTestService; + private readonly IConnectionTelemetryService _connectionTelemetryService; + private readonly SemaphoreSlim _gate = new(1, 1); + private ActiveConnectionSessionDto _activeSession = new( + ConnectionId: null, + SessionState: ConnectionSessionStateDto.Inactive, + StartedAt: null, + SessionLabel: null); + + public ConnectionSessionService( + IConnectionTestService connectionTestService, + IConnectionTelemetryService connectionTelemetryService) + { + _connectionTestService = connectionTestService; + _connectionTelemetryService = connectionTelemetryService; + } + + public async Task> ConnectAsync( + ConnectionDetailsDto details, + CancellationToken cancellationToken = default) + { + string connectionId = string.IsNullOrWhiteSpace(details.Id) + ? Guid.NewGuid().ToString() + : details.Id; + + await _gate.WaitAsync(cancellationToken); + try + { + _activeSession = new ActiveConnectionSessionDto( + ConnectionId: connectionId, + SessionState: ConnectionSessionStateDto.Connecting, + StartedAt: null, + SessionLabel: details.Name); + } + finally + { + _gate.Release(); + } + + OperationResultDto testResult = await _connectionTestService.TestAsync(details, cancellationToken); + if (!testResult.Success) + { + await _gate.WaitAsync(cancellationToken); + try + { + _activeSession = new ActiveConnectionSessionDto( + ConnectionId: connectionId, + SessionState: ConnectionSessionStateDto.Failed, + StartedAt: null, + SessionLabel: details.Name); + } + finally + { + _gate.Release(); + } + + await _connectionTelemetryService.TrackAsync( + "connection.session.connect.failed", + new Dictionary + { + ["connectionId"] = connectionId, + ["provider"] = details.Provider, + ["errorCode"] = testResult.SemanticErrorCode.ToString(), + }, + cancellationToken); + + return new OperationResultDto( + Success: false, + SemanticErrorCode: testResult.SemanticErrorCode, + UserMessage: testResult.UserMessage, + Payload: _activeSession, + TechnicalError: testResult.TechnicalError, + CorrelationId: null); + } + + DateTimeOffset startedAt = DateTimeOffset.UtcNow; + + await _gate.WaitAsync(cancellationToken); + try + { + _activeSession = new ActiveConnectionSessionDto( + ConnectionId: connectionId, + SessionState: ConnectionSessionStateDto.Active, + StartedAt: startedAt, + SessionLabel: details.Name); + } + finally + { + _gate.Release(); + } + + await _connectionTelemetryService.TrackAsync( + "connection.session.connect.succeeded", + new Dictionary + { + ["connectionId"] = connectionId, + ["provider"] = details.Provider, + }, + cancellationToken); + + return new OperationResultDto( + Success: true, + SemanticErrorCode: ConnectionOperationSemanticErrorCode.None, + UserMessage: string.Empty, + Payload: _activeSession, + TechnicalError: null, + CorrelationId: null); + } + + public async Task> DisconnectAsync( + string connectionId, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(connectionId)) + { + return new OperationResultDto( + Success: false, + SemanticErrorCode: ConnectionOperationSemanticErrorCode.ValidationFailed, + UserMessage: "Connection id is required.", + Payload: _activeSession, + TechnicalError: null, + CorrelationId: null); + } + + await _gate.WaitAsync(cancellationToken); + try + { + if (_activeSession.ConnectionId is null) + { + return new OperationResultDto( + Success: false, + SemanticErrorCode: ConnectionOperationSemanticErrorCode.NotFound, + UserMessage: "There is no active connection to disconnect.", + Payload: _activeSession, + TechnicalError: null, + CorrelationId: null); + } + + if (!string.Equals(_activeSession.ConnectionId, connectionId, StringComparison.Ordinal)) + { + return new OperationResultDto( + Success: false, + SemanticErrorCode: ConnectionOperationSemanticErrorCode.Conflict, + UserMessage: "A different connection is active.", + Payload: _activeSession, + TechnicalError: null, + CorrelationId: null); + } + + _activeSession = _activeSession with { SessionState = ConnectionSessionStateDto.Disconnecting }; + _activeSession = new ActiveConnectionSessionDto( + ConnectionId: null, + SessionState: ConnectionSessionStateDto.Inactive, + StartedAt: null, + SessionLabel: null); + } + finally + { + _gate.Release(); + } + + await _connectionTelemetryService.TrackAsync( + "connection.session.disconnect", + new Dictionary + { + ["connectionId"] = connectionId, + }, + cancellationToken); + + return new OperationResultDto( + Success: true, + SemanticErrorCode: ConnectionOperationSemanticErrorCode.None, + UserMessage: string.Empty, + Payload: _activeSession, + TechnicalError: null, + CorrelationId: null); + } + + public Task GetActiveSessionAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(_activeSession); + } +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionTelemetryService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionTelemetryService.cs similarity index 83% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionTelemetryService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionTelemetryService.cs index 13c55a2b..570ac072 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionTelemetryService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionTelemetryService.cs @@ -1,23 +1,23 @@ -using DBWeaver.UI.Services.ConnectionManager.Contracts; -using Microsoft.Extensions.Logging; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ConnectionTelemetryService : IConnectionTelemetryService -{ - private readonly ILogger _logger; - - public ConnectionTelemetryService(ILogger logger) - { - _logger = logger; - } - - public Task TrackAsync( - string eventName, - IReadOnlyDictionary properties, - CancellationToken cancellationToken = default) - { - _logger.LogInformation("Connection telemetry event: {EventName} {@Properties}", eventName, properties); - return Task.CompletedTask; - } -} +using AkkornStudio.UI.Services.ConnectionManager.Contracts; +using Microsoft.Extensions.Logging; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ConnectionTelemetryService : IConnectionTelemetryService +{ + private readonly ILogger _logger; + + public ConnectionTelemetryService(ILogger logger) + { + _logger = logger; + } + + public Task TrackAsync( + string eventName, + IReadOnlyDictionary properties, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("Connection telemetry event: {EventName} {@Properties}", eventName, properties); + return Task.CompletedTask; + } +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionTestService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionTestService.cs similarity index 96% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionTestService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionTestService.cs index 18a58050..b3f3fd30 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionTestService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionTestService.cs @@ -1,253 +1,253 @@ -using DBWeaver.Core; -using DBWeaver.UI.Services.ConnectionManager.Contracts; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ConnectionTestService : IConnectionTestService -{ - private readonly IConnectionTestExecutor _connectionTestExecutor; - private readonly IConnectionValidationService _validationService; - private readonly IProviderCapabilityService _providerCapabilityService; - private readonly IConnectionUrlParserService _urlParserService; - - public ConnectionTestService( - IConnectionTestExecutor connectionTestExecutor, - IConnectionValidationService validationService, - IProviderCapabilityService providerCapabilityService, - IConnectionUrlParserService urlParserService) - { - _connectionTestExecutor = connectionTestExecutor; - _validationService = validationService; - _providerCapabilityService = providerCapabilityService; - _urlParserService = urlParserService; - } - - public async Task> TestAsync( - ConnectionDetailsDto details, - CancellationToken cancellationToken = default) - { - ProviderCapabilityDto? capability = await _providerCapabilityService.GetCapabilityAsync(details.Provider, cancellationToken); - if (capability is null) - { - return Fail( - ConnectionOperationSemanticErrorCode.NotFound, - "Unsupported provider.", - new ConnectionTestResultDto( - Status: ConnectionTestStatusDto.Unavailable, - SummaryMessage: "Unsupported provider.", - TechnicalDetails: null, - LatencyMs: null, - ProviderErrorCode: null, - TestedAt: DateTimeOffset.UtcNow)); - } - - ConnectionValidationResultDto validation = _validationService.Validate(details, capability); - if (!validation.IsValid) - { - string message = validation.Errors.FirstOrDefault()?.Message ?? "Invalid connection details."; - return Fail( - ConnectionOperationSemanticErrorCode.ValidationFailed, - message, - new ConnectionTestResultDto( - Status: ConnectionTestStatusDto.Unavailable, - SummaryMessage: message, - TechnicalDetails: null, - LatencyMs: null, - ProviderErrorCode: null, - TestedAt: DateTimeOffset.UtcNow)); - } - - OperationResultDto profileResult = await BuildProfileAsync(details, cancellationToken); - if (!profileResult.Success || profileResult.Payload is null) - { - return Fail( - profileResult.SemanticErrorCode, - profileResult.UserMessage, - new ConnectionTestResultDto( - Status: ConnectionTestStatusDto.Unavailable, - SummaryMessage: profileResult.UserMessage, - TechnicalDetails: profileResult.TechnicalError, - LatencyMs: null, - ProviderErrorCode: null, - TestedAt: DateTimeOffset.UtcNow), - profileResult.TechnicalError); - } - - ConnectionProfile profile = profileResult.Payload; - ConnectionTestResult result; - - try - { - result = await _connectionTestExecutor.ExecuteAsync( - profile.ToConnectionConfig(), - profile.Provider, - profile.TimeoutSeconds, - cancellationToken); - } - catch (OperationCanceledException ex) - { - return Fail( - ConnectionOperationSemanticErrorCode.Timeout, - "Connection test was canceled.", - new ConnectionTestResultDto( - Status: ConnectionTestStatusDto.Timeout, - SummaryMessage: "Connection test was canceled.", - TechnicalDetails: ex.Message, - LatencyMs: null, - ProviderErrorCode: null, - TestedAt: DateTimeOffset.UtcNow), - ex.Message); - } - catch (Exception ex) - { - return Fail( - ConnectionOperationSemanticErrorCode.Unknown, - "Unexpected error while testing the connection.", - new ConnectionTestResultDto( - Status: ConnectionTestStatusDto.Failure, - SummaryMessage: "Unexpected error while testing the connection.", - TechnicalDetails: ex.Message, - LatencyMs: null, - ProviderErrorCode: null, - TestedAt: DateTimeOffset.UtcNow), - ex.Message); - } - - if (!result.Success) - { - ConnectionOperationSemanticErrorCode semanticCode = MapFailureCode(result.ErrorMessage); - ConnectionTestStatusDto status = semanticCode switch - { - ConnectionOperationSemanticErrorCode.AuthenticationFailed => ConnectionTestStatusDto.AuthenticationFailure, - ConnectionOperationSemanticErrorCode.Timeout => ConnectionTestStatusDto.Timeout, - _ => ConnectionTestStatusDto.Failure, - }; - - string summaryMessage = result.ErrorMessage ?? "Connection test failed."; - return Fail( - semanticCode, - summaryMessage, - new ConnectionTestResultDto( - Status: status, - SummaryMessage: summaryMessage, - TechnicalDetails: result.ErrorMessage, - LatencyMs: null, - ProviderErrorCode: null, - TestedAt: DateTimeOffset.UtcNow)); - } - - int? latencyMs = result.Latency is null - ? null - : (int)result.Latency.Value.TotalMilliseconds; - - return Ok(new ConnectionTestResultDto( - Status: ConnectionTestStatusDto.Success, - SummaryMessage: "Connection test succeeded.", - TechnicalDetails: null, - LatencyMs: latencyMs, - ProviderErrorCode: null, - TestedAt: DateTimeOffset.UtcNow)); - } - - private async Task> BuildProfileAsync( - ConnectionDetailsDto details, - CancellationToken cancellationToken) - { - if (details.Mode == ConnectionProviderModeDto.Fields) - { - return Ok(ConnectionContractMapper.ToProfile(details)); - } - - ConnectionUrlParseResultDto parsed = await _urlParserService.ParseAsync(details.UrlValue ?? string.Empty, details.Provider, cancellationToken); - if (parsed.ParseStatus == ConnectionUrlParseStatusDto.Failed) - { - return Fail( - ConnectionOperationSemanticErrorCode.ParseFailed, - parsed.UserMessage, - parsed.TechnicalDetails); - } - - var mergedFields = new Dictionary(details.FieldValues, StringComparer.OrdinalIgnoreCase); - foreach (UrlParseFieldTokenDto token in parsed.RecognizedFields) - { - mergedFields[token.Key] = token.Value; - } - - ConnectionOperationSemanticErrorCode parseCode = parsed.ParseStatus == ConnectionUrlParseStatusDto.Partial - ? ConnectionOperationSemanticErrorCode.ParsePartial - : ConnectionOperationSemanticErrorCode.None; - - ConnectionProfile profile = ConnectionContractMapper.ToProfile( - details, - fieldsOverride: mergedFields, - providerOverride: parsed.SuggestedProvider ?? details.Provider); - - if (parseCode != ConnectionOperationSemanticErrorCode.None) - { - return new OperationResultDto( - Success: true, - SemanticErrorCode: parseCode, - UserMessage: parsed.UserMessage, - Payload: profile, - TechnicalError: parsed.TechnicalDetails, - CorrelationId: null); - } - - return Ok(profile); - } - - private static ConnectionOperationSemanticErrorCode MapFailureCode(string? error) - { - if (string.IsNullOrWhiteSpace(error)) - return ConnectionOperationSemanticErrorCode.Unknown; - - string lowered = error.ToLowerInvariant(); - - if (lowered.Contains("timeout", StringComparison.Ordinal)) - return ConnectionOperationSemanticErrorCode.Timeout; - - if (lowered.Contains("password", StringComparison.Ordinal) - || lowered.Contains("authentication", StringComparison.Ordinal) - || lowered.Contains("login", StringComparison.Ordinal) - || lowered.Contains("auth", StringComparison.Ordinal)) - { - return ConnectionOperationSemanticErrorCode.AuthenticationFailed; - } - - return ConnectionOperationSemanticErrorCode.Unknown; - } - - private static OperationResultDto Ok(T payload) => - new( - Success: true, - SemanticErrorCode: ConnectionOperationSemanticErrorCode.None, - UserMessage: string.Empty, - Payload: payload, - TechnicalError: null, - CorrelationId: null); - - private static OperationResultDto Fail( - ConnectionOperationSemanticErrorCode code, - string message, - T payload, - string? technicalError = null) => - new( - Success: false, - SemanticErrorCode: code, - UserMessage: message, - Payload: payload, - TechnicalError: technicalError, - CorrelationId: null); - - private static OperationResultDto Fail( - ConnectionOperationSemanticErrorCode code, - string message, - string? technicalError = null) => - new( - Success: false, - SemanticErrorCode: code, - UserMessage: message, - Payload: default, - TechnicalError: technicalError, - CorrelationId: null); -} +using AkkornStudio.Core; +using AkkornStudio.UI.Services.ConnectionManager.Contracts; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ConnectionTestService : IConnectionTestService +{ + private readonly IConnectionTestExecutor _connectionTestExecutor; + private readonly IConnectionValidationService _validationService; + private readonly IProviderCapabilityService _providerCapabilityService; + private readonly IConnectionUrlParserService _urlParserService; + + public ConnectionTestService( + IConnectionTestExecutor connectionTestExecutor, + IConnectionValidationService validationService, + IProviderCapabilityService providerCapabilityService, + IConnectionUrlParserService urlParserService) + { + _connectionTestExecutor = connectionTestExecutor; + _validationService = validationService; + _providerCapabilityService = providerCapabilityService; + _urlParserService = urlParserService; + } + + public async Task> TestAsync( + ConnectionDetailsDto details, + CancellationToken cancellationToken = default) + { + ProviderCapabilityDto? capability = await _providerCapabilityService.GetCapabilityAsync(details.Provider, cancellationToken); + if (capability is null) + { + return Fail( + ConnectionOperationSemanticErrorCode.NotFound, + "Unsupported provider.", + new ConnectionTestResultDto( + Status: ConnectionTestStatusDto.Unavailable, + SummaryMessage: "Unsupported provider.", + TechnicalDetails: null, + LatencyMs: null, + ProviderErrorCode: null, + TestedAt: DateTimeOffset.UtcNow)); + } + + ConnectionValidationResultDto validation = _validationService.Validate(details, capability); + if (!validation.IsValid) + { + string message = validation.Errors.FirstOrDefault()?.Message ?? "Invalid connection details."; + return Fail( + ConnectionOperationSemanticErrorCode.ValidationFailed, + message, + new ConnectionTestResultDto( + Status: ConnectionTestStatusDto.Unavailable, + SummaryMessage: message, + TechnicalDetails: null, + LatencyMs: null, + ProviderErrorCode: null, + TestedAt: DateTimeOffset.UtcNow)); + } + + OperationResultDto profileResult = await BuildProfileAsync(details, cancellationToken); + if (!profileResult.Success || profileResult.Payload is null) + { + return Fail( + profileResult.SemanticErrorCode, + profileResult.UserMessage, + new ConnectionTestResultDto( + Status: ConnectionTestStatusDto.Unavailable, + SummaryMessage: profileResult.UserMessage, + TechnicalDetails: profileResult.TechnicalError, + LatencyMs: null, + ProviderErrorCode: null, + TestedAt: DateTimeOffset.UtcNow), + profileResult.TechnicalError); + } + + ConnectionProfile profile = profileResult.Payload; + ConnectionTestResult result; + + try + { + result = await _connectionTestExecutor.ExecuteAsync( + profile.ToConnectionConfig(), + profile.Provider, + profile.TimeoutSeconds, + cancellationToken); + } + catch (OperationCanceledException ex) + { + return Fail( + ConnectionOperationSemanticErrorCode.Timeout, + "Connection test was canceled.", + new ConnectionTestResultDto( + Status: ConnectionTestStatusDto.Timeout, + SummaryMessage: "Connection test was canceled.", + TechnicalDetails: ex.Message, + LatencyMs: null, + ProviderErrorCode: null, + TestedAt: DateTimeOffset.UtcNow), + ex.Message); + } + catch (Exception ex) + { + return Fail( + ConnectionOperationSemanticErrorCode.Unknown, + "Unexpected error while testing the connection.", + new ConnectionTestResultDto( + Status: ConnectionTestStatusDto.Failure, + SummaryMessage: "Unexpected error while testing the connection.", + TechnicalDetails: ex.Message, + LatencyMs: null, + ProviderErrorCode: null, + TestedAt: DateTimeOffset.UtcNow), + ex.Message); + } + + if (!result.Success) + { + ConnectionOperationSemanticErrorCode semanticCode = MapFailureCode(result.ErrorMessage); + ConnectionTestStatusDto status = semanticCode switch + { + ConnectionOperationSemanticErrorCode.AuthenticationFailed => ConnectionTestStatusDto.AuthenticationFailure, + ConnectionOperationSemanticErrorCode.Timeout => ConnectionTestStatusDto.Timeout, + _ => ConnectionTestStatusDto.Failure, + }; + + string summaryMessage = result.ErrorMessage ?? "Connection test failed."; + return Fail( + semanticCode, + summaryMessage, + new ConnectionTestResultDto( + Status: status, + SummaryMessage: summaryMessage, + TechnicalDetails: result.ErrorMessage, + LatencyMs: null, + ProviderErrorCode: null, + TestedAt: DateTimeOffset.UtcNow)); + } + + int? latencyMs = result.Latency is null + ? null + : (int)result.Latency.Value.TotalMilliseconds; + + return Ok(new ConnectionTestResultDto( + Status: ConnectionTestStatusDto.Success, + SummaryMessage: "Connection test succeeded.", + TechnicalDetails: null, + LatencyMs: latencyMs, + ProviderErrorCode: null, + TestedAt: DateTimeOffset.UtcNow)); + } + + private async Task> BuildProfileAsync( + ConnectionDetailsDto details, + CancellationToken cancellationToken) + { + if (details.Mode == ConnectionProviderModeDto.Fields) + { + return Ok(ConnectionContractMapper.ToProfile(details)); + } + + ConnectionUrlParseResultDto parsed = await _urlParserService.ParseAsync(details.UrlValue ?? string.Empty, details.Provider, cancellationToken); + if (parsed.ParseStatus == ConnectionUrlParseStatusDto.Failed) + { + return Fail( + ConnectionOperationSemanticErrorCode.ParseFailed, + parsed.UserMessage, + parsed.TechnicalDetails); + } + + var mergedFields = new Dictionary(details.FieldValues, StringComparer.OrdinalIgnoreCase); + foreach (UrlParseFieldTokenDto token in parsed.RecognizedFields) + { + mergedFields[token.Key] = token.Value; + } + + ConnectionOperationSemanticErrorCode parseCode = parsed.ParseStatus == ConnectionUrlParseStatusDto.Partial + ? ConnectionOperationSemanticErrorCode.ParsePartial + : ConnectionOperationSemanticErrorCode.None; + + ConnectionProfile profile = ConnectionContractMapper.ToProfile( + details, + fieldsOverride: mergedFields, + providerOverride: parsed.SuggestedProvider ?? details.Provider); + + if (parseCode != ConnectionOperationSemanticErrorCode.None) + { + return new OperationResultDto( + Success: true, + SemanticErrorCode: parseCode, + UserMessage: parsed.UserMessage, + Payload: profile, + TechnicalError: parsed.TechnicalDetails, + CorrelationId: null); + } + + return Ok(profile); + } + + private static ConnectionOperationSemanticErrorCode MapFailureCode(string? error) + { + if (string.IsNullOrWhiteSpace(error)) + return ConnectionOperationSemanticErrorCode.Unknown; + + string lowered = error.ToLowerInvariant(); + + if (lowered.Contains("timeout", StringComparison.Ordinal)) + return ConnectionOperationSemanticErrorCode.Timeout; + + if (lowered.Contains("password", StringComparison.Ordinal) + || lowered.Contains("authentication", StringComparison.Ordinal) + || lowered.Contains("login", StringComparison.Ordinal) + || lowered.Contains("auth", StringComparison.Ordinal)) + { + return ConnectionOperationSemanticErrorCode.AuthenticationFailed; + } + + return ConnectionOperationSemanticErrorCode.Unknown; + } + + private static OperationResultDto Ok(T payload) => + new( + Success: true, + SemanticErrorCode: ConnectionOperationSemanticErrorCode.None, + UserMessage: string.Empty, + Payload: payload, + TechnicalError: null, + CorrelationId: null); + + private static OperationResultDto Fail( + ConnectionOperationSemanticErrorCode code, + string message, + T payload, + string? technicalError = null) => + new( + Success: false, + SemanticErrorCode: code, + UserMessage: message, + Payload: payload, + TechnicalError: technicalError, + CorrelationId: null); + + private static OperationResultDto Fail( + ConnectionOperationSemanticErrorCode code, + string message, + string? technicalError = null) => + new( + Success: false, + SemanticErrorCode: code, + UserMessage: message, + Payload: default, + TechnicalError: technicalError, + CorrelationId: null); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionUrlParserService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionUrlParserService.cs similarity index 96% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionUrlParserService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionUrlParserService.cs index 9f6c4c18..909e890f 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionUrlParserService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionUrlParserService.cs @@ -1,273 +1,273 @@ -using DBWeaver.Core; -using DBWeaver.UI.Services.ConnectionManager.Contracts; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ConnectionUrlParserService : IConnectionUrlParserService -{ - public Task ParseAsync( - string rawUrl, - string? selectedProvider, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(rawUrl)) - { - return Task.FromResult(new ConnectionUrlParseResultDto( - ParseStatus: ConnectionUrlParseStatusDto.Failed, - RecognizedFields: [], - UnrecognizedTokens: [], - SuggestedProvider: null, - ConflictWithSelectedProvider: false, - NormalizedUrl: null, - UserMessage: "Connection URL is empty.", - TechnicalDetails: null)); - } - - if (!TryParseConnectionUrl(rawUrl, out ParsedConnectionUrl parsed, out string error, out IReadOnlyList unknownTokens)) - { - return Task.FromResult(new ConnectionUrlParseResultDto( - ParseStatus: ConnectionUrlParseStatusDto.Failed, - RecognizedFields: [], - UnrecognizedTokens: [], - SuggestedProvider: null, - ConflictWithSelectedProvider: false, - NormalizedUrl: null, - UserMessage: error, - TechnicalDetails: null)); - } - - bool conflict = false; - if (!string.IsNullOrWhiteSpace(selectedProvider) - && ConnectionContractMapper.TryParseProvider(selectedProvider, out DatabaseProvider selected) - && selected != parsed.Provider) - { - conflict = true; - } - - var fields = new List - { - new(ConnectionContractMapper.HostKey, parsed.Host), - new(ConnectionContractMapper.PortKey, parsed.Port.ToString()), - new(ConnectionContractMapper.DatabaseKey, parsed.Database), - new(ConnectionContractMapper.UsernameKey, parsed.Username), - new(ConnectionContractMapper.PasswordKey, parsed.Password), - new(ConnectionContractMapper.UseSslKey, parsed.UseSsl.ToString()), - new(ConnectionContractMapper.TrustServerCertificateKey, parsed.TrustServerCertificate.ToString()), - new(ConnectionContractMapper.UseIntegratedSecurityKey, parsed.UseIntegratedSecurity.ToString()), - }; - - ConnectionUrlParseStatusDto status = unknownTokens.Count == 0 && !conflict - ? ConnectionUrlParseStatusDto.Success - : ConnectionUrlParseStatusDto.Partial; - - string message = status == ConnectionUrlParseStatusDto.Success - ? "Connection URL parsed successfully." - : "Connection URL parsed with warnings."; - - return Task.FromResult(new ConnectionUrlParseResultDto( - ParseStatus: status, - RecognizedFields: fields, - UnrecognizedTokens: unknownTokens, - SuggestedProvider: parsed.Provider.ToString(), - ConflictWithSelectedProvider: conflict, - NormalizedUrl: parsed.NormalizedUrl, - UserMessage: message, - TechnicalDetails: null)); - } - - private static bool TryParseConnectionUrl( - string raw, - out ParsedConnectionUrl parsed, - out string error, - out IReadOnlyList unknownTokens) - { - parsed = default; - error = string.Empty; - unknownTokens = []; - - string input = raw.Trim(); - - if (!input.Contains("://", StringComparison.Ordinal)) - { - string lower = input.ToLowerInvariant(); - if (lower.EndsWith(".db", StringComparison.Ordinal) - || lower.EndsWith(".sqlite", StringComparison.Ordinal) - || lower.EndsWith(".sqlite3", StringComparison.Ordinal)) - { - parsed = new ParsedConnectionUrl( - Provider: DatabaseProvider.SQLite, - Host: AppConstants.DefaultHost, - Port: 0, - Database: input, - Username: string.Empty, - Password: string.Empty, - UseSsl: false, - TrustServerCertificate: true, - UseIntegratedSecurity: false, - NormalizedUrl: $"file://{input}"); - - return true; - } - - error = "Could not determine database type from URL."; - return false; - } - - string encoded = input; - string username = string.Empty; - string password = string.Empty; - - int authorityStart = input.IndexOf("://", StringComparison.Ordinal) + 3; - int lastAt = input.LastIndexOf('@'); - if (lastAt > authorityStart) - { - string credentialPart = input.Substring(authorityStart, lastAt - authorityStart); - int firstColon = credentialPart.IndexOf(':'); - if (firstColon >= 0) - { - username = Uri.UnescapeDataString(credentialPart[..firstColon]); - password = Uri.UnescapeDataString(credentialPart[(firstColon + 1)..]); - } - else - { - username = Uri.UnescapeDataString(credentialPart); - } - - encoded = input[..authorityStart] + input[(lastAt + 1)..]; - } - - if (!Uri.TryCreate(encoded, UriKind.Absolute, out Uri? uri)) - { - error = "Invalid connection URL."; - return false; - } - - if (!ConnectionContractMapper.TryParseProvider(uri.Scheme, out DatabaseProvider provider)) - { - error = $"Unsupported provider scheme: {uri.Scheme}."; - return false; - } - - Dictionary query = ParseQuery(uri.Query); - bool useSsl = ResolveUseSsl(provider, query); - bool trust = ResolveTrustServerCertificate(query); - bool useIntegratedSecurity = ResolveIntegratedSecurity(query); - string database = Uri.UnescapeDataString(uri.AbsolutePath.Trim('/')); - - if (provider == DatabaseProvider.SQLite) - { - database = uri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase) - ? Uri.UnescapeDataString(uri.LocalPath) - : database; - } - - parsed = new ParsedConnectionUrl( - Provider: provider, - Host: string.IsNullOrWhiteSpace(uri.Host) ? AppConstants.DefaultHost : uri.Host, - Port: uri.IsDefaultPort ? ConnectionProfile.DefaultPort(provider) : uri.Port, - Database: database, - Username: string.IsNullOrWhiteSpace(username) ? Uri.UnescapeDataString(uri.UserInfo) : username, - Password: password, - UseSsl: useSsl, - TrustServerCertificate: trust, - UseIntegratedSecurity: useIntegratedSecurity, - NormalizedUrl: uri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped)); - - if (provider == DatabaseProvider.SqlServer && parsed.Port <= 0) - parsed = parsed with { Port = ConnectionProfile.DefaultPort(DatabaseProvider.SqlServer) }; - - unknownTokens = query.Keys - .Where(static key => !KnownQueryKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) - .ToArray(); - - return true; - } - - private static readonly string[] KnownQueryKeys = - [ - "ssl", - "sslmode", - "encrypt", - "trustservercertificate", - "integratedsecurity", - "trusted_connection", - ]; - - private static Dictionary ParseQuery(string query) - { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); - string trimmed = query.TrimStart('?'); - if (string.IsNullOrWhiteSpace(trimmed)) - return result; - - string[] parts = trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries); - foreach (string part in parts) - { - string[] kv = part.Split('=', 2, StringSplitOptions.None); - string key = Uri.UnescapeDataString(kv[0]); - string value = kv.Length == 2 ? Uri.UnescapeDataString(kv[1]) : string.Empty; - result[key] = value; - } - - return result; - } - - private static bool ResolveUseSsl(DatabaseProvider provider, IReadOnlyDictionary query) - { - if (query.TryGetValue("ssl", out string? ssl)) - return IsTruthy(ssl); - - if (query.TryGetValue("sslmode", out string? sslMode)) - return !sslMode.Equals("disable", StringComparison.OrdinalIgnoreCase) - && !sslMode.Equals("none", StringComparison.OrdinalIgnoreCase); - - if (provider == DatabaseProvider.SqlServer && query.TryGetValue("encrypt", out string? encrypt)) - return IsTruthy(encrypt); - - return false; - } - - private static bool ResolveTrustServerCertificate(IReadOnlyDictionary query) - { - if (!query.TryGetValue("trustservercertificate", out string? value)) - return true; - - return IsTruthy(value); - } - - private static bool ResolveIntegratedSecurity(IReadOnlyDictionary query) - { - if (query.TryGetValue("integratedsecurity", out string? integrated)) - return IsTruthy(integrated); - - if (query.TryGetValue("trusted_connection", out string? trustedConnection)) - return IsTruthy(trustedConnection); - - return false; - } - - private static bool IsTruthy(string value) - { - if (bool.TryParse(value, out bool parsed)) - return parsed; - - return value.Equals("1", StringComparison.OrdinalIgnoreCase) - || value.Equals("yes", StringComparison.OrdinalIgnoreCase) - || value.Equals("on", StringComparison.OrdinalIgnoreCase) - || value.Equals("required", StringComparison.OrdinalIgnoreCase) - || value.Equals("require", StringComparison.OrdinalIgnoreCase) - || value.Equals("true", StringComparison.OrdinalIgnoreCase); - } - - private readonly record struct ParsedConnectionUrl( - DatabaseProvider Provider, - string Host, - int Port, - string Database, - string Username, - string Password, - bool UseSsl, - bool TrustServerCertificate, - bool UseIntegratedSecurity, - string NormalizedUrl); -} +using AkkornStudio.Core; +using AkkornStudio.UI.Services.ConnectionManager.Contracts; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ConnectionUrlParserService : IConnectionUrlParserService +{ + public Task ParseAsync( + string rawUrl, + string? selectedProvider, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(rawUrl)) + { + return Task.FromResult(new ConnectionUrlParseResultDto( + ParseStatus: ConnectionUrlParseStatusDto.Failed, + RecognizedFields: [], + UnrecognizedTokens: [], + SuggestedProvider: null, + ConflictWithSelectedProvider: false, + NormalizedUrl: null, + UserMessage: "Connection URL is empty.", + TechnicalDetails: null)); + } + + if (!TryParseConnectionUrl(rawUrl, out ParsedConnectionUrl parsed, out string error, out IReadOnlyList unknownTokens)) + { + return Task.FromResult(new ConnectionUrlParseResultDto( + ParseStatus: ConnectionUrlParseStatusDto.Failed, + RecognizedFields: [], + UnrecognizedTokens: [], + SuggestedProvider: null, + ConflictWithSelectedProvider: false, + NormalizedUrl: null, + UserMessage: error, + TechnicalDetails: null)); + } + + bool conflict = false; + if (!string.IsNullOrWhiteSpace(selectedProvider) + && ConnectionContractMapper.TryParseProvider(selectedProvider, out DatabaseProvider selected) + && selected != parsed.Provider) + { + conflict = true; + } + + var fields = new List + { + new(ConnectionContractMapper.HostKey, parsed.Host), + new(ConnectionContractMapper.PortKey, parsed.Port.ToString()), + new(ConnectionContractMapper.DatabaseKey, parsed.Database), + new(ConnectionContractMapper.UsernameKey, parsed.Username), + new(ConnectionContractMapper.PasswordKey, parsed.Password), + new(ConnectionContractMapper.UseSslKey, parsed.UseSsl.ToString()), + new(ConnectionContractMapper.TrustServerCertificateKey, parsed.TrustServerCertificate.ToString()), + new(ConnectionContractMapper.UseIntegratedSecurityKey, parsed.UseIntegratedSecurity.ToString()), + }; + + ConnectionUrlParseStatusDto status = unknownTokens.Count == 0 && !conflict + ? ConnectionUrlParseStatusDto.Success + : ConnectionUrlParseStatusDto.Partial; + + string message = status == ConnectionUrlParseStatusDto.Success + ? "Connection URL parsed successfully." + : "Connection URL parsed with warnings."; + + return Task.FromResult(new ConnectionUrlParseResultDto( + ParseStatus: status, + RecognizedFields: fields, + UnrecognizedTokens: unknownTokens, + SuggestedProvider: parsed.Provider.ToString(), + ConflictWithSelectedProvider: conflict, + NormalizedUrl: parsed.NormalizedUrl, + UserMessage: message, + TechnicalDetails: null)); + } + + private static bool TryParseConnectionUrl( + string raw, + out ParsedConnectionUrl parsed, + out string error, + out IReadOnlyList unknownTokens) + { + parsed = default; + error = string.Empty; + unknownTokens = []; + + string input = raw.Trim(); + + if (!input.Contains("://", StringComparison.Ordinal)) + { + string lower = input.ToLowerInvariant(); + if (lower.EndsWith(".db", StringComparison.Ordinal) + || lower.EndsWith(".sqlite", StringComparison.Ordinal) + || lower.EndsWith(".sqlite3", StringComparison.Ordinal)) + { + parsed = new ParsedConnectionUrl( + Provider: DatabaseProvider.SQLite, + Host: AppConstants.DefaultHost, + Port: 0, + Database: input, + Username: string.Empty, + Password: string.Empty, + UseSsl: false, + TrustServerCertificate: true, + UseIntegratedSecurity: false, + NormalizedUrl: $"file://{input}"); + + return true; + } + + error = "Could not determine database type from URL."; + return false; + } + + string encoded = input; + string username = string.Empty; + string password = string.Empty; + + int authorityStart = input.IndexOf("://", StringComparison.Ordinal) + 3; + int lastAt = input.LastIndexOf('@'); + if (lastAt > authorityStart) + { + string credentialPart = input.Substring(authorityStart, lastAt - authorityStart); + int firstColon = credentialPart.IndexOf(':'); + if (firstColon >= 0) + { + username = Uri.UnescapeDataString(credentialPart[..firstColon]); + password = Uri.UnescapeDataString(credentialPart[(firstColon + 1)..]); + } + else + { + username = Uri.UnescapeDataString(credentialPart); + } + + encoded = input[..authorityStart] + input[(lastAt + 1)..]; + } + + if (!Uri.TryCreate(encoded, UriKind.Absolute, out Uri? uri)) + { + error = "Invalid connection URL."; + return false; + } + + if (!ConnectionContractMapper.TryParseProvider(uri.Scheme, out DatabaseProvider provider)) + { + error = $"Unsupported provider scheme: {uri.Scheme}."; + return false; + } + + Dictionary query = ParseQuery(uri.Query); + bool useSsl = ResolveUseSsl(provider, query); + bool trust = ResolveTrustServerCertificate(query); + bool useIntegratedSecurity = ResolveIntegratedSecurity(query); + string database = Uri.UnescapeDataString(uri.AbsolutePath.Trim('/')); + + if (provider == DatabaseProvider.SQLite) + { + database = uri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase) + ? Uri.UnescapeDataString(uri.LocalPath) + : database; + } + + parsed = new ParsedConnectionUrl( + Provider: provider, + Host: string.IsNullOrWhiteSpace(uri.Host) ? AppConstants.DefaultHost : uri.Host, + Port: uri.IsDefaultPort ? ConnectionProfile.DefaultPort(provider) : uri.Port, + Database: database, + Username: string.IsNullOrWhiteSpace(username) ? Uri.UnescapeDataString(uri.UserInfo) : username, + Password: password, + UseSsl: useSsl, + TrustServerCertificate: trust, + UseIntegratedSecurity: useIntegratedSecurity, + NormalizedUrl: uri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.Unescaped)); + + if (provider == DatabaseProvider.SqlServer && parsed.Port <= 0) + parsed = parsed with { Port = ConnectionProfile.DefaultPort(DatabaseProvider.SqlServer) }; + + unknownTokens = query.Keys + .Where(static key => !KnownQueryKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) + .ToArray(); + + return true; + } + + private static readonly string[] KnownQueryKeys = + [ + "ssl", + "sslmode", + "encrypt", + "trustservercertificate", + "integratedsecurity", + "trusted_connection", + ]; + + private static Dictionary ParseQuery(string query) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + string trimmed = query.TrimStart('?'); + if (string.IsNullOrWhiteSpace(trimmed)) + return result; + + string[] parts = trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries); + foreach (string part in parts) + { + string[] kv = part.Split('=', 2, StringSplitOptions.None); + string key = Uri.UnescapeDataString(kv[0]); + string value = kv.Length == 2 ? Uri.UnescapeDataString(kv[1]) : string.Empty; + result[key] = value; + } + + return result; + } + + private static bool ResolveUseSsl(DatabaseProvider provider, IReadOnlyDictionary query) + { + if (query.TryGetValue("ssl", out string? ssl)) + return IsTruthy(ssl); + + if (query.TryGetValue("sslmode", out string? sslMode)) + return !sslMode.Equals("disable", StringComparison.OrdinalIgnoreCase) + && !sslMode.Equals("none", StringComparison.OrdinalIgnoreCase); + + if (provider == DatabaseProvider.SqlServer && query.TryGetValue("encrypt", out string? encrypt)) + return IsTruthy(encrypt); + + return false; + } + + private static bool ResolveTrustServerCertificate(IReadOnlyDictionary query) + { + if (!query.TryGetValue("trustservercertificate", out string? value)) + return true; + + return IsTruthy(value); + } + + private static bool ResolveIntegratedSecurity(IReadOnlyDictionary query) + { + if (query.TryGetValue("integratedsecurity", out string? integrated)) + return IsTruthy(integrated); + + if (query.TryGetValue("trusted_connection", out string? trustedConnection)) + return IsTruthy(trustedConnection); + + return false; + } + + private static bool IsTruthy(string value) + { + if (bool.TryParse(value, out bool parsed)) + return parsed; + + return value.Equals("1", StringComparison.OrdinalIgnoreCase) + || value.Equals("yes", StringComparison.OrdinalIgnoreCase) + || value.Equals("on", StringComparison.OrdinalIgnoreCase) + || value.Equals("required", StringComparison.OrdinalIgnoreCase) + || value.Equals("require", StringComparison.OrdinalIgnoreCase) + || value.Equals("true", StringComparison.OrdinalIgnoreCase); + } + + private readonly record struct ParsedConnectionUrl( + DatabaseProvider Provider, + string Host, + int Port, + string Database, + string Username, + string Password, + bool UseSsl, + bool TrustServerCertificate, + bool UseIntegratedSecurity, + string NormalizedUrl); +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionValidationService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionValidationService.cs similarity index 95% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionValidationService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionValidationService.cs index 1023970c..9db318a1 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ConnectionValidationService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ConnectionValidationService.cs @@ -1,133 +1,133 @@ -using DBWeaver.Core; -using DBWeaver.UI.Services.ConnectionManager.Contracts; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ConnectionValidationService : IConnectionValidationService -{ - public ConnectionValidationResultDto Validate(ConnectionDetailsDto details, ProviderCapabilityDto capability) - { - var errors = new List(); - var warnings = new List(); - - if (string.IsNullOrWhiteSpace(details.Name)) - { - errors.Add(new ConnectionValidationMessageDto("name", "name.required", "Connection name is required.")); - } - - if (details.Mode == ConnectionProviderModeDto.Url) - { - if (!capability.SupportsUrlMode) - { - errors.Add(new ConnectionValidationMessageDto("url", "url.unsupported", "The selected provider does not support URL mode.")); - } - - if (string.IsNullOrWhiteSpace(details.UrlValue)) - { - errors.Add(new ConnectionValidationMessageDto("url", "url.required", "Connection URL is required in URL mode.")); - } - - return new ConnectionValidationResultDto(errors.Count == 0, errors, warnings); - } - - IReadOnlyDictionary fields = details.FieldValues; - - if (!TryGetInt(fields, ConnectionContractMapper.TimeoutSecondsKey, out int timeout) || timeout <= 0) - { - errors.Add(new ConnectionValidationMessageDto( - ConnectionContractMapper.TimeoutSecondsKey, - "timeout.invalid", - "Timeout must be greater than zero.")); - } - - bool isSqlite = ConnectionContractMapper.TryParseProvider(details.Provider, out DatabaseProvider provider) - && provider == DatabaseProvider.SQLite; - - if (isSqlite) - { - if (string.IsNullOrWhiteSpace(GetString(fields, ConnectionContractMapper.DatabaseKey))) - { - errors.Add(new ConnectionValidationMessageDto( - ConnectionContractMapper.DatabaseKey, - "database.required", - "SQLite database path is required.")); - } - - return new ConnectionValidationResultDto(errors.Count == 0, errors, warnings); - } - - ValidateRequiredField(fields, errors, ConnectionContractMapper.HostKey, "Host is required."); - ValidateRequiredField(fields, errors, ConnectionContractMapper.DatabaseKey, "Database is required."); - - if (!TryGetInt(fields, ConnectionContractMapper.PortKey, out int port) || port <= 0) - { - errors.Add(new ConnectionValidationMessageDto( - ConnectionContractMapper.PortKey, - "port.invalid", - "Port must be greater than zero.")); - } - - bool useIntegratedSecurity = GetBool(fields, ConnectionContractMapper.UseIntegratedSecurityKey, false); - if (useIntegratedSecurity && !capability.SupportsIntegratedSecurity) - { - errors.Add(new ConnectionValidationMessageDto( - ConnectionContractMapper.UseIntegratedSecurityKey, - "integratedSecurity.unsupported", - "Integrated security is not supported by the selected provider in this environment.")); - } - - if (!useIntegratedSecurity) - { - ValidateRequiredField(fields, errors, ConnectionContractMapper.UsernameKey, "Username is required."); - } - - if (GetBool(fields, ConnectionContractMapper.UseSslKey, false) && !capability.SupportsSsl) - { - warnings.Add(new ConnectionValidationMessageDto( - ConnectionContractMapper.UseSslKey, - "ssl.unsupported", - "SSL was requested, but the selected provider typically does not use SSL in this mode.")); - } - - return new ConnectionValidationResultDto(errors.Count == 0, errors, warnings); - } - - private static void ValidateRequiredField( - IReadOnlyDictionary fields, - ICollection errors, - string key, - string message) - { - if (string.IsNullOrWhiteSpace(GetString(fields, key))) - { - errors.Add(new ConnectionValidationMessageDto(key, $"{key}.required", message)); - } - } - - private static string? GetString(IReadOnlyDictionary fields, string key) - { - return fields.TryGetValue(key, out string? value) ? value : null; - } - - private static bool TryGetInt(IReadOnlyDictionary fields, string key, out int value) - { - value = default; - return fields.TryGetValue(key, out string? raw) && int.TryParse(raw, out value); - } - - private static bool GetBool(IReadOnlyDictionary fields, string key, bool fallback) - { - if (!fields.TryGetValue(key, out string? raw) || string.IsNullOrWhiteSpace(raw)) - return fallback; - - if (bool.TryParse(raw, out bool parsed)) - return parsed; - - return raw.Equals("1", StringComparison.OrdinalIgnoreCase) - || raw.Equals("yes", StringComparison.OrdinalIgnoreCase) - || raw.Equals("on", StringComparison.OrdinalIgnoreCase) - || raw.Equals("required", StringComparison.OrdinalIgnoreCase) - || raw.Equals("require", StringComparison.OrdinalIgnoreCase) - || raw.Equals("true", StringComparison.OrdinalIgnoreCase); - } -} +using AkkornStudio.Core; +using AkkornStudio.UI.Services.ConnectionManager.Contracts; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ConnectionValidationService : IConnectionValidationService +{ + public ConnectionValidationResultDto Validate(ConnectionDetailsDto details, ProviderCapabilityDto capability) + { + var errors = new List(); + var warnings = new List(); + + if (string.IsNullOrWhiteSpace(details.Name)) + { + errors.Add(new ConnectionValidationMessageDto("name", "name.required", "Connection name is required.")); + } + + if (details.Mode == ConnectionProviderModeDto.Url) + { + if (!capability.SupportsUrlMode) + { + errors.Add(new ConnectionValidationMessageDto("url", "url.unsupported", "The selected provider does not support URL mode.")); + } + + if (string.IsNullOrWhiteSpace(details.UrlValue)) + { + errors.Add(new ConnectionValidationMessageDto("url", "url.required", "Connection URL is required in URL mode.")); + } + + return new ConnectionValidationResultDto(errors.Count == 0, errors, warnings); + } + + IReadOnlyDictionary fields = details.FieldValues; + + if (!TryGetInt(fields, ConnectionContractMapper.TimeoutSecondsKey, out int timeout) || timeout <= 0) + { + errors.Add(new ConnectionValidationMessageDto( + ConnectionContractMapper.TimeoutSecondsKey, + "timeout.invalid", + "Timeout must be greater than zero.")); + } + + bool isSqlite = ConnectionContractMapper.TryParseProvider(details.Provider, out DatabaseProvider provider) + && provider == DatabaseProvider.SQLite; + + if (isSqlite) + { + if (string.IsNullOrWhiteSpace(GetString(fields, ConnectionContractMapper.DatabaseKey))) + { + errors.Add(new ConnectionValidationMessageDto( + ConnectionContractMapper.DatabaseKey, + "database.required", + "SQLite database path is required.")); + } + + return new ConnectionValidationResultDto(errors.Count == 0, errors, warnings); + } + + ValidateRequiredField(fields, errors, ConnectionContractMapper.HostKey, "Host is required."); + ValidateRequiredField(fields, errors, ConnectionContractMapper.DatabaseKey, "Database is required."); + + if (!TryGetInt(fields, ConnectionContractMapper.PortKey, out int port) || port <= 0) + { + errors.Add(new ConnectionValidationMessageDto( + ConnectionContractMapper.PortKey, + "port.invalid", + "Port must be greater than zero.")); + } + + bool useIntegratedSecurity = GetBool(fields, ConnectionContractMapper.UseIntegratedSecurityKey, false); + if (useIntegratedSecurity && !capability.SupportsIntegratedSecurity) + { + errors.Add(new ConnectionValidationMessageDto( + ConnectionContractMapper.UseIntegratedSecurityKey, + "integratedSecurity.unsupported", + "Integrated security is not supported by the selected provider in this environment.")); + } + + if (!useIntegratedSecurity) + { + ValidateRequiredField(fields, errors, ConnectionContractMapper.UsernameKey, "Username is required."); + } + + if (GetBool(fields, ConnectionContractMapper.UseSslKey, false) && !capability.SupportsSsl) + { + warnings.Add(new ConnectionValidationMessageDto( + ConnectionContractMapper.UseSslKey, + "ssl.unsupported", + "SSL was requested, but the selected provider typically does not use SSL in this mode.")); + } + + return new ConnectionValidationResultDto(errors.Count == 0, errors, warnings); + } + + private static void ValidateRequiredField( + IReadOnlyDictionary fields, + ICollection errors, + string key, + string message) + { + if (string.IsNullOrWhiteSpace(GetString(fields, key))) + { + errors.Add(new ConnectionValidationMessageDto(key, $"{key}.required", message)); + } + } + + private static string? GetString(IReadOnlyDictionary fields, string key) + { + return fields.TryGetValue(key, out string? value) ? value : null; + } + + private static bool TryGetInt(IReadOnlyDictionary fields, string key, out int value) + { + value = default; + return fields.TryGetValue(key, out string? raw) && int.TryParse(raw, out value); + } + + private static bool GetBool(IReadOnlyDictionary fields, string key, bool fallback) + { + if (!fields.TryGetValue(key, out string? raw) || string.IsNullOrWhiteSpace(raw)) + return fallback; + + if (bool.TryParse(raw, out bool parsed)) + return parsed; + + return raw.Equals("1", StringComparison.OrdinalIgnoreCase) + || raw.Equals("yes", StringComparison.OrdinalIgnoreCase) + || raw.Equals("on", StringComparison.OrdinalIgnoreCase) + || raw.Equals("required", StringComparison.OrdinalIgnoreCase) + || raw.Equals("require", StringComparison.OrdinalIgnoreCase) + || raw.Equals("true", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/DbOrchestratorConnectionTestExecutor.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/DbOrchestratorConnectionTestExecutor.cs similarity index 90% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/DbOrchestratorConnectionTestExecutor.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/DbOrchestratorConnectionTestExecutor.cs index f6c2a7df..38e78512 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/DbOrchestratorConnectionTestExecutor.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/DbOrchestratorConnectionTestExecutor.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.Providers; +using AkkornStudio.Core; +using AkkornStudio.Providers; -namespace DBWeaver.UI.Services.ConnectionManager; +namespace AkkornStudio.UI.Services.ConnectionManager; public sealed class DbOrchestratorConnectionTestExecutor : IConnectionTestExecutor { diff --git a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ProviderCapabilityService.cs b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ProviderCapabilityService.cs similarity index 93% rename from src/DBWeaver.UI/Services/ConnectionManager/Workflow/ProviderCapabilityService.cs rename to src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ProviderCapabilityService.cs index 89c5ea78..a1c1d4c5 100644 --- a/src/DBWeaver.UI/Services/ConnectionManager/Workflow/ProviderCapabilityService.cs +++ b/src/AkkornStudio.UI/Services/ConnectionManager/Workflow/ProviderCapabilityService.cs @@ -1,69 +1,69 @@ -using DBWeaver.UI.Services.ConnectionManager.Contracts; - -namespace DBWeaver.UI.Services.ConnectionManager; - -public sealed class ProviderCapabilityService : IProviderCapabilityService -{ - private static readonly IReadOnlyList _capabilities = - [ - new ProviderCapabilityDto( - Provider: "Postgres", - SupportsUrlMode: true, - SupportsSsl: true, - SupportsIntegratedSecurity: false, - RequiresDatabase: true, - SupportedUrlSchemes: ["postgres", "postgresql"], - RequiredFieldKeys: [ - ConnectionContractMapper.HostKey, - ConnectionContractMapper.PortKey, - ConnectionContractMapper.DatabaseKey, - ConnectionContractMapper.UsernameKey, - ]), - new ProviderCapabilityDto( - Provider: "MySql", - SupportsUrlMode: true, - SupportsSsl: true, - SupportsIntegratedSecurity: false, - RequiresDatabase: true, - SupportedUrlSchemes: ["mysql"], - RequiredFieldKeys: [ - ConnectionContractMapper.HostKey, - ConnectionContractMapper.PortKey, - ConnectionContractMapper.DatabaseKey, - ConnectionContractMapper.UsernameKey, - ]), - new ProviderCapabilityDto( - Provider: "SqlServer", - SupportsUrlMode: true, - SupportsSsl: true, - SupportsIntegratedSecurity: OperatingSystem.IsWindows(), - RequiresDatabase: true, - SupportedUrlSchemes: ["sqlserver", "mssql"], - RequiredFieldKeys: [ - ConnectionContractMapper.HostKey, - ConnectionContractMapper.PortKey, - ConnectionContractMapper.DatabaseKey, - ]), - new ProviderCapabilityDto( - Provider: "SQLite", - SupportsUrlMode: true, - SupportsSsl: false, - SupportsIntegratedSecurity: false, - RequiresDatabase: true, - SupportedUrlSchemes: ["sqlite", "file"], - RequiredFieldKeys: [ConnectionContractMapper.DatabaseKey]), - ]; - - public Task> ListCapabilitiesAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult(_capabilities); - } - - public Task GetCapabilityAsync(string provider, CancellationToken cancellationToken = default) - { - ProviderCapabilityDto? capability = _capabilities.FirstOrDefault(c => - string.Equals(c.Provider, provider, StringComparison.OrdinalIgnoreCase)); - - return Task.FromResult(capability); - } -} +using AkkornStudio.UI.Services.ConnectionManager.Contracts; + +namespace AkkornStudio.UI.Services.ConnectionManager; + +public sealed class ProviderCapabilityService : IProviderCapabilityService +{ + private static readonly IReadOnlyList _capabilities = + [ + new ProviderCapabilityDto( + Provider: "Postgres", + SupportsUrlMode: true, + SupportsSsl: true, + SupportsIntegratedSecurity: false, + RequiresDatabase: true, + SupportedUrlSchemes: ["postgres", "postgresql"], + RequiredFieldKeys: [ + ConnectionContractMapper.HostKey, + ConnectionContractMapper.PortKey, + ConnectionContractMapper.DatabaseKey, + ConnectionContractMapper.UsernameKey, + ]), + new ProviderCapabilityDto( + Provider: "MySql", + SupportsUrlMode: true, + SupportsSsl: true, + SupportsIntegratedSecurity: false, + RequiresDatabase: true, + SupportedUrlSchemes: ["mysql"], + RequiredFieldKeys: [ + ConnectionContractMapper.HostKey, + ConnectionContractMapper.PortKey, + ConnectionContractMapper.DatabaseKey, + ConnectionContractMapper.UsernameKey, + ]), + new ProviderCapabilityDto( + Provider: "SqlServer", + SupportsUrlMode: true, + SupportsSsl: true, + SupportsIntegratedSecurity: OperatingSystem.IsWindows(), + RequiresDatabase: true, + SupportedUrlSchemes: ["sqlserver", "mssql"], + RequiredFieldKeys: [ + ConnectionContractMapper.HostKey, + ConnectionContractMapper.PortKey, + ConnectionContractMapper.DatabaseKey, + ]), + new ProviderCapabilityDto( + Provider: "SQLite", + SupportsUrlMode: true, + SupportsSsl: false, + SupportsIntegratedSecurity: false, + RequiresDatabase: true, + SupportedUrlSchemes: ["sqlite", "file"], + RequiredFieldKeys: [ConnectionContractMapper.DatabaseKey]), + ]; + + public Task> ListCapabilitiesAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(_capabilities); + } + + public Task GetCapabilityAsync(string provider, CancellationToken cancellationToken = default) + { + ProviderCapabilityDto? capability = _capabilities.FirstOrDefault(c => + string.Equals(c.Provider, provider, StringComparison.OrdinalIgnoreCase)); + + return Task.FromResult(capability); + } +} diff --git a/src/DBWeaver.UI/Services/Ddl/DdlSchemaImporter.cs b/src/AkkornStudio.UI/Services/Ddl/DdlSchemaImporter.cs similarity index 94% rename from src/DBWeaver.UI/Services/Ddl/DdlSchemaImporter.cs rename to src/AkkornStudio.UI/Services/Ddl/DdlSchemaImporter.cs index 59702e15..31a25270 100644 --- a/src/DBWeaver.UI/Services/Ddl/DdlSchemaImporter.cs +++ b/src/AkkornStudio.UI/Services/Ddl/DdlSchemaImporter.cs @@ -1,887 +1,918 @@ -using Avalonia; -using System.Text.Json; -using System.Text.RegularExpressions; -using DBWeaver.Core; -using DBWeaver.Metadata; -using DBWeaver.Nodes; -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.Serialization; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services.Ddl; - -public sealed record DdlImportResult( - int TableCount, - int ColumnCount, - int ForeignKeyCount, - int IndexCount, - IReadOnlyList? Warnings = null -); - -public sealed record DdlPartialImportResult( - bool TableAdded, - int AddedNodeCount, - int AddedConnectionCount, - int AddedForeignKeys -); - -/// -/// Imports the connected database schema into the DDL canvas. -/// -public sealed class DdlSchemaImporter -{ - public DdlImportResult Import(DbMetadata metadata, CanvasViewModel canvas) - { - if (metadata is null) - throw new ArgumentNullException(nameof(metadata)); - - if (canvas is null) - throw new ArgumentNullException(nameof(canvas)); - - List tables = metadata - .AllTables - .Where(t => t.Kind == TableKind.Table) - .OrderBy(t => t.Schema, StringComparer.OrdinalIgnoreCase) - .ThenBy(t => t.Name, StringComparer.OrdinalIgnoreCase) - .ToList(); - - List views = metadata - .AllTables - .Where(t => t.Kind is TableKind.View or TableKind.MaterializedView) - .OrderBy(t => t.Schema, StringComparer.OrdinalIgnoreCase) - .ThenBy(t => t.Name, StringComparer.OrdinalIgnoreCase) - .ToList(); - - var warnings = new List(); - - if (tables.Count == 0 && views.Count == 0) - { - canvas.ReplaceGraph([], []); - return new DdlImportResult(0, 0, 0, 0); - } - - var nodes = new List(); - var connections = new List(); - var tableNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); - var sequenceNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); - var columnNodesByTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - const double tableSpacingX = 520; - const double tableSpacingY = 340; - const int maxRowsPerColumn = 4; - - int tableIndex = 0; - int totalColumns = 0; - int totalForeignKeys = 0; - int totalIndexes = 0; - - int sequenceIndex = 0; - foreach (SequenceMetadata sequence in metadata.AllSequences.OrderBy(s => s.Schema, StringComparer.OrdinalIgnoreCase).ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase)) - { - int gridColumn = sequenceIndex / maxRowsPerColumn; - int gridRow = sequenceIndex % maxRowsPerColumn; - - double baseX = 120 + (gridColumn * tableSpacingX); - double baseY = 80 + (gridRow * 180); - - NodeViewModel sequenceNode = NewNode(NodeType.SequenceDefinition, baseX, baseY); - sequenceNode.Parameters["Schema"] = sequence.Schema; - sequenceNode.Parameters["SequenceName"] = sequence.Name; - sequenceNode.Parameters["StartValue"] = sequence.StartValue?.ToString() ?? string.Empty; - sequenceNode.Parameters["Increment"] = sequence.Increment?.ToString() ?? string.Empty; - sequenceNode.Parameters["MinValue"] = sequence.MinValue?.ToString() ?? string.Empty; - sequenceNode.Parameters["MaxValue"] = sequence.MaxValue?.ToString() ?? string.Empty; - sequenceNode.Parameters["Cycle"] = sequence.Cycle.GetValueOrDefault() ? "true" : "false"; - sequenceNode.Parameters["Cache"] = sequence.Cache?.ToString() ?? string.Empty; - - NodeViewModel createSequenceOutputNode = NewNode(NodeType.CreateSequenceOutput, baseX + 280, baseY); - Connect(sequenceNode, "seq", createSequenceOutputNode, "seq", connections); - - nodes.Add(sequenceNode); - nodes.Add(createSequenceOutputNode); - sequenceNodes[sequence.FullName] = sequenceNode; - sequenceIndex++; - } - - foreach (TableMetadata table in tables) - { - int gridColumn = tableIndex / maxRowsPerColumn; - int gridRow = tableIndex % maxRowsPerColumn; - - double baseX = 120 + (gridColumn * tableSpacingX); - double baseY = 80 + (gridRow * tableSpacingY); - - NodeViewModel tableNode = NewNode(NodeType.TableDefinition, baseX, baseY); - tableNode.Parameters["SchemaName"] = table.Schema; - tableNode.Parameters["TableName"] = table.Name; - tableNode.Parameters["IfNotExists"] = "true"; - tableNode.Parameters["Comment"] = table.Comment ?? string.Empty; - - NodeViewModel createOutputNode = NewNode(NodeType.CreateTableOutput, baseX + 280, baseY); - Connect(tableNode, "table", createOutputNode, "table", connections); - - nodes.Add(tableNode); - nodes.Add(createOutputNode); - tableNodes[table.FullName] = tableNode; - - var tableColumnNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); - columnNodesByTable[table.FullName] = tableColumnNodes; - - int columnOffset = 0; - foreach (ColumnMetadata column in table.Columns.OrderBy(c => c.OrdinalPosition)) - { - NodeViewModel columnNode = NewNode(NodeType.ColumnDefinition, baseX - 300, baseY + (columnOffset * 86)); - columnNode.Parameters["ColumnName"] = column.Name; - columnNode.Parameters["DataType"] = NormalizeDataType(column); - columnNode.Parameters["IsNullable"] = column.IsNullable ? "true" : "false"; - columnNode.Parameters["Comment"] = column.Comment ?? string.Empty; - - if (TryResolveSequenceDefault(metadata.Provider, column.DefaultValue, out string? sequenceFullName) - && !string.IsNullOrWhiteSpace(sequenceFullName) - && sequenceNodes.TryGetValue(sequenceFullName, out NodeViewModel? sequenceNode)) - { - Connect(sequenceNode, "seq", columnNode, "sequence", connections); - } - - if (TryBuildEnumTypeNode(metadata.Provider, column, table, baseX, baseY + (columnOffset * 86), out NodeViewModel? enumTypeNode, out NodeViewModel? typeOutputNode)) - { - nodes.Add(enumTypeNode!); - if (typeOutputNode is not null) - { - nodes.Add(typeOutputNode); - Connect(enumTypeNode!, "type_def", typeOutputNode, "type_def", connections); - } - columnNode.Parameters["DataType"] = "ENUM"; - Connect(enumTypeNode!, "type_def", columnNode, "type_def", connections); - } - - nodes.Add(columnNode); - Connect(columnNode, "column", tableNode, "column", connections); - - tableColumnNodes[column.Name] = columnNode; - columnOffset++; - totalColumns++; - } - - IReadOnlyList pkColumns = table.Columns - .Where(c => c.IsPrimaryKey) - .OrderBy(c => c.OrdinalPosition) - .ToList(); - - if (pkColumns.Count > 0) - { - NodeViewModel pkNode = NewNode(NodeType.PrimaryKeyConstraint, baseX - 40, baseY - 120); - pkNode.Parameters["ConstraintName"] = $"PK_{table.Name}"; - nodes.Add(pkNode); - - Connect(pkNode, "pk", tableNode, "constraint", connections); - foreach (ColumnMetadata pkCol in pkColumns) - { - if (tableColumnNodes.TryGetValue(pkCol.Name, out NodeViewModel? colNode)) - Connect(colNode, "column", pkNode, "column", connections); - } - } - - foreach (IndexMetadata uniqueIndex in table.Indexes.Where(i => i.IsUnique && !i.IsPrimaryKey)) - { - NodeViewModel uqNode = NewNode(NodeType.UniqueConstraint, baseX + 40, baseY - 120 - (totalIndexes % 3 * 72)); - uqNode.Parameters["ConstraintName"] = uniqueIndex.Name; - nodes.Add(uqNode); - - Connect(uqNode, "uq", tableNode, "constraint", connections); - foreach (string colName in uniqueIndex.Columns) - { - if (tableColumnNodes.TryGetValue(colName, out NodeViewModel? colNode)) - Connect(colNode, "column", uqNode, "column", connections); - } - - totalIndexes++; - } - - tableIndex++; - } - - var viewNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); - int viewIndex = 0; - foreach (TableMetadata view in views) - { - int gridColumn = (tableIndex + viewIndex) / maxRowsPerColumn; - int gridRow = (tableIndex + viewIndex) % maxRowsPerColumn; - - double baseX = 120 + (gridColumn * tableSpacingX); - double baseY = 80 + (gridRow * tableSpacingY); - - AddViewSubgraph(view, new Point(baseX, baseY), nodes, connections, viewNodes); - - warnings.Add( - string.Format( - L( - "ddlImporter.warning.viewSelectNotReconstructable", - "View '{0}': view SELECT cannot be reconstructed visually - edit it manually in the subcanvas." - ), - view.FullName - ) - ); - viewIndex++; - } - - var fkGroups = metadata.AllForeignKeys - .GroupBy(f => $"{f.ChildSchema}.{f.ChildTable}::{f.ConstraintName}", StringComparer.OrdinalIgnoreCase) - .ToList(); - - int fkVisualIndex = 0; - foreach (IGrouping group in fkGroups) - { - List relations = group.OrderBy(r => r.OrdinalPosition).ToList(); - if (relations.Count == 0) - continue; - - ForeignKeyRelation first = relations[0]; - string childTableFullName = Qualify(first.ChildSchema, first.ChildTable); - string parentTableFullName = Qualify(first.ParentSchema, first.ParentTable); - - if (!tableNodes.TryGetValue(childTableFullName, out NodeViewModel? childTableNode)) - continue; - - if (!columnNodesByTable.TryGetValue(childTableFullName, out Dictionary? childColumns)) - continue; - - NodeViewModel fkNode = NewNode( - NodeType.ForeignKeyConstraint, - childTableNode.Position.X + 120, - childTableNode.Position.Y + 140 + (fkVisualIndex % 3 * 72) - ); - - fkNode.Parameters["ConstraintName"] = first.ConstraintName; - fkNode.Parameters["OnDelete"] = ToReferentialAction(first.OnDelete); - fkNode.Parameters["OnUpdate"] = ToReferentialAction(first.OnUpdate); - - nodes.Add(fkNode); - Connect(fkNode, "fk", childTableNode, "constraint", connections); - - foreach (ForeignKeyRelation relation in relations) - { - if (childColumns.TryGetValue(relation.ChildColumn, out NodeViewModel? childColumnNode)) - Connect(childColumnNode, "column", fkNode, "child_column", connections); - - if (columnNodesByTable.TryGetValue(parentTableFullName, out Dictionary? parentColumns) - && parentColumns.TryGetValue(relation.ParentColumn, out NodeViewModel? parentColumnNode)) - { - Connect(parentColumnNode, "column", fkNode, "parent_column", connections); - } - } - - fkVisualIndex++; - totalForeignKeys++; - } - - canvas.Provider = metadata.Provider; - ApplyAutoLayout(nodes, connections, new Point(120, 80)); - canvas.ReplaceGraph(nodes, connections); - - return new DdlImportResult(tables.Count, totalColumns, totalForeignKeys, totalIndexes, warnings); - } - - public DdlPartialImportResult ImportTable( - DbMetadata metadata, - string fullTableName, - CanvasViewModel canvas, - Point? suggestedOrigin = null - ) - { - if (metadata is null) - throw new ArgumentNullException(nameof(metadata)); - - if (canvas is null) - throw new ArgumentNullException(nameof(canvas)); - - TableMetadata? table = metadata.FindTable(fullTableName); - if (table is null) - throw new InvalidOperationException( - string.Format( - L("ddlImporter.error.tableNotFoundInMetadata", "Table '{0}' was not found in current metadata."), - fullTableName - ) - ); - - var nodes = canvas.Nodes.ToList(); - var connections = canvas.Connections.ToList(); - var tableNodes = BuildExistingTableNodeMap(nodes); - var viewNodes = BuildExistingViewNodeMap(nodes); - var sequenceNodes = BuildExistingSequenceNodeMap(nodes); - var columnNodesByTable = BuildExistingColumnNodeMap(nodes, connections); - - bool isView = table.Kind is TableKind.View or TableKind.MaterializedView; - - if (!isView && tableNodes.ContainsKey(table.FullName)) - return new DdlPartialImportResult(false, 0, 0, 0); - - if (isView && viewNodes.ContainsKey(table.FullName)) - return new DdlPartialImportResult(false, 0, 0, 0); - - Point origin = suggestedOrigin ?? ComputeSuggestedOrigin(nodes); - - int beforeNodes = nodes.Count; - int beforeConnections = connections.Count; - - int fkAdded = 0; - if (isView) - { - AddViewSubgraph(table, origin, nodes, connections, viewNodes); - } - else - { - AddTableSubgraph(metadata.Provider, metadata, table, origin, nodes, connections, tableNodes, sequenceNodes, columnNodesByTable, out int _); - fkAdded = AddForeignKeysForChildTable(metadata, table.FullName, nodes, connections, tableNodes, columnNodesByTable); - } - - ApplyAutoLayout(nodes, connections, new Point(120, 80)); - - canvas.Provider = metadata.Provider; - canvas.ReplaceGraph(nodes, connections); - - return new DdlPartialImportResult( - true, - nodes.Count - beforeNodes, - connections.Count - beforeConnections, - fkAdded - ); - } - - private static Dictionary BuildExistingTableNodeMap(IEnumerable nodes) - { - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (NodeViewModel node in nodes.Where(n => n.Type == NodeType.TableDefinition)) - { - string schema = node.Parameters.TryGetValue("SchemaName", out string? schemaName) - ? schemaName ?? string.Empty - : string.Empty; - string table = node.Parameters.TryGetValue("TableName", out string? tableName) - ? tableName ?? string.Empty - : string.Empty; - - if (string.IsNullOrWhiteSpace(table)) - continue; - - map[Qualify(schema, table)] = node; - } - - return map; - } - - private static Dictionary BuildExistingViewNodeMap(IEnumerable nodes) - { - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (NodeViewModel node in nodes.Where(n => n.Type == NodeType.ViewDefinition)) - { - string schema = node.Parameters.TryGetValue("Schema", out string? schemaName) - ? schemaName ?? string.Empty - : string.Empty; - string view = node.Parameters.TryGetValue("ViewName", out string? viewName) - ? viewName ?? string.Empty - : string.Empty; - - if (string.IsNullOrWhiteSpace(view)) - continue; - - map[Qualify(schema, view)] = node; - } - - return map; - } - - private static Dictionary> BuildExistingColumnNodeMap( - IEnumerable nodes, - IEnumerable connections) - { - _ = nodes; - var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - foreach (ConnectionViewModel connection in connections) - { - if (connection.ToPin is null) - continue; - - if (connection.ToPin.Name != "column") - continue; - - NodeViewModel toNode = connection.ToPin.Owner; - NodeViewModel fromNode = connection.FromPin.Owner; - if (toNode.Type != NodeType.TableDefinition || fromNode.Type != NodeType.ColumnDefinition) - continue; - - string schema = toNode.Parameters.TryGetValue("SchemaName", out string? schemaName) - ? schemaName ?? string.Empty - : string.Empty; - string table = toNode.Parameters.TryGetValue("TableName", out string? tableName) - ? tableName ?? string.Empty - : string.Empty; - string full = Qualify(schema, table); - - if (!result.TryGetValue(full, out Dictionary? tableCols)) - { - tableCols = new Dictionary(StringComparer.OrdinalIgnoreCase); - result[full] = tableCols; - } - - if (!fromNode.Parameters.TryGetValue("ColumnName", out string? colName) || string.IsNullOrWhiteSpace(colName)) - continue; - - tableCols[colName] = fromNode; - } - - return result; - } - - private static Dictionary BuildExistingSequenceNodeMap(IEnumerable nodes) - { - var map = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (NodeViewModel node in nodes.Where(n => n.Type == NodeType.SequenceDefinition)) - { - string schema = node.Parameters.TryGetValue("Schema", out string? schemaName) - ? schemaName ?? string.Empty - : string.Empty; - string name = node.Parameters.TryGetValue("SequenceName", out string? sequenceName) - ? sequenceName ?? string.Empty - : string.Empty; - - if (string.IsNullOrWhiteSpace(name)) - continue; - - map[Qualify(schema, name)] = node; - } - - return map; - } - - private static Point ComputeSuggestedOrigin(IEnumerable nodes) - { - double maxX = nodes.Any() ? nodes.Max(n => n.Position.X) : 120; - return new Point(maxX + 520, 80); - } - - private void AddTableSubgraph( - DatabaseProvider provider, - DbMetadata metadata, - TableMetadata table, - Point origin, - ICollection nodes, - ICollection connections, - IDictionary tableNodes, - IReadOnlyDictionary sequenceNodes, - IDictionary> columnNodesByTable, - out int uniqueIndexesAdded) - { - NodeViewModel tableNode = NewNode(NodeType.TableDefinition, origin.X, origin.Y); - tableNode.Parameters["SchemaName"] = table.Schema; - tableNode.Parameters["TableName"] = table.Name; - tableNode.Parameters["IfNotExists"] = "true"; - tableNode.Parameters["Comment"] = table.Comment ?? string.Empty; - - NodeViewModel createOutputNode = NewNode(NodeType.CreateTableOutput, origin.X + 280, origin.Y); - Connect(tableNode, "table", createOutputNode, "table", connections); - - nodes.Add(tableNode); - nodes.Add(createOutputNode); - tableNodes[table.FullName] = tableNode; - - var tableColumnNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); - columnNodesByTable[table.FullName] = tableColumnNodes; - - int columnOffset = 0; - foreach (ColumnMetadata column in table.Columns.OrderBy(c => c.OrdinalPosition)) - { - NodeViewModel columnNode = NewNode(NodeType.ColumnDefinition, origin.X - 300, origin.Y + (columnOffset * 86)); - columnNode.Parameters["ColumnName"] = column.Name; - columnNode.Parameters["DataType"] = NormalizeDataType(column); - columnNode.Parameters["IsNullable"] = column.IsNullable ? "true" : "false"; - columnNode.Parameters["Comment"] = column.Comment ?? string.Empty; - - if (TryResolveSequenceDefault(metadata.Provider, column.DefaultValue, out string? sequenceFullName) - && !string.IsNullOrWhiteSpace(sequenceFullName) - && sequenceNodes.TryGetValue(sequenceFullName, out NodeViewModel? sequenceNode)) - { - Connect(sequenceNode, "seq", columnNode, "sequence", connections); - } - - if (TryBuildEnumTypeNode(provider, column, table, origin.X, origin.Y + (columnOffset * 86), out NodeViewModel? enumTypeNode, out NodeViewModel? typeOutputNode)) - { - nodes.Add(enumTypeNode!); - if (typeOutputNode is not null) - { - nodes.Add(typeOutputNode); - Connect(enumTypeNode!, "type_def", typeOutputNode, "type_def", connections); - } - columnNode.Parameters["DataType"] = "ENUM"; - Connect(enumTypeNode!, "type_def", columnNode, "type_def", connections); - } - - nodes.Add(columnNode); - Connect(columnNode, "column", tableNode, "column", connections); - - tableColumnNodes[column.Name] = columnNode; - columnOffset++; - } - - IReadOnlyList pkColumns = table.Columns - .Where(c => c.IsPrimaryKey) - .OrderBy(c => c.OrdinalPosition) - .ToList(); - - if (pkColumns.Count > 0) - { - NodeViewModel pkNode = NewNode(NodeType.PrimaryKeyConstraint, origin.X - 40, origin.Y - 120); - pkNode.Parameters["ConstraintName"] = $"PK_{table.Name}"; - nodes.Add(pkNode); - - Connect(pkNode, "pk", tableNode, "constraint", connections); - foreach (ColumnMetadata pkCol in pkColumns) - { - if (tableColumnNodes.TryGetValue(pkCol.Name, out NodeViewModel? colNode)) - Connect(colNode, "column", pkNode, "column", connections); - } - } - - uniqueIndexesAdded = 0; - foreach (IndexMetadata uniqueIndex in table.Indexes.Where(i => i.IsUnique && !i.IsPrimaryKey)) - { - NodeViewModel uqNode = NewNode(NodeType.UniqueConstraint, origin.X + 40, origin.Y - 120 - (uniqueIndexesAdded % 3 * 72)); - uqNode.Parameters["ConstraintName"] = uniqueIndex.Name; - nodes.Add(uqNode); - - Connect(uqNode, "uq", tableNode, "constraint", connections); - foreach (string colName in uniqueIndex.Columns) - { - if (tableColumnNodes.TryGetValue(colName, out NodeViewModel? colNode)) - Connect(colNode, "column", uqNode, "column", connections); - } - - uniqueIndexesAdded++; - } - } - - private static void AddViewSubgraph( - TableMetadata view, - Point origin, - ICollection nodes, - ICollection connections, - IDictionary viewNodes) - { - NodeViewModel viewNode = NewNode(NodeType.ViewDefinition, origin.X, origin.Y); - viewNode.Parameters["Schema"] = view.Schema; - viewNode.Parameters["ViewName"] = view.Name; - viewNode.Parameters["OrReplace"] = "false"; - viewNode.Parameters["IsMaterialized"] = view.Kind == TableKind.MaterializedView ? "true" : "false"; - viewNode.Parameters["SelectSql"] = "SELECT 1"; - - NodeGraph seedGraph = BuildViewSeedSubgraph(); - viewNode.Parameters[CanvasSerializer.ViewSubgraphParameterKey] = JsonSerializer.Serialize(seedGraph); - viewNode.Parameters[CanvasSerializer.ViewFromTableParameterKey] = "(SELECT 1 AS placeholder) view_src"; - - NodeViewModel createViewOutputNode = NewNode(NodeType.CreateViewOutput, origin.X + 280, origin.Y); - Connect(viewNode, "view", createViewOutputNode, "view", connections); - - nodes.Add(viewNode); - nodes.Add(createViewOutputNode); - viewNodes[view.FullName] = viewNode; - } - - private static NodeGraph BuildViewSeedSubgraph() - { - const string queryNodeId = "view_query"; - const string outputNodeId = "view_output"; - - return new NodeGraph - { - Nodes = - [ - new NodeInstance( - queryNodeId, - NodeType.Subquery, - PinLiterals: new Dictionary(), - Parameters: new Dictionary - { - ["query"] = "SELECT 1 AS placeholder", - ["alias"] = "view_src", - }), - new NodeInstance( - outputNodeId, - NodeType.ResultOutput, - PinLiterals: new Dictionary(), - Parameters: new Dictionary()) - ], - Connections = [], - SelectOutputs = [] - }; - } - - private int AddForeignKeysForChildTable( - DbMetadata metadata, - string childTableFullName, - ICollection nodes, - ICollection connections, - IReadOnlyDictionary tableNodes, - IReadOnlyDictionary> columnNodesByTable) - { - if (!tableNodes.TryGetValue(childTableFullName, out NodeViewModel? childTableNode)) - return 0; - - if (!columnNodesByTable.TryGetValue(childTableFullName, out Dictionary? childColumns)) - return 0; - - var fkGroups = metadata.AllForeignKeys - .Where(fk => Qualify(fk.ChildSchema, fk.ChildTable).Equals(childTableFullName, StringComparison.OrdinalIgnoreCase)) - .GroupBy(f => $"{f.ChildSchema}.{f.ChildTable}::{f.ConstraintName}", StringComparer.OrdinalIgnoreCase) - .ToList(); - - int added = 0; - foreach (IGrouping group in fkGroups) - { - List relations = group.OrderBy(r => r.OrdinalPosition).ToList(); - if (relations.Count == 0) - continue; - - ForeignKeyRelation first = relations[0]; - string parentTableFullName = Qualify(first.ParentSchema, first.ParentTable); - if (!tableNodes.TryGetValue(parentTableFullName, out NodeViewModel? _)) - continue; - - if (!columnNodesByTable.TryGetValue(parentTableFullName, out Dictionary? parentColumns)) - continue; - - NodeViewModel fkNode = NewNode( - NodeType.ForeignKeyConstraint, - childTableNode.Position.X + 120, - childTableNode.Position.Y + 140 + (added % 3 * 72) - ); - - fkNode.Parameters["ConstraintName"] = first.ConstraintName; - fkNode.Parameters["OnDelete"] = ToReferentialAction(first.OnDelete); - fkNode.Parameters["OnUpdate"] = ToReferentialAction(first.OnUpdate); - - nodes.Add(fkNode); - Connect(fkNode, "fk", childTableNode, "constraint", connections); - - foreach (ForeignKeyRelation relation in relations) - { - if (childColumns.TryGetValue(relation.ChildColumn, out NodeViewModel? childColumnNode)) - Connect(childColumnNode, "column", fkNode, "child_column", connections); - - if (parentColumns.TryGetValue(relation.ParentColumn, out NodeViewModel? parentColumnNode)) - Connect(parentColumnNode, "column", fkNode, "parent_column", connections); - } - - added++; - } - - return added; - } - - private static void ApplyAutoLayout(IReadOnlyList nodes, IReadOnlyList connections, Point origin) - { - Dictionary layout = NodeLayoutManager.ComputeAutoLayout( - nodes, - connections, - IsDdlSinkNode, - origin - ); - - foreach (KeyValuePair item in layout) - item.Key.Position = item.Value; - } - - private static bool IsDdlSinkNode(NodeViewModel node) => - node.Type is NodeType.CreateTableOutput or NodeType.CreateTypeOutput or NodeType.CreateSequenceOutput or NodeType.AlterTableOutput or NodeType.CreateIndexOutput or NodeType.CreateViewOutput or NodeType.AlterViewOutput; - - private static NodeViewModel NewNode(NodeType type, double x, double y) - { - return new NodeViewModel(NodeDefinitionRegistry.Get(type), new Point(x, y)); - } - - private static void Connect( - NodeViewModel fromNode, - string fromPinName, - NodeViewModel toNode, - string toPinName, - ICollection sink) - { - PinViewModel? fromPin = fromNode.FindPin(fromPinName, PinDirection.Output); - PinViewModel? toPin = toNode.FindPin(toPinName, PinDirection.Input); - - if (fromPin is null || toPin is null) - return; - - sink.Add(new ConnectionViewModel(fromPin, default, default) { ToPin = toPin }); - } - - private static string NormalizeDataType(ColumnMetadata column) - { - string native = column.NativeType.Trim(); - if (string.IsNullOrWhiteSpace(native)) - native = column.DataType.Trim(); - - string lower = native.ToLowerInvariant(); - - if (lower.Contains("bigint")) - return "BIGINT"; - - if (lower.Contains("smallint")) - return "SMALLINT"; - - if (lower.Contains("int") || lower.Contains("serial")) - return "INT"; - - if (lower.Contains("numeric") || lower.Contains("decimal")) - { - if (column.Precision.HasValue && column.Scale.HasValue) - return $"DECIMAL({column.Precision.Value},{column.Scale.Value})"; - - if (column.Precision.HasValue) - return $"DECIMAL({column.Precision.Value})"; - - return "DECIMAL"; - } - - if (lower.Contains("double") || lower.Contains("float") || lower.Contains("real")) - return "DOUBLE"; - - if (lower.Contains("boolean") || lower.Contains("bool") || lower.Contains("bit")) - return "BOOLEAN"; - - if (lower.Contains("timestamp") || lower.Contains("datetime")) - return "TIMESTAMP"; - - if (lower.Contains("date")) - return "DATE"; - - if (lower.Contains("time")) - return "TIME"; - - if (lower.Contains("uuid") || lower.Contains("uniqueidentifier")) - return "UUID"; - - if (lower.Contains("json")) - return "JSON"; - - if (lower.Contains("char") || lower.Contains("text")) - { - if (column.MaxLength.HasValue && column.MaxLength.Value > 0 && lower.Contains("char")) - return $"VARCHAR({column.MaxLength.Value})"; - - return lower.Contains("text") ? "TEXT" : "VARCHAR"; - } - - return native.ToUpperInvariant(); - } - - private static bool TryBuildEnumTypeNode( - DatabaseProvider provider, - ColumnMetadata column, - TableMetadata table, - double anchorX, - double anchorY, - out NodeViewModel? enumTypeNode, - out NodeViewModel? typeOutputNode) - { - enumTypeNode = null; - typeOutputNode = null; - - if (!TryParseEnumValues(column.NativeType, out IReadOnlyList values)) - return false; - - string defaultTypeName = $"{table.Name}_{column.Name}_enum".ToLowerInvariant(); - enumTypeNode = NewNode(NodeType.EnumTypeDefinition, anchorX - 520, anchorY); - enumTypeNode.Parameters["SchemaName"] = string.IsNullOrWhiteSpace(table.Schema) ? "public" : table.Schema; - enumTypeNode.Parameters["TypeName"] = defaultTypeName; - enumTypeNode.Parameters["EnumValues"] = string.Join(",", values); - - if (provider == DatabaseProvider.Postgres) - typeOutputNode = NewNode(NodeType.CreateTypeOutput, anchorX - 260, anchorY); - - return true; - } - - private static bool TryParseEnumValues(string nativeType, out IReadOnlyList values) - { - values = []; - if (string.IsNullOrWhiteSpace(nativeType)) - return false; - - Match match = Regex.Match(nativeType, @"enum\s*\((?.*)\)", RegexOptions.IgnoreCase); - if (!match.Success) - return false; - - string raw = match.Groups["values"].Value; - string[] parts = raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (parts.Length == 0) - return false; - - values = - [ - .. parts - .Select(v => v.Trim().Trim('\'', '"')) - .Where(v => !string.IsNullOrWhiteSpace(v)), - ]; - - return values.Count > 0; - } - - private static string ToReferentialAction(ReferentialAction action) - { - return action switch - { - ReferentialAction.Cascade => "CASCADE", - ReferentialAction.SetNull => "SET NULL", - ReferentialAction.SetDefault => "SET DEFAULT", - ReferentialAction.Restrict => "RESTRICT", - _ => "NO ACTION", - }; - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } - - private static string Qualify(string schema, string table) - { - return string.IsNullOrWhiteSpace(schema) ? table : $"{schema}.{table}"; - } - - private static bool TryResolveSequenceDefault(DatabaseProvider provider, string? defaultExpression, out string? sequenceFullName) - { - sequenceFullName = null; - if (string.IsNullOrWhiteSpace(defaultExpression)) - return false; - - if (provider == DatabaseProvider.Postgres) - { - Match match = Regex.Match(defaultExpression, @"nextval\('(?[^']+)'", RegexOptions.IgnoreCase); - if (!match.Success) - return false; - - string value = match.Groups["name"].Value; - if (value.EndsWith("::regclass", StringComparison.OrdinalIgnoreCase)) - value = value[..^10]; - - value = value.Trim('"'); - sequenceFullName = value; - return true; - } - - if (provider == DatabaseProvider.SqlServer) - { - Match match = Regex.Match(defaultExpression, @"NEXT\s+VALUE\s+FOR\s+(?[\[\]\w\.]+)", RegexOptions.IgnoreCase); - if (!match.Success) - return false; - - string raw = match.Groups["name"].Value; - sequenceFullName = raw.Replace("[", string.Empty).Replace("]", string.Empty); - return true; - } - - return false; - } -} +using Avalonia; +using System.Text.Json; +using System.Text.RegularExpressions; +using AkkornStudio.Core; +using AkkornStudio.Metadata; +using AkkornStudio.Nodes; +using AkkornStudio.UI.Services.Localization; +using AkkornStudio.UI.Serialization; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services.Ddl; + +public sealed record DdlImportResult( + int TableCount, + int ColumnCount, + int ForeignKeyCount, + int IndexCount, + IReadOnlyList? Warnings = null +); + +public sealed record DdlPartialImportResult( + bool TableAdded, + int AddedNodeCount, + int AddedConnectionCount, + int AddedForeignKeys +); + +/// +/// Imports the connected database schema into the DDL canvas. +/// +public sealed class DdlSchemaImporter +{ + public DdlImportResult Import(DbMetadata metadata, CanvasViewModel canvas) + { + if (metadata is null) + throw new ArgumentNullException(nameof(metadata)); + + if (canvas is null) + throw new ArgumentNullException(nameof(canvas)); + + List tables = metadata + .AllTables + .Where(t => t.Kind == TableKind.Table) + .OrderBy(t => t.Schema, StringComparer.OrdinalIgnoreCase) + .ThenBy(t => t.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + + List views = metadata + .AllTables + .Where(t => t.Kind is TableKind.View or TableKind.MaterializedView) + .OrderBy(t => t.Schema, StringComparer.OrdinalIgnoreCase) + .ThenBy(t => t.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + + var warnings = new List(); + + if (tables.Count == 0 && views.Count == 0) + { + canvas.ReplaceGraph([], []); + return new DdlImportResult(0, 0, 0, 0); + } + + var nodes = new List(); + var connections = new List(); + var tableNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); + var sequenceNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); + var columnNodesByTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + const double tableSpacingX = 520; + const double tableSpacingY = 340; + const int maxRowsPerColumn = 4; + + int tableIndex = 0; + int totalColumns = 0; + int totalForeignKeys = 0; + int totalIndexes = 0; + + int sequenceIndex = 0; + foreach (SequenceMetadata sequence in metadata.AllSequences.OrderBy(s => s.Schema, StringComparer.OrdinalIgnoreCase).ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase)) + { + int gridColumn = sequenceIndex / maxRowsPerColumn; + int gridRow = sequenceIndex % maxRowsPerColumn; + + double baseX = 120 + (gridColumn * tableSpacingX); + double baseY = 80 + (gridRow * 180); + + NodeViewModel sequenceNode = NewNode(NodeType.SequenceDefinition, baseX, baseY); + sequenceNode.Parameters["Schema"] = sequence.Schema; + sequenceNode.Parameters["SequenceName"] = sequence.Name; + sequenceNode.Parameters["StartValue"] = sequence.StartValue?.ToString() ?? string.Empty; + sequenceNode.Parameters["Increment"] = sequence.Increment?.ToString() ?? string.Empty; + sequenceNode.Parameters["MinValue"] = sequence.MinValue?.ToString() ?? string.Empty; + sequenceNode.Parameters["MaxValue"] = sequence.MaxValue?.ToString() ?? string.Empty; + sequenceNode.Parameters["Cycle"] = sequence.Cycle.GetValueOrDefault() ? "true" : "false"; + sequenceNode.Parameters["Cache"] = sequence.Cache?.ToString() ?? string.Empty; + + NodeViewModel createSequenceOutputNode = NewNode(NodeType.CreateSequenceOutput, baseX + 280, baseY); + Connect(sequenceNode, "seq", createSequenceOutputNode, "seq", connections); + + nodes.Add(sequenceNode); + nodes.Add(createSequenceOutputNode); + sequenceNodes[sequence.FullName] = sequenceNode; + sequenceIndex++; + } + + foreach (TableMetadata table in tables) + { + int gridColumn = tableIndex / maxRowsPerColumn; + int gridRow = tableIndex % maxRowsPerColumn; + + double baseX = 120 + (gridColumn * tableSpacingX); + double baseY = 80 + (gridRow * tableSpacingY); + + NodeViewModel tableNode = NewNode(NodeType.TableDefinition, baseX, baseY); + tableNode.Parameters["SchemaName"] = table.Schema; + tableNode.Parameters["TableName"] = table.Name; + tableNode.Parameters["IfNotExists"] = "true"; + tableNode.Parameters["Comment"] = table.Comment ?? string.Empty; + + NodeViewModel createOutputNode = NewNode(NodeType.CreateTableOutput, baseX + 280, baseY); + Connect(tableNode, "table", createOutputNode, "table", connections); + + nodes.Add(tableNode); + nodes.Add(createOutputNode); + tableNodes[table.FullName] = tableNode; + + var tableColumnNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); + columnNodesByTable[table.FullName] = tableColumnNodes; + + int columnOffset = 0; + foreach (ColumnMetadata column in table.Columns.OrderBy(c => c.OrdinalPosition)) + { + NodeViewModel columnNode = NewNode(NodeType.ColumnDefinition, baseX - 300, baseY + (columnOffset * 86)); + columnNode.Parameters["ColumnName"] = column.Name; + columnNode.Parameters["DataType"] = NormalizeDataType(column); + columnNode.Parameters["IsNullable"] = column.IsNullable ? "true" : "false"; + columnNode.Parameters["Comment"] = column.Comment ?? string.Empty; + + if (TryResolveSequenceDefault(metadata.Provider, column.DefaultValue, out string? sequenceFullName) + && !string.IsNullOrWhiteSpace(sequenceFullName) + && sequenceNodes.TryGetValue(sequenceFullName, out NodeViewModel? sequenceNode)) + { + Connect(sequenceNode, "seq", columnNode, "sequence", connections); + } + + if (TryBuildEnumTypeNode(metadata.Provider, column, table, baseX, baseY + (columnOffset * 86), out NodeViewModel? enumTypeNode, out NodeViewModel? typeOutputNode)) + { + nodes.Add(enumTypeNode!); + if (typeOutputNode is not null) + { + nodes.Add(typeOutputNode); + Connect(enumTypeNode!, "type_def", typeOutputNode, "type_def", connections); + } + columnNode.Parameters["DataType"] = "ENUM"; + Connect(enumTypeNode!, "type_def", columnNode, "type_def", connections); + } + + nodes.Add(columnNode); + Connect(columnNode, "column", tableNode, "column", connections); + + tableColumnNodes[column.Name] = columnNode; + columnOffset++; + totalColumns++; + } + + IReadOnlyList pkColumns = table.Columns + .Where(c => c.IsPrimaryKey) + .OrderBy(c => c.OrdinalPosition) + .ToList(); + + if (pkColumns.Count > 0) + { + NodeViewModel pkNode = NewNode(NodeType.PrimaryKeyConstraint, baseX - 40, baseY - 120); + pkNode.Parameters["ConstraintName"] = $"PK_{table.Name}"; + nodes.Add(pkNode); + + Connect(pkNode, "pk", tableNode, "constraint", connections); + foreach (ColumnMetadata pkCol in pkColumns) + { + if (tableColumnNodes.TryGetValue(pkCol.Name, out NodeViewModel? colNode)) + Connect(colNode, "column", pkNode, "column", connections); + } + } + + foreach (IndexMetadata uniqueIndex in table.Indexes.Where(i => i.IsUnique && !i.IsPrimaryKey)) + { + NodeViewModel uqNode = NewNode(NodeType.UniqueConstraint, baseX + 40, baseY - 120 - (totalIndexes % 3 * 72)); + uqNode.Parameters["ConstraintName"] = uniqueIndex.Name; + nodes.Add(uqNode); + + Connect(uqNode, "uq", tableNode, "constraint", connections); + foreach (string colName in uniqueIndex.Columns) + { + if (tableColumnNodes.TryGetValue(colName, out NodeViewModel? colNode)) + Connect(colNode, "column", uqNode, "column", connections); + } + + totalIndexes++; + } + + tableIndex++; + } + + var viewNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); + int viewIndex = 0; + foreach (TableMetadata view in views) + { + int gridColumn = (tableIndex + viewIndex) / maxRowsPerColumn; + int gridRow = (tableIndex + viewIndex) % maxRowsPerColumn; + + double baseX = 120 + (gridColumn * tableSpacingX); + double baseY = 80 + (gridRow * tableSpacingY); + + AddViewSubgraph(metadata.Provider, view, new Point(baseX, baseY), nodes, connections, viewNodes); + + warnings.Add( + string.Format( + L( + "ddlImporter.warning.viewSelectNotReconstructable", + "View '{0}': view SELECT cannot be reconstructed visually - edit it manually in the subcanvas." + ), + view.FullName + ) + ); + viewIndex++; + } + + var fkGroups = metadata.AllForeignKeys + .GroupBy(f => $"{f.ChildSchema}.{f.ChildTable}::{f.ConstraintName}", StringComparer.OrdinalIgnoreCase) + .ToList(); + + int fkVisualIndex = 0; + foreach (IGrouping group in fkGroups) + { + List relations = group.OrderBy(r => r.OrdinalPosition).ToList(); + if (relations.Count == 0) + continue; + + ForeignKeyRelation first = relations[0]; + string childTableFullName = Qualify(first.ChildSchema, first.ChildTable); + string parentTableFullName = Qualify(first.ParentSchema, first.ParentTable); + + if (!tableNodes.TryGetValue(childTableFullName, out NodeViewModel? childTableNode)) + continue; + + if (!columnNodesByTable.TryGetValue(childTableFullName, out Dictionary? childColumns)) + continue; + + NodeViewModel fkNode = NewNode( + NodeType.ForeignKeyConstraint, + childTableNode.Position.X + 120, + childTableNode.Position.Y + 140 + (fkVisualIndex % 3 * 72) + ); + + fkNode.Parameters["ConstraintName"] = first.ConstraintName; + fkNode.Parameters["OnDelete"] = ToReferentialAction(first.OnDelete); + fkNode.Parameters["OnUpdate"] = ToReferentialAction(first.OnUpdate); + + nodes.Add(fkNode); + Connect(fkNode, "fk", childTableNode, "constraint", connections); + + foreach (ForeignKeyRelation relation in relations) + { + if (childColumns.TryGetValue(relation.ChildColumn, out NodeViewModel? childColumnNode)) + Connect(childColumnNode, "column", fkNode, "child_column", connections); + + if (columnNodesByTable.TryGetValue(parentTableFullName, out Dictionary? parentColumns) + && parentColumns.TryGetValue(relation.ParentColumn, out NodeViewModel? parentColumnNode)) + { + Connect(parentColumnNode, "column", fkNode, "parent_column", connections); + } + } + + fkVisualIndex++; + totalForeignKeys++; + } + + canvas.Provider = metadata.Provider; + ApplyAutoLayout(nodes, connections, new Point(120, 80)); + canvas.ReplaceGraph(nodes, connections); + + return new DdlImportResult(tables.Count, totalColumns, totalForeignKeys, totalIndexes, warnings); + } + + public DdlPartialImportResult ImportTable( + DbMetadata metadata, + string fullTableName, + CanvasViewModel canvas, + Point? suggestedOrigin = null + ) + { + if (metadata is null) + throw new ArgumentNullException(nameof(metadata)); + + if (canvas is null) + throw new ArgumentNullException(nameof(canvas)); + + TableMetadata? table = metadata.FindTable(fullTableName); + if (table is null) + throw new InvalidOperationException( + string.Format( + L("ddlImporter.error.tableNotFoundInMetadata", "Table '{0}' was not found in current metadata."), + fullTableName + ) + ); + + var nodes = canvas.Nodes.ToList(); + var connections = canvas.Connections.ToList(); + var tableNodes = BuildExistingTableNodeMap(nodes); + var viewNodes = BuildExistingViewNodeMap(nodes); + var sequenceNodes = BuildExistingSequenceNodeMap(nodes); + var columnNodesByTable = BuildExistingColumnNodeMap(nodes, connections); + + bool isView = table.Kind is TableKind.View or TableKind.MaterializedView; + + if (!isView && tableNodes.ContainsKey(table.FullName)) + return new DdlPartialImportResult(false, 0, 0, 0); + + if (isView && viewNodes.ContainsKey(table.FullName)) + return new DdlPartialImportResult(false, 0, 0, 0); + + Point origin = suggestedOrigin ?? ComputeSuggestedOrigin(nodes); + + int beforeNodes = nodes.Count; + int beforeConnections = connections.Count; + + int fkAdded = 0; + if (isView) + { + AddViewSubgraph(metadata.Provider, table, origin, nodes, connections, viewNodes); + } + else + { + AddTableSubgraph(metadata.Provider, metadata, table, origin, nodes, connections, tableNodes, sequenceNodes, columnNodesByTable, out int _); + fkAdded = AddForeignKeysForChildTable(metadata, table.FullName, nodes, connections, tableNodes, columnNodesByTable); + } + + ApplyAutoLayout(nodes, connections, new Point(120, 80)); + + canvas.Provider = metadata.Provider; + canvas.ReplaceGraph(nodes, connections); + + return new DdlPartialImportResult( + true, + nodes.Count - beforeNodes, + connections.Count - beforeConnections, + fkAdded + ); + } + + private static Dictionary BuildExistingTableNodeMap(IEnumerable nodes) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (NodeViewModel node in nodes.Where(n => n.Type == NodeType.TableDefinition)) + { + string schema = node.Parameters.TryGetValue("SchemaName", out string? schemaName) + ? schemaName ?? string.Empty + : string.Empty; + string table = node.Parameters.TryGetValue("TableName", out string? tableName) + ? tableName ?? string.Empty + : string.Empty; + + if (string.IsNullOrWhiteSpace(table)) + continue; + + map[Qualify(schema, table)] = node; + } + + return map; + } + + private static Dictionary BuildExistingViewNodeMap(IEnumerable nodes) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (NodeViewModel node in nodes.Where(n => n.Type == NodeType.ViewDefinition)) + { + string schema = node.Parameters.TryGetValue("Schema", out string? schemaName) + ? schemaName ?? string.Empty + : string.Empty; + string view = node.Parameters.TryGetValue("ViewName", out string? viewName) + ? viewName ?? string.Empty + : string.Empty; + + if (string.IsNullOrWhiteSpace(view)) + continue; + + map[Qualify(schema, view)] = node; + } + + return map; + } + + private static Dictionary> BuildExistingColumnNodeMap( + IEnumerable nodes, + IEnumerable connections) + { + _ = nodes; + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + foreach (ConnectionViewModel connection in connections) + { + if (connection.ToPin is null) + continue; + + if (connection.ToPin.Name != "column") + continue; + + NodeViewModel toNode = connection.ToPin.Owner; + NodeViewModel fromNode = connection.FromPin.Owner; + if (toNode.Type != NodeType.TableDefinition || fromNode.Type != NodeType.ColumnDefinition) + continue; + + string schema = toNode.Parameters.TryGetValue("SchemaName", out string? schemaName) + ? schemaName ?? string.Empty + : string.Empty; + string table = toNode.Parameters.TryGetValue("TableName", out string? tableName) + ? tableName ?? string.Empty + : string.Empty; + string full = Qualify(schema, table); + + if (!result.TryGetValue(full, out Dictionary? tableCols)) + { + tableCols = new Dictionary(StringComparer.OrdinalIgnoreCase); + result[full] = tableCols; + } + + if (!fromNode.Parameters.TryGetValue("ColumnName", out string? colName) || string.IsNullOrWhiteSpace(colName)) + continue; + + tableCols[colName] = fromNode; + } + + return result; + } + + private static Dictionary BuildExistingSequenceNodeMap(IEnumerable nodes) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (NodeViewModel node in nodes.Where(n => n.Type == NodeType.SequenceDefinition)) + { + string schema = node.Parameters.TryGetValue("Schema", out string? schemaName) + ? schemaName ?? string.Empty + : string.Empty; + string name = node.Parameters.TryGetValue("SequenceName", out string? sequenceName) + ? sequenceName ?? string.Empty + : string.Empty; + + if (string.IsNullOrWhiteSpace(name)) + continue; + + map[Qualify(schema, name)] = node; + } + + return map; + } + + private static Point ComputeSuggestedOrigin(IEnumerable nodes) + { + double maxX = nodes.Any() ? nodes.Max(n => n.Position.X) : 120; + return new Point(maxX + 520, 80); + } + + private void AddTableSubgraph( + DatabaseProvider provider, + DbMetadata metadata, + TableMetadata table, + Point origin, + ICollection nodes, + ICollection connections, + IDictionary tableNodes, + IReadOnlyDictionary sequenceNodes, + IDictionary> columnNodesByTable, + out int uniqueIndexesAdded) + { + NodeViewModel tableNode = NewNode(NodeType.TableDefinition, origin.X, origin.Y); + tableNode.Parameters["SchemaName"] = table.Schema; + tableNode.Parameters["TableName"] = table.Name; + tableNode.Parameters["IfNotExists"] = "true"; + tableNode.Parameters["Comment"] = table.Comment ?? string.Empty; + + NodeViewModel createOutputNode = NewNode(NodeType.CreateTableOutput, origin.X + 280, origin.Y); + Connect(tableNode, "table", createOutputNode, "table", connections); + + nodes.Add(tableNode); + nodes.Add(createOutputNode); + tableNodes[table.FullName] = tableNode; + + var tableColumnNodes = new Dictionary(StringComparer.OrdinalIgnoreCase); + columnNodesByTable[table.FullName] = tableColumnNodes; + + int columnOffset = 0; + foreach (ColumnMetadata column in table.Columns.OrderBy(c => c.OrdinalPosition)) + { + NodeViewModel columnNode = NewNode(NodeType.ColumnDefinition, origin.X - 300, origin.Y + (columnOffset * 86)); + columnNode.Parameters["ColumnName"] = column.Name; + columnNode.Parameters["DataType"] = NormalizeDataType(column); + columnNode.Parameters["IsNullable"] = column.IsNullable ? "true" : "false"; + columnNode.Parameters["Comment"] = column.Comment ?? string.Empty; + + if (TryResolveSequenceDefault(metadata.Provider, column.DefaultValue, out string? sequenceFullName) + && !string.IsNullOrWhiteSpace(sequenceFullName) + && sequenceNodes.TryGetValue(sequenceFullName, out NodeViewModel? sequenceNode)) + { + Connect(sequenceNode, "seq", columnNode, "sequence", connections); + } + + if (TryBuildEnumTypeNode(provider, column, table, origin.X, origin.Y + (columnOffset * 86), out NodeViewModel? enumTypeNode, out NodeViewModel? typeOutputNode)) + { + nodes.Add(enumTypeNode!); + if (typeOutputNode is not null) + { + nodes.Add(typeOutputNode); + Connect(enumTypeNode!, "type_def", typeOutputNode, "type_def", connections); + } + columnNode.Parameters["DataType"] = "ENUM"; + Connect(enumTypeNode!, "type_def", columnNode, "type_def", connections); + } + + nodes.Add(columnNode); + Connect(columnNode, "column", tableNode, "column", connections); + + tableColumnNodes[column.Name] = columnNode; + columnOffset++; + } + + IReadOnlyList pkColumns = table.Columns + .Where(c => c.IsPrimaryKey) + .OrderBy(c => c.OrdinalPosition) + .ToList(); + + if (pkColumns.Count > 0) + { + NodeViewModel pkNode = NewNode(NodeType.PrimaryKeyConstraint, origin.X - 40, origin.Y - 120); + pkNode.Parameters["ConstraintName"] = $"PK_{table.Name}"; + nodes.Add(pkNode); + + Connect(pkNode, "pk", tableNode, "constraint", connections); + foreach (ColumnMetadata pkCol in pkColumns) + { + if (tableColumnNodes.TryGetValue(pkCol.Name, out NodeViewModel? colNode)) + Connect(colNode, "column", pkNode, "column", connections); + } + } + + uniqueIndexesAdded = 0; + foreach (IndexMetadata uniqueIndex in table.Indexes.Where(i => i.IsUnique && !i.IsPrimaryKey)) + { + NodeViewModel uqNode = NewNode(NodeType.UniqueConstraint, origin.X + 40, origin.Y - 120 - (uniqueIndexesAdded % 3 * 72)); + uqNode.Parameters["ConstraintName"] = uniqueIndex.Name; + nodes.Add(uqNode); + + Connect(uqNode, "uq", tableNode, "constraint", connections); + foreach (string colName in uniqueIndex.Columns) + { + if (tableColumnNodes.TryGetValue(colName, out NodeViewModel? colNode)) + Connect(colNode, "column", uqNode, "column", connections); + } + + uniqueIndexesAdded++; + } + } + + private static void AddViewSubgraph( + DatabaseProvider provider, + TableMetadata view, + Point origin, + ICollection nodes, + ICollection connections, + IDictionary viewNodes) + { + NodeViewModel viewNode = NewNode(NodeType.ViewDefinition, origin.X, origin.Y); + viewNode.Parameters["Schema"] = view.Schema; + viewNode.Parameters["ViewName"] = view.Name; + viewNode.Parameters["OrReplace"] = "false"; + viewNode.Parameters["IsMaterialized"] = view.Kind == TableKind.MaterializedView ? "true" : "false"; + string selectSql = BuildViewSeedSelectSql(provider, view); + viewNode.Parameters["SelectSql"] = selectSql; + + NodeGraph seedGraph = BuildViewSeedSubgraph(selectSql); + viewNode.Parameters[CanvasSerializer.ViewSubgraphParameterKey] = JsonSerializer.Serialize(seedGraph); + viewNode.Parameters[CanvasSerializer.ViewFromTableParameterKey] = $"({selectSql}) view_src"; + + NodeViewModel createViewOutputNode = NewNode(NodeType.CreateViewOutput, origin.X + 280, origin.Y); + Connect(viewNode, "view", createViewOutputNode, "view", connections); + + nodes.Add(viewNode); + nodes.Add(createViewOutputNode); + viewNodes[view.FullName] = viewNode; + } + + private static NodeGraph BuildViewSeedSubgraph(string selectSql) + { + const string queryNodeId = "view_query"; + const string outputNodeId = "view_output"; + + return new NodeGraph + { + Nodes = + [ + new NodeInstance( + queryNodeId, + NodeType.Subquery, + PinLiterals: new Dictionary(), + Parameters: new Dictionary + { + ["query"] = selectSql, + ["alias"] = "view_src", + }), + new NodeInstance( + outputNodeId, + NodeType.ResultOutput, + PinLiterals: new Dictionary(), + Parameters: new Dictionary()) + ], + Connections = [], + SelectOutputs = [] + }; + } + + private static string BuildViewSeedSelectSql(DatabaseProvider provider, TableMetadata view) + { + IReadOnlyList columns = view.Columns + .OrderBy(static column => column.OrdinalPosition) + .ToList(); + + if (columns.Count == 0) + return "SELECT 1 AS placeholder"; + + string projection = string.Join( + ", ", + columns.Select(column => $"NULL AS {QuoteIdentifier(provider, column.Name)}")); + return $"SELECT {projection}"; + } + + private static string QuoteIdentifier(DatabaseProvider provider, string identifier) + { + string safeIdentifier = string.IsNullOrWhiteSpace(identifier) + ? "column" + : identifier.Trim(); + + return provider switch + { + DatabaseProvider.SqlServer => $"[{safeIdentifier.Replace("]", "]]", StringComparison.Ordinal)}]", + DatabaseProvider.MySql => $"`{safeIdentifier.Replace("`", "``", StringComparison.Ordinal)}`", + _ => $"\"{safeIdentifier.Replace("\"", "\"\"", StringComparison.Ordinal)}\"", + }; + } + + private int AddForeignKeysForChildTable( + DbMetadata metadata, + string childTableFullName, + ICollection nodes, + ICollection connections, + IReadOnlyDictionary tableNodes, + IReadOnlyDictionary> columnNodesByTable) + { + if (!tableNodes.TryGetValue(childTableFullName, out NodeViewModel? childTableNode)) + return 0; + + if (!columnNodesByTable.TryGetValue(childTableFullName, out Dictionary? childColumns)) + return 0; + + var fkGroups = metadata.AllForeignKeys + .Where(fk => Qualify(fk.ChildSchema, fk.ChildTable).Equals(childTableFullName, StringComparison.OrdinalIgnoreCase)) + .GroupBy(f => $"{f.ChildSchema}.{f.ChildTable}::{f.ConstraintName}", StringComparer.OrdinalIgnoreCase) + .ToList(); + + int added = 0; + foreach (IGrouping group in fkGroups) + { + List relations = group.OrderBy(r => r.OrdinalPosition).ToList(); + if (relations.Count == 0) + continue; + + ForeignKeyRelation first = relations[0]; + string parentTableFullName = Qualify(first.ParentSchema, first.ParentTable); + if (!tableNodes.TryGetValue(parentTableFullName, out NodeViewModel? _)) + continue; + + if (!columnNodesByTable.TryGetValue(parentTableFullName, out Dictionary? parentColumns)) + continue; + + NodeViewModel fkNode = NewNode( + NodeType.ForeignKeyConstraint, + childTableNode.Position.X + 120, + childTableNode.Position.Y + 140 + (added % 3 * 72) + ); + + fkNode.Parameters["ConstraintName"] = first.ConstraintName; + fkNode.Parameters["OnDelete"] = ToReferentialAction(first.OnDelete); + fkNode.Parameters["OnUpdate"] = ToReferentialAction(first.OnUpdate); + + nodes.Add(fkNode); + Connect(fkNode, "fk", childTableNode, "constraint", connections); + + foreach (ForeignKeyRelation relation in relations) + { + if (childColumns.TryGetValue(relation.ChildColumn, out NodeViewModel? childColumnNode)) + Connect(childColumnNode, "column", fkNode, "child_column", connections); + + if (parentColumns.TryGetValue(relation.ParentColumn, out NodeViewModel? parentColumnNode)) + Connect(parentColumnNode, "column", fkNode, "parent_column", connections); + } + + added++; + } + + return added; + } + + private static void ApplyAutoLayout(IReadOnlyList nodes, IReadOnlyList connections, Point origin) + { + Dictionary layout = NodeLayoutManager.ComputeAutoLayout( + nodes, + connections, + IsDdlSinkNode, + origin + ); + + foreach (KeyValuePair item in layout) + item.Key.Position = item.Value; + } + + private static bool IsDdlSinkNode(NodeViewModel node) => + node.Type is NodeType.CreateTableOutput or NodeType.CreateTypeOutput or NodeType.CreateSequenceOutput or NodeType.AlterTableOutput or NodeType.CreateIndexOutput or NodeType.CreateViewOutput or NodeType.AlterViewOutput; + + private static NodeViewModel NewNode(NodeType type, double x, double y) + { + return new NodeViewModel(NodeDefinitionRegistry.Get(type), new Point(x, y)); + } + + private static void Connect( + NodeViewModel fromNode, + string fromPinName, + NodeViewModel toNode, + string toPinName, + ICollection sink) + { + PinViewModel? fromPin = fromNode.FindPin(fromPinName, PinDirection.Output); + PinViewModel? toPin = toNode.FindPin(toPinName, PinDirection.Input); + + if (fromPin is null || toPin is null) + return; + + sink.Add(new ConnectionViewModel(fromPin, default, default) { ToPin = toPin }); + } + + private static string NormalizeDataType(ColumnMetadata column) + { + string native = column.NativeType.Trim(); + if (string.IsNullOrWhiteSpace(native)) + native = column.DataType.Trim(); + + string lower = native.ToLowerInvariant(); + + if (lower.Contains("bigint")) + return "BIGINT"; + + if (lower.Contains("smallint")) + return "SMALLINT"; + + if (lower.Contains("int") || lower.Contains("serial")) + return "INT"; + + if (lower.Contains("numeric") || lower.Contains("decimal")) + { + if (column.Precision.HasValue && column.Scale.HasValue) + return $"DECIMAL({column.Precision.Value},{column.Scale.Value})"; + + if (column.Precision.HasValue) + return $"DECIMAL({column.Precision.Value})"; + + return "DECIMAL"; + } + + if (lower.Contains("double") || lower.Contains("float") || lower.Contains("real")) + return "DOUBLE"; + + if (lower.Contains("boolean") || lower.Contains("bool") || lower.Contains("bit")) + return "BOOLEAN"; + + if (lower.Contains("timestamp") || lower.Contains("datetime")) + return "TIMESTAMP"; + + if (lower.Contains("date")) + return "DATE"; + + if (lower.Contains("time")) + return "TIME"; + + if (lower.Contains("uuid") || lower.Contains("uniqueidentifier")) + return "UUID"; + + if (lower.Contains("json")) + return "JSON"; + + if (lower.Contains("char") || lower.Contains("text")) + { + if (column.MaxLength.HasValue && column.MaxLength.Value > 0 && lower.Contains("char")) + return $"VARCHAR({column.MaxLength.Value})"; + + return lower.Contains("text") ? "TEXT" : "VARCHAR"; + } + + return native.ToUpperInvariant(); + } + + private static bool TryBuildEnumTypeNode( + DatabaseProvider provider, + ColumnMetadata column, + TableMetadata table, + double anchorX, + double anchorY, + out NodeViewModel? enumTypeNode, + out NodeViewModel? typeOutputNode) + { + enumTypeNode = null; + typeOutputNode = null; + + if (!TryParseEnumValues(column.NativeType, out IReadOnlyList values)) + return false; + + string defaultTypeName = $"{table.Name}_{column.Name}_enum".ToLowerInvariant(); + enumTypeNode = NewNode(NodeType.EnumTypeDefinition, anchorX - 520, anchorY); + enumTypeNode.Parameters["SchemaName"] = string.IsNullOrWhiteSpace(table.Schema) ? "public" : table.Schema; + enumTypeNode.Parameters["TypeName"] = defaultTypeName; + enumTypeNode.Parameters["EnumValues"] = string.Join(",", values); + + if (provider == DatabaseProvider.Postgres) + typeOutputNode = NewNode(NodeType.CreateTypeOutput, anchorX - 260, anchorY); + + return true; + } + + private static bool TryParseEnumValues(string nativeType, out IReadOnlyList values) + { + values = []; + if (string.IsNullOrWhiteSpace(nativeType)) + return false; + + Match match = Regex.Match(nativeType, @"enum\s*\((?.*)\)", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + string raw = match.Groups["values"].Value; + string[] parts = raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length == 0) + return false; + + values = + [ + .. parts + .Select(v => v.Trim().Trim('\'', '"')) + .Where(v => !string.IsNullOrWhiteSpace(v)), + ]; + + return values.Count > 0; + } + + private static string ToReferentialAction(ReferentialAction action) + { + return action switch + { + ReferentialAction.Cascade => "CASCADE", + ReferentialAction.SetNull => "SET NULL", + ReferentialAction.SetDefault => "SET DEFAULT", + ReferentialAction.Restrict => "RESTRICT", + _ => "NO ACTION", + }; + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } + + private static string Qualify(string schema, string table) + { + return string.IsNullOrWhiteSpace(schema) ? table : $"{schema}.{table}"; + } + + private static bool TryResolveSequenceDefault(DatabaseProvider provider, string? defaultExpression, out string? sequenceFullName) + { + sequenceFullName = null; + if (string.IsNullOrWhiteSpace(defaultExpression)) + return false; + + if (provider == DatabaseProvider.Postgres) + { + Match match = Regex.Match(defaultExpression, @"nextval\('(?[^']+)'", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + string value = match.Groups["name"].Value; + if (value.EndsWith("::regclass", StringComparison.OrdinalIgnoreCase)) + value = value[..^10]; + + value = value.Trim('"'); + sequenceFullName = value; + return true; + } + + if (provider == DatabaseProvider.SqlServer) + { + Match match = Regex.Match(defaultExpression, @"NEXT\s+VALUE\s+FOR\s+(?[\[\]\w\.]+)", RegexOptions.IgnoreCase); + if (!match.Success) + return false; + + string raw = match.Groups["name"].Value; + sequenceFullName = raw.Replace("[", string.Empty).Replace("]", string.Empty); + return true; + } + + return false; + } +} diff --git a/src/DBWeaver.UI/Services/Editors/SqlEditorHighlightingService.cs b/src/AkkornStudio.UI/Services/Editors/SqlEditorHighlightingService.cs similarity index 83% rename from src/DBWeaver.UI/Services/Editors/SqlEditorHighlightingService.cs rename to src/AkkornStudio.UI/Services/Editors/SqlEditorHighlightingService.cs index fe5beddd..dab56b9c 100644 --- a/src/DBWeaver.UI/Services/Editors/SqlEditorHighlightingService.cs +++ b/src/AkkornStudio.UI/Services/Editors/SqlEditorHighlightingService.cs @@ -1,28 +1,28 @@ -using System.Xml; -using Avalonia.Platform; -using AvaloniaEdit.Highlighting; -using AvaloniaEdit.Highlighting.Xshd; - -namespace DBWeaver.UI.Services; - -public static class SqlEditorHighlightingService -{ - private static readonly Lazy CachedDefinition = new(LoadDefinition); - - public static IHighlightingDefinition? GetSqlDefinition() => CachedDefinition.Value; - - private static IHighlightingDefinition? LoadDefinition() - { - try - { - var uri = new Uri("avares://DBWeaver.UI/Assets/Syntax/Sql.xshd"); - using var stream = AssetLoader.Open(uri); - using var reader = XmlReader.Create(stream); - return HighlightingLoader.Load(reader, HighlightingManager.Instance); - } - catch - { - return null; - } - } -} +using System.Xml; +using Avalonia.Platform; +using AvaloniaEdit.Highlighting; +using AvaloniaEdit.Highlighting.Xshd; + +namespace AkkornStudio.UI.Services; + +public static class SqlEditorHighlightingService +{ + private static readonly Lazy CachedDefinition = new(LoadDefinition); + + public static IHighlightingDefinition? GetSqlDefinition() => CachedDefinition.Value; + + private static IHighlightingDefinition? LoadDefinition() + { + try + { + var uri = new Uri("avares://AkkornStudio.UI/Assets/Syntax/Sql.xshd"); + using var stream = AssetLoader.Open(uri); + using var reader = XmlReader.Create(stream); + return HighlightingLoader.Load(reader, HighlightingManager.Instance); + } + catch + { + return null; + } + } +} diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainCostDistributionCalculator.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainCostDistributionCalculator.cs similarity index 94% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainCostDistributionCalculator.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainCostDistributionCalculator.cs index 3d7fd5d1..b5379ebc 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainCostDistributionCalculator.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainCostDistributionCalculator.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainCostDistributionCalculator { diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainExecutionModeEvaluator.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainExecutionModeEvaluator.cs similarity index 86% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainExecutionModeEvaluator.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainExecutionModeEvaluator.cs index 9ca1fea5..2ae88faa 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainExecutionModeEvaluator.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainExecutionModeEvaluator.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainExecutionModeEvaluator { diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainHighlightedTableResolver.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainHighlightedTableResolver.cs similarity index 96% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainHighlightedTableResolver.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainHighlightedTableResolver.cs index 9043d8a1..c22e4ef4 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainHighlightedTableResolver.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainHighlightedTableResolver.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainHighlightedTableResolver { diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainIndexSuggestionEngine.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainIndexSuggestionEngine.cs similarity index 97% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainIndexSuggestionEngine.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainIndexSuggestionEngine.cs index 1d2951f3..4176c1d5 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainIndexSuggestionEngine.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainIndexSuggestionEngine.cs @@ -1,8 +1,8 @@ using System.Text.RegularExpressions; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.Core; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class ExplainIndexSuggestion { diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainNodeToStepMapper.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainNodeToStepMapper.cs similarity index 94% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainNodeToStepMapper.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainNodeToStepMapper.cs index 0e2dac89..317b13b6 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainNodeToStepMapper.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainNodeToStepMapper.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainNodeToStepMapper { diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainPlanComparisonBuilder.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainPlanComparisonBuilder.cs similarity index 95% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainPlanComparisonBuilder.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainPlanComparisonBuilder.cs index 2d6a44da..ab59efa8 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainPlanComparisonBuilder.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainPlanComparisonBuilder.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainPlanComparisonBuilder { diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainSqlPreviewTextResolver.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainSqlPreviewTextResolver.cs similarity index 79% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainSqlPreviewTextResolver.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainSqlPreviewTextResolver.cs index a1907dde..7950da1c 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainSqlPreviewTextResolver.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainSqlPreviewTextResolver.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainSqlPreviewTextResolver { diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainSqlSafetyEvaluator.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainSqlSafetyEvaluator.cs similarity index 94% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainSqlSafetyEvaluator.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainSqlSafetyEvaluator.cs index d245f8bf..af235b7e 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainSqlSafetyEvaluator.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainSqlSafetyEvaluator.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainSqlSafetyEvaluator { diff --git a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainTreeLayoutBuilder.cs b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainTreeLayoutBuilder.cs similarity index 97% rename from src/DBWeaver.UI/Services/Explain/Analysis/ExplainTreeLayoutBuilder.cs rename to src/AkkornStudio.UI/Services/Explain/Analysis/ExplainTreeLayoutBuilder.cs index e9188ea6..c55f87fa 100644 --- a/src/DBWeaver.UI/Services/Explain/Analysis/ExplainTreeLayoutBuilder.cs +++ b/src/AkkornStudio.UI/Services/Explain/Analysis/ExplainTreeLayoutBuilder.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class ExplainTreeVisualNode { diff --git a/src/DBWeaver.UI/Services/Explain/Contracts/ExplainContracts.cs b/src/AkkornStudio.UI/Services/Explain/Contracts/ExplainContracts.cs similarity index 93% rename from src/DBWeaver.UI/Services/Explain/Contracts/ExplainContracts.cs rename to src/AkkornStudio.UI/Services/Explain/Contracts/ExplainContracts.cs index 4c27d112..5cb2720e 100644 --- a/src/DBWeaver.UI/Services/Explain/Contracts/ExplainContracts.cs +++ b/src/AkkornStudio.UI/Services/Explain/Contracts/ExplainContracts.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public enum ExplainFormat { diff --git a/src/DBWeaver.UI/Services/Explain/Contracts/ExplainExecutor.cs b/src/AkkornStudio.UI/Services/Explain/Contracts/ExplainExecutor.cs similarity index 95% rename from src/DBWeaver.UI/Services/Explain/Contracts/ExplainExecutor.cs rename to src/AkkornStudio.UI/Services/Explain/Contracts/ExplainExecutor.cs index 5ff628aa..1cdf99a7 100644 --- a/src/DBWeaver.UI/Services/Explain/Contracts/ExplainExecutor.cs +++ b/src/AkkornStudio.UI/Services/Explain/Contracts/ExplainExecutor.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class ExplainExecutor : IExplainExecutor { diff --git a/src/DBWeaver.UI/Services/Explain/Contracts/IMySqlExplainPlanParser.cs b/src/AkkornStudio.UI/Services/Explain/Contracts/IMySqlExplainPlanParser.cs similarity index 64% rename from src/DBWeaver.UI/Services/Explain/Contracts/IMySqlExplainPlanParser.cs rename to src/AkkornStudio.UI/Services/Explain/Contracts/IMySqlExplainPlanParser.cs index afc394bb..5b72147f 100644 --- a/src/DBWeaver.UI/Services/Explain/Contracts/IMySqlExplainPlanParser.cs +++ b/src/AkkornStudio.UI/Services/Explain/Contracts/IMySqlExplainPlanParser.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IMySqlExplainPlanParser { diff --git a/src/DBWeaver.UI/Services/Explain/Contracts/IMySqlExplainQueryRunner.cs b/src/AkkornStudio.UI/Services/Explain/Contracts/IMySqlExplainQueryRunner.cs similarity index 76% rename from src/DBWeaver.UI/Services/Explain/Contracts/IMySqlExplainQueryRunner.cs rename to src/AkkornStudio.UI/Services/Explain/Contracts/IMySqlExplainQueryRunner.cs index 61144a13..83c86029 100644 --- a/src/DBWeaver.UI/Services/Explain/Contracts/IMySqlExplainQueryRunner.cs +++ b/src/AkkornStudio.UI/Services/Explain/Contracts/IMySqlExplainQueryRunner.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IMySqlExplainQueryRunner { diff --git a/src/DBWeaver.UI/Services/Explain/Contracts/ISqlServerExplainPlanParser.cs b/src/AkkornStudio.UI/Services/Explain/Contracts/ISqlServerExplainPlanParser.cs similarity index 54% rename from src/DBWeaver.UI/Services/Explain/Contracts/ISqlServerExplainPlanParser.cs rename to src/AkkornStudio.UI/Services/Explain/Contracts/ISqlServerExplainPlanParser.cs index d68ad700..fd23a5f3 100644 --- a/src/DBWeaver.UI/Services/Explain/Contracts/ISqlServerExplainPlanParser.cs +++ b/src/AkkornStudio.UI/Services/Explain/Contracts/ISqlServerExplainPlanParser.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface ISqlServerExplainPlanParser { diff --git a/src/DBWeaver.UI/Services/Explain/Contracts/ISqlServerExplainQueryRunner.cs b/src/AkkornStudio.UI/Services/Explain/Contracts/ISqlServerExplainQueryRunner.cs similarity index 76% rename from src/DBWeaver.UI/Services/Explain/Contracts/ISqlServerExplainQueryRunner.cs rename to src/AkkornStudio.UI/Services/Explain/Contracts/ISqlServerExplainQueryRunner.cs index 9442595c..58ac673d 100644 --- a/src/DBWeaver.UI/Services/Explain/Contracts/ISqlServerExplainQueryRunner.cs +++ b/src/AkkornStudio.UI/Services/Explain/Contracts/ISqlServerExplainQueryRunner.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface ISqlServerExplainQueryRunner { diff --git a/src/DBWeaver.UI/Services/Explain/Execution/MySqlExplainExecutor.cs b/src/AkkornStudio.UI/Services/Explain/Execution/MySqlExplainExecutor.cs similarity index 96% rename from src/DBWeaver.UI/Services/Explain/Execution/MySqlExplainExecutor.cs rename to src/AkkornStudio.UI/Services/Explain/Execution/MySqlExplainExecutor.cs index 79150aa4..4c6c166b 100644 --- a/src/DBWeaver.UI/Services/Explain/Execution/MySqlExplainExecutor.cs +++ b/src/AkkornStudio.UI/Services/Explain/Execution/MySqlExplainExecutor.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class MySqlExplainExecutor : IExplainExecutor { diff --git a/src/DBWeaver.UI/Services/Explain/Execution/MySqlExplainQueryRunner.cs b/src/AkkornStudio.UI/Services/Explain/Execution/MySqlExplainQueryRunner.cs similarity index 93% rename from src/DBWeaver.UI/Services/Explain/Execution/MySqlExplainQueryRunner.cs rename to src/AkkornStudio.UI/Services/Explain/Execution/MySqlExplainQueryRunner.cs index 38037812..75a68273 100644 --- a/src/DBWeaver.UI/Services/Explain/Execution/MySqlExplainQueryRunner.cs +++ b/src/AkkornStudio.UI/Services/Explain/Execution/MySqlExplainQueryRunner.cs @@ -1,9 +1,9 @@ using MySqlConnector; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.Core; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.Core; using System.Diagnostics.CodeAnalysis; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; [ExcludeFromCodeCoverage] public sealed class MySqlExplainQueryRunner : IMySqlExplainQueryRunner diff --git a/src/DBWeaver.UI/Services/Explain/Execution/PostgresExplainExecutor.cs b/src/AkkornStudio.UI/Services/Explain/Execution/PostgresExplainExecutor.cs similarity index 96% rename from src/DBWeaver.UI/Services/Explain/Execution/PostgresExplainExecutor.cs rename to src/AkkornStudio.UI/Services/Explain/Execution/PostgresExplainExecutor.cs index c17afcaa..85c1ab57 100644 --- a/src/DBWeaver.UI/Services/Explain/Execution/PostgresExplainExecutor.cs +++ b/src/AkkornStudio.UI/Services/Explain/Execution/PostgresExplainExecutor.cs @@ -1,8 +1,8 @@ using Npgsql; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.Core; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IPostgresExplainQueryRunner { diff --git a/src/DBWeaver.UI/Services/Explain/Execution/SimulatedExplainExecutor.cs b/src/AkkornStudio.UI/Services/Explain/Execution/SimulatedExplainExecutor.cs similarity index 98% rename from src/DBWeaver.UI/Services/Explain/Execution/SimulatedExplainExecutor.cs rename to src/AkkornStudio.UI/Services/Explain/Execution/SimulatedExplainExecutor.cs index 8dd3c2f3..80b6ccb5 100644 --- a/src/DBWeaver.UI/Services/Explain/Execution/SimulatedExplainExecutor.cs +++ b/src/AkkornStudio.UI/Services/Explain/Execution/SimulatedExplainExecutor.cs @@ -1,8 +1,8 @@ using System.Text.RegularExpressions; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.Core; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class SimulatedExplainExecutor : IExplainExecutor { diff --git a/src/DBWeaver.UI/Services/Explain/Execution/SqlServerExplainExecutor.cs b/src/AkkornStudio.UI/Services/Explain/Execution/SqlServerExplainExecutor.cs similarity index 96% rename from src/DBWeaver.UI/Services/Explain/Execution/SqlServerExplainExecutor.cs rename to src/AkkornStudio.UI/Services/Explain/Execution/SqlServerExplainExecutor.cs index d0788cc2..92a07b15 100644 --- a/src/DBWeaver.UI/Services/Explain/Execution/SqlServerExplainExecutor.cs +++ b/src/AkkornStudio.UI/Services/Explain/Execution/SqlServerExplainExecutor.cs @@ -1,7 +1,7 @@ -using DBWeaver.Core; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.Core; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class SqlServerExplainExecutor : IExplainExecutor { diff --git a/src/DBWeaver.UI/Services/Explain/Execution/SqlServerExplainQueryRunner.cs b/src/AkkornStudio.UI/Services/Explain/Execution/SqlServerExplainQueryRunner.cs similarity index 97% rename from src/DBWeaver.UI/Services/Explain/Execution/SqlServerExplainQueryRunner.cs rename to src/AkkornStudio.UI/Services/Explain/Execution/SqlServerExplainQueryRunner.cs index 7834e3be..08f25808 100644 --- a/src/DBWeaver.UI/Services/Explain/Execution/SqlServerExplainQueryRunner.cs +++ b/src/AkkornStudio.UI/Services/Explain/Execution/SqlServerExplainQueryRunner.cs @@ -1,11 +1,11 @@ using Microsoft.Data.SqlClient; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; using System.Data.SqlTypes; using System.Text; -using DBWeaver.Core; +using AkkornStudio.Core; using System.Diagnostics.CodeAnalysis; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; [ExcludeFromCodeCoverage] public sealed class SqlServerExplainQueryRunner : ISqlServerExplainQueryRunner diff --git a/src/DBWeaver.UI/Services/Explain/Execution/SqliteExplainExecutor.cs b/src/AkkornStudio.UI/Services/Explain/Execution/SqliteExplainExecutor.cs similarity index 97% rename from src/DBWeaver.UI/Services/Explain/Execution/SqliteExplainExecutor.cs rename to src/AkkornStudio.UI/Services/Explain/Execution/SqliteExplainExecutor.cs index d766efb6..b962d40e 100644 --- a/src/DBWeaver.UI/Services/Explain/Execution/SqliteExplainExecutor.cs +++ b/src/AkkornStudio.UI/Services/Explain/Execution/SqliteExplainExecutor.cs @@ -1,8 +1,8 @@ using Microsoft.Data.Sqlite; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.Core; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.Core; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class SqliteExplainExecutor : IExplainExecutor { diff --git a/src/DBWeaver.UI/Services/Explain/Models/ExplainComparisonRow.cs b/src/AkkornStudio.UI/Services/Explain/Models/ExplainComparisonRow.cs similarity index 90% rename from src/DBWeaver.UI/Services/Explain/Models/ExplainComparisonRow.cs rename to src/AkkornStudio.UI/Services/Explain/Models/ExplainComparisonRow.cs index e64dd663..7b7009e8 100644 --- a/src/DBWeaver.UI/Services/Explain/Models/ExplainComparisonRow.cs +++ b/src/AkkornStudio.UI/Services/Explain/Models/ExplainComparisonRow.cs @@ -1,5 +1,5 @@ -using DBWeaver.UI.Services.Theming; -namespace DBWeaver.UI.Services.Explain; +using AkkornStudio.UI.Services.Theming; +namespace AkkornStudio.UI.Services.Explain; public sealed class ExplainComparisonRow { diff --git a/src/DBWeaver.UI/Services/Explain/Models/ExplainHistoryItem.cs b/src/AkkornStudio.UI/Services/Explain/Models/ExplainHistoryItem.cs similarity index 95% rename from src/DBWeaver.UI/Services/Explain/Models/ExplainHistoryItem.cs rename to src/AkkornStudio.UI/Services/Explain/Models/ExplainHistoryItem.cs index 9a9dc056..05fbd1bc 100644 --- a/src/DBWeaver.UI/Services/Explain/Models/ExplainHistoryItem.cs +++ b/src/AkkornStudio.UI/Services/Explain/Models/ExplainHistoryItem.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class ExplainHistoryItem { diff --git a/src/DBWeaver.UI/Services/Explain/Models/ExplainHistoryState.cs b/src/AkkornStudio.UI/Services/Explain/Models/ExplainHistoryState.cs similarity index 76% rename from src/DBWeaver.UI/Services/Explain/Models/ExplainHistoryState.cs rename to src/AkkornStudio.UI/Services/Explain/Models/ExplainHistoryState.cs index 42af0685..929f912c 100644 --- a/src/DBWeaver.UI/Services/Explain/Models/ExplainHistoryState.cs +++ b/src/AkkornStudio.UI/Services/Explain/Models/ExplainHistoryState.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed record ExplainHistoryState( DateTimeOffset TimestampUtc, diff --git a/src/DBWeaver.UI/Services/Explain/Models/ExplainSimulationLatencyOptions.cs b/src/AkkornStudio.UI/Services/Explain/Models/ExplainSimulationLatencyOptions.cs similarity index 88% rename from src/DBWeaver.UI/Services/Explain/Models/ExplainSimulationLatencyOptions.cs rename to src/AkkornStudio.UI/Services/Explain/Models/ExplainSimulationLatencyOptions.cs index fa7cfbbc..d6db10cc 100644 --- a/src/DBWeaver.UI/Services/Explain/Models/ExplainSimulationLatencyOptions.cs +++ b/src/AkkornStudio.UI/Services/Explain/Models/ExplainSimulationLatencyOptions.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainSimulationLatencyOptions { diff --git a/src/DBWeaver.UI/Services/Explain/Models/ExplainSnapshot.cs b/src/AkkornStudio.UI/Services/Explain/Models/ExplainSnapshot.cs similarity index 60% rename from src/DBWeaver.UI/Services/Explain/Models/ExplainSnapshot.cs rename to src/AkkornStudio.UI/Services/Explain/Models/ExplainSnapshot.cs index 1bca542a..3a934bce 100644 --- a/src/DBWeaver.UI/Services/Explain/Models/ExplainSnapshot.cs +++ b/src/AkkornStudio.UI/Services/Explain/Models/ExplainSnapshot.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed record ExplainSnapshot( string Label, diff --git a/src/DBWeaver.UI/Services/Explain/Models/ExplainStep.cs b/src/AkkornStudio.UI/Services/Explain/Models/ExplainStep.cs similarity index 96% rename from src/DBWeaver.UI/Services/Explain/Models/ExplainStep.cs rename to src/AkkornStudio.UI/Services/Explain/Models/ExplainStep.cs index 49f6bd66..6fab1031 100644 --- a/src/DBWeaver.UI/Services/Explain/Models/ExplainStep.cs +++ b/src/AkkornStudio.UI/Services/Explain/Models/ExplainStep.cs @@ -1,8 +1,8 @@ using System.Globalization; -using DBWeaver.UI.ViewModels.Canvas; -using DBWeaver.UI.Services.Theming; +using AkkornStudio.UI.ViewModels.Canvas; +using AkkornStudio.UI.Services.Theming; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class ExplainStep { diff --git a/src/DBWeaver.UI/Services/Explain/Models/MySqlParsedPlan.cs b/src/AkkornStudio.UI/Services/Explain/Models/MySqlParsedPlan.cs similarity index 62% rename from src/DBWeaver.UI/Services/Explain/Models/MySqlParsedPlan.cs rename to src/AkkornStudio.UI/Services/Explain/Models/MySqlParsedPlan.cs index 44ad5e05..4095b3ac 100644 --- a/src/DBWeaver.UI/Services/Explain/Models/MySqlParsedPlan.cs +++ b/src/AkkornStudio.UI/Services/Explain/Models/MySqlParsedPlan.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed record MySqlParsedPlan( IReadOnlyList Nodes, diff --git a/src/DBWeaver.UI/Services/Explain/Models/SqlServerParsedPlan.cs b/src/AkkornStudio.UI/Services/Explain/Models/SqlServerParsedPlan.cs similarity index 62% rename from src/DBWeaver.UI/Services/Explain/Models/SqlServerParsedPlan.cs rename to src/AkkornStudio.UI/Services/Explain/Models/SqlServerParsedPlan.cs index 00e3b7ac..33a30215 100644 --- a/src/DBWeaver.UI/Services/Explain/Models/SqlServerParsedPlan.cs +++ b/src/AkkornStudio.UI/Services/Explain/Models/SqlServerParsedPlan.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed record SqlServerParsedPlan( IReadOnlyList Nodes, diff --git a/src/DBWeaver.UI/Services/Explain/Parsers/MySqlExplainPlanParser.cs b/src/AkkornStudio.UI/Services/Explain/Parsers/MySqlExplainPlanParser.cs similarity index 99% rename from src/DBWeaver.UI/Services/Explain/Parsers/MySqlExplainPlanParser.cs rename to src/AkkornStudio.UI/Services/Explain/Parsers/MySqlExplainPlanParser.cs index 98e0f098..f0378bfe 100644 --- a/src/DBWeaver.UI/Services/Explain/Parsers/MySqlExplainPlanParser.cs +++ b/src/AkkornStudio.UI/Services/Explain/Parsers/MySqlExplainPlanParser.cs @@ -1,9 +1,9 @@ using System.Globalization; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; using System.Text.Json; using System.Text.RegularExpressions; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed partial class MySqlExplainPlanParser : IMySqlExplainPlanParser { diff --git a/src/DBWeaver.UI/Services/Explain/Parsers/PostgresExplainPlanParser.cs b/src/AkkornStudio.UI/Services/Explain/Parsers/PostgresExplainPlanParser.cs similarity index 98% rename from src/DBWeaver.UI/Services/Explain/Parsers/PostgresExplainPlanParser.cs rename to src/AkkornStudio.UI/Services/Explain/Parsers/PostgresExplainPlanParser.cs index b67254f3..aeeefc2d 100644 --- a/src/DBWeaver.UI/Services/Explain/Parsers/PostgresExplainPlanParser.cs +++ b/src/AkkornStudio.UI/Services/Explain/Parsers/PostgresExplainPlanParser.cs @@ -1,7 +1,7 @@ using System.Text.Json; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed record PostgresParsedPlan( IReadOnlyList Nodes, diff --git a/src/DBWeaver.UI/Services/Explain/Parsers/SqlServerExplainPlanParser.cs b/src/AkkornStudio.UI/Services/Explain/Parsers/SqlServerExplainPlanParser.cs similarity index 98% rename from src/DBWeaver.UI/Services/Explain/Parsers/SqlServerExplainPlanParser.cs rename to src/AkkornStudio.UI/Services/Explain/Parsers/SqlServerExplainPlanParser.cs index 437db411..85b917d0 100644 --- a/src/DBWeaver.UI/Services/Explain/Parsers/SqlServerExplainPlanParser.cs +++ b/src/AkkornStudio.UI/Services/Explain/Parsers/SqlServerExplainPlanParser.cs @@ -1,8 +1,8 @@ using System.Globalization; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; using System.Xml.Linq; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed class SqlServerExplainPlanParser : ISqlServerExplainPlanParser { diff --git a/src/DBWeaver.UI/Services/Explain/Presentation/ExplainDaliboUrlBuilder.cs b/src/AkkornStudio.UI/Services/Explain/Presentation/ExplainDaliboUrlBuilder.cs similarity index 92% rename from src/DBWeaver.UI/Services/Explain/Presentation/ExplainDaliboUrlBuilder.cs rename to src/AkkornStudio.UI/Services/Explain/Presentation/ExplainDaliboUrlBuilder.cs index d4c04417..bb1661ac 100644 --- a/src/DBWeaver.UI/Services/Explain/Presentation/ExplainDaliboUrlBuilder.cs +++ b/src/AkkornStudio.UI/Services/Explain/Presentation/ExplainDaliboUrlBuilder.cs @@ -1,7 +1,7 @@ using System.Text; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public interface IExplainDaliboUrlBuilder { diff --git a/src/DBWeaver.UI/Services/Explain/Presentation/ExplainPlanExportFormatter.cs b/src/AkkornStudio.UI/Services/Explain/Presentation/ExplainPlanExportFormatter.cs similarity index 96% rename from src/DBWeaver.UI/Services/Explain/Presentation/ExplainPlanExportFormatter.cs rename to src/AkkornStudio.UI/Services/Explain/Presentation/ExplainPlanExportFormatter.cs index 8930cebf..a5298043 100644 --- a/src/DBWeaver.UI/Services/Explain/Presentation/ExplainPlanExportFormatter.cs +++ b/src/AkkornStudio.UI/Services/Explain/Presentation/ExplainPlanExportFormatter.cs @@ -1,8 +1,8 @@ using System.Globalization; -using DBWeaver.UI.ViewModels.Canvas; +using AkkornStudio.UI.ViewModels.Canvas; using System.Text; -namespace DBWeaver.UI.Services.Explain; +namespace AkkornStudio.UI.Services.Explain; public sealed record ExplainPlanExportData( string ProviderLabel, diff --git a/src/DBWeaver.UI/Services/Export/ExportNodeHandler.cs b/src/AkkornStudio.UI/Services/Export/ExportNodeHandler.cs similarity index 95% rename from src/DBWeaver.UI/Services/Export/ExportNodeHandler.cs rename to src/AkkornStudio.UI/Services/Export/ExportNodeHandler.cs index dcaa7a20..847acc73 100644 --- a/src/DBWeaver.UI/Services/Export/ExportNodeHandler.cs +++ b/src/AkkornStudio.UI/Services/Export/ExportNodeHandler.cs @@ -1,287 +1,287 @@ using System.Text; using System.Text.Json; using ClosedXML.Excel; -using DBWeaver.Nodes; -using DBWeaver.UI.Services.Theming; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services.Export; - -/// -/// Handles the file generation triggered by HtmlExport, JsonExport, CsvExport, and ExcelExport nodes. -/// -/// Since the canvas generates SQL (not live query results), the exported files contain: -/// • HTML — a self-contained HTML page embedding the SQL and a schema table of column aliases -/// • JSON — a JSON template array: [{"col1": null, "col2": null, ...}] -/// • CSV — a header row built from the column aliases defined in the connected ResultOutput -/// -public static class ExportNodeHandler -{ - // ── Public entry points ─────────────────────────────────────────────────── - - /// - /// Generates an export file for the given export node. - /// Returns the absolute path of the written file, or null on error. - /// - public static async Task RunExportAsync( - CanvasViewModel canvas, - NodeViewModel exportNode, - string? overridePath = null - ) - { - string filePath = - overridePath - ?? (exportNode.Parameters.TryGetValue("file_name", out string? fn) ? fn : null) - ?? DefaultFileName(exportNode.Type); - - // Resolve columns from the ResultOutput connected upstream - IReadOnlyList columns = ResolveColumns(canvas, exportNode); - string sql = canvas.QueryText ?? string.Empty; - - try - { - string? dir = Path.GetDirectoryName(filePath); - if (!string.IsNullOrEmpty(dir)) - Directory.CreateDirectory(dir); - - if (exportNode.Type == NodeType.ExcelExport) - { - byte[] bytes = BuildXlsx(columns, exportNode); - await File.WriteAllBytesAsync(filePath, bytes); - } - else - { - string content = exportNode.Type switch - { - NodeType.HtmlExport => BuildHtml(columns, sql, exportNode), - NodeType.JsonExport => BuildJson(columns), - NodeType.CsvExport => BuildCsv(columns, exportNode), - _ => throw new NotSupportedException(), - }; - await File.WriteAllTextAsync(filePath, content, Encoding.UTF8); - } - - return Path.GetFullPath(filePath); - } - catch - { - return null; - } - } - - // ── Column resolution ───────────────────────────────────────────────────── - - /// - /// Walks canvas connections to find the ResultOutput upstream of the export node, - /// then returns the ordered list of column display names (aliases). - /// Falls back to an empty list if no ResultOutput is connected. - /// - private static IReadOnlyList ResolveColumns( - CanvasViewModel canvas, - NodeViewModel exportNode - ) - { - // Find the ResultOutput connected to this export node's "query" input pin - NodeViewModel? resultOutputNode = canvas - .Connections.Where(c => - c.ToPin?.Owner == exportNode - && c.ToPin?.Name == "query" - && c.FromPin.Owner.Type == NodeType.ResultOutput - ) - .Select(c => c.FromPin.Owner) - .FirstOrDefault(); - - // Fallback: use any ResultOutput on the canvas - resultOutputNode ??= canvas.Nodes.FirstOrDefault(n => n.Type == NodeType.ResultOutput); - - if (resultOutputNode is null) - return []; - - // Use the ordered column list if populated - if (resultOutputNode.OutputColumnOrder.Count > 0) - return resultOutputNode.OutputColumnOrder.Select(e => e.DisplayName).ToList(); - - // Fallback: read aliases from nodes connected to ResultOutput's "columns" input - return canvas - .Connections.Where(c => - c.ToPin?.Owner == resultOutputNode && c.ToPin?.Name == "columns" - ) - .Select(c => - string.IsNullOrWhiteSpace(c.FromPin.Owner.Alias) - ? c.FromPin.Name - : c.FromPin.Owner.Alias - ) - .ToList(); - } - - private static string DefaultFileName(NodeType type) => - type switch - { - NodeType.HtmlExport => "export.html", - NodeType.JsonExport => "export.json", - NodeType.CsvExport => "export.csv", - NodeType.ExcelExport => "export.xlsx", - _ => "export.txt", - }; - - // ── HTML builder ────────────────────────────────────────────────────────── - - private static string BuildHtml(IReadOnlyList columns, string sql, NodeViewModel node) - { - var sb = new StringBuilder(); - string title = node.Parameters.TryGetValue("file_name", out string? fn) - ? Path.GetFileNameWithoutExtension(fn) - : "SQL Export"; - string colsHtml = - columns.Count > 0 - ? string.Join("", columns.Select(c => $"{HtmlEncode(c)}")) - : "No columns defined"; - string sqlHtml = HtmlEncode(sql.Trim()); - string ts = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - - sb.Append( - $$""" - - - - - -{{title}} - - - -

{{HtmlEncode(title)}}

-

Generated by DBWeaver  ·  {{ts}}  ·  {{columns.Count}} column(s)

- -
-

Generated SQL

-
{{(
-                string.IsNullOrWhiteSpace(sqlHtml)
-                    ? "-- No SQL generated yet. Connect nodes to a Result Output node."
-                    : sqlHtml
-            )}}
-
- -
-

Output Schema

- - {{colsHtml}} - {{( - columns.Count > 0 - ? string.Join("", columns.Select(_ => "")) - : "" - )}} -
Run the query to see data
-
- - -""" - ); - return sb.ToString(); - } - - private static string HtmlEncode(string s) => - s.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); - - // ── JSON builder ────────────────────────────────────────────────────────── - - private static string BuildJson(IReadOnlyList columns) - { - if (columns.Count == 0) - { - return JsonSerializer.Serialize( - new[] - { - new Dictionary - { - ["_note"] = "No columns defined — connect a Result Output node", - }, - }, - new JsonSerializerOptions { WriteIndented = true } - ); - } - - var template = columns.ToDictionary(c => c, _ => (object?)null); - Dictionary[] result = new[] { template }; - return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); - } - - // ── CSV builder ─────────────────────────────────────────────────────────── - - private static string BuildCsv(IReadOnlyList columns, NodeViewModel node) - { - string rawDelim = node.Parameters.TryGetValue("delimiter", out string? d) ? d : ","; - string delimiter = rawDelim == "\\t" ? "\t" : rawDelim; - - if (columns.Count == 0) - return $"# No columns defined — connect a Result Output node{Environment.NewLine}"; - - string header = string.Join(delimiter, columns.Select(c => CsvEscape(c, delimiter))); - return header + Environment.NewLine; - } - - private static string CsvEscape(string value, string delimiter) - { - bool needsQuoting = - value.Contains('"') || value.Contains('\n') || value.Contains(delimiter); - if (!needsQuoting) - return value; - return "\"" + value.Replace("\"", "\"\"") + "\""; - } - - // ── Excel (XLSX) builder ────────────────────────────────────────────────── - - /// - /// Builds an XLSX workbook with a bold header row containing the column names. - /// If no columns are defined a single descriptive cell is written instead. - /// Uses ClosedXML — no Excel installation required. - /// - private static byte[] BuildXlsx(IReadOnlyList columns, NodeViewModel node) - { - string sheetName = - node.Parameters.TryGetValue("sheet_name", out string? sn) - && !string.IsNullOrWhiteSpace(sn) - ? sn - : "Sheet1"; - - using var workbook = new XLWorkbook(); - IXLWorksheet worksheet = workbook.Worksheets.Add(sheetName); - - if (columns.Count == 0) - { - IXLCell cell = worksheet.Cell(1, 1); - cell.Value = "No columns defined — connect a Result Output node"; - cell.Style.Font.Italic = true; - } - else - { - for (int i = 0; i < columns.Count; i++) - { - IXLCell cell = worksheet.Cell(1, i + 1); - cell.Value = columns[i]; - cell.Style.Font.Bold = true; + + + +

{{HtmlEncode(title)}}

+

Generated by AkkornStudio  ·  {{ts}}  ·  {{columns.Count}} column(s)

+ +
+

Generated SQL

+
{{(
+                string.IsNullOrWhiteSpace(sqlHtml)
+                    ? "-- No SQL generated yet. Connect nodes to a Result Output node."
+                    : sqlHtml
+            )}}
+
+ +
+

Output Schema

+ + {{colsHtml}} + {{( + columns.Count > 0 + ? string.Join("", columns.Select(_ => "")) + : "" + )}} +
Run the query to see data
+
+ + +""" + ); + return sb.ToString(); + } + + private static string HtmlEncode(string s) => + s.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); + + // ── JSON builder ────────────────────────────────────────────────────────── + + private static string BuildJson(IReadOnlyList columns) + { + if (columns.Count == 0) + { + return JsonSerializer.Serialize( + new[] + { + new Dictionary + { + ["_note"] = "No columns defined — connect a Result Output node", + }, + }, + new JsonSerializerOptions { WriteIndented = true } + ); + } + + var template = columns.ToDictionary(c => c, _ => (object?)null); + Dictionary[] result = new[] { template }; + return JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }); + } + + // ── CSV builder ─────────────────────────────────────────────────────────── + + private static string BuildCsv(IReadOnlyList columns, NodeViewModel node) + { + string rawDelim = node.Parameters.TryGetValue("delimiter", out string? d) ? d : ","; + string delimiter = rawDelim == "\\t" ? "\t" : rawDelim; + + if (columns.Count == 0) + return $"# No columns defined — connect a Result Output node{Environment.NewLine}"; + + string header = string.Join(delimiter, columns.Select(c => CsvEscape(c, delimiter))); + return header + Environment.NewLine; + } + + private static string CsvEscape(string value, string delimiter) + { + bool needsQuoting = + value.Contains('"') || value.Contains('\n') || value.Contains(delimiter); + if (!needsQuoting) + return value; + return "\"" + value.Replace("\"", "\"\"") + "\""; + } + + // ── Excel (XLSX) builder ────────────────────────────────────────────────── + + /// + /// Builds an XLSX workbook with a bold header row containing the column names. + /// If no columns are defined a single descriptive cell is written instead. + /// Uses ClosedXML — no Excel installation required. + /// + private static byte[] BuildXlsx(IReadOnlyList columns, NodeViewModel node) + { + string sheetName = + node.Parameters.TryGetValue("sheet_name", out string? sn) + && !string.IsNullOrWhiteSpace(sn) + ? sn + : "Sheet1"; + + using var workbook = new XLWorkbook(); + IXLWorksheet worksheet = workbook.Worksheets.Add(sheetName); + + if (columns.Count == 0) + { + IXLCell cell = worksheet.Cell(1, 1); + cell.Value = "No columns defined — connect a Result Output node"; + cell.Style.Font.Italic = true; + } + else + { + for (int i = 0; i < columns.Count; i++) + { + IXLCell cell = worksheet.Cell(1, i + 1); + cell.Value = columns[i]; + cell.Style.Font.Bold = true; cell.Style.Fill.BackgroundColor = XLColor.FromHtml(UiColorConstants.C_1D4ED8); - cell.Style.Font.FontColor = XLColor.White; - } - - // Auto-fit column widths to header text - worksheet.Columns().AdjustToContents(); - } - - using var ms = new MemoryStream(); - workbook.SaveAs(ms); - return ms.ToArray(); - } -} + cell.Style.Font.FontColor = XLColor.White; + } + + // Auto-fit column widths to header text + worksheet.Columns().AdjustToContents(); + } + + using var ms = new MemoryStream(); + workbook.SaveAs(ms); + return ms.ToArray(); + } +} diff --git a/src/DBWeaver.UI/Services/Export/ExportService.cs b/src/AkkornStudio.UI/Services/Export/ExportService.cs similarity index 92% rename from src/DBWeaver.UI/Services/Export/ExportService.cs rename to src/AkkornStudio.UI/Services/Export/ExportService.cs index b1ab40ab..f6f3c20d 100644 --- a/src/DBWeaver.UI/Services/Export/ExportService.cs +++ b/src/AkkornStudio.UI/Services/Export/ExportService.cs @@ -1,105 +1,105 @@ -using Avalonia.Controls; -using Avalonia.Platform.Storage; -using DBWeaver.Nodes; -using DBWeaver.UI.Services.Export; -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.Serialization; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services; - -/// -/// Handles export operations for multiple formats. -/// Supports HTML, JSON, CSV, Excel, and Markdown documentation exports. -/// -public class ExportService(Window window, CanvasViewModel vm) -{ - private readonly Window _window = window; - private readonly CanvasViewModel _vm = vm; - - public async Task RunExportDocumentationAsync() - { - string suggestedName = _vm.CurrentFilePath is not null - ? Path.GetFileNameWithoutExtension(_vm.CurrentFilePath) - : "flow-documentation"; - - var mdType = new FilePickerFileType("Markdown Files") { Patterns = ["*.md"] }; - IStorageFile? result = await _window.StorageProvider.SaveFilePickerAsync( - new FilePickerSaveOptions - { - Title = L("export.documentation.dialogTitle", "Export Flow Documentation"), - DefaultExtension = "md", - FileTypeChoices = [mdType], - SuggestedFileName = suggestedName, - } - ); - - string? path = result?.TryGetLocalPath(); - if (path is null) - return; - - string? written = await FlowDocumentExporter.WriteAsync(_vm, path); - if (written is not null) - _vm.NotifySuccess(L("export.documentation.success", "Documentation exported successfully."), written); - else - _vm.NotifyError( - L("export.documentation.failed", "Documentation export failed."), - L("export.failed.pathPermissionsHint", "Check file path and permissions.") - ); - } - - public async Task RunExportWithDialogAsync( - NodeType exportType, - string fileTypeName, - string extension - ) - { - NodeViewModel? exportNode = _vm.Nodes.FirstOrDefault(n => n.Type == exportType); - if (exportNode is null) - { - string msg = - string.Format( - L( - "export.nodeNotFound", - "No {0} Export node found on the canvas. Add one via the node search menu." - ), - fileTypeName.Replace(" Files", string.Empty) - ); - _vm.NotifyError(msg); - return; - } - - string suggestedName = exportNode.Parameters.TryGetValue("file_name", out string? fn) - ? fn - : $"export.{extension}"; - var fileType = new FilePickerFileType(fileTypeName) { Patterns = [$"*.{extension}"] }; - IStorageFile? result = await _window.StorageProvider.SaveFilePickerAsync( - new FilePickerSaveOptions - { - Title = string.Format(L("export.dialogTitleByExtension", "Export as {0}"), extension.ToUpperInvariant()), - DefaultExtension = extension, - FileTypeChoices = [fileType], - SuggestedFileName = Path.GetFileNameWithoutExtension(suggestedName), - } - ); - - string? path = result?.TryGetLocalPath(); - if (path is null) - return; - - string? written = await _vm.TriggerExportAsync(exportType, path); - if (written is not null) - _vm.NotifySuccess(L("export.success", "Export completed successfully."), written); - else - _vm.NotifyError( - L("export.failed", "Export failed."), - L("export.failed.pathPermissionsHint", "Check file path and permissions.") - ); - } - - private static string L(string key, string fallback) - { - string value = LocalizationService.Instance[key]; - return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; - } -} +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using AkkornStudio.Nodes; +using AkkornStudio.UI.Services.Export; +using AkkornStudio.UI.Services.Localization; +using AkkornStudio.UI.Serialization; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services; + +/// +/// Handles export operations for multiple formats. +/// Supports HTML, JSON, CSV, Excel, and Markdown documentation exports. +/// +public class ExportService(Window window, CanvasViewModel vm) +{ + private readonly Window _window = window; + private readonly CanvasViewModel _vm = vm; + + public async Task RunExportDocumentationAsync() + { + string suggestedName = _vm.CurrentFilePath is not null + ? Path.GetFileNameWithoutExtension(_vm.CurrentFilePath) + : "flow-documentation"; + + var mdType = new FilePickerFileType("Markdown Files") { Patterns = ["*.md"] }; + IStorageFile? result = await _window.StorageProvider.SaveFilePickerAsync( + new FilePickerSaveOptions + { + Title = L("export.documentation.dialogTitle", "Export Flow Documentation"), + DefaultExtension = "md", + FileTypeChoices = [mdType], + SuggestedFileName = suggestedName, + } + ); + + string? path = result?.TryGetLocalPath(); + if (path is null) + return; + + string? written = await FlowDocumentExporter.WriteAsync(_vm, path); + if (written is not null) + _vm.NotifySuccess(L("export.documentation.success", "Documentation exported successfully."), written); + else + _vm.NotifyError( + L("export.documentation.failed", "Documentation export failed."), + L("export.failed.pathPermissionsHint", "Check file path and permissions.") + ); + } + + public async Task RunExportWithDialogAsync( + NodeType exportType, + string fileTypeName, + string extension + ) + { + NodeViewModel? exportNode = _vm.Nodes.FirstOrDefault(n => n.Type == exportType); + if (exportNode is null) + { + string msg = + string.Format( + L( + "export.nodeNotFound", + "No {0} Export node found on the canvas. Add one via the node search menu." + ), + fileTypeName.Replace(" Files", string.Empty) + ); + _vm.NotifyError(msg); + return; + } + + string suggestedName = exportNode.Parameters.TryGetValue("file_name", out string? fn) + ? fn + : $"export.{extension}"; + var fileType = new FilePickerFileType(fileTypeName) { Patterns = [$"*.{extension}"] }; + IStorageFile? result = await _window.StorageProvider.SaveFilePickerAsync( + new FilePickerSaveOptions + { + Title = string.Format(L("export.dialogTitleByExtension", "Export as {0}"), extension.ToUpperInvariant()), + DefaultExtension = extension, + FileTypeChoices = [fileType], + SuggestedFileName = Path.GetFileNameWithoutExtension(suggestedName), + } + ); + + string? path = result?.TryGetLocalPath(); + if (path is null) + return; + + string? written = await _vm.TriggerExportAsync(exportType, path); + if (written is not null) + _vm.NotifySuccess(L("export.success", "Export completed successfully."), written); + else + _vm.NotifyError( + L("export.failed", "Export failed."), + L("export.failed.pathPermissionsHint", "Check file path and permissions.") + ); + } + + private static string L(string key, string fallback) + { + string value = LocalizationService.Instance[key]; + return string.Equals(value, key, StringComparison.Ordinal) ? fallback : value; + } +} diff --git a/src/DBWeaver.UI/Services/Export/FlowDocumentExporter.cs b/src/AkkornStudio.UI/Services/Export/FlowDocumentExporter.cs similarity index 95% rename from src/DBWeaver.UI/Services/Export/FlowDocumentExporter.cs rename to src/AkkornStudio.UI/Services/Export/FlowDocumentExporter.cs index eee3a9b7..3172cd38 100644 --- a/src/DBWeaver.UI/Services/Export/FlowDocumentExporter.cs +++ b/src/AkkornStudio.UI/Services/Export/FlowDocumentExporter.cs @@ -1,213 +1,213 @@ -using System.Text; -using DBWeaver.Nodes; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services.Export; - -/// -/// Exports a human-readable Markdown document describing the current canvas flow. -/// -/// The document includes: -/// • Metadata — date, provider hint, canvas file path, node/connection counts -/// • SQL — the last generated SQL, fenced as a sql code block -/// • Node inventory — table grouped by category, with alias and parameters -/// • Connection map — directed list of wires (From → To) -/// • Export nodes — file names and delimiters configured on export nodes -/// -public static class FlowDocumentExporter -{ - private const string AppVersion = "1.0"; - - // ── Public entry point ──────────────────────────────────────────────────── - - /// - /// Generates the Markdown document as a string. - /// - public static string Build(CanvasViewModel canvas) - { - var sb = new StringBuilder(); - - string title = canvas.CurrentFilePath is not null - ? Path.GetFileNameWithoutExtension(canvas.CurrentFilePath) - : "Untitled Flow"; - string ts = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - int nodeCount = canvas.Nodes.Count; - int connCount = canvas.Connections.Count; - - // ── Front-matter ────────────────────────────────────────────────────── - sb.AppendLine($"# {title}"); - sb.AppendLine(); - sb.AppendLine("| Field | Value |"); - sb.AppendLine("|---|---|"); - sb.AppendLine($"| Generated at | `{ts}` |"); - sb.AppendLine($"| Tool version | DBWeaver {AppVersion} |"); - sb.AppendLine($"| Canvas file | `{canvas.CurrentFilePath ?? "(unsaved)"}` |"); - sb.AppendLine($"| Nodes | {nodeCount} |"); - sb.AppendLine($"| Connections | {connCount} |"); - sb.AppendLine($"| Validation errors | {canvas.ErrorCount} |"); - sb.AppendLine($"| Validation warnings | {canvas.WarningCount} |"); - sb.AppendLine(); - - // ── Generated SQL ───────────────────────────────────────────────────── - sb.AppendLine("## Generated SQL"); - sb.AppendLine(); - string sql = canvas.QueryText?.Trim() ?? ""; - if (string.IsNullOrEmpty(sql)) - sb.AppendLine("> _No SQL generated yet — connect nodes to a Result Output._"); - else - { - sb.AppendLine("```sql"); - sb.AppendLine(sql); - sb.AppendLine("```"); - } - sb.AppendLine(); - - // ── Node inventory ──────────────────────────────────────────────────── - sb.AppendLine("## Node Inventory"); - sb.AppendLine(); - - IOrderedEnumerable> byCategory = canvas - .Nodes.GroupBy(n => n.Category) - .OrderBy(g => g.Key.ToString()); - - foreach (IGrouping? group in byCategory) - { - sb.AppendLine($"### {group.Key}"); - sb.AppendLine(); - sb.AppendLine("| # | Type | Title | Alias | Parameters |"); - sb.AppendLine("|---|---|---|---|---|"); - - int idx = 1; - foreach (NodeViewModel? node in group) - { - string alias = string.IsNullOrWhiteSpace(node.Alias) ? "—" : $"`{node.Alias}`"; - string paramsText = - node.Parameters.Count > 0 - ? string.Join(", ", node.Parameters.Select(kv => $"`{kv.Key}={kv.Value}`")) - : "—"; - sb.AppendLine( - $"| {idx++} | {node.Type} | {EscapeMd(node.Title)} | {alias} | {paramsText} |" - ); - } - sb.AppendLine(); - } - - // ── Connection map ──────────────────────────────────────────────────── - sb.AppendLine("## Connection Map"); - sb.AppendLine(); - - if (canvas.Connections.Count == 0) - { - sb.AppendLine("> _No connections on canvas._"); - } - else - { - sb.AppendLine("| From Node | Pin | To Node | Pin |"); - sb.AppendLine("|---|---|---|---|"); - - foreach (ConnectionViewModel conn in canvas.Connections) - { - NodeViewModel fromNode = conn.FromPin.Owner; - NodeViewModel? toNode = conn.ToPin?.Owner; - - string fromLabel = NodeLabel(fromNode); - string toLabel = toNode is not null ? NodeLabel(toNode) : "_unconnected_"; - string toPin = conn.ToPin?.Name ?? "—"; - - sb.AppendLine($"| {fromLabel} | `{conn.FromPin.Name}` | {toLabel} | `{toPin}` |"); - } - } - sb.AppendLine(); - - // ── Export nodes config ─────────────────────────────────────────────── - var exportNodes = canvas - .Nodes.Where(n => - n.Type - is NodeType.HtmlExport - or NodeType.JsonExport - or NodeType.CsvExport - or NodeType.ExcelExport - ) - .ToList(); - - if (exportNodes.Count > 0) - { - sb.AppendLine("## Export Configuration"); - sb.AppendLine(); - sb.AppendLine("| Node | File Name | Extra |"); - sb.AppendLine("|---|---|---|"); - - foreach (NodeViewModel? n in exportNodes) - { - string fileName = n.Parameters.TryGetValue("file_name", out string? fn) - ? fn - : "(default)"; - string extra = - n.Type == NodeType.CsvExport - && n.Parameters.TryGetValue("delimiter", out string? d) - ? $"delimiter=`{d}`" - : "—"; - sb.AppendLine($"| {n.Type} | `{fileName}` | {extra} |"); - } - sb.AppendLine(); - } - - // ── Orphan / quality notes ──────────────────────────────────────────── - if (canvas.OrphanCount > 0 || canvas.HasNamingViolations) - { - sb.AppendLine("## Quality Notes"); - sb.AppendLine(); - if (canvas.OrphanCount > 0) - sb.AppendLine( - $"- ⚠ **{canvas.OrphanCount} orphan node(s)** not connected to any output." - ); - if (canvas.HasNamingViolations) - sb.AppendLine( - $"- ⚠ **Naming violations detected** — conformance: {canvas.NamingConformance}%. Run _Auto-Fix Naming_ to correct." - ); - sb.AppendLine(); - } - - // ── Footer ──────────────────────────────────────────────────────────── - sb.AppendLine("---"); - sb.AppendLine($"_Document generated by [DBWeaver]({AppVersion}) on {ts}_"); - - return sb.ToString(); - } - - // ── Helpers ─────────────────────────────────────────────────────────────── - - private static string NodeLabel(NodeViewModel n) - { - string? alias = string.IsNullOrWhiteSpace(n.Alias) ? null : n.Alias; - string label = alias ?? n.Title; - return EscapeMd(label); - } - - private static string EscapeMd(string s) => - s.Replace("|", "\\|").Replace("*", "\\*").Replace("_", "\\_"); - - // ── File writer ─────────────────────────────────────────────────────────── - - /// - /// Writes the Markdown document to the given file path. - /// Returns the absolute path on success, null on error. - /// - public static async Task WriteAsync(CanvasViewModel canvas, string outputPath) - { - try - { - string? dir = Path.GetDirectoryName(outputPath); - if (!string.IsNullOrEmpty(dir)) - Directory.CreateDirectory(dir); - - string content = Build(canvas); - await File.WriteAllTextAsync(outputPath, content, Encoding.UTF8); - return Path.GetFullPath(outputPath); - } - catch - { - return null; - } - } -} +using System.Text; +using AkkornStudio.Nodes; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services.Export; + +/// +/// Exports a human-readable Markdown document describing the current canvas flow. +/// +/// The document includes: +/// • Metadata — date, provider hint, canvas file path, node/connection counts +/// • SQL — the last generated SQL, fenced as a sql code block +/// • Node inventory — table grouped by category, with alias and parameters +/// • Connection map — directed list of wires (From → To) +/// • Export nodes — file names and delimiters configured on export nodes +/// +public static class FlowDocumentExporter +{ + private const string AppVersion = "1.0"; + + // ── Public entry point ──────────────────────────────────────────────────── + + /// + /// Generates the Markdown document as a string. + /// + public static string Build(CanvasViewModel canvas) + { + var sb = new StringBuilder(); + + string title = canvas.CurrentFilePath is not null + ? Path.GetFileNameWithoutExtension(canvas.CurrentFilePath) + : "Untitled Flow"; + string ts = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + int nodeCount = canvas.Nodes.Count; + int connCount = canvas.Connections.Count; + + // ── Front-matter ────────────────────────────────────────────────────── + sb.AppendLine($"# {title}"); + sb.AppendLine(); + sb.AppendLine("| Field | Value |"); + sb.AppendLine("|---|---|"); + sb.AppendLine($"| Generated at | `{ts}` |"); + sb.AppendLine($"| Tool version | AkkornStudio {AppVersion} |"); + sb.AppendLine($"| Canvas file | `{canvas.CurrentFilePath ?? "(unsaved)"}` |"); + sb.AppendLine($"| Nodes | {nodeCount} |"); + sb.AppendLine($"| Connections | {connCount} |"); + sb.AppendLine($"| Validation errors | {canvas.ErrorCount} |"); + sb.AppendLine($"| Validation warnings | {canvas.WarningCount} |"); + sb.AppendLine(); + + // ── Generated SQL ───────────────────────────────────────────────────── + sb.AppendLine("## Generated SQL"); + sb.AppendLine(); + string sql = canvas.QueryText?.Trim() ?? ""; + if (string.IsNullOrEmpty(sql)) + sb.AppendLine("> _No SQL generated yet — connect nodes to a Result Output._"); + else + { + sb.AppendLine("```sql"); + sb.AppendLine(sql); + sb.AppendLine("```"); + } + sb.AppendLine(); + + // ── Node inventory ──────────────────────────────────────────────────── + sb.AppendLine("## Node Inventory"); + sb.AppendLine(); + + IOrderedEnumerable> byCategory = canvas + .Nodes.GroupBy(n => n.Category) + .OrderBy(g => g.Key.ToString()); + + foreach (IGrouping? group in byCategory) + { + sb.AppendLine($"### {group.Key}"); + sb.AppendLine(); + sb.AppendLine("| # | Type | Title | Alias | Parameters |"); + sb.AppendLine("|---|---|---|---|---|"); + + int idx = 1; + foreach (NodeViewModel? node in group) + { + string alias = string.IsNullOrWhiteSpace(node.Alias) ? "—" : $"`{node.Alias}`"; + string paramsText = + node.Parameters.Count > 0 + ? string.Join(", ", node.Parameters.Select(kv => $"`{kv.Key}={kv.Value}`")) + : "—"; + sb.AppendLine( + $"| {idx++} | {node.Type} | {EscapeMd(node.Title)} | {alias} | {paramsText} |" + ); + } + sb.AppendLine(); + } + + // ── Connection map ──────────────────────────────────────────────────── + sb.AppendLine("## Connection Map"); + sb.AppendLine(); + + if (canvas.Connections.Count == 0) + { + sb.AppendLine("> _No connections on canvas._"); + } + else + { + sb.AppendLine("| From Node | Pin | To Node | Pin |"); + sb.AppendLine("|---|---|---|---|"); + + foreach (ConnectionViewModel conn in canvas.Connections) + { + NodeViewModel fromNode = conn.FromPin.Owner; + NodeViewModel? toNode = conn.ToPin?.Owner; + + string fromLabel = NodeLabel(fromNode); + string toLabel = toNode is not null ? NodeLabel(toNode) : "_unconnected_"; + string toPin = conn.ToPin?.Name ?? "—"; + + sb.AppendLine($"| {fromLabel} | `{conn.FromPin.Name}` | {toLabel} | `{toPin}` |"); + } + } + sb.AppendLine(); + + // ── Export nodes config ─────────────────────────────────────────────── + var exportNodes = canvas + .Nodes.Where(n => + n.Type + is NodeType.HtmlExport + or NodeType.JsonExport + or NodeType.CsvExport + or NodeType.ExcelExport + ) + .ToList(); + + if (exportNodes.Count > 0) + { + sb.AppendLine("## Export Configuration"); + sb.AppendLine(); + sb.AppendLine("| Node | File Name | Extra |"); + sb.AppendLine("|---|---|---|"); + + foreach (NodeViewModel? n in exportNodes) + { + string fileName = n.Parameters.TryGetValue("file_name", out string? fn) + ? fn + : "(default)"; + string extra = + n.Type == NodeType.CsvExport + && n.Parameters.TryGetValue("delimiter", out string? d) + ? $"delimiter=`{d}`" + : "—"; + sb.AppendLine($"| {n.Type} | `{fileName}` | {extra} |"); + } + sb.AppendLine(); + } + + // ── Orphan / quality notes ──────────────────────────────────────────── + if (canvas.OrphanCount > 0 || canvas.HasNamingViolations) + { + sb.AppendLine("## Quality Notes"); + sb.AppendLine(); + if (canvas.OrphanCount > 0) + sb.AppendLine( + $"- ⚠ **{canvas.OrphanCount} orphan node(s)** not connected to any output." + ); + if (canvas.HasNamingViolations) + sb.AppendLine( + $"- ⚠ **Naming violations detected** — conformance: {canvas.NamingConformance}%. Run _Auto-Fix Naming_ to correct." + ); + sb.AppendLine(); + } + + // ── Footer ──────────────────────────────────────────────────────────── + sb.AppendLine("---"); + sb.AppendLine($"_Document generated by [AkkornStudio]({AppVersion}) on {ts}_"); + + return sb.ToString(); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static string NodeLabel(NodeViewModel n) + { + string? alias = string.IsNullOrWhiteSpace(n.Alias) ? null : n.Alias; + string label = alias ?? n.Title; + return EscapeMd(label); + } + + private static string EscapeMd(string s) => + s.Replace("|", "\\|").Replace("*", "\\*").Replace("_", "\\_"); + + // ── File writer ─────────────────────────────────────────────────────────── + + /// + /// Writes the Markdown document to the given file path. + /// Returns the absolute path on success, null on error. + /// + public static async Task WriteAsync(CanvasViewModel canvas, string outputPath) + { + try + { + string? dir = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + + string content = Build(canvas); + await File.WriteAllTextAsync(outputPath, content, Encoding.UTF8); + return Path.GetFullPath(outputPath); + } + catch + { + return null; + } + } +} diff --git a/src/DBWeaver.UI/Services/Input/ActiveCanvasProvider.cs b/src/AkkornStudio.UI/Services/Input/ActiveCanvasProvider.cs similarity index 88% rename from src/DBWeaver.UI/Services/Input/ActiveCanvasProvider.cs rename to src/AkkornStudio.UI/Services/Input/ActiveCanvasProvider.cs index fa35eb33..505e5203 100644 --- a/src/DBWeaver.UI/Services/Input/ActiveCanvasProvider.cs +++ b/src/AkkornStudio.UI/Services/Input/ActiveCanvasProvider.cs @@ -1,21 +1,21 @@ -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services; - -/// -/// Singleton-safe provider that delegates canvas resolution to a caller-supplied func. -/// A single instance can be shared across multiple consumers and will always -/// return whichever canvas is currently active without capturing a stale reference. -/// -public sealed class ActiveCanvasProvider : IActiveCanvasProvider -{ - private readonly Func _resolve; - - public ActiveCanvasProvider(Func resolve) - { - ArgumentNullException.ThrowIfNull(resolve); - _resolve = resolve; - } - - public CanvasViewModel GetActive() => _resolve(); -} +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services; + +/// +/// Singleton-safe provider that delegates canvas resolution to a caller-supplied func. +/// A single instance can be shared across multiple consumers and will always +/// return whichever canvas is currently active without capturing a stale reference. +/// +public sealed class ActiveCanvasProvider : IActiveCanvasProvider +{ + private readonly Func _resolve; + + public ActiveCanvasProvider(Func resolve) + { + ArgumentNullException.ThrowIfNull(resolve); + _resolve = resolve; + } + + public CanvasViewModel GetActive() => _resolve(); +} diff --git a/src/DBWeaver.UI/Services/Input/IActiveCanvasProvider.cs b/src/AkkornStudio.UI/Services/Input/IActiveCanvasProvider.cs similarity index 77% rename from src/DBWeaver.UI/Services/Input/IActiveCanvasProvider.cs rename to src/AkkornStudio.UI/Services/Input/IActiveCanvasProvider.cs index ba34ef6f..27771a47 100644 --- a/src/DBWeaver.UI/Services/Input/IActiveCanvasProvider.cs +++ b/src/AkkornStudio.UI/Services/Input/IActiveCanvasProvider.cs @@ -1,12 +1,12 @@ -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services; - -/// -/// Resolves the currently active canvas at the time of the call. -/// Allows keyboard shortcuts to route to whichever canvas (Query or DDL) is active. -/// -public interface IActiveCanvasProvider -{ - CanvasViewModel GetActive(); -} +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services; + +/// +/// Resolves the currently active canvas at the time of the call. +/// Allows keyboard shortcuts to route to whichever canvas (Query or DDL) is active. +/// +public interface IActiveCanvasProvider +{ + CanvasViewModel GetActive(); +} diff --git a/src/DBWeaver.UI/Services/Input/KeyboardInputHandler.cs b/src/AkkornStudio.UI/Services/Input/KeyboardInputHandler.cs similarity index 97% rename from src/DBWeaver.UI/Services/Input/KeyboardInputHandler.cs rename to src/AkkornStudio.UI/Services/Input/KeyboardInputHandler.cs index 8f4cfb62..99496a78 100644 --- a/src/DBWeaver.UI/Services/Input/KeyboardInputHandler.cs +++ b/src/AkkornStudio.UI/Services/Input/KeyboardInputHandler.cs @@ -1,30 +1,30 @@ -using Avalonia; +using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.VisualTree; using AvaloniaEdit; using AvaloniaEdit.Editing; -using DBWeaver.UI.Controls; -using DBWeaver.UI.Services.Input.ShortcutRegistry; -using DBWeaver.UI.ViewModels; - -namespace DBWeaver.UI.Services; - -/// -/// Centralizes keyboard input handling. -/// Routes 15+ keyboard shortcuts to appropriate commands and overlays. -/// Resolves the target canvas via so that -/// shortcuts work for both the Query and DDL canvases. -/// +using AkkornStudio.UI.Controls; +using AkkornStudio.UI.Services.Input.ShortcutRegistry; +using AkkornStudio.UI.ViewModels; + +namespace AkkornStudio.UI.Services; + +/// +/// Centralizes keyboard input handling. +/// Routes 15+ keyboard shortcuts to appropriate commands and overlays. +/// Resolves the target canvas via so that +/// shortcuts work for both the Query and DDL canvases. +/// public class KeyboardInputHandler { private static readonly IShortcutRegistry VolatileRegistry = - new global::DBWeaver.UI.Services.Input.ShortcutRegistry.ShortcutRegistry( + new global::AkkornStudio.UI.Services.Input.ShortcutRegistry.ShortcutRegistry( customizationStore: new NoOpShortcutCustomizationStore()); private readonly Window? _window; - private readonly IActiveCanvasProvider _canvasProvider; - private readonly FileOperationsService? _fileOps; + private readonly IActiveCanvasProvider _canvasProvider; + private readonly FileOperationsService? _fileOps; private readonly Action? _onCreateNewCanvas; private readonly Action? _showShortcutsAction; private readonly Action? _openSearchAction; @@ -32,14 +32,14 @@ public class KeyboardInputHandler private readonly CommandPaletteViewModel? _commandPalette; private readonly Func? _canHandleCanvasShortcuts; private readonly ShortcutExecutionService _shortcutExecutionService; - - private CanvasViewModel Vm => _canvasProvider.GetActive(); - - /// - /// Primary constructor used by the application shell. - /// Accepts an so shortcuts always route - /// to whichever canvas (Query or DDL) is active at the time of the key press. - /// + + private CanvasViewModel Vm => _canvasProvider.GetActive(); + + /// + /// Primary constructor used by the application shell. + /// Accepts an so shortcuts always route + /// to whichever canvas (Query or DDL) is active at the time of the key press. + /// public KeyboardInputHandler( Window window, IActiveCanvasProvider canvasProvider, @@ -63,11 +63,11 @@ public KeyboardInputHandler( _openConnectionManagerAction = openConnectionManagerAction; _shortcutExecutionService = new ShortcutExecutionService(shortcutRegistry ?? VolatileRegistry); } - - /// - /// Test-friendly constructor that pins the handler to a fixed canvas. - /// Internally wraps the canvas in a trivial . - /// + + /// + /// Test-friendly constructor that pins the handler to a fixed canvas. + /// Internally wraps the canvas in a trivial . + /// public KeyboardInputHandler( CanvasViewModel vm, CommandPaletteViewModel? commandPalette = null, @@ -90,16 +90,16 @@ public KeyboardInputHandler( _openConnectionManagerAction = openConnectionManagerAction; _shortcutExecutionService = new ShortcutExecutionService(shortcutRegistry ?? VolatileRegistry); } - - /// - /// Constructor that accepts an without a Window. - /// Used in tests and in headless scenarios where no window reference is available. - /// + + /// + /// Constructor that accepts an without a Window. + /// Used in tests and in headless scenarios where no window reference is available. + /// public KeyboardInputHandler(IActiveCanvasProvider canvasProvider, IShortcutRegistry? shortcutRegistry = null) { - _canvasProvider = canvasProvider; - _window = null; - _fileOps = null; + _canvasProvider = canvasProvider; + _window = null; + _fileOps = null; _commandPalette = null; _onCreateNewCanvas = null; _showShortcutsAction = null; @@ -108,22 +108,22 @@ public KeyboardInputHandler(IActiveCanvasProvider canvasProvider, IShortcutRegis _openConnectionManagerAction = null; _shortcutExecutionService = new ShortcutExecutionService(shortcutRegistry ?? VolatileRegistry); } - - public void Wire() - { - if (_window is not null) - _window.KeyDown += OnKeyDown; - } - - public void OnKeyDown(object? s, KeyEventArgs e) - { - if (e.Handled) - return; - - if (HandleShortcut(e.Key, e.KeyModifiers)) - e.Handled = true; - } - + + public void Wire() + { + if (_window is not null) + _window.KeyDown += OnKeyDown; + } + + public void OnKeyDown(object? s, KeyEventArgs e) + { + if (e.Handled) + return; + + if (HandleShortcut(e.Key, e.KeyModifiers)) + e.Handled = true; + } + public bool HandleShortcut(Key key, KeyModifiers modifiers) { if (!CanHandleCanvasShortcuts()) @@ -131,75 +131,75 @@ public bool HandleShortcut(Key key, KeyModifiers modifiers) bool isTextInputFocused = IsTextInputFocused(); bool isCanvasInteractionBlocked = Vm.ConnectionManager.IsVisible - || Vm.SqlImporter.IsVisible - || Vm.Benchmark.IsVisible - || Vm.ExplainPlan.IsVisible - || Vm.AutoJoin.IsVisible - || Vm.FlowVersions.IsVisible - || Vm.FileHistory.IsVisible - || Vm.IsInCteEditor - || _commandPalette?.IsVisible == true; - - // Handle overlay escape keys - if (key == Key.Escape) - { - if (_commandPalette?.IsVisible == true) - { - _commandPalette.Close(); - return true; - } - if (Vm.SearchMenu.IsVisible) - { - Vm.SearchMenu.Close(); - return true; - } - if (Vm.AutoJoin.IsVisible) - { - Vm.AutoJoin.Dismiss(); - return true; - } - if (Vm.DataPreview.IsVisible) - { - Vm.DataPreview.IsVisible = false; - return true; - } - if (Vm.ConnectionManager.IsVisible) - { - Vm.ConnectionManager.IsVisible = false; - return true; - } - if (Vm.Benchmark.IsVisible) - { - Vm.Benchmark.IsVisible = false; - return true; - } - if (Vm.ExplainPlan.IsVisible) - { - Vm.ExplainPlan.Close(); - return true; - } - if (Vm.SqlImporter.IsVisible) - { - Vm.SqlImporter.Close(); - return true; - } - if (Vm.FlowVersions.IsVisible) - { - Vm.FlowVersions.Close(); - return true; - } - if (Vm.FileHistory.IsVisible) - { - Vm.FileHistory.Close(); - return true; - } - if (Vm.IsInCteEditor) - { - Vm.ExitCteEditorCommand.Execute(null); - return true; - } - } - + || Vm.SqlImporter.IsVisible + || Vm.Benchmark.IsVisible + || Vm.ExplainPlan.IsVisible + || Vm.AutoJoin.IsVisible + || Vm.FlowVersions.IsVisible + || Vm.FileHistory.IsVisible + || Vm.IsInCteEditor + || _commandPalette?.IsVisible == true; + + // Handle overlay escape keys + if (key == Key.Escape) + { + if (_commandPalette?.IsVisible == true) + { + _commandPalette.Close(); + return true; + } + if (Vm.SearchMenu.IsVisible) + { + Vm.SearchMenu.Close(); + return true; + } + if (Vm.AutoJoin.IsVisible) + { + Vm.AutoJoin.Dismiss(); + return true; + } + if (Vm.DataPreview.IsVisible) + { + Vm.DataPreview.IsVisible = false; + return true; + } + if (Vm.ConnectionManager.IsVisible) + { + Vm.ConnectionManager.IsVisible = false; + return true; + } + if (Vm.Benchmark.IsVisible) + { + Vm.Benchmark.IsVisible = false; + return true; + } + if (Vm.ExplainPlan.IsVisible) + { + Vm.ExplainPlan.Close(); + return true; + } + if (Vm.SqlImporter.IsVisible) + { + Vm.SqlImporter.Close(); + return true; + } + if (Vm.FlowVersions.IsVisible) + { + Vm.FlowVersions.Close(); + return true; + } + if (Vm.FileHistory.IsVisible) + { + Vm.FileHistory.Close(); + return true; + } + if (Vm.IsInCteEditor) + { + Vm.ExitCteEditorCommand.Execute(null); + return true; + } + } + if (key == Key.Enter && modifiers.HasFlag(KeyModifiers.Control) && modifiers.HasFlag(KeyModifiers.Alt)) { if (Vm.IsInCteEditor) @@ -225,26 +225,26 @@ public bool HandleShortcut(Key key, KeyModifiers modifiers) if ( key == Key.A && modifiers.HasFlag(KeyModifiers.Shift) - && !Vm.SearchMenu.IsVisible - && !isTextInputFocused - && !isCanvasInteractionBlocked - ) - { - OpenSearch(); - return true; - } - if ( - key == Key.F - && modifiers.HasFlag(KeyModifiers.Control) - && !Vm.SearchMenu.IsVisible - && !isTextInputFocused - && !isCanvasInteractionBlocked - ) - { - OpenSearch(); - return true; - } - + && !Vm.SearchMenu.IsVisible + && !isTextInputFocused + && !isCanvasInteractionBlocked + ) + { + OpenSearch(); + return true; + } + if ( + key == Key.F + && modifiers.HasFlag(KeyModifiers.Control) + && !Vm.SearchMenu.IsVisible + && !isTextInputFocused + && !isCanvasInteractionBlocked + ) + { + OpenSearch(); + return true; + } + // Delete selected nodes if ((key == Key.Delete || key == Key.Back) && modifiers == KeyModifiers.None) { @@ -263,38 +263,38 @@ public bool HandleShortcut(Key key, KeyModifiers modifiers) Vm.DeleteSelected(); return true; } - - // Zoom - if ( - (key == Key.OemPlus || key == Key.Add) - && modifiers.HasFlag(KeyModifiers.Control) - ) - { - if (isTextInputFocused || isCanvasInteractionBlocked) - return false; - - Vm.ZoomInCommand.Execute(null); - return true; - } - if ( - (key == Key.OemMinus || key == Key.Subtract) - && modifiers.HasFlag(KeyModifiers.Control) - ) - { - if (isTextInputFocused || isCanvasInteractionBlocked) - return false; - - Vm.ZoomOutCommand.Execute(null); - return true; - } + + // Zoom + if ( + (key == Key.OemPlus || key == Key.Add) + && modifiers.HasFlag(KeyModifiers.Control) + ) + { + if (isTextInputFocused || isCanvasInteractionBlocked) + return false; + + Vm.ZoomInCommand.Execute(null); + return true; + } + if ( + (key == Key.OemMinus || key == Key.Subtract) + && modifiers.HasFlag(KeyModifiers.Control) + ) + { + if (isTextInputFocused || isCanvasInteractionBlocked) + return false; + + Vm.ZoomOutCommand.Execute(null); + return true; + } if ( (key == Key.D0 || key == Key.NumPad0) && modifiers.HasFlag(KeyModifiers.Control) ) - { - if (isTextInputFocused || isCanvasInteractionBlocked) - return false; - + { + if (isTextInputFocused || isCanvasInteractionBlocked) + return false; + Vm.ResetZoomCommand.Execute(null); return true; } @@ -561,22 +561,22 @@ private bool ExecuteRegisteredShortcut( private void OpenSearch() { - if (_openSearchAction is not null) - { - _openSearchAction(); - return; - } - - if (_window is null) - return; - - InfiniteCanvas? canvas = _window.FindControl("TheCanvas"); - Point ctr = canvas is not null - ? Vm.ScreenToCanvas(new Point(canvas.Bounds.Width / 2, canvas.Bounds.Height / 2)) - : new Point(400, 300); - Vm.SearchMenu.Open(ctr); - } - + if (_openSearchAction is not null) + { + _openSearchAction(); + return; + } + + if (_window is null) + return; + + InfiniteCanvas? canvas = _window.FindControl("TheCanvas"); + Point ctr = canvas is not null + ? Vm.ScreenToCanvas(new Point(canvas.Bounds.Width / 2, canvas.Bounds.Height / 2)) + : new Point(400, 300); + Vm.SearchMenu.Open(ctr); + } + private bool TryExecuteCommandPaletteShortcut(string actionId, string legacyShortcutText) { if (_commandPalette is null) @@ -590,20 +590,20 @@ private bool TryExecuteCommandPaletteShortcut(string actionId, string legacyShor if (command is null) return false; - - command.Execute(); - return true; - } - - private bool IsTextInputFocused() - { - if (_window is null) - return false; - - IInputElement? focused = _window.FocusManager?.GetFocusedElement(); - if (focused is null) - return false; - + + command.Execute(); + return true; + } + + private bool IsTextInputFocused() + { + if (_window is null) + return false; + + IInputElement? focused = _window.FocusManager?.GetFocusedElement(); + if (focused is null) + return false; + if (focused is TextBox || focused is ComboBox) return true; if (focused is TextEditor || focused is TextArea) diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/AppSettingsShortcutCustomizationStore.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/AppSettingsShortcutCustomizationStore.cs similarity index 92% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/AppSettingsShortcutCustomizationStore.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/AppSettingsShortcutCustomizationStore.cs index 06e6ea1c..d8f1223b 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/AppSettingsShortcutCustomizationStore.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/AppSettingsShortcutCustomizationStore.cs @@ -1,6 +1,6 @@ -using DBWeaver.UI.Services.Settings; +using AkkornStudio.UI.Services.Settings; -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Shortcut customization store backed by . diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/DefaultShortcutCatalog.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/DefaultShortcutCatalog.cs similarity index 99% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/DefaultShortcutCatalog.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/DefaultShortcutCatalog.cs index 515e2fe7..0cbf96da 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/DefaultShortcutCatalog.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/DefaultShortcutCatalog.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Canonical immutable catalog of default shortcuts. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutConflictDetector.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutConflictDetector.cs similarity index 81% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutConflictDetector.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutConflictDetector.cs index f70a595b..21411c88 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutConflictDetector.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutConflictDetector.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Detects gesture conflicts across effective shortcut definitions. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutCustomizationStore.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutCustomizationStore.cs similarity index 82% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutCustomizationStore.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutCustomizationStore.cs index 86cda4ef..99d03526 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutCustomizationStore.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutCustomizationStore.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Persists shortcut customization overrides. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutGestureParser.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutGestureParser.cs similarity index 75% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutGestureParser.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutGestureParser.cs index b47611ed..011dcc5b 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutGestureParser.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutGestureParser.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Parses and normalizes textual keyboard gestures. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutRegistry.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutRegistry.cs similarity index 91% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutRegistry.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutRegistry.cs index 271ace3d..f4730843 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/IShortcutRegistry.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/IShortcutRegistry.cs @@ -1,6 +1,6 @@ using Avalonia.Input; -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Central registry for effective keyboard shortcuts. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/NoOpShortcutCustomizationStore.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/NoOpShortcutCustomizationStore.cs similarity index 88% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/NoOpShortcutCustomizationStore.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/NoOpShortcutCustomizationStore.cs index abf1e98c..d6323595 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/NoOpShortcutCustomizationStore.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/NoOpShortcutCustomizationStore.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Volatile customization store used in ephemeral contexts such as tests. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutActionId.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutActionId.cs similarity index 88% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutActionId.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutActionId.cs index 4c590ef1..f9f2ce89 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutActionId.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutActionId.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Stable semantic identifier for a shortcut action. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutActionIds.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutActionIds.cs similarity index 97% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutActionIds.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutActionIds.cs index b7d4beaf..8d7ac850 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutActionIds.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutActionIds.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Stable action-id constants used by the default shortcut catalog. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutConflictDetector.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutConflictDetector.cs similarity index 96% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutConflictDetector.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutConflictDetector.cs index b5bf01be..5e30099c 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutConflictDetector.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutConflictDetector.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Default conflict detector considering normalized gesture and shortcut context. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutContext.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutContext.cs similarity index 74% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutContext.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutContext.cs index 47a4356d..6e4ba0a5 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutContext.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutContext.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Availability context for a shortcut. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationEntry.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationEntry.cs similarity index 75% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationEntry.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationEntry.cs index bc067172..cdf29ced 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationEntry.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationEntry.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Persisted customization entry for a shortcut action. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoadResult.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoadResult.cs similarity index 81% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoadResult.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoadResult.cs index a4fd6017..3d1d52c8 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoadResult.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoadResult.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Result of applying customization entries over the default shortcut catalog. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoader.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoader.cs similarity index 99% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoader.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoader.cs index 05493df6..1f6bfe88 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoader.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutCustomizationLoader.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Applies persisted shortcut overrides with per-item fallback and validation issues. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutDefinition.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutDefinition.cs similarity index 88% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutDefinition.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutDefinition.cs index 8382af30..945717aa 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutDefinition.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutDefinition.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Canonical shortcut definition consumed by input, command palette and shortcut UIs. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutExecutionContext.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutExecutionContext.cs similarity index 82% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutExecutionContext.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutExecutionContext.cs index e97108b4..c9e8beb0 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutExecutionContext.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutExecutionContext.cs @@ -1,6 +1,6 @@ using Avalonia.Input; -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Runtime key press context used by shortcut execution. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutExecutionService.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutExecutionService.cs similarity index 94% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutExecutionService.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutExecutionService.cs index 3053b730..2b863fea 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutExecutionService.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutExecutionService.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Resolves key gestures against the shortcut registry and dispatches execution callbacks. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGesture.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGesture.cs similarity index 95% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGesture.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGesture.cs index 9d342cad..df7d59ba 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGesture.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGesture.cs @@ -1,6 +1,6 @@ using Avalonia.Input; -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Represents a keyboard gesture with display and normalized forms. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGestureParseResult.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGestureParseResult.cs similarity index 81% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGestureParseResult.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGestureParseResult.cs index aa4b799f..450a6a4c 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGestureParseResult.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGestureParseResult.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Parse result for a gesture text input. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGestureParser.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGestureParser.cs similarity index 98% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGestureParser.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGestureParser.cs index ad492cf1..338dc862 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutGestureParser.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutGestureParser.cs @@ -1,6 +1,6 @@ using Avalonia.Input; -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Default parser for textual keyboard gestures. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutKeyAliasMap.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutKeyAliasMap.cs similarity index 98% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutKeyAliasMap.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutKeyAliasMap.cs index 885947c9..6133eb59 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutKeyAliasMap.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutKeyAliasMap.cs @@ -1,6 +1,6 @@ using Avalonia.Input; -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Alias map for shortcut parser key and modifier tokens. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutRegistry.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutRegistry.cs similarity index 99% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutRegistry.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutRegistry.cs index 06095654..991623c9 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutRegistry.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutRegistry.cs @@ -1,6 +1,6 @@ using Avalonia.Input; -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Initial shortcut registry implementation with in-memory override and reset support. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutRegistrySnapshot.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutRegistrySnapshot.cs similarity index 81% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutRegistrySnapshot.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutRegistrySnapshot.cs index 094ca845..aa96b7df 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutRegistrySnapshot.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutRegistrySnapshot.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Immutable registry snapshot with effective definitions and validation issues. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutReloadResult.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutReloadResult.cs similarity index 75% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutReloadResult.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutReloadResult.cs index 938977f0..aba10634 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutReloadResult.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutReloadResult.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Result of reloading the shortcut registry state. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutUpdateResult.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutUpdateResult.cs similarity index 79% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutUpdateResult.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutUpdateResult.cs index f2088b57..80e59e13 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutUpdateResult.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutUpdateResult.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Result of a runtime shortcut update operation. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationCodes.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationCodes.cs similarity index 91% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationCodes.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationCodes.cs index 3936585f..305c2685 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationCodes.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationCodes.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Stable validation codes for shortcut parsing and registry operations. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationIssue.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationIssue.cs similarity index 80% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationIssue.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationIssue.cs index 946a8b18..b4dde179 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationIssue.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationIssue.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Structured validation issue for parsing, conflicts or invalid override operations. diff --git a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationMessageResolver.cs b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationMessageResolver.cs similarity index 96% rename from src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationMessageResolver.cs rename to src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationMessageResolver.cs index ce3ac492..0acc6bb6 100644 --- a/src/DBWeaver.UI/Services/Input/ShortcutRegistry/ShortcutValidationMessageResolver.cs +++ b/src/AkkornStudio.UI/Services/Input/ShortcutRegistry/ShortcutValidationMessageResolver.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.Input.ShortcutRegistry; +namespace AkkornStudio.UI.Services.Input.ShortcutRegistry; /// /// Resolves user-facing messages for stable shortcut validation codes. diff --git a/src/DBWeaver.UI/Services/Layout/MainWindowLayoutService.cs b/src/AkkornStudio.UI/Services/Layout/MainWindowLayoutService.cs similarity index 93% rename from src/DBWeaver.UI/Services/Layout/MainWindowLayoutService.cs rename to src/AkkornStudio.UI/Services/Layout/MainWindowLayoutService.cs index 930dc021..63d47918 100644 --- a/src/DBWeaver.UI/Services/Layout/MainWindowLayoutService.cs +++ b/src/AkkornStudio.UI/Services/Layout/MainWindowLayoutService.cs @@ -1,149 +1,149 @@ -using System.IO; -using System.Text.Json; -using Avalonia.Controls; -using System.ComponentModel; -using DBWeaver.UI.ViewModels; -using DBWeaver.UI.ViewModels.Canvas; - -namespace DBWeaver.UI.Services; - -/// -/// Manages grid layout persistence and data preview pane height. -/// Handles saving/loading column widths and preview pane dimensions. -/// -public class MainWindowLayoutService(Window window, CanvasViewModel vm) : IDisposable -{ - private readonly Window _window = window; - private readonly CanvasViewModel _vm = vm; - private double _previewHeight = 220; - private EventHandler? _windowClosingHandler; - private PropertyChangedEventHandler? _previewPropertyChangedHandler; - private bool _isWired; - - private static string AppDataDir => - global::DBWeaver.UI.AppConstants.AppDataDirectory; - - private static string LayoutFile => Path.Combine(AppDataDir, "layout.json"); - - public void Wire() - { - if (_isWired) - return; - - LoadLayout(); - - _windowClosingHandler = (_, _) => SaveLayout(); - _previewPropertyChangedHandler = (_, e) => - { - if (e.PropertyName == nameof(DataPreviewViewModel.IsVisible)) - UpdatePreviewRow(); - }; - - _window.Closing += _windowClosingHandler; - _vm.DataPreview.PropertyChanged += _previewPropertyChangedHandler; - _isWired = true; - - UpdatePreviewRow(); - } - - public void Dispose() - { - if (!_isWired) - return; - - if (_windowClosingHandler is not null) - _window.Closing -= _windowClosingHandler; - - if (_previewPropertyChangedHandler is not null) - _vm.DataPreview.PropertyChanged -= _previewPropertyChangedHandler; - - _windowClosingHandler = null; - _previewPropertyChangedHandler = null; - _isWired = false; - } - - private void UpdatePreviewRow() - { - Grid? centerGrid = _window.FindControl("CenterGrid"); - if (centerGrid is null || centerGrid.RowDefinitions.Count < 3) - return; - - if (_vm.DataPreview.IsVisible) - { - centerGrid.RowDefinitions[1].Height = new GridLength(8); - centerGrid.RowDefinitions[2].Height = new GridLength(_previewHeight); - } - else - { - double current = centerGrid.RowDefinitions[2].Height.Value; - if (current > 0) - _previewHeight = current; - centerGrid.RowDefinitions[1].Height = new GridLength(0); - centerGrid.RowDefinitions[2].Height = new GridLength(0); - } - } - - public void SaveLayout() - { - Grid? bodyGrid = _window.FindControl("BodyGrid"); - Grid? centerGrid = _window.FindControl("CenterGrid"); - if (bodyGrid is null || centerGrid is null || bodyGrid.ColumnDefinitions.Count < 5) - return; - - if (centerGrid.RowDefinitions.Count >= 3) - { - double current = centerGrid.RowDefinitions[2].Height.Value; - if (current > 0) - _previewHeight = current; - } - - var layout = new LayoutState( - bodyGrid.ColumnDefinitions[0].Width.Value, - bodyGrid.ColumnDefinitions[4].Width.Value, - _previewHeight - ); - - try - { - Directory.CreateDirectory(AppDataDir); - File.WriteAllText(LayoutFile, JsonSerializer.Serialize(layout)); - } - catch - { /* best effort */ - } - } - - private void LoadLayout() - { - try - { - if (!File.Exists(LayoutFile)) - return; - - LayoutState? layout = JsonSerializer.Deserialize( - File.ReadAllText(LayoutFile) - ); - if (layout is null) - return; - - Grid? bodyGrid = _window.FindControl("BodyGrid"); - if (bodyGrid is not null && bodyGrid.ColumnDefinitions.Count >= 5) - { - bodyGrid.ColumnDefinitions[0].Width = new GridLength( - Math.Clamp(layout.LeftWidth, 200, 420) - ); - bodyGrid.ColumnDefinitions[4].Width = new GridLength( - Math.Clamp(layout.RightWidth, 220, 500) - ); - } - - if (layout.PreviewHeight > 0) - _previewHeight = Math.Clamp(layout.PreviewHeight, 150, 600); - } - catch - { /* best effort */ - } - } - - public record LayoutState(double LeftWidth, double RightWidth, double PreviewHeight); -} +using System.IO; +using System.Text.Json; +using Avalonia.Controls; +using System.ComponentModel; +using AkkornStudio.UI.ViewModels; +using AkkornStudio.UI.ViewModels.Canvas; + +namespace AkkornStudio.UI.Services; + +/// +/// Manages grid layout persistence and data preview pane height. +/// Handles saving/loading column widths and preview pane dimensions. +/// +public class MainWindowLayoutService(Window window, CanvasViewModel vm) : IDisposable +{ + private readonly Window _window = window; + private readonly CanvasViewModel _vm = vm; + private double _previewHeight = 220; + private EventHandler? _windowClosingHandler; + private PropertyChangedEventHandler? _previewPropertyChangedHandler; + private bool _isWired; + + private static string AppDataDir => + global::AkkornStudio.UI.AppConstants.AppDataDirectory; + + private static string LayoutFile => Path.Combine(AppDataDir, "layout.json"); + + public void Wire() + { + if (_isWired) + return; + + LoadLayout(); + + _windowClosingHandler = (_, _) => SaveLayout(); + _previewPropertyChangedHandler = (_, e) => + { + if (e.PropertyName == nameof(DataPreviewViewModel.IsVisible)) + UpdatePreviewRow(); + }; + + _window.Closing += _windowClosingHandler; + _vm.DataPreview.PropertyChanged += _previewPropertyChangedHandler; + _isWired = true; + + UpdatePreviewRow(); + } + + public void Dispose() + { + if (!_isWired) + return; + + if (_windowClosingHandler is not null) + _window.Closing -= _windowClosingHandler; + + if (_previewPropertyChangedHandler is not null) + _vm.DataPreview.PropertyChanged -= _previewPropertyChangedHandler; + + _windowClosingHandler = null; + _previewPropertyChangedHandler = null; + _isWired = false; + } + + private void UpdatePreviewRow() + { + Grid? centerGrid = _window.FindControl("CenterGrid"); + if (centerGrid is null || centerGrid.RowDefinitions.Count < 3) + return; + + if (_vm.DataPreview.IsVisible) + { + centerGrid.RowDefinitions[1].Height = new GridLength(8); + centerGrid.RowDefinitions[2].Height = new GridLength(_previewHeight); + } + else + { + double current = centerGrid.RowDefinitions[2].Height.Value; + if (current > 0) + _previewHeight = current; + centerGrid.RowDefinitions[1].Height = new GridLength(0); + centerGrid.RowDefinitions[2].Height = new GridLength(0); + } + } + + public void SaveLayout() + { + Grid? bodyGrid = _window.FindControl("BodyGrid"); + Grid? centerGrid = _window.FindControl("CenterGrid"); + if (bodyGrid is null || centerGrid is null || bodyGrid.ColumnDefinitions.Count < 5) + return; + + if (centerGrid.RowDefinitions.Count >= 3) + { + double current = centerGrid.RowDefinitions[2].Height.Value; + if (current > 0) + _previewHeight = current; + } + + var layout = new LayoutState( + bodyGrid.ColumnDefinitions[0].Width.Value, + bodyGrid.ColumnDefinitions[4].Width.Value, + _previewHeight + ); + + try + { + Directory.CreateDirectory(AppDataDir); + File.WriteAllText(LayoutFile, JsonSerializer.Serialize(layout)); + } + catch + { /* best effort */ + } + } + + private void LoadLayout() + { + try + { + if (!File.Exists(LayoutFile)) + return; + + LayoutState? layout = JsonSerializer.Deserialize( + File.ReadAllText(LayoutFile) + ); + if (layout is null) + return; + + Grid? bodyGrid = _window.FindControl("BodyGrid"); + if (bodyGrid is not null && bodyGrid.ColumnDefinitions.Count >= 5) + { + bodyGrid.ColumnDefinitions[0].Width = new GridLength( + Math.Clamp(layout.LeftWidth, 200, 420) + ); + bodyGrid.ColumnDefinitions[4].Width = new GridLength( + Math.Clamp(layout.RightWidth, 220, 500) + ); + } + + if (layout.PreviewHeight > 0) + _previewHeight = Math.Clamp(layout.PreviewHeight, 150, 600); + } + catch + { /* best effort */ + } + } + + public record LayoutState(double LeftWidth, double RightWidth, double PreviewHeight); +} diff --git a/src/DBWeaver.UI/Services/LiveSqlBar/Models/SqlToken.cs b/src/AkkornStudio.UI/Services/LiveSqlBar/Models/SqlToken.cs similarity index 56% rename from src/DBWeaver.UI/Services/LiveSqlBar/Models/SqlToken.cs rename to src/AkkornStudio.UI/Services/LiveSqlBar/Models/SqlToken.cs index 6414d5ef..ba688d82 100644 --- a/src/DBWeaver.UI/Services/LiveSqlBar/Models/SqlToken.cs +++ b/src/AkkornStudio.UI/Services/LiveSqlBar/Models/SqlToken.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.LiveSqlBar; +namespace AkkornStudio.UI.Services.LiveSqlBar; public sealed record SqlToken(string Text, SqlTokenKind Kind); diff --git a/src/DBWeaver.UI/Services/LiveSqlBar/Models/SqlTokenKind.cs b/src/AkkornStudio.UI/Services/LiveSqlBar/Models/SqlTokenKind.cs similarity index 73% rename from src/DBWeaver.UI/Services/LiveSqlBar/Models/SqlTokenKind.cs rename to src/AkkornStudio.UI/Services/LiveSqlBar/Models/SqlTokenKind.cs index a9392c48..beac6ab4 100644 --- a/src/DBWeaver.UI/Services/LiveSqlBar/Models/SqlTokenKind.cs +++ b/src/AkkornStudio.UI/Services/LiveSqlBar/Models/SqlTokenKind.cs @@ -1,4 +1,4 @@ -namespace DBWeaver.UI.Services.LiveSqlBar; +namespace AkkornStudio.UI.Services.LiveSqlBar; public enum SqlTokenKind { diff --git a/src/DBWeaver.UI/Services/LiveSqlBar/Presentation/LiveSqlDiagnosticItem.cs b/src/AkkornStudio.UI/Services/LiveSqlBar/Presentation/LiveSqlDiagnosticItem.cs similarity index 88% rename from src/DBWeaver.UI/Services/LiveSqlBar/Presentation/LiveSqlDiagnosticItem.cs rename to src/AkkornStudio.UI/Services/LiveSqlBar/Presentation/LiveSqlDiagnosticItem.cs index 054c6e22..48a974bd 100644 --- a/src/DBWeaver.UI/Services/LiveSqlBar/Presentation/LiveSqlDiagnosticItem.cs +++ b/src/AkkornStudio.UI/Services/LiveSqlBar/Presentation/LiveSqlDiagnosticItem.cs @@ -1,7 +1,7 @@ -using DBWeaver.UI.Services.QueryPreview.Models; -using DBWeaver.UI.ViewModels; +using AkkornStudio.UI.Services.QueryPreview.Models; +using AkkornStudio.UI.ViewModels; -namespace DBWeaver.UI.Services.LiveSqlBar; +namespace AkkornStudio.UI.Services.LiveSqlBar; public sealed class LiveSqlDiagnosticItem { diff --git a/src/DBWeaver.UI/Services/Localization/ILocalizationService.cs b/src/AkkornStudio.UI/Services/Localization/ILocalizationService.cs similarity index 82% rename from src/DBWeaver.UI/Services/Localization/ILocalizationService.cs rename to src/AkkornStudio.UI/Services/Localization/ILocalizationService.cs index b3086632..a34ccf59 100644 --- a/src/DBWeaver.UI/Services/Localization/ILocalizationService.cs +++ b/src/AkkornStudio.UI/Services/Localization/ILocalizationService.cs @@ -1,16 +1,16 @@ -using System.ComponentModel; - -namespace DBWeaver.UI.Services.Localization; - -public interface ILocalizationService : INotifyPropertyChanged -{ - string CurrentCulture { get; } - - string CurrentLanguageLabel { get; } - - string this[string key] { get; } - - bool ToggleCulture(); - - bool SetCulture(string culture); -} +using System.ComponentModel; + +namespace AkkornStudio.UI.Services.Localization; + +public interface ILocalizationService : INotifyPropertyChanged +{ + string CurrentCulture { get; } + + string CurrentLanguageLabel { get; } + + string this[string key] { get; } + + bool ToggleCulture(); + + bool SetCulture(string culture); +} diff --git a/src/DBWeaver.UI/Services/Localization/LocalizationService.cs b/src/AkkornStudio.UI/Services/Localization/LocalizationService.cs similarity index 95% rename from src/DBWeaver.UI/Services/Localization/LocalizationService.cs rename to src/AkkornStudio.UI/Services/Localization/LocalizationService.cs index e1aeb659..87472bcd 100644 --- a/src/DBWeaver.UI/Services/Localization/LocalizationService.cs +++ b/src/AkkornStudio.UI/Services/Localization/LocalizationService.cs @@ -1,173 +1,173 @@ -using System.ComponentModel; -using System.Text.Json; - -namespace DBWeaver.UI.Services.Localization; - -public sealed class LocalizationService : ILocalizationService -{ - private const string DefaultCulture = "pt-BR"; - private static readonly string[] SupportedCultures = - { - "pt-BR", - "en-US", - "es-ES", - "ru-RU", - "ja-JP", - "zh-TW", - }; - private readonly object _sync = new(); - private Dictionary _strings = new(StringComparer.OrdinalIgnoreCase); - - public static LocalizationService Instance { get; } = new(); - - public event PropertyChangedEventHandler? PropertyChanged; - - public string CurrentCulture { get; private set; } = DefaultCulture; - - public string CurrentLanguageLabel => CurrentCulture.Equals("pt-BR", StringComparison.OrdinalIgnoreCase) - ? "PT-BR" - : CurrentCulture.ToUpperInvariant(); - - public IReadOnlyList AvailableCultures => SupportedCultures; - - public string this[string key] - { - get - { - lock (_sync) - { - return _strings.TryGetValue(key, out string? value) ? value : key; - } - } - } - - private LocalizationService() - { - string culture = LoadSavedCulture() ?? DefaultCulture; - SetCulture(culture); - } - - public bool ToggleCulture() - { - string target = CurrentCulture.Equals("pt-BR", StringComparison.OrdinalIgnoreCase) - ? "en-US" - : "pt-BR"; - return SetCulture(target); - } - - public bool SetCulture(string culture) - { - string normalized = NormalizeCulture(culture); - Dictionary? loaded = LoadFromJson(normalized); - if (loaded is null) - return false; - - lock (_sync) - { - _strings = loaded; - CurrentCulture = normalized; - } - - SaveCulture(normalized); - RaiseAllChanged(); - return true; - } - - private static string NormalizeCulture(string? culture) - { - if (string.IsNullOrWhiteSpace(culture)) - return DefaultCulture; - - string normalized = culture.Trim(); - if (string.Equals(normalized, "en", StringComparison.OrdinalIgnoreCase)) - normalized = "en-US"; - else if (string.Equals(normalized, "pt", StringComparison.OrdinalIgnoreCase)) - normalized = "pt-BR"; - else if (string.Equals(normalized, "es", StringComparison.OrdinalIgnoreCase)) - normalized = "es-ES"; - else if (string.Equals(normalized, "ru", StringComparison.OrdinalIgnoreCase)) - normalized = "ru-RU"; - else if (string.Equals(normalized, "ja", StringComparison.OrdinalIgnoreCase)) - normalized = "ja-JP"; - else if (string.Equals(normalized, "zh-TW", StringComparison.OrdinalIgnoreCase) || - string.Equals(normalized, "zh-Hant", StringComparison.OrdinalIgnoreCase) || - string.Equals(normalized, "zh", StringComparison.OrdinalIgnoreCase)) - normalized = "zh-TW"; - - foreach (string supported in SupportedCultures) - { - if (string.Equals(normalized, supported, StringComparison.OrdinalIgnoreCase)) - return supported; - } - - return DefaultCulture; - } - - private static Dictionary? LoadFromJson(string culture) - { - try - { - string baseDir = AppContext.BaseDirectory; - string path = Path.Combine(baseDir, "Assets", "Localization", $"{culture}.json"); - if (!File.Exists(path)) - return null; - - string json = File.ReadAllText(path); - return JsonSerializer.Deserialize>(json) - ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - } - catch - { - return null; - } - } - - private static string SettingsFilePath => Path.Combine( - global::DBWeaver.UI.AppConstants.AppDataDirectory, - "localization.settings.json"); - - private static string? LoadSavedCulture() - { - try - { - if (!File.Exists(SettingsFilePath)) - return null; - - string json = File.ReadAllText(SettingsFilePath); - Dictionary? payload = JsonSerializer.Deserialize>(json); - if (payload is null) - return null; - - return payload.TryGetValue("culture", out string? culture) ? culture : null; - } - catch - { - return null; - } - } - - private static void SaveCulture(string culture) - { - try - { - Directory.CreateDirectory(Path.GetDirectoryName(SettingsFilePath)!); - string json = JsonSerializer.Serialize(new Dictionary - { - ["culture"] = culture - }, new JsonSerializerOptions { WriteIndented = true }); - File.WriteAllText(SettingsFilePath, json); - } - catch - { - // best effort - } - } - - private void RaiseAllChanged() - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty)); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]")); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentCulture))); - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentLanguageLabel))); - } -} +using System.ComponentModel; +using System.Text.Json; + +namespace AkkornStudio.UI.Services.Localization; + +public sealed class LocalizationService : ILocalizationService +{ + private const string DefaultCulture = "pt-BR"; + private static readonly string[] SupportedCultures = + { + "pt-BR", + "en-US", + "es-ES", + "ru-RU", + "ja-JP", + "zh-TW", + }; + private readonly object _sync = new(); + private Dictionary _strings = new(StringComparer.OrdinalIgnoreCase); + + public static LocalizationService Instance { get; } = new(); + + public event PropertyChangedEventHandler? PropertyChanged; + + public string CurrentCulture { get; private set; } = DefaultCulture; + + public string CurrentLanguageLabel => CurrentCulture.Equals("pt-BR", StringComparison.OrdinalIgnoreCase) + ? "PT-BR" + : CurrentCulture.ToUpperInvariant(); + + public IReadOnlyList AvailableCultures => SupportedCultures; + + public string this[string key] + { + get + { + lock (_sync) + { + return _strings.TryGetValue(key, out string? value) ? value : key; + } + } + } + + private LocalizationService() + { + string culture = LoadSavedCulture() ?? DefaultCulture; + SetCulture(culture); + } + + public bool ToggleCulture() + { + string target = CurrentCulture.Equals("pt-BR", StringComparison.OrdinalIgnoreCase) + ? "en-US" + : "pt-BR"; + return SetCulture(target); + } + + public bool SetCulture(string culture) + { + string normalized = NormalizeCulture(culture); + Dictionary? loaded = LoadFromJson(normalized); + if (loaded is null) + return false; + + lock (_sync) + { + _strings = loaded; + CurrentCulture = normalized; + } + + SaveCulture(normalized); + RaiseAllChanged(); + return true; + } + + private static string NormalizeCulture(string? culture) + { + if (string.IsNullOrWhiteSpace(culture)) + return DefaultCulture; + + string normalized = culture.Trim(); + if (string.Equals(normalized, "en", StringComparison.OrdinalIgnoreCase)) + normalized = "en-US"; + else if (string.Equals(normalized, "pt", StringComparison.OrdinalIgnoreCase)) + normalized = "pt-BR"; + else if (string.Equals(normalized, "es", StringComparison.OrdinalIgnoreCase)) + normalized = "es-ES"; + else if (string.Equals(normalized, "ru", StringComparison.OrdinalIgnoreCase)) + normalized = "ru-RU"; + else if (string.Equals(normalized, "ja", StringComparison.OrdinalIgnoreCase)) + normalized = "ja-JP"; + else if (string.Equals(normalized, "zh-TW", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalized, "zh-Hant", StringComparison.OrdinalIgnoreCase) || + string.Equals(normalized, "zh", StringComparison.OrdinalIgnoreCase)) + normalized = "zh-TW"; + + foreach (string supported in SupportedCultures) + { + if (string.Equals(normalized, supported, StringComparison.OrdinalIgnoreCase)) + return supported; + } + + return DefaultCulture; + } + + private static Dictionary? LoadFromJson(string culture) + { + try + { + string baseDir = AppContext.BaseDirectory; + string path = Path.Combine(baseDir, "Assets", "Localization", $"{culture}.json"); + if (!File.Exists(path)) + return null; + + string json = File.ReadAllText(path); + return JsonSerializer.Deserialize>(json) + ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + } + catch + { + return null; + } + } + + private static string SettingsFilePath => Path.Combine( + global::AkkornStudio.UI.AppConstants.AppDataDirectory, + "localization.settings.json"); + + private static string? LoadSavedCulture() + { + try + { + if (!File.Exists(SettingsFilePath)) + return null; + + string json = File.ReadAllText(SettingsFilePath); + Dictionary? payload = JsonSerializer.Deserialize>(json); + if (payload is null) + return null; + + return payload.TryGetValue("culture", out string? culture) ? culture : null; + } + catch + { + return null; + } + } + + private static void SaveCulture(string culture) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(SettingsFilePath)!); + string json = JsonSerializer.Serialize(new Dictionary + { + ["culture"] = culture + }, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(SettingsFilePath, json); + } + catch + { + // best effort + } + } + + private void RaiseAllChanged() + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty)); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]")); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentCulture))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentLanguageLabel))); + } +} diff --git a/src/DBWeaver.UI/Services/Modal/GlobalModalManager.cs b/src/AkkornStudio.UI/Services/Modal/GlobalModalManager.cs similarity index 93% rename from src/DBWeaver.UI/Services/Modal/GlobalModalManager.cs rename to src/AkkornStudio.UI/Services/Modal/GlobalModalManager.cs index 4aae927f..18c2c18f 100644 --- a/src/DBWeaver.UI/Services/Modal/GlobalModalManager.cs +++ b/src/AkkornStudio.UI/Services/Modal/GlobalModalManager.cs @@ -1,35 +1,35 @@ -namespace DBWeaver.UI.Services.Modal; - -public sealed class GlobalModalManager : IGlobalModalManager -{ - public static GlobalModalManager Instance { get; } = new(); - - private GlobalModalManager() - { - } - - public event Action? ModalRequested; - - public bool Request(GlobalModalRequest request) - { - Action? handlers = ModalRequested; - if (handlers is null) - return false; - - handlers.Invoke(request); - return true; - } - - public bool RequestConnectionManager(bool beginNewProfile = false, bool keepStartVisible = false) => - Request(new GlobalModalRequest( - Kind: GlobalModalKind.ConnectionManager, - BeginNewProfile: beginNewProfile, - KeepStartVisible: keepStartVisible - )); - - public bool RequestSettings(bool keepStartVisible = false) => - Request(new GlobalModalRequest( - Kind: GlobalModalKind.Settings, - KeepStartVisible: keepStartVisible - )); -} +namespace AkkornStudio.UI.Services.Modal; + +public sealed class GlobalModalManager : IGlobalModalManager +{ + public static GlobalModalManager Instance { get; } = new(); + + private GlobalModalManager() + { + } + + public event Action? ModalRequested; + + public bool Request(GlobalModalRequest request) + { + Action? handlers = ModalRequested; + if (handlers is null) + return false; + + handlers.Invoke(request); + return true; + } + + public bool RequestConnectionManager(bool beginNewProfile = false, bool keepStartVisible = false) => + Request(new GlobalModalRequest( + Kind: GlobalModalKind.ConnectionManager, + BeginNewProfile: beginNewProfile, + KeepStartVisible: keepStartVisible + )); + + public bool RequestSettings(bool keepStartVisible = false) => + Request(new GlobalModalRequest( + Kind: GlobalModalKind.Settings, + KeepStartVisible: keepStartVisible + )); +} diff --git a/src/DBWeaver.UI/Services/Modal/IGlobalModalManager.cs b/src/AkkornStudio.UI/Services/Modal/IGlobalModalManager.cs similarity index 89% rename from src/DBWeaver.UI/Services/Modal/IGlobalModalManager.cs rename to src/AkkornStudio.UI/Services/Modal/IGlobalModalManager.cs index 716aebab..af4dc1e8 100644 --- a/src/DBWeaver.UI/Services/Modal/IGlobalModalManager.cs +++ b/src/AkkornStudio.UI/Services/Modal/IGlobalModalManager.cs @@ -1,24 +1,24 @@ -namespace DBWeaver.UI.Services.Modal; - -public enum GlobalModalKind -{ - ConnectionManager, - Settings, -} - -public readonly record struct GlobalModalRequest( - GlobalModalKind Kind, - bool BeginNewProfile = false, - bool KeepStartVisible = false -); - -public interface IGlobalModalManager -{ - event Action? ModalRequested; - - bool Request(GlobalModalRequest request); - - bool RequestConnectionManager(bool beginNewProfile = false, bool keepStartVisible = false); - - bool RequestSettings(bool keepStartVisible = false); -} +namespace AkkornStudio.UI.Services.Modal; + +public enum GlobalModalKind +{ + ConnectionManager, + Settings, +} + +public readonly record struct GlobalModalRequest( + GlobalModalKind Kind, + bool BeginNewProfile = false, + bool KeepStartVisible = false +); + +public interface IGlobalModalManager +{ + event Action? ModalRequested; + + bool Request(GlobalModalRequest request); + + bool RequestConnectionManager(bool beginNewProfile = false, bool keepStartVisible = false); + + bool RequestSettings(bool keepStartVisible = false); +} diff --git a/src/DBWeaver.UI/Services/Node/NodeIconCatalog.cs b/src/AkkornStudio.UI/Services/Node/NodeIconCatalog.cs similarity index 96% rename from src/DBWeaver.UI/Services/Node/NodeIconCatalog.cs rename to src/AkkornStudio.UI/Services/Node/NodeIconCatalog.cs index 040e6215..15494a26 100644 --- a/src/DBWeaver.UI/Services/Node/NodeIconCatalog.cs +++ b/src/AkkornStudio.UI/Services/Node/NodeIconCatalog.cs @@ -1,98 +1,98 @@ -using Material.Icons; -using DBWeaver.Nodes; - -namespace DBWeaver.UI.Services.Node; - -/// -/// Single source of truth for every icon character used in the application. -/// All node category icons, validation severity icons, and diagnostic category icons -/// are defined here so that new components find and reuse them without hardcoding. -/// -/// Convention: add a named constant below, then reference it from ViewModels/AXAML. -/// Fallback for any unknown category/state is . -/// -public static class NodeIconCatalog -{ - // ── Node category icons ──────────────────────────────────────────────────── - - public const string DataSource = "⊞"; - public const string StringTransform = "Aa"; - public const string MathTransform = "∑"; - public const string TypeCast = "⇌"; - public const string Comparison = "≈"; - public const string LogicGate = "&"; - public const string Json = "{}"; - public const string Aggregate = "Σ"; - public const string Conditional = "?"; - public const string Output = "▶"; - public const string Ddl = "▣"; - - /// Fallback shown when no specific icon matches. - public const string Unknown = "○"; - - // ── Validation / severity icons ─────────────────────────────────────────── - - /// Error badge — shown as a filled red circle on the node header. - public const string ValidationError = "!"; - - /// Warning badge — shown as a filled yellow circle on the node header. - public const string ValidationWarning = "⚠"; - - /// Checkmark / success indicator. - public const string Success = "✓"; - - // ── Diagnostic category icons (ErrorDiagnostics) ────────────────────────── - - public const string DiagSafePreview = "🔒"; - public const string DiagConnection = "🔌"; - public const string DiagAuthorization = "🔑"; - public const string DiagTimeout = "⏱"; - public const string DiagSchema = "🗂"; - public const string DiagSyntax = "✏"; - public const string DiagCompatibility = "⚙"; - public const string DiagUnknown = "⚠"; - - // ── Lookup ──────────────────────────────────────────────────────────────── - - /// - /// Returns the canonical icon character for the given node category. - /// Falls back to for unrecognised values. - /// - public static string GetForCategory(NodeCategory category) => - category switch - { - NodeCategory.DataSource => DataSource, - NodeCategory.StringTransform => StringTransform, - NodeCategory.MathTransform => MathTransform, - NodeCategory.TypeCast => TypeCast, - NodeCategory.Comparison => Comparison, - NodeCategory.LogicGate => LogicGate, - NodeCategory.Json => Json, - NodeCategory.Aggregate => Aggregate, - NodeCategory.Conditional => Conditional, - NodeCategory.Output => Output, - NodeCategory.Ddl => Ddl, - _ => Unknown, - }; - - /// - /// Returns the for the given node category. - /// Falls back to for unrecognised values. - /// - public static MaterialIconKind GetKindForCategory(NodeCategory category) => - category switch - { - NodeCategory.DataSource => MaterialIconKind.Table, - NodeCategory.StringTransform => MaterialIconKind.FormatText, - NodeCategory.MathTransform => MaterialIconKind.Calculator, - NodeCategory.TypeCast => MaterialIconKind.SwapHorizontal, - NodeCategory.Comparison => MaterialIconKind.Equal, - NodeCategory.LogicGate => MaterialIconKind.Filter, - NodeCategory.Json => MaterialIconKind.CodeBraces, - NodeCategory.Aggregate => MaterialIconKind.ChartBar, - NodeCategory.Conditional => MaterialIconKind.HelpCircle, - NodeCategory.Output => MaterialIconKind.PlayCircle, - NodeCategory.Ddl => MaterialIconKind.TableEdit, - _ => MaterialIconKind.Help, - }; -} +using Material.Icons; +using AkkornStudio.Nodes; + +namespace AkkornStudio.UI.Services.Node; + +/// +/// Single source of truth for every icon character used in the application. +/// All node category icons, validation severity icons, and diagnostic category icons +/// are defined here so that new components find and reuse them without hardcoding. +/// +/// Convention: add a named constant below, then reference it from ViewModels/AXAML. +/// Fallback for any unknown category/state is . +/// +public static class NodeIconCatalog +{ + // ── Node category icons ──────────────────────────────────────────────────── + + public const string DataSource = "⊞"; + public const string StringTransform = "Aa"; + public const string MathTransform = "∑"; + public const string TypeCast = "⇌"; + public const string Comparison = "≈"; + public const string LogicGate = "&"; + public const string Json = "{}"; + public const string Aggregate = "Σ"; + public const string Conditional = "?"; + public const string Output = "▶"; + public const string Ddl = "▣"; + + /// Fallback shown when no specific icon matches. + public const string Unknown = "○"; + + // ── Validation / severity icons ─────────────────────────────────────────── + + /// Error badge — shown as a filled red circle on the node header. + public const string ValidationError = "!"; + + /// Warning badge — shown as a filled yellow circle on the node header. + public const string ValidationWarning = "⚠"; + + /// Checkmark / success indicator. + public const string Success = "✓"; + + // ── Diagnostic category icons (ErrorDiagnostics) ────────────────────────── + + public const string DiagSafePreview = "🔒"; + public const string DiagConnection = "🔌"; + public const string DiagAuthorization = "🔑"; + public const string DiagTimeout = "⏱"; + public const string DiagSchema = "🗂"; + public const string DiagSyntax = "✏"; + public const string DiagCompatibility = "⚙"; + public const string DiagUnknown = "⚠"; + + // ── Lookup ──────────────────────────────────────────────────────────────── + + /// + /// Returns the canonical icon character for the given node category. + /// Falls back to for unrecognised values. + /// + public static string GetForCategory(NodeCategory category) => + category switch + { + NodeCategory.DataSource => DataSource, + NodeCategory.StringTransform => StringTransform, + NodeCategory.MathTransform => MathTransform, + NodeCategory.TypeCast => TypeCast, + NodeCategory.Comparison => Comparison, + NodeCategory.LogicGate => LogicGate, + NodeCategory.Json => Json, + NodeCategory.Aggregate => Aggregate, + NodeCategory.Conditional => Conditional, + NodeCategory.Output => Output, + NodeCategory.Ddl => Ddl, + _ => Unknown, + }; + + /// + /// Returns the for the given node category. + /// Falls back to for unrecognised values. + /// + public static MaterialIconKind GetKindForCategory(NodeCategory category) => + category switch + { + NodeCategory.DataSource => MaterialIconKind.Table, + NodeCategory.StringTransform => MaterialIconKind.FormatText, + NodeCategory.MathTransform => MaterialIconKind.Calculator, + NodeCategory.TypeCast => MaterialIconKind.SwapHorizontal, + NodeCategory.Comparison => MaterialIconKind.Equal, + NodeCategory.LogicGate => MaterialIconKind.Filter, + NodeCategory.Json => MaterialIconKind.CodeBraces, + NodeCategory.Aggregate => MaterialIconKind.ChartBar, + NodeCategory.Conditional => MaterialIconKind.HelpCircle, + NodeCategory.Output => MaterialIconKind.PlayCircle, + NodeCategory.Ddl => MaterialIconKind.TableEdit, + _ => MaterialIconKind.Help, + }; +} diff --git a/src/DBWeaver.UI/Services/Query/PreviewService.cs b/src/AkkornStudio.UI/Services/Query/PreviewService.cs similarity index 66% rename from src/DBWeaver.UI/Services/Query/PreviewService.cs rename to src/AkkornStudio.UI/Services/Query/PreviewService.cs index cdfdf6db..ba2c6fb1 100644 --- a/src/DBWeaver.UI/Services/Query/PreviewService.cs +++ b/src/AkkornStudio.UI/Services/Query/PreviewService.cs @@ -1,330 +1,453 @@ -using System.Diagnostics; -using Avalonia.Controls; -using Avalonia.Threading; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using DBWeaver.Core; -using DBWeaver.UI.Controls; -using DBWeaver.UI.Services.Localization; -using DBWeaver.UI.ViewModels; -using System.ComponentModel; - -namespace DBWeaver.UI.Services; - -/// -/// Manages data preview pane wiring and query execution. -/// Supports real async cancellation via CancellationTokenSource, -/// live elapsed-time feedback, and clean state transitions. -/// -public class PreviewService(Window window, CanvasViewModel vm, ILogger? logger = null) : IDisposable -{ - private readonly Window _window = window; - private readonly CanvasViewModel _vm = vm; - private readonly QueryExecutorService _queryExecutor = new(); - private readonly ILogger _logger = logger ?? NullLogger.Instance; - - // Active run — cancelled when user clicks Cancel or a new run starts - private CancellationTokenSource? _runCts; - - // Event handlers stored for proper cleanup (prevent memory leaks) - private PropertyChangedEventHandler? _canvasPropertyChangedHandler; - private PropertyChangedEventHandler? _liveSqlPropertyChangedHandler; - private bool _disposed = false; - - // ── Wiring ──────────────────────────────────────────────────────────────── - - public void Wire() - { - ThrowIfDisposed(); - - _logger.LogDebug("Wire() called - scheduling control lookup with delay"); - - // Clean up previous subscriptions if Wire is called again - UnsubscribeFromPropertyChangedEvents(); - - // Schedule the wiring to happen after layout is updated - _ = Dispatcher.UIThread.InvokeAsync(async () => - { - // Wait for layout to complete - await Task.Delay(AppConstants.PreviewDebounceMs); - - _logger.LogDebug("Looking for DataPreviewPanel first..."); - // First, find the PreviewPanel (UserControl) - var previewPanel = _window.FindControl("PreviewPanel"); - _logger.LogDebug("Found PreviewPanel: {Found}", previewPanel is not null); - - Button? run = null; - Button? cancel = null; - Button? cls = null; - - if (previewPanel is not null) - { - _logger.LogDebug("Searching for buttons within PreviewPanel..."); - // Search for buttons within the panel - run = previewPanel.FindControl