dsh-db-tool 1.5.6 → 1.5.7

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 +74 -74
  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,23 @@ 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;}",
391
+ ".dbt-browse-content{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;}",
394
392
  ".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);}",
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{margin-top:10px;padding:12px;background:var(--dbt-surface);border-radius:var(--dbt-radius-card);display:flex;flex-direction:column;gap:8px;}",
403
+ ".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;}",
404
+ ".dbt-celldetail-input:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
407
405
  ".dbt-celledit{display:flex;flex-direction:column;gap:8px;}",
408
406
  ".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
407
  ".dbt-celledit textarea:focus{outline:2px solid var(--dbt-accent);outline-offset:-1px;}",
@@ -416,8 +414,8 @@ window.__ModuleLoader__.load({
416
414
  ".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
415
  ".dbt-seg button.active{background:var(--dbt-seg-active);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.12);}",
418
416
  "/* ===== 降级:透明度减弱 / 动效减弱 ===== */",
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;}}",
417
+ "@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;}}",
418
+ "@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
419
  "@media (prefers-reduced-motion: reduce){.dbt-panel *,.dbt-browse-root *{transition:none!important;animation:none!important;}.dbt-btn:active{transform:none;}}",
422
420
  ].join("\n");
423
421
  document.head.appendChild(styleEl);
@@ -1127,9 +1125,8 @@ window.__ModuleLoader__.load({
1127
1125
  };
1128
1126
  function PreviewGrid(props) {
1129
1127
  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 值(进入浮层时就位)
1128
+ const [sel, setSel] = React.useState(null); // {rowIdx, colName, cell} 底部详情栏当前单元格(null=收起底栏)
1129
+ const [text, setText] = React.useState(""); // 详情栏 textarea 值(选中即就位)
1133
1130
  const [editNull, setEditNull] = React.useState(false); // 「设为 NULL」勾选(null 单元格默认勾选)
1134
1131
  const [cellErr, setCellErr] = React.useState("");
1135
1132
  const [busy, setBusy] = React.useState(false);
@@ -1140,23 +1137,23 @@ window.__ModuleLoader__.load({
1140
1137
  ? schema.filter((c) => c && c.key === "PRI").map((c) => c.name)
1141
1138
  : [];
1142
1139
 
1143
- // 打开浮层(查看态):编辑态初值一并就位——textarea 空 ↔ 勾选设为 NULL
1144
- function openCell(rowIdx, colName, cell) {
1145
- setPop({ rowIdx, colName, cell });
1146
- setEditing(false);
1140
+ // 点单元格开合底栏:同格再点收起;新格选中即把 textarea 初值就位——textarea 空 ↔ 勾选设为 NULL
1141
+ function toggleCell(rowIdx, colName, cell) {
1147
1142
  setCellErr("");
1143
+ if (sel && sel.rowIdx === rowIdx && sel.colName === colName) { setSel(null); return; }
1144
+ setSel({ rowIdx, colName, cell });
1148
1145
  setEditNull(cell === null);
1149
1146
  setText(cell === null ? "" : String(cell));
1150
1147
  }
1151
1148
  function copyCell() {
1152
- if (!pop) return;
1153
- navigator.clipboard.writeText(String(pop.cell)).then(() => {
1149
+ if (!sel) return;
1150
+ navigator.clipboard.writeText(String(sel.cell)).then(() => {
1154
1151
  setCopied(true);
1155
1152
  window.setTimeout(() => setCopied(false), 1500);
1156
1153
  }, () => { /* 剪贴板不可用(无权限等):静默,按钮文案不变 */ });
1157
1154
  }
1158
1155
 
1159
- // 单元格编辑资格:返回 null=可编辑,否则为原因 i18n key(浮层 .dbt-readhint 展示)
1156
+ // 单元格编辑资格:返回 null=可编辑,否则为原因 i18n key(底栏 .dbt-readhint 展示)
1160
1157
  function cellReason(colName, shown) {
1161
1158
  if (!editable || !dialectOf(kind)) return "readOnlyRo";
1162
1159
  if (isMongo) return colName === "_id" ? "readOnlyRo" : null;
@@ -1171,11 +1168,11 @@ window.__ModuleLoader__.load({
1171
1168
  return null;
1172
1169
  }
1173
1170
 
1174
- // 保存:构造写回命令(按 kind 分派)→ 顺序执行(redis 两步)→ 全部成功关浮层并回调 onSaved
1171
+ // 保存:构造写回命令(按 kind 分派)→ 顺序执行(redis 两步)→ 全部成功收起底栏并回调 onSaved
1175
1172
  async function saveCell() {
1176
- if (!pop) return;
1173
+ if (!sel) return;
1177
1174
  setCellErr("");
1178
- const { rowIdx, colName } = pop;
1175
+ const { rowIdx, colName } = sel;
1179
1176
  const row = (preview.rows || [])[rowIdx] || [];
1180
1177
  let r;
1181
1178
  if (kind === "redis") {
@@ -1200,26 +1197,32 @@ window.__ModuleLoader__.load({
1200
1197
  askConfirm,
1201
1198
  );
1202
1199
  }
1203
- setPop(null);
1200
+ setSel(null);
1204
1201
  if (onSaved) onSaved();
1205
1202
  } catch (e) {
1206
- // 取消确认(e.cancelled)不算失败:浮层保持打开供重试
1203
+ // 取消确认(e.cancelled)不算失败:底栏保持打开供重试
1207
1204
  if (!e.cancelled) setCellErr(t("error") + ": " + String(e && e.message ? e.message : e));
1208
1205
  } finally {
1209
1206
  setBusy(false);
1210
1207
  }
1211
1208
  }
1212
1209
 
1213
- const popReason = pop ? cellReason(pop.colName, pop.cell === null ? "NULL" : String(pop.cell)) : null;
1210
+ // 切表/翻页联动:预览数据引用变化即收起底栏,避免行号错位指向旧行
1211
+ React.useEffect(() => { setSel(null); }, [preview]);
1212
+
1213
+ const shown = sel ? (sel.cell === null ? "NULL" : String(sel.cell)) : null;
1214
+ const selReason = sel ? cellReason(sel.colName, shown) : null;
1215
+ // 截断/BLOB 独立提示:命中即补显,若已作为只读原因展示过则不再重复
1216
+ const truncHint = !sel ? null : isTruncatedCell(shown) ? "truncatedNoEdit" : isBlobCell(shown) ? "blobNoEdit" : null;
1214
1217
  return React.createElement(
1215
1218
  "div",
1216
1219
  { className: "dbt-browse-root", style: { display: "flex", flexDirection: "column", flex: 1, minHeight: 0 } },
1217
- // 顶层只读提示:授权为 ro 时整格只读,单元格仍可点击查看完整值
1220
+ // 顶层只读提示:授权为 ro 时整格只读,单元格仍可点开底栏查看完整值
1218
1221
  !editable ? React.createElement("div", { className: "dbt-readhint" }, t("readOnlyRo")) : null,
1219
- // relative 容器承载单元格浮层(简化定位:水平居中贴底)
1222
+ // 预览表格区(flex:1 撑满剩余空间;底部详情栏在表格之后、面板流内)
1220
1223
  React.createElement(
1221
1224
  "div",
1222
- { style: { position: "relative", flex: 1, minHeight: 0, display: "flex", flexDirection: "column" } },
1225
+ { style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column" } },
1223
1226
  React.createElement(
1224
1227
  "div",
1225
1228
  { className: "dbt-browse-tablewrap" },
@@ -1240,51 +1243,48 @@ window.__ModuleLoader__.load({
1240
1243
  React.createElement("td", { key: j, title: row[j] === null ? "NULL" : String(row[j]) },
1241
1244
  React.createElement("button", {
1242
1245
  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]))))),
1246
+ onClick: () => toggleCell(i, colName, row[j]),
1247
+ }, row[j] === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : String(row[j])))))),
1245
1248
  ),
1246
1249
  ),
1247
1250
  ),
1248
- pop ? React.createElement(
1251
+ ),
1252
+ // 底部详情栏(Navicat/检查器式):预览表格下方一栏,textarea 直接可编辑,点保存写回
1253
+ sel ? React.createElement(
1254
+ "div",
1255
+ { className: "dbt-celldetail" },
1256
+ // 标题行:完整值 · 列名;null 单元格带 NULL 芯片;只读原因随之展示
1257
+ React.createElement(
1249
1258
  "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,
1259
+ null,
1260
+ React.createElement("strong", null, t("cellValue") + " · " + sel.colName),
1261
+ sel.cell === null ? React.createElement("span", { className: "dbt-nullchip" }, "NULL") : null,
1262
+ selReason ? React.createElement("span", { className: "dbt-readhint" }, t(selReason)) : null,
1286
1263
  ),
1287
- ),
1264
+ // 完整值编辑区:textarea 恒为编辑形态,不可编辑时只读
1265
+ React.createElement("textarea", {
1266
+ className: "dbt-celldetail-input",
1267
+ value: text,
1268
+ readOnly: selReason !== null,
1269
+ onChange: (e) => setText(e.target.value),
1270
+ }),
1271
+ sel.cell === null ? React.createElement(
1272
+ "label",
1273
+ // 不用 .dbt-row:全局 input flex:1 会把 checkbox 拉伸占满、标签被推到远端(视觉审查命中)
1274
+ { style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", width: "fit-content" } },
1275
+ React.createElement("input", { type: "checkbox", style: { flex: "none", minWidth: 0, width: "auto" }, checked: editNull, onChange: (e) => setEditNull(e.target.checked) }),
1276
+ t("setNull"),
1277
+ ) : null,
1278
+ truncHint && truncHint !== selReason ? React.createElement("span", { className: "dbt-readhint" }, t(truncHint)) : null,
1279
+ cellErr ? React.createElement("span", { className: "dbt-err" }, cellErr) : null,
1280
+ React.createElement(
1281
+ "div",
1282
+ { className: "dbt-row" },
1283
+ React.createElement("button", { className: "dbt-btn", onClick: copyCell }, copied ? t("copied") : t("copyBtn")),
1284
+ selReason === null ? React.createElement("button", { className: "dbt-btn primary", disabled: busy, onClick: saveCell }, busy ? t("cellSaving") : t("save")) : null,
1285
+ React.createElement("button", { className: "dbt-btn", onClick: () => setSel(null) }, t("close")),
1286
+ ),
1287
+ ) : null,
1288
1288
  );
1289
1289
  }
1290
1290
 
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.7",
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",