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:
parent
12fec968fe
commit
88ad579a27
|
|
@ -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())? {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -758,6 +758,8 @@ export default {
|
|||
edit: "Edit",
|
||||
documentView: "Document View",
|
||||
tableView: "Table View",
|
||||
filterPlaceholder: "Filter...",
|
||||
sortPlaceholder: "Sort...",
|
||||
},
|
||||
history: {
|
||||
title: "History",
|
||||
|
|
|
|||
|
|
@ -743,6 +743,8 @@ export default {
|
|||
edit: "编辑",
|
||||
documentView: "文档视图",
|
||||
tableView: "表格视图",
|
||||
filterPlaceholder: "过滤条件...",
|
||||
sortPlaceholder: "排序条件...",
|
||||
},
|
||||
history: {
|
||||
title: "历史",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue