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") {
|
||||
|
||||
@@ -42,6 +42,29 @@ describe("useDataGridFilterBuilder", () => {
|
||||
expect(await builder.apply()).toBe("(id = '1') OR (name = 'Alice')");
|
||||
});
|
||||
|
||||
it("applies only a previously disabled rule while retaining the other conditions", async () => {
|
||||
const builder = useDataGridFilterBuilder({
|
||||
columns: ["id", "method"],
|
||||
isComplete: () => true,
|
||||
buildCondition: async (rule) => `${rule.columnName} = '${rule.rawValue}'`,
|
||||
});
|
||||
builder.rules.value = [
|
||||
{ id: "id-rule", columnName: "id", mode: "equals", rawValue: "1", rawEndValue: "", conjunction: "AND" },
|
||||
{ id: "method-rule", columnName: "method", mode: "equals", rawValue: "POST", rawEndValue: "", conjunction: "OR", disabled: true },
|
||||
];
|
||||
const originalRules = builder.rules.value.map((rule) => ({ ...rule }));
|
||||
builder.enableOnlyRule("method-rule");
|
||||
expect(await builder.buildWhere()).toBe("method = 'POST'");
|
||||
expect(builder.activeCount.value).toBe(1);
|
||||
expect(builder.rules.value).toEqual(originalRules.map((rule) => ({ ...rule, disabled: rule.id !== "method-rule" })));
|
||||
|
||||
builder.enableOnlyRule("missing");
|
||||
builder.enableOnlyRule("method-rule");
|
||||
expect(await builder.buildWhere()).toBe("method = 'POST'");
|
||||
builder.updateRule("id-rule", { disabled: false });
|
||||
expect(await builder.buildWhere()).toBe("(id = '1') OR (method = 'POST')");
|
||||
});
|
||||
|
||||
it("groups conditions in rule order", () => {
|
||||
const rule = (id: string, conjunction: "AND" | "OR"): DataGridStructuredFilterRule => ({ id, columnName: id, mode: "equals", rawValue: id, rawEndValue: "", conjunction });
|
||||
expect(
|
||||
|
||||
@@ -102,6 +102,10 @@ export function useDataGridFilterBuilder(options: UseDataGridFilterBuilderOption
|
||||
return next;
|
||||
});
|
||||
}
|
||||
function enableOnlyRule(id: string) {
|
||||
if (!rules.value.some((rule) => rule.id === id)) return;
|
||||
rules.value = rules.value.map((rule) => ({ ...rule, disabled: rule.id !== id }));
|
||||
}
|
||||
function moveRule(id: string, targetIndex: number) {
|
||||
rules.value = moveDataGridStructuredFilterRule(rules.value, id, targetIndex);
|
||||
}
|
||||
@@ -132,5 +136,5 @@ export function useDataGridFilterBuilder(options: UseDataGridFilterBuilderOption
|
||||
},
|
||||
);
|
||||
|
||||
return { rules, open, columnSearch, appliedWhereInput, filteredColumns, activeCount, defaultRule, ensureRule, addRule, removeRule, updateRule, moveRule, reset, buildWhere, apply };
|
||||
return { rules, open, columnSearch, appliedWhereInput, filteredColumns, activeCount, defaultRule, ensureRule, addRule, removeRule, updateRule, enableOnlyRule, moveRule, reset, buildWhere, apply };
|
||||
}
|
||||
@@ -1815,6 +1815,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "SQL şərti kopyalandı",
|
||||
filterBuilderSummary: "{count} qayda",
|
||||
filterBuilderAddRule: "Qayda əlavə et",
|
||||
filterBuilderApplyOnly: "Yalnız bu qayda ilə filtrlə",
|
||||
filterBuilderCompleteRuleFirst: "Əvvəlcə bu filtr qaydasını tamamlayın",
|
||||
filterBuilderEnableRule: "Qaydanı aktivləşdir",
|
||||
filterBuilderDisableRule: "Qaydanı deaktiv et",
|
||||
filterBuilderReorderRule: "Şərtin sırasını dəyişmək üçün sürükləyin və ya ox düymələrindən istifadə edin",
|
||||
|
||||
@@ -1816,6 +1816,8 @@ export default {
|
||||
filterSqlCopied: "SQL condition copied",
|
||||
filterBuilderSummary: "{count} rules",
|
||||
filterBuilderAddRule: "Add rule",
|
||||
filterBuilderApplyOnly: "Filter only this rule",
|
||||
filterBuilderCompleteRuleFirst: "Complete this filter rule first",
|
||||
filterBuilderEnableRule: "Enable rule",
|
||||
filterBuilderDisableRule: "Disable rule",
|
||||
filterBuilderReorderRule: "Drag or use arrow keys to reorder condition",
|
||||
|
||||
@@ -1689,6 +1689,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "Condición SQL copiada",
|
||||
filterBuilderSummary: "{count} reglas",
|
||||
filterBuilderAddRule: "Agregar regla",
|
||||
filterBuilderApplyOnly: "Filtrar solo por esta regla",
|
||||
filterBuilderCompleteRuleFirst: "Completa primero esta regla de filtro",
|
||||
filterBuilderEnableRule: "Activar regla",
|
||||
filterBuilderDisableRule: "Desactivar regla",
|
||||
filterBuilderReorderRule: "Arrastra o usa las flechas para reordenar la condición",
|
||||
|
||||
@@ -1687,6 +1687,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "Condizione SQL copiata",
|
||||
filterBuilderSummary: "{count} regole",
|
||||
filterBuilderAddRule: "Aggiungi regola",
|
||||
filterBuilderApplyOnly: "Filtra solo per questa regola",
|
||||
filterBuilderCompleteRuleFirst: "Completa prima questa regola di filtro",
|
||||
filterBuilderEnableRule: "Abilita regola",
|
||||
filterBuilderDisableRule: "Disabilita regola",
|
||||
filterBuilderReorderRule: "Trascina o usa i tasti freccia per riordinare la condizione",
|
||||
|
||||
@@ -1699,6 +1699,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "SQL 条件をコピーしました",
|
||||
filterBuilderSummary: "{count}件のルール",
|
||||
filterBuilderAddRule: "ルールを追加",
|
||||
filterBuilderApplyOnly: "この条件のみで絞り込む",
|
||||
filterBuilderCompleteRuleFirst: "先にこの絞り込み条件を入力してください",
|
||||
filterBuilderEnableRule: "条件を有効化",
|
||||
filterBuilderDisableRule: "条件を無効化",
|
||||
filterBuilderReorderRule: "ドラッグまたは矢印キーで条件を並べ替え",
|
||||
|
||||
@@ -1680,6 +1680,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "SQL 조건을 복사했습니다",
|
||||
filterBuilderSummary: "{count}개 규칙",
|
||||
filterBuilderAddRule: "규칙 추가",
|
||||
filterBuilderApplyOnly: "이 조건만 필터링",
|
||||
filterBuilderCompleteRuleFirst: "먼저 이 필터 조건을 완성하세요",
|
||||
filterBuilderEnableRule: "조건 활성화",
|
||||
filterBuilderDisableRule: "조건 비활성화",
|
||||
filterBuilderReorderRule: "드래그하거나 방향키로 조건 순서 변경",
|
||||
|
||||
@@ -1689,6 +1689,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "Condição SQL copiada",
|
||||
filterBuilderSummary: "{count} regras",
|
||||
filterBuilderAddRule: "Adicionar regra",
|
||||
filterBuilderApplyOnly: "Filtrar apenas por esta regra",
|
||||
filterBuilderCompleteRuleFirst: "Preencha primeiro esta regra de filtro",
|
||||
filterBuilderEnableRule: "Ativar regra",
|
||||
filterBuilderDisableRule: "Desativar regra",
|
||||
filterBuilderReorderRule: "Arraste ou use as setas para reordenar a condição",
|
||||
|
||||
@@ -1810,6 +1810,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "SQL koşulu kopyalandı",
|
||||
filterBuilderSummary: "{count} kural",
|
||||
filterBuilderAddRule: "Kural ekle",
|
||||
filterBuilderApplyOnly: "Yalnızca bu kuralla filtrele",
|
||||
filterBuilderCompleteRuleFirst: "Önce bu filtre kuralını tamamlayın",
|
||||
filterBuilderEnableRule: "Kuralı etkinleştir",
|
||||
filterBuilderDisableRule: "Kuralı devre dışı bırak",
|
||||
filterBuilderReorderRule: "Koşulu yeniden sıralamak için sürükleyin veya ok tuşlarını kullanın",
|
||||
|
||||
@@ -1740,6 +1740,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "SQL 条件已复制",
|
||||
filterBuilderSummary: "{count} 条规则",
|
||||
filterBuilderAddRule: "新增条件",
|
||||
filterBuilderApplyOnly: "仅筛选此项",
|
||||
filterBuilderCompleteRuleFirst: "请先补全此筛选条件",
|
||||
filterBuilderEnableRule: "启用条件",
|
||||
filterBuilderDisableRule: "禁用条件",
|
||||
filterBuilderReorderRule: "拖拽或使用方向键排序条件",
|
||||
|
||||
@@ -1687,6 +1687,8 @@ export default withEnglishFallback({
|
||||
filterSqlCopied: "SQL 條件已複製",
|
||||
filterBuilderSummary: "{count} 條規則",
|
||||
filterBuilderAddRule: "新增條件",
|
||||
filterBuilderApplyOnly: "僅篩選此項",
|
||||
filterBuilderCompleteRuleFirst: "請先補齊此篩選條件",
|
||||
filterBuilderEnableRule: "啟用條件",
|
||||
filterBuilderDisableRule: "停用條件",
|
||||
filterBuilderReorderRule: "拖曳或使用方向鍵排序條件",
|
||||
|
||||
Reference in new issue
Block a user