dsh-db-tool 1.5.4 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/client/client.js +83 -27
  2. package/package.json +3 -2
package/client/client.js CHANGED
@@ -107,7 +107,6 @@ window.__ModuleLoader__.load({
107
107
  truncatedNoEdit: "值已截断显示,就地编辑可能覆盖数据,请改用 SQL 控制台",
108
108
  blobNoEdit: "二进制值不支持就地编辑",
109
109
  redisTypeRo: "Redis 该类型不支持就地编辑",
110
- cellSaved: "已保存",
111
110
  // SQL 控制台
112
111
  modeQuery: "只读查询",
113
112
  modeExecute: "写入执行",
@@ -217,7 +216,6 @@ window.__ModuleLoader__.load({
217
216
  truncatedNoEdit: "Value shown truncated; edit via SQL Console instead",
218
217
  blobNoEdit: "Binary values cannot be edited here",
219
218
  redisTypeRo: "This Redis type cannot be edited here",
220
- cellSaved: "Saved",
221
219
  modeQuery: "query (read-only)",
222
220
  modeExecute: "execute (write)",
223
221
  modeScript: "script",
@@ -352,7 +350,7 @@ window.__ModuleLoader__.load({
352
350
  ".dbt-btn.danger:hover{background:rgba(255,69,58,.12);}",
353
351
  ".dbt-btn:disabled{opacity:.4;cursor:default;transform:none;}",
354
352
  "/* ===== 文本反馈 ===== */",
355
- ".dbt-muted{color:var(--dbt-text-secondary);font-size:11px;}",
353
+ ".dbt-muted,.dbt-readhint{color:var(--dbt-text-secondary);font-size:11px;}",
356
354
  ".dbt-err{color:var(--dbt-danger);font-size:12px;white-space:pre-wrap;}",
357
355
  ".dbt-msg{color:var(--dbt-success);font-size:12px;white-space:pre-wrap;}",
358
356
  "/* ===== 行 hairline 表格 + 毛玻璃 sticky 表头 ===== */",
@@ -417,7 +415,6 @@ window.__ModuleLoader__.load({
417
415
  ".dbt-celledit textarea{font-family:var(--dbt-mono);font-size:13px;min-height:96px;resize:vertical;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:8px;padding:8px;}",
418
416
  ".dbt-celledit textarea:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
419
417
  ".dbt-nullchip{display:inline-flex;align-items:center;border:1px solid var(--dbt-warning);color:var(--dbt-warning);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600;}",
420
- ".dbt-readhint{color:var(--dbt-text-secondary);font-size:11px;}",
421
418
  "/* ===== 视图切换入场(仅 transform/opacity;reduced-motion 全局已禁) ===== */",
422
419
  "@keyframes dbt-in{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}",
423
420
  ".dbt-view{display:flex;flex-direction:column;gap:10px;animation:dbt-in .22s var(--dbt-ease);}",
@@ -427,7 +424,7 @@ window.__ModuleLoader__.load({
427
424
  ".dbt-seg button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
428
425
  ".dbt-browse-root .dbt-seg button{font-size:12px;padding:4px 12px;}",
429
426
  "/* ===== 降级:透明度减弱 / 动效减弱 ===== */",
430
- "@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog,.dbt-browse-dialog,.dbt-cellpop{backdrop-filter:none;-webkit-backdrop-filter:none;}.dbt-dialog,.dbt-browse-dialog{background:#2c2c2e;}.dbt-cellpop{background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}.dbt-browse-root .dbt-table th{background:#1e1e20;}}",
427
+ "@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog,.dbt-browse-dialog,.dbt-cellpop{backdrop-filter:none;-webkit-backdrop-filter:none;background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}.dbt-browse-root .dbt-table th{background:#1e1e20;}}",
431
428
  "@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog,.dbt-browse-dialog,.dbt-cellpop{background:#f5f5f7;}.dbt-table th,.dbt-browse-root .dbt-table th{background:#f2f2f7;}}",
432
429
  "@media (prefers-reduced-motion: reduce){.dbt-panel *,.dbt-browse-root *{transition:none!important;animation:none!important;}.dbt-btn:active{transform:none;}}",
433
430
  ].join("\n");
@@ -454,7 +451,8 @@ window.__ModuleLoader__.load({
454
451
  React.createElement("div", { className: "dbt-muted" }, t("confirmHint")),
455
452
  React.createElement(
456
453
  "div",
457
- { className: "dbt-row", style: { justifyContent: "flex-end" } },
454
+ // 末对齐由样式层 .dbt-dialog .dbt-row 规则承担,无需内联 style
455
+ { className: "dbt-row" },
458
456
  React.createElement("button", { className: "dbt-btn danger", onClick: () => done(true) }, t("confirmRun")),
459
457
  React.createElement("button", { className: "dbt-btn", onClick: () => done(false) }, t("confirmCancel")),
460
458
  ),
@@ -497,15 +495,43 @@ window.__ModuleLoader__.load({
497
495
  }
498
496
  // PiP 独立窗口渲染时禁 portal:PiP root 本身就挂在 pipDoc.body 上,再 portal 回主 document 会把 DOM 拉出独立窗口
499
497
  let portalDisabled = false;
500
- // createRoot 多级探测(PiP 独立窗口需要独立 React root):react-dom → react-dom/client
498
+ // createRoot 三重解析(PiP 独立窗口需要独立 React root,结构对齐上方 portal 解析):
499
+ // ①模块加载时同步 require react-dom → react-dom/client(宿主按 specifier 解析,两者是不同键)
500
+ // ②宿主模块系统异步 import("react-dom/client") ③均不可用则 PiP 超时降级 DOM 浮窗
501
501
  let reactCreateRoot = null;
502
+ let rootTried = false; // 异步 import 只发起一次
503
+ let rootPending = null; // 订阅解析成功的回调列表
504
+ let rootSource = null; // 就绪方式:sync / async / null(未就绪,诊断日志用)
502
505
  try {
503
506
  var rd2 = require("react-dom");
504
- reactCreateRoot = rd2 && typeof rd2.createRoot === "function" ? rd2.createRoot.bind(rd2) : null;
505
- if (!reactCreateRoot) {
506
- try { var rdc = require("react-dom/client"); reactCreateRoot = rdc && typeof rdc.createRoot === "function" ? rdc.createRoot.bind(rdc) : null; } catch (e2) { /* 保持 null */ }
507
+ if (rd2 && typeof rd2.createRoot === "function") {
508
+ reactCreateRoot = rd2.createRoot.bind(rd2); rootTried = true; rootSource = "sync";
509
+ } else {
510
+ try { var rdc = require("react-dom/client"); if (rdc && typeof rdc.createRoot === "function") { reactCreateRoot = rdc.createRoot.bind(rdc); rootTried = true; rootSource = "sync"; } } catch (e2) { /* 保持 null */ }
511
+ }
512
+ } catch (e) { /* 保持 null,等异步解析 */ }
513
+ function resolveRootAsync(ctx) {
514
+ if (rootTried) return;
515
+ rootTried = true;
516
+ if (reactCreateRoot) return;
517
+ var mods = ctx && ctx.modules;
518
+ var p = mods && typeof mods.import === "function" ? mods.import("react-dom/client") : null;
519
+ if (p && typeof p.then === "function") {
520
+ p.then(function (m) {
521
+ if (m && typeof m.createRoot === "function") {
522
+ reactCreateRoot = m.createRoot.bind(m);
523
+ rootSource = "async";
524
+ var waiters = rootPending || []; rootPending = null;
525
+ for (var i = 0; i < waiters.length; i++) waiters[i]();
526
+ }
527
+ }).catch(function () { /* 解析失败,PiP 走超时降级 */ });
507
528
  }
508
- } catch (e) { /* 保持 null,PiP 不可用走 DOM 浮窗降级 */ }
529
+ }
530
+ function onRootReady(cb) {
531
+ if (reactCreateRoot) { cb(); return; }
532
+ (rootPending = rootPending || []).push(cb);
533
+ }
534
+ let pipResult = "not-tried"; // 最近一次 PiP 尝试结果:opened / rejected:<err.name>(诊断日志用)
509
535
 
510
536
  /* ---------------- 连接管理 ---------------- */
511
537
  const EMPTY_FORM = { id: "", kind: "mysql", name: "", mode: "url", url: "", urlUser: "", urlPassword: "", host: "", port: "", user: "", password: "", database: "", ssl: false };
@@ -1345,17 +1371,20 @@ window.__ModuleLoader__.load({
1345
1371
 
1346
1372
  function BrowseDialog(props) {
1347
1373
  const { conns, projectPath, grants, askConfirm, onClose, ctx } = props;
1348
- // grants / askConfirm:预留给后续单元格编辑的危险操作确认通道,本任务先接住不使用
1374
+ // grants(editable 判定)与 askConfirm(单元格写回确认)下传 PreviewGrid
1349
1375
  // portal 异步解析接入:portalReady 仅用于触发重渲染(mountDialog 内部读 portalImpl)。
1350
1376
  // createPortal 不换组件实例——portalReady 切换前后是同一 DOM 节点,pos state 保持,居中定位不受 portal 重挂影响。
1351
1377
  const [portalReady, setPortalReady] = React.useState(!!portalImpl);
1352
1378
  React.useEffect(() => {
1353
1379
  resolvePortalAsync(ctx);
1380
+ resolveRootAsync(ctx);
1354
1381
  onPortalReady(function () { setPortalReady(true); });
1355
- // 诊断:一次性输出(帮助真机排查 iframe 隔离与 portal 解析结果)
1382
+ // 诊断:一次性输出(帮助真机排查 iframe 隔离与 portal/createRoot 解析结果、PiP 尝试结果)
1356
1383
  if (!window.__dbtPortalDiag) {
1357
1384
  window.__dbtPortalDiag = true;
1358
- console.info("[dbt] portal:", portalImpl ? "sync/async ok" : "unavailable", "sameDoc:", window.top === window.self);
1385
+ console.info("[dbt] portal:", portalImpl ? "sync/async ok" : "unavailable",
1386
+ "createRoot:", rootSource || "none", "pip:", pipResult,
1387
+ "sameDoc:", window.top === window.self);
1359
1388
  }
1360
1389
  }, []);
1361
1390
  const [sel, setSel] = React.useState(null); // 弹窗内当前选中(null=未选表,右栏显示引导空态)
@@ -1577,12 +1606,16 @@ window.__ModuleLoader__.load({
1577
1606
  confirmReq ? React.createElement(DangerDialog, { challenge: confirmReq, onClose: () => setConfirmReq(null) }) : null,
1578
1607
  );
1579
1608
  }
1580
- function openBrowsePip(ctx, deps) { // deps: {conns, projectPath, grants, onClosed} → 返回 pipWindow | null
1609
+ function openBrowsePip(ctx, deps) { // deps: {conns, projectPath, grants, onClosed, onFallback} → 返回 pipWindow | null
1581
1610
  try {
1582
1611
  const dpip = window.documentPictureInPicture;
1583
- if (!dpip || typeof dpip.requestWindow !== "function" || !reactCreateRoot || !portalImpl) return null;
1612
+ // 入口只硬性要求 dpip;createRoot/portal 未就绪不再放弃——手势内先开窗,root 异步就绪后渲染,超时由调用方降级
1613
+ if (!dpip || typeof dpip.requestWindow !== "function") return null;
1614
+ resolveRootAsync(ctx); // createRoot 同步未就绪时,趁 3 秒超时窗口发起宿主异步 import
1584
1615
  // PiP 只能在用户手势内同步发起 requestWindow,Promise then 里再做 DOM 装配
1585
- return dpip.requestWindow({ width: 1100, height: 720 }).then((w) => {
1616
+ const p = dpip.requestWindow({ width: 1100, height: 720 });
1617
+ pipResult = "opened"; // requestWindow 已发起;拒绝走下方 catch
1618
+ return p.then((w) => {
1586
1619
  const doc = w.document;
1587
1620
  // 相对地址基准:PiP 文档是 about:blank,fetch 的相对 API 路径须按主 origin 解析
1588
1621
  const base = doc.createElement("base");
@@ -1591,25 +1624,46 @@ window.__ModuleLoader__.load({
1591
1624
  // 样式:整套 --dbt-* token 与规则注入 PiP 文档
1592
1625
  if (styleEl) doc.head.appendChild(doc.importNode(styleEl, true));
1593
1626
  doc.body.style.margin = "0";
1594
- portalDisabled = true; // PiP root 已在 pipDoc.body 上,禁 portal 防 DOM 被拉回主窗口
1595
- const root = reactCreateRoot(doc.body);
1627
+ // 加载占位:createRoot 异步解析期间给用户可见反馈(PiP 场景少用,不做 i18n key)
1628
+ const ph = doc.createElement("div");
1629
+ ph.style.cssText = "display:flex;align-items:center;justify-content:center;height:100%;font:14px sans-serif;color:#888;";
1630
+ ph.textContent = "正在打开数据浏览…";
1631
+ doc.body.appendChild(ph);
1632
+ let root = null;
1633
+ let settled = false; // root 已渲染或窗口已关闭:晚到的 onRootReady 回调与超时兜底均不再处理
1596
1634
  const closePip = () => {
1597
- try { root.unmount(); } catch (e) { /* 已卸载 */ }
1635
+ if (root) { try { root.unmount(); } catch (e) { /* 已卸载 */ } }
1598
1636
  portalDisabled = false;
1599
1637
  try { w.close(); } catch (e2) { /* 已关闭 */ }
1600
1638
  };
1601
- root.render(React.createElement(PipBrowseApp, {
1602
- ctx, conns: deps.conns, projectPath: deps.projectPath, grants: deps.grants,
1603
- onClose: () => { closePip(); deps.onClosed(); },
1604
- }));
1639
+ onRootReady(() => {
1640
+ if (settled) return;
1641
+ settled = true;
1642
+ portalDisabled = true; // PiP root 已在 pipDoc.body 上,禁 portal 防 DOM 被拉回主窗口
1643
+ root = reactCreateRoot(doc.body);
1644
+ root.render(React.createElement(PipBrowseApp, {
1645
+ ctx, conns: deps.conns, projectPath: deps.projectPath, grants: deps.grants,
1646
+ onClose: () => { closePip(); deps.onClosed(); },
1647
+ }));
1648
+ ph.remove();
1649
+ });
1605
1650
  w.addEventListener("pagehide", () => { // 用户点 PiP 窗口系统关闭钮
1651
+ settled = true;
1652
+ if (root) { try { root.unmount(); } catch (e) { /* 已卸载 */ } }
1606
1653
  portalDisabled = false;
1607
- try { root.unmount(); } catch (e) { /* 已卸载 */ }
1608
1654
  deps.onClosed();
1609
1655
  });
1656
+ // 超时兜底:3 秒内 createRoot 没就绪 → 关 PiP 清理并回调降级(调用方打开 DOM 浮窗)
1657
+ setTimeout(() => {
1658
+ if (settled) return;
1659
+ settled = true;
1660
+ closePip();
1661
+ deps.onFallback();
1662
+ }, 3000);
1610
1663
  return w;
1611
- }).catch(() => null);
1664
+ }).catch((err) => { pipResult = "rejected:" + (err && err.name || "Error"); return null; });
1612
1665
  } catch (e) {
1666
+ pipResult = "rejected:" + (e && e.name || "Error");
1613
1667
  return Promise.resolve(null);
1614
1668
  }
1615
1669
  }
@@ -1750,6 +1804,8 @@ window.__ModuleLoader__.load({
1750
1804
  const p = openBrowsePip(props.ctx, {
1751
1805
  conns, projectPath, grants,
1752
1806
  onClosed: () => setPipWin(null),
1807
+ // PiP 超时降级(createRoot 未就绪):清掉窗口句柄并回退 DOM 浮窗
1808
+ onFallback: () => { setPipWin(null); setBrowseOpen(true); },
1753
1809
  });
1754
1810
  if (p && typeof p.then === "function") {
1755
1811
  p.then((w) => { if (w) setPipWin(w); else setBrowseOpen(true); });
@@ -1914,7 +1970,7 @@ window.__ModuleLoader__.load({
1914
1970
  return ctx.betterSidebar.registerTab({
1915
1971
  id: TAB_ID,
1916
1972
  title: function () { return t("tabTitle"); },
1917
- icon: function (size) { return icon(size); },
1973
+ icon,
1918
1974
  order: 45,
1919
1975
  single: true,
1920
1976
  component: function (p) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.5.4",
3
+ "version": "1.5.5",
4
4
  "description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,7 +39,8 @@
39
39
  "client": {
40
40
  "inject": [
41
41
  "@deepseek-ai/dsh-client-locale",
42
- "react-dom"
42
+ "react-dom",
43
+ "react-dom/client"
43
44
  ],
44
45
  "platform": "web"
45
46
  }