Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76625c6ac5 | ||
|
|
7ae993898b | ||
|
|
b1bdf20af4 | ||
|
|
5c43ccaac5 | ||
|
|
41a5a9f59e | ||
|
|
127cf84e1b | ||
|
|
5b6fbebe90 | ||
|
|
1e46083f2b | ||
|
|
0fe5b902e9 | ||
|
|
065171bc18 | ||
|
|
0b57de1de5 | ||
|
|
4586dcd44d | ||
|
|
a438fdf21f | ||
|
|
5e52b722cd | ||
|
|
04ba5f01e5 | ||
|
|
eb553eb2e5 | ||
|
|
884ef6ba17 | ||
|
|
65a3e24aed | ||
|
|
7fdf162838 | ||
|
|
fd3509bbd6 | ||
|
|
48214aea9d | ||
|
|
050757bcaa | ||
|
|
b81f18e4f5 | ||
|
|
e327f5f6de | ||
|
|
b3e9c5c20c | ||
|
|
70f1834655 | ||
|
|
e42f3d7307 | ||
|
|
a18bae6672 | ||
|
|
4d5faa7406 | ||
|
|
c7c266c309 | ||
|
|
105da7dfea | ||
|
|
5a183a707c | ||
|
|
d51b92998c | ||
|
|
45c0ccadcd | ||
|
|
d345c25f71 | ||
|
|
4fd9bfecc6 | ||
|
|
4952707bbb | ||
|
|
bf9bb3fc47 | ||
|
|
6c955135d9 | ||
|
|
f77ddbce35 | ||
|
|
f3b163a29e | ||
|
|
9944e1e04c | ||
|
|
e08b81e2ce | ||
|
|
eea80fc31a | ||
|
|
6a2caa5083 | ||
|
|
b9dc55586b | ||
|
|
87e3258b09 | ||
|
|
3421febc32 | ||
|
|
175c79f2ee | ||
|
|
0ff3461815 | ||
|
|
cb4406c715 | ||
|
|
23d25e58fa |
@@ -2229,7 +2229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dbx-cli"
|
||||
version = "0.4.123"
|
||||
version = "0.4.124"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"dbx-core",
|
||||
@@ -2568,7 +2568,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dbx-mcp"
|
||||
version = "0.4.123"
|
||||
version = "0.4.124"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
{
|
||||
"access": "0.1.80",
|
||||
"bigquery": "0.1.84",
|
||||
"cache": "0.1.29",
|
||||
"access": "0.1.81",
|
||||
"bigquery": "0.1.85",
|
||||
"cache": "0.1.30",
|
||||
"cassandra": "0.1.54",
|
||||
"dameng": "0.1.94",
|
||||
"databend": "0.1.71",
|
||||
"databricks": "0.1.78",
|
||||
"db2": "0.1.84",
|
||||
"dameng": "0.1.95",
|
||||
"databend": "0.1.72",
|
||||
"databricks": "0.1.79",
|
||||
"db2": "0.1.85",
|
||||
"duckdb": "0.1.33",
|
||||
"exasol": "0.1.79",
|
||||
"firebird": "0.1.81",
|
||||
"gbase8a": "0.1.78",
|
||||
"gbase8c": "0.1.1",
|
||||
"gbase8s": "0.1.73",
|
||||
"goldendb": "0.1.84",
|
||||
"h2": "0.1.84",
|
||||
"h2-legacy": "0.1.53",
|
||||
"exasol": "0.1.80",
|
||||
"firebird": "0.1.82",
|
||||
"gbase8a": "0.1.79",
|
||||
"gbase8c": "0.1.2",
|
||||
"gbase8s": "0.1.74",
|
||||
"goldendb": "0.1.85",
|
||||
"h2": "0.1.85",
|
||||
"h2-legacy": "0.1.54",
|
||||
"hive": "0.1.60",
|
||||
"argo": "0.1.4",
|
||||
"ignite": "0.1.36",
|
||||
"ignite3": "0.1.36",
|
||||
"spark": "0.1.55",
|
||||
"informix": "0.1.84",
|
||||
"kingbase": "0.1.65",
|
||||
"kylin": "0.1.83",
|
||||
"ignite": "0.1.37",
|
||||
"ignite3": "0.1.37",
|
||||
"spark": "0.1.56",
|
||||
"informix": "0.1.85",
|
||||
"kingbase": "0.1.66",
|
||||
"kylin": "0.1.84",
|
||||
"neo4j": "0.1.53",
|
||||
"nebula": "0.1.3",
|
||||
"oceanbase-oracle": "0.1.80",
|
||||
"oceanbase-oracle": "0.1.81",
|
||||
"oracle": "0.1.71",
|
||||
"oracle-oci": "0.1.4",
|
||||
"snowflake": "0.1.83",
|
||||
"snowflake": "0.1.84",
|
||||
"sqlite-worker": "0.1.7",
|
||||
"spanner": "0.1.36",
|
||||
"saphana": "0.1.79",
|
||||
"sundb": "0.1.84",
|
||||
"teradata": "0.1.78",
|
||||
"spanner": "0.1.37",
|
||||
"saphana": "0.1.80",
|
||||
"sundb": "0.1.85",
|
||||
"teradata": "0.1.79",
|
||||
"tdengine": "0.1.53",
|
||||
"trino": "0.1.83",
|
||||
"transwarp": "0.1.14",
|
||||
"vastbase": "0.1.55",
|
||||
"vertica": "0.1.78",
|
||||
"mongodb": "0.1.87",
|
||||
"highgo": "0.1.81",
|
||||
"uxdb": "0.1.50",
|
||||
"yashandb": "0.1.79",
|
||||
"oscar": "0.1.58",
|
||||
"trino": "0.1.84",
|
||||
"transwarp": "0.1.15",
|
||||
"vastbase": "0.1.56",
|
||||
"vertica": "0.1.79",
|
||||
"mongodb": "0.1.88",
|
||||
"highgo": "0.1.82",
|
||||
"uxdb": "0.1.51",
|
||||
"yashandb": "0.1.80",
|
||||
"oscar": "0.1.59",
|
||||
"xugu": "0.1.50",
|
||||
"iris": "0.1.77",
|
||||
"iris": "0.1.78",
|
||||
"iotdb": "0.1.46",
|
||||
"etcd": "0.1.46",
|
||||
"etcd2": "0.1.4",
|
||||
"zookeeper": "0.1.34",
|
||||
"kafka": "0.1.18",
|
||||
"kafka": "0.1.19",
|
||||
"rocketmq": "0.1.17",
|
||||
"rabbitmq": "0.1.15",
|
||||
"sqlserver-legacy": "0.1.58"
|
||||
"sqlserver-legacy": "0.1.59"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Panayiotis Lipiridis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,8 @@
|
||||
Source: https://github.com/lipis/flag-icons
|
||||
Version: v7.5.0
|
||||
Tag object: 50a8bff005239b0d2d661254094dedb9c75dbef3
|
||||
Commit: 7aa5b2bdddd570ece62c812c0cb588ccdc099e2e
|
||||
Original asset directory: https://github.com/lipis/flag-icons/tree/7aa5b2bdddd570ece62c812c0cb588ccdc099e2e/flags/4x3
|
||||
Assets: us.svg, az.svg, es.svg, id.svg, it.svg, jp.svg, kr.svg, br.svg, pl.svg, ru.svg, th.svg, tr.svg, cn.svg
|
||||
License: MIT; see the unmodified LICENSE file in this directory.
|
||||
The SVG files are unmodified copies from the pinned source commit.
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-az" viewBox="0 0 640 480">
|
||||
<path fill="#3f9c35" d="M.1 0h640v480H.1z"/>
|
||||
<path fill="#ed2939" d="M.1 0h640v320H.1z"/>
|
||||
<path fill="#00b9e4" d="M.1 0h640v160H.1z"/>
|
||||
<circle cx="304" cy="240" r="72" fill="#fff"/>
|
||||
<circle cx="320" cy="240" r="60" fill="#ed2939"/>
|
||||
<path fill="#fff" d="m384 200 7.7 21.5 20.6-9.8-9.8 20.7L424 240l-21.5 7.7 9.8 20.6-20.6-9.8L384 280l-7.7-21.5-20.6 9.8 9.8-20.6L344 240l21.5-7.7-9.8-20.6 20.6 9.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 501 B |
@@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-br" viewBox="0 0 640 480">
|
||||
<g stroke-width="1pt">
|
||||
<path fill="#229e45" fill-rule="evenodd" d="M0 0h640v480H0z"/>
|
||||
<path fill="#f8e509" fill-rule="evenodd" d="m321.4 436 301.5-195.7L319.6 44 17.1 240.7z"/>
|
||||
<path fill="#2b49a3" fill-rule="evenodd" d="M452.8 240c0 70.3-57.1 127.3-127.6 127.3A127.4 127.4 0 1 1 452.8 240"/>
|
||||
<path fill="#ffffef" fill-rule="evenodd" d="m283.3 316.3-4-2.3-4 2 .9-4.5-3.2-3.4 4.5-.5 2.2-4 1.9 4.2 4.4.8-3.3 3m86 26.3-3.9-2.3-4 2 .8-4.5-3.1-3.3 4.5-.5 2.1-4.1 2 4.2 4.4.8-3.4 3.1m-36.2-30-3.4-2-3.5 1.8.8-3.9-2.8-2.9 4-.4 1.8-3.6 1.6 3.7 3.9.7-3 2.7m87-8.5-3.4-2-3.5 1.8.8-3.9-2.7-2.8 3.9-.4 1.8-3.5 1.6 3.6 3.8.7-2.9 2.6m-87.3-22-4-2.2-4 2 .8-4.6-3.1-3.3 4.5-.5 2.1-4.1 2 4.2 4.4.8-3.4 3.2m-104.6-35-4-2.2-4 2 1-4.6-3.3-3.3 4.6-.5 2-4.1 2 4.2 4.4.8-3.3 3.1m13.3 57.2-4-2.3-4 2 .9-4.5-3.2-3.3 4.5-.6 2.1-4 2 4.2 4.4.8-3.3 3.1m132-67.3-3.6-2-3.6 1.8.8-4-2.8-3 4-.5 1.9-3.6 1.7 3.8 4 .7-3 2.7m-6.7 38.3-2.7-1.6-2.9 1.4.6-3.2-2.2-2.3 3.2-.4 1.5-2.8 1.3 3 3 .5-2.2 2.2m-142.2 50.4-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2M419 299.8l-2.2-1.1-2.2 1 .5-2.3-1.7-1.6 2.4-.3 1.2-2 1 2 2.5.5-1.9 1.5"/>
|
||||
<path fill="#ffffef" fill-rule="evenodd" d="m219.3 287.6-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2"/>
|
||||
<path fill="#ffffef" fill-rule="evenodd" d="m219.3 287.6-2.7-1.5-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m42.3 3-2.6-1.4-2.7 1.3.6-3-2.1-2.2 3-.4 1.4-2.7 1.3 2.8 3 .5-2.3 2.1m-4.8 17-2.6-1.5-2.7 1.4.6-3-2.1-2.3 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m87.4-22.2-2.6-1.6-2.8 1.4.6-3-2-2.3 3-.3 1.4-2.7 1.2 2.8 3 .5-2.2 2.1m-25.1 3-2.7-1.5-2.7 1.4.6-3-2-2.3 3-.3 1.4-2.8 1.2 2.9 3 .5-2.2 2.1m-68.8-5.8-1.7-1-1.7.8.4-1.9-1.3-1.4 1.9-.2.8-1.7.8 1.8 1.9.3-1.4 1.3m167.8 45.4-2.6-1.5-2.7 1.4.6-3-2.1-2.3 3-.4 1.4-2.7 1.3 2.8 3 .6-2.3 2m-20.8 6-2.2-1.4-2.3 1.2.5-2.6-1.7-1.8 2.5-.3 1.2-2.3 1 2.4 2.5.4-1.9 1.8m10.4 2.3-2-1.2-2.1 1 .4-2.3-1.6-1.7 2.3-.3 1.1-2 1 2 2.3.5-1.7 1.6m29.1-22.8-2-1-2 1 .5-2.3-1.6-1.7 2.3-.3 1-2 1 2.1 2.1.4-1.6 1.6m-38.8 41.8-2.5-1.4-2.7 1.2.6-2.8-2-2 3-.3 1.3-2.5 1.2 2.6 3 .5-2.3 1.9m.6 14.2-2.4-1.4-2.4 1.3.6-2.8-1.9-2 2.7-.4 1.2-2.5 1.1 2.6 2.7.5-2 2m-19-23.1-1.9-1.2-2 1 .4-2.2-1.5-1.7 2.2-.2 1-2 1 2 2.2.4-1.6 1.6m-17.8 2.3-2-1.2-2 1 .5-2.2-1.6-1.7 2.3-.2 1-2 1 2 2.1.4-1.6 1.6m-30.4-24.6-2-1.1-2 1 .5-2.3-1.6-1.6 2.2-.3 1-2 1 2 2.2.5-1.6 1.5m3.7 57-1.6-.9-1.8.9.4-2-1.3-1.4 1.9-.2.9-1.7.8 1.8 1.9.3-1.4 1.3m-46.2-86.6-4-2.3-4 2 .9-4.5-3.2-3.3 4.5-.6 2.2-4 1.9 4.2 4.4.8-3.3 3.1"/>
|
||||
<path fill="#fff" fill-rule="evenodd" d="M444.4 285.8a125 125 0 0 0 5.8-19.8c-67.8-59.5-143.3-90-238.7-83.7a125 125 0 0 0-8.5 20.9c113-10.8 196 39.2 241.4 82.6"/>
|
||||
<path fill="#309e3a" d="m414 252.4 2.3 1.3a3 3 0 0 0-.3 2.2 3 3 0 0 0 1.4 1.7q1 .8 2 .7.9 0 1.3-.7l.2-.9-.5-1-1.5-1.8a8 8 0 0 1-1.8-3 4 4 0 0 1 2-4.4 4 4 0 0 1 2.3-.2 7 7 0 0 1 2.6 1.2q2.1 1.5 2.6 3.2a4 4 0 0 1-.6 3.3l-2.4-1.5q.5-1 .2-1.7-.2-.8-1.2-1.4a3 3 0 0 0-1.8-.7 1 1 0 0 0-.9.5q-.3.4-.1 1 .2.8 1.6 2.2t2 2.5a4 4 0 0 1-.3 4.2 4 4 0 0 1-1.9 1.5 4 4 0 0 1-2.4.3q-1.3-.3-2.8-1.3-2.2-1.5-2.7-3.3a5 5 0 0 1 .6-4zm-11.6-7.6 2.5 1.3a3 3 0 0 0-.2 2.2 3 3 0 0 0 1.4 1.6q1.1.8 2 .6.9 0 1.3-.8l.2-.8q0-.5-.5-1l-1.6-1.8q-1.7-1.6-2-2.8a4 4 0 0 1 .4-3.1 4 4 0 0 1 1.6-1.4 4 4 0 0 1 2.2-.3 7 7 0 0 1 2.6 1q2.3 1.5 2.7 3.1a4 4 0 0 1-.4 3.4l-2.5-1.4q.5-1 .2-1.7-.4-1-1.3-1.4a3 3 0 0 0-1.9-.6 1 1 0 0 0-.8.5q-.3.4-.1 1 .3.8 1.7 2.2 1.5 1.5 2 2.4a4 4 0 0 1 0 4.2 4 4 0 0 1-1.8 1.6 4 4 0 0 1-2.4.3 8 8 0 0 1-2.9-1.1 6 6 0 0 1-2.8-3.2 5 5 0 0 1 .4-4m-14.2-3.8 7.3-12 8.8 5.5-1.2 2-6.4-4-1.6 2.7 6 3.7-1.3 2-6-3.7-2 3.3 6.7 4-1.2 2zm-20.7-17 1.1-2 5.4 2.7-2.5 5q-1.2.3-3 .2a9 9 0 0 1-3.3-1 8 8 0 0 1-3-2.6 6 6 0 0 1-1-3.5 9 9 0 0 1 1-3.7 8 8 0 0 1 2.6-3 6 6 0 0 1 3.6-1.1q1.4 0 3.2 1 2.4 1.1 3.1 2.8a5 5 0 0 1 .3 3.5l-2.7-.8a3 3 0 0 0-.2-2q-.4-.9-1.6-1.4a4 4 0 0 0-3.1-.3q-1.5.5-2.6 2.6t-.7 3.8a4 4 0 0 0 2 2.4q.8.5 1.7.5h1.8l.8-1.6zm-90.2-22.3 2-14 4.2.7 1.1 9.8 3.9-9 4.2.6-2 13.8-2.7-.4 1.7-10.9-4.4 10.5-2.7-.4-1.1-11.3-1.6 11zm-14.1-1.7 1.3-14 10.3 1-.2 2.4-7.5-.7-.3 3 7 .7-.3 2.4-7-.7-.3 3.8 7.8.7-.2 2.4z"/>
|
||||
<g stroke-opacity=".5">
|
||||
<path fill="#309e3a" d="M216.5 191.3q0-2.2.7-3.6a7 7 0 0 1 1.4-1.9 5 5 0 0 1 1.8-1.2q1.5-.5 3-.5 3.1.1 5 2a7 7 0 0 1 1.6 5.5q0 3.3-2 5.3a7 7 0 0 1-5 1.7 7 7 0 0 1-4.8-2 7 7 0 0 1-1.7-5.3"/>
|
||||
<path fill="#f7ffff" d="M219.4 191.3q0 2.3 1 3.6t2.8 1.3a4 4 0 0 0 2.8-1.1q1-1.2 1.1-3.7.1-2.4-1-3.6a4 4 0 0 0-2.7-1.3 4 4 0 0 0-2.8 1.2q-1.1 1.2-1.2 3.6"/>
|
||||
</g>
|
||||
<g stroke-opacity=".5">
|
||||
<path fill="#309e3a" d="m233 198.5.2-14h6q2.2 0 3.2.5 1 .3 1.6 1.3c.6 1 .6 1.4.6 2.3a4 4 0 0 1-1 2.6 5 5 0 0 1-2.7 1.2l1.5 1.2q.6.6 1.5 2.3l1.7 2.8h-3.4l-2-3.2-1.4-2-.9-.6-1.4-.2h-.6v5.8z"/>
|
||||
<path fill="#fff" d="M236 190.5h2q2.1 0 2.6-.2.5-.1.8-.5.4-.6.3-1 0-.9-.4-1.2-.3-.4-1-.6h-2l-2.3-.1z"/>
|
||||
</g>
|
||||
<g stroke-opacity=".5">
|
||||
<path fill="#309e3a" d="m249 185.2 5.2.3q1.7 0 2.6.3a5 5 0 0 1 2 1.4 6 6 0 0 1 1.2 2.4q.4 1.4.3 3.3a9 9 0 0 1-.5 3q-.6 1.5-1.7 2.4a5 5 0 0 1-2 1q-1 .3-2.5.2l-5.3-.3z"/>
|
||||
<path fill="#fff" d="m251.7 187.7-.5 9.3h3.8q.8 0 1.2-.5.5-.4.8-1.3t.4-2.6l-.1-2.5a3 3 0 0 0-.8-1.4l-1.2-.7-2.3-.3z"/>
|
||||
</g>
|
||||
<g stroke-opacity=".5">
|
||||
<path fill="#309e3a" d="m317.6 210.2 3.3-13.6 4.4 1 3.2 1q1.1.6 1.6 1.9t.2 2.8q-.3 1.2-1 2a4 4 0 0 1-3 1.4q-1 0-3-.5l-1.7-.5-1.2 5.2z"/>
|
||||
<path fill="#fff" d="m323 199.6-.8 3.8 1.5.4q1.6.4 2.2.3a2 2 0 0 0 1.6-1.5q0-.7-.2-1.3a2 2 0 0 0-1-.9l-1.9-.5-1.3-.3z"/>
|
||||
</g>
|
||||
<g stroke-opacity=".5">
|
||||
<path fill="#309e3a" d="m330.6 214.1 4.7-13.2 5.5 2q2.2.8 3 1.4.8.7 1 1.8c.2 1.1.2 1.5 0 2.3q-.6 1.5-1.8 2.2-1.2.6-3 .3.6.7 1 1.6l.8 2.7.6 3.1-3.1-1.1-1-3.6-.7-2.4-.6-.8q-.3-.4-1.3-.7l-.5-.2-2 5.6z"/>
|
||||
<path fill="#fff" d="m336 207.4 1.9.7q2 .7 2.5.7t.9-.3q.5-.3.6-.9.3-.6 0-1.2l-.8-.9-2-.7-2-.7-1.2 3.3z"/>
|
||||
</g>
|
||||
<g stroke-opacity=".5">
|
||||
<path fill="#309e3a" d="M347 213.6a9 9 0 0 1 1.7-3.2l1.8-1.5 2-.7q1.5-.1 3.1.4a7 7 0 0 1 4.2 3.3q1.2 2.4.2 5.7a7 7 0 0 1-3.4 4.5q-2.3 1.3-5.2.4a7 7 0 0 1-4.2-3.3 7 7 0 0 1-.2-5.6"/>
|
||||
<path fill="#fff" d="M349.8 214.4q-.7 2.3 0 3.8c.7 1.5 1.2 1.6 2.3 2q1.5.5 3-.4 1.4-.8 2.1-3.2.8-2.2 0-3.7a4 4 0 0 0-2.2-2 4 4 0 0 0-3 .3q-1.5.8-2.2 3.2"/>
|
||||
</g>
|
||||
<g stroke-opacity=".5">
|
||||
<path fill="#309e3a" d="m374.3 233.1 6.4-12.4 5.3 2.7a10 10 0 0 1 2.7 1.9q.8.7.8 1.9c0 1.2 0 1.5-.4 2.2a4 4 0 0 1-2 2q-1.5.4-3.1-.2.6 1 .8 1.7.3.9.4 2.8l.2 3.2-3-1.5-.4-3.7-.3-2.5-.5-1-1.2-.7-.5-.3-2.7 5.2z"/>
|
||||
<path fill="#fff" d="m380.5 227.2 1.9 1q1.8 1 2.3 1t1-.2q.4-.2.7-.8t.2-1.2l-.7-1-1.8-1-2-1z"/>
|
||||
</g>
|
||||
<g stroke-opacity=".5">
|
||||
<path fill="#309e3a" d="M426.1 258.7a9 9 0 0 1 2.5-2.6 7 7 0 0 1 2.2-.9 6 6 0 0 1 2.2 0q1.5.3 2.8 1.2a7 7 0 0 1 3 4.4q.4 2.6-1.4 5.5a7 7 0 0 1-4.5 3.3 7 7 0 0 1-5.2-1.1 7 7 0 0 1-3-4.4q-.4-2.7 1.4-5.4"/>
|
||||
<path fill="#fff" d="M428.6 260.3q-1.4 2-1.1 3.6a4 4 0 0 0 1.6 2.5q1.5 1 3 .6t2.9-2.4q1.4-2.1 1.1-3.6t-1.6-2.6c-1.4-1.1-2-.8-3-.5q-1.5.3-3 2.4z"/>
|
||||
</g>
|
||||
<path fill="#309e3a" d="m301.8 204.5 2.3-9.8 7.2 1.7-.3 1.6-5.3-1.2-.5 2.2 4.9 1.1-.4 1.7-4.9-1.2-.6 2.7 5.5 1.3-.4 1.6z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-cn" viewBox="0 0 640 480">
|
||||
<defs>
|
||||
<path id="cn-a" fill="#ff0" d="M-.6.8 0-1 .6.8-1-.3h2z"/>
|
||||
</defs>
|
||||
<path fill="#ee1c25" d="M0 0h640v480H0z"/>
|
||||
<use xlink:href="#cn-a" width="30" height="20" transform="matrix(71.9991 0 0 72 120 120)"/>
|
||||
<use xlink:href="#cn-a" width="30" height="20" transform="matrix(-12.33562 -20.5871 20.58684 -12.33577 240.3 48)"/>
|
||||
<use xlink:href="#cn-a" width="30" height="20" transform="matrix(-3.38573 -23.75998 23.75968 -3.38578 288 95.8)"/>
|
||||
<use xlink:href="#cn-a" width="30" height="20" transform="matrix(6.5991 -23.0749 23.0746 6.59919 288 168)"/>
|
||||
<use xlink:href="#cn-a" width="30" height="20" transform="matrix(14.9991 -18.73557 18.73533 14.99929 240 216)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 813 B |
@@ -0,0 +1,544 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-es" viewBox="0 0 640 480">
|
||||
<path fill="#AA151B" d="M0 0h640v480H0z"/>
|
||||
<path fill="#F1BF00" d="M0 120h640v240H0z"/>
|
||||
<path fill="#ad1519" d="m127.3 213.3-.8-.1-1-1-.7-.4-.6-.8s-.7-1.1-.4-2q.5-1.2 1.4-1.5l1.5-.5 1-.4 1.3-.3.5-.3 1-.2 1-.2 1.6.1h4.8c.4 0 1.2.3 1.4.4l2 .7c.5.1 1.6.3 2.2.6q.8.5 1.1 1l.5 1v1.1l-.5.8-.6 1-.8.6s-.5.5-1 .4-4.8-.8-7.6-.8-7.3.9-7.3.9"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" d="m127.3 213.3-.8-.1-1-1-.7-.4-.6-.8s-.7-1.1-.4-2q.5-1.2 1.4-1.5l1.5-.5 1-.4 1.3-.3.5-.3 1-.2 1-.2 1.6.1h4.8c.4 0 1.2.3 1.4.4l2 .7c.5.1 1.6.3 2.2.6q.8.5 1.1 1l.5 1v1.1l-.5.8-.6 1-.8.6s-.5.5-1 .4-4.8-.8-7.6-.8-7.3.9-7.3.9z"/>
|
||||
<path fill="#c8b100" d="M133.3 207q.1-2.1 1.3-2.3 1.3.1 1.4 2.4c0 1.3-.6 2.4-1.4 2.4s-1.3-1.1-1.3-2.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M133.3 207q.1-2.1 1.3-2.3 1.3.1 1.4 2.4c0 1.3-.6 2.4-1.4 2.4s-1.3-1.1-1.3-2.5z"/>
|
||||
<path fill="#c8b100" d="M134 207q0-2 .7-2.1c.3 0 .6 1 .6 2.1q0 2-.6 2.2c-.4 0-.6-1-.6-2.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M134 207q0-2 .7-2.1c.3 0 .6 1 .6 2.1q0 2-.6 2.2c-.4 0-.6-1-.6-2.2z"/>
|
||||
<path fill="#c8b100" d="M133.8 204.5q.1-.7.8-.8 1 0 1 .8-.1.8-1 .9c-.9.1-.8-.4-.8-.9"/>
|
||||
<path fill="#c8b100" d="M135.3 204.2v.6h-1.4v-.6h.5V203h-.7v-.6h.7v-.5h.5v.5h.6v.6h-.6v1.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M135.3 204.2v.6h-1.4v-.6h.5V203h-.7v-.6h.7v-.5h.5v.5h.6v.6h-.6v1.2h.4"/>
|
||||
<path fill="#c8b100" d="M135.9 204.2v.6h-2.5v-.6h1V203h-.7v-.6h.7v-.5h.5v.5h.6v.6h-.6v1.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M135.9 204.2v.6h-2.5v-.6h1V203h-.7v-.6h.7v-.5h.5v.5h.6v.6h-.6v1.2h1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M134.9 203.7q.6.2.6.8-.1.8-.8.9-1 0-1-.9 0-.6.7-.8"/>
|
||||
<path fill="#c8b100" d="M134.7 213.2H130v-1.1l-.3-1.2-.2-1.5c-1.3-1.7-2.5-2.8-2.9-2.5q.1-.5.5-.7c1.1-.7 3.5 1 5.2 3.6l.5.7h3.8l.4-.7c1.8-2.7 4.1-4.3 5.2-3.6q.4.2.5.7c-.4-.3-1.6.8-2.9 2.5l-.2 1.5-.2 1.2-.1 1.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M134.7 213.2H130v-1.1l-.3-1.2-.2-1.5c-1.3-1.7-2.5-2.8-2.9-2.5q.1-.5.5-.7c1.1-.7 3.5 1 5.2 3.6l.5.7h3.8l.4-.7c1.8-2.7 4.1-4.3 5.2-3.6q.4.2.5.7c-.4-.3-1.6.8-2.9 2.5l-.2 1.5-.2 1.2-.1 1.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M126.8 206.8c1-.5 3 1.1 4.6 3.6m11-3.6c-.8-.5-2.8 1.1-4.5 3.6"/>
|
||||
<path fill="#c8b100" d="m127.8 215.3-.5-1a27 27 0 0 1 14.7 0l-.5.8-.3.8a23 23 0 0 0-6.6-.8c-2.6 0-5.2.3-6.5.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m127.8 215.3-.5-1a27 27 0 0 1 14.7 0l-.5.8-.3.8a23 23 0 0 0-6.6-.8c-2.6 0-5.2.3-6.5.8l-.3-.6"/>
|
||||
<path fill="#c8b100" d="M134.6 217.7c2.4 0 5-.4 5.9-.6q1-.3 1-.8 0-.3-.4-.4c-1.4-.5-4-.8-6.5-.8s-5 .3-6.4.8q-.3 0-.4.3 0 .6 1 .9c1 .2 3.5.6 5.8.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M134.6 217.7c2.4 0 5-.4 5.9-.6q1-.3 1-.8 0-.3-.4-.4c-1.4-.5-4-.8-6.5-.8s-5 .3-6.4.8q-.3 0-.4.3 0 .6 1 .9c1 .2 3.5.6 5.8.6z"/>
|
||||
<path fill="#c8b100" d="m142.1 213.2-.5-.5s-.6.3-1.3.2c-.6 0-.9-1-.9-1s-.7.7-1.3.7-1-.6-1-.6-.7.5-1.3.4-1.2-.8-1.2-.8-.6.8-1.2.8c-.6.1-1-.5-1-.5s-.4.6-1.1.7-1.4-.6-1.4-.6-.5.7-1 1c-.5 0-1.2-.4-1.2-.4l-.2.5-.3.1.2.5a27 27 0 0 1 7.2-.9q4.5 0 7.4 1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m142.1 213.2-.5-.5s-.6.3-1.3.2c-.6 0-.9-1-.9-1s-.7.7-1.3.7-1-.6-1-.6-.7.5-1.3.4-1.2-.8-1.2-.8-.6.8-1.2.8c-.6.1-1-.5-1-.5s-.4.6-1.1.7-1.4-.6-1.4-.6-.5.7-1 1c-.5 0-1.2-.4-1.2-.4l-.2.5-.3.1.2.5a27 27 0 0 1 7.2-.9q4.5 0 7.4 1z"/>
|
||||
<path fill="#c8b100" d="M134.7 210.7h.2v.4q0 1 1 1a1 1 0 0 0 1-.7l.2-.3v.4q.3.8 1.1.8 1 0 1-1v-.1l.4-.4.2.5-.1.4a1 1 0 0 0 1 1q.6 0 .9-.5l.2-.2v.3q0 .5.4.7l1-.4.7-.7v.4s-.5.8-1 1q-.3.4-.8.3-.5 0-.7-.6-.3.3-.7.2-1 0-1.4-.8-.5.5-1.1.5a2 2 0 0 1-1.2-.6l-1 .4a2 2 0 0 1-1.3-.6 1.6 1.6 0 0 1-2.4.2 2 2 0 0 1-1.2.6l-1.1-.5q-.4.8-1.4.8l-.7-.2q-.2.5-.7.6t-.9-.2l-1-1 .1-.5.8.7q.8.5.9.4.4-.1.4-.7v-.3l.2.2q.3.5.9.5a1 1 0 0 0 1-1v-.9l.4.4v.1q.1 1 1 1t1.1-.9v-.3l.2.3q.3.6 1 .7 1 0 1.1-1v-.3h.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M134.7 210.7h.2v.4q0 1 1 1a1 1 0 0 0 1-.7l.2-.3v.4q.3.8 1.1.8 1 0 1-1v-.1l.4-.4.2.5-.1.4a1 1 0 0 0 1 1q.6 0 .9-.5l.2-.2v.3q0 .5.4.7l1-.4.7-.7v.4s-.5.8-1 1q-.3.4-.8.3-.5 0-.7-.6-.3.3-.7.2-1 0-1.4-.8-.5.5-1.1.5a2 2 0 0 1-1.2-.6l-1 .4a2 2 0 0 1-1.3-.6 1.6 1.6 0 0 1-2.4.2 2 2 0 0 1-1.2.6l-1.1-.5q-.4.8-1.4.8l-.7-.2q-.2.5-.7.6t-.9-.2l-1-1 .1-.5.8.7q.8.5.9.4.4-.1.4-.7v-.3l.2.2q.3.5.9.5a1 1 0 0 0 1-1v-.9l.4.4v.1q.1 1 1 1t1.1-.9v-.3l.2.3q.3.6 1 .7 1 0 1.1-1v-.3h.3z"/>
|
||||
<path fill="#c8b100" d="M134.6 213.3q-4.5 0-7.3 1l-.3-.2.1-.3a27 27 0 0 1 7.5-1q4.6 0 7.6 1l.1.3-.3.2a27 27 0 0 0-7.4-1"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" d="M134.6 213.3q-4.5 0-7.3 1l-.3-.2.1-.3a27 27 0 0 1 7.5-1q4.6 0 7.6 1l.1.3-.3.2a27 27 0 0 0-7.4-1z"/>
|
||||
<path fill="#fff" d="M131.8 214.4q0-.4.5-.4a.4.4 0 0 1 .4.4q0 .3-.4.4a.4.4 0 0 1-.5-.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M131.8 214.4q0-.4.5-.4a.4.4 0 0 1 .4.4q0 .3-.4.4a.4.4 0 0 1-.5-.4z"/>
|
||||
<path fill="#ad1519" d="M134.7 214.5h-1q-.2 0-.3-.3l.3-.3h2l.2.3-.3.3h-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M134.7 214.5h-1q-.2 0-.3-.3l.3-.3h2l.2.3-.3.3h-1"/>
|
||||
<path fill="#058e6e" d="M130 214.9h-.7q-.2 0-.3-.2l.2-.3.7-.1.7-.1q.3 0 .4.2a.3.3 0 0 1-.3.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M130 214.9h-.7q-.2 0-.3-.2l.2-.3.7-.1.7-.1q.3 0 .4.2a.3.3 0 0 1-.3.4h-.7"/>
|
||||
<path fill="#ad1519" d="m127.3 215.3.3-.4h.7l-.4.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m127.3 215.3.3-.4h.7l-.4.6-.6-.2"/>
|
||||
<path fill="#fff" d="M136.6 214.4q0-.4.4-.4a.4.4 0 0 1 .5.4.4.4 0 0 1-.5.4.4.4 0 0 1-.4-.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M136.6 214.4q0-.4.4-.4a.4.4 0 0 1 .5.4.4.4 0 0 1-.5.4.4.4 0 0 1-.4-.4z"/>
|
||||
<path fill="#058e6e" d="M139.3 214.9h.6a.3.3 0 0 0 .4-.2l-.3-.3-.6-.1-.7-.1q-.3 0-.4.2 0 .3.3.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M139.3 214.9h.6a.3.3 0 0 0 .4-.2l-.3-.3-.6-.1-.7-.1q-.3 0-.4.2 0 .3.3.4h.7"/>
|
||||
<path fill="#ad1519" d="m142 215.4-.3-.5h-.7l.3.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m142 215.4-.3-.5h-.7l.3.6.6-.1"/>
|
||||
<path fill="#ad1519" d="M134.6 217.1a25 25 0 0 1-6-.6 26 26 0 0 1 12.1 0q-2.5.6-6 .6"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" d="M134.6 217.1a25 25 0 0 1-6-.6 26 26 0 0 1 12.1 0q-2.5.6-6 .6z"/>
|
||||
<path fill="#c8b100" d="m142 212-.1-.3q-.3 0-.4.2 0 .4.2.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m142 212-.1-.3q-.3 0-.4.2 0 .4.2.4z"/>
|
||||
<path fill="#c8b100" d="M137.3 211.2q0-.3-.2-.4l-.2.3q0 .3.2.4l.3-.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M137.3 211.2q0-.3-.2-.4l-.2.3q0 .3.2.4l.3-.3z"/>
|
||||
<path fill="#c8b100" d="m132 211.2.1-.4q.3 0 .3.3t-.2.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m132 211.2.1-.4q.3 0 .3.3t-.2.4z"/>
|
||||
<path fill="#c8b100" d="m127.3 212 .1-.3q.3 0 .4.2 0 .4-.2.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m127.3 212 .1-.3q.3 0 .4.2 0 .4-.2.4z"/>
|
||||
<path fill="#c8b100" d="m134.6 208.5-.8.5.6 1.3.2.1.2-.1.7-1.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m134.6 208.5-.8.5.6 1.3.2.1.2-.1.7-1.3-.9-.5"/>
|
||||
<path fill="#c8b100" d="m132.8 210.5.4.5 1.3-.4.1-.2-.1-.2-1.3-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m132.8 210.5.4.5 1.3-.4.1-.2-.1-.2-1.3-.3-.4.6"/>
|
||||
<path fill="#c8b100" d="m136.4 210.5-.3.5-1.3-.4-.2-.2.2-.2 1.3-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m136.4 210.5-.3.5-1.3-.4-.2-.2.2-.2 1.3-.3.3.6"/>
|
||||
<path fill="#c8b100" d="m129.3 209-.7.7.9 1 .2.1.1-.1.3-1.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m129.3 209-.7.7.9 1 .2.1.1-.1.3-1.3-.8-.3"/>
|
||||
<path fill="#c8b100" d="m128 211.2.4.5 1.2-.6v-.2l-.1-.2-1.3-.1-.3.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m128 211.2.4.5 1.2-.6v-.2l-.1-.2-1.3-.1-.3.6"/>
|
||||
<path fill="#c8b100" d="m131.5 210.5-.3.6H130l-.2-.2.1-.3 1.2-.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m131.5 210.5-.3.6H130l-.2-.2.1-.3 1.2-.6.5.5"/>
|
||||
<path fill="#c8b100" d="M126.6 211.4v.6l-1.4.2-.2-.1v-.2l1-.9z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M126.6 211.4v.6l-1.4.2-.2-.1v-.2l1-.9.6.4"/>
|
||||
<path fill="#c8b100" d="M129.2 210.9q0-.5.5-.5t.5.5l-.5.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M129.2 210.9q0-.5.5-.5t.5.5l-.5.4z"/>
|
||||
<path fill="#c8b100" d="m140 209 .7.7-.9 1-.2.1-.1-.1-.3-1.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m140 209 .7.7-.9 1-.2.1-.1-.1-.3-1.3.8-.3"/>
|
||||
<path fill="#c8b100" d="m141.4 211.2-.5.5-1.2-.6v-.2l.1-.2 1.3-.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m141.4 211.2-.5.5-1.2-.6v-.2l.1-.2 1.3-.1.3.6"/>
|
||||
<path fill="#c8b100" d="m137.8 210.5.3.6h1.3l.2-.2-.1-.3-1.2-.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m137.8 210.5.3.6h1.3l.2-.2-.1-.3-1.2-.6-.5.5"/>
|
||||
<path fill="#c8b100" d="m142.5 211.4.1.6 1.3.2.2-.1v-.2l-1-.9z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m142.5 211.4.1.6 1.3.2.2-.1v-.2l-1-.9-.6.4"/>
|
||||
<path fill="#c8b100" d="m134.2 210.4.4-.4q.5 0 .5.4l-.5.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m134.2 210.4.4-.4q.5 0 .5.4l-.5.5z"/>
|
||||
<path fill="#c8b100" d="M139.1 210.9q.1-.5.5-.5l.5.5-.5.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M139.1 210.9q.1-.5.5-.5l.5.5-.5.4z"/>
|
||||
<path fill="#c8b100" d="m124.8 212.2-.6-.7c-.2-.2-.7-.3-.7-.3q0-.2.6-.3l.4.2v-.2s.3 0 .4.3v1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m124.8 212.2-.6-.7c-.2-.2-.7-.3-.7-.3q0-.2.6-.3l.4.2v-.2s.3 0 .4.3v1z"/>
|
||||
<path fill="#c8b100" d="M124.8 212q.3-.3.5 0t.2.5l-.5-.1q-.3-.2-.2-.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M124.8 212q.3-.3.5 0t.2.5l-.5-.1q-.3-.2-.2-.5z"/>
|
||||
<path fill="#c8b100" d="m144.3 212.2.6-.7.7-.3q0-.2-.6-.3l-.4.2v-.2s-.3 0-.4.3v.7z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m144.3 212.2.6-.7.7-.3q0-.2-.6-.3l-.4.2v-.2s-.3 0-.4.3v.7z"/>
|
||||
<path fill="#c8b100" d="M144.3 212q-.1-.3-.5 0-.3.2-.1.5l.5-.1q.3-.2.1-.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M144.3 212q-.1-.3-.5 0-.3.2-.1.5l.5-.1q.3-.2.1-.5z"/>
|
||||
<path fill="#c8b100" d="M124 223h21.4v-5.5H124z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M124 223h21.4v-5.5H124z"/>
|
||||
<path fill="#c8b100" d="M126.2 226.8h16.9a1.4 1.4 0 0 1-1-1.2q.1-1 1-1.3h-16.9q1 .3 1 1.3a1.3 1.3 0 0 1-1 1.2"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="M126.2 226.8h16.9a1.4 1.4 0 0 1-1-1.2q.1-1 1-1.3h-16.9q1 .3 1 1.3a1.3 1.3 0 0 1-1 1.2z"/>
|
||||
<path fill="#c8b100" d="M126.6 226.8h16q1 0 1 .7t-1 .8h-16q-.9-.1-1-.8 0-1 1-.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M126.6 226.8h16q1 0 1 .7t-1 .8h-16q-.9-.1-1-.8 0-1 1-.8z"/>
|
||||
<path fill="#c8b100" d="M126.6 223h16q1 .1 1 .7t-1 .6h-16q-.9 0-1-.6.1-.5 1-.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M126.6 223h16q1 .1 1 .7t-1 .6h-16q-.9 0-1-.6.1-.5 1-.6z"/>
|
||||
<path fill="#005bbf" d="M149.6 317.4q-2.2 0-3.7-.8a8 8 0 0 0-3.8-.8q-2.1 0-3.7.8a8 8 0 0 1-3.8.8q-2.3 0-3.7-.8a8 8 0 0 0-3.7-.8 8 8 0 0 0-3.7.8 8 8 0 0 1-3.8.8v2.4q2.3 0 3.8-.9a8 8 0 0 1 3.7-.8q2.2 0 3.7.8c1.5.8 2.2.9 3.7.9a8 8 0 0 0 3.8-.9q1.6-.8 3.7-.8 2.3 0 3.8.8c1.5.8 2.2.9 3.7.9z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M149.6 317.4q-2.2 0-3.7-.8a8 8 0 0 0-3.8-.8q-2.1 0-3.7.8a8 8 0 0 1-3.8.8q-2.3 0-3.7-.8a8 8 0 0 0-3.7-.8 8 8 0 0 0-3.7.8 8 8 0 0 1-3.8.8v2.4q2.3 0 3.8-.9a8 8 0 0 1 3.7-.8q2.2 0 3.7.8c1.5.8 2.2.9 3.7.9a8 8 0 0 0 3.8-.9q1.6-.8 3.7-.8 2.3 0 3.8.8c1.5.8 2.2.9 3.7.9z"/>
|
||||
<path fill="#ccc" d="M149.6 319.8a8 8 0 0 1-3.7-.9 8 8 0 0 0-3.8-.8q-2.1 0-3.7.8c-1.6.8-2.3.9-3.8.9s-2.8-.4-3.7-.9a8 8 0 0 0-3.7-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.9v2.3q2.3 0 3.8-.9a8 8 0 0 1 3.7-.7q2.2 0 3.7.7a8 8 0 0 0 7.5 0 9 9 0 0 1 7.5.1 8 8 0 0 0 3.7.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M149.6 319.8a8 8 0 0 1-3.7-.9 8 8 0 0 0-3.8-.8q-2.1 0-3.7.8c-1.6.8-2.3.9-3.8.9s-2.8-.4-3.7-.9a8 8 0 0 0-3.7-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.9v2.3q2.3 0 3.8-.9a8 8 0 0 1 3.7-.7q2.2 0 3.7.7a8 8 0 0 0 7.5 0 9 9 0 0 1 7.5.1 8 8 0 0 0 3.7.8v-2.3"/>
|
||||
<path fill="#005bbf" d="M149.6 322a7 7 0 0 1-3.7-.8 8 8 0 0 0-3.8-.7q-2.1 0-3.7.7-1.5.9-3.8.9c-2.3 0-2.8-.4-3.7-.9a8 8 0 0 0-3.7-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.9v2.3q2.3 0 3.8-.9a10 10 0 0 1 7.4 0 7 7 0 0 0 3.7.9 8 8 0 0 0 3.8-.8q1.6-.8 3.7-.8 2.3 0 3.8.8c1.5.8 2.2.8 3.7.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M149.6 322a7 7 0 0 1-3.7-.8 8 8 0 0 0-3.8-.7q-2.1 0-3.7.7-1.5.9-3.8.9c-2.3 0-2.8-.4-3.7-.9a8 8 0 0 0-3.7-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.9v2.3q2.3 0 3.8-.9a10 10 0 0 1 7.4 0 7 7 0 0 0 3.7.9 8 8 0 0 0 3.8-.8q1.6-.8 3.7-.8 2.3 0 3.8.8c1.5.8 2.2.8 3.7.8V322"/>
|
||||
<path fill="#ccc" d="M149.6 326.7a8 8 0 0 1-3.7-.8q-1.6-.8-3.7-.8a8 8 0 0 0-3.8.8q-1.5.8-3.8.8a7 7 0 0 1-3.7-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.7.8c-1.5.8-2.3.8-3.8.8v-2.3a8 8 0 0 0 3.8-.9 10 10 0 0 1 7.4 0 8 8 0 0 0 3.7.9 8 8 0 0 0 3.8-.8q1.5-.8 3.8-.8t3.7.8c1.5.8 2.3.8 3.7.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M149.6 326.7a8 8 0 0 1-3.7-.8q-1.6-.8-3.7-.8a8 8 0 0 0-3.8.8q-1.5.8-3.8.8a7 7 0 0 1-3.7-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.7.8c-1.5.8-2.3.8-3.8.8v-2.3a8 8 0 0 0 3.8-.9 10 10 0 0 1 7.4 0 8 8 0 0 0 3.7.9 8 8 0 0 0 3.8-.8q1.5-.8 3.8-.8t3.7.8c1.5.8 2.3.8 3.7.8v2.3"/>
|
||||
<path fill="#005bbf" d="M149.6 329a8 8 0 0 1-3.7-.8q-1.6-.8-3.7-.8a8 8 0 0 0-3.8.8q-1.5.8-3.8.8a7 7 0 0 1-3.7-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.7.8c-1.5.8-2.3.8-3.8.8v-2.3a8 8 0 0 0 3.8-.8q1.5-.8 3.7-.8t3.7.7a8 8 0 0 0 7.5 0q1.5-.6 3.8-.7t3.7.8c1.5.8 2.2.8 3.7.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M149.6 329a8 8 0 0 1-3.7-.8q-1.6-.8-3.7-.8a8 8 0 0 0-3.8.8q-1.5.8-3.8.8a7 7 0 0 1-3.7-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.7.8c-1.5.8-2.3.8-3.8.8v-2.3a8 8 0 0 0 3.8-.8q1.5-.8 3.7-.8t3.7.7a8 8 0 0 0 7.5 0q1.5-.6 3.8-.7t3.7.8c1.5.8 2.2.8 3.7.8z"/>
|
||||
<path fill="#c8b100" d="m126.2 308 .2.5c0 1.5-1.3 2.6-2.7 2.6h22a3 3 0 0 1-2.7-2.6v-.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="m126.2 308 .2.5c0 1.5-1.3 2.6-2.7 2.6h22a3 3 0 0 1-2.7-2.6v-.5z"/>
|
||||
<path fill="#c8b100" d="M126.6 306.5h16q1 0 1 .8 0 .6-1 .7h-16q-.9 0-1-.8.1-.6 1-.7"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M126.6 306.5h16q1 0 1 .8 0 .6-1 .7h-16q-.9 0-1-.8.1-.6 1-.7z"/>
|
||||
<path fill="#c8b100" d="M123.7 316.7h22V311h-22z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M123.7 316.7h22V311h-22z"/>
|
||||
<path fill="#ad1519" d="M122 286.7c-2.2 1.2-3.7 2.5-3.4 3.2q.2.8 1.8 1.6c1.5 1.1 2.5 3 1.7 4a5.5 5.5 0 0 0-.1-8.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M122 286.7c-2.2 1.2-3.7 2.5-3.4 3.2q.2.8 1.8 1.6c1.5 1.1 2.5 3 1.7 4a5.5 5.5 0 0 0-.1-8.8z"/>
|
||||
<path fill="#ccc" d="M126.8 305.6h15.6V229h-15.6v76.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M138 229.2v76.3m1.7-76.3v76.3m-12.9 0h15.6v-76.4h-15.6v76.5z"/>
|
||||
<path fill="#ad1519" d="M158.4 257.7a50 50 0 0 0-23.3-2c-9.4 1.6-16.5 5.3-15.9 8.4v.2l-3.5-8.2c-.6-3.3 7.2-7.5 17.6-9.2a43 43 0 0 1 9.2-.7c6.6 0 12.4.8 15.8 2.1v9.4"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="M158.4 257.7a50 50 0 0 0-23.3-2c-9.4 1.6-16.5 5.3-15.9 8.4v.2l-3.5-8.2c-.6-3.3 7.2-7.5 17.6-9.2a43 43 0 0 1 9.2-.7c6.6 0 12.4.8 15.8 2.1v9.4"/>
|
||||
<path fill="#ad1519" d="M126.8 267.3c-4.3-.3-7.3-1.4-7.6-3.2q-.3-2.2 3.8-4.5l3.8.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M126.8 267.3c-4.3-.3-7.3-1.4-7.6-3.2q-.3-2.2 3.8-4.5l3.8.3v7.4"/>
|
||||
<path fill="#ad1519" d="M142.5 261.5q4.1.6 5.7 1.9l.1.2c.5 1-1.9 3-5.9 5.4v-7.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M142.5 261.5q4.1.6 5.7 1.9l.1.2c.5 1-1.9 3-5.9 5.4v-7.5"/>
|
||||
<path fill="#ad1519" d="M117.1 282c-.4-1.2 3.8-3.6 9.8-5.8l7.8-3.2c8.3-3.7 14.4-7.9 13.6-9.4v-.2c.4.4 1 8 1 8 .8 1.3-4.8 5.5-12.4 9.1-2.5 1.2-7.6 3-10 4-4.4 1.4-8.7 4.3-8.3 5.3l-1.5-7.7"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="M117.1 282c-.4-1.2 3.8-3.6 9.8-5.8l7.8-3.2c8.3-3.7 14.4-7.9 13.6-9.4v-.2c.4.4 1 8 1 8 .8 1.3-4.8 5.5-12.4 9.1-2.5 1.2-7.6 3-10 4-4.4 1.4-8.7 4.3-8.3 5.3l-1.5-7.7z"/>
|
||||
<path fill="#c8b100" d="M125.8 254c1.9-.6 3.1-1.5 2.5-3q-.6-1.3-2.8-.6l-2.6 1 2.3 5.8.8-.3.8-.3zm-1.2-2.7.7-.3q1-.3 1.4.8.4.8-.5 1.5l-.6.3zm7.3-2.5-.9.3h-.8l1.3 6.1 4.3-.8-.2-.4v-.4l-2.5.6zm8.4 5.2q1.2-3.3 2.7-6.4h-1l-1.8 4.6-2.4-4.3-1 .1h-1l3.5 6zm8.8-4.7.4-.9a3 3 0 0 0-1.7-.6q-2.6 0-2.8 1.7c-.2 2.1 3.2 2 3 3.4q-.2 1-1.4.8-1.3 0-1.4-1.2h-.3l-.4 1.1a4 4 0 0 0 1.8.6q2.7.2 3.2-1.7c.2-2-3.3-2.1-3.1-3.4q0-.8 1.3-.7 1 0 1.2.9z"/>
|
||||
<path fill="#ad1519" d="M277.9 211.6s-.7.8-1.3.9c-.5 0-1.1-.5-1.1-.5s-.5.5-1 .6-1.4-.6-1.4-.6l-1 1-1.1-.3c-.1-.1-.3.4-.7.6h-.4l-.6-.4-.7-.7-.5-.3-.4-1v-.5q0-1 2.2-1.7a4 4 0 0 1 2 0q.9-.8 3-.8c2.1 0 2.4.3 3 .7a6 6 0 0 1 2.9-.7q2.1 0 3 .8.8-.3 2 0 2.2.6 2.2 1.7v.5l-.4 1-.6.3-.6.7-.6.3s-.3.2-.4 0q-.6-.3-.7-.5c-.1-.2-.6.4-1 .2s-1-1-1-1-.9.8-1.4.7c-.6-.1-1-.6-1-.6s-.7.6-1.2.5-1.2-.9-1.2-.9"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M277.9 211.6s-.7.8-1.3.9c-.5 0-1.1-.5-1.1-.5s-.5.5-1 .6-1.4-.6-1.4-.6l-1 1-1.1-.3c-.1-.1-.3.4-.7.6h-.4l-.6-.4-.7-.7-.5-.3-.4-1v-.5q0-1 2.2-1.7a4 4 0 0 1 2 0q.9-.8 3-.8c2.1 0 2.4.3 3 .7a6 6 0 0 1 2.9-.7q2.1 0 3 .8.8-.3 2 0 2.2.6 2.2 1.7v.5l-.4 1-.6.3-.6.7-.6.3s-.3.2-.4 0q-.6-.3-.7-.5c-.1-.2-.6.4-1 .2s-1-1-1-1-.9.8-1.4.7c-.6-.1-1-.6-1-.6s-.7.6-1.2.5-1.2-.9-1.2-.9z"/>
|
||||
<path fill="#c8b100" d="M276.5 207.6c0-1 .6-2 1.3-2s1.3 1 1.3 2-.5 1.8-1.3 1.8q-1.2-.1-1.3-1.9"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M276.5 207.6c0-1 .6-2 1.3-2s1.3 1 1.3 2-.5 1.8-1.3 1.8q-1.2-.1-1.3-1.9z"/>
|
||||
<path fill="#c8b100" d="M277.3 207.6q0-1.7.5-1.8.6.2.7 1.8c.1 1.6-.3 1.7-.6 1.7q-.6-.2-.6-1.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M277.3 207.6q0-1.7.5-1.8.6.2.7 1.8c.1 1.6-.3 1.7-.6 1.7q-.6-.2-.6-1.8z"/>
|
||||
<path fill="#c8b100" d="m271 215.3-.5-1a27 27 0 0 1 14.8 0l-.6.8-.3.8a23 23 0 0 0-6.6-.8c-2.6 0-5.2.3-6.6.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m271 215.3-.5-1a27 27 0 0 1 14.8 0l-.6.8-.3.8a23 23 0 0 0-6.6-.8c-2.6 0-5.2.3-6.6.8l-.2-.6"/>
|
||||
<path fill="#c8b100" d="M277.8 217.7c2.4 0 5-.4 5.9-.6q1-.3 1-.8 0-.3-.4-.4a24 24 0 0 0-6.5-.8c-2.5 0-5 .3-6.4.8q-.3 0-.4.3 0 .6 1 .9c1 .2 3.5.6 5.8.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M277.8 217.7c2.4 0 5-.4 5.9-.6q1-.3 1-.8 0-.3-.4-.4a24 24 0 0 0-6.5-.8c-2.5 0-5 .3-6.4.8q-.3 0-.4.3 0 .6 1 .9c1 .2 3.5.6 5.8.6z"/>
|
||||
<path fill="#fff" d="M283.5 208.4q0-.3.4-.4.5 0 .5.4t-.5.4a.4.4 0 0 1-.4-.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".2" d="M283.5 208.4q0-.3.4-.4.5 0 .5.4t-.5.4a.4.4 0 0 1-.4-.4zm-.2-1.4a.4.4 0 0 1 .4-.4q.4 0 .4.4t-.4.4a.4.4 0 0 1-.4-.4zm-1.1-1q0-.3.4-.3t.4.4-.4.4a.4.4 0 0 1-.4-.5zm-1.4-.4q0-.3.4-.4.5 0 .5.4t-.4.4q-.5-.1-.5-.4zm-1.4 0q0-.3.5-.3t.4.4q0 .3-.4.4a.4.4 0 0 1-.5-.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".3" d="m287.8 211.2.2-1a2.7 2.7 0 0 0-2.7-2.8q-.8 0-1.3.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m283 209.2.2-.8q-.2-1.8-2.5-2-1 0-1.6.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".2" d="M288.2 210q0-.5.4-.5t.4.4-.4.4q-.5 0-.4-.4zm-.2-1.6q0-.3.4-.4a.4.4 0 0 1 .5.4q0 .3-.4.4-.5 0-.5-.4zm-1-1.1a.4.4 0 0 1 .5-.4q.3 0 .4.4a.4.4 0 0 1-.4.4.4.4 0 0 1-.5-.4zm-1.3-.7q0-.4.5-.4t.4.4q0 .5-.4.5a.4.4 0 0 1-.5-.5zm-1.4.1q0-.3.5-.4.6 0 .4.4t-.4.4q-.4-.1-.5-.4z"/>
|
||||
<path fill="#c8b100" d="m285.3 213.2-.5-.5s-.6.3-1.3.2c-.6 0-.9-1-.9-1s-.7.7-1.3.7-1-.6-1-.6-.7.5-1.3.4-1.2-.8-1.2-.8-.6.8-1.2.8c-.6.1-1-.5-1-.5s-.3.6-1.1.7-1.4-.6-1.4-.6-.4.7-1 1c-.5 0-1.2-.4-1.2-.4l-.1.5-.3.1.1.5a27 27 0 0 1 7.3-.9q4.3 0 7.3 1l.2-.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m285.3 213.2-.5-.5s-.6.3-1.3.2c-.6 0-.9-1-.9-1s-.7.7-1.3.7-1-.6-1-.6-.7.5-1.3.4-1.2-.8-1.2-.8-.6.8-1.2.8c-.6.1-1-.5-1-.5s-.3.6-1.1.7-1.4-.6-1.4-.6-.4.7-1 1c-.5 0-1.2-.4-1.2-.4l-.1.5-.3.1.1.5a27 27 0 0 1 7.3-.9q4.3 0 7.3 1l.2-.6z"/>
|
||||
<path fill="#fff" d="M271.3 208.4q0-.3.4-.4.5 0 .4.4a.4.4 0 0 1-.4.4.4.4 0 0 1-.4-.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".2" d="M271.3 208.4q0-.3.4-.4.5 0 .4.4a.4.4 0 0 1-.4.4.4.4 0 0 1-.4-.4zm.2-1.4q0-.4.4-.4t.5.4q0 .5-.5.4a.4.4 0 0 1-.4-.4zm1-1q.1-.3.5-.3t.5.4q0 .3-.5.4a.4.4 0 0 1-.4-.5zm1.4-.4q0-.3.5-.4.6 0 .4.4t-.4.4q-.4-.1-.5-.4zm1.4 0q0-.3.5-.3.3 0 .4.4 0 .3-.4.4a.4.4 0 0 1-.5-.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-linecap="round" stroke-width=".3" d="m267.8 211.2-.2-1a2.7 2.7 0 0 1 2.7-2.8q.8 0 1.4.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m272.7 209.2-.3-.8c0-1 1.2-2 2.6-2a3 3 0 0 1 1.5.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".2" d="M266.6 210q0-.5.4-.5t.4.4a.4.4 0 0 1-.4.4q-.3 0-.4-.4zm.1-1.6q.1-.3.5-.4.5 0 .4.4t-.4.4q-.4-.1-.4-.4zm1-1.1q0-.4.5-.4a.4.4 0 0 1 .4.4.4.4 0 0 1-.4.4.4.4 0 0 1-.5-.4zm1.3-.7q0-.4.5-.4.3 0 .4.4 0 .5-.4.5a.4.4 0 0 1-.5-.5zm1.4.1q0-.3.5-.4a.4.4 0 0 1 .4.4.4.4 0 0 1-.4.4q-.5 0-.5-.4z"/>
|
||||
<path fill="#c8b100" d="M277.9 210.7h.2v.4q.1 1 1 1a1 1 0 0 0 1-.7l.2-.3v.4q.3.8 1.1.8 1 0 1-1v-.1l.4-.4.2.5-.1.4a1 1 0 0 0 1 1q.6 0 .9-.5l.2-.2v.3q0 .5.4.7l1-.4c1-.5.7-.7.7-.7v.4s-.5.8-1 1q-.3.4-.8.3-.5 0-.7-.6l-.7.2q-1 0-1.4-.8l-1.1.5q-.8 0-1.2-.6l-1 .4q-.9 0-1.4-.6-.4.6-1.2.6-.6 0-1-.4a2 2 0 0 1-1.3.6q-.6 0-1.1-.5-.4.8-1.4.8-.4 0-.7-.2-.2.5-.7.6t-.9-.2l-1-1 .1-.5.8.7q.8.5.9.4.4-.1.4-.7v-.3l.2.2q.3.5.9.5a1 1 0 0 0 1-1v-.9l.4.4v.1q.1 1 1 1t1.1-.9v-.3l.2.3q.4.6 1 .7 1 0 1.1-1v-.3h.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M277.9 210.7h.2v.4q.1 1 1 1a1 1 0 0 0 1-.7l.2-.3v.4q.3.8 1.1.8 1 0 1-1v-.1l.4-.4.2.5-.1.4a1 1 0 0 0 1 1q.6 0 .9-.5l.2-.2v.3q0 .5.4.7l1-.4c1-.5.7-.7.7-.7v.4s-.5.8-1 1q-.3.4-.8.3-.5 0-.7-.6l-.7.2q-1 0-1.4-.8l-1.1.5q-.8 0-1.2-.6l-1 .4q-.9 0-1.4-.6-.4.6-1.2.6-.6 0-1-.4a2 2 0 0 1-1.3.6q-.6 0-1.1-.5-.4.8-1.4.8-.4 0-.7-.2-.2.5-.7.6t-.9-.2l-1-1 .1-.5.8.7q.8.5.9.4.4-.1.4-.7v-.3l.2.2q.3.5.9.5a1 1 0 0 0 1-1v-.9l.4.4v.1q.1 1 1 1t1.1-.9v-.3l.2.3q.4.6 1 .7 1 0 1.1-1v-.3h.2z"/>
|
||||
<path fill="#c8b100" d="M277.8 213.3q-4.5 0-7.3 1l-.3-.2.1-.3q3-1 7.5-1t7.6 1l.1.3-.3.2a27 27 0 0 0-7.4-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M277.8 213.3q-4.5 0-7.3 1l-.3-.2.1-.3q3-1 7.5-1t7.6 1l.1.3-.3.2a27 27 0 0 0-7.4-1z"/>
|
||||
<path fill="#fff" d="M275 214.4q0-.4.5-.4a.4.4 0 0 1 .4.4.4.4 0 0 1-.4.4q-.5 0-.5-.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M275 214.4q0-.4.5-.4a.4.4 0 0 1 .4.4.4.4 0 0 1-.4.4q-.5 0-.5-.4z"/>
|
||||
<path fill="#ad1519" d="M277.9 214.5h-1q-.2 0-.3-.3l.3-.3h2l.2.3-.3.3h-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M277.9 214.5h-1q-.2 0-.3-.3l.3-.3h2l.2.3-.3.3h-1"/>
|
||||
<path fill="#058e6e" d="M273.2 214.9h-.6a.3.3 0 0 1-.4-.2l.3-.3.6-.1.7-.1q.3 0 .4.2a.3.3 0 0 1-.3.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M273.2 214.9h-.6a.3.3 0 0 1-.4-.2l.3-.3.6-.1.7-.1q.3 0 .4.2a.3.3 0 0 1-.3.4h-.7"/>
|
||||
<path fill="#ad1519" d="m270.5 215.3.3-.4h.7l-.4.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m270.5 215.3.3-.4h.7l-.4.6-.6-.2"/>
|
||||
<path fill="#fff" d="M279.8 214.4q0-.4.4-.4t.5.4q0 .3-.5.4a.4.4 0 0 1-.4-.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M279.8 214.4q0-.4.4-.4t.5.4q0 .3-.5.4a.4.4 0 0 1-.4-.4z"/>
|
||||
<path fill="#058e6e" d="M282.5 214.9h.7l.3-.2-.2-.3-.7-.1-.7-.1q-.3 0-.4.2 0 .3.3.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M282.5 214.9h.7l.3-.2-.2-.3-.7-.1-.7-.1q-.3 0-.4.2 0 .3.3.4h.7"/>
|
||||
<path fill="#ad1519" d="m285.1 215.4-.2-.5h-.7l.3.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m285.1 215.4-.2-.5h-.7l.3.6.6-.1"/>
|
||||
<path fill="#ad1519" d="M277.8 217.1a25 25 0 0 1-6-.6 25 25 0 0 1 6-.7q3.6 0 6.1.7-2.5.6-6 .6"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" d="M277.8 217.1a25 25 0 0 1-6-.6 25 25 0 0 1 6-.7q3.6 0 6.1.7-2.5.6-6 .6z"/>
|
||||
<path fill="#c8b100" d="m285.2 212-.1-.3q-.3 0-.4.2l.1.4q.3 0 .4-.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m285.2 212-.1-.3q-.3 0-.4.2l.1.4q.3 0 .4-.3z"/>
|
||||
<path fill="#c8b100" d="M280.6 211.2q0-.3-.3-.4l-.2.3q0 .3.2.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M280.6 211.2q0-.3-.3-.4l-.2.3q0 .3.2.4z"/>
|
||||
<path fill="#c8b100" d="M275.2 211.2q0-.3.2-.4l.3.3-.2.4q-.3 0-.3-.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M275.2 211.2q0-.3.2-.4l.3.3-.2.4q-.3 0-.3-.3z"/>
|
||||
<path fill="#c8b100" d="m270.5 212 .1-.3q.3 0 .4.2l-.1.4q-.3 0-.4-.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m270.5 212 .1-.3q.3 0 .4.2l-.1.4q-.3 0-.4-.3z"/>
|
||||
<path fill="#c8b100" d="m277.8 208.5-.8.5.6 1.3.2.1.3-.1.6-1.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m277.8 208.5-.8.5.6 1.3.2.1.3-.1.6-1.3-.9-.5"/>
|
||||
<path fill="#c8b100" d="m276 210.5.4.5 1.3-.4.1-.2-.1-.2-1.3-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m276 210.5.4.5 1.3-.4.1-.2-.1-.2-1.3-.3-.4.6"/>
|
||||
<path fill="#c8b100" d="m279.6 210.5-.3.5-1.3-.4-.1-.2v-.2l1.4-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m279.6 210.5-.3.5-1.3-.4-.1-.2v-.2l1.4-.3.4.6"/>
|
||||
<path fill="#c8b100" d="m272.5 209-.7.7.9 1 .2.1.2-.1.2-1.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m272.5 209-.7.7.9 1 .2.1.2-.1.2-1.3-.8-.3"/>
|
||||
<path fill="#c8b100" d="m271.1 211.2.5.5 1.2-.6v-.2l-.1-.2-1.3-.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m271.1 211.2.5.5 1.2-.6v-.2l-.1-.2-1.3-.1-.3.6"/>
|
||||
<path fill="#c8b100" d="m274.7 210.5-.3.6h-1.3l-.2-.2.1-.3 1.2-.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m274.7 210.5-.3.6h-1.3l-.2-.2.1-.3 1.2-.6.5.5"/>
|
||||
<path fill="#c8b100" d="M269.8 211.4v.6l-1.4.2-.2-.1v-.2l1-.9z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M269.8 211.4v.6l-1.4.2-.2-.1v-.2l1-.9.6.4"/>
|
||||
<path fill="#c8b100" d="M272.4 210.9q0-.5.5-.5l.5.5-.5.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M272.4 210.9q0-.5.5-.5l.5.5-.5.4z"/>
|
||||
<path fill="#c8b100" d="m283.2 209 .7.7-.9 1-.2.1-.1-.1-.3-1.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m283.2 209 .7.7-.9 1-.2.1-.1-.1-.3-1.3.8-.3"/>
|
||||
<path fill="#c8b100" d="m284.6 211.2-.5.5-1.2-.6v-.2l.1-.2 1.3-.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m284.6 211.2-.5.5-1.2-.6v-.2l.1-.2 1.3-.1.3.6"/>
|
||||
<path fill="#c8b100" d="m281 210.5.3.6h1.3l.2-.2-.1-.3-1.2-.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m281 210.5.3.6h1.3l.2-.2-.1-.3-1.2-.6-.5.5"/>
|
||||
<path fill="#c8b100" d="M285.7 211.4v.6l1.4.2.2-.1v-.2l-1-.9z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M285.7 211.4v.6l1.4.2.2-.1v-.2l-1-.9-.6.4"/>
|
||||
<path fill="#c8b100" d="M277.4 210.4q0-.3.5-.4.4 0 .4.4t-.4.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M277.4 210.4q0-.3.5-.4.4 0 .4.4t-.4.5z"/>
|
||||
<path fill="#c8b100" d="M282.3 210.9q.1-.5.5-.5t.5.5q0 .6-.5.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M282.3 210.9q.1-.5.5-.5t.5.5q0 .6-.5.4z"/>
|
||||
<path fill="#c8b100" d="M277 205.4q.1-.8.8-.8 1 0 1 .8c0 .8-.5.8-1 .8a1 1 0 0 1-.8-.8"/>
|
||||
<path fill="#c8b100" d="M278.5 205.1v.6H277v-.6h.4v-1.3h-.5v-.5h.5v-.6h.6v.6h.6v.6h-.6v1.2h.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M278.5 205.1v.6H277v-.6h.4v-1.3h-.5v-.5h.5v-.6h.6v.6h.6v.6h-.6v1.2h.4z"/>
|
||||
<path fill="#c8b100" d="M279 205.1v.6h-2.4v-.6h1v-1.3h-.7v-.5h.6v-.6h.6v.6h.6v.6h-.6v1.2h1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M278.1 204.6q.6.1.6.8t-.9.8a1 1 0 0 1-.8-.8q0-.6.6-.8"/>
|
||||
<path fill="#c8b100" d="m268 212.2-.6-.7-.7-.3q0-.2.6-.3.3 0 .4.2v-.2s.3 0 .4.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m268 212.2-.6-.7-.7-.3q0-.2.6-.3.3 0 .4.2v-.2s.3 0 .4.3z"/>
|
||||
<path fill="#c8b100" d="M268 212q.3-.3.5 0 .4.2.1.5l-.5-.1q-.2-.2 0-.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M268 212q.3-.3.5 0 .4.2.1.5l-.5-.1q-.2-.2 0-.5z"/>
|
||||
<path fill="#c8b100" d="m287.5 212.2.6-.7c.2-.2.7-.3.7-.3q0-.2-.6-.3l-.4.2v-.2s-.3 0-.4.3v.7z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m287.5 212.2.6-.7c.2-.2.7-.3.7-.3q0-.2-.6-.3l-.4.2v-.2s-.3 0-.4.3v.7z"/>
|
||||
<path fill="#c8b100" d="M287.5 212q-.2-.3-.5 0t-.1.5l.5-.1q.3-.2.1-.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M287.5 212q-.2-.3-.5 0t-.1.5l.5-.1q.3-.2.1-.5z"/>
|
||||
<path fill="#c8b100" d="M267.2 223h21.4v-5.5h-21.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M267.2 223h21.4v-5.5h-21.4z"/>
|
||||
<path fill="#c8b100" d="M286.3 226.8h-16.9q1-.4 1-1.2 0-1-1-1.3h17-.1q-1 .3-1 1.3 0 .8 1 1.2"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="M286.3 226.8h-16.9q1-.4 1-1.2 0-1-1-1.3h17-.1q-1 .3-1 1.3 0 .8 1 1.2z"/>
|
||||
<path fill="#c8b100" d="M269.9 226.8h16q1 0 1 .7t-1 .8h-16q-1-.1-1-.8.1-1 1-.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M269.9 226.8h16q1 0 1 .7t-1 .8h-16q-1-.1-1-.8.1-1 1-.8z"/>
|
||||
<path fill="#c8b100" d="M269.9 223h16q1 .1 1 .7t-1 .6h-16q-1 0-1-.6t1-.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M269.9 223h16q1 .1 1 .7t-1 .6h-16q-1 0-1-.6t1-.6z"/>
|
||||
<path fill="#005bbf" d="M263 317.4q2.1 0 3.7-.8a8 8 0 0 1 3.7-.8q2.2 0 3.8.8c1.6.8 2.3.8 3.7.8q2.3 0 3.8-.8a8 8 0 0 1 3.6-.8 8 8 0 0 1 3.7.8q1.6.8 3.8.8v2.4a8 8 0 0 1-3.8-.9 8 8 0 0 0-3.7-.8q-2.2 0-3.6.8-1.5.8-3.8.9a8 8 0 0 1-3.7-.9 8 8 0 0 0-3.8-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.9v-2.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M263 317.4q2.1 0 3.7-.8a8 8 0 0 1 3.7-.8q2.2 0 3.8.8c1.6.8 2.3.8 3.7.8q2.3 0 3.8-.8a8 8 0 0 1 3.6-.8 8 8 0 0 1 3.7.8q1.6.8 3.8.8v2.4a8 8 0 0 1-3.8-.9 8 8 0 0 0-3.7-.8q-2.2 0-3.6.8-1.5.8-3.8.9a8 8 0 0 1-3.7-.9 8 8 0 0 0-3.8-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.9v-2.4z"/>
|
||||
<path fill="#ccc" d="M263 319.8q2.1 0 3.7-.9c1.6-.9 2.3-.8 3.7-.8q2.2 0 3.8.8c1.6.8 2.3.9 3.7.9a8 8 0 0 0 3.8-.9 8 8 0 0 1 3.6-.8q2.3 0 3.7.8 1.6.8 3.8.9v2.3a8 8 0 0 1-3.8-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.6.7-1.5.8-3.8.9a7 7 0 0 1-3.7-.9q-1.5-.6-3.8-.7a8 8 0 0 0-3.7.7 8 8 0 0 1-3.8.9v-2.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M263 319.8q2.1 0 3.7-.9c1.6-.9 2.3-.8 3.7-.8q2.2 0 3.8.8c1.6.8 2.3.9 3.7.9a8 8 0 0 0 3.8-.9 8 8 0 0 1 3.6-.8q2.3 0 3.7.8 1.6.8 3.8.9v2.3a8 8 0 0 1-3.8-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.6.7-1.5.8-3.8.9a7 7 0 0 1-3.7-.9q-1.5-.6-3.8-.7a8 8 0 0 0-3.7.7 8 8 0 0 1-3.8.9v-2.3"/>
|
||||
<path fill="#005bbf" d="M263 322q2.1 0 3.7-.8 1.6-.6 3.7-.7 2.2 0 3.8.7c1.6.7 2.3.9 3.7.9a8 8 0 0 0 3.8-.9 8 8 0 0 1 3.6-.8 8 8 0 0 1 3.7.8q1.6.8 3.8.9v2.3a8 8 0 0 1-3.8-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.6.7-1.5.9-3.8.9t-3.7-.8a8 8 0 0 0-3.8-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.8V322"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M263 322q2.1 0 3.7-.8 1.6-.6 3.7-.7 2.2 0 3.8.7c1.6.7 2.3.9 3.7.9a8 8 0 0 0 3.8-.9 8 8 0 0 1 3.6-.8 8 8 0 0 1 3.7.8q1.6.8 3.8.9v2.3a8 8 0 0 1-3.8-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.6.7-1.5.9-3.8.9t-3.7-.8a8 8 0 0 0-3.8-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.8V322"/>
|
||||
<path fill="#ccc" d="M263 326.7a8 8 0 0 0 3.7-.8q1.6-.8 3.7-.8 2.2 0 3.8.8c1.6.8 2.3.8 3.7.8q2.3 0 3.8-.9a8 8 0 0 1 3.6-.7q2.3 0 3.7.8a8 8 0 0 0 3.8.8v-2.3a8 8 0 0 1-3.8-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.6.7-1.5.8-3.8.9t-3.7-.8a8 8 0 0 0-3.8-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.8v2.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M263 326.7a8 8 0 0 0 3.7-.8q1.6-.8 3.7-.8 2.2 0 3.8.8c1.6.8 2.3.8 3.7.8q2.3 0 3.8-.9a8 8 0 0 1 3.6-.7q2.3 0 3.7.8a8 8 0 0 0 3.8.8v-2.3a8 8 0 0 1-3.8-.9 8 8 0 0 0-3.7-.7q-2.2 0-3.6.7-1.5.8-3.8.9t-3.7-.8a8 8 0 0 0-3.8-.8 8 8 0 0 0-3.7.8q-1.5.8-3.8.8v2.3"/>
|
||||
<path fill="#005bbf" d="M263 329a8 8 0 0 0 3.7-.8q1.6-.8 3.7-.8 2.2 0 3.8.8c1.6.8 2.3.8 3.7.8a8 8 0 0 0 3.8-.9 8 8 0 0 1 3.6-.7q2.3 0 3.7.8 1.6.8 3.8.8v-2.3a8 8 0 0 1-3.8-.8 8 8 0 0 0-3.7-.8 8 8 0 0 0-3.6.7 8 8 0 0 1-3.8.9q-2.2 0-3.7-.8t-3.8-.8q-2.1 0-3.7.8c-1.6.8-2.3.8-3.8.8v2.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M263 329a8 8 0 0 0 3.7-.8q1.6-.8 3.7-.8 2.2 0 3.8.8c1.6.8 2.3.8 3.7.8a8 8 0 0 0 3.8-.9 8 8 0 0 1 3.6-.7q2.3 0 3.7.8 1.6.8 3.8.8v-2.3a8 8 0 0 1-3.8-.8 8 8 0 0 0-3.7-.8 8 8 0 0 0-3.6.7 8 8 0 0 1-3.8.9q-2.2 0-3.7-.8t-3.8-.8q-2.1 0-3.7.8c-1.6.8-2.3.8-3.8.8v2.3z"/>
|
||||
<path fill="#c8b100" d="m286.3 308-.1.5c0 1.5 1.2 2.6 2.7 2.6h-22c1.5 0 2.7-1.2 2.7-2.6l-.1-.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="m286.3 308-.1.5c0 1.5 1.2 2.6 2.7 2.6h-22c1.5 0 2.7-1.2 2.7-2.6l-.1-.5z"/>
|
||||
<path fill="#c8b100" d="M269.9 306.5h16q1 0 1 .8 0 .6-1 .7h-16q-1 0-1-.8.1-.6 1-.7"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M269.9 306.5h16q1 0 1 .8 0 .6-1 .7h-16q-1 0-1-.8.1-.6 1-.7z"/>
|
||||
<path fill="#c8b100" d="M266.9 316.7h22V311h-22z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M266.9 316.7h22V311h-22z"/>
|
||||
<path fill="#ad1519" d="M290.6 286.7c2.1 1.2 3.6 2.5 3.4 3.2q-.2.8-1.8 1.6c-1.6 1.1-2.5 3-1.8 4a5.5 5.5 0 0 1 .2-8.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M290.6 286.7c2.1 1.2 3.6 2.5 3.4 3.2q-.2.8-1.8 1.6c-1.6 1.1-2.5 3-1.8 4a5.5 5.5 0 0 1 .2-8.8z"/>
|
||||
<path fill="#ccc" d="M270.1 305.6h15.6V229h-15.6v76.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M281.4 229.1v76.3m1.8-76.3v76.3m-13 .2h15.5V229h-15.6v76.5z"/>
|
||||
<path fill="#ad1519" d="M254.2 257.7a50 50 0 0 1 23.3-2c9.3 1.6 16.4 5.3 15.9 8.4v.2l3.5-8.2c.6-3.3-7.3-7.5-17.6-9.2a54 54 0 0 0-9.2-.7c-6.7 0-12.4.8-15.9 2.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="M254.2 257.7a50 50 0 0 1 23.3-2c9.3 1.6 16.4 5.3 15.9 8.4v.2l3.5-8.2c.6-3.3-7.3-7.5-17.6-9.2a54 54 0 0 0-9.2-.7c-6.7 0-12.4.8-15.9 2.1v9.4"/>
|
||||
<path fill="#ad1519" d="M285.7 267.3c4.4-.3 7.3-1.4 7.7-3.2q.2-2.2-3.8-4.5l-3.9.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M285.7 267.3c4.4-.3 7.3-1.4 7.7-3.2q.2-2.2-3.8-4.5l-3.9.3v7.4"/>
|
||||
<path fill="#ad1519" d="M270 261.5a13 13 0 0 0-5.7 1.9v.2c-.5 1 1.8 3 5.8 5.4v-7.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M270 261.5a13 13 0 0 0-5.7 1.9v.2c-.5 1 1.8 3 5.8 5.4v-7.5"/>
|
||||
<path fill="#ad1519" d="M295.4 282c.4-1.2-3.8-3.6-9.7-5.8-2.8-1-5-2-7.8-3.2-8.3-3.7-14.4-7.9-13.6-9.4v-.2c-.4.4-1 8-1 8-.8 1.3 4.8 5.5 12.4 9.1 2.4 1.2 7.6 3 10 4 4.3 1.4 8.7 4.3 8.3 5.3l1.4-7.7"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="M295.4 282c.4-1.2-3.8-3.6-9.7-5.8-2.8-1-5-2-7.8-3.2-8.3-3.7-14.4-7.9-13.6-9.4v-.2c-.4.4-1 8-1 8-.8 1.3 4.8 5.5 12.4 9.1 2.4 1.2 7.6 3 10 4 4.3 1.4 8.7 4.3 8.3 5.3l1.4-7.7z"/>
|
||||
<path fill="#c8b100" d="m263.9 254.4 2.1-6.6h-.5l-.5.1-1.4 4.8q-1.6-2-2.7-4.1l-1 .2h-1l4 5.7h.5zm6-6.6h-1.8v6.2h4.2v-.7h-2.6zm6.8 1 2 .3v-.7l-5.8-.5v.8h2l-.4 5.6h1.6l.5-5.4m2.4 6 .8.2.8.2.7-2.9.6 1.2.8 2.1 1 .2q.6 0 1 .3l-.3-.7-1.3-2.9q1.6 0 2.1-1.2.3-.8-.7-1.5-.8-.4-1.7-.5l-2.4-.5zm3-5.2c.7.2 1.5.3 1.5 1v.5q-.5 1.3-2 .9zm8 7-.2 2 .8.5.9.5.5-7-.7-.3-6.1 3.8.5.3.4.2 1.7-1.2 2.3 1.3zm-1.7-1.5 2-1.4-.2 2.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M182.2 192.4c0-1 1-2 2-2s2.2 1 2.2 2c0 1.1-1 2-2.1 2a2 2 0 0 1-2.1-2z"/>
|
||||
<path fill="#ad1519" stroke="#000" stroke-width=".3" d="M205.7 175.4c6.3 0 12 1 15.7 2.4a32 32 0 0 0 14.6 2.3c2.7 0 6.5.8 10.3 2.4a27 27 0 0 1 7.4 4.7l-1.5 1.4-.4 3.8-4.1 4.7-2 1.8-5 3.9-2.5.2-.7 2.1-31.6-3.7-31.7 3.7-.8-2.1-2.5-.2-4.9-4-2-1.7-4.1-4.7-.5-3.8-1.5-1.4a28 28 0 0 1 7.5-4.7 26 26 0 0 1 10.2-2.4q3 .3 6.6-.2a30 30 0 0 0 8-2q5.6-2.4 15.5-2.5z"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="M206.2 217.1c-11.8 0-22.4-1.4-29.9-3.6a1 1 0 0 1-.8-1.2q0-.8.8-1.2a109 109 0 0 1 29.9-3.6c11.7 0 22.3 1.4 29.8 3.6a1.3 1.3 0 0 1 0 2.4c-7.5 2.2-18 3.6-29.8 3.6"/>
|
||||
<path fill="#ad1519" d="M206.1 215.6c-10.6 0-20.2-1.2-27.5-3.1 7.3-2 16.9-3 27.5-3.1a115 115 0 0 1 27.6 3c-7.3 2-17 3.2-27.6 3.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M206.9 215.7v-6.3m-1.7 6.3v-6.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".2" d="M203.6 215.7v-6.3m-1.6 6.3v-6.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M200.6 215.7v-6.3m-2.8 5.9v-5.7m1.3 5.8v-6m-3.8 5.6v-5.2m1.3 5.4v-5.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M192 214.8V210m1 4.7V210m1.2 5v-5m-3.4 4.7v-4.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".5" d="M189.7 214.5v-4.2m-1.2 4.1v-4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".6" d="M186 214v-3m1.3 3.2v-3.5m-2.5 3.1V211"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".7" d="M183.7 213.6v-2.3m-1.3 2v-1.8m-1.2 1.6v-1.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".9" d="M179.8 212.8v-.7"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M213.7 215.3v-5.8m-2.9 6v-6.1m-2.1 6.2v-6.3"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="M206 207.4a108 108 0 0 0-30 3.9c.6-.3.5-1-.3-3-1-2.5-2.4-2.4-2.4-2.4 8.3-2.5 20-4 32.8-4a123 123 0 0 1 33 4s-1.5-.1-2.5 2.3q-1.2 2.8-.2 3c-7.5-2.2-18.4-3.7-30.3-3.7"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="M206.1 201.9c-12.9 0-24.5 1.5-32.8 4a1 1 0 0 1-1.3-.6 1 1 0 0 1 .7-1.3 121 121 0 0 1 33.4-4.2c13.2 0 25.2 1.7 33.5 4.2q1 .4.7 1.3t-1.3.6c-8.4-2.5-20-4-32.9-4"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".4" d="M206.1 215.6c-10.6 0-20.2-1.2-27.5-3.1 7.3-2 16.9-3 27.5-3.1a115 115 0 0 1 27.6 3c-7.3 2-17 3.2-27.6 3.2z"/>
|
||||
<path fill="#fff" stroke="#000" stroke-width=".4" d="M197 204.8q0-.9 1-1 .9.1 1 1 0 1.2-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="#ad1519" stroke="#000" stroke-width=".4" d="M206.1 205.6H203a1 1 0 0 1 0-2h6.4q.9.1 1 1 0 1.2-1 1h-3.2"/>
|
||||
<path fill="#058e6e" stroke="#000" stroke-width=".4" d="m190.3 206.5-2.3.2q-.9 0-1.2-.8a1 1 0 0 1 1-1.1l2.2-.3 2.4-.3q.9 0 1.1.9 0 .9-.9 1l-2.3.4"/>
|
||||
<path fill="#fff" stroke="#000" stroke-width=".4" d="M181 206.7q.1-1 1.1-1t1 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="#ad1519" stroke="#000" stroke-width=".4" d="m174 208.5 1.2-1.6 3.3.4-2.6 2-1.8-.8"/>
|
||||
<path fill="#058e6e" stroke="#000" stroke-width=".4" d="m222 206.5 2.3.2q.9 0 1.1-.8a1 1 0 0 0-.9-1.1l-2.2-.3-2.4-.3a1 1 0 0 0-1.1.9q0 .9.9 1l2.3.4"/>
|
||||
<path fill="#fff" stroke="#000" stroke-width=".4" d="M213.3 204.8q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1m15.8 1.9q.1-1 1-1t1.1 1q0 .9-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="#ad1519" stroke="#000" stroke-width=".4" d="m238.2 208.5-1.1-1.6-3.3.4 2.6 2 1.8-.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M177.3 212.8c7.4-2.1 17.6-3.4 28.8-3.4 11.3 0 21.4 1.3 28.9 3.4"/>
|
||||
<path fill="#c8b100" d="m182.3 183.8 1.4 1 2-3.2a7 7 0 0 1-3.6-7.2c.2-4.1 5.2-7.6 11.7-7.6q5 .1 8.5 2.4l.2-1.8a17 17 0 0 0-8.7-2.1c-7.4 0-13.2 4.1-13.5 9.1a9 9 0 0 0 3 7.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="m182.3 183.8 1.4 1 2-3.2a7 7 0 0 1-3.6-7.2c.2-4.1 5.2-7.6 11.7-7.6q5 .1 8.5 2.4l.2-1.8a17 17 0 0 0-8.7-2.1c-7.4 0-13.2 4.1-13.5 9.1a9 9 0 0 0 3 7.6l-1 1.8"/>
|
||||
<path fill="#c8b100" d="M182.4 183.8a9 9 0 0 1-4-7.3q.2-5 5.3-8a9 9 0 0 0-3.4 6.8 9 9 0 0 0 3 6.7z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M182.4 183.8a9 9 0 0 1-4-7.3q.2-5 5.3-8a9 9 0 0 0-3.4 6.8 9 9 0 0 0 3 6.7l-.9 1.8"/>
|
||||
<path fill="#c8b100" d="M160.1 187.1a9 9 0 0 1-2.3-5.9q0-2 1-3.8c2-4.2 8.4-7.2 16-7.2q3 0 5.9.6l-1 1.4a26 26 0 0 0-4.9-.4c-7 0-12.8 2.7-14.5 6.3a7 7 0 0 0-.7 3.1 7 7 0 0 0 2.7 5.6l-2.6 4.1-1.3-1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M160.1 187.1a9 9 0 0 1-2.3-5.9q0-2 1-3.8c2-4.2 8.4-7.2 16-7.2q3 0 5.9.6l-1 1.4a26 26 0 0 0-4.9-.4c-7 0-12.8 2.7-14.5 6.3a7 7 0 0 0-.7 3.1 7 7 0 0 0 2.7 5.6l-2.6 4.1-1.3-1z"/>
|
||||
<path fill="#c8b100" d="M162.7 173.3a11 11 0 0 0-4 4.1 9 9 0 0 0-.9 3.8q.1 3.5 2.3 5.9l-1.5 2.5a10 10 0 0 1-2.3-6.5c0-4 2.5-7.5 6.4-9.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M162.7 173.3a11 11 0 0 0-4 4.1 9 9 0 0 0-.9 3.8q.1 3.5 2.3 5.9l-1.5 2.5a10 10 0 0 1-2.3-6.5c0-4 2.5-7.5 6.4-9.8z"/>
|
||||
<path fill="#c8b100" d="M206 164.4c1.7 0 3.2 1.1 3.5 2.6q.4 2.1.4 4.5v1.1c.1 3.3.6 6.3 1.3 8.1l-5.2 5-5.2-5c.7-1.8 1.2-4.8 1.3-8.1v-1.1q0-2.4.4-4.5c.3-1.5 1.8-2.6 3.5-2.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M206 164.4c1.7 0 3.2 1.1 3.5 2.6q.4 2.1.4 4.5v1.1c.1 3.3.6 6.3 1.3 8.1l-5.2 5-5.2-5c.7-1.8 1.2-4.8 1.3-8.1v-1.1q0-2.4.4-4.5c.3-1.5 1.8-2.6 3.5-2.6z"/>
|
||||
<path fill="#c8b100" d="M206 166q1.6.1 1.8 1.4.3 1.8.4 4.2v1q.2 5 1.2 7.7l-3.4 3.2-3.4-3.2q1-2.7 1.2-7.7v-1l.4-4.2a2 2 0 0 1 1.8-1.4"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M206 166q1.6.1 1.8 1.4.3 1.8.4 4.2v1q.2 5 1.2 7.7l-3.4 3.2-3.4-3.2q1-2.7 1.2-7.7v-1l.4-4.2a2 2 0 0 1 1.8-1.4z"/>
|
||||
<path fill="#c8b100" d="m229.7 183.8-1.3 1-2-3.2a7 7 0 0 0 3.6-6.3v-.9c-.2-4.1-5.3-7.6-11.7-7.6a15 15 0 0 0-8.5 2.4l-.2-1.8a17 17 0 0 1 8.7-2.1c7.4 0 13.2 4.1 13.4 9.1a9 9 0 0 1-3 7.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="m229.7 183.8-1.3 1-2-3.2a7 7 0 0 0 3.6-6.3v-.9c-.2-4.1-5.3-7.6-11.7-7.6a15 15 0 0 0-8.5 2.4l-.2-1.8a17 17 0 0 1 8.7-2.1c7.4 0 13.2 4.1 13.4 9.1a9 9 0 0 1-3 7.6l1 1.8"/>
|
||||
<path fill="#c8b100" d="M229.6 183.8a9 9 0 0 0 4.1-7.3c0-3.2-2.1-6.1-5.3-8a9 9 0 0 1 3.4 6.8 9 9 0 0 1-3.2 6.7z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M229.6 183.8a9 9 0 0 0 4.1-7.3c0-3.2-2.1-6.1-5.3-8a9 9 0 0 1 3.4 6.8 9 9 0 0 1-3.2 6.7l1 1.8"/>
|
||||
<path fill="#c8b100" d="M252 187.1a9 9 0 0 0 2.2-5.9 9 9 0 0 0-.9-3.8c-2-4.2-8.4-7.2-16-7.2a29 29 0 0 0-6 .6l1 1.4a25 25 0 0 1 5-.4c7 0 12.8 2.7 14.4 6.3q.7 1.5.7 3.1a7 7 0 0 1-2.6 5.6l2.5 4.1 1.3-1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M252 187.1a9 9 0 0 0 2.2-5.9 9 9 0 0 0-.9-3.8c-2-4.2-8.4-7.2-16-7.2a29 29 0 0 0-6 .6l1 1.4a25 25 0 0 1 5-.4c7 0 12.8 2.7 14.4 6.3q.7 1.5.7 3.1a7 7 0 0 1-2.6 5.6l2.5 4.1 1.3-1z"/>
|
||||
<path fill="#c8b100" d="M249.3 173.3a11 11 0 0 1 4 4.1 9 9 0 0 1 .9 3.8 9 9 0 0 1-2.3 5.9l1.6 2.5a10 10 0 0 0 2.3-6.5c0-4-2.6-7.5-6.5-9.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M249.3 173.3a11 11 0 0 1 4 4.1 9 9 0 0 1 .9 3.8 9 9 0 0 1-2.3 5.9l1.6 2.5a10 10 0 0 0 2.3-6.5c0-4-2.6-7.5-6.5-9.8z"/>
|
||||
<path fill="#fff" d="M204.2 181.4q.1-1.7 1.8-1.8c1.7-.1 1.9.8 1.9 1.8s-.9 1.7-1.9 1.7a2 2 0 0 1-1.8-1.7"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M204.2 181.4q.1-1.7 1.8-1.8c1.7-.1 1.9.8 1.9 1.8s-.9 1.7-1.9 1.7a2 2 0 0 1-1.8-1.7z"/>
|
||||
<path fill="#fff" stroke="#000" stroke-width=".4" d="M204.2 178q.1-1.7 1.8-1.8c1.7-.1 1.9.8 1.9 1.8s-.9 1.7-1.9 1.7a2 2 0 0 1-1.8-1.7m.4-3.7q.1-1.2 1.4-1.3 1.4.1 1.5 1.3-.1 1.3-1.5 1.4c-1.4.1-1.4-.6-1.4-1.4m.4-3.3q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1m.2-2.8q.1-.8.8-.8t.9.8q0 .7-.9.8a1 1 0 0 1-.8-.8"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="m206.2 191.8 1.2.2a4.6 4.6 0 0 0 4.5 6 5 5 0 0 0 4.4-3c.1 0 .5-1.7.7-1.7s.1 1.8.2 1.7c.3 2.3 2.4 3.8 4.7 3.8a4.6 4.6 0 0 0 4.7-5l1.5-1.5.7 2a4 4 0 0 0-.4 1.9 4.4 4.4 0 0 0 4.5 4.2q2.5 0 3.8-1.9l.9-1.2v1.5c0 1.5.6 2.8 2 3 0 0 1.7.1 4-1.6 2.1-1.7 3.3-3.1 3.3-3.1l.2 1.7s-1.8 2.8-3.8 4c-1 .6-2.7 1.3-4 1q-2.1-.5-3-2.6a7 7 0 0 1-3.3 1 7 7 0 0 1-6.1-3.7 7 7 0 0 1-10.4-.3 7 7 0 0 1-4.6 1.8 7 7 0 0 1-5.7-3 7 7 0 0 1-5.7 3 7 7 0 0 1-4.7-1.8 7 7 0 0 1-10.4.3 7 7 0 0 1-6 3.7 7 7 0 0 1-3.4-1q-.8 2.1-3 2.7c-1.2.2-2.9-.5-4-1.1-2-1.2-3.8-4-3.8-4l.2-1.7s1.2 1.4 3.4 3.1 3.9 1.6 3.9 1.6c1.4-.2 2-1.5 2-3v-1.5l1 1.2a5 5 0 0 0 3.7 2c2.5 0 4.5-2 4.5-4.3a4 4 0 0 0-.4-2l.8-1.9 1.5 1.5v.6c0 2.4 2 4.4 4.6 4.4 2.4 0 4.4-1.5 4.7-3.8 0 0 0-1.6.2-1.7s.6 1.7.7 1.6a5 5 0 0 0 4.5 3.1 4.6 4.6 0 0 0 4.5-6l1.2-.2"/>
|
||||
<path fill="#fff" stroke="#000" stroke-width=".4" d="M238.6 197.7q.4-1.4-.6-1.8-.9-.2-1.5 1.1-.4 1.4.6 1.8.9.2 1.5-1.1m-20.5-4c0-.8-.3-1.6-1-1.6q-.8 0-1.2 1.4 0 1.3.9 1.6 1-.1 1.3-1.4m-23.9 0c0-.8.4-1.6 1-1.6q1 0 1.2 1.4 0 1.3-.9 1.6-1-.1-1.2-1.4m-20.6 4q-.3-1.4.6-1.8 1-.2 1.5 1.1t-.5 1.8q-1 .2-1.6-1.1"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="M182.7 184a5 5 0 0 1 2.2 2.9s0-.3.6-.6 1-.3 1-.3l-.1 1.3-.3 2.2-.7 1.6a2 2 0 0 0-1.5-.4 2 2 0 0 0-1.2.9s-.7-.6-1.2-1.3l-1.1-2-.7-1.1s.5-.2 1.1 0q.9.1.8.2a5 5 0 0 1 1-3.4m.4 9.8-.6-1q0-.7.3-1.2s-.9-.5-1.8-.7c-.7-.2-2-.2-2.3-.2h-1l.2.5.5.7a5 5 0 0 0-3 2 5 5 0 0 0 3.5 1l-.2.8v.6l1-.4c.3-.1 1.5-.5 2-1 .8-.4 1.5-1.1 1.5-1.1m2.7-.5.2-1.1-.6-1 1.4-1.3 2-.9 1.1-.4v.6l-.2.8a5 5 0 0 1 3.4 1 5 5 0 0 1-2.9 2l.7 1.2h-1c-.4 0-1.6 0-2.3-.2l-1.8-.7"/>
|
||||
<path fill="#ad1519" stroke="#000" stroke-width=".4" d="M182.2 192.4c0-1 1-2 2-2s2.2 1 2.2 2c0 1.1-1 2-2.1 2a2 2 0 0 1-2.1-2"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="M206.1 180.8a6 6 0 0 1 1.9 3.7s.2-.3.9-.5q1.1-.3 1.2-.2l-.5 1.4-.8 2.4-1 1.7a2 2 0 0 0-1.7-.7q-1 0-1.6.7-.2-.1-1-1.7l-.8-2.4-.5-1.4 1.2.2q1 .4.9.5.2-2.2 1.8-3.7"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="M204.6 191.8a2 2 0 0 1-.5-1.2q0-.8.4-1.3s-.8-.7-1.8-1c-.7-.4-2-.7-2.5-.7l-1.2-.2.2.6.4.9a6 6 0 0 0-3.7 1.7q1.6 1.5 3.7 1.6l-.4 1-.2.6 1.2-.2c.4-.1 1.8-.4 2.5-.7 1-.4 1.9-1 1.9-1m3 0a2 2 0 0 0 .1-2.6s.9-.7 1.8-1a8 8 0 0 1 2.5-.7l1.2-.3-.1.7-.4.9q2.2.2 3.6 1.7a6 6 0 0 1-3.6 1.6l.5 1.6-1.2-.2-2.5-.7c-1-.4-1.8-1-1.8-1m22-8a5 5 0 0 0-2.2 3l-.7-.6-1-.3.2 1.3c0 .3 0 1.3.3 2.2q.4 1.6.6 1.6a2 2 0 0 1 1.5-.4q.9.2 1.3.9l1.1-1.3q1-1.4 1.1-2l.7-1.1s-.4-.2-1 0l-1 .2a5 5 0 0 0-1-3.4m-.3 9.8q.5-.4.6-1l-.2-1.2s.8-.5 1.7-.7c.7-.2 2-.2 2.3-.2h1.1l-.3.5-.4.7a5 5 0 0 1 2.9 2 5 5 0 0 1-3.5 1l.2.8v.6l-1-.4c-.3-.1-1.4-.5-2-1-.8-.4-1.4-1.1-1.4-1.1m-2.8-.5-.2-1.1q0-.7.6-1s-.6-.8-1.4-1.3c-.6-.4-1.7-.8-2-.9l-1-.4v.6l.2.8a5 5 0 0 0-3.5 1q1 1.4 3 2l-.5.7-.3.5h1c.4 0 1.7 0 2.3-.2l1.8-.7"/>
|
||||
<path fill="#ad1519" stroke="#000" stroke-width=".4" d="M226 192.4c0-1 1-2 2-2s2.1 1 2.1 2a2 2 0 0 1-2 2 2 2 0 0 1-2.1-2m23.2 4.4c-.4-.5-1.4-.4-2.2.2q-1.2 1.2-.5 2.2 1 .8 2.3-.3c.7-.6 1-1.6.5-2"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="m246.3 198 .7-1c.7-.6 1.8-.7 2.3-.2l.1.2s1-2 2.3-2.6 3.4-.5 3.4-.5a3 3 0 0 0-2.9-2.8 3 3 0 0 0-2.4 1l-.2-1s-1.3.3-1.9 1.8 0 3.6 0 3.6-.3-.9-.7-1.5a8 8 0 0 0-2.4-1.6l-1.3-.7-.1.5v.8a8 8 0 0 0-3.7.5 5 5 0 0 0 2.5 2.2l-.8.7-.4.5 1.3.2 2.5.2 1.7-.2m-80.3 0q0-.6-.7-1c-.7-.7-1.7-.8-2.2-.3l-.2.3s-1-2-2.3-2.7-3.3-.5-3.3-.5a3 3 0 0 1 2.8-2.8q1.6 0 2.4 1l.2-1s1.3.3 2 1.8c.5 1.5-.1 3.6-.1 3.6s.3-.9.8-1.5a8 8 0 0 1 2.4-1.6l1.3-.7v1.3a8 8 0 0 1 3.7.5 5 5 0 0 1-2.5 2.2l.8.7.4.5-1.2.2-2.6.2-1.7-.2"/>
|
||||
<path fill="#ad1519" stroke="#000" stroke-width=".4" d="M163 196.8q1-.7 2.4.3 1.2 1 .4 2c-.5.6-1.5.5-2.2-.2s-1-1.6-.5-2m41-6.3q.2-1.8 2-2c1.8-.2 2.1.9 2.1 2 0 1-1 2-2 2a2 2 0 0 1-2.1-2"/>
|
||||
<path fill="#005bbf" stroke="#000" stroke-width=".3" d="M201.8 160.6c0-2.2 1.9-4 4.3-4s4.2 1.8 4.2 4-1.9 4-4.3 4a4 4 0 0 1-4.2-4"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".3" d="M205 149.3v2.2h-2.4v2.2h2.3v6.3H202l-.2.6q0 .9.3 1.6h7.9q.3-.7.3-1.6l-.2-.6h-2.8v-6.3h2.3v-2.2h-2.3v-2.2z"/>
|
||||
<path fill="#ccc" d="M206.5 330.6a82 82 0 0 1-35.5-8.2 23 23 0 0 1-12.8-20.4v-32h96.4v32a23 23 0 0 1-12.8 20.4 81 81 0 0 1-35.3 8.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".5" d="M206.5 330.6a82 82 0 0 1-35.5-8.2 23 23 0 0 1-12.8-20.4v-32h96.4v32a23 23 0 0 1-12.8 20.4 81 81 0 0 1-35.3 8.2z"/>
|
||||
<path fill="#ccc" d="M206.3 270h48.3v-53.5h-48.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".5" d="M206.3 270h48.3v-53.5h-48.3z"/>
|
||||
<path fill="#ad1519" d="M206.3 302c0 12.6-10.7 22.9-24 22.9s-24.2-10.3-24.2-23v-32h48.2v32"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".5" d="M168.6 320.9c1.5.8 3.6 2 5.8 2.6l-.1-54.7h-5.7z"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-linejoin="round" stroke-width=".5" d="M158 301.6a24 24 0 0 0 5.5 15v-47.5h-5.4z"/>
|
||||
<path fill="#c7b500" stroke="#000" stroke-width=".5" d="M179.4 324.7a27 27 0 0 0 5.6 0v-55.9h-5.6v56z"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".5" d="M190 323.5a19 19 0 0 0 5.8-2.5v-52.2H190z"/>
|
||||
<path fill="#ad1519" d="M158.1 270h48.2v-53.5H158z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".5" d="M158.1 270h48.2v-53.5H158z"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".5" d="M201 316c2.4-2 4.6-6.8 5.4-12.2l.1-35H201l.1 47.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".5" d="M206.3 302c0 12.6-10.7 22.9-24 22.9s-24.2-10.3-24.2-23v-32h48.2v32"/>
|
||||
<path fill="#ad1519" d="M254.6 270v32c0 12.6-10.8 22.9-24.1 22.9s-24.2-10.3-24.2-23v-32h48.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".5" d="M254.6 270v32c0 12.6-10.8 22.9-24.1 22.9s-24.2-10.3-24.2-23v-32h48.3"/>
|
||||
<path fill="#c8b100" d="m215.1 294.1.1.5q-.1 1-1.1 1a1 1 0 0 1-1.1-1v-.5h-1.5a2.5 2.5 0 0 0 1.8 2.9v3.9h1.6V297a3 3 0 0 0 1.7-1.6h4.4v-1.2zm21.8 0v1.2h-4l-.3.6 4.6 5.2-1.2 1-4.6-5.3-.2.1v8.7h-1.6V297h-.2l-4.8 5.2-1.2-1 4.7-5.3-.2-.4h-4V294h13zm2.6 0v1.2h4.4q.5 1.2 1.7 1.6v3.9h1.6V297a3 3 0 0 0 1.8-2.4v-.5h-1.6l.1.5q-.1 1-1 1-1.1 0-1.2-1l.1-.5zm-6.7 22.1a16 16 0 0 0 3.7-1l.8 1.4a18 18 0 0 1-4.3 1.2 3 3 0 0 1-2.6 2 3 3 0 0 1-2.5-2 18 18 0 0 1-4.6-1.2l.8-1.4q2 .8 4 1a3 3 0 0 1 1.5-1.3v-6.7h1.6v6.7q1.1.3 1.6 1.4zm-11-2.2-.8 1.4a17 17 0 0 1-3.6-3.1q-1.4.3-2.5-.5a2.4 2.4 0 0 1-.3-3.5l.1-.1a15 15 0 0 1-1.3-4.8h1.7a13 13 0 0 0 1 4q.8 0 1.4.2l4.1-4.5 1.3 1-4.1 4.5q.8 1.5-.1 2.8a15 15 0 0 0 3.1 2.6m-6-4.8q.6-.7 1.5 0c.9.7.5 1 .1 1.4a1 1 0 0 1-1.6.1 1 1 0 0 1 0-1.5m-2.2-4.5-1.6-.3-.3-4.3 1.7-.6v2.5q0 1.4.2 2.7m1.4-5.3 1.7.4v2.2c0-.8.3 2.1.3 2.1l-1.7.6-.3-2.7zm5.6 13.7a16 16 0 0 0 4.8 2.6l.4-1.6a14 14 0 0 1-4-2zm-.8 1.4a17 17 0 0 0 4.8 2.6l-1.2 1.1a19 19 0 0 1-4-2zm2.2-9.4 1.6.7 3-3.3-1-1.4zm-1.3-1-1-1.4 3-3.3 1.6.7zm18.1 9.9.8 1.4a17 17 0 0 0 3.6-3.1q1.4.3 2.5-.5a2.4 2.4 0 0 0 .3-3.5l-.1-.1a15 15 0 0 0 1.3-4.8h-1.7a13 13 0 0 1-1 4l-1.4.2-4.1-4.5-1.3 1 4.1 4.5a2.4 2.4 0 0 0 .1 2.8 15 15 0 0 1-3.1 2.6m6-4.8a1 1 0 0 0-1.5 0 1 1 0 0 0-.1 1.4 1 1 0 0 0 1.6.1 1 1 0 0 0 0-1.5m2.2-4.5 1.6-.3.3-4.3-1.7-.6v2.5q0 1.5-.2 2.8zm-1.4-5.3-1.7.4v2.2c0-.8-.3 2.1-.3 2.1l1.7.6.3-2.7zm-5.6 13.7a16 16 0 0 1-4.8 2.6l-.4-1.6a14 14 0 0 0 4-2zm.8 1.4a17 17 0 0 1-4.8 2.6l1.2 1.1a19 19 0 0 0 4-2zm-2.2-9.4-1.6.7-2.9-3.3 1-1.4zm1.3-1 1-1.4-3-3.3-1.6.7zm-20.1-8.7.5 1.6h4.5l.5-1.6zm21.1 0-.5 1.6h-4.5l-.5-1.6zm-11.6 21.9q.1-1 1.1-1a1 1 0 0 1 1.1 1q-.1 1-1 1a1 1 0 0 1-1.2-1m1.9-7.8 1.7-.4v-4.3l-1.7-.5zm-1.6 0-1.7-.4v-4.3l1.7-.5z"/>
|
||||
<path fill="#c8b100" d="M211.5 294.2q.4-1.5 1.8-2V287h1.6v5.3q1.3.5 1.7 1.6h4.4v.3h-6a1 1 0 0 0-1-.6q-.6 0-1 .6zm12.2 0v-.3h4.1l.2-.3-5-5.7 1.2-1 5 5.6.2-.1V285h1.6v7.3h.3l4.9-5.5 1.2 1-4.9 5.5.3.6h4v.3zm21.6 0a1 1 0 0 1 1-.6q.7 0 1 .6h1.6q-.4-1.5-1.8-2V287h-1.6v5.3q-1.2.4-1.7 1.6h-4.4v.3zm-30.2-15 6 6.8 1.3-1-6.1-6.7.3-.6h4.4V276h-4.4a3 3 0 0 0-2.5-1.7 2.6 2.6 0 0 0-2.7 2.5 3 3 0 0 0 1.8 2.4v5.2h1.6v-5.2zm32 0v5.3h-1.7v-5.2l-.4-.2-6 6.8-1.3-1 6.2-6.9-.1-.3h-4.5V276h4.5a3 3 0 0 1 2.4-1.7 2.6 2.6 0 0 1 2.7 2.5 2.5 2.5 0 0 1-1.9 2.4zm-16.1 0v3.3h-1.7v-3.2a3 3 0 0 1-1.7-1.6h-4V276h4a3 3 0 0 1 2.5-1.7q2 .1 2.5 1.7h4v1.6h-4a3 3 0 0 1-1.6 1.6m-17.8 4-1.7.4v4.3l1.7.5zm1.6 0 1.7.4v4.3l-1.7.5zm30.6 0-1.7.4v4.3l1.7.5zm1.6 0 1.7.4v4.3l-1.7.5zm-25.5.8 1.6-.7 2.9 3.3-1 1.4zm-1.3 1-1 1.4 3 3.3 1.6-.7zm18.5-1.1-1.6-.7-3 3.3 1 1.4zm1.2 1 1 1.4-3 3.3-1.5-.7zm-20.3 9 .5-1.6h4.5l.5 1.6zm-6.7-17q0-1 1.2-1a1 1 0 0 1 1 1q0 1-1 1a1 1 0 0 1-1.2-1m12.1.8-.5 1.6H220l-.5-1.6zm0-1.6-.5-1.6H220l-.5 1.6zm15.7 17.8-.5-1.6h-4.5l-.5 1.6zm4.4-17q.1-1 1.1-1a1 1 0 0 1 1.1 1q-.1 1-1 1a1 1 0 0 1-1.2-1m-16.1 0q.1-1 1.1-1a1 1 0 0 1 1.1 1q-.1 1-1.1 1a1 1 0 0 1-1.1-1m6.2.8.5 1.6h4.6l.5-1.6zm0-1.6.5-1.6h4.6l.5 1.6zm-5.9 5-1.7.5v4.3l1.7.5V281m1.7 0 1.6.5v4.3l-1.6.5z"/>
|
||||
<path fill="none" stroke="#c8b100" stroke-width=".3" d="M232.7 316.3a16 16 0 0 0 3.7-1.1l.8 1.4a18 18 0 0 1-4.3 1.2 3 3 0 0 1-2.6 2 3 3 0 0 1-2.5-2 18 18 0 0 1-4.6-1.2l.8-1.4q2 .8 4 1a3 3 0 0 1 1.5-1.3v-6.7h1.6v6.7q1.1.3 1.6 1.4zm-4.7-20.4-.2-.5h-4V294h4l.2-.4-5-5.6 1.2-1 5 5.5h.2V285h1.7v7.3h.2l4.9-5.5 1.2 1-4.9 5.5.3.6h4v1.5h-4q0 .3-.3.5l4.7 5.3-1.3 1-4.6-5.3-.2.1v8.7h-1.6V297l-.2-.1-4.8 5.3-1.2-1 4.7-5.3m-12.8-16.7 6 6.8 1.3-1-6.1-6.7.3-.6h4.4V276h-4.4a3 3 0 0 0-2.5-1.7 2.6 2.6 0 0 0-2.6 2.5 3 3 0 0 0 1.7 2.4v5.2h1.6v-5.2zm6.5 34.8-.8 1.4a17 17 0 0 1-3.6-3.1q-1.4.3-2.5-.5a2.4 2.4 0 0 1-.3-3.5l.1-.1a15 15 0 0 1-1.2-4.8h1.6a13 13 0 0 0 1 4q.8 0 1.4.2l4.1-4.5 1.3 1-4.1 4.5q.8 1.5-.1 2.8a15 15 0 0 0 3.1 2.6zm-8.4-13.1V297a3 3 0 0 1-1.8-2.4q.2-1.6 1.8-2.4V287h1.6v5.3q1.3.4 1.7 1.6h4.4v1.5h-4.4a3 3 0 0 1-1.6 1.6v3.9h-1.7m2.3 8.3q.7-.7 1.6 0c.9.7.5 1 .1 1.4a1 1 0 0 1-1.6.1 1 1 0 0 1 0-1.5zm-2-4.5-1.7-.3-.3-4.3 1.7-.6v2.5q0 1.4.3 2.7zm1.4-5.3 1.6.4v2.2c0-.8.3 2.1.3 2.1l-1.7.6-.3-2.7v-2.6zm5.5 13.7a16 16 0 0 0 4.8 2.6l.4-1.6a14 14 0 0 1-4-2l-1.2 1m-.8 1.4a17 17 0 0 0 4.8 2.6l-1.2 1.1a19 19 0 0 1-4-2l.4-1.7"/>
|
||||
<path fill="none" stroke="#c8b100" stroke-width=".3" d="m221.9 305.1 1.6.7 3-3.3-1-1.4-3.6 4m-1.3-1-1-1.4 3-3.3 1.6.7-3.6 4m-7.6-9.5q.1-1 1-1 1.1.1 1.2 1-.1 1-1.1 1.1a1 1 0 0 1-1.1-1zm25.7 19.4.8 1.4a17 17 0 0 0 3.6-3.1q1.4.3 2.6-.5a2.4 2.4 0 0 0 .2-3.5l-.1-.1a15 15 0 0 0 1.3-4.8h-1.7a13 13 0 0 1-1 4l-1.4.2-4.1-4.5-1.3 1 4.1 4.5a2.4 2.4 0 0 0 .1 2.8 15 15 0 0 1-3 2.6zm8.4-13.1V297a3 3 0 0 0 1.8-2.4q0-1.6-1.8-2.4V287h-1.6v5.3q-1.2.4-1.7 1.6h-4.4v1.5h4.4q.5 1.2 1.7 1.6v3.9zm-2.3 8.3a1 1 0 0 0-1.6 0 1 1 0 0 0-.1 1.4 1 1 0 0 0 1.6.1 1 1 0 0 0 0-1.5zm2-4.5 1.7-.3.3-4.3-1.7-.6v2.5q0 1.4-.2 2.7zm-1.3-5.3-1.7.4v2.2c0-.8-.3 2.1-.3 2.1l1.7.6.3-2.7v-2.6m1.6-20.1v5.2h-1.6v-5.2l-.4-.2-6 6.8-1.2-1 6-7v-.2h-4.5V276h4.4a3 3 0 0 1 2.5-1.7 2.6 2.6 0 0 1 2.6 2.5 3 3 0 0 1-1.8 2.4zm-16 0v3.2h-1.7v-3.2a3 3 0 0 1-1.7-1.6h-4V276h4q.7-1.6 2.5-1.7c1.8-.1 2.2.7 2.5 1.7h4v1.6h-4a3 3 0 0 1-1.6 1.6zm8.8 33.8a16 16 0 0 1-4.8 2.6l-.4-1.6a14 14 0 0 0 4-2l1.2 1m.8 1.4a17 17 0 0 1-4.8 2.6l1.2 1.1a19 19 0 0 0 4-2l-.4-1.7m-27.4-31.4-1.7.5v4.3l1.7.5v-5.2m1.7 0 1.6.4v4.3l-1.6.5V283m30.5 0-1.7.5v4.3l1.7.5V283"/>
|
||||
<path fill="none" stroke="#c8b100" stroke-width=".3" d="m247.1 283.1 1.7.5v4.3l-1.7.5V283m-8.6 22-1.6.7-2.9-3.3 1-1.4 3.5 4m1.3-1 1-1.4-3-3.3-1.6.7 3.6 4m-18.2-20 1.6-.7 3 3.3-1 1.4-3.6-4m-1.3 1-1 1.4 3 3.3 1.6-.7-3.6-4m18.5-1.1-1.6-.7-3 3.3 1 1.4 3.6-4m1.2 1 1 1.4-3 3.2-1.5-.6 3.5-4m-20.3 9 .5-1.6h4.5l.5 1.6h-5.5m0 1.5.5 1.6h4.5l.5-1.6h-5.5M213 277q0-1 1.2-1 1 0 1 1c0 1-.4 1-1 1a1 1 0 0 1-1.2-1zm12.1.8-.5 1.6h-4.5l-.5-1.6h5.5m0-1.6-.5-1.6h-4.5l-.5 1.6h5.5m20.1 18.5q.1-.9 1.1-1 1 .1 1.1 1-.1 1-1 1.1a1 1 0 0 1-1.2-1zm-4.4-.7-.5-1.6h-4.5l-.5 1.6h5.5m0 1.5-.5 1.6h-4.5l-.5-1.6h5.5m-11.6 21.9q.1-1 1.1-1t1.1 1c.1 1-.5 1-1 1a1 1 0 0 1-1.2-1zm1.9-7.8 1.7-.4v-4.3l-1.7-.5v5.2m-1.6 0-1.7-.4v-4.3l1.7-.5v5.2m15.7-32.6q.1-1 1.1-1a1 1 0 0 1 1.1 1q-.1 1-1 1a1 1 0 0 1-1.2-1zm-16.1 0q.1-1 1.1-1a1 1 0 0 1 1.1 1q-.1 1-1 1a1 1 0 0 1-1.2-1zm6.2.8.5 1.6h4.6l.5-1.6h-5.5m0-1.6.4-1.6h4.6l.5 1.6h-5.5m-6 5-1.6.5v4.3l1.6.5V281m1.7 0 1.6.5v4.3l-1.6.5V281"/>
|
||||
<path fill="#058e6e" d="M227.7 294.7a2.6 2.6 0 0 1 2.6-2.5 2.6 2.6 0 0 1 2.6 2.5 2.6 2.6 0 0 1-2.6 2.4c-1.4 0-2.6-1-2.6-2.4"/>
|
||||
<path fill="#db4446" d="M230.9 229.7v-.6l.1-.3-2.3-.1a6 6 0 0 1-2.3-1.2q-1-1-1.6-1.2c-1.3-.2-2.3.4-2.3.4s1 .4 1.7 1.3 1.5 1.3 1.8 1.4c.6.2 2.6 0 3.1.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M230.9 229.7v-.6l.1-.3-2.3-.1a6 6 0 0 1-2.3-1.2q-1-1-1.6-1.2c-1.3-.2-2.3.4-2.3.4s1 .4 1.7 1.3 1.5 1.3 1.8 1.4c.6.2 2.6 0 3.1.1z"/>
|
||||
<path fill="#ed72aa" stroke="#000" stroke-width=".4" d="M238.1 227.5v1.4c.2.6-.1 1.2 0 1.5q0 .5.3.9l.2.9-.7-.5-.6-.4v1l.6 1.1 1 1.3c.2.5.1 1.4.1 1.4s-.4-.7-.8-.8l-1.2-.7s.7.8.7 1.5l-.3 1.6c-.1.2-.3-.7-.8-1.1l-1-.9s.4 1.2.4 2v2.3l-.9-1-1-.7c0-.2.5.6.6 1.1s.3 2.3 1.8 4.5c1 1.3 2.3 3.6 5.3 2.9s1.9-4.8 1.3-6.7a17 17 0 0 1-1-4.6c0-.8.6-2.9.5-3.3a8 8 0 0 1 .2-3.1l.9-2.3.4-1.3.1-1.3.7 1.3.1 1.5s.1-1 1-1.6 1.8-1.1 2-1.4l.3-.5c-.1 0 0 1.8-.6 2.6l-1.7 2s.7-.3 1.2-.3h.9s-.6.4-1.4 1.6c-.8 1-.5 1.2-1 2.1s-1 1-1.7 1.5c-1 .8-.5 4.2-.4 4.7.2.5 2 4.5 2 5.5s.2 3.2-1.5 4.6c-1.1 1-3 1-3.4 1.2-.4.3-1.2 1.1-1.2 2.8s.6 2 1 2.4c.6.5 1.2.2 1.3.6l.5.7q.3.3.2.8c0 .3-.8 1.1-1.1 1.7l-.8 2.4c0 .2-.1 1 .1 1.3 0 0 .9 1 .3 1.2-.4.2-.8-.2-1-.2l-.9.5q-.3-.1-.4-.8l-.1-.7q-.3 0-.4.5c0 .2 0 .8-.3.8l-.8-.5c-.2 0-.8-.2-.8-.4q.1-.6.7-1 .8-.1.5-.5t-.7 0c-.3.4-.8 0-.7-.2v-.8c0-.2-.4-.5.1-.8s.8.2 1.4.1q.8 0 1-.6.3-.6-.2-1.4l-.9-.8-.3-.9v2.2l-.7-.8c-.3-.3-.6-1.3-.6-1.3v1.3l.2.8c-.1.1-.8-.7-1-.8l-1-1-.4-1.4v-1.5l.4-1h-1.4c-.7 0-1.2-.3-1.5.2q-.4.8.2 2.8.5 1.8.4 2.1l-.7.8h-.9l-1.2-.3h-1.3l-1.1-.3c-.3.1-.8.3-.6.7q.1.8-.5.7l-.9-.2q-.8 0-.8-.4t.4-.7q.3-.5 0-.5h-.6q-.3.4-.8.4-.3-.1-.4-1c-.1-.9-.7-1.2 0-1.1.5 0 1.3.4 1.4 0q.2-.4-.2-.7c-.4-.3-.8-.4-.3-.7l.7-.5c.1-.2.4-.8.7-.6.6.2 0 .7.6 1.3s1 1 2 .8q1.4 0 1.3-.5l-.1-1v-1s-.4.3-.5.6l-.4.8v-2l-.2-.8-.3.9-.1 1s-.7-.5-.5-1.5l.1-2c.2-.3.7-1.5 2-1.6h2.6l2-.3s-2.8-1.4-3.5-1.9a10 10 0 0 1-2-2l-.6-1.6s-.5 0-1 .3l-1.2 1-.7 1 .1-1.2v-.8s-.4 1.2-1 1.7l-1.4 1v-.8l.2-1s-.4.8-1.1 1c-.7 0-1.8 0-1.9.4q.2.8 0 1.4c0 .3-.4.5-.4.5l-.8-.4-.7.2c-.1.1-.3-.4-.2-.7s.7-.6.5-.8l-.8.2c-.3.1-.8.3-.8-.2q.2-.6 0-1 0-.4.2-.6l1.2-.1q0-.4-.8-.6-1-.2-.5-.8.3-.2.5-.6c.1-.2.2-.7.7-.5.5.3.4.8 1 1a4 4 0 0 0 2-.2l1.5-1 1.5-1-1-.8q-.5-.7-1-1l-1.8-.6-1.7-.5.8-.3q.4-.5.8-.6h.3-1.4c-.3-.1-1-.6-1.3-.6l-.8.1s.8-.4 1.4-.5l1-.1s-.9-.3-1.1-.6l-.6-1q-.2-.3-.6-.5l-1 .3q-.6 0-.6-.6l-.1-.5c-.2-.3-.6-.8-.2-1h1.4q-.2-.4-.8-.8c-.4-.2-1-.5-.7-.8l.8-.5c.2-.3.3-1 .7-.7.4.2.8 1.2 1.1 1.1s.3-.8.3-1c0-.4 0-1 .2-.9l1 .5q.8-.1 1 .2 0 .5-.6 1-.4.6-.3 1.4.4 1 1.2 1.4t1.7.9c.5.3 1.7 1.2 2.1 1.3l.8.4s.5-.2 1.1-.2 2.1 0 2.6-.2 1.3-.6 1-1c-.1-.6-1.3-1-1.2-1.4q0-.5 1.2-.4c.8 0 1.8.1 2-1q.5-1.4-.8-1.8c-1-.2-1.8-.2-2-1q-.4-1-.2-1.1.3-.3 1.4-.4 1.3 0 1.9-.2l.6-.9c.3-.2 1.4-.4 1.4-.4s1.4.7 2.7 1.7l2.2 2.1"/>
|
||||
<path d="m228.1 226.8-.2-.6v-.3s.8 0 .7.3q0 .2-.3.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m228.1 226.8-.2-.6v-.3s.8 0 .7.3q0 .2-.3.3z"/>
|
||||
<path d="M232 225.4v-.4s.7 0 1 .3c.5.4.9 1 .9 1l-.8-.4h-.5l-.3-.1v-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M232 225.4v-.4s.7 0 1 .3c.5.4.9 1 .9 1l-.8-.4h-.5l-.3-.1v-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m237.3 231.3-.4-.7-.3-.4"/>
|
||||
<path fill="#db4446" d="M217.4 226.6s.5.4.8.4h.8s.2-.5.1-.8c-.2-1.2-1.2-1.4-1.2-1.4s.3.7.1 1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M217.4 226.6s.5.4.8.4h.8s.2-.5.1-.8c-.2-1.2-1.2-1.4-1.2-1.4s.3.7.1 1z"/>
|
||||
<path fill="#db4446" d="M215.2 227.6s-.4-.7-1.3-.6-1.4.8-1.4.8h1.2c.3.3.4 1 .4 1l.7-.6z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M215.2 227.6s-.4-.7-1.3-.6-1.4.8-1.4.8h1.2c.3.3.4 1 .4 1l.7-.6z"/>
|
||||
<path fill="#db4446" d="M214.2 230.6s-.8.1-1.2.6-.3 1.3-.3 1.3.4-.5.9-.5l1 .2-.1-.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M214.2 230.6s-.8.1-1.2.6-.3 1.3-.3 1.3.4-.5.9-.5l1 .2-.1-.8z"/>
|
||||
<path d="m228.2 230.5.3-.5.3.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m228.2 230.5.3-.5.3.5h-.7"/>
|
||||
<path d="m229 230.5.3-.5.4.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m229 230.5.3-.5.4.5h-.8"/>
|
||||
<path d="m228.6 227.3.8.3-.7.4-.1-.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m228.6 227.3.8.3-.7.4-.1-.6"/>
|
||||
<path d="m229.5 227.6.7.2-.5.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m229.5 227.6.7.2-.5.4-.2-.6"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M224.2 233.7s-.7.2-1 .6q-.4.9-.3 1c.1.1.6-.5 1.5-.3l1.2.3 1.3-.3s-.7.8-.7 1.3l.2 1.1c0 .7-.6 1.6-.6 1.6l1-.3 1.7-.8.9-1s-.2 1 0 1.4l.2 1.6.8-.6q.5-.2.9-.7l.3-1s0 .8.4 1.3l.6 1.6s.3-.8.6-1.1l.7-1-.1-.9.4.8m-11 .6s.5-.8 1-1l1.1-.8.9-.4m1 5 1.3-.8 1-1"/>
|
||||
<path fill="#db4446" d="M216.6 240.4s-.4-.5-1.1-.3c-.7 0-1.2.9-1.2.9s.6-.2 1-.1.6.4.6.4l.4-.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M216.6 240.4s-.4-.5-1.1-.3c-.7 0-1.2.9-1.2.9s.6-.2 1-.1.6.4.6.4l.4-.4z"/>
|
||||
<path fill="#db4446" d="M215.8 243.2s-.6 0-1.1.3c-.5.4-.5 1.2-.5 1.2s.4-.4.8-.3l.9.2v-.6c.2-.4-.1-.8-.1-.8"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M215.8 243.2s-.6 0-1.1.3c-.5.4-.5 1.2-.5 1.2s.4-.4.8-.3l.9.2v-.6c.2-.4-.1-.8-.1-.8z"/>
|
||||
<path fill="#db4446" d="M217.2 245.8s0 .8.3 1.3c.4.5 1.1.5 1.1.5l-.3-.7.3-.8q.1-.1-.7-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M217.2 245.8s0 .8.3 1.3c.4.5 1.1.5 1.1.5l-.3-.7.3-.8q.1-.1-.7-.3zm16 1.3s2 1.2 1.9 2.2c0 1-1 2.3-1 2.3"/>
|
||||
<path fill="#db4446" d="M224.2 252.6s-.4-.6-1.1-.6-1.4.7-1.4.7.8-.1 1 .2l.5.6.5-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M224.2 252.6s-.4-.6-1.1-.6-1.4.7-1.4.7.8-.1 1 .2l.5.6.5-.3z"/>
|
||||
<path fill="#db4446" d="M222.2 255.3s-1-.1-1.4.3-.4 1.3-.4 1.3.6-.6 1-.5q.9.1 1 .3v-.7l-.3-.7"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M222.2 255.3s-1-.1-1.4.3-.4 1.3-.4 1.3.6-.6 1-.5q.9.1 1 .3v-.7l-.3-.7z"/>
|
||||
<path fill="#db4446" d="M224 258.1s-.3.7 0 1.1 1 .8 1 .8-.3-.4-.2-.8c.1-.3.7-.8.7-.8l-1.4-.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M224 258.1s-.3.7 0 1.1 1 .8 1 .8-.3-.4-.2-.8c.1-.3.7-.8.7-.8l-1.4-.2z"/>
|
||||
<path fill="#db4446" d="M236 259.3s-.8-.2-1.2 0c-.5.3-.8 1.4-.8 1.4s.7-.6 1.2-.5q.9.1 1 .3v-.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M236 259.3s-.8-.2-1.2 0c-.5.3-.8 1.4-.8 1.4s.7-.6 1.2-.5q.9.1 1 .3v-.8z"/>
|
||||
<path fill="#db4446" d="M236.4 262.2s-.6.6-.4 1.1l.6 1s0-.7.2-1l1-.3-.7-.5z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M236.4 262.2s-.6.6-.4 1.1l.6 1s0-.7.2-1l1-.3-.7-.5z"/>
|
||||
<path fill="#db4446" d="M239.4 263s-.3.8.2 1.3l1 .5q-.2-.3-.2-1.1.3-.7.5-.7l-.8-.2-.7.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M239.4 263s-.3.8.2 1.3l1 .5q-.2-.3-.2-1.1.3-.7.5-.7l-.8-.2-.7.3z"/>
|
||||
<path fill="#ffd691" stroke="#000" stroke-width=".5" d="M208.8 316.4q3 1 3 3.8c0 2.3-2.2 4-5 4-3 0-5.3-1.7-5.3-4 0-1.7 1-3.6 3-3.8l-.2-.4-.7-.7h1.2l.8.5.5-.7.6-.5.6.6.3.5.7-.4.8-.3s0 .4-.2.7l-.1.7"/>
|
||||
<path fill="#058e6e" stroke="#000" stroke-width=".5" d="M206.3 326.7s-3.8-2.6-5.5-3c-2-.4-4.5 0-5.5 0 0 0 1.2.8 1.8 1.4.5.5 2.3 1.5 3.3 1.8 3 .8 6-.2 6-.2m1 .2s2.4-2.5 5-2.9c3-.4 5 .3 6.2.6l-1.5.8c-.5.3-2 1.5-4 1.6-2 0-4.4-.3-4.8-.2l-.9.1"/>
|
||||
<path fill="#ad1519" stroke="#000" stroke-width=".5" d="M206.7 323.8a5 5 0 0 1 0-7.1 5 5 0 0 1 1.5 3.5 5 5 0 0 1-1.5 3.6"/>
|
||||
<path fill="#058e6e" stroke="#000" stroke-width=".5" d="M205.7 329s.6-1.5.6-2.7l-.1-2.1h.8s.3 1.1.3 2l-.1 2.4-.7.1-.8.3"/>
|
||||
<path fill="#fff" d="M254 190.7q.1-.9 1-1 1 .1 1.1 1-.1 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M254 190.7q.1-.9 1-1 1 .1 1.1 1-.1 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M255.4 188.2q.1-1 1.1-1t1 1c0 1-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M255.4 188.2q.1-1 1.1-1t1 1c0 1-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M256.4 185.2q.1-.9 1-1 1 .1 1.1 1c.1.9-.5 1-1 1a1 1 0 0 1-1.1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M256.4 185.2q.1-.9 1-1 1 .1 1.1 1c.1.9-.5 1-1 1a1 1 0 0 1-1.1-1z"/>
|
||||
<path fill="#fff" d="M256.5 182q.1-.9 1-1 1 .1 1.1 1-.1 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M256.5 182q.1-.9 1-1 1 .1 1.1 1-.1 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M255.7 179q.1-1 1-1 1.1 0 1.2 1c.1 1-.5 1-1.1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M255.7 179q.1-1 1-1 1.1 0 1.2 1c.1 1-.5 1-1.1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M254.1 176.1q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M254.1 176.1q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M252 173.8q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M252 173.8q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M249.4 171.8q.1-.9 1.1-1a1 1 0 0 1 0 2q-1 0-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M249.4 171.8q.1-.9 1.1-1a1 1 0 0 1 0 2q-1 0-1-1z"/>
|
||||
<path fill="#fff" d="M246.5 170.3q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M246.5 170.3q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M243.3 169.1q.1-.9 1.1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M243.3 169.1q.1-.9 1.1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M239.9 168.5q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M239.9 168.5q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M236.6 168.3q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M236.6 168.3q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M233.3 168.5q.1-1 1-1t1.1 1c.1 1-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M233.3 168.5q.1-1 1-1t1.1 1c.1 1-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M230.1 168.5q.1-1 1-1t1.1 1c.1 1-.5 1-1 1a1 1 0 0 1-1.1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M230.1 168.5q.1-1 1-1t1.1 1c.1 1-.5 1-1 1a1 1 0 0 1-1.1-1z"/>
|
||||
<path fill="#fff" stroke="#000" stroke-width=".4" d="M231.7 171.2q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1m.6 3.1q0-1 1-1c1 0 1 .4 1 1q0 .9-1 1a1 1 0 0 1-1-1m0 3q.2-.9 1.1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1m-1 2.8q.1-.9 1-1 1 .1 1.1 1 0 1-1 1a1 1 0 0 1-1-1m-1.9 2.6q.1-.9 1-1 1.1.1 1.2 1 0 1.2-1.1 1-1 0-1-1"/>
|
||||
<path fill="#fff" d="M227.6 166.5q.1-.9 1.1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M227.6 166.5q.1-.9 1.1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M224.8 165q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M224.8 165q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M221.6 164q.1-1 1-1t1.1 1q-.1.9-1 1-1-.1-1.1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M221.6 164q.1-1 1-1t1.1 1q-.1.9-1 1-1-.1-1.1-1z"/>
|
||||
<path fill="#fff" d="M218.3 163.4q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1.1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M218.3 163.4q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1.1-1z"/>
|
||||
<path fill="#fff" d="M215 163.5q.1-1 1.1-1t1 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M215 163.5q.1-1 1.1-1t1 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M211.7 164q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M211.7 164q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M208.6 165.1q.1-.9 1-1 1 .1 1.1 1c.1.9-.5 1-1 1a1 1 0 0 1-1.1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M208.6 165.1q.1-.9 1-1 1 .1 1.1 1c.1.9-.5 1-1 1a1 1 0 0 1-1.1-1z"/>
|
||||
<path fill="#fff" d="M156 190.7q0-.9 1-1c1-.1 1 .5 1 1q0 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M156 190.7q0-.9 1-1c1-.1 1 .5 1 1q0 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M154.5 188.2q.1-1 1-1t1 1c0 1-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M154.5 188.2q.1-1 1-1t1 1c0 1-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M153.5 185.2q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M153.5 185.2q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M153.4 182q.1-.9 1-1 1 .1 1.1 1-.1 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M153.4 182q.1-.9 1-1 1 .1 1.1 1-.1 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M154.2 179q.1-1 1-1t1 1c0 1-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M154.2 179q.1-1 1-1t1 1c0 1-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M155.8 176.1q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M155.8 176.1q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M158 173.8q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M158 173.8q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M160.5 171.8q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M160.5 171.8q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M163.5 170.3q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M163.5 170.3q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M166.6 169.1q.1-.9 1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M166.6 169.1q.1-.9 1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M170 168.5q.1-.9 1.1-1a1 1 0 0 1 0 2q-1 0-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M170 168.5q.1-.9 1.1-1a1 1 0 0 1 0 2q-1 0-1-1z"/>
|
||||
<path fill="#fff" d="M173.4 168.3q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M173.4 168.3q0-.9 1-1c1-.1 1 .5 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M176.6 168.5q.1-1 1-1t1.1 1c.1 1-.5 1-1 1a1 1 0 0 1-1.1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M176.6 168.5q.1-1 1-1t1.1 1c.1 1-.5 1-1 1a1 1 0 0 1-1.1-1z"/>
|
||||
<path fill="#fff" d="M179.8 168.5q.1-1 1-1 1.1 0 1.2 1c.1 1-.5 1-1.1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M179.8 168.5q.1-1 1-1 1.1 0 1.2 1c.1 1-.5 1-1.1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" stroke="#000" stroke-width=".4" d="M178.2 171.2q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1m-.7 3.1q0-1 1-1c1 0 1 .4 1 1q0 .9-1 1a1 1 0 0 1-1-1m-.2 3q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1m.9 2.8q.1-.9 1-1 1 .1 1.1 1-.1 1-1 1a1 1 0 0 1-1.1-1m1.8 2.6q.1-.9 1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1"/>
|
||||
<path fill="#fff" d="M182.3 166.5q.1-.9 1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M182.3 166.5q.1-.9 1-1a1 1 0 0 1 0 2 1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M185.2 165q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M185.2 165q0-1 1-1c1 0 1 .4 1 1s-.4 1-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M188.3 164q.1-1 1-1t1.1 1q0 .9-1 1c-1 .1-1-.5-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M188.3 164q.1-1 1-1t1.1 1q0 .9-1 1c-1 .1-1-.5-1-1z"/>
|
||||
<path fill="#fff" d="M191.6 163.4q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M191.6 163.4q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M194.9 163.5q0-1 1-1c1 0 1 .4 1 1q0 .9-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M194.9 163.5q0-1 1-1c1 0 1 .4 1 1q0 .9-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M198.2 164q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M198.2 164q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#fff" d="M201.3 165.1q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".4" d="M201.3 165.1q.1-.9 1-1 1 .1 1.1 1 0 1.2-1 1a1 1 0 0 1-1-1z"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="M174.7 228.9h-1v-1h-1.5v3.6h1.6v2.5h-3.4v7h1.8v14.3h-3.5v7.3h27.2v-7.3h-3.5V241h1.8v-7h-3.4v-2.5h1.6V228h-1.6v.9h-.8v-1h-1.6v1h-1.1v-1h-1.6v3.6h1.6v2.5H184v-7.8h1.7v-3.5H184v.9h-1v-1h-1.5v1h-.9v-1H179v3.6h1.7v7.8h-3.3v-2.5h1.6V228h-1.6v.9h-.9v-1h-1.8zm-6 33.7H196m-27.3-1.8H196m-27.3-1.8H196m-27.3-1.7H196m-27.3-2H196m-23.8-1.6h20.2m-20.2-1.8h20.2m-20.2-2h20.2m-20.2-1.7h20.2m-20.2-1.8h20.2m-20.2-1.8h20.2m-20.2-1.7h20.2m-22-1.8h23.8m-23.8-1.8h23.8m-23.8-1.8h23.8m-23.8-1.8h23.8m-20.4-1.7h17m-10.2-1.8h3.4m-3.4-1.8h3.4m-3.4-1.8h3.4m-3.4-1.7h3.4m-5.1-2.2h6.8m-12 7.5h3.6m-5-2.2h6.6m-6.7 32.6v-1.8m0-1.8v-1.7m-1.8 1.7v1.8m3.4 0V259m1.7 3.6v-1.8m0-1.8v-1.7m0-2v-1.6m0-1.8v-2m-1.7 7.4v-2m-3.4 2v-2m7 0v2m1.5-2v-1.6m-5.1-1.8v1.8m3.5-1.8v1.8m3.3-1.8v1.8M179 252v-2m1.7-1.7v1.7m0-5.3v1.8m-1.7-3.6v1.8m1.7-3.5v1.7m-3.3-1.7v1.7m-3.5-1.7v1.7m-1.6-3.5v1.8m3.3-1.8v1.8m3.4-1.8v1.8m1.7-3.6v1.8m-3.3-1.8v1.8m-3.5-1.8v1.8m-1.6-3.6v1.8m6.7-1.8v1.8m-3.4-5.3v1.8m15.3-1.8h-3.5m5-2.2h-6.6m6.7 32.6v-1.8m0-1.8v-1.7m1.8 1.7v1.8m-3.4 0V259m-1.7 3.6v-1.8m0-1.8v-1.7m0-2v-1.6m0-1.8v-2m1.7 7.4v-2m3.4 2v-2m-7 0v2m-1.5-2v-1.6m5.1-1.8v1.8m-3.5-1.8v1.8m-3.3-1.8v1.8m1.7-1.8v-2m-1.7-1.7v1.7m0-5.3v1.8m1.7-3.6v1.8m-1.7-3.5v1.7m3.3-1.7v1.7m3.5-1.7v1.7m1.6-3.5v1.8m-3.3-1.8v1.8m-3.4-1.8v1.8m-1.7-3.6v1.8m3.3-1.8v1.8m3.5-1.8v1.8m1.6-3.6v1.8m-6.7-1.8v1.8m3.4-5.3v1.8m-7 18v-2m0-5.4v-1.8m0 5.4v-1.8m0-5.3v-1.8m0-1.8v-1.7m0-3.6v-1.8m0-1.7v-1.8m-8.3 4.6h3.5m3.3-5.3h3.4m3.3 5.3h3.5"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="M186.8 262.6v-4.7c0-.8-.4-3.5-4.6-3.5-4 0-4.4 2.7-4.4 3.5v4.7z"/>
|
||||
<path fill="#c8b100" stroke="#000" stroke-width=".4" d="m179.3 258.2-2.2-.3c0-.9.2-2.2.9-2.6l2 1.5c-.3.2-.7 1-.7 1.4zm6 0 2.2-.3c0-.9-.2-2.2-.9-2.6l-2 1.5c.3.2.7 1 .7 1.4zm-2.2-2.3 1-2-2-.4-1.7.4 1.1 2zm-4.2-5.5v-4.9c0-1.3-1-2.4-2.5-2.4s-2.4 1-2.4 2.4v4.9zm6.8 0v-4.9c0-1.3 1-2.4 2.5-2.4s2.4 1 2.4 2.4v4.9zm-1.7-12 .4-4.4h-4.2l.2 4.4zm3.3 0-.4-4.4h4.4l-.5 4.4zm-10 0 .2-4.4h-4.2l.5 4.4z"/>
|
||||
<path fill="#0039f0" d="M185.3 262.6v-4c0-.7-.5-2.7-3.1-2.7-2.4 0-2.9 2-2.9 2.7v4zm-6.9-12.7v-4.2c0-1-.6-2.2-2-2.2s-2 1.1-2 2.2v4.3h4zm7.8 0v-4.2c0-1 .7-2.2 2-2.2s2 1.1 2 2.2v4.3h-4z"/>
|
||||
<path fill="#ad1519" d="M190.8 269.8c0-9.7 7-17.6 15.6-17.6s15.6 7.9 15.6 17.6-7 17.5-15.6 17.5-15.6-7.8-15.6-17.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".6" d="M190.8 269.8c0-9.7 7-17.6 15.6-17.6s15.6 7.9 15.6 17.6-7 17.5-15.6 17.5-15.6-7.8-15.6-17.5z"/>
|
||||
<path fill="#005bbf" d="M195.4 269.7c0-7 5-12.8 11-12.8s11 5.7 11 12.8-5 13-11 13-11-5.8-11-13"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".6" d="M195.4 269.7c0-7 5-12.8 11-12.8s11 5.7 11 12.8-5 13-11 13-11-5.8-11-13z"/>
|
||||
<path fill="#c8b100" d="M201.2 260.9s-1.3 1.4-1.3 2.7a6 6 0 0 0 .6 2.4q-.4-.8-1.4-.8-1.3.1-1.4 1.3l.2.8.5.9q.2-.5 1-.5 1.1 0 1 1v.2h-1.2v1h1l-.8 1.5 1-.4.8.9.8-.9 1 .4-.7-1.5h1v-1h-1.1v-.3a1 1 0 0 1 1-1q.6 0 1 .6l.4-1 .2-.7a1.4 1.4 0 0 0-1.4-1.3q-1 0-1.4.9s.6-1.2.6-2.5-1.4-2.7-1.4-2.7"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" d="M201.2 260.9s-1.3 1.4-1.3 2.7a6 6 0 0 0 .6 2.4q-.4-.8-1.4-.8-1.3.1-1.4 1.3l.2.8.5.9q.2-.5 1-.5 1.1 0 1 1v.2h-1.2v1h1l-.8 1.5 1-.4.8.9.8-.9 1 .4-.7-1.5h1v-1h-1.1v-.3a1 1 0 0 1 1-1q.6 0 1 .6l.4-1 .2-.7a1.4 1.4 0 0 0-1.4-1.3q-1 0-1.4.9s.6-1.2.6-2.5-1.4-2.7-1.4-2.7z"/>
|
||||
<path fill="#c8b100" d="M199.2 269.9h4.1v-1h-4.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M199.2 269.9h4.1v-1h-4.1z"/>
|
||||
<path fill="#c8b100" d="M211.4 260.9s-1.3 1.4-1.3 2.7.6 2.4.6 2.4q-.3-.8-1.4-.8-1.3.1-1.4 1.3l.2.8.5.9q.3-.5 1-.5a1 1 0 0 1 1 1v.2h-1.2v1h1l-.8 1.5 1-.4.8.9.8-.9 1 .4-.7-1.5h1v-1h-1.1v-.3a1 1 0 0 1 1-1q.7 0 1 .6l.4-1 .2-.7a1.4 1.4 0 0 0-1.4-1.3q-1 0-1.4.9s.6-1.2.6-2.5-1.4-2.7-1.4-2.7"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" d="M211.4 260.9s-1.3 1.4-1.3 2.7.6 2.4.6 2.4q-.3-.8-1.4-.8-1.3.1-1.4 1.3l.2.8.5.9q.3-.5 1-.5a1 1 0 0 1 1 1v.2h-1.2v1h1l-.8 1.5 1-.4.8.9.8-.9 1 .4-.7-1.5h1v-1h-1.1v-.3a1 1 0 0 1 1-1q.7 0 1 .6l.4-1 .2-.7a1.4 1.4 0 0 0-1.4-1.3q-1 0-1.4.9s.6-1.2.6-2.5-1.4-2.7-1.4-2.7z"/>
|
||||
<path fill="#c8b100" d="M209.4 269.9h4.1v-1h-4.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M209.4 269.9h4.1v-1h-4.1z"/>
|
||||
<path fill="#c8b100" d="M206.3 269.6s-1.3 1.5-1.3 2.8.6 2.4.6 2.4q-.3-.8-1.4-.9-1.3.1-1.4 1.4l.2.7.5 1q.2-.6 1-.6a1 1 0 0 1 1 1v.3h-1.2v1h1l-.8 1.5 1-.4.8.9.8-1 1 .5-.7-1.5h1v-1h-1.1v-.3a1 1 0 0 1 1-1q.6 0 .9.6l.5-1 .2-.7a1.4 1.4 0 0 0-1.4-1.4q-1 0-1.4 1s.6-1.2.6-2.5-1.4-2.7-1.4-2.7"/>
|
||||
<path fill="none" stroke="#000" stroke-linejoin="round" stroke-width=".3" d="M206.3 269.6s-1.3 1.5-1.3 2.8.6 2.4.6 2.4q-.3-.8-1.4-.9-1.3.1-1.4 1.4l.2.7.5 1q.2-.6 1-.6a1 1 0 0 1 1 1v.3h-1.2v1h1l-.8 1.5 1-.4.8.9.8-1 1 .5-.7-1.5h1v-1h-1.1v-.3a1 1 0 0 1 1-1q.6 0 .9.6l.5-1 .2-.7a1.4 1.4 0 0 0-1.4-1.4q-1 0-1.4 1s.6-1.2.6-2.5-1.4-2.7-1.4-2.7z"/>
|
||||
<path fill="#c8b100" d="M204.3 278.6h4.1v-1h-4.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M204.3 278.6h4.1v-1h-4.1z"/>
|
||||
<path fill="#c8b100" d="M237.6 223.4h-.3l-.3.4q-.4.3-.8 0l-.1-.4h-.5q-.4-.3-.1-.7v-.5h-.3l-.1.2q-.3.4-.7.2v-.2h-.3c-.5.2-.7-1-.7-1.2l-.2.2s.2.7.1 1.2l-.3 1.2a9 9 0 0 1 2.9 1.6 9 9 0 0 1 2.2 2.3l1.2-.5c.6-.2 1.3-.2 1.3-.2l.2-.2c-.3 0-1.5.1-1.5-.4v-.2h-.2q-.3-.3 0-.7l.2-.1v-.3h-.3l-.2.1q-.4.5-.8 0l-.1-.4h-.5q-.4-.3 0-.8l.2-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M237.6 223.4h-.3l-.3.4q-.4.3-.8 0l-.1-.4h-.5q-.4-.3-.1-.7v-.5h-.3l-.1.2q-.3.4-.7.2v-.2h-.3c-.5.2-.7-1-.7-1.2l-.2.2s.2.7.1 1.2l-.3 1.2a9 9 0 0 1 2.9 1.6 9 9 0 0 1 2.2 2.3l1.2-.5c.6-.2 1.3-.2 1.3-.2l.2-.2c-.3 0-1.5.1-1.5-.4v-.2h-.2q-.3-.3 0-.7l.2-.1v-.3h-.3l-.2.1q-.4.5-.8 0l-.1-.4h-.5q-.4-.3 0-.8l.2-.3z"/>
|
||||
<path d="M235.4 224h.2v.3h-.1q-.1-.1 0-.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M235.4 224h.2v.3h-.1q-.1-.1 0-.2z"/>
|
||||
<path d="m236.3 224.8-.3-.2v-.2h.1l.4.3.3.2v.2h-.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="m236.3 224.8-.3-.2v-.2h.1l.4.3.3.2v.2h-.2l-.3-.3"/>
|
||||
<path d="m234.6 223.7-.2-.2s-.1 0 0-.1l.3.1.3.1v.2h-.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="m234.6 223.7-.2-.2s-.1 0 0-.1l.3.1.3.1v.2h-.1l-.3-.1"/>
|
||||
<path d="M233.7 223h.2v.2h-.2s-.1-.1 0-.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M233.7 223h.2v.2h-.2s-.1-.1 0-.2z"/>
|
||||
<path d="M237.3 225.5v-.2h-.3l.1.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M237.3 225.5v-.2h-.3l.1.2z"/>
|
||||
<path d="m237.9 226.2.2.2h.1v-.2l-.2-.2-.2-.2h-.1v.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="m237.9 226.2.2.2h.1v-.2l-.2-.2-.2-.2h-.1v.2l.2.2"/>
|
||||
<path d="M238.8 227v-.3h-.3v.2h.3"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M238.8 227v-.3h-.3v.2h.3z"/>
|
||||
<path fill="#c8b100" d="M236.2 221.1h-.6l-.1.9v.1h.2l.7-.5-.3-.5"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M236.2 221.1h-.6l-.1.9v.1h.2l.7-.5-.3-.5"/>
|
||||
<path fill="#c8b100" d="M234.6 221.6v.5l.9.1h.1v-.2l-.5-.7z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M234.6 221.6v.5l.9.1h.1v-.2l-.5-.7-.5.3"/>
|
||||
<path fill="#c8b100" d="m236.4 222.6-.4.3-.6-.7v-.1h1.1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m236.4 222.6-.4.3-.6-.7v-.1h1.1v.5"/>
|
||||
<path fill="#c8b100" d="M235.3 222h.4v.3h-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M235.3 222h.4v.3h-.3z"/>
|
||||
<path fill="#c8b100" d="m233.2 221.1-.2-.7-.4-.4s.4-.2.8.1 0 .9 0 .9z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m233.2 221.1-.2-.7-.4-.4s.4-.2.8.1 0 .9 0 .9z"/>
|
||||
<path fill="#c8b100" d="m234.2 221.4-.4.4-.6-.6v-.2h1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m234.2 221.4-.4.4-.6-.6v-.2h1v.4"/>
|
||||
<path fill="#c8b100" d="m233.1 221 .3-.1v.3q0 .3-.2.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m233.1 221 .3-.1v.3q0 .3-.2.2z"/>
|
||||
<path fill="#c8b100" d="M238.3 222.5h-.5l-.3.7v.2h.2l.8-.4z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M238.3 222.5h-.5l-.3.7v.2h.2l.8-.4-.2-.5"/>
|
||||
<path fill="#c8b100" d="M236.7 222.8v.5l.8.2h.1v-.2l-.4-.7z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M236.7 222.8v.5l.8.2h.1v-.2l-.4-.7-.5.2"/>
|
||||
<path fill="#c8b100" d="m238.4 224-.5.2-.4-.7v-.2h.1l.9.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m238.4 224-.5.2-.4-.7v-.2h.1l.9.2-.1.5"/>
|
||||
<path fill="#c8b100" d="M237.3 223.2h.4v.4h-.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M237.3 223.2h.4v.4h-.3z"/>
|
||||
<path fill="#c8b100" d="m240.2 224.3.1.5-.8.3h-.2v-.2l.4-.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m240.2 224.3.1.5-.8.3h-.2v-.2l.4-.8.5.2"/>
|
||||
<path fill="#c8b100" d="m240 225.8-.5.1-.3-.8v-.1h.2l.8.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m240 225.8-.5.1-.3-.8v-.1h.2l.8.3-.1.5"/>
|
||||
<path fill="#c8b100" d="m238.6 224.3-.2.5.9.3h.1v-.1l-.3-.8z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m238.6 224.3-.2.5.9.3h.1v-.1l-.3-.8-.5.1"/>
|
||||
<path fill="#c8b100" d="M239.5 225.2v-.3h-.4v.3z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M239.5 225.2v-.3h-.4v.3z"/>
|
||||
<path fill="#c8b100" d="M240.8 227h.8l.5.3s.1-.4-.3-.7c-.3-.3-.8.2-.8.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M240.8 227h.8l.5.3s.1-.4-.3-.7c-.3-.3-.8.2-.8.2z"/>
|
||||
<path fill="#c8b100" d="m240.3 226.1-.3.5.8.5v-.1h.2l-.1-1z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="m240.3 226.1-.3.5.8.5v-.1h.2l-.1-1-.6.1"/>
|
||||
<path fill="#c8b100" d="M241 227s.1-.1 0-.2h-.3q-.3 0-.1.2z"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".3" d="M241 227s.1-.1 0-.2h-.3q-.3 0-.1.2zm38-21.9v.6h-2.4v-.6h1v-1.3h-.7v-.5h.6v-.6h.6v.6h.6v.6h-.6v1.2h1"/>
|
||||
<path fill="none" d="M134.4 217.1v-1.2m-.4 1.2v-1.2m-.2 1.2v-1.2m-.3 1.2v-1.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M133.2 217.1v-1.2m-.5 1.1v-1m.2 1v-1m-.7 1v-1m.2 1v-1m-.9 1v-1m.2 1v-1m.3 1v-1m-.7 1v-1m-.3.9v-.8m-.1.8v-.8m-.5.7v-.6m.2.6v-.6m-.4.5v-.5m-.2.5v-.4m-.3.3v-.3m-.3.3v-.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".2" d="M129.2 216.6v-.2"/>
|
||||
<path fill="none" d="M135.7 217v-1m-.5 1v-1m-.4 1.2V216m143 1.1V216m-.4 1.1V216m-.3 1.1V216m-.3 1.2V216"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".1" d="M276.6 217.1V216m-.6 1v-1m.3 1v-1m-.8 1v-1m.3 1v-1m-.9 1v-1m.2 1v-1m.2 1v-1m-.6 1v-1m-.3.9v-.8m-.2.8v-.8m-.4.7v-.6m.2.6v-.6m-.5.6v-.6m-.2.5v-.4m-.3.4v-.4m-.2.3v-.2"/>
|
||||
<path fill="none" stroke="#000" stroke-width=".2" d="M272.6 216.6v-.2"/>
|
||||
<path fill="none" d="M279.1 217v-1m-.6 1v-1m-.4 1.1V216"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-id" viewBox="0 0 640 480">
|
||||
<path fill="#e70011" d="M0 0h640v240H0Z"/>
|
||||
<path fill="#fff" d="M0 240h640v240H0Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 178 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-it" viewBox="0 0 640 480">
|
||||
<g fill-rule="evenodd" stroke-width="1pt">
|
||||
<path fill="#fff" d="M0 0h640v480H0z"/>
|
||||
<path fill="#009246" d="M0 0h213.3v480H0z"/>
|
||||
<path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-jp" viewBox="0 0 640 480">
|
||||
<defs>
|
||||
<clipPath id="jp-a">
|
||||
<path fill-opacity=".7" d="M-88 32h640v480H-88z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g fill-rule="evenodd" stroke-width="1pt" clip-path="url(#jp-a)" transform="translate(88 -32)">
|
||||
<path fill="#fff" d="M-128 32h720v480h-720z"/>
|
||||
<circle cx="523.1" cy="344.1" r="194.9" fill="#bc002d" transform="translate(-168.4 8.6)scale(.76554)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="flag-icons-kr" viewBox="0 0 640 480">
|
||||
<defs>
|
||||
<clipPath id="kr-a">
|
||||
<path fill-opacity=".7" d="M-95.8-.4h682.7v512H-95.8z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g fill-rule="evenodd" clip-path="url(#kr-a)" transform="translate(89.8 .4)scale(.9375)">
|
||||
<path fill="#fff" d="M-95.8-.4H587v512H-95.8Z"/>
|
||||
<g transform="rotate(-56.3 361.6 -101.3)scale(10.66667)">
|
||||
<g id="kr-c">
|
||||
<path id="kr-b" fill="#000001" d="M-6-26H6v2H-6Zm0 3H6v2H-6Zm0 3H6v2H-6Z"/>
|
||||
<use xlink:href="#kr-b" width="100%" height="100%" y="44"/>
|
||||
</g>
|
||||
<path stroke="#fff" d="M0 17v10"/>
|
||||
<path fill="#cd2e3a" d="M0-12a12 12 0 0 1 0 24Z"/>
|
||||
<path fill="#0047a0" d="M0-12a12 12 0 0 0 0 24A6 6 0 0 0 0 0Z"/>
|
||||
<circle cy="-6" r="6" fill="#cd2e3a"/>
|
||||
</g>
|
||||
<g transform="rotate(-123.7 191.2 62.2)scale(10.66667)">
|
||||
<use xlink:href="#kr-c" width="100%" height="100%"/>
|
||||
<path stroke="#fff" d="M0-23.5v3M0 17v3.5m0 3v3"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-pl" viewBox="0 0 640 480">
|
||||
<g fill-rule="evenodd">
|
||||
<path fill="#fff" d="M640 480H0V0h640z"/>
|
||||
<path fill="#dc143c" d="M640 480H0V240h640z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 219 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ru" viewBox="0 0 640 480">
|
||||
<path fill="#fff" d="M0 0h640v160H0z"/>
|
||||
<path fill="#0039a6" d="M0 160h640v160H0z"/>
|
||||
<path fill="#d52b1e" d="M0 320h640v160H0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-th" viewBox="0 0 640 480">
|
||||
<g fill-rule="evenodd">
|
||||
<path fill="#f4f5f8" d="M0 0h640v480H0z"/>
|
||||
<path fill="#2d2a4a" d="M0 162.5h640v160H0z"/>
|
||||
<path fill="#a51931" d="M0 0h640v82.5H0zm0 400h640v80H0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-tr" viewBox="0 0 640 480">
|
||||
<g fill-rule="evenodd">
|
||||
<path fill="#e30a17" d="M0 0h640v480H0z"/>
|
||||
<path fill="#fff" d="M407 247.5c0 66.2-54.6 119.9-122 119.9s-122-53.7-122-120 54.6-119.8 122-119.8 122 53.7 122 119.9"/>
|
||||
<path fill="#e30a17" d="M413 247.5c0 53-43.6 95.9-97.5 95.9s-97.6-43-97.6-96 43.7-95.8 97.6-95.8 97.6 42.9 97.6 95.9z"/>
|
||||
<path fill="#fff" d="m430.7 191.5-1 44.3-41.3 11.2 40.8 14.5-1 40.7 26.5-31.8 40.2 14-23.2-34.1 28.3-33.9-43.5 12-25.8-37z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 549 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-us" viewBox="0 0 640 480">
|
||||
<path fill="#bd3d44" d="M0 0h640v480H0"/>
|
||||
<path stroke="#fff" stroke-width="37" d="M0 55.3h640M0 129h640M0 203h640M0 277h640M0 351h640M0 425h640"/>
|
||||
<path fill="#192f5d" d="M0 0h364.8v258.5H0"/>
|
||||
<marker id="us-a" markerHeight="30" markerWidth="30">
|
||||
<path fill="#fff" d="m14 0 9 27L0 10h28L5 27z"/>
|
||||
</marker>
|
||||
<path fill="none" marker-mid="url(#us-a)" d="m0 0 16 11h61 61 61 61 60L47 37h61 61 60 61L16 63h61 61 61 61 60L47 89h61 61 60 61L16 115h61 61 61 61 60L47 141h61 61 60 61L16 166h61 61 61 61 60L47 192h61 61 60 61L16 218h61 61 61 61 60z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 648 B |
@@ -3185,13 +3185,17 @@ async function openPendingPluginInstallLinks() {
|
||||
}
|
||||
}
|
||||
|
||||
function setConnectionDialogOpen(value: boolean) {
|
||||
function setConnectionDialogOpen(value: boolean, options?: { restoreFocus?: boolean }) {
|
||||
showConnectionDialog.value = value;
|
||||
if (!value) {
|
||||
connectionDialogPrefill.value = null;
|
||||
connectionDialogUpdate.value = null;
|
||||
connectionPluginProvider.value = null;
|
||||
connectionDialogInitialTab.value = undefined;
|
||||
// Handlers that immediately open another surface (driver store page,
|
||||
// settings, or a reopened connection dialog) opt out: the delayed tree
|
||||
// refocus would steal focus back from the new surface's autofocus.
|
||||
if (options?.restoreFocus !== false) void appSidebarRef.value?.focusSelectedNode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3379,13 +3383,18 @@ async function openConnectionQuery(connectionId: string) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const tabId = queryStore.createTab(connectionId, initialTarget.database);
|
||||
// The quick-start page opens a query directly, bypassing newQuery() and its
|
||||
// default-schema fallback. Pass the configured schema here too, otherwise an
|
||||
// Oracle connection with a default schema opens the editor without a session
|
||||
// schema and unqualified tables resolve against the session default (#11844).
|
||||
const tabId = queryStore.createTab(connectionId, initialTarget.database, undefined, "query", initialTarget.schema);
|
||||
try {
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
const options = await getDatabaseOptions(connectionId);
|
||||
const target = quickConnectionOpenTarget(connection, options);
|
||||
if (target.kind === "query") {
|
||||
queryStore.updateDatabase(tabId, target.database);
|
||||
if (target.schema) queryStore.updateSchema(tabId, target.schema);
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(
|
||||
@@ -4747,19 +4756,19 @@ async function handleKeydown(e: KeyboardEvent) {
|
||||
if (isExecuteSqlShortcut(e, shortcuts) && defaultQueryShortcutTarget) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void newQuery({ preferredSource: "sidebar", openDefault: true });
|
||||
if (!e.repeat) void newQuery({ preferredSource: "sidebar", openDefault: true });
|
||||
return;
|
||||
}
|
||||
if (activeTab.value?.mode === "query" && isExecuteSqlInNewResultTabShortcut(e, shortcuts) && e.target instanceof Element && e.target.closest("[data-query-editor-root]")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!contentAreaRef.value?.shouldBlockQueryEditorExecutionShortcut?.(e)) requestActiveEditorExecuteInNewResultTab();
|
||||
if (!e.repeat && !activeTab.value.isExecuting && !contentAreaRef.value?.shouldBlockQueryEditorExecutionShortcut?.(e)) requestActiveEditorExecuteInNewResultTab();
|
||||
return;
|
||||
}
|
||||
if (activeTab.value?.mode === "query" && isExecuteSqlShortcut(e, shortcuts) && e.target instanceof Element && e.target.closest("[data-query-editor-root]")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!contentAreaRef.value?.shouldBlockQueryEditorExecutionShortcut?.(e)) requestActiveEditorExecute();
|
||||
if (!e.repeat && !activeTab.value.isExecuting && !contentAreaRef.value?.shouldBlockQueryEditorExecutionShortcut?.(e)) requestActiveEditorExecute();
|
||||
return;
|
||||
}
|
||||
if (activeTab.value?.mode === "query" && isSendSelectionToAiShortcut(e, shortcuts) && e.target instanceof Element && e.target.closest("[data-query-editor-root]")) {
|
||||
@@ -5280,7 +5289,7 @@ onUnmounted(() => {
|
||||
<div
|
||||
data-editor-content
|
||||
v-show="isSpecialPageActive || (!isAiPanelMaximized && !isHistoryPanelMaximized) || isZenMode"
|
||||
:class="isDetachedWindowContext ? 'flex-1 min-w-0 overflow-hidden bg-background' : isClassicLayout ? 'flex-1 min-w-0 overflow-hidden' : 'flex-1 min-w-0 overflow-hidden rounded-md border border-border/80 bg-background'"
|
||||
:class="isDetachedWindowContext ? 'flex-1 min-w-0 overflow-hidden bg-background' : isClassicLayout ? 'flex-1 min-w-0 overflow-hidden' : 'flex-1 min-w-0 overflow-hidden dbx-panel bg-background'"
|
||||
@mousedown="rememberEditorContentSurface"
|
||||
@focusin="rememberEditorContentSurface"
|
||||
>
|
||||
@@ -5578,7 +5587,7 @@ onUnmounted(() => {
|
||||
<div
|
||||
v-if="!isDetachedWindowContext && showAiPanel"
|
||||
v-show="!isSpecialPageActive && !isHistoryPanelMaximized && !isZenMode"
|
||||
:class="[isClassicLayout ? 'h-full relative z-30 isolate bg-background' : 'h-full relative z-30 isolate rounded-md border border-border/80 bg-background', isAiPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[240px] max-w-full']"
|
||||
:class="[isClassicLayout ? 'h-full relative z-30 isolate bg-background' : 'h-full relative z-30 isolate dbx-panel bg-background', isAiPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[240px] max-w-full']"
|
||||
:style="isAiPanelMaximized ? {} : { width: aiPanelWidth + 'px' }"
|
||||
>
|
||||
<div v-if="!isAiPanelMaximized" class="panel-resize-handle panel-resize-handle--left" @pointerdown="startAiPanelResize" />
|
||||
@@ -5607,7 +5616,7 @@ onUnmounted(() => {
|
||||
<div
|
||||
v-if="!isDetachedWindowContext && showHistory"
|
||||
v-show="!isSpecialPageActive && !isAiPanelMaximized && !isZenMode"
|
||||
:class="[isClassicLayout ? 'h-full relative z-30 isolate bg-background' : 'h-full relative z-30 isolate rounded-md border border-border/80 bg-background', isHistoryPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[240px] max-w-full']"
|
||||
:class="[isClassicLayout ? 'h-full relative z-30 isolate bg-background' : 'h-full relative z-30 isolate dbx-panel bg-background', isHistoryPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[240px] max-w-full']"
|
||||
:style="isHistoryPanelMaximized ? {} : { width: historyWidth + 'px', maxWidth: 'calc(100% - 240px)' }"
|
||||
>
|
||||
<div v-if="!isHistoryPanelMaximized" class="panel-resize-handle panel-resize-handle--left" @pointerdown="startHistoryResize" />
|
||||
@@ -5629,7 +5638,7 @@ onUnmounted(() => {
|
||||
<div
|
||||
v-if="!isDetachedWindowContext && showSqlLibraryPanel"
|
||||
v-show="!isSpecialPageActive && !isAiPanelMaximized && !isHistoryPanelMaximized && !isZenMode"
|
||||
:class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'"
|
||||
:class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate dbx-panel bg-background'"
|
||||
:style="{ width: sqlLibraryWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startSqlLibraryResize" />
|
||||
@@ -5643,7 +5652,7 @@ onUnmounted(() => {
|
||||
<div
|
||||
v-if="!isDetachedWindowContext && showSqlFilePanel"
|
||||
v-show="!isSpecialPageActive && !isAiPanelMaximized && !isHistoryPanelMaximized && !isZenMode"
|
||||
:class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'"
|
||||
:class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate dbx-panel bg-background'"
|
||||
:style="{ width: sqlFilePanelWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startSqlFilePanelResize" />
|
||||
@@ -5687,15 +5696,15 @@ onUnmounted(() => {
|
||||
)
|
||||
"
|
||||
@open-driver-store="
|
||||
setConnectionDialogOpen(false);
|
||||
setConnectionDialogOpen(false, { restoreFocus: false });
|
||||
openDriverStorePage($event);
|
||||
"
|
||||
@open-tunnel-profile-settings="
|
||||
setConnectionDialogOpen(false);
|
||||
setConnectionDialogOpen(false, { restoreFocus: false });
|
||||
openSettings('tunnels');
|
||||
"
|
||||
@open-connection-settings="
|
||||
setConnectionDialogOpen(false);
|
||||
setConnectionDialogOpen(false, { restoreFocus: false });
|
||||
openConnectionSettings($event, 'advanced');
|
||||
"
|
||||
@open-lineage-target="openLineageTarget"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import PasswordInput from "@/components/ui/PasswordInput.vue";
|
||||
import SavePasswordCheckbox from "@/components/connection/SavePasswordCheckbox.vue";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { sidebarProductionBadgeScope } from "@/lib/sidebar/sidebarProductionBadge";
|
||||
import type { ProductionBadgeScope } from "@/types/database";
|
||||
@@ -680,6 +681,7 @@ const gaussdbDriverMode = computed<GaussdbConnectionMode>({
|
||||
},
|
||||
});
|
||||
const isGaussdbMJdbcConnection = computed(() => gaussdbDriverMode.value === "m-jdbc");
|
||||
const isGaussdbDwsConnection = computed(() => gaussdbDriverMode.value === "dws");
|
||||
const showGaussdbIdentifierQuoteStyle = computed(() => supportsGaussdbIdentifierQuoteStyle(form.value));
|
||||
const gaussdbQuoteStyle = computed<GaussdbIdentifierQuoteStyle>({
|
||||
get: () => gaussdbIdentifierQuoteStyle(form.value),
|
||||
@@ -5316,6 +5318,13 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi
|
||||
if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) {
|
||||
throw new Error(t("connection.etcdClientCertPairRequired"));
|
||||
}
|
||||
} else if (config.db_type === "redis") {
|
||||
config.etcd_endpoints = undefined;
|
||||
config.client_cert_path = config.client_cert_path?.trim() || "";
|
||||
config.client_key_path = config.client_key_path?.trim() || "";
|
||||
if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) {
|
||||
throw new Error(t("connection.redisClientCertPairRequired"));
|
||||
}
|
||||
} else if (form.value.db_type !== "consul" && config.db_type !== "elasticsearch" && config.db_type !== "easysearch" && config.db_type !== "solr") {
|
||||
config.etcd_endpoints = undefined;
|
||||
config.client_cert_path = undefined;
|
||||
@@ -5337,6 +5346,7 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi
|
||||
config.db_type !== "doris" &&
|
||||
config.db_type !== "mongodb" &&
|
||||
config.db_type !== "victoriametrics" &&
|
||||
config.db_type !== "redis" &&
|
||||
config.db_type !== "zookeeper" &&
|
||||
config.db_type !== "elasticsearch" &&
|
||||
config.db_type !== "easysearch" &&
|
||||
@@ -6929,6 +6939,29 @@ async function browseEtcdTlsFile(target: "ca" | "cert" | "key") {
|
||||
}
|
||||
}
|
||||
|
||||
async function browseRedisTlsFile(target: "ca" | "cert" | "key") {
|
||||
if (isTauriRuntime()) {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
title: target === "ca" ? t("connection.redisCaCertBrowse") : target === "cert" ? t("connection.redisClientCertBrowse") : t("connection.redisClientKeyBrowse"),
|
||||
multiple: false,
|
||||
filters: [
|
||||
{ name: "PEM", extensions: ["pem", "crt", "cer", "key"] },
|
||||
{ name: "All Files", extensions: ["*"] },
|
||||
],
|
||||
});
|
||||
if (selected && typeof selected === "string") {
|
||||
if (target === "ca") {
|
||||
form.value.ca_cert_path = selected;
|
||||
} else if (target === "cert") {
|
||||
form.value.client_cert_path = selected;
|
||||
} else {
|
||||
form.value.client_key_path = selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function browseHiveKerberosFile(target: "krb5" | "jaas") {
|
||||
if (isTauriRuntime()) {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
@@ -7605,18 +7638,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<span />
|
||||
<div class="col-span-3 flex items-center gap-1.5 text-sm">
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="form.save_password" type="checkbox" class="h-4 w-4 rounded border-border accent-primary" :aria-label="t('connection.savePassword')" />
|
||||
<span class="whitespace-nowrap">{{ t("connection.savePassword") }}</span>
|
||||
</label>
|
||||
<HelpTooltip :label="t('connection.savePassword')">
|
||||
{{ form.save_password ? t("connection.savePasswordHint") : t("connection.savePasswordSessionHint") }}
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelTopClass">{{ t("connection.jdbcDriverPaths") }}</Label>
|
||||
<div class="col-span-3 space-y-2">
|
||||
@@ -7755,6 +7777,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.sqliteCipherKey") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" :placeholder="t('connection.sqliteCipherKeyPlaceholder')" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-if="form.db_type === 'sqlite' && !sqliteUsesSsh" v-model="form.save_password" />
|
||||
<div v-if="form.db_type === 'sqlite' && !sqliteUsesSsh" class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelTopClass">{{ t("connection.sqliteExtensions") }}</Label>
|
||||
<div class="col-span-3 space-y-1">
|
||||
@@ -7802,6 +7825,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}{{ form.db_type === "access" ? t("connection.optionalSuffix") : "" }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -8235,6 +8259,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" :placeholder="t('connection.databasePlaceholder')" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.redisKeySeparator") }}</Label>
|
||||
<Input v-model="form.redis_key_separator" class="col-span-3 h-8 text-xs" placeholder=":" />
|
||||
@@ -8270,6 +8295,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.consulToken") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" :placeholder="t('connection.consulTokenPlaceholder')" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.consulDatacenter") }}</Label>
|
||||
<Input v-model="consulDatacenter" class="col-span-3" placeholder="dc1" />
|
||||
@@ -8375,6 +8401,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
</template>
|
||||
|
||||
<!-- ZooKeeper: host, connect string, user, password -->
|
||||
@@ -8418,6 +8445,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
</template>
|
||||
|
||||
<!-- DynamoDB: endpoint, region, AWS credentials -->
|
||||
@@ -8446,6 +8474,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.dynamodbSecretAccessKey") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.dynamodbSessionToken") }}</Label>
|
||||
<PasswordInput v-model="form.connection_string" class="col-span-3" :placeholder="t('connection.dynamodbSessionTokenPlaceholder')" />
|
||||
@@ -8550,6 +8579,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-if="mongoAuthMechanism !== 'MONGODB-OIDC'" v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.defaultDatabase") }}</Label>
|
||||
<Input v-model="form.database" class="col-span-3" :placeholder="t('connection.databasePlaceholder')" />
|
||||
@@ -8701,6 +8731,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.victoriametricsApiPath") }}</Label>
|
||||
<div class="col-span-3 space-y-1.5">
|
||||
@@ -8757,6 +8788,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">Token</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
</template>
|
||||
<template v-else-if="influxDbVersion === '3'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
@@ -8767,6 +8799,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">Token</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
@@ -8777,6 +8810,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.database") }}</Label>
|
||||
<Input v-model="form.database" class="col-span-3" :placeholder="t('connection.databasePlaceholder')" />
|
||||
@@ -8805,6 +8839,7 @@ function openExternalUrl(url: string) {
|
||||
<PasswordInput v-model="form.password" class="col-span-3" placeholder="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..." />
|
||||
</div>
|
||||
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 text-xs text-muted-foreground">{{ t("connection.tursoTokenHint") }} <code class="px-1 py-0.5 rounded bg-muted text-xs">turso db tokens create <database-name></code></p>
|
||||
@@ -8894,6 +8929,7 @@ function openExternalUrl(url: string) {
|
||||
<Label :class="connectionLabelClass">{{ t("connection.salesforceAccessToken") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" :placeholder="t('connection.salesforceAccessTokenPlaceholder')" />
|
||||
</div>
|
||||
<SavePasswordCheckbox v-model="form.save_password" />
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 text-xs leading-5 text-muted-foreground">{{ t("connection.salesforceAccessTokenHint") }}</p>
|
||||
@@ -9264,18 +9300,7 @@ function openExternalUrl(url: string) {
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
|
||||
<div v-if="form.db_type !== 'spanner'" class="grid grid-cols-4 items-center gap-4">
|
||||
<span />
|
||||
<div class="col-span-3 flex items-center gap-1.5 text-sm">
|
||||
<label class="flex items-center gap-2">
|
||||
<input v-model="form.save_password" type="checkbox" class="h-4 w-4 rounded border-border accent-primary" :aria-label="t('connection.savePassword')" />
|
||||
<span class="whitespace-nowrap">{{ t("connection.savePassword") }}</span>
|
||||
</label>
|
||||
<HelpTooltip :label="t('connection.savePassword')">
|
||||
{{ form.save_password ? t("connection.savePasswordHint") : t("connection.savePasswordSessionHint") }}
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<SavePasswordCheckbox v-if="form.db_type !== 'spanner'" v-model="form.save_password" />
|
||||
|
||||
<div v-if="form.db_type !== 'hbase' && form.db_type !== 'meilisearch' && form.db_type !== 'solr' && form.db_type !== 'spanner'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ databaseLabel }}</Label>
|
||||
@@ -9845,6 +9870,55 @@ function openExternalUrl(url: string) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<template v-if="form.db_type === 'redis'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.redisCaCert") }}</Label>
|
||||
<div class="col-span-3 flex items-center gap-1">
|
||||
<Input v-model="form.ca_cert_path" class="flex-1" :placeholder="t('connection.redisCaCertPlaceholder')" :disabled="!form.ssl" />
|
||||
<Tooltip v-if="isDesktop">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" :disabled="!form.ssl" @click="browseRedisTlsFile('ca')">
|
||||
<FolderOpen class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("connection.redisCaCertBrowse") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.redisClientCert") }}</Label>
|
||||
<div class="col-span-3 flex items-center gap-1">
|
||||
<Input v-model="form.client_cert_path" class="flex-1" :placeholder="t('connection.redisClientCertPlaceholder')" :disabled="!form.ssl" />
|
||||
<Tooltip v-if="isDesktop">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" :disabled="!form.ssl" @click="browseRedisTlsFile('cert')">
|
||||
<FolderOpen class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("connection.redisClientCertBrowse") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.redisClientKey") }}</Label>
|
||||
<div class="col-span-3 flex items-center gap-1">
|
||||
<Input v-model="form.client_key_path" class="flex-1" :placeholder="t('connection.redisClientKeyPlaceholder')" :disabled="!form.ssl" />
|
||||
<Tooltip v-if="isDesktop">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" :disabled="!form.ssl" @click="browseRedisTlsFile('key')">
|
||||
<FolderOpen class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("connection.redisClientKeyBrowse") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 text-xs leading-5 text-muted-foreground">{{ t("connection.redisTlsCertificateHint") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="form.db_type === 'etcd' || form.db_type === 'consul' || form.db_type === 'zookeeper' || form.db_type === 'elasticsearch' || form.db_type === 'easysearch' || form.db_type === 'nebula'">
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallPaddedClass">
|
||||
@@ -10239,10 +10313,11 @@ function openExternalUrl(url: string) {
|
||||
<SelectContent>
|
||||
<SelectItem value="native">{{ t("connection.gaussdbConnectionModeNative") }}</SelectItem>
|
||||
<SelectItem value="m-jdbc">{{ t("connection.gaussdbConnectionModeMJdbc") }}</SelectItem>
|
||||
<SelectItem value="dws">{{ t("connection.gaussdbConnectionModeDws") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-xs leading-5 text-muted-foreground">
|
||||
{{ isGaussdbMJdbcConnection ? t("connection.gaussdbConnectionModeMJdbcHint") : t("connection.gaussdbConnectionModeNativeHint") }}
|
||||
{{ isGaussdbMJdbcConnection ? t("connection.gaussdbConnectionModeMJdbcHint") : isGaussdbDwsConnection ? t("connection.gaussdbConnectionModeDwsHint") : t("connection.gaussdbConnectionModeNativeHint") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { HelpTooltip } from "@/components/ui/tooltip";
|
||||
|
||||
const props = withDefaults(defineProps<{ modelValue?: boolean }>(), { modelValue: true });
|
||||
const emit = defineEmits<{ "update:modelValue": [value: boolean] }>();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<span />
|
||||
<div class="col-span-3 flex items-center gap-1.5 text-sm">
|
||||
<label class="flex items-center gap-2">
|
||||
<input :checked="props.modelValue" type="checkbox" class="h-4 w-4 rounded border-border accent-primary" :aria-label="t('connection.savePassword')" @change="emit('update:modelValue', ($event.target as HTMLInputElement).checked)" />
|
||||
<span class="whitespace-nowrap">{{ t("connection.savePassword") }}</span>
|
||||
</label>
|
||||
<HelpTooltip :label="t('connection.savePassword')">
|
||||
{{ props.modelValue ? t("connection.savePasswordHint") : t("connection.savePasswordSessionHint") }}
|
||||
</HelpTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,278 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
const { store, settings, backend } = vi.hoisted(() => ({
|
||||
store: {
|
||||
connectionGroupOptions: [],
|
||||
newConnectionGroupId: null,
|
||||
selectedConnectionGroupId: null,
|
||||
addConnection: vi.fn(),
|
||||
updateConnection: vi.fn(),
|
||||
connect: vi.fn(),
|
||||
ensureConnected: vi.fn(),
|
||||
addEphemeralConnection: vi.fn(),
|
||||
removeConnection: vi.fn(),
|
||||
startEditing: vi.fn(),
|
||||
stopEditing: vi.fn(),
|
||||
clearConnectionError: vi.fn(),
|
||||
updateConnectionDatabaseInfo: vi.fn(),
|
||||
applyGlobalTimeouts: vi.fn((config) => config),
|
||||
getConfig: vi.fn(),
|
||||
},
|
||||
settings: {
|
||||
editorSettings: { sidebarShowConnectionNotes: false, globalConnectTimeoutSecs: 10, globalQueryTimeoutSecs: 0 },
|
||||
rememberedDatabaseForConnection: vi.fn(() => ""),
|
||||
persistEditorSettings: vi.fn(),
|
||||
updateEditorSettings: vi.fn(),
|
||||
updateEditorSettingsAndPersist: vi.fn(),
|
||||
},
|
||||
backend: {
|
||||
connectDb: vi.fn(),
|
||||
disconnectDb: vi.fn(),
|
||||
testConnectionWithInfo: vi.fn(),
|
||||
listDatabases: vi.fn(),
|
||||
listPlugins: vi.fn(),
|
||||
listJdbcDrivers: vi.fn(),
|
||||
listJdbcMavenBundles: vi.fn(),
|
||||
listJdbcLocalBundles: vi.fn(),
|
||||
listSshConfigHosts: vi.fn(),
|
||||
listInstalledAgentsLocal: vi.fn(),
|
||||
listenAgentInstallProgress: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({ useConnectionStore: () => store, CONNECTION_ATTEMPT_CANCELLED_MESSAGE: "cancelled" }));
|
||||
vi.mock("@/stores/settingsStore", async (original) => ({ ...(await original<typeof import("@/stores/settingsStore")>()), useSettingsStore: () => settings }));
|
||||
vi.mock("@/stores/tunnelProfileStore", () => ({ useTunnelProfileStore: () => ({ profiles: [], profileById: () => undefined, init: vi.fn() }) }));
|
||||
vi.mock("@/lib/backend/api", async (original) => ({ ...(await original<typeof import("@/lib/backend/api")>()), ...backend }));
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
||||
|
||||
vi.mock("@/components/ui/dialog", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
const passthrough = defineComponent({
|
||||
setup:
|
||||
(_props, { slots }) =>
|
||||
() =>
|
||||
h("div", slots.default?.()),
|
||||
});
|
||||
const Dialog = defineComponent({
|
||||
props: { open: Boolean },
|
||||
emits: ["update:open"],
|
||||
setup:
|
||||
(props, { slots, emit }) =>
|
||||
() =>
|
||||
props.open ? h("section", [h("button", { "data-testid": "dismiss-dialog", onClick: () => emit("update:open", false) }, "Dismiss"), slots.default?.()]) : null,
|
||||
});
|
||||
return { Dialog, DialogContent: passthrough, DialogHeader: passthrough, DialogTitle: passthrough, DialogFooter: passthrough };
|
||||
});
|
||||
vi.mock("@/components/ui/button", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Button: defineComponent({
|
||||
inheritAttrs: false,
|
||||
setup:
|
||||
(_props, { attrs, slots }) =>
|
||||
() =>
|
||||
h("button", attrs, slots.default?.()),
|
||||
}),
|
||||
};
|
||||
});
|
||||
vi.mock("@/components/ui/input", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Input: defineComponent({
|
||||
props: ["modelValue"],
|
||||
emits: ["update:modelValue"],
|
||||
setup:
|
||||
(props, { attrs, emit }) =>
|
||||
() =>
|
||||
h("input", { ...attrs, value: props.modelValue, onInput: (event: Event) => emit("update:modelValue", (event.target as HTMLInputElement).value) }),
|
||||
}),
|
||||
};
|
||||
});
|
||||
vi.mock("@/components/ui/PasswordInput.vue", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
default: defineComponent({
|
||||
props: ["modelValue"],
|
||||
setup:
|
||||
(props, { attrs }) =>
|
||||
() =>
|
||||
h("input", { ...attrs, type: "password", value: props.modelValue }),
|
||||
}),
|
||||
};
|
||||
});
|
||||
vi.mock("@/components/ui/tabs", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
const pass = defineComponent({
|
||||
setup:
|
||||
(_props, { slots }) =>
|
||||
() =>
|
||||
h("div", slots.default?.()),
|
||||
});
|
||||
const TabsContent = defineComponent({
|
||||
props: ["value"],
|
||||
setup:
|
||||
(props, { slots }) =>
|
||||
() =>
|
||||
props.value === "connection" ? h("div", slots.default?.()) : null,
|
||||
});
|
||||
return { Tabs: pass, TabsContent, TabsList: pass, TabsTrigger: pass };
|
||||
});
|
||||
vi.mock("@/components/ui/tooltip", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
const pass = defineComponent({
|
||||
setup:
|
||||
(_props, { slots }) =>
|
||||
() =>
|
||||
h("span", slots.default?.()),
|
||||
});
|
||||
return { HelpTooltip: pass, Tooltip: pass, TooltipContent: pass, TooltipTrigger: pass };
|
||||
});
|
||||
|
||||
import ConnectionDialog from "@/components/connection/ConnectionDialog.vue";
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
function salesforceConnection(overrides: Partial<ConnectionConfig> = {}): ConnectionConfig {
|
||||
return {
|
||||
id: "sfdc-sandbox",
|
||||
name: "Salesforce sandbox",
|
||||
db_type: "salesforce",
|
||||
driver_profile: "salesforce",
|
||||
driver_label: "Salesforce",
|
||||
host: "https://acme--qas1.sandbox.my.salesforce.com",
|
||||
port: 443,
|
||||
password: "saved-access-token",
|
||||
ssl: true,
|
||||
save_password: true,
|
||||
...overrides,
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
async function settle() {
|
||||
for (let i = 0; i < 8; i++) await nextTick();
|
||||
}
|
||||
|
||||
async function mountDialog(config: ConnectionConfig) {
|
||||
const props = reactive({ open: true, editConfig: config });
|
||||
store.getConfig.mockReturnValue(config);
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () =>
|
||||
h(ConnectionDialog, {
|
||||
...props,
|
||||
"onUpdate:open": (value: boolean) => {
|
||||
props.open = value;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
app.use(i18n);
|
||||
mountedApps.push(app);
|
||||
app.mount(container);
|
||||
await settle();
|
||||
return props;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
i18n.global.locale.value = "en";
|
||||
for (const fn of Object.values(backend)) fn.mockResolvedValue([]);
|
||||
backend.listenAgentInstallProgress.mockResolvedValue(() => {});
|
||||
store.updateConnection.mockResolvedValue(undefined);
|
||||
store.addConnection.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function redisConnection(overrides: Partial<ConnectionConfig> = {}): ConnectionConfig {
|
||||
return {
|
||||
id: "redis-1",
|
||||
name: "Redis",
|
||||
db_type: "redis",
|
||||
host: "127.0.0.1",
|
||||
port: 6379,
|
||||
password: "secret",
|
||||
save_password: true,
|
||||
...overrides,
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function savePasswordCheckbox(): HTMLInputElement | null {
|
||||
return document.querySelector<HTMLInputElement>('input[aria-label="Save password"]');
|
||||
}
|
||||
|
||||
describe("ConnectionDialog save-password checkbox", () => {
|
||||
it("renders on the Redis form and toggles form.save_password", async () => {
|
||||
await mountDialog(redisConnection());
|
||||
|
||||
const checkbox = savePasswordCheckbox();
|
||||
expect(checkbox, "Redis form should render the save-password checkbox").toBeTruthy();
|
||||
expect(checkbox!.checked).toBe(true);
|
||||
|
||||
checkbox!.click();
|
||||
await settle();
|
||||
|
||||
expect(savePasswordCheckbox()!.checked).toBe(false);
|
||||
});
|
||||
|
||||
it("renders on the MongoDB form mode", async () => {
|
||||
await mountDialog({
|
||||
id: "mongo-1",
|
||||
name: "MongoDB",
|
||||
db_type: "mongodb",
|
||||
host: "127.0.0.1",
|
||||
port: 27017,
|
||||
username: "dbx",
|
||||
password: "secret",
|
||||
save_password: true,
|
||||
} as ConnectionConfig);
|
||||
|
||||
expect(savePasswordCheckbox(), "MongoDB form should render the save-password checkbox").toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders on the etcd form", async () => {
|
||||
await mountDialog({
|
||||
id: "etcd-1",
|
||||
name: "etcd",
|
||||
db_type: "etcd",
|
||||
host: "127.0.0.1",
|
||||
port: 2379,
|
||||
username: "root",
|
||||
password: "secret",
|
||||
save_password: true,
|
||||
} as ConnectionConfig);
|
||||
|
||||
expect(savePasswordCheckbox(), "etcd form should render the save-password checkbox").toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders on the Salesforce access-token form", async () => {
|
||||
await mountDialog(salesforceConnection());
|
||||
|
||||
expect(savePasswordCheckbox(), "Salesforce token form should render the save-password checkbox").toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not render when the connection has no password field", async () => {
|
||||
await mountDialog({
|
||||
id: "spanner-1",
|
||||
name: "Spanner",
|
||||
db_type: "spanner",
|
||||
host: "project/instance",
|
||||
database: "db",
|
||||
save_password: true,
|
||||
} as ConnectionConfig);
|
||||
|
||||
expect(savePasswordCheckbox()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -387,6 +387,7 @@ import {
|
||||
} from "@/lib/settings/settingsSearch";
|
||||
import { findSettingsSearchHighlightTarget } from "@/lib/settings/settingsSearchHighlight";
|
||||
import { LOCALE_OPTIONS } from "@/lib/app/localeOptions";
|
||||
import LocaleFlag from "@/components/icons/LocaleFlag.vue";
|
||||
import {
|
||||
DEFAULT_WEB_DAV_AUTO_UPLOAD_INTERVAL_MINUTES,
|
||||
DEFAULT_WEB_DAV_REMOTE_PATH,
|
||||
@@ -8293,22 +8294,18 @@ onUnmounted(() => {
|
||||
<Label class="whitespace-normal leading-tight">{{ t("settings.languageTitle") }}</Label>
|
||||
</div>
|
||||
<Select :model-value="currentLocale()" @update:model-value="onLocaleChange" @update:open="onLocaleOpenChange">
|
||||
<SelectTrigger class="h-8 w-full gap-0.5 px-0.5">
|
||||
<SelectTrigger class="h-8 w-full gap-0.5 px-0.5" :aria-label="t('settings.languageTitle')">
|
||||
<SelectValue>
|
||||
<span v-if="selectedLocaleOption" class="flex min-w-0 items-center gap-0.5">
|
||||
<span class="inline-flex h-5 shrink-0 items-center justify-center text-sm font-medium leading-none">
|
||||
{{ selectedLocaleOption.flag }}
|
||||
</span>
|
||||
<LocaleFlag :option="selectedLocaleOption" />
|
||||
<span class="truncate">{{ selectedLocaleOption.label }}</span>
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent class="w-[150px]" @pointerleave="restoreLocaleOptionPreview">
|
||||
<SelectItem v-for="locale in LOCALE_OPTIONS" :key="locale.value" :value="locale.value" @pointerenter="scheduleLocaleOptionPreview(locale.value)" @focus="previewLocaleOption(locale.value)">
|
||||
<SelectItem v-for="locale in LOCALE_OPTIONS" :key="locale.value" :value="locale.value" :text-value="locale.label" @pointerenter="scheduleLocaleOptionPreview(locale.value)" @focus="previewLocaleOption(locale.value)">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="inline-flex h-5 w-6 shrink-0 items-center justify-center text-sm font-medium leading-none">
|
||||
{{ locale.flag }}
|
||||
</span>
|
||||
<LocaleFlag :option="locale" />
|
||||
<span>{{ locale.label }}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
@@ -137,8 +137,9 @@ import { selectLineEnds } from "@/lib/editor/selectLineEnds";
|
||||
import { scrollEditorLine } from "@/lib/editor/scrollEditorLine";
|
||||
import { createQueryEditorStringMouseSelection } from "@/lib/editor/queryEditorStringMouseSelection";
|
||||
import { createQueryEditorCompletionShortcutBindings } from "@/lib/editor/queryEditorCompletionShortcut";
|
||||
import { handleQueryEditorCompletionNavigation } from "@/lib/editor/queryEditorCompletionNavigation";
|
||||
import { createQueryEditorSelectionCaseShortcutBindings } from "@/lib/editor/queryEditorSelectionCaseShortcut";
|
||||
import { createQueryEditorExecutionShortcutBindings, createQueryEditorPostCompositionKeyGuard } from "@/lib/editor/queryEditorExecutionShortcut";
|
||||
import { createQueryEditorExecutionShortcutBindings, createQueryEditorExecutionShortcutHandler, createQueryEditorPostCompositionKeyGuard } from "@/lib/editor/queryEditorExecutionShortcut";
|
||||
import { supportsQueryEditorBlockComments, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
|
||||
import { queryContextObjectRoute, queryTableCandidateAtSqlPosition, resolveQueryContextCandidateDatabase, resolveQueryContextObjectTarget, type QueryContextObjectAction } from "@/lib/sql/queryCursorTableTarget";
|
||||
import * as api from "@/lib/backend/api";
|
||||
@@ -1311,8 +1312,16 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
shortcuts.executeSql,
|
||||
() => requestExecute({ bypassPicker: true }),
|
||||
(currentView) => shouldBlockExecutionShortcut(undefined, currentView),
|
||||
() => !props.isExecuting,
|
||||
);
|
||||
const executeInNewResultTabBindings = props.hideExecutionControls
|
||||
? []
|
||||
: createQueryEditorExecutionShortcutBindings(
|
||||
shortcuts.executeSqlInNewResultTab,
|
||||
requestExecuteInNewResultTab,
|
||||
(currentView) => shouldBlockExecutionShortcut(undefined, currentView),
|
||||
() => !props.isExecuting,
|
||||
);
|
||||
const executeInNewResultTabBindings = props.hideExecutionControls ? [] : createQueryEditorExecutionShortcutBindings(shortcuts.executeSqlInNewResultTab, requestExecuteInNewResultTab, (currentView) => shouldBlockExecutionShortcut(undefined, currentView));
|
||||
const explainBindings = props.enableExplainShortcut
|
||||
? createQueryEditorExecutionShortcutBindings(
|
||||
shortcuts.explainSql,
|
||||
@@ -1330,6 +1339,11 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
openReplace,
|
||||
isReadOnly: () => !!props.readOnly,
|
||||
});
|
||||
const executionShortcutHandler = createQueryEditorExecutionShortcutHandler({
|
||||
getShortcuts: () => shortcuts,
|
||||
isExecuting: () => !!props.isExecuting,
|
||||
enableExplainShortcut: () => !!props.enableExplainShortcut,
|
||||
});
|
||||
const sqlShortcutActions = enabledSqlShortcutActions(settingsStore.editorSettings.sqlShortcuts);
|
||||
const sqlShortcutKeymapBindings = uniqueSqlShortcutBindings(sqlShortcutActions).filter((shortcut) => !isCharacterProducingShortcut(shortcut));
|
||||
// Do not set preventDefault: true — when run returns false (wrong DB scope / no table token),
|
||||
@@ -1354,6 +1368,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
() => props.databaseType,
|
||||
);
|
||||
const combinedDomKeydownHandler = (event: KeyboardEvent, view: EditorViewType) => {
|
||||
if (executionShortcutHandler(event)) return true;
|
||||
if (replaceShortcutHandler(event)) return true;
|
||||
return sqlShortcutDomHandler(event, view);
|
||||
};
|
||||
@@ -2157,6 +2172,11 @@ const codeMirrorLifecycle = useQueryEditorCodeMirror({
|
||||
EditorView.domEventHandlers({
|
||||
keydown(event, currentView) {
|
||||
const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
|
||||
// The configured Quick Open shortcut (Ctrl-P on Windows/Linux)
|
||||
// must keep working even while a completion list is open; yield
|
||||
// before completion navigation can consume and preventDefault it.
|
||||
if (matchesShortcut(event, shortcuts.quickOpen)) return false;
|
||||
if (handleQueryEditorCompletionNavigation(currentView, event, initializedRuntime.codeMirrorMoveCompletionSelection)) return true;
|
||||
if (matchesShortcut(event, shortcuts.selectLineEnds)) {
|
||||
event.preventDefault();
|
||||
return selectLineEnds(currentView);
|
||||
@@ -2896,6 +2916,8 @@ function unfoldAll(): boolean {
|
||||
}
|
||||
|
||||
function shouldBlockExecutionShortcut(event?: KeyboardEvent, currentView: EditorViewType | null = view.value): boolean {
|
||||
if (props.isExecuting) return true;
|
||||
if (event?.repeat) return true;
|
||||
return (currentView ? isEditorComposing(currentView) : false) || (event ? postCompositionKeyGuard.blocks(event) : false);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ export function createQueryEditorHoverContent(options: QueryEditorHoverContentOp
|
||||
layoutController || handleCopy || searchController
|
||||
? () => {
|
||||
layoutController?.mount();
|
||||
searchController?.pinTooltip(dom.closest<HTMLElement>(".cm-tooltip") ?? dom);
|
||||
if (handleCopy) document.addEventListener("copy", handleCopy);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface QueryEditorProps {
|
||||
hideExecutionControls?: boolean;
|
||||
enableExplainShortcut?: boolean;
|
||||
canExplain?: boolean;
|
||||
isExecuting?: boolean;
|
||||
initialViewport?: { scrollTop: number; scrollLeft: number };
|
||||
initialSelection?: { anchor: number; head: number };
|
||||
revealRequest?: { id: number; line: number; column?: number };
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { EditorView as EditorViewType } from "@codemirror/view";
|
||||
import { type QueryCompletionItem, type BatchColumnSelectionActionItem } from "./useQueryEditorBatchSelection";
|
||||
import type { CompletionMetadataScope } from "./queryEditorTypes";
|
||||
import { completionMatchRanges } from "@/lib/common/completionMatch";
|
||||
import { matchesIdentifierSearch } from "@/lib/sql/identifierSearch";
|
||||
import {
|
||||
buildSqlCompletionItemsFromContext,
|
||||
buildPostgresSequenceLiteralCompletionItems,
|
||||
@@ -328,6 +329,13 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
return buildCompletionResult(actions.length > 0 ? [...replacement.items, ...actions] : replacement.items, replacement.from, getSqlCompletionResultValidFor(fullDoc, position), completionContext.prefix);
|
||||
}
|
||||
|
||||
function itemHasHanText(item: QueryCompletionItem | BatchColumnSelectionActionItem): boolean {
|
||||
if (/\p{Script=Han}/u.test(item.label)) return true;
|
||||
if ("comment" in item && typeof item.comment === "string" && /\p{Script=Han}/u.test(item.comment)) return true;
|
||||
if (typeof item.detail === "string" && /\p{Script=Han}/u.test(item.detail)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// CodeMirror's built-in matcher only matches single-character queries against
|
||||
// the label start, and cannot match pinyin initials against Han labels. Our
|
||||
// provider already filters and ranks items itself (substring + pinyin), so
|
||||
@@ -335,7 +343,14 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
function shouldBypassCompletionFilter(prefix: string, items: Array<QueryCompletionItem | BatchColumnSelectionActionItem>): boolean {
|
||||
if (!prefix) return false;
|
||||
if (/\p{Script=Han}/u.test(prefix)) return true;
|
||||
return /^[a-z0-9]+$/i.test(prefix) && items.some((item) => /\p{Script=Han}/u.test(item.label));
|
||||
if (/^[a-z0-9]+$/i.test(prefix) && items.some(itemHasHanText)) return true;
|
||||
return items.some((item) => {
|
||||
const renderedLabel = "displayLabel" in item && typeof item.displayLabel === "string" ? item.displayLabel : item.label;
|
||||
if (renderedLabel.toLowerCase().startsWith(prefix.toLowerCase())) return false;
|
||||
if ("comment" in item && typeof item.comment === "string" && matchesIdentifierSearch(item.comment, prefix)) return true;
|
||||
if (typeof item.detail === "string" && matchesIdentifierSearch(item.detail, prefix)) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function completionItemsForBypassedFilter(prefix: string, items: Array<QueryCompletionItem | BatchColumnSelectionActionItem>): Array<QueryCompletionItem | BatchColumnSelectionActionItem> {
|
||||
@@ -346,7 +361,11 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
// prefix, otherwise checked columns cannot be applied from that menu.
|
||||
if (isBatchColumnSelectionAction(item)) return true;
|
||||
const renderedLabel = "displayLabel" in item && typeof item.displayLabel === "string" ? item.displayLabel : item.label;
|
||||
return /\p{Script=Han}/u.test(item.label) || renderedLabel.toLowerCase().startsWith(normalized);
|
||||
if (renderedLabel.toLowerCase().startsWith(normalized)) return true;
|
||||
if (itemHasHanText(item)) return true;
|
||||
if ("comment" in item && typeof item.comment === "string" && matchesIdentifierSearch(item.comment, prefix)) return true;
|
||||
if (typeof item.detail === "string" && matchesIdentifierSearch(item.detail, prefix)) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -120,9 +120,11 @@ export function useQueryEditorDiagnostics(options: QueryEditorDiagnosticsOptions
|
||||
line: span.start_line - 1,
|
||||
column: span.start_column - 1,
|
||||
});
|
||||
const endLine = Math.max(span.end_line - 1, span.start_line - 1);
|
||||
const endColumn = endLine === span.start_line - 1 ? Math.max(span.end_column, span.start_column) : span.end_column;
|
||||
const to = lineColumnToOffset(sql, {
|
||||
line: Math.max(span.end_line - 1, span.start_line - 1),
|
||||
column: Math.max(span.end_column, span.start_column),
|
||||
line: endLine,
|
||||
column: endColumn,
|
||||
});
|
||||
if (from == null || to == null || to <= from) return null;
|
||||
return { from, to };
|
||||
|
||||
@@ -53,6 +53,7 @@ export function useQueryEditorExecution(options: QueryEditorExecutionOptions) {
|
||||
}
|
||||
|
||||
function requestExecute(options: RequestExecuteOptions = {}) {
|
||||
if (props.isExecuting) return false;
|
||||
executionViewportOwnership.cancelPendingRequest();
|
||||
const currentView = view.value;
|
||||
if (!currentView) return false;
|
||||
@@ -61,6 +62,7 @@ export function useQueryEditorExecution(options: QueryEditorExecutionOptions) {
|
||||
}
|
||||
|
||||
function requestExecuteInNewResultTab() {
|
||||
if (props.isExecuting) return false;
|
||||
return requestExecute({ bypassPicker: true, openInNewResultTab: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,18 @@ import {
|
||||
import { adjacentDataGridDetailIndex, type DataGridDetailNavigationDelta } from "@/lib/dataGrid/dataGridDetailNavigation";
|
||||
import { applyColumnFormatter, DataGridDateTimePatterns, displayTimeZoneOption, formatIoTDBTimestampEditorValue, iotdbTimestampFractionDigits, iotdbTimestampPrecision, parseIoTDBTimestampEditorValue, type ColumnFormatterConfig } from "@/lib/dataGrid/columnFormatter";
|
||||
import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
|
||||
import { BOOLEAN_CELL_EDITOR_VALUES, booleanCellDisplayText, booleanCellEditorValue, isBooleanCellValue, isBooleanColumnType, isPointInBooleanCheckbox, nextBooleanCellValue, normalizeBooleanCellValue, parseBooleanCellEditorValue } from "@/lib/dataGrid/dataGridBooleanColumn";
|
||||
import {
|
||||
BOOLEAN_CELL_EDITOR_VALUES,
|
||||
booleanCellDisplayText,
|
||||
booleanCellEditorDisplayValues,
|
||||
booleanCellEditorValue,
|
||||
isBooleanCellValue,
|
||||
isBooleanColumnType,
|
||||
isPointInBooleanCheckbox,
|
||||
nextBooleanCellValue,
|
||||
normalizeBooleanCellValue,
|
||||
parseBooleanCellEditorValue,
|
||||
} from "@/lib/dataGrid/dataGridBooleanColumn";
|
||||
import { resolveDataGridColumnNullability, resolveDataGridColumnsByResultIndex } from "@/lib/dataGrid/dataGridColumnMetadata";
|
||||
import { resolveDataGridNewRowCellPlaceholder } from "@/lib/dataGrid/dataGridDefaultPlaceholder";
|
||||
import {
|
||||
@@ -241,6 +252,7 @@ import {
|
||||
elasticsearchCursorPageJumpRequestCount,
|
||||
hasCompleteLocalDataGridResult,
|
||||
hasShortDataGridSqlPage,
|
||||
isDataGridSurfaceBusy,
|
||||
reconcileDataGridExactTotalWithObservedPage,
|
||||
resolveDataGridPaginationTotal,
|
||||
showDataGridRerunTotalCountAction,
|
||||
@@ -473,6 +485,7 @@ const hideNullColumns = computed(() => settingsStore.editorSettings.dataGridHide
|
||||
const hideEmptyColumns = computed(() => settingsStore.editorSettings.dataGridHideEmptyColumns);
|
||||
const booleanCellsUseCheckbox = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode === "checkbox");
|
||||
const booleanCellValueStyle = computed(() => settingsStore.editorSettings.dataGridBooleanValueStyle);
|
||||
const booleanEditorDisplayValues = computed(() => booleanCellEditorDisplayValues(booleanCellValueStyle.value));
|
||||
const booleanDisplayMode = computed(() => settingsStore.editorSettings.dataGridBooleanDisplayMode);
|
||||
const colorizeDataGridCellTypes = computed(() => settingsStore.editorSettings.colorizeDataGridCellTypes);
|
||||
const { isDark, themePalette } = useTheme();
|
||||
@@ -2420,12 +2433,14 @@ const {
|
||||
onHideNullColumnsChange: (value) => settingsStore.updateEditorSettings({ dataGridHideNullColumns: value }),
|
||||
onRefreshMetrics: scheduleColumnLayoutRefresh,
|
||||
});
|
||||
// The go-to-column list mirrors the column filter panel, so both follow the
|
||||
// user's local column order instead of the database order.
|
||||
const goToColumnItems = computed(() =>
|
||||
buildDataGridColumnLookupItems({
|
||||
columns: props.result.columns,
|
||||
sourceColumns: props.sourceColumns,
|
||||
columnComments: props.resultColumnComments,
|
||||
displayableIndexes: displayableColumnIndexes.value,
|
||||
displayableIndexes: orderedDisplayableColumnIndexes.value,
|
||||
commentByColumn: columnCommentMap.value,
|
||||
}),
|
||||
);
|
||||
@@ -2731,12 +2746,11 @@ const columnWidthMode = computed(() => settingsStore.editorSettings.dataGridColu
|
||||
const tableFontFamily = computed(() => settingsStore.editorSettings.tableFontFamily);
|
||||
const columnWidthCacheKey = computed(() => props.columnWidthCacheKey?.trim() || props.cacheKey?.trim() || undefined);
|
||||
const columnStructureSignature = computed(() => createDataGridColumnStructureSignature(props.result.columns, props.result.column_types));
|
||||
// Bumped once the configured header font is ready, so widths measured against a temporary fallback
|
||||
// font get re-measured without reacting to unrelated fonts loaded elsewhere in the application.
|
||||
const dataGridFontReadyTick = ref(0);
|
||||
const columnFormatterReadyTick = ref(0);
|
||||
const columnHeaderMeasurementKey = computed(() => [tableFontSize.value, tableFontFamily.value, dataGridFontReadyTick.value, columnFormatterReadyTick.value]);
|
||||
// The configured header font is re-measured once ready (grown-only, never clearing user
|
||||
// adjustments), without reacting to unrelated fonts loaded elsewhere in the application.
|
||||
const columnHeaderMeasurementKey = computed(() => [tableFontSize.value, tableFontFamily.value]);
|
||||
let columnHeaderMeasureContext: CanvasRenderingContext2D | null | undefined;
|
||||
let triggerGrowColumnWidths: (() => void) | undefined;
|
||||
|
||||
if (typeof document !== "undefined" && document.fonts) {
|
||||
let fontLoadRequestId = 0;
|
||||
@@ -2749,7 +2763,9 @@ if (typeof document !== "undefined" && document.fonts) {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (requestId === fontLoadRequestId) dataGridFontReadyTick.value++;
|
||||
if (requestId === fontLoadRequestId) {
|
||||
triggerGrowColumnWidths?.();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -2774,7 +2790,7 @@ function columnWidthDisplayValue(value: CellValue, columnIndex: number): CellVal
|
||||
return showWhitespaceEnabled.value && typeof display === "string" ? gridCellDisplayValue(display, flatteningMultiLineEnabled.value, true) : display;
|
||||
}
|
||||
|
||||
const { initColumnWidths, onResizeStart, autoFitColumn, autoFitAllColumns, renderedColumnWidths, totalWidth, columnVars, getIsResizing } = useDataGridColumnResize({
|
||||
const { initColumnWidths, growColumnWidthsToFitSamples, onResizeStart, autoFitColumn, autoFitAllColumns, renderedColumnWidths, totalWidth, columnVars, getIsResizing } = useDataGridColumnResize({
|
||||
columns: visibleColumns,
|
||||
sourceRows: computed(() => props.result.rows),
|
||||
columnIndexes: visibleColumnIndexes,
|
||||
@@ -2790,6 +2806,7 @@ const { initColumnWidths, onResizeStart, autoFitColumn, autoFitAllColumns, rende
|
||||
viewportWidth: gridViewportWidth,
|
||||
displayValue: columnWidthDisplayValue,
|
||||
});
|
||||
triggerGrowColumnWidths = growColumnWidthsToFitSamples;
|
||||
const gridStyle = computed(() => ({
|
||||
...columnVars.value,
|
||||
"--header-total-w": dataGridHeaderContentWidth("var(--total-w)", gridScrollbarGutter.value),
|
||||
@@ -3644,6 +3661,8 @@ watch(
|
||||
);
|
||||
const manualTotalRowCount = ref<number | undefined>(undefined);
|
||||
const manualTotalRowCountLoading = ref(false);
|
||||
/** True only for count-then-navigate flows (e.g. jump to last page), where the grid must stay covered. */
|
||||
const manualTotalRowCountCoversSurface = ref(false);
|
||||
let manualTotalRowCountGeneration = 0;
|
||||
function invalidateManualTotalRowCount() {
|
||||
manualTotalRowCountGeneration++;
|
||||
@@ -3772,8 +3791,20 @@ const rowPreparationProgress = shallowRef<DataGridRowPreparationProgress | null>
|
||||
let rowPreparationController: AbortController | null = null;
|
||||
let gridClipboardLifecycle = 0;
|
||||
let gridClipboardActive = true;
|
||||
/** Automatic background counts keep rows interactive; explicit count navigation still blocks the surface. */
|
||||
const gridSurfaceBusy = computed(() => isRefreshingData.value || props.loading === true || manualTotalRowCountLoading.value || pageJumpBusy.value || rowPreparationProgress.value !== null);
|
||||
/**
|
||||
* Automatic background counts keep rows interactive. A manual footer count only covers the grid when it is
|
||||
* part of a navigation flow; counting total rows on demand must not look like a table refresh.
|
||||
*/
|
||||
const gridSurfaceBusy = computed(() =>
|
||||
isDataGridSurfaceBusy({
|
||||
isRefreshingData: isRefreshingData.value,
|
||||
loading: props.loading === true,
|
||||
pageJumpBusy: pageJumpBusy.value,
|
||||
rowPreparationBusy: rowPreparationProgress.value !== null,
|
||||
manualTotalRowCountLoading: manualTotalRowCountLoading.value,
|
||||
manualTotalRowCountCoversSurface: manualTotalRowCountCoversSurface.value,
|
||||
}),
|
||||
);
|
||||
const gridPaginationBusy = computed(() => gridSurfaceBusy.value || totalRowCountBusy.value);
|
||||
const dataGridNativeSelectionBlockOwner = {};
|
||||
watch(
|
||||
@@ -4251,20 +4282,26 @@ function jumpToCountedLastPage(total: number) {
|
||||
requestServerPageJump(lastPageNum);
|
||||
}
|
||||
|
||||
async function beginManualTotalRowCount(): Promise<number | undefined> {
|
||||
async function beginManualTotalRowCount(coversSurface = false): Promise<number | undefined> {
|
||||
if (manualTotalRowCountLoading.value) return undefined;
|
||||
const generation = manualTotalRowCountGeneration;
|
||||
manualTotalRowCountCoversSurface.value = coversSurface;
|
||||
manualTotalRowCountLoading.value = true;
|
||||
// Flush busy UI (overlay / spinner) before the slow COUNT starts.
|
||||
// Flush the button spinner (and the overlay for count-then-navigate flows) before the slow COUNT starts.
|
||||
await nextTick();
|
||||
return generation;
|
||||
}
|
||||
|
||||
function finishManualTotalRowCount() {
|
||||
manualTotalRowCountLoading.value = false;
|
||||
manualTotalRowCountCoversSurface.value = false;
|
||||
}
|
||||
|
||||
async function lastPage() {
|
||||
if (infiniteScrollEnabled.value || allRowsLoaded.value) return;
|
||||
// Navicat-style: always re-COUNT when jumping to the last page.
|
||||
if (props.countTotalRows) {
|
||||
const generation = await beginManualTotalRowCount();
|
||||
const generation = await beginManualTotalRowCount(true);
|
||||
if (generation === undefined) return;
|
||||
try {
|
||||
if (generation !== manualTotalRowCountGeneration) return;
|
||||
@@ -4283,12 +4320,12 @@ async function lastPage() {
|
||||
5000,
|
||||
);
|
||||
} finally {
|
||||
manualTotalRowCountLoading.value = false;
|
||||
finishManualTotalRowCount();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (props.connectionId && (props.countSql || canCountTableRows.value)) {
|
||||
const generation = await beginManualTotalRowCount();
|
||||
const generation = await beginManualTotalRowCount(true);
|
||||
if (generation === undefined) return;
|
||||
try {
|
||||
if (generation !== manualTotalRowCountGeneration) return;
|
||||
@@ -4306,7 +4343,7 @@ async function lastPage() {
|
||||
} catch {
|
||||
// COUNT query failed — ignore silently
|
||||
} finally {
|
||||
manualTotalRowCountLoading.value = false;
|
||||
finishManualTotalRowCount();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4378,7 +4415,7 @@ async function calculateTotalRowCount() {
|
||||
} catch (e: any) {
|
||||
toast(t("grid.calculateTotalRowsFailed", { message: e?.message || String(e) }), 5000);
|
||||
} finally {
|
||||
manualTotalRowCountLoading.value = false;
|
||||
finishManualTotalRowCount();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5647,7 +5684,6 @@ const {
|
||||
toast,
|
||||
});
|
||||
columnFormatterForWidth = columnFormatter;
|
||||
columnFormatterReadyTick.value++;
|
||||
|
||||
initColumnWidths();
|
||||
watch([visibleColumnIndexes, () => renderedColumnWidths.value.length], () => scheduleColumnLayoutRefresh());
|
||||
@@ -8989,6 +9025,9 @@ function changeExportQueryTimeout() {
|
||||
|
||||
const userFacingSql = ref("");
|
||||
let userFacingSqlGeneration = 0;
|
||||
// Legacy snapshots may retain a display page without the actual execution text.
|
||||
// Do not present the original, unpaginated statement as the executed SQL there.
|
||||
const actualExecutionSql = computed(() => props.result.sqlPage?.executedSql || props.result.executedStatement || (!props.pageSql ? props.result.sourceStatement || props.sql || "" : ""));
|
||||
|
||||
function sqlWithDisplayDatabaseName(sql: string): string {
|
||||
const database = props.tableMeta?.database ?? props.database;
|
||||
@@ -9002,7 +9041,7 @@ function sqlWithDisplayDatabaseName(sql: string): string {
|
||||
|
||||
async function syncUserFacingSql() {
|
||||
const generation = ++userFacingSqlGeneration;
|
||||
const executionSql = (props.pageSql || props.sql)?.trim() ?? "";
|
||||
const executionSql = (props.result.sqlPage?.sql || props.pageSql || props.sql)?.trim() ?? "";
|
||||
const includeDatabaseName = settingsStore.editorSettings.generateSqlIncludeDatabaseName;
|
||||
const shouldRebuildSql = executionSql.includes("__DBX_LARGE_VALUE_BYTES_") || includeDatabaseName;
|
||||
if (props.context !== "table-data" || !shouldRebuildSql || !props.tableMeta?.tableName) {
|
||||
@@ -9044,7 +9083,7 @@ async function syncUserFacingSql() {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.sql, props.pageSql, props.context, props.tableMeta, props.pageLimit, props.pageOffset, props.executedPageLimit, props.executedPageOffset, currentWhereInput(), effectiveOrderBy(), settingsStore.editorSettings.generateSqlIncludeDatabaseName],
|
||||
() => [props.sql, props.pageSql, props.result.sqlPage, props.context, props.tableMeta, props.pageLimit, props.pageOffset, props.executedPageLimit, props.executedPageOffset, currentWhereInput(), effectiveOrderBy(), settingsStore.editorSettings.generateSqlIncludeDatabaseName],
|
||||
() => void syncUserFacingSql(),
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -12405,8 +12444,9 @@ async function copyUserFacingSqlWithoutComments() {
|
||||
}
|
||||
|
||||
const statusSqlContextMenuItems = computed<ContextMenuItem[]>(() => [
|
||||
{ label: t("grid.copyQuerySql"), icon: Copy, action: () => void copyUserFacingSql() },
|
||||
{ label: t(props.result.sqlPage ? "grid.copyCurrentPageSql" : "grid.copyQuerySql"), icon: Copy, action: () => void copyUserFacingSql() },
|
||||
{ label: t("grid.copyExecutableSql"), icon: Copy, action: () => void copyUserFacingSqlWithoutComments() },
|
||||
...(actualExecutionSql.value ? [{ label: t("grid.copyActualExecutionSql"), icon: Copy, action: () => void copyText(actualExecutionSql.value) }] : []),
|
||||
]);
|
||||
|
||||
type TableInfoTabItem = {
|
||||
@@ -14889,6 +14929,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
v-else-if="isBooleanGridCell(displayItems[cell.recordIndex], cell.valueIndex)"
|
||||
v-model="booleanEditorModelValue"
|
||||
:values="BOOLEAN_CELL_EDITOR_VALUES"
|
||||
:display-values="booleanEditorDisplayValues"
|
||||
:nullable="isBooleanGridColumnNullable(cell.valueIndex)"
|
||||
:initial-null="isGridCellInitialNull(displayItems[cell.recordIndex]?.id, cell.valueIndex)"
|
||||
cell-layout="transpose"
|
||||
@@ -15595,6 +15636,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
v-else-if="isBooleanGridCell(getRowItem(canvasEditingCell.rowId), canvasEditingCell.actualColIdx)"
|
||||
v-model="booleanEditorModelValue"
|
||||
:values="BOOLEAN_CELL_EDITOR_VALUES"
|
||||
:display-values="booleanEditorDisplayValues"
|
||||
:nullable="isBooleanGridColumnNullable(canvasEditingCell.actualColIdx)"
|
||||
:initial-null="isGridCellInitialNull(canvasEditingCell.rowId, canvasEditingCell.actualColIdx)"
|
||||
@cancel="cancelEdit"
|
||||
@@ -15823,6 +15865,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
v-else-if="isBooleanGridCell(item, col.actualColIdx)"
|
||||
v-model="booleanEditorModelValue"
|
||||
:values="BOOLEAN_CELL_EDITOR_VALUES"
|
||||
:display-values="booleanEditorDisplayValues"
|
||||
:nullable="isBooleanGridColumnNullable(col.actualColIdx)"
|
||||
:initial-null="isGridCellInitialNull(item.id, col.actualColIdx)"
|
||||
@cancel="cancelEdit"
|
||||
@@ -16381,11 +16424,18 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<span class="block min-w-0 max-w-full truncate opacity-60 cursor-pointer hover:opacity-100" data-result-sql @click="copyUserFacingSql" @contextmenu="onContextMenu">
|
||||
<span v-if="result.sqlPage">{{ t("grid.currentPageSql") }}: </span>
|
||||
{{ sqlOneLiner }}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" class="max-w-md">
|
||||
<div v-if="result.sqlPage" class="mb-1 font-medium">{{ t("grid.currentPageSql") }}</div>
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ userFacingSql }}</pre>
|
||||
<p v-if="result.sqlPage?.probe" class="mt-2 text-xs">{{ t("grid.pageProbeDescription", { count: result.sqlPage.executedLimit }) }}</p>
|
||||
<template v-if="actualExecutionSql && actualExecutionSql.trim() !== userFacingSql.trim()">
|
||||
<div class="mt-3 mb-1 font-medium">{{ t("grid.actualExecutionSql") }}</div>
|
||||
<pre class="max-h-48 overflow-auto text-xs font-mono whitespace-pre-wrap">{{ actualExecutionSql }}</pre>
|
||||
</template>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</CustomContextMenu>
|
||||
@@ -16451,6 +16501,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
v-model:column-open="columnDetailDialogOpen"
|
||||
:row-detail="rowDetail"
|
||||
:column-detail="columnDetail"
|
||||
:database-type="resolvedDatabaseType"
|
||||
:type-color-class="typeColorClass"
|
||||
@navigate-row="navigateRowDetail"
|
||||
@navigate-column="navigateColumnDetail"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useCellDetailEditor, type UseCellDetailEditorReturn } from "@/composabl
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { BINARY_CELL_DOWNLOAD_MODES, isBinaryCellColumnType, binaryCellUtf8Text, isBlobCellColumnType, type BinaryCellDownloadMode } from "@/lib/dataGrid/binaryCellDownload";
|
||||
import { isGeometryColumnType } from "@/lib/dataGrid/cellDetailPresentation";
|
||||
import { cellDetailPreviewText, isGeometryColumnType } from "@/lib/dataGrid/cellDetailPresentation";
|
||||
import { isHexGeometry, renderWktOnCanvas } from "@/lib/dataGrid/geometryPreview";
|
||||
import type { DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
@@ -49,7 +49,7 @@ const jsonFormatted = computed(() => settingsStore.editorSettings.cellDetailJson
|
||||
const jsonView = computed(() => jsonFormatted.value && !!props.detail?.formattedJson);
|
||||
const wordWrapEnabled = computed(() => settingsStore.editorSettings.wordWrap);
|
||||
const binaryTextPreview = computed(() => (props.detail && isBlobCellColumnType(props.detail.type) ? binaryCellUtf8Text(props.detail.value, props.detail.type, props.databaseType) : null));
|
||||
const presentedValuePreview = computed(() => (binaryTextPreview.value === null ? props.detail?.rawValuePreview : props.detail?.displayValuePreview) ?? "");
|
||||
const presentedValuePreview = computed(() => (binaryTextPreview.value === null && props.detail ? cellDetailPreviewText(props.detail, props.databaseType) : props.detail?.displayValuePreview) ?? "");
|
||||
|
||||
function toggleWordWrap() {
|
||||
settingsStore.updateEditorSettings({ wordWrap: !wordWrapEnabled.value });
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TabsContent } from "@/components/ui/tabs";
|
||||
import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
|
||||
import { useDataGridCellDetail } from "@/composables/useDataGridCellDetail";
|
||||
import { BINARY_CELL_DOWNLOAD_MODES, isBinaryCellColumnType, binaryCellUtf8Text, isBlobCellColumnType, type BinaryCellDownloadMode } from "@/lib/dataGrid/binaryCellDownload";
|
||||
import { isGeometryColumnType } from "@/lib/dataGrid/cellDetailPresentation";
|
||||
import { cellDetailPreviewText, isGeometryColumnType } from "@/lib/dataGrid/cellDetailPresentation";
|
||||
import { isHexGeometry } from "@/lib/dataGrid/geometryPreview";
|
||||
import type { DataGridCellDetail } from "@/lib/dataGrid/dataGridDetail";
|
||||
import type { TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
|
||||
@@ -78,7 +78,7 @@ void detailsEditorContainer;
|
||||
void sideJsonPreviewContainer;
|
||||
|
||||
const binaryTextPreview = computed(() => (isBlobCellColumnType(props.detail.type) ? binaryCellUtf8Text(props.detail.value, props.detail.type, props.databaseType) : null));
|
||||
const presentedValuePreview = computed(() => (binaryTextPreview.value === null ? props.detail.rawValuePreview : props.detail.displayValuePreview));
|
||||
const presentedValuePreview = computed(() => (binaryTextPreview.value === null ? cellDetailPreviewText(props.detail, props.databaseType) : props.detail.displayValuePreview));
|
||||
const transformOpen = ref(false);
|
||||
// 底部面板横向空间充足:有图片预览时与值文本左右并排,侧栏窄面板保持上下堆叠。
|
||||
const valueBesideImage = computed(() => props.panelIsBottom && !props.editing && !!props.detail.imagePreviewUrl);
|
||||
|
||||
@@ -5,13 +5,16 @@ import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { cellDetailPreviewText } from "@/lib/dataGrid/cellDetailPresentation";
|
||||
import { filterDataGridDetailFields, type DataGridCellDetail, type DataGridColumnDetail, type DataGridRowDetail } from "@/lib/dataGrid/dataGridDetail";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { detailNavigationDelta, shouldIgnoreDataGridDetailNavigation, type DataGridDetailNavigationDelta } from "@/lib/dataGrid/dataGridDetailNavigation";
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
rowDetail: DataGridRowDetail | null;
|
||||
columnDetail: DataGridColumnDetail | null;
|
||||
databaseType?: DatabaseType;
|
||||
typeColorClass: (type: string) => string;
|
||||
openImagePreview: (src: string, title: string) => void;
|
||||
copyRowDetailFieldValue: (field: DataGridCellDetail) => void;
|
||||
@@ -99,7 +102,7 @@ function onColumnDetailKeydown(event: KeyboardEvent) {
|
||||
<a v-if="field.imagePreviewUrl" :href="field.imagePreviewUrl" role="button" class="mb-2 block max-h-48 overflow-hidden rounded border bg-muted/20" @click.prevent="openImagePreview(field.imagePreviewUrl, field.column)"
|
||||
><img :src="field.imagePreviewUrl" :alt="field.column" loading="lazy" decoding="async" referrerpolicy="no-referrer" class="max-h-48 w-full object-contain"
|
||||
/></a>
|
||||
<pre class="dbx-data-grid-value-font max-h-44 overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': field.isNull ?? field.value === null }">{{ field.rawValuePreview }}</pre>
|
||||
<pre class="dbx-data-grid-value-font max-h-44 overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': field.isNull ?? field.value === null }">{{ cellDetailPreviewText(field, databaseType) }}</pre>
|
||||
<div v-if="field.isValuePreviewTruncated" class="mt-1 text-[11px] text-muted-foreground">{{ t("grid.largeValuePreviewHint", { count: field.rawValuePreview.length }) }}</div>
|
||||
<div v-if="field.formattedJson" class="mt-2 space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.formattedJson") }}</div>
|
||||
@@ -169,7 +172,7 @@ function onColumnDetailKeydown(event: KeyboardEvent) {
|
||||
<a v-if="field.imagePreviewUrl" :href="field.imagePreviewUrl" role="button" class="mb-2 block max-h-40 overflow-hidden rounded border bg-muted/20" @click.prevent="openImagePreview(field.imagePreviewUrl, field.column)"
|
||||
><img :src="field.imagePreviewUrl" :alt="field.column" loading="lazy" decoding="async" referrerpolicy="no-referrer" class="max-h-40 w-full object-contain"
|
||||
/></a>
|
||||
<pre class="dbx-data-grid-value-font max-h-36 overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': field.isNull ?? field.value === null }">{{ field.rawValuePreview }}</pre>
|
||||
<pre class="dbx-data-grid-value-font max-h-36 overflow-auto rounded border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-words" :class="{ 'italic text-muted-foreground': field.isNull ?? field.value === null }">{{ cellDetailPreviewText(field, databaseType) }}</pre>
|
||||
<div v-if="field.isValuePreviewTruncated" class="mt-1 text-[11px] text-muted-foreground">{{ t("grid.largeValuePreviewHint", { count: field.rawValuePreview.length }) }}</div>
|
||||
<div v-if="field.formattedJson" class="mt-2 space-y-1">
|
||||
<div class="text-muted-foreground">{{ t("grid.formattedJson") }}</div>
|
||||
|
||||
@@ -131,7 +131,7 @@ function actionLabelClass(action: DataGridToolbarActionKey) {
|
||||
{{ autoRefresh?.enabled ? autoRefresh.stopLabel : autoRefresh?.startLabel }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem v-for="seconds in autoRefreshIntervals" :key="seconds" class="gap-2" :disabled="autoRefresh?.disabled" @select="void selectDataGridToolbarAutoRefreshInterval(autoRefresh, seconds)">
|
||||
<Check v-if="autoRefresh?.intervalSeconds === seconds" class="h-3.5 w-3.5" />
|
||||
<Check v-if="autoRefresh?.enabled && autoRefresh.intervalSeconds === seconds" class="h-3.5 w-3.5" />
|
||||
<span v-else class="h-3.5 w-3.5" />
|
||||
{{ autoRefresh?.intervalLabel(seconds) }}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -8,6 +8,8 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string;
|
||||
values: string[];
|
||||
/** Optional labels aligned with `values`; the committed value is still `values[i]`. */
|
||||
displayValues?: string[];
|
||||
nullable?: boolean;
|
||||
initialNull?: boolean;
|
||||
cellLayout?: "grid" | "transpose";
|
||||
@@ -33,9 +35,15 @@ const closeHandled = ref(false);
|
||||
|
||||
const triggerClass = computed(() => ["cell-edit-input absolute inset-0 z-10 flex items-center gap-1 border-2 border-primary bg-background py-0 text-left text-xs outline-none", props.cellLayout === "transpose" ? "px-1.5" : "px-2.5"].join(" "));
|
||||
|
||||
function optionLabel(value: string, index: number): string {
|
||||
return props.displayValues?.[index] ?? value;
|
||||
}
|
||||
|
||||
const displayValue = computed(() => {
|
||||
if (props.initialNull && props.modelValue === "") return t("grid.nullValue");
|
||||
return props.modelValue === "" ? "''" : props.modelValue;
|
||||
if (props.modelValue === "") return "''";
|
||||
const index = props.values.indexOf(props.modelValue);
|
||||
return index >= 0 ? optionLabel(props.modelValue, index) : props.modelValue;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
@@ -99,7 +107,7 @@ function onPopoverInteractOutside(event: PointerDownOutsideEvent | FocusOutsideE
|
||||
{{ t("grid.nullValue") }}
|
||||
</button>
|
||||
<button
|
||||
v-for="val in values"
|
||||
v-for="(val, index) in values"
|
||||
:key="val"
|
||||
type="button"
|
||||
class="flex min-h-7 w-full items-center rounded-sm px-2 py-1 text-left text-xs hover:bg-accent hover:text-accent-foreground"
|
||||
@@ -110,7 +118,7 @@ function onPopoverInteractOutside(event: PointerDownOutsideEvent | FocusOutsideE
|
||||
<code>''</code>
|
||||
<span class="ml-2 text-muted-foreground">{{ t("grid.emptyStringValue") }}</span>
|
||||
</template>
|
||||
<template v-else>{{ val }}</template>
|
||||
<template v-else>{{ optionLabel(val, index) }}</template>
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -84,7 +84,11 @@ function mountGrid(
|
||||
hidden_column_indexes: options.displayableColumns === false ? [0] : undefined,
|
||||
});
|
||||
|
||||
const grid = ref<{ tableInfoToolbarCapability: DataGridToolbarActionCapability; goToColumnToolbarCapability: DataGridToolbarActionCapability }>();
|
||||
const grid = ref<{
|
||||
tableInfoToolbarCapability: DataGridToolbarActionCapability;
|
||||
goToColumnToolbarCapability: DataGridToolbarActionCapability;
|
||||
moveDisplayableColumn: (fromDisplayableIndex: number, toDisplayableIndex: number) => void;
|
||||
}>();
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const Root = defineComponent({
|
||||
@@ -199,6 +203,10 @@ function goToColumnItem(name: string, position: number): HTMLButtonElement {
|
||||
return button;
|
||||
}
|
||||
|
||||
function goToColumnOrder(): string[] {
|
||||
return [...document.querySelectorAll<HTMLButtonElement>("[data-column-lookup-panel] button")].map((button) => button.textContent?.replaceAll(/\s/g, "") ?? "").filter((text) => text.includes("#"));
|
||||
}
|
||||
|
||||
function goToColumnEvent(target: HTMLElement): KeyboardEvent {
|
||||
const event = new KeyboardEvent("keydown", { bubbles: true, cancelable: true, ctrlKey: true, key: "g" });
|
||||
target.dispatchEvent(event);
|
||||
@@ -460,6 +468,21 @@ describe("DataGrid go-to-column shortcut", () => {
|
||||
expect(host.querySelector("[data-column-lookup-panel]")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("lists columns in the local column order after the user reorders them", async () => {
|
||||
const { host, grid } = mountGrid({ columns: ["id", "name"] });
|
||||
await settle();
|
||||
|
||||
grid.value!.moveDisplayableColumn(1, 0);
|
||||
await settle();
|
||||
|
||||
const root = gridRoot(host);
|
||||
root.focus();
|
||||
goToColumnEvent(root);
|
||||
await settle();
|
||||
|
||||
expect(goToColumnOrder()).toEqual(["name#2", "id#1"]);
|
||||
});
|
||||
|
||||
it("does not consume the configured shortcut without a displayable column", async () => {
|
||||
const { host } = mountGrid({ displayableColumns: false });
|
||||
await settle();
|
||||
|
||||
@@ -112,6 +112,7 @@ import DataGridSearchBar from "@/components/grid/DataGridSearchBar.vue";
|
||||
import DataGridSortBuilder from "@/components/grid/DataGridSortBuilder.vue";
|
||||
import DataGridSortWorkbench from "@/components/grid/DataGridSortWorkbench.vue";
|
||||
import DataGridTextSortWorkbench from "@/components/grid/DataGridTextSortWorkbench.vue";
|
||||
import EnumCellEditor from "@/components/grid/EnumCellEditor.vue";
|
||||
|
||||
function detail(patch: Partial<DataGridCellDetail> = {}): DataGridCellDetail {
|
||||
return {
|
||||
@@ -2252,6 +2253,55 @@ describe("cell detail surfaces", () => {
|
||||
expect(findOne(panel.root, (node) => node.type === "textarea").value).toBe("中文");
|
||||
});
|
||||
|
||||
it("shows the grid boolean label in the panel and dialog without changing the raw value", () => {
|
||||
const booleanDetail = detail({
|
||||
type: "bit",
|
||||
value: true,
|
||||
rawValue: "true",
|
||||
rawValuePreview: "true",
|
||||
displayValue: "TRUE",
|
||||
displayValuePreview: "TRUE",
|
||||
formattedJson: "",
|
||||
});
|
||||
const panel = mountComponent(DataGridCellDetailPanel, {
|
||||
detail: booleanDetail,
|
||||
panelIsBottom: true,
|
||||
metadataCollapsed: false,
|
||||
valueFillsHeight: false,
|
||||
editing: false,
|
||||
sideJsonView: false,
|
||||
showCompactJson: false,
|
||||
canCompactJson: false,
|
||||
typeColorClass: () => "",
|
||||
canDownloadBinaryValue: () => false,
|
||||
downloadBinaryValue: vi.fn(),
|
||||
canImportBinaryValue: () => false,
|
||||
importBinaryValue: vi.fn(),
|
||||
openImagePreview: vi.fn(),
|
||||
canCopySqlCondition: () => true,
|
||||
databaseType: "sqlserver",
|
||||
});
|
||||
const dialog = mountComponent(DataGridCellDetailDialog, {
|
||||
open: true,
|
||||
detail: booleanDetail,
|
||||
typeColorClass: () => "",
|
||||
openImagePreview: vi.fn(),
|
||||
copyText: vi.fn(),
|
||||
canDownloadBinaryValue: () => false,
|
||||
downloadBinaryValue: vi.fn(),
|
||||
canImportBinaryValue: () => false,
|
||||
importBinaryValue: vi.fn(),
|
||||
databaseType: "sqlserver",
|
||||
});
|
||||
|
||||
for (const mounted of [panel, dialog]) {
|
||||
expect(hostText(mounted.root)).toContain("TRUE");
|
||||
expect(hostText(mounted.root)).not.toContain(">true<");
|
||||
}
|
||||
// rawValue remains the edit/copy payload; only the preview is labeled.
|
||||
expect(booleanDetail.rawValue).toBe("true");
|
||||
});
|
||||
|
||||
it("keeps the value transform entry for non-binary detail cells", () => {
|
||||
const panel = mountComponent(DataGridCellDetailPanel, {
|
||||
detail: detail({ type: "VARCHAR(32)", value: "hello", rawValue: "hello", rawValuePreview: "hello", displayValue: "hello", displayValuePreview: "hello", formattedJson: "" }),
|
||||
@@ -2583,3 +2633,41 @@ describe("DataGrid column header tooltips", () => {
|
||||
grid.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("EnumCellEditor boolean labels", () => {
|
||||
it("shows TRUE/FALSE but commits the raw boolean value", () => {
|
||||
const commit = vi.fn();
|
||||
const editor = mountComponent(EnumCellEditor, {
|
||||
modelValue: "true",
|
||||
values: ["true", "false"],
|
||||
displayValues: ["TRUE", "FALSE"],
|
||||
nullable: true,
|
||||
onCommit: commit,
|
||||
});
|
||||
|
||||
expect(hostText(editor.root)).toContain("TRUE");
|
||||
dispatch(
|
||||
findOne(editor.root, (node) => node.type === "button" && hostText(node).trim() === "FALSE"),
|
||||
"click",
|
||||
);
|
||||
expect(commit).toHaveBeenCalledWith("false");
|
||||
});
|
||||
|
||||
it("shows 1/0 for the zero-one style while keeping the raw values", () => {
|
||||
const commit = vi.fn();
|
||||
const editor = mountComponent(EnumCellEditor, {
|
||||
modelValue: "false",
|
||||
values: ["true", "false"],
|
||||
displayValues: ["1", "0"],
|
||||
nullable: true,
|
||||
onCommit: commit,
|
||||
});
|
||||
|
||||
expect(hostText(editor.root)).toContain("0");
|
||||
dispatch(
|
||||
findOne(editor.root, (node) => node.type === "button" && hostText(node).trim() === "1"),
|
||||
"click",
|
||||
);
|
||||
expect(commit).toHaveBeenCalledWith("true");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ function mountWithProvider(render: () => ReturnType<typeof h>) {
|
||||
return mounted;
|
||||
}
|
||||
|
||||
function mountToolbar() {
|
||||
function mountToolbar(autoRefreshEnabled = false) {
|
||||
return mountWithProvider(() =>
|
||||
h(DataGridToolbar, {
|
||||
compact: true,
|
||||
@@ -78,7 +78,7 @@ function mountToolbar() {
|
||||
shortLabel: "Auto",
|
||||
startLabel: "Start auto-refresh",
|
||||
stopLabel: "Stop auto-refresh",
|
||||
enabled: false,
|
||||
enabled: autoRefreshEnabled,
|
||||
intervalSeconds: 30,
|
||||
sweepKey: 0,
|
||||
intervalOptions: [10, 30, 60],
|
||||
@@ -200,6 +200,16 @@ describe("data grid icon-only toolbar tooltips", () => {
|
||||
expect(showsTooltip("Auto-refresh results")).toBe(true);
|
||||
});
|
||||
|
||||
it.each([false, true])("marks the interval as selected only when auto refresh is enabled (%s)", async (enabled) => {
|
||||
const { host } = mountToolbar(enabled);
|
||||
await settle();
|
||||
await click(toolbarButton(host, "autoRefresh"));
|
||||
|
||||
const intervalItem = [...document.querySelectorAll<HTMLElement>('[data-slot="dropdown-menu-item"]')].find((item) => item.textContent?.trim() === "Refresh every 30s");
|
||||
expect(intervalItem).toBeDefined();
|
||||
expect(!!intervalItem!.querySelector("svg")).toBe(enabled);
|
||||
});
|
||||
|
||||
it("opens the column lookup side panel through its toolbar capability", async () => {
|
||||
const { host, grid } = mountGrid();
|
||||
await settle();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { LocaleOption } from "@/lib/app/localeOptions";
|
||||
import { webPath } from "@/lib/common/webPath";
|
||||
|
||||
defineProps<{ option: LocaleOption }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span aria-hidden="true" class="inline-flex h-5 w-6 shrink-0 items-center justify-center text-sm font-medium leading-none">
|
||||
<img v-if="option.flagIcon" :src="webPath(`/icons/flags/${option.flagIcon}.svg`)" alt="" width="20" height="15" class="h-[15px] w-5 rounded-[1px] object-contain" />
|
||||
<span v-else>{{ option.flag }}</span>
|
||||
</span>
|
||||
</template>
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
autoMapImportColumns,
|
||||
buildTableImportParseOptions,
|
||||
defaultTableImportEmptyStringAsNull,
|
||||
formatTableImportPreviewCell,
|
||||
formatTableImportElapsed,
|
||||
nextTableImportWizardStep,
|
||||
previousTableImportWizardStep,
|
||||
@@ -973,12 +974,6 @@ function updateColumnDataType(sourceColumn: string, value: any) {
|
||||
};
|
||||
}
|
||||
|
||||
function formatCell(value: unknown) {
|
||||
if (value === null) return "NULL";
|
||||
if (typeof value === "object") return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
wizardStep.value = previousTableImportWizardStep(wizardStep.value);
|
||||
}
|
||||
@@ -1836,7 +1831,7 @@ watch(rawProgressPercent, (percent) => {
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIndex) in preview.rows" :key="rowIndex">
|
||||
<td v-for="(cell, colIndex) in row" :key="colIndex" class="max-w-[180px] border-b border-r px-2 py-1.5 font-mono" :class="{ 'text-muted-foreground': cell === null }">
|
||||
<span class="block truncate">{{ formatCell(cell) }}</span>
|
||||
<span class="block truncate">{{ formatTableImportPreviewCell(cell) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -220,12 +220,16 @@ function openTableStructureForSelection(): boolean {
|
||||
return connectionTreeRef.value?.openTableStructureForSelection?.() ?? false;
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, locateTabInSidebar, disconnectAllActiveConnections, openTableStructureForSelection });
|
||||
async function focusSelectedNode() {
|
||||
await connectionTreeRef.value?.focusSelectedNode?.();
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, locateTabInSidebar, disconnectAllActiveConnections, focusSelectedNode, openTableStructureForSelection });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-app-sidebar class="app-sidebar-panel h-full shrink-0 relative select-none" :class="classicLayout ? '' : 'rounded-md border border-border/80 bg-background'" :style="{ width: sidebarWidth + 'px' }" @keydown="onSidebarKeydown">
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div data-app-sidebar class="app-sidebar-panel h-full shrink-0 relative select-none" :class="classicLayout ? '' : 'dbx-panel bg-background'" :style="{ width: sidebarWidth + 'px' }" @keydown="onSidebarKeydown">
|
||||
<div class="h-full flex flex-col overflow-hidden rounded-[inherit]">
|
||||
<div class="app-sidebar-toolbar flex items-center gap-px px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20" :class="classicLayout ? 'h-9' : 'h-10'">
|
||||
<span v-if="showConnectionMultiSelectToolbar" class="flex min-w-0 self-stretch items-center" data-tauri-drag-region>
|
||||
<span class="truncate" data-tauri-drag-region>{{ t("sidebar.connections") }}</span>
|
||||
@@ -319,7 +323,7 @@ defineExpose({ focusSearch, locateTabInSidebar, disconnectAllActiveConnections,
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex-1 min-h-0">
|
||||
<div data-sidebar-connection-tree class="flex-1 min-h-0">
|
||||
<ConnectionTree ref="connectionTreeRef" @open-settings="(initialTab) => emit('open-settings', initialTab)" @add-to-ai="(nodes) => emit('add-to-ai', nodes)" />
|
||||
</div>
|
||||
<div v-if="activeConnectionEndpoint" data-sidebar-connection-endpoint class="flex shrink-0 items-center gap-1.5 border-t border-border/60 px-2.5 py-1.5 text-[11px] leading-none text-muted-foreground" :title="activeWindowConnection?.name">
|
||||
|
||||
@@ -2049,6 +2049,7 @@ defineExpose({
|
||||
:hide-execution-controls="!!activeTab.ddlViewer"
|
||||
enable-explain-shortcut
|
||||
:can-explain="!activeTab.isExecuting && !activeTab.isExplaining && !!executableSql.trim()"
|
||||
:is-executing="activeTab.isExecuting"
|
||||
@update:model-value="emit('editorUpdate', activeTab.id, $event)"
|
||||
@selection-change="emit('editorSelectionChange', activeTab.id, $event)"
|
||||
@send-selection-to-ai="emit('sendSelectionToAi', activeTab.id, $event)"
|
||||
|
||||
@@ -103,7 +103,20 @@ import {
|
||||
createSaveAllTabsMenuItem,
|
||||
} from "@/lib/tabs/tabMenu";
|
||||
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
|
||||
import { findTabWithSameTitleSuffix, joinTabTitleParts, TAB_TITLE_SUFFIX_MAX_LENGTH, tabConnectionColor, dirtyTabTitleStyle, tabColorStyle as sharedTabColorStyle, tabDatabaseIconType, tabDisplayTitle, tabDisplayTitleParts, tabIconClass, tabTooltipLines } from "@/lib/tabs/tabPresentation";
|
||||
import {
|
||||
connectionColor,
|
||||
findTabWithSameTitleSuffix,
|
||||
joinTabTitleParts,
|
||||
TAB_TITLE_SUFFIX_MAX_LENGTH,
|
||||
tabConnectionColor,
|
||||
dirtyTabTitleStyle,
|
||||
tabColorStyle as sharedTabColorStyle,
|
||||
tabDatabaseIconType,
|
||||
tabDisplayTitle,
|
||||
tabDisplayTitleParts,
|
||||
tabIconClass,
|
||||
tabTooltipLines,
|
||||
} from "@/lib/tabs/tabPresentation";
|
||||
import { activeTabSidebarTarget } from "@/lib/sidebar/sidebarActiveTabTarget";
|
||||
import "./appTabBar.css";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
@@ -1112,9 +1125,9 @@ const stripSections = computed(() => {
|
||||
});
|
||||
|
||||
function tabColorStyle(tab: QueryTab): CSSProperties | undefined {
|
||||
// Keep ungrouped sidebar tabs aligned with the horizontal tab strip: every
|
||||
// tab gets a connection-colored wash, while the active tab gets the stronger
|
||||
// border and leading accent.
|
||||
// Grouped sidebar tabs rely on the tree rails; ungrouped ones keep a
|
||||
// connection-colored wash (configured or generated), with a stronger border
|
||||
// and leading accent on the active tab.
|
||||
if (isVerticalLayout.value) {
|
||||
if (settingsStore.editorSettings.tabGroupMode !== "none" || !settingsStore.editorSettings.colorizeConnectionTabs) return undefined;
|
||||
const active = isTabActive(tab);
|
||||
@@ -1130,10 +1143,15 @@ function tabColorStyle(tab: QueryTab): CSSProperties | undefined {
|
||||
: { borderColor: hexToRgba(color, 0.18) }),
|
||||
} as CSSProperties;
|
||||
}
|
||||
// Keep horizontal tabs quiet by default; group headers and rails already
|
||||
// provide the connection identity, while the selected tab gets the wash.
|
||||
if (!isTabActive(tab)) {
|
||||
return undefined;
|
||||
// Grouped horizontal tabs already show the group identity on the header
|
||||
// and rail (the connection color when grouping by connection), so keep
|
||||
// their washes quiet to make the active tab easy to spot (issue #12485).
|
||||
// Ungrouped tabs have no rail: a color the user configured in the
|
||||
// connection settings still marks them, while the auto-generated hue stays
|
||||
// quiet.
|
||||
const grouped = settingsStore.editorSettings.tabGroupMode !== "none";
|
||||
if (grouped || !connectionColor(tab.connectionId)) return undefined;
|
||||
}
|
||||
return sharedTabColorStyle(tab, isTabActive(tab), isClassicLayout.value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { createApp, h, nextTick, reactive } from "vue";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { QueryResult, QueryTab } from "@/types/database";
|
||||
|
||||
vi.mock("@/components/editor/QueryEditor.vue", () => ({ __esModule: true, default: { render: () => null } }));
|
||||
vi.mock("@/composables/useToolbarOverflow", async () => {
|
||||
const { ref } = await import("vue");
|
||||
return { useToolbarOverflow: () => ({ tier: ref(0) }) };
|
||||
});
|
||||
vi.mock("@/lib/tabs/tabResultCache", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/tabs/tabResultCache")>()),
|
||||
writeTabResultSnapshot: vi.fn().mockResolvedValue(false),
|
||||
}));
|
||||
vi.mock("@/components/grid/DataGrid.vue", () => ({ __esModule: true, default: { render: () => null } }));
|
||||
import ContentArea from "../ContentArea.vue";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
afterEach(async () => {
|
||||
await vi.dynamicImportSettled();
|
||||
cleanups.splice(0).forEach((cleanup) => cleanup());
|
||||
document.body.replaceChildren();
|
||||
window.localStorage?.clear();
|
||||
});
|
||||
|
||||
async function mountResults(enabled: boolean, pinned = false) {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
const settings = useSettingsStore();
|
||||
settings.editorSettings.pinResultTabOnClick = enabled;
|
||||
const result: QueryResult = { columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 };
|
||||
const tab = reactive<QueryTab>({
|
||||
id: "query",
|
||||
title: "Query",
|
||||
connectionId: "sqlite",
|
||||
database: "app",
|
||||
mode: "query",
|
||||
sql: "SELECT 1",
|
||||
isExecuting: false,
|
||||
result,
|
||||
results: [result],
|
||||
activeResultRunId: "first",
|
||||
resultRuns: [
|
||||
{ id: "first", title: "First", sequence: 1, sql: "SELECT 1", createdAt: 1, result, results: [result] },
|
||||
{ id: "second", title: "Second", sequence: 2, sql: "SELECT 1", createdAt: 2, result, results: [result], pinned },
|
||||
],
|
||||
});
|
||||
const queries = useQueryStore();
|
||||
queries.tabs.push(tab);
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp({
|
||||
render: () =>
|
||||
h(ContentArea, {
|
||||
activeTab: tab,
|
||||
activeOutputView: "result",
|
||||
executableSql: "",
|
||||
formatSqlRequest: null,
|
||||
compressSqlRequest: null,
|
||||
selectedSql: "",
|
||||
cursorPos: 0,
|
||||
resultOnly: true,
|
||||
blockDangerousRedisCommands: false,
|
||||
}),
|
||||
});
|
||||
app.use(pinia);
|
||||
app.use(createI18n({ legacy: false, locale: "en", messages: { en: {} }, missingWarn: false, fallbackWarn: false }));
|
||||
app.mount(host);
|
||||
cleanups.push(() => app.unmount());
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
return { host, tab, settings, queries };
|
||||
}
|
||||
|
||||
describe("result-tab click pinning", () => {
|
||||
it.each([true, false])("selects the clicked run and only pins when enabled=%s", async (enabled) => {
|
||||
const { host, tab } = await mountResults(enabled);
|
||||
host.querySelectorAll<HTMLButtonElement>("[data-result-run-tab]")[1]!.click();
|
||||
await vi.waitFor(() => expect(tab.activeResultRunId).toBe("second"));
|
||||
for (let index = 0; index < 10; index++) await nextTick();
|
||||
expect(tab.resultRuns![1]!.pinned === true).toBe(enabled);
|
||||
});
|
||||
|
||||
it("does not unpin retained results when disabled and still permits explicit pinning", async () => {
|
||||
const { host, tab, queries } = await mountResults(false, true);
|
||||
host.querySelectorAll<HTMLButtonElement>("[data-result-run-tab]")[1]!.click();
|
||||
await vi.waitFor(() => expect(tab.activeResultRunId).toBe("second"));
|
||||
expect(tab.resultRuns![1]!.pinned).toBe(true);
|
||||
queries.toggleResultRunPinned(tab.id, "first");
|
||||
expect(tab.resultRuns![0]!.pinned).toBe(true);
|
||||
});
|
||||
|
||||
it("applies a setting change to already mounted result tabs", async () => {
|
||||
const { host, tab, settings } = await mountResults(false);
|
||||
settings.editorSettings.pinResultTabOnClick = true;
|
||||
await nextTick();
|
||||
host.querySelectorAll<HTMLButtonElement>("[data-result-run-tab]")[1]!.click();
|
||||
await vi.waitFor(() => expect(tab.resultRuns![1]!.pinned).toBe(true));
|
||||
});
|
||||
});
|
||||
@@ -222,6 +222,49 @@ describe("EditorGroupTabBar group behavior", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["classic", "separated"] as const)("keeps configured connection colors on inactive horizontal tabs when grouping is off in %s layout", async (layout) => {
|
||||
const store = useQueryStore();
|
||||
const settings = useSettingsStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
connectionStore.connections = [{ id: "pg-1", name: "Postgres", db_type: "postgres", driver_profile: "postgres", color: "#3b82f6" } as ConnectionConfig, { id: "pg-2", name: "Auto", db_type: "postgres", driver_profile: "postgres", color: "" } as ConnectionConfig];
|
||||
settings.editorSettings.appLayout = layout;
|
||||
settings.editorSettings.tabGroupMode = "none";
|
||||
settings.editorSettings.tabPlacement = "top";
|
||||
settings.editorSettings.colorizeConnectionTabs = true;
|
||||
const activeTabId = store.createTab("pg-1", "app", "PG 1", "query");
|
||||
const configuredTabId = store.createTab("pg-1", "app", "PG 2", "query");
|
||||
const autoTabId = store.createTab("pg-2", "app", "PG 3", "query");
|
||||
const { app, host } = mountBar(store.groups[0]!.id, store.tabs.slice(), activeTabId, pinia);
|
||||
|
||||
try {
|
||||
for (const placement of ["top", "bottom"] as const) {
|
||||
settings.editorSettings.tabPlacement = placement;
|
||||
await settle();
|
||||
const activeTab = host.querySelector<HTMLElement>(`[data-tab-id="${activeTabId}"]`)!;
|
||||
const configuredTab = host.querySelector<HTMLElement>(`[data-tab-id="${configuredTabId}"]`)!;
|
||||
const autoTab = host.querySelector<HTMLElement>(`[data-tab-id="${autoTabId}"]`)!;
|
||||
const activeBackground = activeTab.style.getPropertyValue("--app-tab-background");
|
||||
const configuredBackground = configuredTab.style.getPropertyValue("--app-tab-background");
|
||||
expect(configuredTab.getAttribute("data-active-tab")).toBe("false");
|
||||
// A color the user picked in the connection settings keeps marking its
|
||||
// tabs when no group header/rail carries it; auto-generated hues stay
|
||||
// quiet and the active tab still reads stronger than its peers.
|
||||
expect(configuredBackground).toBe(layout === "classic" ? "rgba(59, 130, 246, 0.07)" : "rgba(59, 130, 246, 0.09)");
|
||||
expect(autoTab.style.getPropertyValue("--app-tab-background")).toBe("");
|
||||
expect(activeBackground).toBe("rgba(59, 130, 246, 0.24)");
|
||||
}
|
||||
|
||||
// Turning the connection-tab colors off must silence the inactive wash
|
||||
// even for a configured connection color.
|
||||
settings.editorSettings.colorizeConnectionTabs = false;
|
||||
await settle();
|
||||
expect(host.querySelector<HTMLElement>(`[data-tab-id="${configuredTabId}"]`)!.style.getPropertyValue("--app-tab-background")).toBe("");
|
||||
} finally {
|
||||
app.unmount();
|
||||
host.remove();
|
||||
}
|
||||
});
|
||||
|
||||
it("renders one header per connection cluster and collapses it to a count badge", async () => {
|
||||
const store = useQueryStore();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { OBJECT_BROWSER_COMPACT_WIDTHS, normalizeObjectBrowserCompactWidth, type ObjectBrowserCompactWidth } from "@/lib/table/objectBrowserCompactWidth";
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { ArrowDown, ArrowUp, Check, Copy, Database, GripVertical, Grid3X3, LayoutGrid, List, Loader2, RefreshCw, Search, Settings2, TerminalSquare, X } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@@ -117,11 +118,17 @@ const viewMode = computed(() => settingsStore.editorSettings.objectBrowserViewMo
|
||||
const isListView = computed(() => viewMode.value === "list");
|
||||
const isGridView = computed(() => viewMode.value === "grid");
|
||||
const isCompactView = computed(() => viewMode.value === "compact");
|
||||
const compactCardWidth = computed(() => normalizeObjectBrowserCompactWidth(settingsStore.editorSettings.objectBrowserCompactWidth));
|
||||
|
||||
function setCompactCardWidth(width: ObjectBrowserCompactWidth) {
|
||||
settingsStore.updateEditorSettings({ objectBrowserCompactWidth: width });
|
||||
}
|
||||
|
||||
// Measured condensation for the header row: tier 1 moves the sort/view
|
||||
// controls into the overflow menu, tier 2 drops the connection chip (the tab
|
||||
// title already carries it). See useToolbarOverflow for the tier contract.
|
||||
const toolbarRef = ref<HTMLElement | null>(null);
|
||||
const { tier: toolbarTier } = useToolbarOverflow(toolbarRef, [() => props.connection.id]);
|
||||
const { tier: toolbarTier } = useToolbarOverflow(toolbarRef, [() => props.connection.id, () => settingsStore.editorSettings.objectBrowserViewMode]);
|
||||
const showToolbarOverflow = computed(() => toolbarTier.value >= 1);
|
||||
const showConnectionChip = computed(() => toolbarTier.value < 2);
|
||||
const showInlineSortAndView = computed(() => toolbarTier.value < 1);
|
||||
@@ -554,6 +561,17 @@ defineExpose({ focusSearch, refresh });
|
||||
<button type="button" class="flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground" :class="{ 'bg-background text-foreground shadow-sm': isCompactView }" :title="t('objects.viewCompact')" @click="setViewMode('compact')">
|
||||
<Grid3X3 class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<select
|
||||
v-if="isCompactView"
|
||||
data-compact-width-select
|
||||
class="ml-1 h-6 shrink-0 cursor-pointer rounded-sm border-l bg-transparent px-1 text-xs text-muted-foreground outline-none hover:text-foreground focus:text-foreground focus-visible:ring-1 focus-visible:ring-ring"
|
||||
:value="compactCardWidth"
|
||||
:aria-label="t('objects.compactCardWidth')"
|
||||
:title="t('objects.compactCardWidth')"
|
||||
@change="setCompactCardWidth(normalizeObjectBrowserCompactWidth(Number(($event.target as HTMLSelectElement).value)))"
|
||||
>
|
||||
<option v-for="width in OBJECT_BROWSER_COMPACT_WIDTHS" :key="width" :value="width" class="bg-background text-foreground">{{ width }}px</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" :title="t('contextMenu.refreshTab')" :disabled="loading" @click="refresh">
|
||||
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loading }" />
|
||||
@@ -589,6 +607,15 @@ defineExpose({ focusSearch, refresh });
|
||||
<Grid3X3 class="h-3.5 w-3.5" />
|
||||
{{ t("objects.viewCompact") }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub v-if="isCompactView">
|
||||
<DropdownMenuSubTrigger>{{ t("objects.compactCardWidth") }}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem v-for="width in OBJECT_BROWSER_COMPACT_WIDTHS" :key="width" @select="setCompactCardWidth(width)">
|
||||
<Check v-if="compactCardWidth === width" class="h-3.5 w-3.5" />
|
||||
{{ width }}px
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</ToolbarOverflowMenu>
|
||||
</div>
|
||||
|
||||
@@ -655,7 +682,14 @@ defineExpose({ focusSearch, refresh });
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else ref="gridScrollerRef" class="grid min-h-0 flex-1 content-start overflow-auto" :class="isCompactView ? 'grid-cols-[repeat(auto-fill,minmax(128px,1fr))] gap-1 p-1' : 'grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-3 p-3'" @scroll.passive="onGridScroll">
|
||||
<div
|
||||
v-else
|
||||
ref="gridScrollerRef"
|
||||
class="grid min-h-0 flex-1 content-start overflow-auto"
|
||||
:class="isCompactView ? 'gap-1 p-1' : 'grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-3 p-3'"
|
||||
:style="isCompactView ? { gridTemplateColumns: `repeat(auto-fill, minmax(min(${compactCardWidth}px, 100%), 1fr))` } : undefined"
|
||||
@scroll.passive="onGridScroll"
|
||||
>
|
||||
<div
|
||||
v-for="row in visibleRows"
|
||||
:key="row.name"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { OBJECT_BROWSER_COMPACT_WIDTHS, normalizeObjectBrowserCompactWidth, type ObjectBrowserCompactWidth } from "@/lib/table/objectBrowserCompactWidth";
|
||||
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import DatabaseActionsMenu from "@/components/objects/DatabaseActionsMenu.vue";
|
||||
import FirebirdObjectManager from "@/components/objects/FirebirdObjectManager.vue";
|
||||
@@ -277,6 +278,7 @@ const compileErrorMessage = ref("");
|
||||
const sidePanelRow = ref<ObjectBrowserRow | null>(null);
|
||||
const openedInitialEvent = ref("");
|
||||
const isEventEditor = computed(() => sidePanelMode.value === "event-editor");
|
||||
const isSidePanelOpen = computed(() => !!sidePanelRow.value || isEventEditor.value);
|
||||
const sidePanelMode = ref<"table-info" | "source" | "type-info" | "event-editor">("source");
|
||||
const eventEditorKey = computed(() =>
|
||||
eventEditorInstanceKey({
|
||||
@@ -541,7 +543,7 @@ const singleKindFilter = computed<ObjectFilter | null>(() => (objectFilters.valu
|
||||
// filter there and drops the database chip. See useToolbarOverflow for the
|
||||
// tier contract.
|
||||
const toolbarRef = ref<HTMLElement | null>(null);
|
||||
const { tier: toolbarTier } = useToolbarOverflow(toolbarRef, [() => props.database, () => selectedSchema.value, () => needsSchema.value, () => showObjectFilter.value, () => singleKindFilter.value]);
|
||||
const { tier: toolbarTier } = useToolbarOverflow(toolbarRef, [() => props.database, () => selectedSchema.value, () => needsSchema.value, () => showObjectFilter.value, () => singleKindFilter.value, () => settingsStore.editorSettings.objectBrowserViewMode]);
|
||||
const showToolbarOverflow = computed(() => toolbarTier.value >= 1);
|
||||
const showInlineSortAndView = computed(() => toolbarTier.value < 1);
|
||||
const showInlineCheckboxToggle = computed(() => toolbarTier.value < 1);
|
||||
@@ -555,6 +557,11 @@ const viewMode = computed<ObjectBrowserViewMode>(() => settingsStore.editorSetti
|
||||
const isListView = computed(() => viewMode.value === "list");
|
||||
const isGridView = computed(() => viewMode.value === "grid");
|
||||
const isCompactView = computed(() => viewMode.value === "compact");
|
||||
const compactCardWidth = computed(() => normalizeObjectBrowserCompactWidth(settingsStore.editorSettings.objectBrowserCompactWidth));
|
||||
|
||||
function setCompactCardWidth(width: ObjectBrowserCompactWidth) {
|
||||
settingsStore.updateEditorSettings({ objectBrowserCompactWidth: width });
|
||||
}
|
||||
|
||||
type ObjectBrowserScroller =
|
||||
| HTMLElement
|
||||
@@ -776,7 +783,6 @@ const selectableRows = computed(() => rows.value);
|
||||
// The previous flat `v-for` rendered every card (plus its CustomContextMenu)
|
||||
// at once, which stalls the UI on schemas with thousands of objects.
|
||||
const OBJECT_GRID_MIN_CARD_WIDTH = 160; // former `minmax(160px, 1fr)` floor
|
||||
const OBJECT_COMPACT_MIN_CARD_WIDTH = 128;
|
||||
const OBJECT_GRID_GAP = 12; // 0.75rem, former grid gap (both axes)
|
||||
const OBJECT_COMPACT_GAP = 4;
|
||||
// Card height is dataset-stable: if any object has timestamps/comments, every card
|
||||
@@ -803,7 +809,7 @@ const gridColumns = ref(1);
|
||||
let gridResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
function recomputeGridColumns(width: number) {
|
||||
const minWidth = isCompactView.value ? OBJECT_COMPACT_MIN_CARD_WIDTH : OBJECT_GRID_MIN_CARD_WIDTH;
|
||||
const minWidth = isCompactView.value ? compactCardWidth.value : OBJECT_GRID_MIN_CARD_WIDTH;
|
||||
const gap = isCompactView.value ? OBJECT_COMPACT_GAP : OBJECT_GRID_GAP;
|
||||
gridColumns.value = Math.max(1, Math.floor((width + gap) / (minWidth + gap)));
|
||||
}
|
||||
@@ -831,7 +837,7 @@ watch(
|
||||
},
|
||||
{ flush: "post" },
|
||||
);
|
||||
watch(viewMode, () =>
|
||||
watch([viewMode, compactCardWidth], () =>
|
||||
nextTick(() => {
|
||||
const el = gridContainerRef.value;
|
||||
if (el) recomputeGridColumns(el.clientWidth - parseFloat(getComputedStyle(el).paddingLeft) - parseFloat(getComputedStyle(el).paddingRight));
|
||||
@@ -1189,13 +1195,13 @@ function onRowClick(row: ObjectBrowserRow, event: MouseEvent) {
|
||||
clearTimeout(singleClickTimer);
|
||||
singleClickTimer = null;
|
||||
}
|
||||
const firstClickRan = settingsStore.editorSettings.objectBrowserAutoOpenDetailPanel !== false;
|
||||
const firstClickRan = settingsStore.editorSettings.objectBrowserAutoOpenDetailPanel !== false || isSidePanelOpen.value;
|
||||
if (!firstClickRan || action !== singleClickRowAction(row, effectiveDatabaseType.value)) {
|
||||
executeRowAction(row, action);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (settingsStore.editorSettings.objectBrowserAutoOpenDetailPanel === false) {
|
||||
if (settingsStore.editorSettings.objectBrowserAutoOpenDetailPanel === false && !isSidePanelOpen.value) {
|
||||
return;
|
||||
}
|
||||
// Single click: defer when the row has a distinct double-click action so a
|
||||
@@ -1314,11 +1320,15 @@ async function openTableInfo(row: ObjectBrowserRow, initialTab?: TableInfoTab) {
|
||||
selectedRowId.value = row.id;
|
||||
// Toggle off if clicking the same table
|
||||
if (sidePanelRow.value?.id === row.id && sidePanelMode.value === "table-info" && !initialTab) {
|
||||
if (settingsStore.editorSettings.objectBrowserAutoOpenDetailPanel === false) {
|
||||
return;
|
||||
}
|
||||
closeSidePanel();
|
||||
return;
|
||||
}
|
||||
sidePanelRow.value = row;
|
||||
sidePanelMode.value = "table-info";
|
||||
sourceRow.value = null;
|
||||
const epoch = sidePanelGuard.start();
|
||||
// Reset state
|
||||
tableColumns.value = [];
|
||||
@@ -1706,12 +1716,11 @@ function onSidePanelResizeEnd() {
|
||||
|
||||
function closeSidePanel() {
|
||||
sidePanelRow.value = null;
|
||||
sourceRow.value = null;
|
||||
sidePanelMode.value = "source";
|
||||
sidePanelGuard.bump();
|
||||
}
|
||||
|
||||
const isSidePanelOpen = computed(() => !!sidePanelRow.value || isEventEditor.value);
|
||||
|
||||
function toggleSidePanel() {
|
||||
if (isSidePanelOpen.value) {
|
||||
if (selectedRowId.value && sidePanelRow.value && sidePanelRow.value.id !== selectedRowId.value) {
|
||||
@@ -1740,11 +1749,15 @@ function toggleAutoOpenDetailPanel() {
|
||||
async function openTypeInfo(row: ObjectBrowserRow) {
|
||||
selectedRowId.value = row.id;
|
||||
if (sidePanelRow.value?.id === row.id && sidePanelMode.value === "type-info") {
|
||||
if (settingsStore.editorSettings.objectBrowserAutoOpenDetailPanel === false) {
|
||||
return;
|
||||
}
|
||||
closeSidePanel();
|
||||
return;
|
||||
}
|
||||
sidePanelRow.value = row;
|
||||
sidePanelMode.value = "type-info";
|
||||
sourceRow.value = null;
|
||||
sidePanelGuard.bump();
|
||||
}
|
||||
|
||||
@@ -1752,6 +1765,7 @@ function openTypeDdl(row: ObjectBrowserRow) {
|
||||
selectedRowId.value = row.id;
|
||||
sidePanelRow.value = row;
|
||||
sidePanelMode.value = "type-info";
|
||||
sourceRow.value = null;
|
||||
sidePanelGuard.bump();
|
||||
// DDL tab is selected by the panel once details load; switching the tab is
|
||||
// deferred via a dedicated request so the panel can focus it.
|
||||
@@ -1790,6 +1804,9 @@ function openSourceTab(row: ObjectBrowserRow) {
|
||||
async function openSource(row: ObjectBrowserRow) {
|
||||
// Toggle off if clicking the same source row
|
||||
if (sidePanelRow.value?.id === row.id && sidePanelMode.value === "source") {
|
||||
if (settingsStore.editorSettings.objectBrowserAutoOpenDetailPanel === false) {
|
||||
return;
|
||||
}
|
||||
closeSidePanel();
|
||||
return;
|
||||
}
|
||||
@@ -4712,6 +4729,17 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<button type="button" class="flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground" :class="{ 'bg-background text-foreground shadow-sm': isCompactView }" :title="t('objects.viewCompact')" @click="setViewMode('compact')">
|
||||
<Grid3X3 class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<select
|
||||
v-if="isCompactView"
|
||||
data-compact-width-select
|
||||
class="ml-1 h-6 shrink-0 cursor-pointer rounded-sm border-l bg-transparent px-1 text-xs text-muted-foreground outline-none hover:text-foreground focus:text-foreground focus-visible:ring-1 focus-visible:ring-ring"
|
||||
:value="compactCardWidth"
|
||||
:aria-label="t('objects.compactCardWidth')"
|
||||
:title="t('objects.compactCardWidth')"
|
||||
@change="setCompactCardWidth(normalizeObjectBrowserCompactWidth(Number(($event.target as HTMLSelectElement).value)))"
|
||||
>
|
||||
<option v-for="width in OBJECT_BROWSER_COMPACT_WIDTHS" :key="width" :value="width" class="bg-background text-foreground">{{ width }}px</option>
|
||||
</select>
|
||||
</div>
|
||||
<Button v-if="showInlineCheckboxToggle && supportsBatchTableActions" variant="ghost" size="icon" class="h-7 w-7" :class="{ 'text-primary': settingsStore.editorSettings.objectBrowserShowCheckbox }" :title="t('objects.toggleCheckbox')" @click="toggleCheckboxColumn">
|
||||
<CheckSquare v-if="settingsStore.editorSettings.objectBrowserShowCheckbox" class="h-3.5 w-3.5" />
|
||||
@@ -4758,6 +4786,15 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<Grid3X3 class="h-3.5 w-3.5" />
|
||||
{{ t("objects.viewCompact") }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub v-if="isCompactView">
|
||||
<DropdownMenuSubTrigger>{{ t("objects.compactCardWidth") }}</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuItem v-for="width in OBJECT_BROWSER_COMPACT_WIDTHS" :key="width" @select="setCompactCardWidth(width)">
|
||||
<Check v-if="compactCardWidth === width" class="h-3.5 w-3.5" />
|
||||
{{ width }}px
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuCheckboxItem v-if="supportsBatchTableActions" :model-value="settingsStore.editorSettings.objectBrowserShowCheckbox" @select.prevent @update:model-value="toggleCheckboxColumn()">{{ t("objects.toggleCheckbox") }}</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuItem @select="toggleSidePanel">
|
||||
<PanelRight class="h-3.5 w-3.5" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
||||
import { createApp, defineComponent, h, nextTick, reactive, ref, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import DatabaseBrowser from "../DatabaseBrowser.vue";
|
||||
import { provideTabUiState, TAB_UI_STATE_TRACK_DEBOUNCE_MS } from "@/lib/tabs/tabUiState";
|
||||
@@ -20,7 +20,8 @@ const mocks = vi.hoisted(() => ({
|
||||
listDatabases: vi.fn(),
|
||||
listDatabaseMetadata: vi.fn(),
|
||||
editorSettings: {
|
||||
objectBrowserViewMode: "list" as "list" | "grid",
|
||||
objectBrowserViewMode: "list" as "list" | "grid" | "compact",
|
||||
objectBrowserCompactWidth: 160,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -45,9 +46,9 @@ vi.mock("@/stores/queryStore", () => ({
|
||||
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({
|
||||
editorSettings: mocks.editorSettings,
|
||||
editorSettings: reactive(mocks.editorSettings),
|
||||
updateEditorSettings: (patch: Partial<typeof mocks.editorSettings>) => {
|
||||
Object.assign(mocks.editorSettings, patch);
|
||||
Object.assign(reactive(mocks.editorSettings), patch);
|
||||
},
|
||||
}),
|
||||
}));
|
||||
@@ -95,6 +96,7 @@ describe("DatabaseBrowser scroll position preservation (#12432)", () => {
|
||||
mocks.listDatabases.mockResolvedValue(testDatabases);
|
||||
mocks.listDatabaseMetadata.mockResolvedValue([]);
|
||||
mocks.editorSettings.objectBrowserViewMode = "list";
|
||||
mocks.editorSettings.objectBrowserCompactWidth = 160;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -106,6 +108,24 @@ describe("DatabaseBrowser scroll position preservation (#12432)", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("updates compact width immediately without losing cards", async () => {
|
||||
mocks.editorSettings.objectBrowserViewMode = "compact";
|
||||
host = document.createElement("div");
|
||||
document.body.appendChild(host);
|
||||
app = createApp(DatabaseBrowser, { connection: testConnection });
|
||||
app.mount(host);
|
||||
await vi.waitFor(() => expect(host!.querySelectorAll(".grid.content-start .h-7")).toHaveLength(testDatabases.length));
|
||||
const scroller = host.querySelector<HTMLElement>(".grid.content-start")!;
|
||||
expect(scroller.style.gridTemplateColumns).toBe("repeat(auto-fill, minmax(min(160px, 100%), 1fr))");
|
||||
const select = host.querySelector<HTMLSelectElement>("[data-compact-width-select]")!;
|
||||
select.value = "200";
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await nextTick();
|
||||
expect(scroller.style.gridTemplateColumns).toBe("repeat(auto-fill, minmax(min(200px, 100%), 1fr))");
|
||||
expect(mocks.editorSettings.objectBrowserCompactWidth).toBe(200);
|
||||
expect(scroller.querySelectorAll(".h-7")).toHaveLength(testDatabases.length);
|
||||
});
|
||||
|
||||
it("restores scrollTop from tab UI state when mounting in list view", async () => {
|
||||
let capturedState: Record<string, any> = {};
|
||||
const Parent = defineComponent({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
||||
import { createApp, defineComponent, h, nextTick, reactive, ref, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ObjectBrowser from "@/components/objects/ObjectBrowser.vue";
|
||||
import { invalidateObjectBrowserRowsCache } from "@/lib/table/objectBrowserRowsCache";
|
||||
@@ -33,16 +33,18 @@ vi.mock("@/stores/connectionStore", () => ({
|
||||
}));
|
||||
vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => ({}) }));
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({
|
||||
editorSettings: {
|
||||
useSettingsStore: () => {
|
||||
const editorSettings = reactive({
|
||||
objectBrowserCompactWidth: 160,
|
||||
shortcuts: { refreshData: "F5" },
|
||||
objectBrowserViewMode: mocks.viewMode,
|
||||
objectBrowserShowCheckbox: false,
|
||||
sidebarActivation: "double",
|
||||
sidebarMenuLayout: mocks.menuLayout,
|
||||
sidebarMenuPinnedActions: { table: mocks.pinnedActions },
|
||||
},
|
||||
}),
|
||||
});
|
||||
return { editorSettings, updateEditorSettings: (patch: Partial<typeof editorSettings>) => Object.assign(editorSettings, patch) };
|
||||
},
|
||||
}));
|
||||
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => (key === "sidebar.locateActiveTab" ? "Locate in sidebar" : key), locale: ref("en-US") }) }));
|
||||
vi.mock("@/i18n", () => ({ default: { install: () => undefined } }));
|
||||
@@ -156,6 +158,32 @@ function menuButton(label: string): HTMLButtonElement {
|
||||
return buttons[0]!;
|
||||
}
|
||||
|
||||
it("reflows compact rows immediately when changing width and keeps names and row height", async () => {
|
||||
mocks.viewMode = "compact";
|
||||
const widthSpy = vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockImplementation(function (this: HTMLElement) {
|
||||
return this.classList.contains("object-browser-grid-wrapper") ? 1016 : 0;
|
||||
});
|
||||
try {
|
||||
const host = await mountBrowser();
|
||||
const rows = () => [...host.querySelectorAll<HTMLElement>(".object-browser-grid-row")];
|
||||
const names = () => [...host.querySelectorAll<HTMLElement>("[data-object-compact-card]")].map((card) => card.title);
|
||||
expect(rows()).toHaveLength(1);
|
||||
expect(rows()[0]!.style.gridTemplateColumns).toBe("repeat(6, minmax(0, 1fr))");
|
||||
const before = names();
|
||||
const select = host.querySelector<HTMLSelectElement>("[data-compact-width-select]")!;
|
||||
expect(select.value).toBe("160");
|
||||
select.value = "240";
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
await vi.waitFor(() => expect(rows()).toHaveLength(2));
|
||||
expect(rows()[0]!.style.gridTemplateColumns).toBe("repeat(4, minmax(0, 1fr))");
|
||||
expect(rows()[0]!.style.height).toBe("28px");
|
||||
expect(names()).toEqual(before);
|
||||
expect(select.value).toBe("240");
|
||||
} finally {
|
||||
widthSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
describe.each(["list", "grid", "compact"] as const)("ObjectBrowser locate in sidebar (%s)", (viewMode) => {
|
||||
beforeEach(() => {
|
||||
mocks.viewMode = viewMode;
|
||||
|
||||
@@ -290,4 +290,38 @@ describe("ObjectBrowser routine double click", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("switches side panel when clicking different objects while detail panel is open and auto-open is false", async () => {
|
||||
mocks.editorSettings.objectBrowserAutoOpenDetailPanel = false;
|
||||
const host = await mountBrowser();
|
||||
vi.useFakeTimers();
|
||||
|
||||
const spRow = rowFor(host, "sp_daily_report");
|
||||
click(spRow, 1);
|
||||
await vi.advanceTimersByTimeAsync(SINGLE_CLICK_DELAY * 2);
|
||||
expect(mocks.getObjectSource).not.toHaveBeenCalled();
|
||||
|
||||
// Manually open the side panel
|
||||
const toggleBtn = host.querySelector<HTMLButtonElement>('button[title="objects.openDetailPanel"]');
|
||||
expect(toggleBtn).not.toBeNull();
|
||||
toggleBtn?.click();
|
||||
await vi.advanceTimersByTimeAsync(SINGLE_CLICK_DELAY * 2);
|
||||
|
||||
expect(mocks.getObjectSource).toHaveBeenCalledWith("mysql-routines", "app", "app", "sp_daily_report", "PROCEDURE", "IN day DATE");
|
||||
expect(spRow.style.boxShadow).toBe("inset 3px 0 0 var(--primary)");
|
||||
|
||||
// Switch to another object: sequence
|
||||
const seqRow = rowFor(host, "seq_invoice");
|
||||
click(seqRow, 1);
|
||||
await vi.advanceTimersByTimeAsync(SINGLE_CLICK_DELAY * 2);
|
||||
|
||||
expect(mocks.getObjectSource).toHaveBeenCalledWith("mysql-routines", "app", "app", "seq_invoice", "SEQUENCE", undefined);
|
||||
expect(spRow.style.boxShadow).toBe("");
|
||||
expect(seqRow.style.boxShadow).toBe("inset 3px 0 0 var(--primary)");
|
||||
|
||||
// Clicking the same row again does not close the panel when auto-open is false
|
||||
click(seqRow, 1);
|
||||
await vi.advanceTimersByTimeAsync(SINGLE_CLICK_DELAY * 2);
|
||||
expect(seqRow.style.boxShadow).toBe("inset 3px 0 0 var(--primary)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,6 +109,19 @@ describe("plugin shortcut bar", () => {
|
||||
expect(document.querySelectorAll("[data-dbx-context-menu] button")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("keeps the remaining sidebar shortcuts when one plugin visibility changes", async () => {
|
||||
mount("sidebar-bottom");
|
||||
mocks.state.editorSettings.pluginShortcuts.hiddenPluginIds = ["a"];
|
||||
mocks.entries.value = mocks.entries.value.filter((entry: any) => entry.pluginId !== "a");
|
||||
await flush();
|
||||
expect([...container.querySelectorAll("[data-shortcut-label]")].map((label) => label.textContent)).toEqual(["Function b", "Function c"]);
|
||||
|
||||
mocks.state.editorSettings.pluginShortcuts.hiddenPluginIds = [];
|
||||
mocks.entries.value = [...mocks.allEntries.value];
|
||||
await flush();
|
||||
expect([...container.querySelectorAll("[data-shortcut-label]")].map((label) => label.textContent)).toEqual(["Function a", "Function b", "Function c"]);
|
||||
});
|
||||
|
||||
it.each(["left-top", "right-top", "sidebar-bottom"] as const)("keeps the measured preview inside both edges at %s", async (position) => {
|
||||
const original = HTMLElement.prototype.getBoundingClientRect;
|
||||
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (this: HTMLElement) {
|
||||
|
||||
@@ -654,4 +654,54 @@ describe("Redis collection member search", () => {
|
||||
expect(document.body.textContent).toContain("1 members");
|
||||
expect(document.body.textContent).not.toContain("/ 100 members loaded");
|
||||
});
|
||||
|
||||
it("loads more hash fields when clicking load more in hash viewer", async () => {
|
||||
mocks.redisGetValue.mockResolvedValue({
|
||||
key_display: "key",
|
||||
key_raw: "key",
|
||||
ttl: -1,
|
||||
redis_type: "hash",
|
||||
data: {
|
||||
kind: "hash",
|
||||
items: [{ field: blob("f1"), value: blob("v1") }],
|
||||
total: 4000,
|
||||
scan_cursor: 1234,
|
||||
},
|
||||
});
|
||||
mocks.redisLoadMore.mockResolvedValueOnce({
|
||||
kind: "hash",
|
||||
items: [{ field: blob("f2"), value: blob("v2") }],
|
||||
scan_cursor: 5678,
|
||||
});
|
||||
mountViewer();
|
||||
await settle();
|
||||
expect(document.body.textContent).toContain("f1");
|
||||
expect(document.body.textContent).not.toContain("f2");
|
||||
loadMoreButton().click();
|
||||
await settle();
|
||||
expect(mocks.redisLoadMore).toHaveBeenCalledWith("connection", 0, "key", "hash", 1234, 200, undefined, undefined);
|
||||
expect(document.body.textContent).toContain("f1");
|
||||
expect(document.body.textContent).toContain("f2");
|
||||
});
|
||||
|
||||
it("shows error toast when ordinary hash load more fails", async () => {
|
||||
mocks.redisGetValue.mockResolvedValue({
|
||||
key_display: "key",
|
||||
key_raw: "key",
|
||||
ttl: -1,
|
||||
redis_type: "hash",
|
||||
data: {
|
||||
kind: "hash",
|
||||
items: [{ field: blob("f1"), value: blob("v1") }],
|
||||
total: 4000,
|
||||
scan_cursor: 1234,
|
||||
},
|
||||
});
|
||||
mocks.redisLoadMore.mockRejectedValueOnce(new Error("Collection cursor is invalid, expired, or belongs to a different connection/database"));
|
||||
mountViewer();
|
||||
await settle();
|
||||
loadMoreButton().click();
|
||||
await settle();
|
||||
expect(mocks.toast).toHaveBeenCalledWith("Collection cursor is invalid, expired, or belongs to a different connection/database", 3000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1898,8 +1898,14 @@ async function loadMore() {
|
||||
if (!collectionSearchSupported.value) return;
|
||||
const scope = collectionScope();
|
||||
if (!scope) return;
|
||||
if (activeCollectionSearchQuery.value) await collectionSearchController.start();
|
||||
else await collectionSearchController.browse(scope, collectionItems.value, scanCursor.value!);
|
||||
if (activeCollectionSearchQuery.value) {
|
||||
await collectionSearchController.start();
|
||||
} else {
|
||||
await collectionSearchController.browse(scope, collectionItems.value, scanCursor.value!);
|
||||
if (collectionSearchState.value.status === "failed" && collectionSearchState.value.error) {
|
||||
toast(errorMessage(collectionSearchState.value.error), 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveString() {
|
||||
|
||||
@@ -12,7 +12,10 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { buildDatabaseSearchSql, buildSearchResultWhere, findMatchedSearchColumns } from "@/lib/database/databaseSearch";
|
||||
import { databaseSearchBatchRange, databaseSearchNextBatchSize } from "@/lib/database/databaseSearchBatch";
|
||||
import type { DatabaseSearchResultItem, DatabaseSearchScope, DatabaseSearchTableTask, DatabaseSearchTabState, DatabaseType, TableInfo } from "@/types/database";
|
||||
import { fetchDatabaseSearchColumns, findSearchColumns, type DatabaseSearchColumnLite, type SearchColumnsByTable } from "@/lib/database/databaseSearchColumns";
|
||||
import { nextProbeChunkEnd, probeTables } from "@/lib/database/databaseSearchProbe";
|
||||
import { supportsBulkDatabaseSearch } from "@/lib/database/databaseSearchCapabilities";
|
||||
import type { ColumnInfo, DatabaseSearchResultItem, DatabaseSearchScope, DatabaseSearchTableTask, DatabaseSearchTabState, DatabaseType, TableInfo } from "@/types/database";
|
||||
import { buildDatabaseStructureSearchSql, effectiveDatabaseSearchScope, runDatabaseStructureSearch, structureHitPreview, supportsDatabaseStructureSearchFromManifest } from "@/lib/database/databaseStructureSearch";
|
||||
import { usesDocumentStructureSearch, usesSqlCatalogStructureSearch } from "@/lib/database/databaseStructureSearchCapabilities";
|
||||
import type { NavigationTarget } from "@/composables/useNavigationTargets";
|
||||
@@ -60,6 +63,8 @@ const generalError = ref(props.initialState?.generalError ?? "");
|
||||
const currentExecutionId = ref("");
|
||||
const tableTasks = ref<DatabaseSearchTableTask[]>(props.initialState?.tableTasks ? [...props.initialState.tableTasks] : []);
|
||||
const nextTableIndex = ref(props.initialState?.nextTableIndex ?? 0);
|
||||
// 批量取列结果(键小写)。不进持久化 tab state:组件存活期间复用,重建后为空走逐表回退。
|
||||
const columnsByTable = ref<SearchColumnsByTable>(new Map());
|
||||
const activeKeyword = ref(props.initialState?.activeKeyword ?? "");
|
||||
const activePerTableLimit = ref(props.initialState?.activePerTableLimit ?? 20);
|
||||
let runId = 0;
|
||||
@@ -132,6 +137,7 @@ function resetSearchState() {
|
||||
currentExecutionId.value = "";
|
||||
tableTasks.value = [];
|
||||
nextTableIndex.value = 0;
|
||||
columnsByTable.value = new Map();
|
||||
activeKeyword.value = "";
|
||||
activePerTableLimit.value = 20;
|
||||
emitState();
|
||||
@@ -226,6 +232,8 @@ async function startSearch() {
|
||||
progressTotal.value = tableTasks.value.length;
|
||||
loadingTables.value = false;
|
||||
emitState();
|
||||
await loadBulkColumns(currentRun);
|
||||
if (currentRun !== runId || cancelled.value) return;
|
||||
await scanTableRange(currentRun, false);
|
||||
} catch (error: any) {
|
||||
if (currentRun === runId && !cancelled.value) {
|
||||
@@ -333,25 +341,125 @@ async function runStructureSearch(currentRun: number) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量取列:按任务表涉及的 schema 各发一条目录查询,失败静默(空 Map → 逐表回退)。 */
|
||||
async function loadBulkColumns(currentRun: number) {
|
||||
const databaseType = connection.value?.db_type;
|
||||
if (!databaseType || !supportsBulkDatabaseSearch(databaseType)) return;
|
||||
const schemas = [...new Set(tableTasks.value.map((task) => task.schema))];
|
||||
const merged: SearchColumnsByTable = new Map();
|
||||
for (const schema of schemas) {
|
||||
if (currentRun !== runId || cancelled.value) return;
|
||||
const executionId = makeExecutionId();
|
||||
currentExecutionId.value = executionId;
|
||||
const partial = await fetchDatabaseSearchColumns({
|
||||
databaseType,
|
||||
database: props.database,
|
||||
schema,
|
||||
executeQuery: (sql, runExecutionId) => api.executeQuery(props.connectionId, props.database, sql, undefined, runExecutionId),
|
||||
executionId,
|
||||
});
|
||||
if (currentExecutionId.value === executionId) currentExecutionId.value = "";
|
||||
if (currentRun !== runId || cancelled.value) return;
|
||||
partial.forEach((columns, key) => merged.set(key, columns));
|
||||
}
|
||||
columnsByTable.value = merged;
|
||||
}
|
||||
|
||||
async function scanTableRange(currentRun: number, scanAllRemaining: boolean) {
|
||||
const range = databaseSearchBatchRange(nextTableIndex.value, tableTasks.value.length, scanAllRemaining);
|
||||
const databaseType = connection.value!.db_type;
|
||||
const probeReady = supportsBulkDatabaseSearch(databaseType) && columnsByTable.value.size > 0;
|
||||
// 每张任务的预取列(可能为 undefined——该表无目录信息,批内走逐表回退)
|
||||
const taskColumns = tableTasks.value.map((task) => findSearchColumns(columnsByTable.value, task.schema, props.database, task.table.name));
|
||||
|
||||
while (nextTableIndex.value < range.end) {
|
||||
if (currentRun !== runId || cancelled.value) break;
|
||||
const task = tableTasks.value[nextTableIndex.value];
|
||||
if (!task) break;
|
||||
await searchTable(task, connection.value!.db_type, currentRun);
|
||||
const chunkStart = nextTableIndex.value;
|
||||
const chunkEnd = probeReady ? nextProbeChunkEnd(chunkStart, range.end, taskColumns, activeKeyword.value) : Math.min(chunkStart + 1, range.end);
|
||||
const chunk = tableTasks.value.slice(chunkStart, chunkEnd);
|
||||
if (!chunk.length) break;
|
||||
|
||||
let hitChunkIndexes: Set<number> | undefined;
|
||||
if (probeReady) {
|
||||
hitChunkIndexes = await probeChunk(chunk, taskColumns.slice(chunkStart, chunkEnd), databaseType, currentRun);
|
||||
}
|
||||
if (currentRun !== runId || cancelled.value) break;
|
||||
nextTableIndex.value += 1;
|
||||
progressDone.value = nextTableIndex.value;
|
||||
emitState();
|
||||
|
||||
for (let offset = 0; offset < chunk.length; offset++) {
|
||||
if (currentRun !== runId || cancelled.value) break;
|
||||
const taskIndex = chunkStart + offset;
|
||||
const task = chunk[offset];
|
||||
const prefetched = taskColumns[taskIndex];
|
||||
// 探测路径下:未命中的表跳过取数;无目录信息的表恒走逐表
|
||||
if (hitChunkIndexes && prefetched && !hitChunkIndexes.has(taskIndex)) {
|
||||
// skip: 探测未命中
|
||||
} else {
|
||||
await searchTable(task, databaseType, currentRun, prefetched);
|
||||
}
|
||||
nextTableIndex.value = taskIndex + 1;
|
||||
progressDone.value = nextTableIndex.value;
|
||||
emitState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function searchTable(task: DatabaseSearchTableTask, databaseType: DatabaseType, currentRun: number) {
|
||||
/** 对一个探测批执行多表探测,返回命中表的全局下标集合;语句失败整批回退(全部视为待取数)。 */
|
||||
async function probeChunk(chunk: DatabaseSearchTableTask[], chunkColumns: Array<DatabaseSearchColumnLite[] | undefined>, databaseType: DatabaseType, currentRun: number): Promise<Set<number>> {
|
||||
const chunkStart = nextTableIndex.value;
|
||||
const probeable: number[] = [];
|
||||
chunkColumns.forEach((columns, offset) => {
|
||||
if (columns?.length) probeable.push(offset);
|
||||
});
|
||||
|
||||
if (!probeable.length) return new Set();
|
||||
|
||||
const executionId = makeExecutionId();
|
||||
currentExecutionId.value = executionId;
|
||||
let hitProbeIndexes: Set<number>;
|
||||
try {
|
||||
hitProbeIndexes = await probeTables({
|
||||
databaseType,
|
||||
driverProfile: connection.value?.driver_profile,
|
||||
term: activeKeyword.value,
|
||||
limit: activePerTableLimit.value,
|
||||
tables: probeable.map((offset) => ({
|
||||
schema: chunk[offset].schema,
|
||||
tableName: chunk[offset].table.name,
|
||||
columns: chunkColumns[offset]!,
|
||||
})),
|
||||
executeQuery: (sql, runExecutionId) => api.executeQuery(props.connectionId, props.database, sql, undefined, runExecutionId),
|
||||
executionId,
|
||||
});
|
||||
} catch {
|
||||
// 探测批失败(单表权限/中途 DDL 等):整批回退逐表,错误重新隔离到单表
|
||||
hitProbeIndexes = new Set(probeable.map((_, index) => index));
|
||||
}
|
||||
if (currentExecutionId.value === executionId) currentExecutionId.value = "";
|
||||
if (currentRun !== runId || cancelled.value) return new Set();
|
||||
|
||||
const hitTaskIndexes = new Set<number>();
|
||||
hitProbeIndexes.forEach((probeIndex) => {
|
||||
const offset = probeable[probeIndex];
|
||||
if (offset !== undefined) hitTaskIndexes.add(chunkStart + offset);
|
||||
});
|
||||
return hitTaskIndexes;
|
||||
}
|
||||
|
||||
async function searchTable(task: DatabaseSearchTableTask, databaseType: DatabaseType, currentRun: number, prefetchedColumns?: DatabaseSearchColumnLite[]) {
|
||||
const tableLabel = task.schema ? `${task.schema}.${task.table.name}` : task.table.name;
|
||||
try {
|
||||
const schema = task.schema || props.database;
|
||||
const columns = await api.getColumns(props.connectionId, props.database, schema, task.table.name);
|
||||
// 批量取列命中时跳过逐表 getColumns;数据搜索单表查询只消费这三个字段
|
||||
const columns: ColumnInfo[] = prefetchedColumns?.length
|
||||
? prefetchedColumns.map((column) => ({
|
||||
name: column.name,
|
||||
data_type: column.data_type,
|
||||
is_primary_key: column.is_primary_key,
|
||||
is_nullable: false,
|
||||
column_default: null,
|
||||
extra: null,
|
||||
}))
|
||||
: await api.getColumns(props.connectionId, props.database, schema, task.table.name);
|
||||
const query = await buildDatabaseSearchSql({
|
||||
databaseType,
|
||||
driverProfile: connection.value?.driver_profile,
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, defineComponent, h, nextTick, ref, type App } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import i18n from "@/i18n";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import DatabaseSearchPanel from "@/components/search/DatabaseSearchPanel.vue";
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
listSchemas: vi.fn(),
|
||||
listTables: vi.fn(),
|
||||
getColumns: vi.fn(),
|
||||
executeQuery: vi.fn(),
|
||||
buildDatabaseSearchSql: vi.fn(),
|
||||
buildDatabaseSearchColumnsSql: vi.fn(),
|
||||
buildDatabaseSearchProbeSql: vi.fn(),
|
||||
buildSearchResultWhere: vi.fn(),
|
||||
cancelQuery: vi.fn(),
|
||||
}));
|
||||
|
||||
const connectionConfig: { id: string; name: string; db_type: string; driver_profile: string | null } = {
|
||||
id: "conn-1",
|
||||
name: "test-conn",
|
||||
db_type: "dameng",
|
||||
driver_profile: null,
|
||||
};
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
useConnectionStore: () => ({
|
||||
getConfig: (id: string) => (connectionConfig.id === id ? connectionConfig : undefined),
|
||||
ensureConnected: vi.fn(async () => {}),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/button", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Button: defineComponent({
|
||||
props: { disabled: Boolean, variant: String, size: String },
|
||||
emits: ["click"],
|
||||
setup(props, { slots, emit }) {
|
||||
return () => h("button", { disabled: props.disabled, onClick: (e: MouseEvent) => emit("click", e) }, slots.default?.());
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/input", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Input: defineComponent({
|
||||
props: { modelValue: [String, Number], disabled: Boolean, placeholder: String, type: String },
|
||||
emits: ["update:modelValue", "keydown"],
|
||||
setup(props, { emit }) {
|
||||
return () =>
|
||||
h("input", {
|
||||
value: props.modelValue,
|
||||
disabled: props.disabled,
|
||||
placeholder: props.placeholder,
|
||||
type: props.type || "text",
|
||||
onInput: (e: any) => emit("update:modelValue", e.target.value),
|
||||
onKeydown: (e: any) => emit("keydown", e),
|
||||
});
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/badge", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Badge: defineComponent({
|
||||
setup(_props, { slots }) {
|
||||
return () => h("span", slots.default?.());
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/label", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Label: defineComponent({
|
||||
setup(_props, { slots }) {
|
||||
return () => h("label", slots.default?.());
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/select", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
const passthrough = defineComponent({
|
||||
setup(_props, { slots }) {
|
||||
return () => h("div", slots.default?.());
|
||||
},
|
||||
});
|
||||
return { Select: passthrough, SelectContent: passthrough, SelectItem: passthrough, SelectTrigger: passthrough, SelectValue: passthrough };
|
||||
});
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
connectionConfig.db_type = "dameng";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 模块工厂里的 vi.fn 不受 restoreAllMocks 影响,必须显式清调用历史与实现
|
||||
vi.clearAllMocks();
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
const TABLES = [
|
||||
{ name: "USERS", table_type: "TABLE" },
|
||||
{ name: "FILES", table_type: "TABLE" },
|
||||
{ name: "V_ORDERS", table_type: "VIEW" },
|
||||
];
|
||||
|
||||
const BULK_COLUMN_ROWS = {
|
||||
columns: ["table_name", "column_name", "data_type", "is_primary_key"],
|
||||
rows: [
|
||||
["USERS", "ID", "NUMBER", 1],
|
||||
["USERS", "EMAIL", "VARCHAR2", 0],
|
||||
["FILES", "DATA", "BLOB", 0],
|
||||
],
|
||||
};
|
||||
|
||||
async function mountPanel() {
|
||||
vi.mocked(api.listTables).mockResolvedValue(TABLES as any);
|
||||
vi.mocked(api.getColumns).mockResolvedValue([]);
|
||||
vi.mocked(api.buildSearchResultWhere).mockResolvedValue('"ID" = 1');
|
||||
vi.mocked(api.buildDatabaseSearchSql).mockResolvedValue({ sql: "SELECT_USERS", searchableColumns: ["EMAIL"] });
|
||||
vi.mocked(api.buildDatabaseSearchColumnsSql).mockResolvedValue({ sql: "COLS_SQL" });
|
||||
vi.mocked(api.buildDatabaseSearchProbeSql).mockResolvedValue({ sql: "PROBE_SQL", includedIndices: [0] });
|
||||
|
||||
const states: any[] = [];
|
||||
const panelRef = ref<any>(null);
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () =>
|
||||
h(DatabaseSearchPanel, {
|
||||
ref: panelRef,
|
||||
connectionId: "conn-1",
|
||||
database: "shop",
|
||||
schema: "DLWEB",
|
||||
initialState: { keyword: "admin" },
|
||||
inDialog: true,
|
||||
"onUpdate:state": (state: any) => states.push(state),
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(i18n);
|
||||
mountedApps.push(app);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
return { panelRef, states };
|
||||
}
|
||||
|
||||
describe("DatabaseSearchPanel scan flow", () => {
|
||||
it("bulk path: one columns query + one probe, fetch rows only for hit tables", async () => {
|
||||
vi.mocked(api.executeQuery).mockImplementation(async (_conn: string, _db: string, sql: string) => {
|
||||
if (sql === "COLS_SQL") return BULK_COLUMN_ROWS as any;
|
||||
if (sql === "PROBE_SQL") return { columns: ["TABLE_LABEL", "HITS"], rows: [["t0", 2]] } as any;
|
||||
if (sql === "SELECT_USERS") return { columns: ["ID", "EMAIL"], rows: [[1, "admin@x.com"]] } as any;
|
||||
throw new Error(`unexpected sql: ${sql}`);
|
||||
});
|
||||
|
||||
const { panelRef, states } = await mountPanel();
|
||||
await panelRef.value.startSearch();
|
||||
await vi.waitFor(() => {
|
||||
expect(states.at(-1)?.progressDone).toBe(2);
|
||||
});
|
||||
|
||||
// 批量取列:一条目录查询,零次逐表 getColumns
|
||||
expect(api.buildDatabaseSearchColumnsSql).toHaveBeenCalledTimes(1);
|
||||
expect(api.getColumns).not.toHaveBeenCalled();
|
||||
|
||||
// 探测:两个任务表(视图已被排除),只有 USERS 命中并取数
|
||||
expect(api.buildDatabaseSearchProbeSql).toHaveBeenCalledTimes(1);
|
||||
const probeArg = vi.mocked(api.buildDatabaseSearchProbeSql).mock.calls[0][0];
|
||||
expect(probeArg.tables.map((t: any) => t.tableName)).toEqual(["USERS", "FILES"]);
|
||||
expect(api.buildDatabaseSearchSql).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(api.buildDatabaseSearchSql).mock.calls[0][0].tableName).toBe("USERS");
|
||||
// 预取列被传入单表查询(证明不再走 getColumns)
|
||||
expect(vi.mocked(api.buildDatabaseSearchSql).mock.calls[0][0].columns.map((c: any) => c.name)).toEqual(["ID", "EMAIL"]);
|
||||
|
||||
// 结果与回表条件一致
|
||||
const finalState = states.at(-1);
|
||||
expect(finalState.results).toHaveLength(1);
|
||||
expect(finalState.results[0].tableName).toBe("USERS");
|
||||
expect(finalState.results[0].whereInput).toBe('"ID" = 1');
|
||||
expect(finalState.progressTotal).toBe(2);
|
||||
});
|
||||
|
||||
it("probe failure falls back to per-table scan for the whole batch", async () => {
|
||||
vi.mocked(api.executeQuery).mockImplementation(async (_conn: string, _db: string, sql: string) => {
|
||||
if (sql === "COLS_SQL") return BULK_COLUMN_ROWS as any;
|
||||
if (sql === "PROBE_SQL") throw new Error("ORA-00942: table or view does not exist");
|
||||
if (sql === "SELECT_USERS") return { columns: ["ID", "EMAIL"], rows: [[1, "admin@x.com"]] } as any;
|
||||
throw new Error(`unexpected sql: ${sql}`);
|
||||
});
|
||||
|
||||
const { panelRef, states } = await mountPanel();
|
||||
await panelRef.value.startSearch();
|
||||
await vi.waitFor(() => {
|
||||
expect(states.at(-1)?.progressDone).toBe(2);
|
||||
});
|
||||
|
||||
// 整批回退:两张表都执行了逐表取数查询,列信息仍来自批量 Map
|
||||
expect(api.buildDatabaseSearchSql).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(api.buildDatabaseSearchSql).mock.calls.map((c) => c[0].tableName)).toEqual(["USERS", "FILES"]);
|
||||
expect(api.getColumns).not.toHaveBeenCalled();
|
||||
// 失败被吞掉,不产生 generalError
|
||||
expect(states.at(-1)?.generalError).toBe("");
|
||||
});
|
||||
|
||||
it("unsupported dialect silently falls back to per-table getColumns", async () => {
|
||||
connectionConfig.db_type = "sqlite";
|
||||
vi.mocked(api.getColumns).mockResolvedValue([{ name: "EMAIL", data_type: "varchar", is_nullable: true, column_default: null, is_primary_key: false, extra: null }] as any);
|
||||
vi.mocked(api.executeQuery).mockImplementation(async (_conn: string, _db: string, sql: string) => {
|
||||
if (sql === "SELECT_USERS") return { columns: ["EMAIL"], rows: [["admin@x.com"]] } as any;
|
||||
throw new Error(`unexpected sql: ${sql}`);
|
||||
});
|
||||
|
||||
const { panelRef, states } = await mountPanel();
|
||||
await panelRef.value.startSearch();
|
||||
await vi.waitFor(() => {
|
||||
expect(states.at(-1)?.progressDone).toBe(2);
|
||||
});
|
||||
|
||||
expect(api.buildDatabaseSearchColumnsSql).not.toHaveBeenCalled();
|
||||
expect(api.buildDatabaseSearchProbeSql).not.toHaveBeenCalled();
|
||||
expect(api.getColumns).toHaveBeenCalledTimes(2);
|
||||
expect(api.buildDatabaseSearchSql).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("tables without probe hits advance progress without any fetch query", async () => {
|
||||
vi.mocked(api.executeQuery).mockImplementation(async (_conn: string, _db: string, sql: string) => {
|
||||
if (sql === "COLS_SQL") return BULK_COLUMN_ROWS as any;
|
||||
throw new Error(`unexpected sql: ${sql}`);
|
||||
});
|
||||
|
||||
const { panelRef, states } = await mountPanel();
|
||||
// mountPanel 会设置默认探测 mock,这里在启动前覆盖为 null(全部无可搜列等价场景)
|
||||
vi.mocked(api.buildDatabaseSearchProbeSql).mockResolvedValue(null);
|
||||
await panelRef.value.startSearch();
|
||||
await vi.waitFor(() => {
|
||||
expect(states.at(-1)?.progressDone).toBe(2);
|
||||
});
|
||||
|
||||
expect(api.buildDatabaseSearchSql).not.toHaveBeenCalled();
|
||||
expect(states.at(-1)?.results).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1650,6 +1650,22 @@ async function focusSidebarTreeNode(nodeId: string) {
|
||||
row.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
function sidebarDialogOpen(): boolean {
|
||||
return sidebarDdlOpen.value || sidebarElasticsearchIndexMetadataOpen.value || sidebarObjectSourceOpen.value || sidebarProcedureOpen.value || sidebarVisibleDatabasesOpen.value || sidebarVisibleSchemasOpen.value || sidebarVisibleNacosNamespacesOpen.value || sidebarTableNameFilterOpen.value;
|
||||
}
|
||||
|
||||
function restoreSidebarTreeFocusAfterDialog(nodeId: string | null | undefined) {
|
||||
if (!nodeId) return;
|
||||
void nextTick(() => {
|
||||
if (!sidebarDialogOpen()) void focusSidebarTreeNode(nodeId);
|
||||
});
|
||||
}
|
||||
|
||||
async function focusSelectedNode() {
|
||||
const nodeId = store.selectedTreeNodeId;
|
||||
if (nodeId) await focusSidebarTreeNode(nodeId);
|
||||
}
|
||||
|
||||
function clearSidebarSelection() {
|
||||
// Clicking the blank area of the tree clears the current selection. Row
|
||||
// clicks call event.stopPropagation(), so this only fires for blank clicks
|
||||
@@ -2372,27 +2388,51 @@ watch(sidebarElasticsearchIndexMetadataOpen, (open) => {
|
||||
});
|
||||
|
||||
watch(sidebarObjectSourceOpen, (open) => {
|
||||
if (!open) sidebarObjectSourceTarget.value = null;
|
||||
if (!open) {
|
||||
const nodeId = sidebarObjectSourceTarget.value?.node.id;
|
||||
sidebarObjectSourceTarget.value = null;
|
||||
restoreSidebarTreeFocusAfterDialog(nodeId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(sidebarProcedureOpen, (open) => {
|
||||
if (!open) sidebarProcedureTarget.value = null;
|
||||
if (!open) {
|
||||
const nodeId = sidebarProcedureTarget.value?.id;
|
||||
sidebarProcedureTarget.value = null;
|
||||
restoreSidebarTreeFocusAfterDialog(nodeId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(sidebarVisibleDatabasesOpen, (open) => {
|
||||
if (!open) sidebarVisibleDatabasesTarget.value = null;
|
||||
if (!open) {
|
||||
const nodeId = sidebarVisibleDatabasesTarget.value?.id;
|
||||
sidebarVisibleDatabasesTarget.value = null;
|
||||
restoreSidebarTreeFocusAfterDialog(nodeId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(sidebarVisibleSchemasOpen, (open) => {
|
||||
if (!open) sidebarVisibleSchemasTarget.value = null;
|
||||
if (!open) {
|
||||
const nodeId = sidebarVisibleSchemasTarget.value?.id;
|
||||
sidebarVisibleSchemasTarget.value = null;
|
||||
restoreSidebarTreeFocusAfterDialog(nodeId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(sidebarVisibleNacosNamespacesOpen, (open) => {
|
||||
if (!open) sidebarVisibleNacosNamespacesTarget.value = null;
|
||||
if (!open) {
|
||||
const nodeId = sidebarVisibleNacosNamespacesTarget.value?.id;
|
||||
sidebarVisibleNacosNamespacesTarget.value = null;
|
||||
restoreSidebarTreeFocusAfterDialog(nodeId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(sidebarTableNameFilterOpen, (open) => {
|
||||
if (!open) sidebarTableNameFilterTarget.value = null;
|
||||
if (!open) {
|
||||
const nodeId = sidebarTableNameFilterTarget.value?.id;
|
||||
sidebarTableNameFilterTarget.value = null;
|
||||
restoreSidebarTreeFocusAfterDialog(nodeId);
|
||||
}
|
||||
});
|
||||
|
||||
function collapseAllTreeNodes() {
|
||||
@@ -2705,7 +2745,15 @@ onUnmounted(() => {
|
||||
if (sidebarTreeContentMeasureFrame) window.cancelAnimationFrame(sidebarTreeContentMeasureFrame);
|
||||
});
|
||||
|
||||
defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSidebar, disconnectAllActiveConnections, openTableStructureForSelection: openSidebarTableStructureForSelection });
|
||||
defineExpose({
|
||||
focusSearch,
|
||||
createNewGroup,
|
||||
collapseAllTreeNodes,
|
||||
locateTabInSidebar,
|
||||
disconnectAllActiveConnections,
|
||||
focusSelectedNode,
|
||||
openTableStructureForSelection: openSidebarTableStructureForSelection,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -118,7 +118,7 @@ import { isXuguSyntheticTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
import { buildXuguSchedulerJobSql, type XuguSchedulerJobAction } from "@/lib/database/xuguSchedulerJobSql";
|
||||
import { canViewDatabaseObjectDependencies, databaseDependencyProviderFor } from "@/lib/database/databaseObjectDependencies";
|
||||
import { elasticsearchClearIndexPreview, isElasticsearchClearConfirmed, isElasticsearchIndexPattern } from "@/lib/sidebar/elasticsearchIndexActions";
|
||||
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
|
||||
import { sidebarObjectTemplateForGroup } from "@/lib/sidebar/databaseObjectTemplates";
|
||||
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate, insertTemplatePrimaryKeyConflicts } from "@/lib/table/tableSqlTemplates";
|
||||
import SqlTemplateInsertRowsDialog from "@/components/sidebar/SqlTemplateInsertRowsDialog.vue";
|
||||
import { joinExportedDdls } from "@/lib/export/ddlExport";
|
||||
@@ -964,7 +964,7 @@ async function toggle(requestId = beginNavigationRequest()) {
|
||||
// Keep the click path aligned with every object-group definition. In
|
||||
// particular, schema-level trigger/type groups have no tableName, so they
|
||||
// must use the generic object loader rather than the table-trigger loader.
|
||||
const databaseObjectGroup = !!objectTypesForGroupNode(node.type);
|
||||
const databaseObjectGroup = !!objectTypesForGroupNode(node.type) && !node.tableName;
|
||||
const needsConfiguredTablePageDrain = node.type === "group-tables" && connectionStore.getConfig(node.connectionId || "")?.sidebar_auto_load_all_tables === true && hasTableTreeLoadMore(node.children ?? []);
|
||||
if (databaseObjectGroup && connectionStore.canUseLoadedTreeNodeToggle(node) && !needsConfiguredTablePageDrain) {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
@@ -3070,6 +3070,17 @@ async function executeXuguSchedulerJobAction(action: XuguSchedulerJobAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function findTreeNodeById(nodes: readonly TreeNode[], id: string): TreeNode | null {
|
||||
for (const item of nodes) {
|
||||
if (item.id === id) return item;
|
||||
if (item.children?.length) {
|
||||
const found = findTreeNodeById(item.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function toggleTriggerState(node: TreeNode, enabled: boolean) {
|
||||
if (!node.connectionId || !node.database) return;
|
||||
const databaseType = databaseTypeForNode(node);
|
||||
@@ -3089,8 +3100,18 @@ async function toggleTriggerState(node: TreeNode, enabled: boolean) {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
||||
if (!executed) return;
|
||||
if (node.meta && typeof node.meta === "object") {
|
||||
(node.meta as TriggerInfo).enabled = enabled;
|
||||
const updatedMeta: TriggerInfo = {
|
||||
// The fallback must satisfy TriggerInfo; the spread ternary's narrower
|
||||
// arm is what tripped vue-tsc (TS2322) in CI.
|
||||
...(node.meta && typeof node.meta === "object" ? (node.meta as TriggerInfo) : { name: triggerName, event: "", timing: "" }),
|
||||
enabled,
|
||||
};
|
||||
// `node` may be a frozen action-target copy (createSidebarActionTarget);
|
||||
// assigning to it throws in strict mode, and the live node below is the
|
||||
// only assignment that matters.
|
||||
const liveNode = findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? findTreeNodeById(connectionStore.treeNodes, node.id);
|
||||
if (liveNode) {
|
||||
liveNode.meta = updatedMeta;
|
||||
}
|
||||
toast(t(enabled ? "contextMenu.enableTriggerSuccess" : "contextMenu.disableTriggerSuccess", { name: triggerName }), 3000);
|
||||
await connectionStore.refreshTreeNode(node);
|
||||
@@ -5688,10 +5709,10 @@ async function confirmDeleteHBaseTable() {
|
||||
}
|
||||
}
|
||||
|
||||
function createMysqlObjectTemplate() {
|
||||
function createSidebarObjectTemplate() {
|
||||
const node = activeNode.value;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
const template = mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node);
|
||||
const template = sidebarObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node);
|
||||
if (!template) return;
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
const tabId = queryStore.createTab(node.connectionId, node.database, t(template.titleKey), "query", node.schema, undefined, node.catalog);
|
||||
@@ -7752,11 +7773,11 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
|
||||
appendTableVGroupContainerItems(node, items);
|
||||
// 9. Group Labels (group-columns, group-tables, etc.)
|
||||
if (isGroupLabel(node)) {
|
||||
const mysqlObjectTemplate = node.connectionId ? mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node) : null;
|
||||
const objectTemplate = node.connectionId ? sidebarObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node) : null;
|
||||
const hasMongoCreateIndexAction = node.type === "group-indexes" && canCreateMongoIndex.value;
|
||||
const hasMongoDropAllIndexesAction = node.type === "group-indexes" && canDropAllMongoIndexes.value;
|
||||
const canCreateGroupView = node.type === "group-views" && !!node.connectionId && !!node.database && !["neo4j", "nebula"].includes(currentDatabaseType() || "");
|
||||
const hasGroupAction = (node.type === "group-tables" && canCreateTable.value) || canCreateGroupView || !!mysqlObjectTemplate || hasMongoCreateIndexAction || hasMongoDropAllIndexesAction;
|
||||
const hasGroupAction = (node.type === "group-tables" && canCreateTable.value) || canCreateGroupView || !!objectTemplate || hasMongoCreateIndexAction || hasMongoDropAllIndexesAction;
|
||||
const canLoadAllObjectGroup = node.type === "group-tables" || node.type === "group-dolt-system-tables" || node.type === "group-views" || node.type === "group-materialized-views";
|
||||
if (node.type === "group-tables" && canCreateTable.value) {
|
||||
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
|
||||
@@ -7773,8 +7794,8 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
|
||||
if (node.type === "group-events" && node.connectionId && node.database) {
|
||||
items.push({ label: t("contextMenu.createEvent"), action: openMysqlEventCreateEditor, icon: Plus });
|
||||
}
|
||||
if (mysqlObjectTemplate) {
|
||||
items.push({ label: t(mysqlObjectTemplate.titleKey), action: createMysqlObjectTemplate, icon: Plus });
|
||||
if (objectTemplate) {
|
||||
items.push({ label: t(objectTemplate.titleKey), action: createSidebarObjectTemplate, icon: Plus });
|
||||
}
|
||||
if (hasMongoCreateIndexAction) {
|
||||
items.push({ label: t("contextMenu.createMongoIndex"), action: openCreateMongoIndexDialog, icon: Plus });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject, shallowRef, watch, onBeforeUnmount } from "vue";
|
||||
import { ref, computed, inject, nextTick, shallowRef, watch, onBeforeUnmount } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
Database,
|
||||
@@ -230,7 +230,12 @@ const stopPasteHandlerRegistration = watch(
|
||||
|
||||
const activeNode = shallowRef<TreeNode>(props.node);
|
||||
|
||||
const isDisabledTrigger = computed(() => activeNode.value.type === "trigger" && (activeNode.value.meta as TriggerInfo | undefined)?.enabled === false);
|
||||
const isDisabledTrigger = computed(() => {
|
||||
const current = activeNode.value;
|
||||
if (current.type !== "trigger") return false;
|
||||
const meta = (props.node.meta as TriggerInfo | undefined) ?? (current.meta as TriggerInfo | undefined);
|
||||
return meta?.enabled === false;
|
||||
});
|
||||
|
||||
const showProductionBadge = computed(() => {
|
||||
const node = activeNode.value;
|
||||
@@ -1109,6 +1114,16 @@ const renameInputRef = ref<HTMLInputElement>();
|
||||
/** Snapshot of the row being renamed: reprojecting the tree mid-rename recycles activeNode. */
|
||||
const renameTargetNode = shallowRef<TreeNode | null>(null);
|
||||
|
||||
function restoreRowFocus() {
|
||||
void nextTick(() => {
|
||||
// Only reclaim focus when it is genuinely lost (fell back to <body>).
|
||||
// A blur triggered by the user focusing something else (editor, search
|
||||
// box, Tab navigation) must keep that target focused.
|
||||
if (document.activeElement && document.activeElement !== document.body) return;
|
||||
rowRef.value?.focus({ preventScroll: true });
|
||||
});
|
||||
}
|
||||
|
||||
function startRenameGroup() {
|
||||
renameTargetNode.value = activeNode.value;
|
||||
renameInput.value = activeNode.value.label;
|
||||
@@ -1167,12 +1182,17 @@ function finishRenameGroup() {
|
||||
const trimmed = renameInput.value.trim();
|
||||
// An empty name cancels the rename and keeps the group as-is — never delete
|
||||
// here. Deleting a group is done explicitly via the context menu (issue #681).
|
||||
if (!trimmed || trimmed === target.label) return;
|
||||
if (!trimmed || trimmed === target.label) {
|
||||
restoreRowFocus();
|
||||
return;
|
||||
}
|
||||
if (target.type === "table-vgroup" && target.vgroupId) {
|
||||
connectionStore.renameTableVGroup(target, target.vgroupId, trimmed);
|
||||
restoreRowFocus();
|
||||
return;
|
||||
}
|
||||
connectionStore.renameConnectionGroup(target.id, trimmed);
|
||||
restoreRowFocus();
|
||||
}
|
||||
|
||||
async function finishRenameSavedSql() {
|
||||
@@ -1180,13 +1200,17 @@ async function finishRenameSavedSql() {
|
||||
isRenamingSavedSql.value = false;
|
||||
const fileId = activeNode.value.savedSqlId;
|
||||
const trimmed = renameInput.value.trim();
|
||||
if (!fileId || !trimmed || ensureSqlExtension(trimmed) === activeNode.value.label) return;
|
||||
if (!fileId || !trimmed || ensureSqlExtension(trimmed) === activeNode.value.label) {
|
||||
restoreRowFocus();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
await savedSqlStore.renameFile(fileId, ensureSqlExtension(trimmed));
|
||||
} catch (e: any) {
|
||||
toast(t("savedSql.renameFailed", { message: savedSqlErrorMessage(e, t) }), 5000);
|
||||
}
|
||||
restoreRowFocus();
|
||||
}
|
||||
|
||||
async function finishRenameConnection() {
|
||||
@@ -1194,12 +1218,16 @@ async function finishRenameConnection() {
|
||||
isRenamingConnection.value = false;
|
||||
const connectionId = activeNode.value.connectionId;
|
||||
const trimmed = renameInput.value.trim();
|
||||
if (!connectionId || !trimmed || trimmed === activeNode.value.label) return;
|
||||
if (!connectionId || !trimmed || trimmed === activeNode.value.label) {
|
||||
restoreRowFocus();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await connectionStore.renameConnection(connectionId, trimmed);
|
||||
} catch (e: any) {
|
||||
toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
restoreRowFocus();
|
||||
}
|
||||
|
||||
function finishRename() {
|
||||
@@ -1212,6 +1240,7 @@ function cancelRename() {
|
||||
isRenamingGroup.value = false;
|
||||
isRenamingSavedSql.value = false;
|
||||
isRenamingConnection.value = false;
|
||||
restoreRowFocus();
|
||||
}
|
||||
|
||||
const PINNED_TREE_NODE_DRAG_TYPE = "__pinned-tree-node__";
|
||||
|
||||
@@ -160,6 +160,39 @@ describe("TreeItem connection quick rename", () => {
|
||||
expect(renameConnection).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Enter", connection.name],
|
||||
["Escape", "Reporting EU"],
|
||||
])("restores focus to the connection row after %s dismisses rename", async (key, value) => {
|
||||
const { container, input } = await mountConnectionRename();
|
||||
typeName(input, value);
|
||||
|
||||
press(input, key);
|
||||
|
||||
const row = container.querySelector<HTMLElement>(`[data-node-id="${connection.id}"]`);
|
||||
await vi.waitFor(() => expect(document.activeElement).toBe(row));
|
||||
});
|
||||
|
||||
it("keeps focus on the element the user clicked when a blur commits rename", async () => {
|
||||
const { container, input } = await mountConnectionRename();
|
||||
typeName(input, "Reporting EU");
|
||||
|
||||
const outside = document.createElement("button");
|
||||
container.append(outside);
|
||||
outside.focus();
|
||||
input.dispatchEvent(new FocusEvent("blur"));
|
||||
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
// The blur commit must not steal focus back to the tree row: the user
|
||||
// deliberately moved focus elsewhere before the commit landed.
|
||||
const row = container.querySelector<HTMLElement>(`[data-node-id="${connection.id}"]`);
|
||||
expect(document.activeElement).toBe(outside);
|
||||
expect(document.activeElement).not.toBe(row);
|
||||
expect(renameConnection).toHaveBeenCalledWith(connection.id, "Reporting EU");
|
||||
});
|
||||
|
||||
it.each(["", " ", connection.name])("cancels blank or unchanged input %j", async (name) => {
|
||||
const { input } = await mountConnectionRename();
|
||||
typeName(input, name);
|
||||
|
||||
@@ -170,6 +170,19 @@ describe("TreeItem trigger status", () => {
|
||||
expect(container.querySelector(disabledSelector)).toBeNull();
|
||||
});
|
||||
|
||||
it("updates the badge when trigger node meta is replaced with updated enabled status", async () => {
|
||||
const { container, node } = await mountTreeItem(triggerNode(false));
|
||||
expect(container.querySelector(disabledSelector)).not.toBeNull();
|
||||
|
||||
node.value.meta = { ...(node.value.meta as TriggerInfo), enabled: true };
|
||||
await nextTick();
|
||||
expect(container.querySelector(disabledSelector)).toBeNull();
|
||||
|
||||
node.value.meta = { ...(node.value.meta as TriggerInfo), enabled: false };
|
||||
await nextTick();
|
||||
expect(container.querySelector(disabledSelector)).not.toBeNull();
|
||||
});
|
||||
|
||||
it("localizes the badge tooltip and accessible name without disabling row activation", async () => {
|
||||
const initialNode = triggerNode(false);
|
||||
const { container, host } = await mountTreeItem(initialNode);
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { ensureReadOnlyWriteAccess } from "@/lib/database/readOnlyWriteAccess";
|
||||
import { useProductionSafetyStore } from "@/stores/productionSafetyStore";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { TransferContent, TransferObjectKind, TransferTableNameCase } from "@/lib/backend/api";
|
||||
import type { TransferColumnNameCase, TransferContent, TransferObjectKind, TransferTableNameCase } from "@/lib/backend/api";
|
||||
import { crossFamilyTransferableKinds, isSameTransferFamily, isTransferPairSupported, transferObjectFamily, transferObjectKindsForDatabase } from "@/lib/database/transferObjectKinds";
|
||||
import ObjectSelectionTree from "@/components/transfer/ObjectSelectionTree.vue";
|
||||
import TransferTaskTree from "@/components/transfer/TransferTaskTree.vue";
|
||||
@@ -208,6 +208,7 @@ const targetTableStrategy = computed<TransferStrategy>({
|
||||
},
|
||||
});
|
||||
const targetTableNameCase = ref<TransferTableNameCase>("preserve");
|
||||
const targetColumnNameCase = ref<TransferColumnNameCase>("preserve");
|
||||
const quoteTargetColumnNames = ref(true);
|
||||
const batchSize = ref(1000);
|
||||
const operationTimeoutMode = ref<"inherit" | "custom">("inherit");
|
||||
@@ -297,7 +298,10 @@ function structureOverwriteSupported(): boolean {
|
||||
// The table-name-case selector is hidden for Xugu pairs; reset any earlier choice so the
|
||||
// blocking xuguPreserveTableNames hint cannot strand the user without a visible control.
|
||||
watch(isXuguTransferPair, (isXuguPair) => {
|
||||
if (isXuguPair) targetTableNameCase.value = "preserve";
|
||||
if (isXuguPair) {
|
||||
targetTableNameCase.value = "preserve";
|
||||
targetColumnNameCase.value = "preserve";
|
||||
}
|
||||
});
|
||||
const targetStrategyType = computed(() => (targetTransferType.value === "xugu" ? "xugu" : connectionType(targetConnectionId.value)));
|
||||
const xuguTransferHint = computed(() => {
|
||||
@@ -823,6 +827,7 @@ function resetState(cancelTaskLoad = true) {
|
||||
transferContent.value = "structureAndData";
|
||||
selectedStrategy.value = "append";
|
||||
targetTableNameCase.value = "preserve";
|
||||
targetColumnNameCase.value = "preserve";
|
||||
quoteTargetColumnNames.value = true;
|
||||
batchSize.value = 1000;
|
||||
operationTimeoutMode.value = "inherit";
|
||||
@@ -945,6 +950,7 @@ async function requestStartTransfer() {
|
||||
...buildTransferObjectSelectionField(selectedObjects.value, treeDisabledGroups.value),
|
||||
...transferStrategyOptions(targetTableStrategy.value),
|
||||
targetTableNameCase: targetTableNameCase.value,
|
||||
targetColumnNameCase: targetColumnNameCase.value,
|
||||
quoteTargetColumnNames: quoteTargetColumnNames.value,
|
||||
ownershipPolicy: "preserve",
|
||||
batchSize: batchSize.value,
|
||||
@@ -1029,6 +1035,7 @@ function currentConfig(): TransferTaskConfig {
|
||||
content: transferContent.value,
|
||||
...transferStrategyOptions(targetTableStrategy.value),
|
||||
targetTableNameCase: targetTableNameCase.value,
|
||||
targetColumnNameCase: targetColumnNameCase.value,
|
||||
quoteTargetColumnNames: quoteTargetColumnNames.value,
|
||||
batchSize: batchSize.value,
|
||||
operationTimeoutSecs: operationTimeoutMode.value === "custom" && operationTimeoutValid.value ? operationTimeoutSecs.value : undefined,
|
||||
@@ -1083,6 +1090,7 @@ async function loadTaskIntoForm(task: TransferTask) {
|
||||
transferContent.value = config.content;
|
||||
selectedStrategy.value = resolveTransferStrategy(config);
|
||||
targetTableNameCase.value = config.targetTableNameCase;
|
||||
targetColumnNameCase.value = config.targetColumnNameCase ?? "preserve";
|
||||
quoteTargetColumnNames.value = config.quoteTargetColumnNames;
|
||||
batchSize.value = config.batchSize;
|
||||
operationTimeoutMode.value = config.operationTimeoutSecs === undefined ? "inherit" : "custom";
|
||||
@@ -1632,6 +1640,19 @@ async function saveConfigTask() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="!isXuguTransferPair" class="flex items-center gap-3">
|
||||
<Label class="text-xs shrink-0">{{ t("transfer.targetColumnNameCase") }}</Label>
|
||||
<Select v-model="targetColumnNameCase">
|
||||
<SelectTrigger class="h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="preserve">{{ t("transfer.tableNameCasePreserve") }}</SelectItem>
|
||||
<SelectItem value="lower">{{ t("transfer.tableNameCaseLower") }}</SelectItem>
|
||||
<SelectItem value="upper">{{ t("transfer.tableNameCaseUpper") }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="showTargetColumnQuoteOption" class="flex items-center gap-3">
|
||||
<Label for="transfer-quote-target-column-names" class="text-xs shrink-0">{{ t("transfer.quoteTargetColumnNames") }}</Label>
|
||||
<Switch id="transfer-quote-target-column-names" v-model="quoteTargetColumnNames" size="sm" />
|
||||
|
||||
@@ -22,6 +22,14 @@ defineSlots<{
|
||||
default(props: { onContextMenu: (event: MouseEvent) => void; isOpen: boolean }): any;
|
||||
}>();
|
||||
|
||||
// The pointer must settle on another row before it takes the submenu over from
|
||||
// the open panel: a diagonal slide towards that panel crosses the rows in
|
||||
// between, and swinging straight into their menus is what makes the menu feel
|
||||
// unusable.
|
||||
const SUBMENU_SWITCH_DELAY_MS = 300;
|
||||
// How long the submenu survives while the pointer is outside both panels.
|
||||
const SUBMENU_CLOSE_DELAY_MS = 150;
|
||||
|
||||
const show = ref(false);
|
||||
const x = ref(0);
|
||||
const y = ref(0);
|
||||
@@ -34,11 +42,15 @@ const subRef = ref<HTMLElement>();
|
||||
const subX = ref(0);
|
||||
const subY = ref(0);
|
||||
let subCloseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let subSwitchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let subAnchorRect: { left: number; right: number; top: number; bottom: number } | null = null;
|
||||
// The wedge between the trigger row and the submenu panel, rebuilt each time the
|
||||
// pointer leaves a trigger. While the pointer stays inside it the submenu is
|
||||
// treated as hovered, so a diagonal move towards the panel does not close it.
|
||||
let subGraceArea: GraceAreaPoint[] | null = null;
|
||||
// The row under the pointer, kept with its element so a delayed hand-over can
|
||||
// anchor the panel without waiting for another hover.
|
||||
let hoveredRow: { index: number; trigger: HTMLElement } | null = null;
|
||||
let contextMenuRegistration: ContextMenuRegistration | null = null;
|
||||
let menuRevision = 0;
|
||||
|
||||
@@ -48,6 +60,9 @@ function close() {
|
||||
activeSubIndex.value = null;
|
||||
subAnchorRect = null;
|
||||
subGraceArea = null;
|
||||
hoveredRow = null;
|
||||
cancelSubClose();
|
||||
cancelSubSwitch();
|
||||
activeItems.value = [];
|
||||
show.value = false;
|
||||
emit("close");
|
||||
@@ -79,7 +94,12 @@ function onScroll(e: Event) {
|
||||
// Submenus (and tall main menus) are scrollable; ignore their own scroll
|
||||
// so wheel/trackpad scrolling does not dismiss the menu.
|
||||
if (isScrollInsideMenu(e)) {
|
||||
if (e.target instanceof Node && menuRef.value?.contains(e.target)) activeSubIndex.value = null;
|
||||
if (e.target instanceof Node && menuRef.value?.contains(e.target)) {
|
||||
// Scrolling the main menu under a stationary pointer must not reopen the
|
||||
// submenu on the next pointer move.
|
||||
hoveredRow = null;
|
||||
closeSubmenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
close();
|
||||
@@ -120,7 +140,9 @@ onMounted(() => {
|
||||
function handleItemClick(item: ContextMenuItem, index: number, event: MouseEvent) {
|
||||
if (itemIsDisabled(item)) return;
|
||||
if (item.children?.length) {
|
||||
onItemMouseEnter(index, event);
|
||||
const trigger = event.currentTarget as HTMLElement;
|
||||
hoveredRow = { index, trigger };
|
||||
openSubmenu(index, trigger);
|
||||
return;
|
||||
}
|
||||
if (item.closeOnSelect !== false) close();
|
||||
@@ -146,12 +168,9 @@ function runItemAction(item: ContextMenuItem) {
|
||||
if (item.closeOnSelect !== false || !item.refreshItems) return;
|
||||
const refresh = () => {
|
||||
if (!show.value || revision !== menuRevision) return;
|
||||
activeSubIndex.value = null;
|
||||
hoveredRow = null;
|
||||
subAnchorRect = null;
|
||||
if (subCloseTimer) {
|
||||
clearTimeout(subCloseTimer);
|
||||
subCloseTimer = null;
|
||||
}
|
||||
closeSubmenu();
|
||||
activeItems.value = item.refreshItems!();
|
||||
nextTick(() => {
|
||||
if (!show.value || revision !== menuRevision) return;
|
||||
@@ -190,36 +209,24 @@ function onContextMenu(event: MouseEvent, itemsOverride?: ContextMenuItem[]) {
|
||||
function onItemMouseEnter(index: number, event: MouseEvent) {
|
||||
lastMouseX = event.clientX;
|
||||
lastMouseY = event.clientY;
|
||||
subGraceArea = null;
|
||||
const item = activeItems.value[index];
|
||||
if (!item?.children?.length || itemIsDisabled(item)) {
|
||||
// Moving to an item without children — close submenu immediately, no delay needed
|
||||
activeSubIndex.value = null;
|
||||
return;
|
||||
}
|
||||
if (subCloseTimer) {
|
||||
clearTimeout(subCloseTimer);
|
||||
subCloseTimer = null;
|
||||
}
|
||||
const trigger = event.currentTarget as HTMLElement;
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
subAnchorRect = { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom };
|
||||
subX.value = rect.right + 4;
|
||||
subY.value = rect.top;
|
||||
activeSubIndex.value = index;
|
||||
nextTick(() => adjustSubPosition());
|
||||
// A new row restarts the settle delay; moving within one row must not.
|
||||
if (hoveredRow?.index !== index) cancelSubSwitch();
|
||||
hoveredRow = { index, trigger: event.currentTarget as HTMLElement };
|
||||
syncSubmenu();
|
||||
}
|
||||
|
||||
function onItemMouseLeave(event: MouseEvent) {
|
||||
if (activeSubIndex.value === null) return;
|
||||
subGraceArea = buildSubmenuGraceAreaForPointer(event.clientX, event.clientY);
|
||||
scheduleSubClose();
|
||||
hoveredRow = null;
|
||||
// Leaving a trigger opens the wedge that keeps its submenu alive while the
|
||||
// pointer crosses the gap towards the panel.
|
||||
if (activeSubIndex.value !== null) subGraceArea = buildSubmenuGraceAreaForPointer(event.clientX, event.clientY);
|
||||
syncSubmenu();
|
||||
}
|
||||
|
||||
// Last known pointer position. Updated by a document-level pointermove while the
|
||||
// menu is open; hover events alone are not enough, because the pointer spends the
|
||||
// trigger-to-submenu journey in the gap between the two panels, where nothing
|
||||
// fires.
|
||||
// Last known pointer position. Updated by every row hover and by a document-level
|
||||
// pointermove while the menu is open; hover events alone are not enough, because
|
||||
// the pointer spends the trigger-to-submenu journey in the gap between the two
|
||||
// panels, where nothing fires.
|
||||
let lastMouseX = 0;
|
||||
let lastMouseY = 0;
|
||||
|
||||
@@ -248,38 +255,108 @@ function pointerIsHeadingToSubmenu(): boolean {
|
||||
return isMouseOverSub() || isPointerInGraceArea({ x: lastMouseX, y: lastMouseY }, subGraceArea);
|
||||
}
|
||||
|
||||
/**
|
||||
* The one place that decides which row owns the submenu. Row hover and pointer
|
||||
* moves all funnel through it, so the panel is re-evaluated while the pointer
|
||||
* travels instead of being settled once, when it crosses a row edge.
|
||||
*/
|
||||
function syncSubmenu(fromTimer = false) {
|
||||
if (activeSubIndex.value === null && !hoveredRow) return;
|
||||
if (activeSubIndex.value !== null && pointerIsHeadingToSubmenu()) {
|
||||
// The pointer is in the panel, or in the wedge leading to it. Keep that panel
|
||||
// even while the pointer sits on a neighbouring row: handing it over here is
|
||||
// what made a diagonal slide land in the wrong list.
|
||||
cancelSubClose();
|
||||
cancelSubSwitch();
|
||||
return;
|
||||
}
|
||||
const row = hoveredRow;
|
||||
const item = row ? activeItems.value[row.index] : undefined;
|
||||
if (row && item?.children?.length && !itemIsDisabled(item)) {
|
||||
if (activeSubIndex.value === row.index) {
|
||||
cancelSubClose();
|
||||
cancelSubSwitch();
|
||||
return;
|
||||
}
|
||||
// A panel is already open for another row. The pointer may only be crossing
|
||||
// this one on its way to that panel, so let it settle before handing over.
|
||||
if (activeSubIndex.value !== null && !fromTimer) {
|
||||
cancelSubClose();
|
||||
if (!subSwitchTimer) {
|
||||
subSwitchTimer = setTimeout(() => {
|
||||
subSwitchTimer = null;
|
||||
syncSubmenu(true);
|
||||
}, SUBMENU_SWITCH_DELAY_MS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
openSubmenu(row.index, row.trigger);
|
||||
return;
|
||||
}
|
||||
cancelSubSwitch();
|
||||
if (row) {
|
||||
closeSubmenu();
|
||||
return;
|
||||
}
|
||||
if (activeSubIndex.value === null) return;
|
||||
if (fromTimer) closeSubmenu();
|
||||
else scheduleSubClose();
|
||||
}
|
||||
|
||||
function onDocumentPointerMove(event: PointerEvent) {
|
||||
lastMouseX = event.clientX;
|
||||
lastMouseY = event.clientY;
|
||||
// Cancel a pending close as soon as the pointer proves it is moving towards the
|
||||
// submenu, rather than waiting for the timer and re-checking its position.
|
||||
if (subCloseTimer && pointerIsHeadingToSubmenu()) {
|
||||
clearTimeout(subCloseTimer);
|
||||
subCloseTimer = null;
|
||||
}
|
||||
syncSubmenu();
|
||||
}
|
||||
|
||||
function scheduleSubClose() {
|
||||
if (subCloseTimer) clearTimeout(subCloseTimer);
|
||||
if (subCloseTimer) return;
|
||||
subCloseTimer = setTimeout(() => {
|
||||
subCloseTimer = null;
|
||||
if (!pointerIsHeadingToSubmenu()) {
|
||||
activeSubIndex.value = null;
|
||||
subGraceArea = null;
|
||||
}
|
||||
}, 150);
|
||||
syncSubmenu(true);
|
||||
}, SUBMENU_CLOSE_DELAY_MS);
|
||||
}
|
||||
|
||||
function cancelSubClose() {
|
||||
if (!subCloseTimer) return;
|
||||
clearTimeout(subCloseTimer);
|
||||
subCloseTimer = null;
|
||||
}
|
||||
|
||||
function cancelSubSwitch() {
|
||||
if (!subSwitchTimer) return;
|
||||
clearTimeout(subSwitchTimer);
|
||||
subSwitchTimer = null;
|
||||
}
|
||||
|
||||
function openSubmenu(index: number, trigger: HTMLElement) {
|
||||
cancelSubClose();
|
||||
cancelSubSwitch();
|
||||
if (activeSubIndex.value === index) return;
|
||||
subGraceArea = null;
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
subAnchorRect = { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom };
|
||||
subX.value = rect.right + 4;
|
||||
subY.value = rect.top;
|
||||
activeSubIndex.value = index;
|
||||
nextTick(() => adjustSubPosition());
|
||||
}
|
||||
|
||||
function closeSubmenu() {
|
||||
cancelSubClose();
|
||||
cancelSubSwitch();
|
||||
activeSubIndex.value = null;
|
||||
subGraceArea = null;
|
||||
}
|
||||
|
||||
function onSubMouseEnter() {
|
||||
if (subCloseTimer) {
|
||||
clearTimeout(subCloseTimer);
|
||||
subCloseTimer = null;
|
||||
}
|
||||
cancelSubClose();
|
||||
}
|
||||
|
||||
function onSubMouseLeave() {
|
||||
activeSubIndex.value = null;
|
||||
subGraceArea = null;
|
||||
function onSubMouseLeave(event: MouseEvent) {
|
||||
lastMouseX = event.clientX;
|
||||
lastMouseY = event.clientY;
|
||||
syncSubmenu();
|
||||
}
|
||||
|
||||
function adjustSubPosition() {
|
||||
@@ -311,10 +388,9 @@ function adjustSubPosition() {
|
||||
// under the mouse cursor. Since the mouse didn't move, mouseenter won't
|
||||
// fire — cancel any pending close to prevent the submenu from flashing.
|
||||
nextTick(() => {
|
||||
if (isMouseOverSub() && subCloseTimer) {
|
||||
clearTimeout(subCloseTimer);
|
||||
subCloseTimer = null;
|
||||
}
|
||||
if (!isMouseOverSub()) return;
|
||||
cancelSubClose();
|
||||
cancelSubSwitch();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -339,6 +415,8 @@ function shortcutKeys(shortcut?: string): string[] {
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cancelSubClose();
|
||||
cancelSubSwitch();
|
||||
contextMenuRegistration?.dispose();
|
||||
contextMenuRegistration = null;
|
||||
document.removeEventListener("pointerdown", onPointerDownOutside, true);
|
||||
|
||||
@@ -586,3 +586,111 @@ describe("submenu pointer grace area", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("submenu hand-over between rows", () => {
|
||||
// Tall panel, so the wedge from a row towards it is what a diagonal slide
|
||||
// rides. happy-dom performs no layout, so the panel rectangle is stubbed.
|
||||
const SUBMENU_RECT = { left: 200, right: 300, top: 100, bottom: 300, width: 100, height: 200 } as DOMRect;
|
||||
|
||||
const ITEMS = [{ label: "Data tools", children: [{ label: "Export" }] }, { label: "Copy", children: [{ label: "Copy cell" }] }, { label: "Refresh" }];
|
||||
|
||||
function rowButton(label: string): HTMLButtonElement {
|
||||
return Array.from(document.body.querySelectorAll<HTMLButtonElement>("button")).find((button) => button.textContent === label)!;
|
||||
}
|
||||
|
||||
function bodyText(): string {
|
||||
return document.body.textContent ?? "";
|
||||
}
|
||||
|
||||
async function mountRowMenu() {
|
||||
const root = defineComponent({
|
||||
setup: () => () => h(CustomContextMenu, { items: ITEMS }, { default: ({ onContextMenu }: { onContextMenu: (event: MouseEvent) => void }) => h("div", { id: "handover-target", onContextmenu: onContextMenu }, "Target") }),
|
||||
});
|
||||
const container = document.createElement("div");
|
||||
mountedContainers.push(container);
|
||||
document.body.append(container);
|
||||
const app = createApp(root);
|
||||
app.mount(container);
|
||||
container.querySelector("#handover-target")?.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, clientX: 10, clientY: 20 }));
|
||||
await nextTick();
|
||||
rowButton("Data tools").dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, clientX: 50, clientY: 140 }));
|
||||
await nextTick();
|
||||
const submenu = Array.from(document.body.querySelectorAll("[data-dbx-context-menu]")).at(-1) as HTMLElement;
|
||||
submenu.getBoundingClientRect = () => SUBMENU_RECT;
|
||||
return { app };
|
||||
}
|
||||
|
||||
// Leaves the open trigger at its bottom edge and enters the row below, which is
|
||||
// what a diagonal slide towards the panel looks like.
|
||||
function slideInto(label: string, x: number, y: number) {
|
||||
rowButton("Data tools").dispatchEvent(new MouseEvent("mouseleave", { bubbles: true, clientX: 50, clientY: 140 }));
|
||||
rowButton(label).dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, clientX: x, clientY: y }));
|
||||
}
|
||||
|
||||
function movePointerTo(x: number, y: number) {
|
||||
document.dispatchEvent(new PointerEvent("pointermove", { bubbles: true, clientX: x, clientY: y }));
|
||||
}
|
||||
|
||||
it("keeps the open submenu while the pointer crosses a neighbouring trigger row", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { app } = await mountRowMenu();
|
||||
expect(bodyText()).toContain("Export");
|
||||
|
||||
slideInto("Copy", 60, 145);
|
||||
await nextTick();
|
||||
vi.advanceTimersByTime(1000);
|
||||
await nextTick();
|
||||
|
||||
// Still inside the wedge towards its own panel, so the crossed row must not
|
||||
// take the submenu over, not even after the settle delay.
|
||||
expect(bodyText()).toContain("Export");
|
||||
expect(bodyText()).not.toContain("Copy cell");
|
||||
app.unmount();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the open submenu while the pointer crosses a row without children", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { app } = await mountRowMenu();
|
||||
|
||||
slideInto("Refresh", 60, 145);
|
||||
await nextTick();
|
||||
vi.advanceTimersByTime(1000);
|
||||
await nextTick();
|
||||
|
||||
expect(bodyText()).toContain("Export");
|
||||
app.unmount();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("hands the submenu over once the pointer settles on a neighbouring row", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { app } = await mountRowMenu();
|
||||
|
||||
// Crossing the row inside the wedge keeps the panel. Moving on down, below
|
||||
// the wedge, is the pointer asking for this row after all.
|
||||
slideInto("Copy", 60, 145);
|
||||
await nextTick();
|
||||
movePointerTo(60, 260);
|
||||
await nextTick();
|
||||
vi.advanceTimersByTime(100);
|
||||
await nextTick();
|
||||
expect(bodyText()).toContain("Export");
|
||||
|
||||
vi.advanceTimersByTime(400);
|
||||
await nextTick();
|
||||
expect(bodyText()).toContain("Copy cell");
|
||||
expect(bodyText()).not.toContain("Export");
|
||||
app.unmount();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,58 @@ describe("useDataGridAutoRefresh", () => {
|
||||
autoRefresh.stop();
|
||||
});
|
||||
|
||||
it.each([5, 10, 30, 60, 300])("starts refreshing when the %i second interval is selected", (seconds) => {
|
||||
vi.useFakeTimers();
|
||||
const refresh = vi.fn();
|
||||
const autoRefresh = useDataGridAutoRefresh({ canRefresh: computed(() => true), refresh });
|
||||
|
||||
autoRefresh.setIntervalSeconds(seconds);
|
||||
|
||||
expect(autoRefresh.enabled.value).toBe(true);
|
||||
vi.advanceTimersByTime(seconds * 1000 - 1);
|
||||
expect(refresh).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(refresh).toHaveBeenCalledOnce();
|
||||
vi.advanceTimersByTime(seconds * 1000);
|
||||
expect(refresh).toHaveBeenCalledTimes(2);
|
||||
autoRefresh.stop();
|
||||
});
|
||||
|
||||
it("restarts refreshing when the same interval is selected after disabling", () => {
|
||||
vi.useFakeTimers();
|
||||
const refresh = vi.fn();
|
||||
const autoRefresh = useDataGridAutoRefresh({ canRefresh: computed(() => true), refresh, initialIntervalSeconds: 5 });
|
||||
|
||||
autoRefresh.toggle();
|
||||
autoRefresh.toggle();
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(refresh).not.toHaveBeenCalled();
|
||||
|
||||
autoRefresh.setIntervalSeconds(5);
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
expect(autoRefresh.enabled.value).toBe(true);
|
||||
expect(refresh).toHaveBeenCalledOnce();
|
||||
autoRefresh.stop();
|
||||
});
|
||||
|
||||
it("replaces the running countdown when a different interval is selected", () => {
|
||||
vi.useFakeTimers();
|
||||
const refresh = vi.fn();
|
||||
const autoRefresh = useDataGridAutoRefresh({ canRefresh: computed(() => true), refresh, initialIntervalSeconds: 5 });
|
||||
|
||||
autoRefresh.toggle();
|
||||
vi.advanceTimersByTime(4000);
|
||||
autoRefresh.setIntervalSeconds(10);
|
||||
vi.advanceTimersByTime(9999);
|
||||
expect(refresh).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(refresh).toHaveBeenCalledOnce();
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(refresh).toHaveBeenCalledTimes(2);
|
||||
autoRefresh.stop();
|
||||
});
|
||||
|
||||
it("does not refresh while the operation is unavailable", () => {
|
||||
vi.useFakeTimers();
|
||||
const refresh = vi.fn();
|
||||
|
||||
@@ -348,6 +348,33 @@ describe("useDataGridColumnResize", () => {
|
||||
expect(remounted.columnWidths.value).toEqual(first.columnWidths.value);
|
||||
});
|
||||
|
||||
it("restores adjusted column widths for external SQL file executions across tab reopens", () => {
|
||||
const firstTab = { id: "tab-instance-1", externalSqlPath: "/queries/monthly_report.sql", activeResultIndex: 0 };
|
||||
const secondTab = { id: "tab-instance-2", externalSqlPath: "/queries/monthly_report.sql", activeResultIndex: 0 };
|
||||
const firstKey = resultGridColumnWidthCacheKey(firstTab);
|
||||
const secondKey = resultGridColumnWidthCacheKey(secondTab);
|
||||
|
||||
expect(secondKey).toBe(firstKey);
|
||||
|
||||
const firstGrid = createResizeState({
|
||||
columns: ["id", "report_data"],
|
||||
rows: [[1, "summary"]],
|
||||
cacheKey: firstKey,
|
||||
});
|
||||
firstGrid.initColumnWidths();
|
||||
firstGrid.onResizeStart(1, new MouseEvent("mousedown", { clientX: 100, cancelable: true }));
|
||||
document.dispatchEvent(new MouseEvent("mouseup", { clientX: 350 }));
|
||||
|
||||
const secondGrid = createResizeState({
|
||||
columns: ["id", "report_data"],
|
||||
rows: [[2, "another summary"]],
|
||||
cacheKey: secondKey,
|
||||
});
|
||||
secondGrid.initColumnWidths();
|
||||
|
||||
expect(secondGrid.columnWidths.value[1]).toBe(firstGrid.columnWidths.value[1]);
|
||||
});
|
||||
|
||||
it("isolates widths by result cache key", () => {
|
||||
const first = createResizeState({ columns: ["id"], rows: [["x".repeat(120)]], cacheKey: "result-a" });
|
||||
first.initColumnWidths();
|
||||
|
||||
@@ -2966,4 +2966,25 @@ describe("useDataGridExport MySQL BLOB 文本复制 (#12413)", () => {
|
||||
|
||||
expect(extractDataGridSelection).toHaveBeenLastCalledWith(expect.objectContaining({ extractor: "raw", rows: [["0x616263"]] }));
|
||||
});
|
||||
|
||||
it("copies text-like Oracle-family blobs as text, matching the grid preview", async () => {
|
||||
const matrix: CellSelectionMatrix = { rowIndexes: [0], columnIndexes: [1], columns: ["secret"], rows: [["0x616263"]] };
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "abc", mimeType: "text/plain", fileExtension: "txt", rowCount: 1, columnCount: 1 });
|
||||
const state = createExportState(blobTable, ["id", "secret"], matrix, [1, "0x616263"], undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, false, 1, 1, "oracle");
|
||||
|
||||
await expect(state.copyWithPreference("smart")).resolves.toBe(true);
|
||||
|
||||
expect(extractDataGridSelection).toHaveBeenCalledWith(expect.objectContaining({ extractor: "raw", rows: [["abc"]] }));
|
||||
expect(copyToClipboard).toHaveBeenCalledWith("abc");
|
||||
|
||||
// The SQL extractor keeps the canonical hex even for a text-like blob, mirroring
|
||||
// the MySQL #12413 case: copy-as-INSERT must not decode bytes (#12679 review).
|
||||
const sqlMatrix: CellSelectionMatrix = { rowIndexes: [0], columnIndexes: [1], columns: ["secret"], rows: [["0x616263"]] };
|
||||
vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ text: "INSERT INTO `test_blob` (`secret`) VALUES (0x616263);", mimeType: "application/sql", fileExtension: "sql", rowCount: 1, columnCount: 1 });
|
||||
const sqlState = createExportState(blobTable, ["id", "secret"], sqlMatrix, [1, "0x616263"], undefined, undefined, [], DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS, false, undefined, false, 1, 1, "oracle");
|
||||
|
||||
await expect(sqlState.copyWithExtractor("sql-inserts")).resolves.toBe(true);
|
||||
|
||||
expect(extractDataGridSelection).toHaveBeenLastCalledWith(expect.objectContaining({ extractor: "sql-inserts", rows: [[1, "0x616263"]] }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,10 +24,10 @@ async function setup() {
|
||||
},
|
||||
);
|
||||
root = document.createElement("div");
|
||||
root.innerHTML = '<div></div><nav><div data-shortcut-resize></div><div style="padding:4px"><div style="gap:0px"><span data-shortcut-id="a"></span></div></div></nav>';
|
||||
root.innerHTML = '<div data-sidebar-connection-tree></div><div data-sidebar-connection-endpoint></div><nav><div data-shortcut-resize></div><div style="padding:4px"><div style="gap:0px"><span data-shortcut-id="a"></span></div></div></nav>';
|
||||
document.body.append(root);
|
||||
const tree = root.children[0] as HTMLElement;
|
||||
const section = root.children[1] as HTMLElement;
|
||||
const section = root.querySelector("nav") as HTMLElement;
|
||||
const handle = section.children[0] as HTMLElement;
|
||||
const scroll = section.children[1] as HTMLElement;
|
||||
let width = 184;
|
||||
@@ -76,6 +76,15 @@ async function setup() {
|
||||
};
|
||||
}
|
||||
describe("shortcut sidebar height", () => {
|
||||
it("measures the tree rather than a fixed sibling like the endpoint bar", async () => {
|
||||
const { sizing } = await setup();
|
||||
const endpoint = root.querySelector<HTMLElement>("[data-sidebar-connection-endpoint]")!;
|
||||
Object.defineProperty(endpoint, "offsetHeight", { get: () => 25 });
|
||||
for (let measurement = 0; measurement < 3; measurement++) {
|
||||
resizeObserver();
|
||||
expect(sizing.height.value).toBe(148);
|
||||
}
|
||||
});
|
||||
it("reserves the title row as well as 120px for the connection tree", async () => {
|
||||
const { sizing, savedHeight, setTotal } = await setup();
|
||||
const header = document.createElement("div");
|
||||
|
||||
@@ -221,6 +221,30 @@ describe("useSqlExecution", () => {
|
||||
expect(refreshObjects).toHaveBeenCalledWith("conn-1", "app", "public");
|
||||
});
|
||||
|
||||
it("rejects execution when the target tab is already executing", async () => {
|
||||
const sql = "SELECT 1;";
|
||||
const activeTab = ref<QueryTab | undefined>({ ...queryTab("app"), sql, isExecuting: true });
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
|
||||
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
|
||||
const queryStore = useQueryStore();
|
||||
const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql");
|
||||
const onExecutionCancelled = vi.fn();
|
||||
|
||||
const execution = useSqlExecution({
|
||||
activeTab: computed(() => activeTab.value),
|
||||
activeConnection: computed(() => activeConnection.value),
|
||||
executableSql: computed(() => sql),
|
||||
activeOutputView,
|
||||
onExecutionCancelled,
|
||||
});
|
||||
|
||||
const outcome = await execution.tryExecute(undefined, { editorViewportRequestId: 123 });
|
||||
|
||||
expect(outcome).toEqual({ status: "not-started", reason: "busy", tabId: "tab-1" });
|
||||
expect(executeCurrentSql).not.toHaveBeenCalled();
|
||||
expect(onExecutionCancelled).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it("sends every placeholder syntax and @set unchanged when substitution is disabled", async () => {
|
||||
const sql = ["@set tenant = 42;", "SELECT ?, :named, ${shell}, #{mybatis}, @sqlserver, @tenant;"].join("\n");
|
||||
const activeTab = ref<QueryTab | undefined>({ ...queryTab("app"), sql });
|
||||
|
||||
@@ -37,7 +37,9 @@ export function useDataGridAutoRefresh(options: UseDataGridAutoRefreshOptions) {
|
||||
|
||||
function setIntervalSeconds(seconds: number) {
|
||||
intervalSeconds.value = seconds;
|
||||
if (enabled.value) start();
|
||||
// Selecting an interval also starts auto-refresh, including after a stop.
|
||||
enabled.value = true;
|
||||
start();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
|
||||
@@ -22,7 +22,10 @@ export function usePluginShortcutHeight(options: { section: Ref<HTMLElement | nu
|
||||
const section = options.section.value;
|
||||
const scroll = options.scroll.value;
|
||||
if (!section || !scroll || !options.sidebarList.value) return;
|
||||
const tree = section.previousElementSibling as HTMLElement | null;
|
||||
// The endpoint row may sit between the connection tree and this section.
|
||||
// Resolve the flexible tree wrapper explicitly so its height is not
|
||||
// mistaken for the available space when the shortcut list is refreshed.
|
||||
const tree = (section.parentElement?.querySelector<HTMLElement>(":scope > [data-sidebar-connection-tree]") ?? section.previousElementSibling) as HTMLElement | null;
|
||||
const divider = section.querySelector<HTMLElement>("[data-shortcut-resize]");
|
||||
const header = section.querySelector<HTMLElement>("[data-shortcut-header]");
|
||||
available.value = (tree?.offsetHeight ?? 0) + section.offsetHeight - (divider?.offsetHeight ?? 0) - (header?.offsetHeight ?? 0);
|
||||
|
||||
@@ -295,7 +295,8 @@ export function useSqlExecution(deps: {
|
||||
cancelEditorViewportRequest(options.editorViewportRequestId);
|
||||
return notStarted("no-tab", options.tabId);
|
||||
}
|
||||
const { sql, sourceOffset, editorViewportRequestId } = await resolvedExecutableSql(sqlOverride, context);
|
||||
const { sql, sourceOffset, editorViewportRequestId: snapshotRequestId } = await resolvedExecutableSql(sqlOverride, context);
|
||||
const editorViewportRequestId = snapshotRequestId ?? options.editorViewportRequestId;
|
||||
const executionOptions = { ...options, editorViewportRequestId, tabId: context.tabId };
|
||||
if (!sql.trim()) {
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
@@ -306,6 +307,10 @@ export function useSqlExecution(deps: {
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
return notStarted("no-tab", context.tabId);
|
||||
}
|
||||
if (tab.isExecuting) {
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
return notStarted("busy", context.tabId);
|
||||
}
|
||||
if (requiresDatabaseSelection(tab, context.connection, sql)) {
|
||||
deps.onMissingDatabase?.(context.tabId);
|
||||
cancelEditorViewportRequest(editorViewportRequestId);
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("Thai locale", () => {
|
||||
expect(localeFromLanguageTag("th_TH")).toBe("th");
|
||||
expect(detectLocaleFromLanguages(["fr-FR", "th-TH"])).toBe("th");
|
||||
expect(detectLocaleFromLanguages(["fr-FR"])).toBe("en");
|
||||
expect(LOCALE_OPTIONS).toContainEqual({ value: "th", flag: "🇹🇭", label: "ภาษาไทย" });
|
||||
expect(LOCALE_OPTIONS).toContainEqual(expect.objectContaining({ value: "th", flag: "🇹🇭", label: "ภาษาไทย" }));
|
||||
});
|
||||
|
||||
it("compiles and interpolates Thai through Vue I18n", () => {
|
||||
|
||||
@@ -1119,6 +1119,9 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "M rejimi (istehsalçının JDBC drayveri)",
|
||||
gaussdbConnectionModeNativeHint: "GaussDB A/PG/Oracle uyğunluq rejimləri üçün DBX-in daxili PostgreSQL ilə uyğun protokolundan istifadə edir.",
|
||||
gaussdbConnectionModeMJdbcHint: "GaussDB M rejimi üçün com.huawei.gaussdb.jdbc.Driver ilə jdbc:gaussdb-dən istifadə edir.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (PostgreSQL ilə uyğun daxili drayver)",
|
||||
gaussdbConnectionModeDwsHint:
|
||||
"Bu bağlantını açıq şəkildə Huawei DWS kimi işarələyir. Plugin Host Estimated Plan API açıq DWS profili üçün serverin kiçik versiyasından asılı olmayaraq aktiv olur; adi GaussDB və M-JDBC bağlantıları dəstəklənmir. Daxili PostgreSQL-ə uyğun drayverdən istifadə edilir.",
|
||||
gaussdbMJdbcDriver: "GaussDB JDBC drayveri",
|
||||
gaussdbMJdbcDriverPlaceholder: "Server versiyasına uyğun gaussdbjdbc.jar seçin",
|
||||
gaussdbMJdbcDriverHint: "Drayver hər GaussDB server versiyası ilə birlikdə buraxılır. gaussdbjdbc.jar faylını uyğun GaussDB drayver paketindən əldə edin.",
|
||||
@@ -6303,6 +6306,7 @@ export default withEnglishFallback({
|
||||
modeOverwrite: "Üzərinə yaz (TRUNCATE + INSERT)",
|
||||
modeUpsert: "Əlavə et və ya yenilə (PK üzrə INSERT və ya UPDATE)",
|
||||
targetTableNameCase: "Hədəf cədvəl adının hərf registri",
|
||||
targetColumnNameCase: "Hədəf sütun adının hərf registri",
|
||||
quoteTargetColumnNames: "Hədəf sütun adlarını dırnağa al",
|
||||
tableNameCasePreserve: "Qoru",
|
||||
tableNameCaseLower: "Kiçik hərflər",
|
||||
|
||||
@@ -1402,6 +1402,17 @@ export default {
|
||||
sundbCustomDriverHint: "The SunDB Agent ships the vendor JDBC driver, so no JAR is needed (driver class csii.sundb.jdbc.SundbDriver). Provide a JAR here only to load a newer vendor driver; custom drivers are loaded in isolation inside the Agent.",
|
||||
redisTlsInsecure: "Skip certificate verification",
|
||||
redisTlsInsecureHint: "Equivalent to redis-cli --tls --insecure, for self-signed certificates or private CAs.",
|
||||
redisCaCert: "CA certificate (optional)",
|
||||
redisCaCertPlaceholder: "Path to a PEM CA certificate",
|
||||
redisCaCertBrowse: "Select Redis CA certificate",
|
||||
redisClientCert: "Client certificate (optional)",
|
||||
redisClientCertPlaceholder: "Path to a PEM client certificate",
|
||||
redisClientCertBrowse: "Select Redis client certificate",
|
||||
redisClientKey: "Client private key (optional)",
|
||||
redisClientKeyPlaceholder: "Path to a PEM client private key",
|
||||
redisClientKeyBrowse: "Select Redis client private key",
|
||||
redisTlsCertificateHint: "Leave the CA certificate empty to use the system trust store. Configure both the client certificate and key when the server requires mutual TLS (tls-auth-clients yes).",
|
||||
redisClientCertPairRequired: "Redis TLS client certificate and private key must be configured together",
|
||||
mysqlTlsMode: "TLS Mode",
|
||||
firebirdCharset: "Charset",
|
||||
firebirdCharsetHint: "Controls the connection charset and decoding of CHARACTER SET NONE fields. For GBK or other legacy data stored in ISO8859_1 fields, also set Legacy read encoding.",
|
||||
@@ -1768,6 +1779,9 @@ export default {
|
||||
gaussdbConnectionModeMJdbc: "M mode (vendor JDBC driver)",
|
||||
gaussdbConnectionModeNativeHint: "Uses DBX's built-in PostgreSQL-compatible protocol for GaussDB A/PG/Oracle compatibility modes.",
|
||||
gaussdbConnectionModeMJdbcHint: "Uses jdbc:gaussdb with com.huawei.gaussdb.jdbc.Driver for GaussDB M mode.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (native PostgreSQL-compatible driver)",
|
||||
gaussdbConnectionModeDwsHint:
|
||||
"Explicitly marks this connection as Huawei DWS. The Estimated Plan Host API is enabled for this profile without pinning a GaussDB minor version; generic GaussDB and M-JDBC connections remain unsupported. It uses the built-in PostgreSQL-compatible driver. Select only for verified DWS connections.",
|
||||
gaussdbMJdbcDriver: "GaussDB JDBC driver",
|
||||
gaussdbMJdbcDriverPlaceholder: "Select gaussdbjdbc.jar matching the server version",
|
||||
gaussdbMJdbcDriverHint: "The driver is released with each GaussDB server version. Obtain gaussdbjdbc.jar from the matching GaussDB driver package.",
|
||||
@@ -3318,6 +3332,11 @@ export default {
|
||||
noAlterSqlAvailable: "No ALTER SQL available for this column",
|
||||
copyAlterSqlFailed: "Copy failed: {message}",
|
||||
copySqlCondition: "Copy SQL Condition",
|
||||
currentPageSql: "Current page SQL",
|
||||
actualExecutionSql: "Actual execution SQL",
|
||||
copyCurrentPageSql: "Copy current page SQL",
|
||||
copyActualExecutionSql: "Copy actual execution SQL",
|
||||
pageProbeDescription: "Displays up to {count} rows for this page; reads at most one extra row to check whether another page exists.",
|
||||
copyQuerySql: "Copy SQL",
|
||||
copyExecutableSql: "Copy SQL (without comments)",
|
||||
transpose: "Transpose Row",
|
||||
@@ -5871,6 +5890,7 @@ export default {
|
||||
viewList: "List view",
|
||||
viewGrid: "Grid view",
|
||||
viewCompact: "Compact grid view",
|
||||
compactCardWidth: "Compact card minimum width",
|
||||
configureColumns: "Configure columns",
|
||||
sortAsc: "Ascending",
|
||||
sortDesc: "Descending",
|
||||
@@ -7613,6 +7633,7 @@ export default {
|
||||
modeReplaceExisting: "Replace existing objects",
|
||||
modeUpsert: "Upsert (INSERT or UPDATE by PK)",
|
||||
targetTableNameCase: "Target table name case",
|
||||
targetColumnNameCase: "Target column name case",
|
||||
quoteTargetColumnNames: "Quote target column names",
|
||||
tableNameCasePreserve: "Preserve",
|
||||
tableNameCaseLower: "Lowercase",
|
||||
|
||||
@@ -1422,6 +1422,9 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "Modo M (controlador JDBC del proveedor)",
|
||||
gaussdbConnectionModeNativeHint: "Usa el protocolo compatible con PostgreSQL integrado en DBX para los modos A/PG/Oracle de GaussDB.",
|
||||
gaussdbConnectionModeMJdbcHint: "Usa jdbc:gaussdb con com.huawei.gaussdb.jdbc.Driver para el modo M de GaussDB.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (controlador nativo compatible con PostgreSQL)",
|
||||
gaussdbConnectionModeDwsHint:
|
||||
"Marca explícitamente esta conexión como Huawei DWS. La API Estimated Plan de Plugin Host se activa para este perfil sin limitarse a una versión menor de GaussDB; las conexiones genéricas de GaussDB y M-JDBC siguen sin estar admitidas. Se utiliza el controlador integrado compatible con PostgreSQL.",
|
||||
gaussdbMJdbcDriver: "Controlador JDBC de GaussDB",
|
||||
gaussdbMJdbcDriverPlaceholder: "Seleccione gaussdbjdbc.jar compatible con la versión del servidor",
|
||||
gaussdbMJdbcDriverHint: "El controlador se publica con cada versión de GaussDB. Obtenga gaussdbjdbc.jar del paquete de controladores correspondiente.",
|
||||
@@ -1910,6 +1913,17 @@ export default withEnglishFallback({
|
||||
pluginSshKeyPickerEmpty: "No se encontraron claves",
|
||||
pluginSshKeyEncrypted: "Cifrado",
|
||||
objectCacheRefreshFailed: "Error al actualizar la caché de objetos: {message}",
|
||||
redisCaCert: "Certificado de CA (opcional)",
|
||||
redisCaCertPlaceholder: "Ruta del certificado CA en formato PEM",
|
||||
redisCaCertBrowse: "Seleccionar certificado CA de Redis",
|
||||
redisClientCert: "Certificado de cliente (opcional)",
|
||||
redisClientCertPlaceholder: "Ruta del certificado de cliente en formato PEM",
|
||||
redisClientCertBrowse: "Seleccionar certificado de cliente de Redis",
|
||||
redisClientKey: "Clave privada de cliente (opcional)",
|
||||
redisClientKeyPlaceholder: "Ruta de la clave privada de cliente en formato PEM",
|
||||
redisClientKeyBrowse: "Seleccionar clave privada de cliente de Redis",
|
||||
redisTlsCertificateHint: "Si el certificado CA se deja vacío, se utiliza el almacén de confianza del sistema; si el servidor requiere autenticación mutua (tls-auth-clients yes), configure también el certificado de cliente y la clave privada.",
|
||||
redisClientCertPairRequired: "El certificado de cliente y la clave privada para TLS de Redis deben configurarse a la vez",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "La consulta anterior de DuckDB aún se está deteniendo. Vuelva a intentarlo en breve.",
|
||||
@@ -3202,6 +3216,11 @@ export default withEnglishFallback({
|
||||
tableDataPreviewDismiss: "No volver a mostrar",
|
||||
tableDataPreviewLoaded: "Se cargaron {count} celdas con texto completo de la página actual",
|
||||
tableDataPreviewLoadFailed: "Error al cargar el texto completo: {message}",
|
||||
currentPageSql: "SQL de la página actual",
|
||||
actualExecutionSql: "SQL realmente ejecutado",
|
||||
copyCurrentPageSql: "Copiar SQL de la página actual",
|
||||
copyActualExecutionSql: "Copiar SQL realmente ejecutado",
|
||||
pageProbeDescription: "Esta página muestra como máximo {count} filas; se lee como máximo 1 fila adicional para determinar si hay una página siguiente.",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "La exportación en streaming no es compatible con esta consulta. Simplifíquela o use un controlador compatible.",
|
||||
@@ -5182,6 +5201,7 @@ export default withEnglishFallback({
|
||||
invalid: "inválido",
|
||||
refreshDiscardConfirm: "Actualizar descartará los cambios sin guardar en el código fuente, ¿deseas continuar?",
|
||||
viewCompact: "Vista de cuadrícula compacta",
|
||||
compactCardWidth: "Ancho mínimo de tarjeta compacta",
|
||||
configureColumns: "Configurar columnas de visualización",
|
||||
},
|
||||
structureEditor: {
|
||||
@@ -6829,6 +6849,7 @@ export default withEnglishFallback({
|
||||
modeReplaceExisting: "Reemplazar objetos existentes",
|
||||
modeUpsert: "Upsert (INSERT o UPDATE por PK)",
|
||||
targetTableNameCase: "Mayúsculas/minúsculas de tabla destino",
|
||||
targetColumnNameCase: "Mayúsculas/minúsculas de columna destino",
|
||||
quoteTargetColumnNames: "Citar columnas de destino",
|
||||
tableNameCasePreserve: "Conservar",
|
||||
tableNameCaseLower: "Convertir a minúsculas",
|
||||
|
||||
@@ -1384,6 +1384,9 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "Mode M (driver JDBC vendor)",
|
||||
gaussdbConnectionModeNativeHint: "Menggunakan protokol kompatibel PostgreSQL bawaan DBX untuk mode kompatibilitas GaussDB A/PG/Oracle.",
|
||||
gaussdbConnectionModeMJdbcHint: "Menggunakan jdbc:gaussdb dengan com.huawei.gaussdb.jdbc.Driver untuk mode M GaussDB.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (driver bawaan kompatibel PostgreSQL)",
|
||||
gaussdbConnectionModeDwsHint:
|
||||
"Menandai koneksi ini secara eksplisit sebagai Huawei DWS. API Estimated Plan Plugin Host aktif untuk profil ini tanpa membatasi versi minor GaussDB; koneksi GaussDB umum dan M-JDBC tetap tidak didukung. Koneksi menggunakan driver bawaan yang kompatibel dengan PostgreSQL.",
|
||||
gaussdbMJdbcDriver: "Driver JDBC GaussDB",
|
||||
gaussdbMJdbcDriverPlaceholder: "Pilih gaussdbjdbc.jar yang sesuai dengan versi server",
|
||||
gaussdbMJdbcDriverHint: "Driver dirilis bersama setiap versi server GaussDB. Dapatkan gaussdbjdbc.jar dari paket driver GaussDB yang sesuai.",
|
||||
@@ -6966,6 +6969,7 @@ export default withEnglishFallback({
|
||||
modeOverwrite: "Timpa (TRUNCATE + INSERT)",
|
||||
modeUpsert: "Upsert (INSERT atau UPDATE berdasarkan PK)",
|
||||
targetTableNameCase: "Kapitalisasi nama tabel target",
|
||||
targetColumnNameCase: "Kapitalisasi nama kolom target",
|
||||
quoteTargetColumnNames: "Beri tanda kutip pada nama kolom target",
|
||||
tableNameCasePreserve: "Pertahankan",
|
||||
tableNameCaseLower: "Huruf kecil",
|
||||
|
||||
@@ -1345,6 +1345,9 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "Modalità M (driver JDBC del fornitore)",
|
||||
gaussdbConnectionModeNativeHint: "Usa il protocollo compatibile con PostgreSQL integrato in DBX per le modalità A/PG/Oracle di GaussDB.",
|
||||
gaussdbConnectionModeMJdbcHint: "Usa jdbc:gaussdb con com.huawei.gaussdb.jdbc.Driver per la modalità M di GaussDB.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (driver nativo compatibile con PostgreSQL)",
|
||||
gaussdbConnectionModeDwsHint:
|
||||
"Identifica esplicitamente questa connessione come Huawei DWS. L'API Estimated Plan di Plugin Host si attiva per questo profilo senza vincolarsi a una versione secondaria di GaussDB; le connessioni GaussDB generiche e M-JDBC restano non supportate. La connessione usa il driver integrato compatibile con PostgreSQL.",
|
||||
gaussdbMJdbcDriver: "Driver JDBC GaussDB",
|
||||
gaussdbMJdbcDriverPlaceholder: "Seleziona gaussdbjdbc.jar corrispondente alla versione del server",
|
||||
gaussdbMJdbcDriverHint: "Il driver viene distribuito con ogni versione di GaussDB. Ottieni gaussdbjdbc.jar dal pacchetto driver corrispondente.",
|
||||
@@ -1819,6 +1822,17 @@ export default withEnglishFallback({
|
||||
pluginSshKeyPickerEmpty: "Nessuna chiave trovata",
|
||||
pluginSshKeyEncrypted: "Crittografata",
|
||||
objectCacheRefreshFailed: "Aggiornamento della cache degli oggetti non riuscito: {message}",
|
||||
redisCaCert: "Certificato CA (opzionale)",
|
||||
redisCaCertPlaceholder: "Percorso del certificato CA in formato PEM",
|
||||
redisCaCertBrowse: "Seleziona certificato CA Redis",
|
||||
redisClientCert: "Certificato client (opzionale)",
|
||||
redisClientCertPlaceholder: "Percorso del certificato client in formato PEM",
|
||||
redisClientCertBrowse: "Seleziona certificato client Redis",
|
||||
redisClientKey: "Chiave privata client (opzionale)",
|
||||
redisClientKeyPlaceholder: "Percorso della chiave privata client in formato PEM",
|
||||
redisClientKeyBrowse: "Seleziona chiave privata client Redis",
|
||||
redisTlsCertificateHint: "Se il certificato CA viene lasciato vuoto, viene utilizzato il trust store di sistema; se il server richiede l'autenticazione reciproca (tls-auth-clients yes), configurare anche il certificato client e la chiave privata.",
|
||||
redisClientCertPairRequired: "Il certificato client e la chiave privata TLS di Redis devono essere configurati insieme",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "La query DuckDB precedente è ancora in fase di arresto. Riprova a breve.",
|
||||
@@ -3075,6 +3089,11 @@ export default withEnglishFallback({
|
||||
tableDataPreviewDismiss: "Non mostrare più",
|
||||
tableDataPreviewLoaded: "Caricate {count} celle con testo completo nella pagina corrente",
|
||||
tableDataPreviewLoadFailed: "Impossibile caricare il testo completo: {message}",
|
||||
currentPageSql: "SQL della pagina corrente",
|
||||
actualExecutionSql: "SQL effettivamente eseguito",
|
||||
copyCurrentPageSql: "Copia SQL della pagina corrente",
|
||||
copyActualExecutionSql: "Copia SQL effettivamente eseguito",
|
||||
pageProbeDescription: "Questa pagina mostra al massimo {count} righe; viene letta al massimo 1 riga aggiuntiva per determinare se esiste una pagina successiva.",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "L'esportazione in streaming non è supportata per questa query. Semplificala o usa un driver supportato.",
|
||||
@@ -5002,6 +5021,7 @@ export default withEnglishFallback({
|
||||
invalid: "non valido",
|
||||
refreshDiscardConfirm: "L'aggiornamento eliminerà le modifiche non salvate al codice sorgente. Continuare?",
|
||||
viewCompact: "Vista compatta a griglia",
|
||||
compactCardWidth: "Larghezza minima scheda compatta",
|
||||
configureColumns: "Configura colonne visualizzate",
|
||||
},
|
||||
structureEditor: {
|
||||
@@ -6646,6 +6666,7 @@ export default withEnglishFallback({
|
||||
modeReplaceExisting: "Sostituisci oggetti esistenti",
|
||||
modeUpsert: "Aggiorna/Inserisci (INSERT o UPDATE per chiave primaria)",
|
||||
targetTableNameCase: "Maiuscole/minuscole tabella destinazione",
|
||||
targetColumnNameCase: "Maiuscole/minuscole colonna destinazione",
|
||||
quoteTargetColumnNames: "Quota colonne di destinazione",
|
||||
tableNameCasePreserve: "Mantieni",
|
||||
tableNameCaseLower: "Converti in minuscolo",
|
||||
|
||||
@@ -1315,6 +1315,9 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "M モード(ベンダー JDBC ドライバー)",
|
||||
gaussdbConnectionModeNativeHint: "GaussDB の A/PG/Oracle 互換モードでは、DBX 内蔵の PostgreSQL 互換プロトコルを使用します。",
|
||||
gaussdbConnectionModeMJdbcHint: "GaussDB M モードでは、jdbc:gaussdb と com.huawei.gaussdb.jdbc.Driver を使用します。",
|
||||
gaussdbConnectionModeDws: "Huawei DWS(PostgreSQL 互換の内蔵ドライバー)",
|
||||
gaussdbConnectionModeDwsHint:
|
||||
"この接続を Huawei DWS として明示的に識別します。このプロファイルでは GaussDB のマイナーバージョンを固定せずに Plugin Host の Estimated Plan API を有効にします。一般的な GaussDB 接続と M-JDBC は引き続きサポートされません。接続には PostgreSQL 互換の内蔵ドライバーを使用します。",
|
||||
gaussdbMJdbcDriver: "GaussDB JDBC ドライバー",
|
||||
gaussdbMJdbcDriverPlaceholder: "サーバーバージョンに一致する gaussdbjdbc.jar を選択",
|
||||
gaussdbMJdbcDriverHint: "ドライバーは GaussDB サーバーの各バージョンとともに配布されます。対応するドライバーパッケージから gaussdbjdbc.jar を取得してください。",
|
||||
@@ -1840,6 +1843,17 @@ export default withEnglishFallback({
|
||||
pluginSshKeyPickerEmpty: "キーが見つかりません",
|
||||
pluginSshKeyEncrypted: "暗号化済み",
|
||||
objectCacheRefreshFailed: "オブジェクトキャッシュの更新に失敗しました:{message}",
|
||||
redisCaCert: "CA 証明書(任意)",
|
||||
redisCaCertPlaceholder: "PEM 形式の CA 証明書パス",
|
||||
redisCaCertBrowse: "Redis CA 証明書を選択",
|
||||
redisClientCert: "クライアント証明書(任意)",
|
||||
redisClientCertPlaceholder: "PEM 形式のクライアント証明書パス",
|
||||
redisClientCertBrowse: "Redis クライアント証明書を選択",
|
||||
redisClientKey: "クライアント秘密鍵(任意)",
|
||||
redisClientKeyPlaceholder: "PEM 形式のクライアント秘密鍵パス",
|
||||
redisClientKeyBrowse: "Redis クライアント秘密鍵を選択",
|
||||
redisTlsCertificateHint: "CA 証明書を空欄にするとシステムの信頼ストアを使用します。サーバーが相互認証(tls-auth-clients yes)を要求する場合は、クライアント証明書と秘密鍵も合わせて設定してください。",
|
||||
redisClientCertPairRequired: "Redis TLS のクライアント証明書と秘密鍵は同時に設定する必要があります",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "前回の DuckDB クエリはまだ停止処理中です。しばらくしてから再試行してください。",
|
||||
@@ -3099,6 +3113,11 @@ export default withEnglishFallback({
|
||||
tableDataPreviewDismiss: "今後表示しない",
|
||||
tableDataPreviewLoaded: "現在のページの完全なテキストセルを {count} 個読み込みました",
|
||||
tableDataPreviewLoadFailed: "完全なテキストの読み込みに失敗しました:{message}",
|
||||
currentPageSql: "現在のページの SQL",
|
||||
actualExecutionSql: "実際に実行された SQL",
|
||||
copyCurrentPageSql: "現在のページの SQL をコピー",
|
||||
copyActualExecutionSql: "実際に実行された SQL をコピー",
|
||||
pageProbeDescription: "このページには最大 {count} 行を表示し、次のページがあるかどうかを判断するために最大 1 行を追加で読み取ります。",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "このクエリはストリーミングエクスポートに対応していません。クエリを簡略化するか、対応しているドライバーを使用してください。",
|
||||
@@ -5064,6 +5083,7 @@ export default withEnglishFallback({
|
||||
invalid: "無効",
|
||||
refreshDiscardConfirm: "更新すると、保存されていないソースコードの変更が破棄されます。続行しますか?",
|
||||
viewCompact: "コンパクトグリッドビュー",
|
||||
compactCardWidth: "コンパクトカードの最小幅",
|
||||
configureColumns: "表示列を設定",
|
||||
},
|
||||
structureEditor: {
|
||||
@@ -6666,6 +6686,7 @@ export default withEnglishFallback({
|
||||
modeReplaceExisting: "既存のオブジェクトを置き換え",
|
||||
modeUpsert: "アップサート (主キーでINSERT or UPDATE)",
|
||||
targetTableNameCase: "対象テーブル名の大小文字",
|
||||
targetColumnNameCase: "対象列名の大小文字",
|
||||
quoteTargetColumnNames: "対象列名を引用",
|
||||
tableNameCasePreserve: "そのまま",
|
||||
tableNameCaseLower: "小文字に変換",
|
||||
|
||||
@@ -1441,6 +1441,8 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "M 모드(공급업체 JDBC 드라이버)",
|
||||
gaussdbConnectionModeNativeHint: "GaussDB A/PG/Oracle 호환 모드에서는 DBX 내장 PostgreSQL 호환 프로토콜을 사용합니다.",
|
||||
gaussdbConnectionModeMJdbcHint: "GaussDB M 모드에서는 jdbc:gaussdb와 com.huawei.gaussdb.jdbc.Driver를 사용합니다.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (PostgreSQL 호환 내장 드라이버)",
|
||||
gaussdbConnectionModeDwsHint: "이 연결을 Huawei DWS로 명시적으로 표시합니다. 이 프로필에서는 GaussDB 마이너 버전을 고정하지 않고 Plugin Host Estimated Plan API를 활성화합니다. 일반 GaussDB 및 M-JDBC 연결은 계속 지원되지 않습니다. PostgreSQL 호환 내장 드라이버를 사용합니다.",
|
||||
gaussdbMJdbcDriver: "GaussDB JDBC 드라이버",
|
||||
gaussdbMJdbcDriverPlaceholder: "서버 버전에 맞는 gaussdbjdbc.jar 선택",
|
||||
gaussdbMJdbcDriverHint: "드라이버는 각 GaussDB 서버 버전과 함께 배포됩니다. 해당 GaussDB 드라이버 패키지에서 gaussdbjdbc.jar를 가져오세요.",
|
||||
@@ -1743,6 +1745,17 @@ export default withEnglishFallback({
|
||||
mqttDirectionFilterAll: "전체",
|
||||
mqttDirectionFilterReceived: "수신됨",
|
||||
mqttDirectionFilterSent: "발신됨",
|
||||
redisCaCert: "CA 인증서(선택 사항)",
|
||||
redisCaCertPlaceholder: "PEM 형식 CA 인증서 경로",
|
||||
redisCaCertBrowse: "Redis CA 인증서 선택",
|
||||
redisClientCert: "클라이언트 인증서(선택 사항)",
|
||||
redisClientCertPlaceholder: "PEM 형식 클라이언트 인증서 경로",
|
||||
redisClientCertBrowse: "Redis 클라이언트 인증서 선택",
|
||||
redisClientKey: "클라이언트 개인 키(선택 사항)",
|
||||
redisClientKeyPlaceholder: "PEM 형식 클라이언트 개인 키 경로",
|
||||
redisClientKeyBrowse: "Redis 클라이언트 개인 키 선택",
|
||||
redisTlsCertificateHint: "CA 인증서를 비워 두면 시스템 신뢰 저장소를 사용합니다. 서버에서 상호 인증(tls-auth-clients yes)을 요구하는 경우 클라이언트 인증서와 개인 키를 함께 구성하세요.",
|
||||
redisClientCertPairRequired: "Redis TLS 클라이언트 인증서와 개인 키는 반드시 함께 구성해야 합니다",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "이전 DuckDB 쿼리가 아직 중지 중입니다. 잠시 후 다시 시도해 주세요.",
|
||||
@@ -3025,6 +3038,11 @@ export default withEnglishFallback({
|
||||
tableDataPreviewDismiss: "다시 표시하지 않음",
|
||||
tableDataPreviewLoaded: "현재 페이지의 전체 텍스트 셀 {count}개를 불러왔습니다",
|
||||
tableDataPreviewLoadFailed: "전체 텍스트를 불러오지 못했습니다: {message}",
|
||||
currentPageSql: "현재 페이지 SQL",
|
||||
actualExecutionSql: "실제 실행 SQL",
|
||||
copyCurrentPageSql: "현재 페이지 SQL 복사",
|
||||
copyActualExecutionSql: "실제 실행 SQL 복사",
|
||||
pageProbeDescription: "이 페이지에는 최대 {count}행이 표시되며, 다음 페이지가 있는지 판단하기 위해 최대 1행을 추가로 읽습니다.",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "이 쿼리는 스트리밍 내보내기를 지원하지 않습니다. 쿼리를 단순화하거나 지원되는 드라이버를 사용하세요.",
|
||||
@@ -5044,6 +5062,7 @@ export default withEnglishFallback({
|
||||
sortBy: "정렬 기준",
|
||||
refreshDiscardConfirm: "새로 고침하면 저장되지 않은 소스 코드 변경 사항이 삭제됩니다. 계속하시겠습니까?",
|
||||
viewCompact: "컴팩트 3x3 그리드 뷰",
|
||||
compactCardWidth: "컴팩트 카드 최소 너비",
|
||||
configureColumns: "표시 열 구성",
|
||||
},
|
||||
structureEditor: {
|
||||
@@ -6445,6 +6464,7 @@ export default withEnglishFallback({
|
||||
modeReplaceExisting: "기존 객체 교체",
|
||||
modeUpsert: "Upsert (PK 기준 INSERT 또는 UPDATE)",
|
||||
targetTableNameCase: "대상 테이블 이름 대소문자",
|
||||
targetColumnNameCase: "대상 컬럼 이름 대소문자",
|
||||
quoteTargetColumnNames: "대상 열 이름 인용",
|
||||
tableNameCasePreserve: "유지",
|
||||
tableNameCaseLower: "소문자",
|
||||
|
||||
@@ -7013,6 +7013,7 @@ export default withEnglishFallback({
|
||||
modeOverwrite: "Nadpisz (TRUNCATE + INSERT)",
|
||||
modeUpsert: "Wstaw lub zaktualizuj (INSERT lub UPDATE wg PK)",
|
||||
targetTableNameCase: "Wielkość liter nazwy tabeli docelowej",
|
||||
targetColumnNameCase: "Wielkość liter nazwy kolumny docelowej",
|
||||
quoteTargetColumnNames: "Ujmuj nazwy kolumn docelowych w cudzysłowy",
|
||||
tableNameCasePreserve: "Zachowaj",
|
||||
tableNameCaseLower: "Małe litery",
|
||||
|
||||
@@ -1346,6 +1346,9 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "Modo M (driver JDBC do fornecedor)",
|
||||
gaussdbConnectionModeNativeHint: "Usa o protocolo compatível com PostgreSQL integrado ao DBX para os modos A/PG/Oracle do GaussDB.",
|
||||
gaussdbConnectionModeMJdbcHint: "Usa jdbc:gaussdb com com.huawei.gaussdb.jdbc.Driver para o modo M do GaussDB.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (driver nativo compatível com PostgreSQL)",
|
||||
gaussdbConnectionModeDwsHint:
|
||||
"Marca explicitamente esta conexão como Huawei DWS. A API Estimated Plan do Plugin Host é ativada para este perfil sem limitar uma versão secundária do GaussDB; conexões genéricas do GaussDB e M-JDBC continuam sem suporte. A conexão usa o driver integrado compatível com PostgreSQL.",
|
||||
gaussdbMJdbcDriver: "Driver JDBC do GaussDB",
|
||||
gaussdbMJdbcDriverPlaceholder: "Selecione gaussdbjdbc.jar compatível com a versão do servidor",
|
||||
gaussdbMJdbcDriverHint: "O driver é distribuído com cada versão do GaussDB. Obtenha gaussdbjdbc.jar no pacote de driver correspondente.",
|
||||
@@ -1820,6 +1823,17 @@ export default withEnglishFallback({
|
||||
pluginSshKeyPickerEmpty: "Nenhuma chave encontrada",
|
||||
pluginSshKeyEncrypted: "Criptografada",
|
||||
objectCacheRefreshFailed: "Falha ao atualizar o cache de objetos: {message}",
|
||||
redisCaCert: "Certificado CA (opcional)",
|
||||
redisCaCertPlaceholder: "Caminho do certificado CA no formato PEM",
|
||||
redisCaCertBrowse: "Selecionar certificado CA do Redis",
|
||||
redisClientCert: "Certificado do cliente (opcional)",
|
||||
redisClientCertPlaceholder: "Caminho do certificado do cliente no formato PEM",
|
||||
redisClientCertBrowse: "Selecionar certificado do cliente do Redis",
|
||||
redisClientKey: "Chave privada do cliente (opcional)",
|
||||
redisClientKeyPlaceholder: "Caminho da chave privada do cliente no formato PEM",
|
||||
redisClientKeyBrowse: "Selecionar chave privada do cliente do Redis",
|
||||
redisTlsCertificateHint: "Quando o certificado CA estiver em branco, o repositório de confiança do sistema será usado; se o servidor exigir autenticação mútua (tls-auth-clients yes), configure também o certificado do cliente e a chave privada.",
|
||||
redisClientCertPairRequired: "O certificado do cliente Redis TLS e a chave privada devem ser configurados juntos",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "A consulta anterior do DuckDB ainda está sendo interrompida. Tente novamente em breve.",
|
||||
@@ -3075,6 +3089,11 @@ export default withEnglishFallback({
|
||||
tableDataPreviewDismiss: "Não mostrar novamente",
|
||||
tableDataPreviewLoaded: "Carregadas {count} células de texto completo na página atual",
|
||||
tableDataPreviewLoadFailed: "Falha ao carregar o texto completo: {message}",
|
||||
currentPageSql: "SQL da página atual",
|
||||
actualExecutionSql: "SQL de execução real",
|
||||
copyCurrentPageSql: "Copiar SQL da página atual",
|
||||
copyActualExecutionSql: "Copiar SQL de execução real",
|
||||
pageProbeDescription: "Esta página exibe no máximo {count} linhas, com leitura adicional de no máximo 1 linha para determinar se há uma próxima página.",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "A exportação em streaming não é compatível com esta consulta. Simplifique-a ou use um driver compatível.",
|
||||
@@ -5002,6 +5021,7 @@ export default withEnglishFallback({
|
||||
invalid: "Inválido",
|
||||
refreshDiscardConfirm: "Atualizar descartará as modificações não salvas no código-fonte. Deseja continuar?",
|
||||
viewCompact: "Visualização compacta em grade",
|
||||
compactCardWidth: "Largura mínima do cartão compacto",
|
||||
configureColumns: "Configurar colunas de exibição",
|
||||
},
|
||||
structureEditor: {
|
||||
@@ -6646,6 +6666,7 @@ export default withEnglishFallback({
|
||||
modeReplaceExisting: "Substituir objetos existentes",
|
||||
modeUpsert: "Upsert (INSERT ou UPDATE por PK)",
|
||||
targetTableNameCase: "Maiúsculas/minúsculas da tabela de destino",
|
||||
targetColumnNameCase: "Maiúsculas/minúsculas da coluna de destino",
|
||||
quoteTargetColumnNames: "Citar colunas de destino",
|
||||
tableNameCasePreserve: "Preservar",
|
||||
tableNameCaseLower: "Converter para minúsculas",
|
||||
|
||||
@@ -1534,6 +1534,9 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "Режим M (JDBC-драйвер производителя)",
|
||||
gaussdbConnectionModeNativeHint: "Использует встроенный в DBX протокол, совместимый с PostgreSQL, для режимов совместимости GaussDB A/PG/Oracle.",
|
||||
gaussdbConnectionModeMJdbcHint: "Использует jdbc:gaussdb с com.huawei.gaussdb.jdbc.Driver для режима GaussDB M.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (встроенный драйвер с совместимостью PostgreSQL)",
|
||||
gaussdbConnectionModeDwsHint:
|
||||
"Явно указывает, что это подключение Huawei DWS. Для этого профиля API Estimated Plan Plugin Host включается без привязки к минорной версии GaussDB; обычные подключения GaussDB и M-JDBC по-прежнему не поддерживаются. Используется встроенный драйвер с совместимостью PostgreSQL.",
|
||||
gaussdbMJdbcDriver: "JDBC-драйвер GaussDB",
|
||||
gaussdbMJdbcDriverPlaceholder: "Выберите gaussdbjdbc.jar, соответствующий версии сервера",
|
||||
gaussdbMJdbcDriverHint: "Драйвер выпускается для каждой версии сервера GaussDB. Получите gaussdbjdbc.jar из соответствующего пакета драйверов GaussDB.",
|
||||
@@ -1820,6 +1823,17 @@ export default withEnglishFallback({
|
||||
cancelConnecting: "Отменить подключение",
|
||||
connectCancelled: "Подключение отменено",
|
||||
sshTotpCancelled: "Ввод одноразового кода отменён",
|
||||
redisCaCert: "Сертификат CA (необязательно)",
|
||||
redisCaCertPlaceholder: "Путь к сертификату CA в формате PEM",
|
||||
redisCaCertBrowse: "Выбрать CA-сертификат Redis",
|
||||
redisClientCert: "Клиентский сертификат (необязательно)",
|
||||
redisClientCertPlaceholder: "Путь к клиентскому сертификату в формате PEM",
|
||||
redisClientCertBrowse: "Выбрать клиентский сертификат Redis",
|
||||
redisClientKey: "Закрытый ключ клиента (необязательно)",
|
||||
redisClientKeyPlaceholder: "Путь к закрытому ключу клиента в формате PEM",
|
||||
redisClientKeyBrowse: "Выбрать закрытый ключ клиента Redis",
|
||||
redisTlsCertificateHint: "Если сертификат CA не указан, используется системное хранилище доверенных сертификатов; если сервер требует взаимной аутентификации (tls-auth-clients yes), настройте одновременно клиентский сертификат и закрытый ключ.",
|
||||
redisClientCertPairRequired: "Для Redis TLS необходимо одновременно настроить клиентский сертификат и закрытый ключ",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "Предыдущий запрос DuckDB ещё завершается. Повторите попытку чуть позже.",
|
||||
@@ -3159,6 +3173,11 @@ export default withEnglishFallback({
|
||||
tableDataPreviewDismiss: "Больше не показывать",
|
||||
tableDataPreviewLoaded: "Загружено {count} ячеек с полным текстом на текущей странице",
|
||||
tableDataPreviewLoadFailed: "Не удалось загрузить полный текст: {message}",
|
||||
currentPageSql: "SQL текущей страницы",
|
||||
actualExecutionSql: "Фактически выполненный SQL",
|
||||
copyCurrentPageSql: "Копировать SQL текущей страницы",
|
||||
copyActualExecutionSql: "Копировать фактически выполненный SQL",
|
||||
pageProbeDescription: "На этой странице отображается не более {count} строк; дополнительно считывается не более 1 строки, чтобы определить, есть ли следующая страница.",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "Потоковый экспорт не поддерживается для этого запроса. Упростите его или используйте поддерживаемый драйвер.",
|
||||
@@ -5523,6 +5542,7 @@ export default withEnglishFallback({
|
||||
sortDesc: "По убыванию",
|
||||
sortBy: "Сортировать по",
|
||||
viewCompact: "Компактный вид сетки",
|
||||
compactCardWidth: "Минимальная ширина компактной карточки",
|
||||
configureColumns: "Настроить отображаемые столбцы",
|
||||
},
|
||||
structureEditor: {
|
||||
@@ -7879,6 +7899,7 @@ export default withEnglishFallback({
|
||||
modeReplaceExisting: "Заменить существующие объекты",
|
||||
modeUpsert: "Upsert (INSERT или UPDATE по PK)",
|
||||
targetTableNameCase: "Регистр имён целевых таблиц",
|
||||
targetColumnNameCase: "Регистр имён целевых столбцов",
|
||||
quoteTargetColumnNames: "Заключать имена целевых столбцов в кавычки",
|
||||
tableNameCasePreserve: "Сохранять",
|
||||
tableNameCaseLower: "Нижний регистр",
|
||||
|
||||
@@ -1730,6 +1730,8 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "โหมด M (Driver JDBC ของผู้จำหน่าย)",
|
||||
gaussdbConnectionModeNativeHint: "ใช้โปรโตคอลที่เข้ากันได้กับ PostgreSQL ในตัวของ DBX สำหรับโหมดความเข้ากันได้กับ GaussDB A/PG/Oracle",
|
||||
gaussdbConnectionModeMJdbcHint: "ใช้ jdbc:gaussdb กับ com.huawei.gaussdb.jdbc.Driver สำหรับโหมด GaussDB M",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (Driver ในตัวที่รองรับ PostgreSQL)",
|
||||
gaussdbConnectionModeDwsHint: "ระบุการเชื่อมต่อนี้อย่างชัดเจนว่าเป็น Huawei DWS โดยโปรไฟล์นี้จะเปิดใช้ API Estimated Plan ของ Plugin Host โดยไม่จำกัดเวอร์ชันย่อยของ GaussDB การเชื่อมต่อ GaussDB ทั่วไปและ M-JDBC ยังไม่รองรับ และยังใช้ไดรเวอร์ในตัวที่รองรับ PostgreSQL",
|
||||
gaussdbMJdbcDriver: "Driver JDBC ของ GaussDB",
|
||||
gaussdbMJdbcDriverPlaceholder: "เลือก gaussdbjdbc.jar ที่ตรงกับเวอร์ชัน Server",
|
||||
gaussdbMJdbcDriverHint: "Driver ออกพร้อมกับ GaussDB แต่ละเวอร์ชัน รับ gaussdbjdbc.jar จากแพ็กเกจ Driver ของ GaussDB ที่ตรงกัน",
|
||||
@@ -2018,6 +2020,17 @@ export default withEnglishFallback({
|
||||
cancelConnecting: "ยกเลิกการเชื่อมต่อ",
|
||||
connectCancelled: "ยกเลิกการเชื่อมต่อแล้ว",
|
||||
sshTotpCancelled: "ยกเลิกการป้อน Code แบบใช้ครั้งเดียวแล้ว",
|
||||
redisCaCert: "ใบรับรอง CA (ไม่บังคับ)",
|
||||
redisCaCertPlaceholder: "พาธของใบรับรอง CA ในรูปแบบ PEM",
|
||||
redisCaCertBrowse: "เลือกใบรับรอง CA ของ Redis",
|
||||
redisClientCert: "ใบรับรองไคลเอนต์ (ไม่บังคับ)",
|
||||
redisClientCertPlaceholder: "พาธของใบรับรองไคลเอนต์ในรูปแบบ PEM",
|
||||
redisClientCertBrowse: "เลือกใบรับรองไคลเอนต์ของ Redis",
|
||||
redisClientKey: "คีย์ส่วนตัวของไคลเอนต์ (ไม่บังคับ)",
|
||||
redisClientKeyPlaceholder: "พาธของคีย์ส่วนตัวไคลเอนต์ในรูปแบบ PEM",
|
||||
redisClientKeyBrowse: "เลือกคีย์ส่วนตัวไคลเอนต์ของ Redis",
|
||||
redisTlsCertificateHint: "เมื่อเว้นว่างใบรับรอง CA ระบบจะใช้ที่เก็บความเชื่อถือของระบบ; เมื่อเซิร์ฟเวอร์ต้องการการตรวจสอบสิทธิ์แบบสองทาง (tls-auth-clients yes) กรุณาตั้งค่าใบรับรองไคลเอนต์และคีย์ส่วนตัวพร้อมกัน",
|
||||
redisClientCertPairRequired: "ต้องกำหนดค่าใบรับรองไคลเอนต์และคีย์ส่วนตัวสำหรับ Redis TLS พร้อมกัน",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "Query DuckDB ก่อนหน้ายังหยุดไม่เสร็จ โปรดลองอีกครั้งในไม่ช้า",
|
||||
@@ -3383,6 +3396,11 @@ export default withEnglishFallback({
|
||||
tableDataPreviewDismiss: "ไม่แสดงอีก",
|
||||
tableDataPreviewLoaded: "โหลดเซลล์ข้อความฉบับเต็มในหน้าปัจจุบันแล้ว {count} เซลล์",
|
||||
tableDataPreviewLoadFailed: "โหลดข้อความฉบับเต็มไม่สำเร็จ: {message}",
|
||||
currentPageSql: "SQL ของหน้าปัจจุบัน",
|
||||
actualExecutionSql: "SQL ที่ดำเนินการจริง",
|
||||
copyCurrentPageSql: "คัดลอก SQL ของหน้าปัจจุบัน",
|
||||
copyActualExecutionSql: "คัดลอก SQL ที่ดำเนินการจริง",
|
||||
pageProbeDescription: "หน้านี้แสดงได้สูงสุด {count} แถว และอ่านเพิ่มได้สูงสุด 1 แถวเพื่อตรวจสอบว่ามีหน้าถัดไปหรือไม่",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "ไม่รองรับการส่งออกแบบ Streaming สำหรับ Query นี้ ทำให้เรียบง่ายขึ้นหรือใช้ Driver ที่รองรับ",
|
||||
@@ -5857,6 +5875,7 @@ export default withEnglishFallback({
|
||||
sortDesc: "เรียงจากมากไปน้อย",
|
||||
sortBy: "เรียงตาม",
|
||||
viewCompact: "มุมมองกริดแบบกะทัดรัด",
|
||||
compactCardWidth: "ความกว้างขั้นต่ำของการ์ดแบบกะทัดรัด",
|
||||
configureColumns: "กำหนดค่าคอลัมน์ที่แสดง",
|
||||
},
|
||||
structureEditor: {
|
||||
@@ -8269,6 +8288,7 @@ export default withEnglishFallback({
|
||||
modeReplaceExisting: "แทนที่ออบเจ็กต์ที่มีอยู่",
|
||||
modeUpsert: "Upsert (INSERT หรือ UPDATE ด้วย PK)",
|
||||
targetTableNameCase: "ตัวพิมพ์เล็กใหญ่ของชื่อ Table เป้าหมาย",
|
||||
targetColumnNameCase: "ตัวพิมพ์เล็กใหญ่ของชื่อคอลัมน์เป้าหมาย",
|
||||
quoteTargetColumnNames: "ใส่ Quote ชื่อ Column เป้าหมาย",
|
||||
tableNameCasePreserve: "คงเดิม",
|
||||
tableNameCaseLower: "ตัวพิมพ์เล็ก",
|
||||
|
||||
@@ -1119,6 +1119,8 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "M modu (üreticinin JDBC sürücüsü)",
|
||||
gaussdbConnectionModeNativeHint: "GaussDB A/PG/Oracle uyumluluk modları için DBX'in yerleşik PostgreSQL uyumlu protokolünü kullanır.",
|
||||
gaussdbConnectionModeMJdbcHint: "GaussDB M modu için com.huawei.gaussdb.jdbc.Driver ile jdbc:gaussdb kullanır.",
|
||||
gaussdbConnectionModeDws: "Huawei DWS (yerel PostgreSQL uyumlu sürücü)",
|
||||
gaussdbConnectionModeDwsHint: "Bu bağlantıyı açıkça Huawei DWS olarak işaretler. Plugin Host Estimated Plan API'si bu profil için GaussDB alt sürümüne sabitlenmeden etkinleşir; genel GaussDB ve M-JDBC bağlantıları desteklenmez. Yerleşik PostgreSQL uyumlu sürücü kullanılmaya devam eder.",
|
||||
gaussdbMJdbcDriver: "GaussDB JDBC sürücüsü",
|
||||
gaussdbMJdbcDriverPlaceholder: "Sunucu sürümüyle eşleşen gaussdbjdbc.jar dosyasını seçin",
|
||||
gaussdbMJdbcDriverHint: "Sürücü her GaussDB sunucu sürümüyle birlikte yayımlanır. gaussdbjdbc.jar dosyasını eşleşen GaussDB sürücü paketinden alın.",
|
||||
@@ -6195,6 +6197,7 @@ export default withEnglishFallback({
|
||||
modeOverwrite: "Üzerine yaz (TRUNCATE + INSERT)",
|
||||
modeUpsert: "Upsert (PK'ye göre INSERT veya UPDATE)",
|
||||
targetTableNameCase: "Hedef tablo adı harf durumu",
|
||||
targetColumnNameCase: "Hedef sütun adı harf durumu",
|
||||
quoteTargetColumnNames: "Hedef sütun adlarını tırnak içine al",
|
||||
tableNameCasePreserve: "Koru",
|
||||
tableNameCaseLower: "Küçük harf",
|
||||
|
||||
@@ -1337,6 +1337,17 @@ export default withEnglishFallback({
|
||||
sundbCustomDriverHint: "科蓝 SUNDB Agent 已内置厂商 JDBC 驱动,无需指定 JAR(驱动类 csii.sundb.jdbc.SundbDriver)。仅在需要加载更新版厂商驱动时在此指定 JAR;自定义驱动在 Agent 内隔离加载。",
|
||||
redisTlsInsecure: "跳过证书验证",
|
||||
redisTlsInsecureHint: "等价于 redis-cli --tls --insecure,适用于自签名证书或私有 CA。",
|
||||
redisCaCert: "CA 证书(可选)",
|
||||
redisCaCertPlaceholder: "PEM 格式 CA 证书路径",
|
||||
redisCaCertBrowse: "选择 Redis CA 证书",
|
||||
redisClientCert: "客户端证书(可选)",
|
||||
redisClientCertPlaceholder: "PEM 格式客户端证书路径",
|
||||
redisClientCertBrowse: "选择 Redis 客户端证书",
|
||||
redisClientKey: "客户端私钥(可选)",
|
||||
redisClientKeyPlaceholder: "PEM 格式客户端私钥路径",
|
||||
redisClientKeyBrowse: "选择 Redis 客户端私钥",
|
||||
redisTlsCertificateHint: "CA 证书留空时使用系统信任库;服务端要求双向认证(tls-auth-clients yes)时,请同时配置客户端证书和私钥。",
|
||||
redisClientCertPairRequired: "Redis TLS 客户端证书和私钥必须同时配置",
|
||||
mysqlTlsMode: "TLS 模式",
|
||||
firebirdCharset: "连接字符集",
|
||||
firebirdCharsetHint: "控制连接字符集及 CHARACTER SET NONE 字段的解码。字段声明为 ISO8859_1、实际存为 GBK 等编码时,还需设置旧库读取编码。",
|
||||
@@ -1703,6 +1714,8 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "M 模式(厂商 JDBC 驱动)",
|
||||
gaussdbConnectionModeNativeHint: "使用 DBX 内置的 PostgreSQL 兼容协议,适用于 GaussDB 的 A/PG/Oracle 兼容模式。",
|
||||
gaussdbConnectionModeMJdbcHint: "使用 jdbc:gaussdb 协议和 com.huawei.gaussdb.jdbc.Driver,适用于 GaussDB M 模式。",
|
||||
gaussdbConnectionModeDws: "Huawei DWS(内置 PostgreSQL 兼容驱动)",
|
||||
gaussdbConnectionModeDwsHint: "显式将此连接标记为 Huawei DWS。该 profile 下按 DWS 身份启用 Plugin Host Estimated Plan API,不再限制 GaussDB 小版本;普通 GaussDB 和 M-JDBC 连接仍不支持。连接仍使用内置 PostgreSQL 兼容驱动。",
|
||||
gaussdbMJdbcDriver: "GaussDB JDBC 驱动",
|
||||
gaussdbMJdbcDriverPlaceholder: "选择与服务端版本匹配的 gaussdbjdbc.jar",
|
||||
gaussdbMJdbcDriverHint: "驱动包随 GaussDB 服务端版本发布,请从对应版本的 GaussDB 驱动包中获取 gaussdbjdbc.jar。",
|
||||
@@ -3252,6 +3265,11 @@ export default withEnglishFallback({
|
||||
noAlterSqlAvailable: "该列无 ALTER SQL 可复制",
|
||||
copyAlterSqlFailed: "复制失败:{message}",
|
||||
copySqlCondition: "复制 SQL 条件",
|
||||
currentPageSql: "当前页 SQL",
|
||||
actualExecutionSql: "实际执行 SQL",
|
||||
copyCurrentPageSql: "复制当前页 SQL",
|
||||
copyActualExecutionSql: "复制实际执行 SQL",
|
||||
pageProbeDescription: "本页最多显示 {count} 行,额外读取最多 1 行以判断是否还有下一页。",
|
||||
copyQuerySql: "复制 SQL",
|
||||
copyExecutableSql: "复制 SQL(不含注释)",
|
||||
transpose: "转置查看",
|
||||
@@ -5860,6 +5878,7 @@ export default withEnglishFallback({
|
||||
viewList: "列表视图",
|
||||
viewGrid: "平铺视图",
|
||||
viewCompact: "紧凑九宫格视图",
|
||||
compactCardWidth: "紧凑卡片最小宽度",
|
||||
configureColumns: "配置显示列",
|
||||
sortAsc: "升序",
|
||||
sortDesc: "降序",
|
||||
@@ -7595,6 +7614,7 @@ export default withEnglishFallback({
|
||||
modeReplaceExisting: "存在则替换对象",
|
||||
modeUpsert: "更新插入 (按主键 INSERT 或 UPDATE)",
|
||||
targetTableNameCase: "目标表名大小写",
|
||||
targetColumnNameCase: "目标字段名大小写",
|
||||
quoteTargetColumnNames: "引用目标字段名",
|
||||
tableNameCasePreserve: "保持原样",
|
||||
tableNameCaseLower: "转为小写",
|
||||
|
||||
@@ -1355,6 +1355,8 @@ export default withEnglishFallback({
|
||||
gaussdbConnectionModeMJdbc: "M 模式(原廠 JDBC 驅動程式)",
|
||||
gaussdbConnectionModeNativeHint: "使用 DBX 內建的 PostgreSQL 相容協定,適用於 GaussDB 的 A/PG/Oracle 相容模式。",
|
||||
gaussdbConnectionModeMJdbcHint: "使用 jdbc:gaussdb 協定與 com.huawei.gaussdb.jdbc.Driver,適用於 GaussDB M 模式。",
|
||||
gaussdbConnectionModeDws: "Huawei DWS(內建 PostgreSQL 相容驅動程式)",
|
||||
gaussdbConnectionModeDwsHint: "明確將此連線標記為 Huawei DWS。此 profile 依據 DWS 身分啟用 Plugin Host Estimated Plan API,不再限制 GaussDB 小版本;一般 GaussDB 與 M-JDBC 連線仍不支援。連線仍使用內建 PostgreSQL 相容驅動程式。",
|
||||
gaussdbMJdbcDriver: "GaussDB JDBC 驅動程式",
|
||||
gaussdbMJdbcDriverPlaceholder: "選擇與伺服器版本相符的 gaussdbjdbc.jar",
|
||||
gaussdbMJdbcDriverHint: "驅動程式隨 GaussDB 伺服器版本發布,請從對應版本的 GaussDB 驅動程式套件取得 gaussdbjdbc.jar。",
|
||||
@@ -1821,6 +1823,17 @@ export default withEnglishFallback({
|
||||
mqttClientKeyPath: "用戶端私鑰路徑",
|
||||
mqttNoLocal: "禁止本機轉送",
|
||||
readOnlyExplanation: "唯讀模式:直接阻止目前連線的寫入操作,需要暫時解鎖後才能寫入。",
|
||||
redisCaCert: "CA 憑證(選填)",
|
||||
redisCaCertPlaceholder: "PEM 格式 CA 憑證路徑",
|
||||
redisCaCertBrowse: "選擇 Redis CA 憑證",
|
||||
redisClientCert: "用戶端憑證(選填)",
|
||||
redisClientCertPlaceholder: "PEM 格式用戶端憑證路徑",
|
||||
redisClientCertBrowse: "選擇 Redis 用戶端憑證",
|
||||
redisClientKey: "用戶端私鑰(選填)",
|
||||
redisClientKeyPlaceholder: "PEM 格式用戶端私鑰路徑",
|
||||
redisClientKeyBrowse: "選擇 Redis 用戶端私鑰",
|
||||
redisTlsCertificateHint: "CA 憑證留空時,將使用系統信任存放區;若伺服器要求雙向驗證(tls-auth-clients yes),請同時設定用戶端憑證與私鑰。",
|
||||
redisClientCertPairRequired: "Redis TLS 用戶端憑證與私鑰必須同時設定",
|
||||
},
|
||||
editor: {
|
||||
duckdbDraining: "上一筆 DuckDB 查詢仍在停止,請稍後重試。",
|
||||
@@ -2858,6 +2871,11 @@ export default withEnglishFallback({
|
||||
noAlterSqlAvailable: "此欄沒有可複製的 ALTER SQL",
|
||||
copyAlterSqlFailed: "複製失敗:{message}",
|
||||
copySqlCondition: "複製 SQL 條件",
|
||||
currentPageSql: "目前頁面 SQL",
|
||||
actualExecutionSql: "實際執行 SQL",
|
||||
copyCurrentPageSql: "複製目前頁面 SQL",
|
||||
copyActualExecutionSql: "複製實際執行 SQL",
|
||||
pageProbeDescription: "本頁最多顯示 {count} 列,額外讀取最多 1 列以判斷是否還有下一頁。",
|
||||
copyQuerySql: "複製 SQL",
|
||||
copyExecutableSql: "複製 SQL(不含註解)",
|
||||
transpose: "轉置檢視",
|
||||
@@ -4748,6 +4766,7 @@ export default withEnglishFallback({
|
||||
invalid: "無效",
|
||||
refreshDiscardConfirm: "重新整理將捨棄未儲存的原始碼修改,是否繼續?",
|
||||
viewCompact: "緊湊九宮格檢視",
|
||||
compactCardWidth: "緊湊卡片最小寬度",
|
||||
configureColumns: "設定顯示欄",
|
||||
},
|
||||
structureEditor: {
|
||||
@@ -6046,6 +6065,7 @@ export default withEnglishFallback({
|
||||
overallProgress: "整體進度",
|
||||
dataTransfer: "資料傳輸",
|
||||
targetTableNameCase: "目標資料表名稱大小寫",
|
||||
targetColumnNameCase: "目標欄位名稱大小寫",
|
||||
quoteTargetColumnNames: "引用目標欄位名稱",
|
||||
tableNameCasePreserve: "保留",
|
||||
tableNameCaseLower: "小寫",
|
||||
|
||||
@@ -42,4 +42,12 @@ describe("quickConnectionOpenTarget", () => {
|
||||
database: "app",
|
||||
});
|
||||
});
|
||||
|
||||
it("carries the connection's default schema into the query tab (#11844)", () => {
|
||||
expect(quickConnectionOpenTarget({ ...connection("oracle"), database: "XE", default_schema: "REPORTING" })).toEqual({
|
||||
kind: "query",
|
||||
database: "XE",
|
||||
schema: "REPORTING",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearDataGridColumnWidthStates, createDataGridColumnMeasurementSignature, createDataGridColumnStructureSignature, DATA_GRID_COLUMN_WIDTH_STATE_LIMIT, loadDataGridColumnWidthState, removeDataGridColumnWidthState, saveDataGridColumnWidthState } from "@/lib/dataGrid/dataGridColumnWidthState";
|
||||
|
||||
describe("dataGridColumnWidthState", () => {
|
||||
const structureSignature = createDataGridColumnStructureSignature(["id", "name"], ["INT", "VARCHAR"]);
|
||||
const measurementSignature = createDataGridColumnMeasurementSignature("standard", true, [12, "Inter"]);
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
clearDataGridColumnWidthStates();
|
||||
});
|
||||
|
||||
it("stores and retrieves column width state in memory", () => {
|
||||
const identity = { cacheKey: "result-1", structureSignature, measurementSignature };
|
||||
saveDataGridColumnWidthState(identity, [0, 1], [100, 250], new Set([1]));
|
||||
|
||||
const loaded = loadDataGridColumnWidthState(identity, [0, 1]);
|
||||
expect(loaded?.widths).toEqual([100, 250]);
|
||||
expect(loaded?.userSizedColumnIndexes).toEqual(new Set([1]));
|
||||
});
|
||||
|
||||
it("persists user-sized column widths to localStorage and restores them when in-memory cache is cold", async () => {
|
||||
const identity = { cacheKey: "result-file-/queries/test.sql-0", structureSignature, measurementSignature };
|
||||
saveDataGridColumnWidthState(identity, [0, 1], [120, 300], new Set([1]));
|
||||
|
||||
// Emulate new session or cold in-memory cache
|
||||
const rawSaved = localStorage.getItem("dbx-data-grid-column-widths:result-file-/queries/test.sql-0");
|
||||
expect(rawSaved).toBeTruthy();
|
||||
|
||||
// Simulate an app restart: fresh module instance => empty in-memory map, same localStorage.
|
||||
vi.resetModules();
|
||||
const fresh = await import("@/lib/dataGrid/dataGridColumnWidthState");
|
||||
const coldIdentity = { cacheKey: "result-file-/queries/test.sql-0", structureSignature, measurementSignature };
|
||||
expect(fresh.dataGridColumnWidthStateCount()).toBe(0);
|
||||
const reloaded = fresh.loadDataGridColumnWidthState(coldIdentity, [0, 1]);
|
||||
expect(reloaded?.widths).toEqual([120, 300]);
|
||||
expect(reloaded?.userSizedColumnIndexes).toEqual(new Set([1]));
|
||||
});
|
||||
|
||||
it("does not persist to localStorage if no columns are user-sized", () => {
|
||||
const identity = { cacheKey: "auto-fit-only", structureSignature, measurementSignature };
|
||||
saveDataGridColumnWidthState(identity, [0, 1], [80, 150], new Set());
|
||||
|
||||
expect(localStorage.getItem("dbx-data-grid-column-widths:auto-fit-only")).toBeNull();
|
||||
});
|
||||
|
||||
it("removes from localStorage when removeDataGridColumnWidthState is called", () => {
|
||||
const identity = { cacheKey: "to-remove", structureSignature, measurementSignature };
|
||||
saveDataGridColumnWidthState(identity, [0, 1], [100, 200], new Set([0, 1]));
|
||||
expect(localStorage.getItem("dbx-data-grid-column-widths:to-remove")).toBeTruthy();
|
||||
|
||||
removeDataGridColumnWidthState("to-remove");
|
||||
expect(localStorage.getItem("dbx-data-grid-column-widths:to-remove")).toBeNull();
|
||||
expect(loadDataGridColumnWidthState(identity, [0, 1])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects persisted widths when structure signature does not match", () => {
|
||||
const identity = { cacheKey: "result-file-modified", structureSignature, measurementSignature };
|
||||
saveDataGridColumnWidthState(identity, [0, 1], [100, 200], new Set([1]));
|
||||
|
||||
const differentStructure = createDataGridColumnStructureSignature(["id", "name", "email"]);
|
||||
const loaded = loadDataGridColumnWidthState({ cacheKey: "result-file-modified", structureSignature: differentStructure, measurementSignature }, [0, 1, 2]);
|
||||
expect(loaded).toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects persisted widths when measurement signature does not match", () => {
|
||||
const identity = { cacheKey: "result-font-change", structureSignature, measurementSignature };
|
||||
saveDataGridColumnWidthState(identity, [0, 1], [100, 200], new Set([1]));
|
||||
|
||||
const differentMeasurement = createDataGridColumnMeasurementSignature("standard", true, [16, "Roboto"]);
|
||||
const loaded = loadDataGridColumnWidthState({ cacheKey: "result-font-change", structureSignature, measurementSignature: differentMeasurement }, [0, 1]);
|
||||
expect(loaded).toBeUndefined();
|
||||
});
|
||||
|
||||
it("evicts oldest localStorage entries when exceeding state limit", () => {
|
||||
for (let i = 0; i < DATA_GRID_COLUMN_WIDTH_STATE_LIMIT + 5; i++) {
|
||||
saveDataGridColumnWidthState({ cacheKey: `item-${i}`, structureSignature, measurementSignature }, [0], [100 + i], new Set([0]));
|
||||
}
|
||||
|
||||
// Earliest 5 should be evicted from localStorage
|
||||
expect(localStorage.getItem("dbx-data-grid-column-widths:item-0")).toBeNull();
|
||||
expect(localStorage.getItem("dbx-data-grid-column-widths:item-4")).toBeNull();
|
||||
// Latest should exist
|
||||
expect(localStorage.getItem(`dbx-data-grid-column-widths:item-${DATA_GRID_COLUMN_WIDTH_STATE_LIMIT + 4}`)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
import { databaseObjectTreeNodeSchema } from "@/lib/database/databaseFeatureSupport";
|
||||
import {
|
||||
GAUSSDB_DWS_PRODUCT_PROFILE,
|
||||
GAUSSDB_DWS_PRODUCT_PROFILE_KEY,
|
||||
GAUSSDB_M_JDBC_DRIVER_CLASS,
|
||||
connectionDatabaseMetadataSchema,
|
||||
connectionObjectTreeNodeSchema,
|
||||
@@ -85,6 +87,27 @@ describe("jdbc dialect inference", () => {
|
||||
).toBe("iris");
|
||||
});
|
||||
|
||||
it("detects Firebird JDBC connections", () => {
|
||||
expect(
|
||||
inferJdbcDialect({
|
||||
db_type: "jdbc",
|
||||
connection_string: "jdbc:firebirdsql://localhost:3050/data/test.fdb",
|
||||
}),
|
||||
).toBe("firebird");
|
||||
expect(
|
||||
inferJdbcDialect({
|
||||
db_type: "jdbc",
|
||||
jdbc_driver_class: "org.firebirdsql.jdbc.FBDriver",
|
||||
}),
|
||||
).toBe("firebird");
|
||||
expect(
|
||||
inferJdbcDialect({
|
||||
db_type: "jdbc",
|
||||
driver_profile: "firebird",
|
||||
}),
|
||||
).toBe("firebird");
|
||||
});
|
||||
|
||||
it("uses JDBC driver profiles when inferring dialect", () => {
|
||||
expect(
|
||||
inferJdbcDialect({
|
||||
@@ -368,6 +391,25 @@ describe("GaussDB connection mode", () => {
|
||||
expect(connection.jdbc_driver_class).toBeUndefined();
|
||||
});
|
||||
|
||||
it("marks DWS explicitly while retaining the native GaussDB driver profile", () => {
|
||||
const connection = { db_type: "gaussdb", driver_profile: "gaussdb", external_config: { retained: true } } as ConnectionConfig;
|
||||
|
||||
setGaussdbConnectionMode(connection, "dws");
|
||||
expect(gaussdbConnectionMode(connection)).toBe("dws");
|
||||
expect(connection.driver_profile).toBe("gaussdb");
|
||||
expect(connection.jdbc_driver_class).toBeUndefined();
|
||||
expect(connection.external_config).toEqual({ retained: true, [GAUSSDB_DWS_PRODUCT_PROFILE_KEY]: GAUSSDB_DWS_PRODUCT_PROFILE });
|
||||
expect(effectiveDatabaseTypeForConnection(connection)).toBe("gaussdb");
|
||||
|
||||
setGaussdbConnectionMode(connection, "m-jdbc");
|
||||
expect(gaussdbConnectionMode(connection)).toBe("m-jdbc");
|
||||
expect(connection.external_config).toEqual({ retained: true });
|
||||
|
||||
setGaussdbConnectionMode(connection, "native");
|
||||
expect(gaussdbConnectionMode(connection)).toBe("native");
|
||||
expect(connection.external_config).toEqual({ retained: true });
|
||||
});
|
||||
|
||||
it("uses the driver default and preserves targetServerType from legacy URL fields", () => {
|
||||
const connection = { db_type: "gaussdb", driver_profile: "gaussdb-m" } as ConnectionConfig;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ describe("triggerToggleSql", () => {
|
||||
expect(supportsTriggerToggle("gaussdb")).toBe(true);
|
||||
expect(supportsTriggerToggle("kwdb")).toBe(true);
|
||||
expect(supportsTriggerToggle("xugu")).toBe(true);
|
||||
expect(supportsTriggerToggle("firebird")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unsupported database types or undefined", () => {
|
||||
@@ -147,6 +148,25 @@ describe("triggerToggleSql", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("generates Firebird trigger enable/disable SQL", () => {
|
||||
expect(
|
||||
buildTriggerToggleSql({
|
||||
databaseType: "firebird",
|
||||
tableName: "orders",
|
||||
triggerName: "trg_orders_audit",
|
||||
enabled: true,
|
||||
}),
|
||||
).toBe('ALTER TRIGGER "trg_orders_audit" ACTIVE;');
|
||||
|
||||
expect(
|
||||
buildTriggerToggleSql({
|
||||
databaseType: "firebird",
|
||||
triggerName: 'trg_"orders"_audit',
|
||||
enabled: false,
|
||||
}),
|
||||
).toBe('ALTER TRIGGER "trg_""orders""_audit" INACTIVE;');
|
||||
});
|
||||
|
||||
it("returns null for PostgreSQL when tableName is missing", () => {
|
||||
expect(
|
||||
buildTriggerToggleSql({
|
||||
|
||||
@@ -145,4 +145,26 @@ describe("codemirrorSqlDialect", () => {
|
||||
expect(nodeNameAt(dialect, doubleQuoteStatement, '"escaped\\"quote"')).toBe("String");
|
||||
expect(nodeNameAt(dialect, doubleQuoteStatement, "col")).toBe("Identifier");
|
||||
});
|
||||
|
||||
it("tags QuotedIdentifier as tags.name so quoted columns/tables match unquoted identifiers (#12122)", async () => {
|
||||
const { highlightTree, tags } = await import("@lezer/highlight");
|
||||
const { HighlightStyle } = await import("@codemirror/language");
|
||||
|
||||
const style = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: "keyword" },
|
||||
{ tag: tags.string, color: "string" },
|
||||
{ tag: tags.name, color: "identifier" },
|
||||
]);
|
||||
|
||||
const mysqlDialect = createDbxCodeMirrorSqlDialect(langSql, "mysql", "mysql");
|
||||
const doc = "SELECT `quoted_col`, plain_col, 'literal_string' FROM `tbl`";
|
||||
const tree = mysqlDialect.language.parser.parse(doc);
|
||||
const classes = new Map<string, string>();
|
||||
highlightTree(tree, style, (from, to, cls) => classes.set(doc.slice(from, to), cls));
|
||||
|
||||
expect(classes.get("`quoted_col`")).toBeDefined();
|
||||
expect(classes.get("`quoted_col`")).toBe(classes.get("plain_col"));
|
||||
expect(classes.get("`quoted_col`")).toBe(classes.get("`tbl`"));
|
||||
expect(classes.get("`quoted_col`")).not.toBe(classes.get("'literal_string'"));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -297,7 +297,7 @@ describe("QueryEditor completion Tab keymap", () => {
|
||||
});
|
||||
|
||||
it("keeps ASCII single-character filtering while allowing Han pinyin matches", () => {
|
||||
const source = [extractFunction("isBatchColumnSelectionAction"), extractFunction("completionItemsForBypassedFilter"), extractFunction("shouldBypassCompletionFilter"), extractFunction("buildCompletionResult")].join("\n");
|
||||
const source = [extractFunction("isBatchColumnSelectionAction"), extractFunction("itemHasHanText"), extractFunction("completionItemsForBypassedFilter"), extractFunction("shouldBypassCompletionFilter"), extractFunction("buildCompletionResult")].join("\n");
|
||||
const javascript = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.None, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
@@ -316,7 +316,7 @@ describe("QueryEditor completion Tab keymap", () => {
|
||||
});
|
||||
|
||||
it("bypasses CodeMirror filtering for Han substring matches", () => {
|
||||
const javascript = ts.transpileModule(extractFunction("shouldBypassCompletionFilter"), {
|
||||
const javascript = ts.transpileModule([extractFunction("itemHasHanText"), extractFunction("shouldBypassCompletionFilter")].join("\n"), {
|
||||
compilerOptions: { module: ts.ModuleKind.None, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
const shouldBypassCompletionFilter = new Function(`${javascript}\nreturn shouldBypassCompletionFilter;`)() as (prefix: string, items: Array<{ label: string }>) => boolean;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { handleQueryEditorCompletionNavigation, queryEditorCompletionNavigationDirection } from "@/lib/editor/queryEditorCompletionNavigation";
|
||||
|
||||
function event(key: string, modifiers: Partial<KeyboardEventInit> = {}): KeyboardEvent {
|
||||
return new KeyboardEvent("keydown", { key, ctrlKey: true, cancelable: true, ...modifiers });
|
||||
}
|
||||
|
||||
describe("queryEditorCompletionNavigation", () => {
|
||||
it("maps only plain Ctrl-N and Ctrl-P to completion navigation", () => {
|
||||
expect(queryEditorCompletionNavigationDirection(event("n"))).toBe(true);
|
||||
expect(queryEditorCompletionNavigationDirection(event("P"))).toBe(false);
|
||||
expect(queryEditorCompletionNavigationDirection(event("n", { ctrlKey: false }))).toBeNull();
|
||||
expect(queryEditorCompletionNavigationDirection(event("n", { metaKey: true }))).toBeNull();
|
||||
expect(queryEditorCompletionNavigationDirection(event("n", { altKey: true }))).toBeNull();
|
||||
expect(queryEditorCompletionNavigationDirection(event("n", { shiftKey: true }))).toBeNull();
|
||||
expect(queryEditorCompletionNavigationDirection(event("x"))).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores keys while an IME composition is active", () => {
|
||||
expect(queryEditorCompletionNavigationDirection({ key: "n", ctrlKey: true, metaKey: false, altKey: false, shiftKey: false, isComposing: true })).toBeNull();
|
||||
expect(queryEditorCompletionNavigationDirection({ key: "p", ctrlKey: true, metaKey: false, altKey: false, shiftKey: false, isComposing: true })).toBeNull();
|
||||
});
|
||||
|
||||
it("moves the active completion and consumes the key only when CodeMirror handled it", () => {
|
||||
const view = {} as EditorView;
|
||||
const next = vi.fn(() => true);
|
||||
const previous = vi.fn(() => false);
|
||||
const moveCompletionSelection = vi.fn((forward: boolean) => (forward ? next : previous));
|
||||
|
||||
const nextEvent = event("n");
|
||||
expect(handleQueryEditorCompletionNavigation(view, nextEvent, moveCompletionSelection)).toBe(true);
|
||||
expect(moveCompletionSelection).toHaveBeenCalledWith(true);
|
||||
expect(next).toHaveBeenCalledWith(view);
|
||||
expect(nextEvent.defaultPrevented).toBe(true);
|
||||
|
||||
const previousEvent = event("p");
|
||||
expect(handleQueryEditorCompletionNavigation(view, previousEvent, moveCompletionSelection)).toBe(false);
|
||||
expect(moveCompletionSelection).toHaveBeenCalledWith(false);
|
||||
expect(previous).toHaveBeenCalledWith(view);
|
||||
expect(previousEvent.defaultPrevented).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView, keymap, runScopeHandlers } from "@codemirror/view";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createQueryEditorExecutionShortcutBindings } from "../../editor/queryEditorExecutionShortcut";
|
||||
import { createQueryEditorExecutionShortcutBindings, createQueryEditorExecutionShortcutHandler } from "../../editor/queryEditorExecutionShortcut";
|
||||
|
||||
function createView(bindings: ReturnType<typeof createQueryEditorExecutionShortcutBindings>[]): EditorView {
|
||||
return new EditorView({
|
||||
@@ -105,4 +105,78 @@ describe("QueryEditor execution shortcuts", () => {
|
||||
expect(executeInNewResultTab).toHaveBeenCalledOnce();
|
||||
view.destroy();
|
||||
});
|
||||
|
||||
describe("createQueryEditorExecutionShortcutHandler", () => {
|
||||
const shortcuts = {
|
||||
executeSql: "Mod+Enter",
|
||||
executeSqlInNewResultTab: "Mod+\\",
|
||||
explainSql: "Mod+E",
|
||||
};
|
||||
|
||||
it("passes through non-repeated events when not executing", () => {
|
||||
const handler = createQueryEditorExecutionShortcutHandler({
|
||||
getShortcuts: () => shortcuts,
|
||||
isExecuting: () => false,
|
||||
});
|
||||
|
||||
const event = executeEvent("Enter", { repeat: false });
|
||||
const preventDefault = vi.spyOn(event, "preventDefault");
|
||||
const stopPropagation = vi.spyOn(event, "stopPropagation");
|
||||
|
||||
expect(handler(event)).toBe(false);
|
||||
expect(preventDefault).not.toHaveBeenCalled();
|
||||
expect(stopPropagation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("suppresses execution shortcuts when event.repeat is true", () => {
|
||||
const handler = createQueryEditorExecutionShortcutHandler({
|
||||
getShortcuts: () => shortcuts,
|
||||
isExecuting: () => false,
|
||||
});
|
||||
|
||||
const event = executeEvent("Enter", { repeat: true });
|
||||
const preventDefault = vi.spyOn(event, "preventDefault");
|
||||
const stopPropagation = vi.spyOn(event, "stopPropagation");
|
||||
|
||||
expect(handler(event)).toBe(true);
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("suppresses execution shortcuts when isExecuting is true", () => {
|
||||
const handler = createQueryEditorExecutionShortcutHandler({
|
||||
getShortcuts: () => shortcuts,
|
||||
isExecuting: () => true,
|
||||
});
|
||||
|
||||
const event = executeEvent("Enter", { repeat: false });
|
||||
const preventDefault = vi.spyOn(event, "preventDefault");
|
||||
const stopPropagation = vi.spyOn(event, "stopPropagation");
|
||||
|
||||
expect(handler(event)).toBe(true);
|
||||
expect(preventDefault).toHaveBeenCalled();
|
||||
expect(stopPropagation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("suppresses explain shortcut when enabled and isExecuting is true", () => {
|
||||
const handler = createQueryEditorExecutionShortcutHandler({
|
||||
getShortcuts: () => shortcuts,
|
||||
isExecuting: () => true,
|
||||
enableExplainShortcut: () => true,
|
||||
});
|
||||
|
||||
const event = executeEvent("e", { repeat: false });
|
||||
expect(handler(event)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not suppress unrelated keys even if repeating", () => {
|
||||
const handler = createQueryEditorExecutionShortcutHandler({
|
||||
getShortcuts: () => shortcuts,
|
||||
isExecuting: () => true,
|
||||
});
|
||||
|
||||
const event = new KeyboardEvent("keydown", { key: "a", repeat: true });
|
||||
expect(handler(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildEditorFontThemeRules, buildSqlCompletionThemeRules, EDITOR_FONT_FAMILY_CSS_VAR, editorDiagnosticColors, editorThemeAppearanceFor, IDE_EDITOR_THEMES, loadEditorTheme, resolveCustomThemeBackgrounds, resolveEditorTheme, SQL_BUILTIN_HIGHLIGHT_TAG } from "@/lib/editor/editorThemes";
|
||||
import {
|
||||
buildEditorFontThemeRules,
|
||||
buildSqlCompletionThemeRules,
|
||||
EDITOR_FONT_FAMILY_CSS_VAR,
|
||||
editorDiagnosticColors,
|
||||
editorThemeAppearanceFor,
|
||||
IDE_EDITOR_THEMES,
|
||||
loadEditorTheme,
|
||||
resolveCustomThemeBackgrounds,
|
||||
resolveEditorTheme,
|
||||
SQL_BUILTIN_HIGHLIGHT_TAG,
|
||||
SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG,
|
||||
} from "@/lib/editor/editorThemes";
|
||||
import { DEFAULT_APP_CUSTOM_UI_COLORS, wcagContrastRatio, type AppThemePalette } from "@/lib/app/appTheme";
|
||||
import { DEFAULT_CUSTOM_THEME_COLORS, type CustomThemeColors, type EditorTheme } from "@/stores/settingsStore";
|
||||
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
|
||||
@@ -306,6 +318,43 @@ describe("SQL builtin highlight tag", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("SQL quoted identifier highlighting (#12122)", () => {
|
||||
it("highlights quoted identifiers with identifier/variable color, distinct from string literals", async () => {
|
||||
const { highlightTree, tags } = await import("@lezer/highlight");
|
||||
const { HighlightStyle } = await import("@codemirror/language");
|
||||
const style = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: "keyword" },
|
||||
{ tag: tags.string, color: "string" },
|
||||
{ tag: [tags.name, tags.variableName, SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG], color: "variable" },
|
||||
]);
|
||||
|
||||
const dialects = [
|
||||
{ name: "mysql", sql: "SELECT `col`, col, 'val' FROM `tbl`" },
|
||||
{ name: "postgres", sql: 'SELECT "col", col, \'val\' FROM "tbl"' },
|
||||
{ name: "sqlserver", sql: "SELECT [col], col, 'val' FROM [tbl]" },
|
||||
];
|
||||
|
||||
for (const { name, sql } of dialects) {
|
||||
const dialect = createDbxCodeMirrorSqlDialect(langSql, name as any, name as any);
|
||||
const tree = dialect.language.parser.parse(sql);
|
||||
const classesByToken = new Map<string, string>();
|
||||
highlightTree(tree, style, (from, to, cls) => classesByToken.set(sql.slice(from, to), cls));
|
||||
|
||||
const quotedCol = name === "mysql" ? "`col`" : name === "postgres" ? '"col"' : "[col]";
|
||||
const quotedTbl = name === "mysql" ? "`tbl`" : name === "postgres" ? '"tbl"' : "[tbl]";
|
||||
|
||||
expect(classesByToken.get(quotedCol), `${name} quoted col`).toBe(classesByToken.get("col"));
|
||||
expect(classesByToken.get(quotedCol), `${name} quoted col vs string`).not.toBe(classesByToken.get("'val'"));
|
||||
expect(classesByToken.get(quotedTbl), `${name} quoted tbl`).toBe(classesByToken.get("col"));
|
||||
}
|
||||
});
|
||||
|
||||
it("maps SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG to special(string) for theme compatibility", async () => {
|
||||
const { tags } = await import("@lezer/highlight");
|
||||
expect(SQL_QUOTED_IDENTIFIER_HIGHLIGHT_TAG).toBe(tags.special(tags.string));
|
||||
});
|
||||
});
|
||||
|
||||
describe("editor gutters", () => {
|
||||
it("does not let the gutter minimum height cycle against the scroller content height", () => {
|
||||
const rules = buildEditorFontThemeRules();
|
||||
|
||||
@@ -129,6 +129,36 @@ describe("sidebar object-group routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes table trigger groups using listTriggers instead of loading database objects", async () => {
|
||||
const listObjects = vi.fn<() => Promise<ObjectInfo[]>>().mockResolvedValue([]);
|
||||
const listTriggers = vi.fn<() => Promise<TriggerInfo[]>>().mockResolvedValue([
|
||||
{
|
||||
name: "trg_orders_audit",
|
||||
timing: "AFTER",
|
||||
event: "UPDATE",
|
||||
},
|
||||
]);
|
||||
const { connection, store } = await createStore({ listObjects, listTriggers });
|
||||
const tableTriggerGroup: TreeNode = {
|
||||
...objectGroup("group-triggers", `${connection.id}:app:app:orders:__triggers`),
|
||||
tableName: "orders",
|
||||
};
|
||||
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [tableTriggerGroup] }];
|
||||
const storedTriggerGroup = store.treeNodes[0].children![0];
|
||||
|
||||
await store.refreshTreeNode(storedTriggerGroup);
|
||||
|
||||
expect(listTriggers).toHaveBeenCalledWith(connection.id, "app", "app", "orders", undefined);
|
||||
expect(listObjects).not.toHaveBeenCalled();
|
||||
expect(storedTriggerGroup.children).toHaveLength(1);
|
||||
expect(storedTriggerGroup.children![0]).toMatchObject({
|
||||
label: "trg_orders_audit (AFTER UPDATE)",
|
||||
objectName: "trg_orders_audit",
|
||||
tableName: "orders",
|
||||
type: "trigger",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves SQL Server table trigger status metadata across refreshes", async () => {
|
||||
const listObjects = vi.fn<() => Promise<ObjectInfo[]>>().mockResolvedValue([]);
|
||||
const listTriggers = vi.fn<() => Promise<TriggerInfo[]>>();
|
||||
|
||||
@@ -1,28 +1,104 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildSqlSemanticDiagnostics } from "@/lib/sql/semantic/diagnostics";
|
||||
import type { SqlReferenceAnalysis } from "@/types/database";
|
||||
import { buildSqlSemanticDiagnostics, sqlTextSpanToOffsetRange } from "@/lib/sql/semantic/diagnostics";
|
||||
import type { SqlReferenceAnalysis, SqlTextSpan } from "@/types/database";
|
||||
|
||||
const span = (startColumn: number, endColumn: number) => ({
|
||||
const span = (startColumn: number, endColumn: number): SqlTextSpan => ({
|
||||
start_line: 1,
|
||||
start_column: startColumn,
|
||||
end_line: 1,
|
||||
end_column: endColumn,
|
||||
});
|
||||
|
||||
describe("sqlTextSpanToOffsetRange inclusive column semantics", () => {
|
||||
it("converts 1-based inclusive single-character and multi-character spans to exact 0-based slice ranges", () => {
|
||||
const sql = "SELECT id, name AS user_name FROM users;";
|
||||
|
||||
// Single character 'i' in "id" at line 1, column 8
|
||||
const singleChar = sqlTextSpanToOffsetRange(sql, { start_line: 1, start_column: 8, end_line: 1, end_column: 8 });
|
||||
expect(singleChar).toEqual({ from: 7, to: 8 });
|
||||
expect(sql.slice(singleChar!.from, singleChar!.to)).toBe("i");
|
||||
|
||||
// Two-character "id" from column 8 to 9 (inclusive)
|
||||
const idRange = sqlTextSpanToOffsetRange(sql, { start_line: 1, start_column: 8, end_line: 1, end_column: 9 });
|
||||
expect(idRange).toEqual({ from: 7, to: 9 });
|
||||
expect(sql.slice(idRange!.from, idRange!.to)).toBe("id");
|
||||
|
||||
// Nine-character "user_name"
|
||||
const aliasStart = sql.indexOf("user_name");
|
||||
const aliasSpan: SqlTextSpan = {
|
||||
start_line: 1,
|
||||
start_column: aliasStart + 1,
|
||||
end_line: 1,
|
||||
end_column: aliasStart + "user_name".length,
|
||||
};
|
||||
const aliasRange = sqlTextSpanToOffsetRange(sql, aliasSpan);
|
||||
expect(aliasRange).toEqual({ from: aliasStart, to: aliasStart + "user_name".length });
|
||||
expect(sql.slice(aliasRange!.from, aliasRange!.to)).toBe("user_name");
|
||||
});
|
||||
|
||||
it("does not overshoot into trailing delimiters such as commas or semicolons", () => {
|
||||
const sql = "SELECT USER_ID, col FROM users GROUP BY USER_ID;";
|
||||
const userIdCol = sql.indexOf("USER_ID");
|
||||
// "USER_ID" is 7 characters; 1-based columns are 8..=14
|
||||
const spanBeforeComma: SqlTextSpan = {
|
||||
start_line: 1,
|
||||
start_column: userIdCol + 1,
|
||||
end_line: 1,
|
||||
end_column: userIdCol + "USER_ID".length,
|
||||
};
|
||||
const rangeBeforeComma = sqlTextSpanToOffsetRange(sql, spanBeforeComma);
|
||||
expect(rangeBeforeComma).toEqual({ from: userIdCol, to: userIdCol + 7 });
|
||||
expect(sql.slice(rangeBeforeComma!.from, rangeBeforeComma!.to)).toBe("USER_ID");
|
||||
expect(sql[rangeBeforeComma!.to]).toBe(",");
|
||||
|
||||
const lastCol = sql.lastIndexOf("USER_ID");
|
||||
const spanBeforeSemi: SqlTextSpan = {
|
||||
start_line: 1,
|
||||
start_column: lastCol + 1,
|
||||
end_line: 1,
|
||||
end_column: lastCol + "USER_ID".length,
|
||||
};
|
||||
const rangeBeforeSemi = sqlTextSpanToOffsetRange(sql, spanBeforeSemi);
|
||||
expect(rangeBeforeSemi).toEqual({ from: lastCol, to: lastCol + 7 });
|
||||
expect(sql.slice(rangeBeforeSemi!.from, rangeBeforeSemi!.to)).toBe("USER_ID");
|
||||
expect(sql[rangeBeforeSemi!.to]).toBe(";");
|
||||
});
|
||||
|
||||
it("handles multi-line spans without overshooting the end line column", () => {
|
||||
const sql = "SELECT\n user_name\nFROM users;";
|
||||
const multiLineSpan: SqlTextSpan = {
|
||||
start_line: 2,
|
||||
start_column: 3,
|
||||
end_line: 2,
|
||||
end_column: 11,
|
||||
};
|
||||
const range = sqlTextSpanToOffsetRange(sql, multiLineSpan);
|
||||
expect(range).not.toBeNull();
|
||||
expect(sql.slice(range!.from, range!.to)).toBe("user_name");
|
||||
});
|
||||
|
||||
it("returns null for invalid or empty spans", () => {
|
||||
const sql = "SELECT 1;";
|
||||
expect(sqlTextSpanToOffsetRange(sql, { start_line: 0, start_column: 0, end_line: 0, end_column: 0 })).toBeNull();
|
||||
expect(sqlTextSpanToOffsetRange(sql, { start_line: 1, start_column: 10, end_line: 1, end_column: 5 })).toBeNull();
|
||||
expect(sqlTextSpanToOffsetRange(sql, { start_line: 5, start_column: 1, end_line: 5, end_column: 2 })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSqlSemanticDiagnostics GROUP BY violations", () => {
|
||||
it("maps analyzer GROUP BY violations to error diagnostics", () => {
|
||||
const sql = "SELECT USER_ID, SUM(MONEY) FROM users GROUP BY USER_NAME";
|
||||
const analysis: SqlReferenceAnalysis = {
|
||||
tables: [],
|
||||
columns: [],
|
||||
group_by_violations: [{ span: span(8, 15), column: "USER_ID", qualifier: null }],
|
||||
group_by_violations: [{ span: span(8, 14), column: "USER_ID", qualifier: null }],
|
||||
};
|
||||
|
||||
const diagnostics = buildSqlSemanticDiagnostics(analysis, { tables: [], columnsByTable: new Map(), sql });
|
||||
|
||||
expect(diagnostics).toEqual([
|
||||
{
|
||||
span: span(8, 15),
|
||||
span: span(8, 14),
|
||||
message: "Column USER_ID must appear in the GROUP BY clause or be used in an aggregate function",
|
||||
severity: "error",
|
||||
},
|
||||
@@ -33,7 +109,7 @@ describe("buildSqlSemanticDiagnostics GROUP BY violations", () => {
|
||||
const analysis: SqlReferenceAnalysis = {
|
||||
tables: [],
|
||||
columns: [],
|
||||
group_by_violations: [{ span: span(8, 15), column: "name", qualifier: "u" }],
|
||||
group_by_violations: [{ span: span(8, 11), column: "name", qualifier: "u" }],
|
||||
};
|
||||
|
||||
const diagnostics = buildSqlSemanticDiagnostics(analysis, { tables: [], columnsByTable: new Map(), databaseType: "postgres" });
|
||||
@@ -46,7 +122,7 @@ describe("buildSqlSemanticDiagnostics GROUP BY violations", () => {
|
||||
const analysis: SqlReferenceAnalysis = {
|
||||
tables: [],
|
||||
columns: [],
|
||||
group_by_violations: [{ span: span(1, 5), column: "name", qualifier: "u" }],
|
||||
group_by_violations: [{ span: span(1, 4), column: "name", qualifier: "u" }],
|
||||
};
|
||||
|
||||
const diagnostics = buildSqlSemanticDiagnostics(analysis, { tables: [], columnsByTable: new Map() });
|
||||
@@ -109,8 +185,7 @@ describe("buildSqlSemanticDiagnostics projection aliases in GROUP BY / ORDER BY
|
||||
tables: [{ name: "ipu_quotation", span: span(sql.indexOf("ipu_quotation") + 1, sql.indexOf("ipu_quotation") + "ipu_quotation".length) }],
|
||||
columns: [
|
||||
{ name: "submitter_id", qualifier: null, span: span(sql.indexOf("submitter_id") + 1, sql.indexOf("submitter_id") + "submitter_id".length) },
|
||||
// span from reference parser points to token including 1-based bounds before ';'
|
||||
{ name: "repair_type", qualifier: null, span: span(aliasStart + 1, aliasEnd + 1) },
|
||||
{ name: "repair_type", qualifier: null, span: span(aliasStart + 1, aliasEnd) },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -134,8 +209,8 @@ describe("buildSqlSemanticDiagnostics projection aliases in GROUP BY / ORDER BY
|
||||
tables: [{ name: "users", span: span(sql.indexOf("users") + 1, sql.indexOf("users") + "users".length) }],
|
||||
columns: [
|
||||
{ name: "id", qualifier: null, span: span(sql.indexOf("id") + 1, sql.indexOf("id") + 2) },
|
||||
{ name: "name", qualifier: null, span: span(sql.indexOf("name") + 1, sql.indexOf("name") + 5) },
|
||||
{ name: "user_name", qualifier: null, span: span(aliasStart + 1, aliasEnd + 1) },
|
||||
{ name: "name", qualifier: null, span: span(sql.indexOf("name") + 1, sql.indexOf("name") + 4) },
|
||||
{ name: "user_name", qualifier: null, span: span(aliasStart + 1, aliasEnd) },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -159,8 +234,110 @@ describe("buildSqlSemanticDiagnostics projection aliases in GROUP BY / ORDER BY
|
||||
tables: [{ name: "users", span: span(sql.indexOf("users") + 1, sql.indexOf("users") + "users".length) }],
|
||||
columns: [
|
||||
{ name: "id", qualifier: null, span: span(sql.indexOf("id") + 1, sql.indexOf("id") + 2) },
|
||||
{ name: "name", qualifier: null, span: span(sql.indexOf("name") + 1, sql.indexOf("name") + 5) },
|
||||
{ name: "missing_col", qualifier: null, span: span(colStart + 1, colEnd + 1) },
|
||||
{ name: "name", qualifier: null, span: span(sql.indexOf("name") + 1, sql.indexOf("name") + 4) },
|
||||
{ name: "missing_col", qualifier: null, span: span(colStart + 1, colEnd) },
|
||||
],
|
||||
};
|
||||
|
||||
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
|
||||
tables: [{ name: "users" }],
|
||||
columnsByTable: new Map([["users", [{ name: "id" }, { name: "name" }]]]),
|
||||
loadedColumnTables: new Set(["users"]),
|
||||
sql,
|
||||
databaseType: "mysql",
|
||||
});
|
||||
|
||||
expect(diagnostics.map((d) => d.message)).toEqual(["Unknown column missing_col"]);
|
||||
});
|
||||
|
||||
it("accepts projection aliases in GROUP BY when another expression contains a string literal with a semicolon", () => {
|
||||
const sql = "SELECT CASE WHEN status = ';' THEN 'semi;' ELSE 'other' END AS status_alias, COUNT(*) AS total FROM orders GROUP BY CONCAT(category, ';'), status_alias;";
|
||||
const aliasStart = sql.indexOf(", status_alias;") + ", ".length;
|
||||
const aliasEnd = aliasStart + "status_alias".length;
|
||||
|
||||
const analysis: SqlReferenceAnalysis = {
|
||||
tables: [{ name: "orders", span: span(sql.indexOf("orders") + 1, sql.indexOf("orders") + "orders".length) }],
|
||||
columns: [
|
||||
{ name: "status", qualifier: null, span: span(sql.indexOf("status") + 1, sql.indexOf("status") + "status".length) },
|
||||
{ name: "category", qualifier: null, span: span(sql.indexOf("category") + 1, sql.indexOf("category") + "category".length) },
|
||||
{ name: "status_alias", qualifier: null, span: span(aliasStart + 1, aliasEnd) },
|
||||
],
|
||||
};
|
||||
|
||||
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
|
||||
tables: [{ name: "orders" }],
|
||||
columnsByTable: new Map([["orders", [{ name: "status" }, { name: "category" }]]]),
|
||||
loadedColumnTables: new Set(["orders"]),
|
||||
sql,
|
||||
databaseType: "mysql",
|
||||
});
|
||||
|
||||
expect(diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
it("accepts projection aliases in GROUP BY in the second statement of a multi-statement script", () => {
|
||||
const sql = "SELECT 1;\nSELECT id, name AS user_name FROM users GROUP BY user_name;";
|
||||
const secondLine = "SELECT id, name AS user_name FROM users GROUP BY user_name;";
|
||||
const aliasStartOnLine = secondLine.indexOf("GROUP BY user_name") + "GROUP BY ".length;
|
||||
const aliasEndOnLine = aliasStartOnLine + "user_name".length;
|
||||
|
||||
const analysis: SqlReferenceAnalysis = {
|
||||
tables: [{ name: "users", span: { start_line: 2, start_column: secondLine.indexOf("users") + 1, end_line: 2, end_column: secondLine.indexOf("users") + "users".length } }],
|
||||
columns: [
|
||||
{ name: "id", qualifier: null, span: { start_line: 2, start_column: secondLine.indexOf("id") + 1, end_line: 2, end_column: secondLine.indexOf("id") + 2 } },
|
||||
{ name: "name", qualifier: null, span: { start_line: 2, start_column: secondLine.indexOf("name") + 1, end_line: 2, end_column: secondLine.indexOf("name") + 4 } },
|
||||
{ name: "user_name", qualifier: null, span: { start_line: 2, start_column: aliasStartOnLine + 1, end_line: 2, end_column: aliasEndOnLine } },
|
||||
],
|
||||
};
|
||||
|
||||
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
|
||||
tables: [{ name: "users" }],
|
||||
columnsByTable: new Map([["users", [{ name: "id" }, { name: "name" }]]]),
|
||||
loadedColumnTables: new Set(["users"]),
|
||||
sql,
|
||||
databaseType: "mysql",
|
||||
});
|
||||
|
||||
expect(diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
it("accepts projection aliases in GROUP BY in the first statement followed by another statement", () => {
|
||||
const sql = "SELECT id, name AS user_name FROM users GROUP BY user_name;\nSELECT 2;";
|
||||
const aliasStart = sql.indexOf("GROUP BY user_name") + "GROUP BY ".length;
|
||||
const aliasEnd = aliasStart + "user_name".length;
|
||||
|
||||
const analysis: SqlReferenceAnalysis = {
|
||||
tables: [{ name: "users", span: span(sql.indexOf("users") + 1, sql.indexOf("users") + "users".length) }],
|
||||
columns: [
|
||||
{ name: "id", qualifier: null, span: span(sql.indexOf("id") + 1, sql.indexOf("id") + 2) },
|
||||
{ name: "name", qualifier: null, span: span(sql.indexOf("name") + 1, sql.indexOf("name") + 4) },
|
||||
{ name: "user_name", qualifier: null, span: span(aliasStart + 1, aliasEnd) },
|
||||
],
|
||||
};
|
||||
|
||||
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
|
||||
tables: [{ name: "users" }],
|
||||
columnsByTable: new Map([["users", [{ name: "id" }, { name: "name" }]]]),
|
||||
loadedColumnTables: new Set(["users"]),
|
||||
sql,
|
||||
databaseType: "mysql",
|
||||
});
|
||||
|
||||
expect(diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
it("still reports genuinely unknown columns in multi-statement scripts", () => {
|
||||
const sql = "SELECT 1;\nSELECT id, name AS user_name FROM users GROUP BY missing_col;";
|
||||
const secondLine = "SELECT id, name AS user_name FROM users GROUP BY missing_col;";
|
||||
const colStartOnLine = secondLine.indexOf("missing_col");
|
||||
const colEndOnLine = colStartOnLine + "missing_col".length;
|
||||
|
||||
const analysis: SqlReferenceAnalysis = {
|
||||
tables: [{ name: "users", span: { start_line: 2, start_column: secondLine.indexOf("users") + 1, end_line: 2, end_column: secondLine.indexOf("users") + "users".length } }],
|
||||
columns: [
|
||||
{ name: "id", qualifier: null, span: { start_line: 2, start_column: secondLine.indexOf("id") + 1, end_line: 2, end_column: secondLine.indexOf("id") + 2 } },
|
||||
{ name: "name", qualifier: null, span: { start_line: 2, start_column: secondLine.indexOf("name") + 1, end_line: 2, end_column: secondLine.indexOf("name") + 4 } },
|
||||
{ name: "missing_col", qualifier: null, span: { start_line: 2, start_column: colStartOnLine + 1, end_line: 2, end_column: colEndOnLine } },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -177,3 +177,36 @@ describe("sqlCompletion identifier column matching", () => {
|
||||
expect(items.some((item) => item.label === "created_at")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sqlCompletion comment matching", () => {
|
||||
it("matches tables by Chinese comment and pinyin", () => {
|
||||
const items = buildSqlCompletionItems("SELECT * FROM yh", "SELECT * FROM yh".length, {
|
||||
databaseType: "mysql",
|
||||
tables: [
|
||||
{ name: "t_user", type: "table", comment: "用户表" },
|
||||
{ name: "t_order", type: "table", comment: "订单表" },
|
||||
],
|
||||
columnsByTable: new Map(),
|
||||
});
|
||||
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "t_user", type: "table" })]));
|
||||
expect(items.some((item) => item.label === "t_order")).toBe(false);
|
||||
});
|
||||
|
||||
it("matches columns by Chinese comment and pinyin", () => {
|
||||
const items = buildSqlCompletionItems("SELECT * FROM t_user WHERE cjsj", "SELECT * FROM t_user WHERE cjsj".length, {
|
||||
databaseType: "mysql",
|
||||
tables: [{ name: "t_user", type: "table" }],
|
||||
columnsByTable: new Map([
|
||||
[
|
||||
"t_user",
|
||||
[
|
||||
{ name: "create_time", table: "t_user", comment: "创建时间" },
|
||||
{ name: "update_time", table: "t_user", comment: "更新时间" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
});
|
||||
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "create_time", type: "column" })]));
|
||||
expect(items.some((item) => item.label === "update_time")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -840,3 +840,48 @@ describe("classic layout style (v0.6.15 reproduction)", () => {
|
||||
expect(await formatSqlText("select * from t", "mysql", { ...classic, fromClauseLayout: "sameLine" })).toBe(lines("SELECT", " *", "FROM t"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("PostgreSQL MERGE formatting (#12592)", () => {
|
||||
// sql-formatter 15's postgresql grammar predates PG15 MERGE, so the DBX
|
||||
// layout printer used to emit one token per line for these statements.
|
||||
const merge = "MERGE INTO target t USING source s ON t.id = s.id WHEN MATCHED THEN UPDATE SET name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);";
|
||||
|
||||
it("lays out the UPDATE and INSERT actions as clauses", async () => {
|
||||
const formatted = await formatSqlText(merge, "postgres");
|
||||
|
||||
expect(formatted).toBe(["MERGE INTO target t USING source s ON t.id = s.id", "WHEN MATCHED THEN", "UPDATE SET name = s.name", "WHEN NOT MATCHED THEN", "INSERT (id, name)", "VALUES (s.id, s.name);"].join("\n"));
|
||||
// Formatting is idempotent: the clause keywords stay clause boundaries.
|
||||
await expect(formatSqlText(formatted, "postgres")).resolves.toBe(formatted);
|
||||
});
|
||||
|
||||
it("keeps conditions, DELETE actions and RETURNING intact", async () => {
|
||||
const sql = "MERGE INTO products p USING new_products np ON p.product_id = np.product_id WHEN MATCHED AND np.price > p.price THEN UPDATE SET price = np.price WHEN NOT MATCHED THEN INSERT (product_id, product_name, price) VALUES (np.product_id, np.product_name, np.price) RETURNING p.*;";
|
||||
|
||||
await expect(formatSqlText(sql, "postgres")).resolves.toBe(
|
||||
[
|
||||
"MERGE INTO products p USING new_products np ON p.product_id = np.product_id",
|
||||
"WHEN MATCHED AND np.price > p.price THEN",
|
||||
"UPDATE SET price = np.price",
|
||||
"WHEN NOT MATCHED THEN",
|
||||
"INSERT (product_id, product_name, price)",
|
||||
"VALUES (np.product_id, np.product_name, np.price)",
|
||||
"RETURNING p.*;",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const deleteAction = "MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN DELETE WHEN NOT MATCHED THEN INSERT (id) VALUES (s.id);";
|
||||
await expect(formatSqlText(deleteAction, "postgres")).resolves.toBe(["MERGE INTO t USING s ON t.id = s.id", "WHEN MATCHED THEN DELETE", "WHEN NOT MATCHED THEN", "INSERT (id)", "VALUES (s.id);"].join("\n"));
|
||||
});
|
||||
|
||||
it("does not change regular INSERT, UPDATE or JOIN formatting", async () => {
|
||||
await expect(formatSqlText("insert into t (a, b) values (1, 2) on conflict (a) do update set b = excluded.b;", "postgres")).resolves.toBe("INSERT INTO t (a, b)\nVALUES (1, 2)\nON CONFLICT (a) DO UPDATE\nSET b = excluded.b;");
|
||||
await expect(formatSqlText("update t set a = 1, b = 2 where id = 3;", "postgres")).resolves.toBe("UPDATE t SET a = 1, b = 2 WHERE id = 3;");
|
||||
await expect(formatSqlText("select a.id, b.name from a join b on a.id = b.id where a.x = 1;", "postgres")).resolves.toBe("SELECT a.id,\n b.name\nFROM a\n JOIN b ON a.id = b.id\nWHERE a.x = 1;");
|
||||
});
|
||||
|
||||
it("applies to PostgreSQL-compatible connections too", async () => {
|
||||
// GaussDB / Kingbase / HighGo / openGauss reuse the postgres grammar.
|
||||
expect(sqlFormatDialectForDbType("gaussdb")).toBe("postgres");
|
||||
await expect(formatSqlText(merge, sqlFormatDialectForDbType("kingbase"))).resolves.toContain("WHEN MATCHED THEN\nUPDATE SET name = s.name");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,4 +72,85 @@ describe("batch table drop", () => {
|
||||
expect(result.succeeded).toEqual([]);
|
||||
expect(result.failed?.message).toBe("Batch drop did not report a result for every statement");
|
||||
});
|
||||
|
||||
it("succeeds for single table drop when backend returns an indexless result", async () => {
|
||||
const singlePlan = [{ target: "platforms", sql: "DROP TABLE `platforms`" }];
|
||||
const onProgress = vi.fn();
|
||||
const result = await runBatchTableDrop({
|
||||
databaseType: "mysql",
|
||||
plan: singlePlan,
|
||||
executeStatement: vi.fn(),
|
||||
executeBatch: async () => [queryResult()],
|
||||
onProgress,
|
||||
});
|
||||
|
||||
expect(result.succeeded).toEqual(["platforms"]);
|
||||
expect(result.failed).toBeUndefined();
|
||||
expect(onProgress).toHaveBeenCalledWith({ completed: 1, total: 1, success: true });
|
||||
});
|
||||
|
||||
it("reports failure for single table drop when execution error occurs", async () => {
|
||||
const singlePlan = [{ target: "platforms", sql: "DROP TABLE `platforms`" }];
|
||||
const onProgress = vi.fn();
|
||||
const result = await runBatchTableDrop({
|
||||
databaseType: "oceanbase-oracle",
|
||||
plan: singlePlan,
|
||||
executeStatement: vi.fn(),
|
||||
executeBatch: async () => [
|
||||
queryResult({
|
||||
execution_error: true,
|
||||
error: {
|
||||
version: 1,
|
||||
code: "OBE-00942",
|
||||
messageKey: "error.database",
|
||||
messageParams: {},
|
||||
source: "agent",
|
||||
operationOutcome: "unknown",
|
||||
detail: "table or view does not exist",
|
||||
},
|
||||
}),
|
||||
],
|
||||
onProgress,
|
||||
});
|
||||
|
||||
expect(result.succeeded).toEqual([]);
|
||||
expect(result.failed?.message).toBe("table or view does not exist");
|
||||
});
|
||||
|
||||
it("maps 1-to-1 sequential indexless batch results when lengths match", async () => {
|
||||
const twoPlan = [
|
||||
{ target: "orders", sql: "DROP TABLE orders" },
|
||||
{ target: "customers", sql: "DROP TABLE customers" },
|
||||
];
|
||||
const onProgress = vi.fn();
|
||||
const result = await runBatchTableDrop({
|
||||
databaseType: "mysql",
|
||||
plan: twoPlan,
|
||||
executeStatement: vi.fn(),
|
||||
executeBatch: async () => [queryResult(), queryResult({ execution_error: true, rows: [["foreign key constraint fails"]] })],
|
||||
onProgress,
|
||||
});
|
||||
|
||||
expect(result.succeeded).toEqual(["orders"]);
|
||||
expect(result.failed?.message).toBe("foreign key constraint fails");
|
||||
});
|
||||
|
||||
it("handles empty drop plan immediately without executing queries", async () => {
|
||||
const executeStatement = vi.fn();
|
||||
const executeBatch = vi.fn();
|
||||
const onProgress = vi.fn();
|
||||
const result = await runBatchTableDrop({
|
||||
databaseType: "mysql",
|
||||
plan: [],
|
||||
executeStatement,
|
||||
executeBatch,
|
||||
onProgress,
|
||||
});
|
||||
|
||||
expect(result.succeeded).toEqual([]);
|
||||
expect(result.failed).toBeUndefined();
|
||||
expect(executeStatement).not.toHaveBeenCalled();
|
||||
expect(executeBatch).not.toHaveBeenCalled();
|
||||
expect(onProgress).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
buildTableImportParseOptions,
|
||||
defaultTableImportEmptyStringAsNull,
|
||||
formatTableImportElapsed,
|
||||
formatTableImportPreviewCell,
|
||||
importDataTypeForDatabase,
|
||||
nextTableImportWizardStep,
|
||||
previousTableImportWizardStep,
|
||||
@@ -70,6 +71,12 @@ describe("tableImport", () => {
|
||||
expect(formatTableImportElapsed(61_000)).toBe("1m 1s");
|
||||
});
|
||||
|
||||
it("shows SQL current timestamp markers without exposing internal JSON", () => {
|
||||
expect(formatTableImportPreviewCell({ $dbxSqlImportExpression: "currentTimestamp" })).toBe("CURRENT_TIMESTAMP");
|
||||
expect(formatTableImportPreviewCell({ $dbxSqlImportExpression: "other" })).toBe('{"$dbxSqlImportExpression":"other"}');
|
||||
expect(formatTableImportPreviewCell({ value: 1 })).toBe('{"value":1}');
|
||||
});
|
||||
|
||||
it("uses the backend elapsed time as the terminal source of truth", () => {
|
||||
expect(resolveTableImportElapsed(1_500, 1_000, false)).toBe(1_500);
|
||||
expect(resolveTableImportElapsed(1_500, 2_000, false)).toBe(2_000);
|
||||
|
||||