From a3f157cb29f5fb4188e6d7eb8545eef6a5911854 Mon Sep 17 00:00:00 2001 From: Joes Date: Sat, 3 Oct 2026 17:57:15 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E3=80=81GridFS=20=E4=B8=8E=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=20UI/=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 本地化:新增/更新大量 Loc 键(Design_*, Fs_*, Query_* 等)用于索引编辑器提示、GridFS 拖放区、查询目标与连接/数据库下拉文案。 - 设计页(DesignIndexesView / DesignModels / DesignTabViewModel.NewIndex): - 索引编辑器由侧栏改为下半部三栏全宽编辑器(字段表 / 类型与选项 / 命令预览),支持 IsCreateOpen 开关。 - 在索引表尾钉入实时同步的草稿行(DraftIndex/HasDraft)并显示预估大小与“待创建”列。 - 字段行改为序号列、上移/下移/删除按钮,行距 RowPitch 调整为 28。 - 新增 NewKeyRow 字段(Number、CanMoveUp/Down、IsLast、KindText),SwatchToken 改为 OptionOf,多属性刷新支持。 - 增加 MoveKeyUp/MoveKeyDown 命令与重编号逻辑(RenumberKeys);抽样结果改用 OptionOf,UpdateImpact 返回预估大小并构建 DraftIndex;Create 流程与可用性、错误、预估显示整合到新 UI。 - GridFS(GridFsTabView): - 拖放区改为整块可点击区域,新增文本与两个链接按钮(选择文件/选择文件夹),样式区分 pointerover 与 over。 - 添加 DropZone.Tapped 处理:空白点击触发 UploadFilesCommand,按钮分别处理选文件与选文件夹以避免重复弹窗。 - 查询编辑器(QueryTabView / QueryTabViewModel): - 工具行拆为两行(上行为编辑操作、下行为执行目标与相关设置),连接与数据库用"target"样式选择按钮显示。 - 右侧 Helper 区新增“集合在哪个库(@db)”展示与“集合缺失”警告。 - 引入连接下拉交互 OpenConnectionMenu 与连接状态显示(连接中转圈、名称/小字状态)。 - 将执行目标逻辑抽成 QueryTabViewModel.Target:新增 SwitchConnectionCommand / SwitchConnectionAsync,支持“安静连接(quiet)”——切换时不打开占位标签、不改变对象树选中、不打开对象列表。 - 切换连接采用原位替换标签 Workspace.ReplaceTab,CarryOver 文本/未保存状态/光标/maxTimeMS 等。 - 新增异步诊断 CheckCollectionsAsync/CheckCollectionsCoreAsync:检测脚本中 db.collection.method(...) 所指集合是否在目标库存在(排除创建类方法与 system.*),不存在时加入 Warning;支持 ForgetCollections 在脚本建删集合时清缓存。 - QueryTabView 与 ViewModel 协同处理连接/库切换、切换过程消息与失败提示。 - 工作区/连接管理(MongoWorkspaceViewModel): - ConnectAsync / AttachAsync 增加 quiet 参数以支持安静连接流程;AttachAsync 在 quiet 时不改变对象树选中且不打开对象列表。 - 新增 ReplaceTab 实现“原位替换标签”的语义。 - 测试与工具: - 更新/新增单元与集成测试:DesignTests(字段序号、上/下移按钮、草稿行预估与目标文案断言)、QueryTests(执行目标切换的原位替换、安静连接、带文本与未保存状态、切库后集合缺失诊断、use 优先级等场景)。 - 增加 UI 测试辅助 WaitAsync 用于 Pump。 - 总体影响: - 提升索引设计器可用性(更宽编辑器、草稿行、显式序号与上下移),改进 GridFS 拖放 UX,并增强查询编辑器对“在哪个连接/哪个库上运行”的可见性与切换体验。伴随模型/VM 新字段与命令、诊断增强、安静连接与原位换标签支持以及对应本地化与测试补充。 --- plugins/VelaShell.Plugin.Mongo/Loc.Design.cs | 17 +- plugins/VelaShell.Plugin.Mongo/Loc.GridFs.cs | 5 +- plugins/VelaShell.Plugin.Mongo/Loc.Query.cs | 14 +- plugins/VelaShell.Plugin.Mongo/README.md | 15 +- .../Ui/Design/DesignIndexesView.axaml | 747 +++++++++++------- .../Ui/Design/DesignIndexesView.axaml.cs | 8 +- .../Ui/Design/DesignModels.cs | 55 +- .../Ui/Design/DesignTabViewModel.NewIndex.cs | 84 +- .../Ui/GridFs/GridFsTabView.axaml | 37 +- .../Ui/GridFs/GridFsTabView.axaml.cs | 16 +- .../Ui/Query/QueryTabView.axaml | 172 ++-- .../Ui/Query/QueryTabView.axaml.cs | 65 +- .../Ui/Query/QueryTabViewModel.Editor.cs | 1 + .../Ui/Query/QueryTabViewModel.Panels.cs | 33 + .../Ui/Query/QueryTabViewModel.Run.cs | 1 + .../Ui/Query/QueryTabViewModel.Target.cs | 194 +++++ .../Ui/Query/QueryTabViewModel.cs | 5 +- .../MongoWorkspaceViewModel.Connections.cs | 24 +- .../Ui/Shell/MongoWorkspaceViewModel.cs | 24 + .../DesignTests.cs | 20 + .../QueryTests.cs | 78 ++ 21 files changed, 1214 insertions(+), 401 deletions(-) create mode 100644 plugins/VelaShell.Plugin.Mongo/Ui/Query/QueryTabViewModel.Target.cs diff --git a/plugins/VelaShell.Plugin.Mongo/Loc.Design.cs b/plugins/VelaShell.Plugin.Mongo/Loc.Design.cs index 33ddd99..d406189 100644 --- a/plugins/VelaShell.Plugin.Mongo/Loc.Design.cs +++ b/plugins/VelaShell.Plugin.Mongo/Loc.Design.cs @@ -106,7 +106,7 @@ private static readonly (string Key, string En, string Zh)[] DesignTexts = // ── 新建索引 ── ("Design_Fields", "Fields", "字段"), - ("Design_DragHint", "Drag to reorder", "拖动调整顺序"), + ("Design_DragHint", "Key order is the prefix order · ↑ ↓ or drag ⠿", "键序即前缀顺序 · ↑ ↓ 或拖动 ⠿ 调整"), ("Design_FieldPlaceholder", "field.path", "字段路径"), ("Design_AddField", "Add field", "添加字段"), ("Design_Kind", "Type", "类型"), @@ -130,6 +130,21 @@ private static readonly (string Key, string En, string Zh)[] DesignTexts = ("Design_RoleSort", "sort", "排序"), ("Design_RoleRange", "range", "范围"), ("Design_CreateIndex", "Create index", "创建索引"), + ("Design_EditorTarget", "Runs createIndexes on {0}", "将在 {0} 上执行 createIndexes"), + ("Design_DraftBadge", "new", "新建"), + ("Design_DraftUsage", "not created yet", "待创建"), + ("Design_ColOrder", "Order / type", "排序 / 类型"), + ("Design_ColRole", "ESR role", "ESR 角色"), + ("Design_ColSampled", "Sampled type", "抽样类型"), + ("Design_MoveUp", "Move up", "上移"), + ("Design_MoveDown", "Move down", "下移"), + ("Design_RemoveField", "Remove field", "移除字段"), + ("Design_TypeOptions", "Type & options", "类型与选项"), + ("Design_Preview", "Command preview", "命令预览"), + ("Design_OptUniqueHint", "rejects duplicate keys", "拒绝重复键值"), + ("Design_OptSparseHint", "skips docs without the field", "跳过缺该字段的文档"), + ("Design_OptPartialHint", "only docs matching the filter", "只索引满足条件的文档"), + ("Design_CloseEditor", "Close the editor", "关闭编辑器"), ("Design_NoSampleFields", "No sampled fields — type a field path", "没有抽样字段 · 直接输入字段路径"), ("Design_ErrNoFields", "Add at least one field", "至少添加一个字段"), ("Design_ErrUniqueKind", "Text, hashed, wildcard and 2dsphere indexes can't be unique", "文本、哈希、通配符与 2dsphere 索引不能设为唯一"), diff --git a/plugins/VelaShell.Plugin.Mongo/Loc.GridFs.cs b/plugins/VelaShell.Plugin.Mongo/Loc.GridFs.cs index 5cd6508..e577e71 100644 --- a/plugins/VelaShell.Plugin.Mongo/Loc.GridFs.cs +++ b/plugins/VelaShell.Plugin.Mongo/Loc.GridFs.cs @@ -39,7 +39,10 @@ private static readonly (string Key, string En, string Zh)[] GridFsTexts = ("Fs_NoMatch", "No files match “{0}”", "没有匹配 “{0}” 的文件"), ("Fs_Truncated", "Showing the first {0} items — narrow it down with search", "仅显示前 {0} 项 · 用搜索缩小范围"), ("Fs_OpenFolder", "Open", "打开"), - ("Fs_DropHere", "Drop files here to upload to {0}", "拖拽文件到此处上传到 {0}"), + ("Fs_DropHere", "Drop files here, or click to choose files — they go to {0}", "拖拽文件到此处,或点击选择文件 · 上传到 {0}"), + ("Fs_DropPickFiles", "Choose files…", "选择文件…"), + ("Fs_DropPickFolder", "Choose a folder…", "选择文件夹…"), + ("Fs_DropClickTip", "Click to choose files to upload", "点击选择要上传的文件"), ("Fs_DropHint", "No 16 MB per-file limit · chunkSize 255 KB · metadata can be edited before upload", "单文件上限不受 16 MB 限制 · chunkSize 255 KB · 可在上传前编辑 metadata"), diff --git a/plugins/VelaShell.Plugin.Mongo/Loc.Query.cs b/plugins/VelaShell.Plugin.Mongo/Loc.Query.cs index fa7e11c..f76876a 100644 --- a/plugins/VelaShell.Plugin.Mongo/Loc.Query.cs +++ b/plugins/VelaShell.Plugin.Mongo/Loc.Query.cs @@ -17,7 +17,19 @@ private static readonly (string Key, string En, string Zh)[] QueryTexts = ("Query_Unlimited", "unlimited", "不限"), ("Query_MaxTimeTip", "Default server-side time limit for each statement (a .maxTimeMS() in the statement wins)", "每条语句默认的服务器端超时(语句里写了 .maxTimeMS() 以语句为准)"), - ("Query_DatabaseTip", "Current database — same as running use", "当前库 —— 切换等同于执行 use"), + ("Query_DatabaseTip", "Database the statements run on — same as running use; db. resolves here", "语句在哪个库上执行 —— 切换等同于执行 use;db.<集合> 在这个库里解析"), + ("Query_ConnectionLabel", "Connection", "连接"), + ("Query_DatabaseLabel", "Database", "数据库"), + ("Query_ConnectionTip", "Connection the statements run on — switching keeps the script", "语句在哪条连接上执行 —— 切换时脚本原样带过去"), + ("Query_ConnNotConnected", "not connected", "未连接"), + ("Query_ConnFailed", "failed", "连接失败"), + ("Query_SwitchingTo", "Connecting {0}…", "正在连接 {0}…"), + ("Query_TargetHint", "Runs on {0} / {1}", "在 {0} / {1} 上执行"), + ("Query_SwitchWhileRunning", "Stop the running statements before switching connections", "先停止正在运行的语句,再切换连接"), + ("Query_SwitchFailed", "Couldn't connect {0}", "无法连接 {0}"), + ("Query_SwitchedTo", "The query now runs on {0} · {1}", "查询已切换到 {0} · {1}"), + ("Query_SwitchedDbMissing", "{0} has no database {1}; using {2}", "{0} 上没有库 {1},改用 {2}"), + ("Query_CollectionMissing", "No collection {0} in database {1} — check the database picker or use()", "库 {1} 里没有集合 {0} —— 核对上方的数据库或 use()"), // ── 状态 ── ("Query_StatusUnsaved", "Query {0} · unsaved", "查询 {0} · 未保存"), diff --git a/plugins/VelaShell.Plugin.Mongo/README.md b/plugins/VelaShell.Plugin.Mongo/README.md index 21f2a57..7519223 100644 --- a/plugins/VelaShell.Plugin.Mongo/README.md +++ b/plugins/VelaShell.Plugin.Mongo/README.md @@ -2,7 +2,7 @@ > id `velashell.mongo` · 进程内装载 · 命令面板入口(`contributes.commands`,与 Docker 面板同一个路数) · `minSdkVersion` 2.0.2 -照设计稿 `velashell-plugin-mongo.pen`(仓库根,23 个画板)实现的 MongoDB 客户端: +照设计稿 `velashell-plugin-mongo.pen`(仓库根,24 个画板)实现的 MongoDB 客户端: Navicat 式的对象树与大图标工具栏,补齐 Compass 的聚合管道、Schema 分析、索引建议与 GridFS; 网格 / 树 / JSON 三视图共用一份数据,所有编辑先进暂存区,提交前可预览将执行的命令。 @@ -48,7 +48,7 @@ Navicat 式的对象树与大图标工具栏,补齐 Compass 的聚合管道、Sc | (稿外)系统库 | admin / config / local 默认不列(`showSystemDatabases`,高级选项);对象树表头的眼睛按钮在本次会话里切换;连接里点名的默认库照列 | Navicat 的默认;日常几乎不进这三个库,列着只会把用户库往下挤 | | 10 的「分组」 | 对话框里一栏(可选已有分组,也可新写);对象树里分了组的连接挂在可收起的组名节下面,不分组的在最上面 | — | | 01 / 13 对象树根上只画了一条连接 | 根上是**全部**已保存的连接(Navicat 的习惯):灰点没连、绿点连着、红点没连上;双击连上,连着的那条下面挂库;右键里有连接 / 断开 / 编辑 / 复制 / 删除 | 连接由插件自己管之后,树根就是连接列表 | -| 14 执行计划时编辑器收矮到 230 | 不动分界:切到执行计划页,编辑器与结果区的高度照用户拖过的分隔条 | 结果页与执行计划页是同一块面板的两个页签;自动收矮再在切回时重置成默认值,用户拖好的高度就白拖了 | +| 14 执行计划时编辑器收矮到 194 | 不动分界:切到执行计划页,编辑器与结果区的高度照用户拖过的分隔条 | 结果页与执行计划页是同一块面板的两个页签;自动收矮再在切回时重置成默认值,用户拖好的高度就白拖了 | | 01 对象树的分组文件夹右键只有刷新 | 「集合 / 视图 / GridFS 存储桶」分组(及其中对象、库行)右键可新建对应对象;桶行右键可上传文件 / 文件夹、删除存储桶 | 不打开对象列表也能就地新建;新建桶原先只能从已打开的桶标签里进 | | 会话标签上的连接名(「mongo-inner-01」) | 宿主标签条上是一个「MongoDB」面板;连接名写在对象树根、对象标签与状态条上 | 一个面板装着全部连接(与 Docker 面板一样);对象标签按连接分开,同名集合在两条连接里是两个标签 | | 22「连接中 / 连接失败」两张卡 | 照稿实现在插件里:连接时先开一个占位标签(「正在连接 X」+ 取消),连上了换成默认库的对象列表,没连上就原地变成「无法连接 X」+ 等宽字的原因 + 编辑连接 / 关闭标签页 / 重新连接;证书不受信任时多一个「信任此证书并重连」(只信这一张的指纹)。连接**中途断开**时内容区顶部一条横幅 | 证书信任原先是宿主的流程,连接自己管之后由插件给 | @@ -75,6 +75,17 @@ Navicat 式的对象树与大图标工具栏,补齐 Compass 的聚合管道、Sc 日期不再给"现在"那一条文字候选。 - **筛选框的 Enter**:补全弹层开着时是「接受补全」,关着才是「查找」。筛选框在编辑区上另挂了隧道处理器, 而同一元素上的隧道处理器按**注册的逆序**调用 —— 它比 `CodeEditor` 自己的先拿到 Enter,所以要先看 `CodeEditor.IsCompletionOpen`。 +- **新建索引**(设计稿 07b,`DesignIndexesView` + `DesignTabViewModel.NewIndex.cs`):Navicat 设计表的做法 —— 点「新建索引」后索引页下半部分 + 换成整宽编辑器(字段表 / 类型与选项 / 命令预览与预估),索引表末尾钉一行「新建」草稿(`DraftIndex`,与表里的行同一个 `IndexRow` 模型, + 不进 `Indexes`,所以重名检查、删除、隐藏都碰不到它)。字段表的行距是 28,`DesignIndexesView.RowPitch` 跟着它算拖动落点; + 上移 / 下移按钮走 `MoveKeyUpCommand / MoveKeyDownCommand`,与拖动是同一个 `MoveKey`。编辑器开着时索引表的滚动区 + (`ScrollViewer#TableScroll.compact`)最多 180 高。 +- **查询的执行目标**(`QueryTabViewModel.Target.cs`):工具行下行的「连接 ▾ 数据库 ▾」。标签的连接定死在构造里, + 所以切换连接是 `MongoWorkspaceViewModel.ReplaceTab` 原位换一个新连接上的查询标签(`CarryOver` 带过文本、未保存基线、光标、 + maxTimeMS);没连着的连接走 `ConnectAsync(entry, quiet: true)`(不开占位标签、不动树的选中、不开对象列表)。 + 诊断的第二段(`CheckCollectionsAsync`,异步)查 `db.集合` 在生效的库里有没有,没有就是一条 Warning; + 脚本里建删了集合(`ChangesCatalog`)时 `ForgetCollections` 丢掉那个库的缓存。 +- **GridFS 拖放区可点**:`DropZone` 的 Tapped 走 `UploadFilesCommand`;源头在框里那两个链接按钮里时不再重复弹(它们自己处理)。 - **代码里建的右键菜单**一律走 `MenuKit.Command`:普通项不设 `Foreground` —— 哪怕设成 `null`,本地值也会压过宿主 `ContextMenu MenuItem` 样式,文字透明到悬停才露出来。 - **绑定不穿过可空的中段**:`{Binding Dialog.Title}`、`{Binding Editor.HasError}` 这类路径在中段为 null 时 diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignIndexesView.axaml b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignIndexesView.axaml index 7ac8bf7..47c2012 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignIndexesView.axaml +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignIndexesView.axaml @@ -5,343 +5,502 @@ x:DataType="ui:DesignTabViewModel"> + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignIndexesView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignIndexesView.axaml.cs index 76a128d..d871e3f 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignIndexesView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignIndexesView.axaml.cs @@ -11,13 +11,13 @@ namespace VelaShell.Plugin.Mongo.Ui; /// /// 集合设计 · 索引页。代码里只做两件 AXAML 不好做的事: -/// 字段 / 方向下拉的菜单(弹出层在独立可视树里,菜单项要直达行模型,现建最省事), -/// 以及字段行按住左侧把手拖动排序。 +/// 索引编辑器字段表里字段 / 方向下拉的菜单(弹出层在独立可视树里,菜单项要直达行模型,现建最省事), +/// 以及字段行按住序号列的把手拖动排序(行尾的上移 / 下移按钮走视图模型命令)。 /// public sealed partial class DesignIndexesView : UserControl { - /// 字段行的行距(26 高 + 6 间距)。 - private const double RowPitch = 32; + /// 字段表的行距(28 高、行间无空隙)。 + private const double RowPitch = 28; private NewKeyRow? _dragging; diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignModels.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignModels.cs index 13c346d..88db60e 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignModels.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignModels.cs @@ -225,32 +225,34 @@ internal sealed class AdviceCard public required Func Action { get; init; } } -/// 新建索引面板里的一行字段。 +/// 索引编辑器「字段」表里的一行(键序即这一行在表里的位置)。 internal sealed class NewKeyRow : ObservableObject { private readonly Action _changed; - private readonly Func _tokenOf; + private readonly Func _optionOf; private readonly Func _label; private string _field; private int _direction; private string _role; private string _kind = "normal"; private bool _isFirst; + private bool _isLast; + private int _number; /// 构造。 /// 字段。 /// 方向(1 / -1)。 /// ESR 角色文字(等值 / 排序 / 范围);未知为空。 - /// 字段 → 类型色令牌(来自抽样)。 - /// 任何改动后通知面板重算命令预览。 + /// 字段 → 抽样到的那一项(类型色、类型名);抽样里没有为 。 + /// 任何改动后通知编辑器重算命令预览。 /// 键值 → 方向下拉上的字(1 升序)。 - public NewKeyRow(string field, int direction, string role, Func tokenOf, Action changed, Func label) + public NewKeyRow(string field, int direction, string role, Func optionOf, Action changed, Func label) { _label = label; _field = field; _direction = direction; _role = role; - _tokenOf = tokenOf; + _optionOf = optionOf; _changed = changed; } @@ -262,12 +264,38 @@ public string Field { if (SetProperty(ref _field, value ?? "")) { - RaisePropertyChanged(nameof(SwatchToken)); + RaisePropertiesChanged(nameof(SwatchToken), nameof(KindText)); _changed(); } } } + /// 序号(1 起;表里第一列)。 + public int Number + { + get => _number; + set => SetProperty(ref _number, value); + } + + /// 是最后一行(「下移」不可用)。 + public bool IsLast + { + get => _isLast; + set + { + if (SetProperty(ref _isLast, value)) + { + RaisePropertyChanged(nameof(CanMoveDown)); + } + } + } + + /// 能上移。 + public bool CanMoveUp => !_isFirst; + + /// 能下移。 + public bool CanMoveDown => !_isLast; + /// 方向(1 / -1)。 public int Direction { @@ -298,11 +326,14 @@ public string Role /// 有角色小标签。 public bool HasRole => _role.Length > 0; - /// 字段类型色。 - public string SwatchToken => _tokenOf(_field); + /// 字段类型色(抽样里没有就是灰的)。 + public string SwatchToken => _optionOf(_field)?.Token ?? "VelaTextMuted"; + + /// 抽样到的类型名(String);抽样里没有为 —。 + public string KindText => _optionOf(_field)?.KindName ?? "—"; - /// 抽样结果到了:类型色要重算。 - internal void RefreshSwatch() => RaisePropertyChanged(nameof(SwatchToken)); + /// 抽样结果到了:类型色与类型名要重算。 + internal void RefreshSwatch() => RaisePropertiesChanged(nameof(SwatchToken), nameof(KindText)); /// 当前索引类型(面板的分段:normal / text / 2dsphere / hashed / wildcard)。 public string Kind @@ -325,7 +356,7 @@ public bool IsFirst { if (SetProperty(ref _isFirst, value)) { - RaisePropertiesChanged(nameof(DirectionValue), nameof(HasDirection), nameof(DirectionText)); + RaisePropertiesChanged(nameof(DirectionValue), nameof(HasDirection), nameof(DirectionText), nameof(CanMoveUp)); } } } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.NewIndex.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.NewIndex.cs index 4b18ad8..28db521 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.NewIndex.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.NewIndex.cs @@ -6,10 +6,18 @@ namespace VelaShell.Plugin.Mongo.Ui; -/// 集合设计 · 右侧「新建索引」面板(设计稿 07)。 +/// +/// 集合设计 · 索引页下半部分的「新建索引」编辑器(设计稿 07,Navicat 的设计表做法)。 +/// +/// 编辑器开着时索引表末尾多一行「新建」草稿,跟着表单实时变(键、类型、属性、预估大小), +/// 下半部分整宽分三栏:字段表(序号 / 字段 / 排序 / ESR / 抽样类型 / 上移下移删除)、类型与选项、命令预览与预估。 +/// 原先挤在右侧 340px 面板里,字段行一窄就点错 —— 摊开之后每一列都有自己的位置。 +/// +/// internal sealed partial class DesignTabViewModel { private bool _isCreateOpen; + private IndexRow? _draftIndex; private string _newKind = "normal"; private bool _newUnique; private bool _newSparse; @@ -28,13 +36,38 @@ internal sealed partial class DesignTabViewModel private bool _isCreating; private IReadOnlyList _fieldOptions = []; - /// 面板开着。 + /// 编辑器开着(下半部分从索引建议换成编辑器,索引表收矮、末尾挂草稿行)。 public bool IsCreateOpen { get => _isCreateOpen; - set => SetProperty(ref _isCreateOpen, value); + set + { + if (SetProperty(ref _isCreateOpen, value)) + { + RaisePropertyChanged(nameof(HasDraft)); + } + } + } + + /// 索引表末尾的「新建」草稿行;还没有字段时为 。 + public IndexRow? DraftIndex + { + get => _draftIndex; + private set + { + if (SetProperty(ref _draftIndex, value)) + { + RaisePropertyChanged(nameof(HasDraft)); + } + } } + /// 草稿行可见(创建期间表里挂的是「构建中」那一行,草稿让位)。 + public bool HasDraft => _isCreateOpen && _draftIndex is not null && !_isCreating; + + /// 编辑器底栏左侧那句「将在 shop.orders 上执行 createIndexes」。 + public string NewTarget => Loc.Format("Design_EditorTarget", Namespace); + /// 键字段行。 public ObservableCollection NewKeys { get; } = []; @@ -214,6 +247,7 @@ private set { if (SetProperty(ref _isCreating, value)) { + RaisePropertyChanged(nameof(HasDraft)); CreateIndexCommand.RaiseCanExecuteChanged(); } } @@ -247,6 +281,12 @@ private set /// 删除一个字段行。 public RelayCommand RemoveKeyCommand { get; private set; } = null!; + /// 字段行上移一位(键序前移)。 + public RelayCommand MoveKeyUpCommand { get; private set; } = null!; + + /// 字段行下移一位。 + public RelayCommand MoveKeyDownCommand { get; private set; } = null!; + /// 创建索引。 public AsyncCommand CreateIndexCommand { get; private set; } = null!; @@ -261,6 +301,8 @@ private void InitializeNewIndex() RenumberKeys(); RecomputeNewIndex(); }); + MoveKeyUpCommand = new(row => MoveKey(NewKeys.IndexOf(row), NewKeys.IndexOf(row) - 1)); + MoveKeyDownCommand = new(row => MoveKey(NewKeys.IndexOf(row), NewKeys.IndexOf(row) + 1)); CreateIndexCommand = new(CreateIndexAsync, () => !_isCreating && _newError.Length == 0 && NewKeys.Any(static k => k.Field.Length > 0)); } @@ -303,7 +345,7 @@ internal void OpenCreatePanel(BsonDocument? key, EsrShape? shape) /// 加一行字段。 internal void AddKey(string field, int direction, string role, bool recompute = true) { - var row = new NewKeyRow(field, direction, role.Length > 0 ? role : GuessRole(field), TokenOf, RecomputeNewIndex, DirectionLabel) + var row = new NewKeyRow(field, direction, role.Length > 0 ? role : GuessRole(field), OptionOf, RecomputeNewIndex, DirectionLabel) { Kind = _newKind }; @@ -331,7 +373,9 @@ private void RenumberKeys() { for (int i = 0; i < NewKeys.Count; i++) { + NewKeys[i].Number = i + 1; NewKeys[i].IsFirst = i == 0; + NewKeys[i].IsLast = i == NewKeys.Count - 1; } } @@ -341,9 +385,8 @@ internal string DirectionLabel(BsonValue value) => ? Loc[value.ToDouble() < 0 ? "Design_DirDesc" : "Design_DirAsc"] : value.ToString() ?? ""; - /// 字段 → 类型色(抽样里没有就是灰的)。 - private string TokenOf(string field) => - _fieldOptions.FirstOrDefault(f => f.Path == field)?.Token ?? "VelaTextMuted"; + /// 字段 → 抽样到的那一项;抽样里没有为 。 + private FieldOption? OptionOf(string field) => _fieldOptions.FirstOrDefault(f => f.Path == field); /// ESR 角色 → 小标签文字。 private string RoleText(string? role) => role switch @@ -492,15 +535,35 @@ private void RecomputeNewIndex() NewCommand = key.ElementCount == 0 ? "" : $"{ShellRef}.createIndex(\n {BsonText.Literal(key)}" + (options.ElementCount > 0 ? $",\n {BsonText.Literal(options)}" : "") + "\n)"; - UpdateImpact(key); + long size = UpdateImpact(key); + DraftIndex = key.ElementCount == 0 ? null : Draft(key, options, size); CreateIndexCommand?.RaiseCanExecuteChanged(); } + /// 草稿行:与表里的行同一个模型,大小列写预估,使用列写「待创建」。 + private IndexRow Draft(BsonDocument key, BsonDocument options, long size) + { + string name = _newName.Trim().Length > 0 ? _newName.Trim() : IndexAdvisor.DefaultName(key); + var spec = new BsonDocument { { "key", key }, { "name", name } }; + spec.Merge(options, overwriteExistingElements: false); + IndexForm form = IndexAdvisor.ShapeOf(spec); + return new IndexRow + { + Name = name, + Spec = spec, + Keys = [.. IndexAdvisor.DisplayKeys(spec).Select(static k => new IndexKeyChip(k.Field, k.Value))], + Form = form, + TypeText = FormText(form), + Attributes = Attributes(spec, 0), + SizeText = "≈ " + BsonText.Bytes(size) + }; + } + /// /// 预估:大小 ≈ 文档数 ×(各键字段平均值长 + 每项开销),构建耗时按数据量粗估; - /// 再写一句当前版本的构建方式(4.4 起的优化构建只在首尾短暂持排他锁)。 + /// 再写一句当前版本的构建方式(4.4 起的优化构建只在首尾短暂持排他锁)。返回预估大小(字节)。 /// - private void UpdateImpact(BsonDocument key) + private long UpdateImpact(BsonDocument key) { long count = _indexStats?.Count ?? _stats?.Count ?? 0; long data = _indexStats?.Size ?? _stats?.Size ?? 0; @@ -525,6 +588,7 @@ private void UpdateImpact(BsonDocument key) detail += " " + Loc["Design_ImpactPartial"]; } NewImpactDetail = detail; + return size; } /// diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabView.axaml b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabView.axaml index 2c35a2d..a2af11a 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabView.axaml +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabView.axaml @@ -121,7 +121,14 @@ - + + + + + + + + + + + + @@ -366,9 +385,9 @@ - - diff --git a/tests/VelaShell.Plugin.Mongo.Tests/QueryTests.cs b/tests/VelaShell.Plugin.Mongo.Tests/QueryTests.cs index 111528b..cd64da8 100644 --- a/tests/VelaShell.Plugin.Mongo.Tests/QueryTests.cs +++ b/tests/VelaShell.Plugin.Mongo.Tests/QueryTests.cs @@ -1,5 +1,8 @@ +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Headless; +using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.VisualTree; using MongoDB.Bson; @@ -66,6 +69,56 @@ private static async Task WaitAsync(Func condition, int rounds = 300) Assert.IsTrue(condition(), "timed out waiting for the condition"); } + /// + /// 有未保存修改的标签照样能关:平时标签上是橙点、× 藏着,鼠标移到标签上换成 ×; + /// 点 × 先弹「放弃未保存的修改?」—— 取消就留着,确认「放弃并关闭」就关掉。 + /// + [TestMethod] + public void Modified_tab_closes_after_confirming_discard() => Screens.OnUi(async () => + { + await TestServer.RequireAsync(); + await using Workbench bench = await Screens.OpenWorkbenchAsync(); + bench.Session.OpenQuery("shop", "db.orders.find({})"); + await Screens.PumpAsync(); + QueryTabViewModel tab = bench.ViewModel.Tabs.OfType().Last(); + tab.Text += " "; + await Screens.PumpAsync(5); + Assert.IsTrue(tab.IsModified); + + ListBox strip = bench.View.GetVisualDescendants().OfType().Single(static l => l.Name == "TabStrip"); + var item = (ListBoxItem)strip.ContainerFromItem(tab)!; + Button close = item.GetVisualDescendants().OfType