feat(mongo): add filter/sort support and fix table view editing

- Add filter and sort parameters to MongoDB find_documents across the
  full stack (driver, ops, tauri cmd, web routes, frontend API)
- Add find/sort input bar to MongoDocBrowser table view via DataGrid
  search-bar slot, matching WHERE/ORDER BY positioning
- Fix table view editing: allow cell editing when customSave is provided
  (bypasses primary key requirement for MongoDB)
- Fix replace_one WriteError 66 by removing _id from replacement doc
- Align action buttons (refresh, add row, commit) to the right in grid
This commit is contained in:
t8y2 2026-05-14 14:43:26 +08:00
parent 12fec968fe
commit 88ad579a27
10 changed files with 121 additions and 13 deletions

View File

@ -41,12 +41,31 @@ pub async fn find_documents(
collection: &str,
skip: u64,
limit: i64,
filter: Option<&str>,
sort: Option<&str>,
) -> Result<MongoDocumentResult, String> {
let col = client.database(database).collection::<Document>(collection);
let total = col.count_documents(doc! {}).await.map_err(|e| e.to_string())?;
let filter_doc: Document = match filter {
Some(f) if !f.trim().is_empty() => {
let json: serde_json::Value = serde_json::from_str(f).map_err(|e| format!("Invalid filter JSON: {e}"))?;
mongodb::bson::to_document(&json).map_err(|e| format!("Invalid filter: {e}"))?
}
_ => doc! {},
};
let mut cursor = col.find(doc! {}).skip(skip).limit(limit).await.map_err(|e| e.to_string())?;
let total = col.count_documents(filter_doc.clone()).await.map_err(|e| e.to_string())?;
let mut find = col.find(filter_doc).skip(skip).limit(limit);
if let Some(s) = sort {
if !s.trim().is_empty() {
let json: serde_json::Value = serde_json::from_str(s).map_err(|e| format!("Invalid sort JSON: {e}"))?;
let sort_doc = mongodb::bson::to_document(&json).map_err(|e| format!("Invalid sort: {e}"))?;
find = find.sort(sort_doc);
}
}
let mut cursor = find.await.map_err(|e| e.to_string())?;
let mut documents = Vec::new();
while cursor.advance().await.map_err(|e| e.to_string())? {

View File

@ -31,10 +31,14 @@ pub async fn mongo_find_documents_core(
collection: &str,
skip: u64,
limit: i64,
filter: Option<&str>,
sort: Option<&str>,
) -> Result<MongoDocumentResult, String> {
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::find_documents(client, database, collection, skip, limit).await,
PoolKind::MongoDb(client) => {
mongo_driver::find_documents(client, database, collection, skip, limit, filter, sort).await
}
PoolKind::Elasticsearch(client) => {
let client = client.clone();
drop(connections);

View File

@ -29,8 +29,20 @@ pub async fn mongo_find_documents(
collection: String,
skip: u64,
limit: i64,
filter: Option<String>,
sort: Option<String>,
) -> Result<MongoDocumentResult, String> {
dbx_core::mongo_ops::mongo_find_documents_core(&state, &connection_id, &database, &collection, skip, limit).await
dbx_core::mongo_ops::mongo_find_documents_core(
&state,
&connection_id,
&database,
&collection,
skip,
limit,
filter.as_deref(),
sort.as_deref(),
)
.await
}
#[tauri::command]

View File

@ -28,6 +28,8 @@ pub struct MongoFindRequest {
pub collection: String,
pub skip: Option<u64>,
pub limit: Option<i64>,
pub filter: Option<String>,
pub sort: Option<String>,
}
#[derive(Deserialize)]
@ -88,6 +90,8 @@ pub async fn find_documents(
&req.collection,
req.skip.unwrap_or(0),
req.limit.unwrap_or(50),
req.filter.as_deref(),
req.sort.as_deref(),
)
.await
.map_err(AppError)?;

View File

@ -829,8 +829,10 @@ const tableUsesSyntheticRowId = computed(() =>
usesSyntheticRowIdKey(props.databaseType, props.tableMeta?.primaryKeys ?? []),
);
const hiveTableTransactional = ref<boolean | undefined>(undefined);
const canEditExistingRows = computed(() =>
canEditExistingTableRows(props.databaseType, hiveTableTransactional.value, props.tableMeta?.primaryKeys ?? []),
const canEditExistingRows = computed(
() =>
!!props.customSave ||
canEditExistingTableRows(props.databaseType, hiveTableTransactional.value, props.tableMeta?.primaryKeys ?? []),
);
watch(
() => [props.databaseType, props.connectionId, props.database, props.tableMeta?.schema, props.tableMeta?.tableName],
@ -2081,7 +2083,9 @@ defineExpose({
</div>
</template>
<div class="flex shrink-0 items-center gap-1 px-1">
<slot name="search-bar" />
<div class="flex shrink-0 items-center gap-1 px-1 ml-auto">
<Button
variant="ghost"
size="sm"

View File

@ -2,7 +2,7 @@
import { computed, ref, onMounted } from "vue";
import { uuid } from "@/lib/utils";
import { useI18n } from "vue-i18n";
import { RefreshCw, Trash2, Plus, Save, ChevronLeft, ChevronRight, Table2, Braces } from "lucide-vue-next";
import { RefreshCw, Trash2, Plus, Save, ChevronLeft, ChevronRight, Table2, Braces, X } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
@ -37,6 +37,8 @@ const error = ref("");
const editFields = ref<EditNode[]>([]);
const showDeleteConfirm = ref(false);
const viewMode = ref<"document" | "table">("document");
const filterInput = ref("");
const sortInput = ref("");
type PendingDelete = { kind: "document"; index: number } | { kind: "field"; index: number; name: string };
@ -145,12 +147,16 @@ async function load() {
loading.value = true;
error.value = "";
try {
const filter = filterInput.value.trim() || undefined;
const sort = sortInput.value.trim() || undefined;
const result = await api.mongoFindDocuments(
props.connectionId,
props.database,
props.collection,
page.value * pageSize,
pageSize,
filter,
sort,
);
documents.value = result.documents.map(asRecord);
total.value = result.total;
@ -161,6 +167,11 @@ async function load() {
}
}
function applyFilter() {
page.value = 0;
load();
}
function asRecord(value: unknown): JsonRecord {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as JsonRecord;
@ -440,8 +451,7 @@ onMounted(load);
</Button>
</div>
<span>{{ t("mongo.documents", { count: total }) }}</span>
<span class="flex-1" />
<span class="shrink-0 ml-1">{{ t("mongo.documents", { count: total }) }}</span>
<Button v-if="viewMode === 'document'" variant="ghost" size="icon" class="h-5 w-5" @click="startNew"
><Plus class="h-3 w-3"
@ -476,7 +486,54 @@ onMounted(load);
editable
:custom-save="gridSave"
@reload="load"
/>
>
<template #search-bar>
<div class="flex-1 flex items-center gap-1 px-2 py-0.5 border-l min-w-0">
<span class="text-blue-600 dark:text-blue-400 text-xs font-medium select-none shrink-0">find</span>
<input
v-model="filterInput"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="flex-1 h-5 min-w-0 text-xs bg-transparent outline-none placeholder:text-muted-foreground/60 font-mono"
placeholder="{}"
@keydown.enter="applyFilter"
/>
<button
v-if="filterInput.trim()"
class="text-muted-foreground hover:text-foreground shrink-0"
@click="
filterInput = '';
applyFilter();
"
>
<X class="w-3 h-3" />
</button>
</div>
<div class="flex items-center gap-1 px-2 py-0.5 border-l border-r min-w-0" style="flex: 0.6">
<span class="text-orange-600 dark:text-orange-400 text-xs font-medium select-none shrink-0">sort</span>
<input
v-model="sortInput"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="flex-1 h-5 min-w-0 text-xs bg-transparent outline-none placeholder:text-muted-foreground/60 font-mono"
placeholder="{}"
@keydown.enter="applyFilter"
/>
<button
v-if="sortInput.trim()"
class="text-muted-foreground hover:text-foreground shrink-0"
@click="
sortInput = '';
applyFilter();
"
>
<X class="w-3 h-3" />
</button>
</div>
</template>
</DataGrid>
<!-- Document view (split pane) -->
<Splitpanes v-else class="flex-1 min-h-0">

View File

@ -758,6 +758,8 @@ export default {
edit: "Edit",
documentView: "Document View",
tableView: "Table View",
filterPlaceholder: "Filter...",
sortPlaceholder: "Sort...",
},
history: {
title: "History",

View File

@ -743,6 +743,8 @@ export default {
edit: "编辑",
documentView: "文档视图",
tableView: "表格视图",
filterPlaceholder: "过滤条件...",
sortPlaceholder: "排序条件...",
},
history: {
title: "历史",

View File

@ -682,8 +682,10 @@ export async function mongoFindDocuments(
collection: string,
skip: number,
limit: number,
filter?: string,
sort?: string,
): Promise<MongoDocumentResult> {
return post("/api/mongo/find-documents", { connectionId, database, collection, skip, limit });
return post("/api/mongo/find-documents", { connectionId, database, collection, skip, limit, filter, sort });
}
export async function mongoInsertDocument(

View File

@ -516,8 +516,10 @@ export async function mongoFindDocuments(
collection: string,
skip: number,
limit: number,
filter?: string,
sort?: string,
): Promise<MongoDocumentResult> {
return invoke("mongo_find_documents", { connectionId, database, collection, skip, limit });
return invoke("mongo_find_documents", { connectionId, database, collection, skip, limit, filter, sort });
}
export async function mongoInsertDocument(