dsh-db-tool 1.5.6 → 1.5.8

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 +76 -75
  2. package/package.json +1 -1
package/client/client.js CHANGED
@@ -93,8 +93,7 @@ window.__ModuleLoader__.load({
93
93
  emptyData: "暂无数据",
94
94
  retry: "重试",
95
95
  selectTableHint: "在左侧选择表后查看数据",
96
- // 单元格查看/编辑(Navicat 式就地写回)
97
- editCell: "编辑",
96
+ // 单元格底部详情栏(Navicat 式就地写回)
98
97
  cellValue: "完整值",
99
98
  copyBtn: "复制",
100
99
  copied: "已复制",
@@ -200,8 +199,7 @@ window.__ModuleLoader__.load({
200
199
  emptyData: "No rows",
201
200
  retry: "Retry",
202
201
  selectTableHint: "Select a table on the left to view data",
203
- // Cell view/edit (Navicat-style in-place write-back)
204
- editCell: "Edit",
202
+ // Cell bottom detail bar (Navicat-style in-place write-back)
205
203
  cellValue: "Full value",
206
204
  copyBtn: "Copy",
207
205
  copied: "Copied",
@@ -349,7 +347,7 @@ window.__ModuleLoader__.load({
349
347
  ".dbt-btn.danger:hover{background:rgba(255,69,58,.12);}",
350
348
  ".dbt-btn:disabled{opacity:.4;cursor:default;transform:none;}",
351
349
  "/* ===== 文本反馈 ===== */",
352
- ".dbt-panel strong{font-size:13px;font-weight:600;}",
350
+ ".dbt-panel strong,.dbt-browse-root strong{font-size:13px;font-weight:600;}",
353
351
  ".dbt-muted,.dbt-readhint{color:var(--dbt-text-secondary);font-size:11px;}",
354
352
  ".dbt-err{color:var(--dbt-danger);font-size:12px;white-space:pre-wrap;}",
355
353
  ".dbt-msg{color:var(--dbt-success);font-size:12px;white-space:pre-wrap;}",
@@ -387,23 +385,24 @@ window.__ModuleLoader__.load({
387
385
  ".dbt-treename{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
388
386
  /* ===== Navicat 式浏览(内嵌面板,纵向流式;浮窗壳类已随 DOM 回收) ===== */
389
387
  ".dbt-browse-body{display:flex;flex-direction:column;gap:10px;min-height:0;}",
390
- ".dbt-browse-tree{flex:none;max-height:280px;overflow-y:auto;border-bottom:1px solid var(--dbt-separator);}",
391
- ".dbt-browse-main{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px;}",
388
+ ".dbt-browse-tree{flex:none;max-height:280px;overflow-y:auto;background:var(--dbt-surface);border-radius:var(--dbt-radius-card);}",
389
+ ".dbt-browse-main{flex:1;min-height:0;display:flex;flex-direction:column;gap:8px;}",
392
390
  ".dbt-browse-tabs{flex:none;}",
393
- ".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;gap:10px;padding:12px;}",
394
- ".dbt-browse-tablewrap{flex:none;max-height:320px;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
395
- ".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:10px 12px;border-top:1px solid var(--dbt-separator);}",
391
+ ".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;}",
392
+ ".dbt-browse-tablewrap{flex:1 1 auto;min-height:120px;overflow:auto;border:none;border-radius:var(--dbt-radius-card);background:var(--dbt-surface);}",
393
+ ".dbt-browse-footer{flex:none;display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--dbt-surface);border-radius:var(--dbt-radius-ctrl);}",
396
394
  ".dbt-browse-empty{display:flex;align-items:center;justify-content:center;color:var(--dbt-text-secondary);font-size:13px;padding:24px 12px;}",
397
395
  ".dbt-browse-errorbox{display:flex;flex-direction:column;gap:8px;align-items:flex-start;}",
398
396
  ".dbt-browse-skel{display:flex;flex-direction:column;gap:8px;}",
399
397
  ".dbt-browse-skelrow{display:flex;gap:8px;}",
400
398
  ".dbt-browse-skelcell{flex:1;height:24px;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-pill);}",
401
- "/* ===== 浏览面板:单元格查看/编辑浮层(定位锚点为 .dbt-browse-content) ===== */",
399
+ "/* ===== 浏览面板:单元格按钮与详情底栏(Apple 分区卡) ===== */",
402
400
  ".dbt-cellbtn{border:none;background:transparent;color:inherit;font:inherit;padding:0;margin:0;cursor:pointer;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;width:100%;text-align:left;}",
403
401
  ".dbt-cellbtn:focus-visible{outline:2px solid var(--dbt-accent);outline-offset:-1px;border-radius:2px;}",
404
- ".dbt-cellpop{position:absolute;z-index:20;width:min(460px,100%);background:var(--dbt-dialog-bg);border:1px solid var(--dbt-separator);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.3);-webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);padding:12px;display:flex;flex-direction:column;gap:8px;box-sizing:border-box;font-size:13px;}",
405
- ".dbt-cellpop-value{font-family:var(--dbt-mono);font-size:12px;white-space:pre-wrap;word-break:break-all;max-height:240px;overflow:auto;background:var(--dbt-surface-strong);border-radius:var(--dbt-radius-ctrl);padding:8px;}",
406
- ".dbt-cellpop-actions{display:flex;gap:8px;align-items:center;}",
402
+ ".dbt-celldetail{flex:none;margin-top:10px;padding:12px;background:#2c2c2e;border-radius:var(--dbt-radius-card);display:flex;flex-direction:column;gap:8px;}",
403
+ "@media (prefers-color-scheme: light){.dbt-celldetail{background:#f5f5f7;}}",
404
+ ".dbt-celldetail-input{font-family:var(--dbt-mono);font-size:12px;min-height:72px;resize:vertical;background:var(--dbt-surface-strong);border:none;color:inherit;border-radius:var(--dbt-radius-ctrl);padding:8px;width:100%;box-sizing:border-box;}",
405
+ ".dbt-celldetail-input:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
407
406
  ".dbt-celledit{display:flex;flex-direction:column;gap:8px;}",
408
407
  ".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:var(--dbt-radius-ctrl);padding:5px 12px;}",
409
408
  ".dbt-celledit textarea:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
@@ -416,8 +415,8 @@ window.__ModuleLoader__.load({
416
415
  ".dbt-seg button{flex:none;border:none;background:transparent;color:inherit;font-size:12px;padding:5px 12px;border-radius:var(--dbt-radius-pill);cursor:pointer;transition:all .18s var(--dbt-ease);}",
417
416
  ".dbt-seg button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
418
417
  "/* ===== 降级:透明度减弱 / 动效减弱 ===== */",
419
- "@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-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;}}",
420
- "@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog,.dbt-cellpop{background:#f5f5f7;}.dbt-table th,.dbt-browse-root .dbt-table th{background:#f2f2f7;}}",
418
+ "@media (prefers-reduced-transparency: reduce){.dbt-overlay{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.55);}.dbt-dialog{backdrop-filter:none;-webkit-backdrop-filter:none;background:#2c2c2e;}.dbt-celldetail{background:#2c2c2e;}.dbt-table th{backdrop-filter:none;-webkit-backdrop-filter:none;background:#1e1e20;}.dbt-browse-root .dbt-table th{background:#1e1e20;}}",
419
+ "@media (prefers-color-scheme: light) and (prefers-reduced-transparency: reduce){.dbt-dialog{background:#f5f5f7;}.dbt-celldetail{background:#f2f2f7;}.dbt-table th,.dbt-browse-root .dbt-table th{background:#f2f2f7;}}",
421
420
  "@media (prefers-reduced-motion: reduce){.dbt-panel *,.dbt-browse-root *{transition:none!important;animation:none!important;}.dbt-btn:active{transform:none;}}",
422
421
  ].join("\n");
423
422
  document.head.appendChild(styleEl);
@@ -1127,9 +1126,8 @@ window.__ModuleLoader__.load({
1127
1126
  };
1128
1127
  function PreviewGrid(props) {
1129
1128
  const { preview, schema, kind, editable, tableType, tableName, dbRef, schemaName, connId, projectPath, onSaved, askConfirm } = props;
1130
- const [pop, setPop] = React.useState(null); // {rowIdx, colName, cell} 单元格浮层
1131
- const [editing, setEditing] = React.useState(false);
1132
- const [text, setText] = React.useState(""); // 编辑态 textarea 值(进入浮层时就位)
1129
+ const [sel, setSel] = React.useState(null); // {rowIdx, colName, cell} 底部详情栏当前单元格(null=收起底栏)
1130
+ const [text, setText] = React.useState(""); // 详情栏 textarea 值(选中即就位)
1133
1131
  const [editNull, setEditNull] = React.useState(false); // 「设为 NULL」勾选(null 单元格默认勾选)
1134
1132
  const [cellErr, setCellErr] = React.useState("");
1135
1133
  const [busy, setBusy] = React.useState(false);
@@ -1140,23 +1138,23 @@ window.__ModuleLoader__.load({
1140
1138
  ? schema.filter((c) => c && c.key === "PRI").map((c) => c.name)
1141
1139
  : [];
1142
1140
 
1143
- // 打开浮层(查看态):编辑态初值一并就位——textarea 空 ↔ 勾选设为 NULL
1144
- function openCell(rowIdx, colName, cell) {
1145
- setPop({ rowIdx, colName, cell });
1146
- setEditing(false);
1141
+ // 点单元格开合底栏:同格再点收起;新格选中即把 textarea 初值就位——textarea 空 ↔ 勾选设为 NULL
1142
+ function toggleCell(rowIdx, colName, cell) {
1147
1143
  setCellErr("");
1144
+ if (sel && sel.rowIdx === rowIdx && sel.colName === colName) { setSel(null); return; }
1145
+ setSel({ rowIdx, colName, cell });
1148
1146
  setEditNull(cell === null);
1149
1147
  setText(cell === null ? "" : String(cell));
1150
1148
  }
1151
1149
  function copyCell() {
1152
- if (!pop) return;
1153
- navigator.clipboard.writeText(String(pop.cell)).then(() => {
1150
+ if (!sel) return;
1151
+ navigator.clipboard.writeText(String(sel.cell)).then(() => {
1154
1152
  setCopied(true);
1155
1153
  window.setTimeout(() => setCopied(false), 1500);
1156
1154
  }, () => { /* 剪贴板不可用(无权限等):静默,按钮文案不变 */ });
1157
1155
  }
1158
1156
 
1159
- // 单元格编辑资格:返回 null=可编辑,否则为原因 i18n key(浮层 .dbt-readhint 展示)
1157
+ // 单元格编辑资格:返回 null=可编辑,否则为原因 i18n key(底栏 .dbt-readhint 展示)
1160
1158
  function cellReason(colName, shown) {
1161
1159
  if (!editable || !dialectOf(kind)) return "readOnlyRo";
1162
1160
  if (isMongo) return colName === "_id" ? "readOnlyRo" : null;
@@ -1171,11 +1169,11 @@ window.__ModuleLoader__.load({
1171
1169
  return null;
1172
1170
  }
1173
1171
 
1174
- // 保存:构造写回命令(按 kind 分派)→ 顺序执行(redis 两步)→ 全部成功关浮层并回调 onSaved
1172
+ // 保存:构造写回命令(按 kind 分派)→ 顺序执行(redis 两步)→ 全部成功收起底栏并回调 onSaved
1175
1173
  async function saveCell() {
1176
- if (!pop) return;
1174
+ if (!sel) return;
1177
1175
  setCellErr("");
1178
- const { rowIdx, colName } = pop;
1176
+ const { rowIdx, colName } = sel;
1179
1177
  const row = (preview.rows || [])[rowIdx] || [];
1180
1178
  let r;
1181
1179
  if (kind === "redis") {
@@ -1200,26 +1198,32 @@ window.__ModuleLoader__.load({
1200
1198
  askConfirm,
1201
1199
  );
1202
1200
  }
1203
- setPop(null);
1201
+ setSel(null);
1204
1202
  if (onSaved) onSaved();
1205
1203
  } catch (e) {
1206
- // 取消确认(e.cancelled)不算失败:浮层保持打开供重试
1204
+ // 取消确认(e.cancelled)不算失败:底栏保持打开供重试
1207
1205
  if (!e.cancelled) setCellErr(t("error") + ": " + String(e && e.message ? e.message : e));
1208
1206
  } finally {
1209
1207
  setBusy(false);
1210
1208
  }
1211
1209
  }
1212
1210
 
1213
- const popReason = pop ? cellReason(pop.colName, pop.cell === null ? "NULL" : String(pop.cell)) : null;
1211
+ // 切表/翻页联动:预览数据引用变化即收起底栏,避免行号错位指向旧行
1212
+ React.useEffect(() => { setSel(null); }, [preview]);
1213
+
1214
+ const shown = sel ? (sel.cell === null ? "NULL" : String(sel.cell)) : null;
1215
+ const selReason = sel ? cellReason(sel.colName, shown) : null;
1216
+ // 截断/BLOB 独立提示:命中即补显,若已作为只读原因展示过则不再重复
1217
+ const truncHint = !sel ? null : isTruncatedCell(shown) ? "truncatedNoEdit" : isBlobCell(shown) ? "blobNoEdit" : null;
1214
1218
  return React.createElement(
1215
1219
  "div",
1216
1220
  { className: "dbt-browse-root", style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0 } },
1217
- // 顶层只读提示:授权为 ro 时整格只读,单元格仍可点击查看完整值
1221
+ // 顶层只读提示:授权为 ro 时整格只读,单元格仍可点开底栏查看完整值
1218
1222
  !editable ? React.createElement("div", { className: "dbt-readhint" }, t("readOnlyRo")) : null,
1219
- // relative 容器承载单元格浮层(简化定位:水平居中贴底)
1223
+ // 预览表格区(flex:1 撑满剩余空间;底部详情栏在表格之后、面板流内)
1220
1224
  React.createElement(
1221
1225
  "div",
1222
- { style: { position: "relative", flex: 1, minHeight: 0, display: "flex", flexDirection: "column" } },
1226
+ { style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column" } },
1223
1227
  React.createElement(
1224
1228
  "div",
1225
1229
  { className: "dbt-browse-tablewrap" },
@@ -1240,51 +1244,48 @@ window.__ModuleLoader__.load({
1240
1244
  React.createElement("td", { key: j, title: row[j] === null ? "NULL" : String(row[j]) },
1241
1245
  React.createElement("button", {
1242
1246
  className: "dbt-cellbtn", type: "button",
1243
- onClick: () => openCell(i, colName, row[j]),
1244
- }, row[j] === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(row[j]))))),
1247
+ onClick: () => toggleCell(i, colName, row[j]),
1248
+ }, row[j] === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(row[j])))))),
1245
1249
  ),
1246
1250
  ),
1247
1251
  ),
1248
- pop ? React.createElement(
1252
+ ),
1253
+ // 底部详情栏(Navicat/检查器式):预览表格下方一栏,textarea 直接可编辑,点保存写回
1254
+ sel ? React.createElement(
1255
+ "div",
1256
+ { className: "dbt-celldetail" },
1257
+ // 标题行:完整值 · 列名;null 单元格带 NULL 芯片;只读原因随之展示
1258
+ React.createElement(
1249
1259
  "div",
1250
- { className: "dbt-cellpop", style: { position: "absolute", left: "50%", transform: "translateX(-50%)", bottom: 12 } },
1251
- React.createElement("strong", null, t("cellValue") + " · " + pop.colName),
1252
- React.createElement(
1253
- "div",
1254
- { className: "dbt-cellpop-value" },
1255
- pop.cell === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(pop.cell),
1256
- ),
1257
- popReason ? React.createElement("div", { className: "dbt-readhint" }, t(popReason)) : null,
1258
- cellErr ? React.createElement("div", { className: "dbt-err" }, cellErr) : null,
1259
- editing
1260
- ? React.createElement(
1261
- "div",
1262
- { className: "dbt-celledit" },
1263
- React.createElement("textarea", { value: text, onChange: (e) => setText(e.target.value) }),
1264
- React.createElement(
1265
- "label",
1266
- // 不用 .dbt-row:全局 input flex:1 会把 checkbox 拉伸占满、标签被推到远端(视觉审查命中)
1267
- { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", width: "fit-content" } },
1268
- React.createElement("input", { type: "checkbox", style: { flex: "none", minWidth: 0, width: "auto" }, checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
1269
- t("setNull"),
1270
- ),
1271
- React.createElement(
1272
- "div",
1273
- { className: "dbt-row" },
1274
- React.createElement("button", { className: "dbt-btn primary", disabled: busy, onClick: saveCell }, busy ? t("cellSaving") : t("save")),
1275
- React.createElement("button", { className: "dbt-btn", disabled: busy, onClick: () => setEditing(false) }, t("cancel")),
1276
- ),
1277
- )
1278
- : React.createElement(
1279
- "div",
1280
- { className: "dbt-cellpop-actions" },
1281
- React.createElement("button", { className: "dbt-btn", onClick: copyCell }, copied ? t("copied") : t("copyBtn")),
1282
- popReason === null ? React.createElement("button", { className: "dbt-btn primary", onClick: () => setEditing(true) }, t("editCell")) : null,
1283
- React.createElement("button", { className: "dbt-btn", onClick: () => setPop(null) }, t("close")),
1284
- ),
1285
- ) : null,
1260
+ null,
1261
+ React.createElement("strong", null, t("cellValue") + " · " + sel.colName),
1262
+ sel.cell === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : null,
1263
+ selReason ? React.createElement("span", { className: "dbt-readhint" }, t(selReason)) : null,
1286
1264
  ),
1287
- ),
1265
+ // 完整值编辑区:textarea 恒为编辑形态,不可编辑时只读
1266
+ React.createElement("textarea", {
1267
+ className: "dbt-celldetail-input",
1268
+ value: text,
1269
+ readOnly: selReason !== null,
1270
+ onChange: (e) => setText(e.target.value),
1271
+ }),
1272
+ sel.cell === null ? React.createElement(
1273
+ "label",
1274
+ // 不用 .dbt-row:全局 input flex:1 会把 checkbox 拉伸占满、标签被推到远端(视觉审查命中)
1275
+ { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", width: "fit-content" } },
1276
+ React.createElement("input", { type: "checkbox", style: { flex: "none", minWidth: 0, width: "auto" }, checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
1277
+ t("setNull"),
1278
+ ) : null,
1279
+ truncHint && truncHint !== selReason ? React.createElement("span", { className: "dbt-readhint" }, t(truncHint)) : null,
1280
+ cellErr ? React.createElement("span", { className: "dbt-err" }, cellErr) : null,
1281
+ React.createElement(
1282
+ "div",
1283
+ { className: "dbt-row" },
1284
+ React.createElement("button", { className: "dbt-btn", onClick: copyCell }, copied ? t("copied") : t("copyBtn")),
1285
+ selReason === null ? React.createElement("button", { className: "dbt-btn primary", disabled: busy, onClick: saveCell }, busy ? t("cellSaving") : t("save")) : null,
1286
+ React.createElement("button", { className: "dbt-btn", onClick: () => setSel(null) }, t("close")),
1287
+ ),
1288
+ ) : null,
1288
1289
  );
1289
1290
  }
1290
1291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
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",