feat(grid): support applying a single structured filter condition
This commit is contained in:
19 files changed
+143
-6
No files matched your search
@@ -1595,13 +1595,47 @@ function buildGroupedWhere(conditions: string[], rules: StructuredFilterRule[]):
|
||||
return result;
|
||||
}
|
||||
|
||||
async function applyStructuredFilters() {
|
||||
if (!canUseWhereSearch.value) return;
|
||||
appliedStructuredWhereInput.value = await buildStructuredWhereFromRules(structuredFilterRules.value);
|
||||
async function applyStructuredWhere(where: string) {
|
||||
appliedStructuredWhereInput.value = where;
|
||||
if (!isFilterEditorPinnedOpen.value) filterBuilderOpen.value = false;
|
||||
await applyWhereFilter();
|
||||
}
|
||||
|
||||
async function applyStructuredFilters() {
|
||||
if (!canUseWhereSearch.value) return;
|
||||
await applyStructuredWhere(await buildStructuredWhereFromRules(structuredFilterRules.value));
|
||||
}
|
||||
|
||||
const applyingOnlyStructuredFilter = ref(false);
|
||||
async function applyOnlyStructuredFilter(ruleId: string) {
|
||||
if (!canUseWhereSearch.value || applyingOnlyStructuredFilter.value || isApplyingWhere.value) return;
|
||||
const rule = structuredFilterRules.value.find((item) => item.id === ruleId);
|
||||
if (!rule) return;
|
||||
if (!rule.columnName || !isStructuredFilterRuleComplete(rule)) {
|
||||
toast(t("grid.filterBuilderCompleteRuleFirst"));
|
||||
return;
|
||||
}
|
||||
applyingOnlyStructuredFilter.value = true;
|
||||
const scopeKey = structuredFilterScopeKey.value;
|
||||
const cacheKey = structuredFilterCacheKey.value;
|
||||
const rulesSnapshot = JSON.stringify(structuredFilterRules.value);
|
||||
try {
|
||||
// Build before changing enabled states so an invalid condition cannot clear the filter.
|
||||
const where = await buildStructuredWhereFromRules([{ ...rule, disabled: false }]);
|
||||
if (scopeKey !== structuredFilterScopeKey.value || cacheKey !== structuredFilterCacheKey.value || rulesSnapshot !== JSON.stringify(structuredFilterRules.value)) return;
|
||||
if (!where) {
|
||||
toast(t("grid.filterBuilderCompleteRuleFirst"));
|
||||
return;
|
||||
}
|
||||
filterBuilder.enableOnlyRule(ruleId);
|
||||
await applyStructuredWhere(where);
|
||||
} catch (error: unknown) {
|
||||
toast(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
applyingOnlyStructuredFilter.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
let structuredFilterPreviewRequestId = 0;
|
||||
async function refreshStructuredFilterPreview() {
|
||||
const requestId = ++structuredFilterPreviewRequestId;
|
||||
@@ -11521,6 +11555,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
@ensure-rule="ensureStructuredFilterRule"
|
||||
@add-rule="addStructuredFilterRule"
|
||||
@apply-filters="applyStructuredFilters"
|
||||
:apply-only-busy="applyingOnlyStructuredFilter || isApplyingWhere"
|
||||
@apply-only="applyOnlyStructuredFilter"
|
||||
@reset-filters="resetStructuredFilters"
|
||||
@clear-filters="clearAllFilters"
|
||||
@remove-rule="removeStructuredFilterRule"
|
||||
@@ -11659,6 +11695,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
@ensure-rule="ensureStructuredFilterRule"
|
||||
@add-rule="addStructuredFilterRule"
|
||||
@apply="applyStructuredFilters"
|
||||
:apply-only-busy="applyingOnlyStructuredFilter || isApplyingWhere"
|
||||
@apply-only="applyOnlyStructuredFilter"
|
||||
@reset="resetStructuredFilters"
|
||||
@clear="clearAllFilters"
|
||||
@copy-sql="copyFilterSqlPreview"
|
||||
@@ -11681,6 +11719,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
@ensure-rule="ensureStructuredFilterRule"
|
||||
@add-rule="addStructuredFilterRule"
|
||||
@apply="applyStructuredFilters"
|
||||
:apply-only-busy="applyingOnlyStructuredFilter || isApplyingWhere"
|
||||
@apply-only="applyOnlyStructuredFilter"
|
||||
@reset="resetStructuredFilters"
|
||||
@clear="clearAllFilters"
|
||||
@copy-sql="copyFilterSqlPreview"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { Check, Eye, EyeOff, GripVertical, Plus, Search, Trash2, X } from "@lucide/vue";
|
||||
import { Check, Eye, EyeOff, Focus, GripVertical, Plus, Search, Trash2, X } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -32,6 +32,8 @@ const props = withDefaults(
|
||||
modeOptions: Array<{ value: DataGridContextFilterMode; labelKey: string }>;
|
||||
columnSearch: string;
|
||||
disabled?: boolean;
|
||||
showApplyOnly?: boolean;
|
||||
applyOnlyBusy?: boolean;
|
||||
showHeader?: boolean;
|
||||
showFooter?: boolean;
|
||||
layout?: "popover" | "panel" | "text";
|
||||
@@ -41,6 +43,7 @@ const props = withDefaults(
|
||||
const emit = defineEmits<{
|
||||
add: [];
|
||||
apply: [];
|
||||
applyOnly: [id: string];
|
||||
reset: [];
|
||||
clear: [];
|
||||
remove: [id: string];
|
||||
@@ -707,6 +710,9 @@ function blurValueRule(id: string) {
|
||||
{{ t("grid.filterBuilderValueShortcutHint") }}
|
||||
</div>
|
||||
<div class="flex items-center gap-0.5" :class="props.layout === 'text' ? 'col-start-6 row-start-1' : usesExpandedLayout(rule.mode) ? 'col-start-5 row-start-1 row-span-2' : 'col-start-5 row-start-1'">
|
||||
<Button v-if="props.showApplyOnly" variant="ghost" size="icon" class="h-7 w-7" :disabled="props.disabled || props.applyOnlyBusy" :title="t('grid.filterBuilderApplyOnly')" :aria-label="t('grid.filterBuilderApplyOnly')" @click="emit('applyOnly', rule.id)"
|
||||
><Focus class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<template v-if="props.layout === 'text'">
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6" :aria-label="t('grid.filterBuilderAddRule')" @click="emit('add')"><Plus class="h-3.5 w-3.5" /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6" :disabled="props.rules.length === 1" @click="emit('remove', rule.id)"><X class="h-3.5 w-3.5" /></Button>
|
||||
|
||||
@@ -14,6 +14,7 @@ const props = defineProps<{
|
||||
filteredColumns: string[];
|
||||
modeOptions: Array<{ value: DataGridContextFilterMode; labelKey: string }>;
|
||||
columnSearch: string;
|
||||
applyOnlyBusy?: boolean;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
@@ -21,6 +22,7 @@ const emit = defineEmits<{
|
||||
"update:columnSearch": [value: string];
|
||||
ensureRule: [];
|
||||
addRule: [];
|
||||
applyOnly: [id: string];
|
||||
apply: [];
|
||||
reset: [];
|
||||
clear: [];
|
||||
@@ -55,6 +57,8 @@ watch(
|
||||
<DataGridFilterBuilder
|
||||
class="min-w-[560px]"
|
||||
:rules="rules"
|
||||
:show-apply-only="true"
|
||||
:apply-only-busy="applyOnlyBusy"
|
||||
:columns="columns"
|
||||
:filtered-columns="filteredColumns"
|
||||
:mode-options="modeOptions"
|
||||
@@ -64,6 +68,7 @@ watch(
|
||||
:show-header="false"
|
||||
:show-footer="false"
|
||||
@add="emit('addRule')"
|
||||
@apply-only="emit('applyOnly', $event)"
|
||||
@apply="emit('apply')"
|
||||
@reset="emit('reset')"
|
||||
@clear="emit('clear')"
|
||||
|
||||
@@ -41,6 +41,7 @@ const props = defineProps<{
|
||||
filteredColumns: string[];
|
||||
modeOptions: Array<{ value: DataGridContextFilterMode; labelKey: string }>;
|
||||
columnSearch: string;
|
||||
applyOnlyBusy?: boolean;
|
||||
applyWhere: (value?: string) => void | boolean | Promise<void | boolean>;
|
||||
applyOrderBy: (value?: string) => void | boolean | Promise<void | boolean>;
|
||||
clearOrderBy: () => void | Promise<void>;
|
||||
@@ -53,6 +54,7 @@ const emit = defineEmits<{
|
||||
"update:columnSearch": [value: string];
|
||||
ensureRule: [];
|
||||
addRule: [];
|
||||
applyOnly: [id: string];
|
||||
applyFilters: [];
|
||||
resetFilters: [];
|
||||
clearFilters: [];
|
||||
@@ -202,6 +204,8 @@ onUnmounted(onResizeEnd);
|
||||
<DataGridFilterBuilder
|
||||
ref="filterBuilderRef"
|
||||
:rules="rules"
|
||||
:show-apply-only="true"
|
||||
:apply-only-busy="applyOnlyBusy"
|
||||
:columns="[...columns]"
|
||||
:filtered-columns="filteredColumns"
|
||||
:mode-options="modeOptions"
|
||||
@@ -209,6 +213,7 @@ onUnmounted(onResizeEnd);
|
||||
:disabled="!canUseWhereSearch"
|
||||
:show-header="false"
|
||||
@add="emit('addRule')"
|
||||
@apply-only="emit('applyOnly', $event)"
|
||||
@apply="emit('applyFilters')"
|
||||
@reset="emit('resetFilters')"
|
||||
@clear="emit('clearFilters')"
|
||||
|
||||
@@ -17,6 +17,7 @@ const props = defineProps<{
|
||||
filteredColumns: string[];
|
||||
modeOptions: Array<{ value: DataGridContextFilterMode; labelKey: string }>;
|
||||
columnSearch: string;
|
||||
applyOnlyBusy?: boolean;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
@@ -25,6 +26,7 @@ const emit = defineEmits<{
|
||||
"update:columnSearch": [value: string];
|
||||
ensureRule: [];
|
||||
addRule: [];
|
||||
applyOnly: [id: string];
|
||||
apply: [];
|
||||
reset: [];
|
||||
clear: [];
|
||||
@@ -144,6 +146,8 @@ watch(
|
||||
<DataGridFilterBuilder
|
||||
class="min-w-[520px]"
|
||||
:rules="rules"
|
||||
:show-apply-only="true"
|
||||
:apply-only-busy="applyOnlyBusy"
|
||||
:columns="columns"
|
||||
:filtered-columns="filteredColumns"
|
||||
:mode-options="modeOptions"
|
||||
@@ -153,6 +157,7 @@ watch(
|
||||
:show-header="false"
|
||||
:show-footer="false"
|
||||
@add="emit('addRule')"
|
||||
@apply-only="emit('applyOnly', $event)"
|
||||
@apply="emit('apply')"
|
||||
@reset="emit('reset')"
|
||||
@clear="emit('clear')"
|
||||
|
||||
@@ -28,6 +28,7 @@ vi.mock("@lucide/vue", async () => {
|
||||
ChevronsRight: icon,
|
||||
Download: icon,
|
||||
Filter: icon,
|
||||
Focus: icon,
|
||||
FileDiff: icon,
|
||||
Loader2: icon,
|
||||
FileUp: icon,
|
||||
@@ -618,6 +619,34 @@ describe("DataGridColumnHeader", () => {
|
||||
});
|
||||
|
||||
describe("DataGridFilterBuilder", () => {
|
||||
it.each(["popover", "panel", "text"])("offers apply-only for disabled rules in the %s layout only when enabled", async (layout) => {
|
||||
const applyOnly = vi.fn();
|
||||
const mounted = mountComponent(DataGridFilterBuilder, {
|
||||
rules: [{ id: "r1", columnName: "id", mode: "equals", rawValue: "7", rawEndValue: "", conjunction: "AND", disabled: true }],
|
||||
columns: ["id"],
|
||||
filteredColumns: ["id"],
|
||||
modeOptions: [{ value: "equals", labelKey: "equals" }],
|
||||
columnSearch: "",
|
||||
layout,
|
||||
onApplyOnly: applyOnly,
|
||||
});
|
||||
const buttons = () => findAll(mounted.root, (node) => node.props["aria-label"] === "grid.filterBuilderApplyOnly");
|
||||
expect(buttons()).toHaveLength(0);
|
||||
await mounted.setProps({ showApplyOnly: true });
|
||||
expect(buttons()).toHaveLength(1);
|
||||
expect(buttons()[0].props.disabled).toBeFalsy();
|
||||
dispatch(buttons()[0], "click");
|
||||
expect(applyOnly).toHaveBeenCalledWith("r1");
|
||||
await mounted.setProps({ applyOnlyBusy: true });
|
||||
expect(buttons()[0].props.disabled).toBe(true);
|
||||
await mounted.setProps({ applyOnlyBusy: false });
|
||||
expect(buttons()[0].props.disabled).toBeFalsy();
|
||||
dispatch(buttons()[0], "click");
|
||||
expect(applyOnly).toHaveBeenCalledTimes(2);
|
||||
await mounted.setProps({ disabled: true });
|
||||
expect(buttons()[0].props.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("renders a compact text rule without framed form controls", async () => {
|
||||
const updateRule = vi.fn();
|
||||
const add = vi.fn();
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import type { DataGridStructuredFilterRule } from "@/composables/useDataGridFilt
|
||||
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) }));
|
||||
vi.mock("@lucide/vue", () => {
|
||||
const icon = defineComponent({ setup: () => () => h("span") });
|
||||
return { Check: icon, Eye: icon, EyeOff: icon, GripVertical: icon, Plus: icon, Search: icon, Trash2: icon, X: icon };
|
||||
return { Check: icon, Eye: icon, EyeOff: icon, Focus: icon, GripVertical: icon, Plus: icon, Search: icon, Trash2: icon, X: icon };
|
||||
});
|
||||
|
||||
function passthrough(tag = "div") {
|
||||
|
||||
Reference in new issue
Block a user