fix(document): surface filter rejection reason in builder

This commit is contained in:
Loc Nguyen authored and GitHub committed 2026-09-24 14:09:08 +08:00
1 parent d854fa2dfe
commit f7688794f8
2 files changed
+59 -1

No files matched your search

@@ -321,6 +321,9 @@ type DocumentGridChanges = {
rows: MongoInputValue[][];
};
const documentFilterBuilderOpen = ref(false);
/// Why an Apply click did not filter. The shared `error` banner lives in the document pane, which
/// the open filter popover covers, so a rejected rule there reads as a dead button.
const documentFilterBuilderError = ref("");
const documentFilterFieldPopoverOpen = ref<Record<string, boolean>>({});
const documentFilterFieldSearch = ref<Record<string, string>>({});
const documentFilterRules = ref<DocumentFilterRule[]>(restoredDocumentBrowserState?.documentFilterRules ?? []);
@@ -908,12 +911,14 @@ function documentFilterFieldKindLabel(kind: DocumentFieldPathNode["kind"]): stri
}
function removeDocumentFilterRule(ruleId: string) {
documentFilterBuilderError.value = "";
documentFilterRules.value = documentFilterRules.value.filter((rule) => rule.id !== ruleId);
setDocumentFilterFieldPopoverOpen(ruleId, false);
if (documentFilterRules.value.length === 0) appliedDocumentFilter.value = null;
}
function updateDocumentFilterRule(ruleId: string, patch: Partial<DocumentFilterRule>) {
documentFilterBuilderError.value = "";
documentFilterRules.value = documentFilterRules.value.map((rule) => {
if (rule.id !== ruleId) return rule;
const next = { ...rule, ...patch };
@@ -947,6 +952,7 @@ function elasticsearchQueryTypeLabel(queryType: ElasticsearchQueryType): string
}
function resetDocumentFilterBuilder() {
documentFilterBuilderError.value = "";
appliedDocumentFilter.value = null;
documentFilterFieldPopoverOpen.value = {};
documentFilterFieldSearch.value = {};
@@ -1399,9 +1405,10 @@ async function applyDocumentStructuredFilters() {
}))
.filter((item): item is { rule: DocumentFilterRule; condition: Record<string, unknown> } => !!item.condition);
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
documentFilterBuilderError.value = e instanceof Error ? e.message : String(e);
return;
}
documentFilterBuilderError.value = "";
error.value = "";
const structured = combineDocumentFilterConditions(
items.map((item) => item.condition),
@@ -3186,6 +3193,10 @@ defineExpose({ focusSearch });
{{ t("grid.filterBuilderEmpty") }}
</div>
<p v-if="documentFilterBuilderError" data-document-filter-builder-error class="text-xs text-destructive">
{{ documentFilterBuilderError }}
</p>
<div class="flex items-center justify-between gap-2">
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="addDocumentFilterRule">
<Plus class="mr-1 h-3.5 w-3.5" />
@@ -1052,6 +1052,53 @@ describe("DocumentBrowser MongoDB filter value types", () => {
expect(JSON.parse(filter)).toEqual({ _id: "1" });
});
it("keeps the filter builder open and says why a rejected rule did not apply", async () => {
app?.unmount();
backend.documentFindDocuments.mockReset();
backend.documentFindDocuments.mockResolvedValue({
documents: [{ _id: 1, title: "Numeric id" }],
raw_documents: [],
total: 1,
total_is_exact: true,
});
app = createApp(DocumentBrowser, {
connectionId: "mongo-1",
database: "test",
collection: "numeric_ids",
databaseType: "mongodb",
});
app.mount(root!);
await flushUi();
root!.querySelector<HTMLButtonElement>('[data-testid="data-grid"] button')!.click();
await flushUi();
const valueInput = document.body.querySelector<HTMLInputElement>('input[placeholder="grid.filterBuilderValue"]')!;
valueInput.value = "abc";
valueInput.dispatchEvent(new Event("input", { bubbles: true }));
await flushUi();
const callsBeforeApply = backend.documentFindDocuments.mock.calls.length;
buttonWithText("grid.applyFilter").click();
await flushUi();
// The shared error banner sits in the document pane, which this popover covers, so the
// reason has to appear inside the builder or the click reads as a dead button.
expect(document.body.querySelector("[data-document-filter-builder-error]")?.textContent).toContain("number");
expect(document.body.querySelector('[data-testid="popover-content"]')).not.toBeNull();
expect(backend.documentFindDocuments.mock.calls.length).toBe(callsBeforeApply);
// Correcting the rule drops the stale message.
valueInput.value = "1";
valueInput.dispatchEvent(new Event("input", { bubbles: true }));
await flushUi();
expect(document.body.querySelector("[data-document-filter-builder-error]")).toBeNull();
buttonWithText("grid.applyFilter").click();
await flushUi();
expect(JSON.parse(backend.documentFindDocuments.mock.calls.at(-1)?.[5])).toEqual({ _id: 1 });
});
it("exposes the applicable shared table view options", async () => {
app?.unmount();
app = createApp(DocumentBrowser, {