@qijenchen/design-system 0.1.0-beta.84 → 0.1.0-beta.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/DataTable/data-table-filter-group.d.ts +47 -0
- package/dist/components/DataTable/data-table-filter-group.d.ts.map +1 -0
- package/dist/components/DataTable/data-table-filter-group.js +154 -0
- package/dist/components/DataTable/data-table-filter-group.js.map +1 -0
- package/dist/components/DataTable/data-table-filter-panel.d.ts.map +1 -1
- package/dist/components/DataTable/data-table-filter-panel.js +2 -324
- package/dist/components/DataTable/data-table-filter-panel.js.map +1 -1
- package/dist/components/DataTable/data-table-filter-value-picker.d.ts +29 -0
- package/dist/components/DataTable/data-table-filter-value-picker.d.ts.map +1 -0
- package/dist/components/DataTable/data-table-filter-value-picker.js +187 -0
- package/dist/components/DataTable/data-table-filter-value-picker.js.map +1 -0
- package/dist/components/DataTable/data-table.d.ts.map +1 -1
- package/dist/components/DataTable/data-table.js.map +1 -1
- package/dist/components/Field/index.d.ts +1 -0
- package/dist/components/Field/index.d.ts.map +1 -1
- package/dist/components/Field/index.js +2 -0
- package/dist/components/Field/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/ds-canonical/hooks/check_datatable_invariants.sh +6 -3
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/components/Checkbox/checkbox.spec.md +2 -0
- package/src/components/DataTable/data-table-filter-group.tsx +231 -0
- package/src/components/DataTable/data-table-filter-panel.tsx +13 -430
- package/src/components/DataTable/data-table-filter-value-picker.tsx +237 -0
- package/src/components/DataTable/data-table.spec.md +1 -1
- package/src/components/DataTable/data-table.tsx +4 -1
- package/src/components/DataTable/filter-operators.spec.md +3 -3
- package/src/components/Field/index.ts +1 -0
- package/src/components/Select/select.spec.md +2 -0
- package/src/index.ts +4 -0
- package/src/tokens/color/color.spec.md +1 -1
- package/src/tokens/uiSize/uiSize.css +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type SelectOption } from '../../components/Select/select';
|
|
2
|
+
import type { ColumnType } from './column-types';
|
|
3
|
+
import type { Conjunction, FilterCondition, FilterGroup } from './filter-tree';
|
|
4
|
+
export interface FilterColumn {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
type: ColumnType;
|
|
8
|
+
options?: Array<{
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** People pool for person/multiPerson filter picker(對齊 cell-registry meta.people SSOT)*/
|
|
13
|
+
people?: Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
avatarUrl?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
}>;
|
|
18
|
+
includeTime?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function getDefaultOperator(type?: ColumnType): string;
|
|
21
|
+
export declare const datePrecisionOf: (col: FilterColumn | undefined) => "day" | "ms" | undefined;
|
|
22
|
+
export declare function FilterRow({ index, condition, conjunction, filterableColumns, fieldOptions, onChangeConjunction, onChangeField, onChangeOp, onChangeValue, onRemove, }: {
|
|
23
|
+
index: number;
|
|
24
|
+
condition: FilterCondition;
|
|
25
|
+
conjunction: Conjunction;
|
|
26
|
+
filterableColumns: FilterColumn[];
|
|
27
|
+
fieldOptions: SelectOption[];
|
|
28
|
+
onChangeConjunction: (c: Conjunction) => void;
|
|
29
|
+
onChangeField: (v: string) => void;
|
|
30
|
+
onChangeOp: (v: string) => void;
|
|
31
|
+
onChangeValue: (v: unknown) => void;
|
|
32
|
+
onRemove: () => void;
|
|
33
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
34
|
+
export declare function GroupBlock({ index, group, rootConjunction, filterableColumns, fieldOptions, onChangeRootConjunction, onChangeGroupConjunction, onChangeCondition, onRemoveCondition, onAddCondition, onRemoveGroup, }: {
|
|
35
|
+
index: number;
|
|
36
|
+
group: FilterGroup;
|
|
37
|
+
rootConjunction: Conjunction;
|
|
38
|
+
filterableColumns: FilterColumn[];
|
|
39
|
+
fieldOptions: SelectOption[];
|
|
40
|
+
onChangeRootConjunction: (c: Conjunction) => void;
|
|
41
|
+
onChangeGroupConjunction: (c: Conjunction) => void;
|
|
42
|
+
onChangeCondition: (condId: string, patch: Partial<FilterCondition>) => void;
|
|
43
|
+
onRemoveCondition: (condId: string) => void;
|
|
44
|
+
onAddCondition: () => void;
|
|
45
|
+
onRemoveGroup: () => void;
|
|
46
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
47
|
+
//# sourceMappingURL=data-table-filter-group.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-table-filter-group.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/data-table-filter-group.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,0CAA0C,CAAA;AAEpF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAQhD,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAK9E,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjD,yFAAyF;IACzF,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1E,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAOD,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM,CAE5D;AAID,eAAO,MAAM,eAAe,GAAI,KAAK,YAAY,GAAG,SAAS,KAAG,KAAK,GAAG,IAAI,GAAG,SACV,CAAA;AA0CrE,wBAAgB,SAAS,CAAC,EACxB,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAC9D,mBAAmB,EAAE,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,GACxE,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,eAAe,CAAA;IAC1B,WAAW,EAAE,WAAW,CAAA;IACxB,iBAAiB,EAAE,YAAY,EAAE,CAAA;IACjC,YAAY,EAAE,YAAY,EAAE,CAAA;IAC5B,mBAAmB,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,CAAA;IAC7C,aAAa,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/B,aAAa,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,2CAgEA;AAID,wBAAgB,UAAU,CAAC,EACzB,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,YAAY,EAC9D,uBAAuB,EAAE,wBAAwB,EACjD,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,GACpE,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,WAAW,CAAA;IAClB,eAAe,EAAE,WAAW,CAAA;IAC5B,iBAAiB,EAAE,YAAY,EAAE,CAAA;IACjC,YAAY,EAAE,YAAY,EAAE,CAAA;IAC5B,uBAAuB,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,CAAA;IACjD,wBAAwB,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,IAAI,CAAA;IAClD,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,eAAe,CAAC,KAAK,IAAI,CAAA;IAC5E,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,aAAa,EAAE,MAAM,IAAI,CAAA;CAC1B,2CAoCA"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Trash2, Plus } from "lucide-react";
|
|
3
|
+
import { Button } from "../Button/button.js";
|
|
4
|
+
import { Select } from "../Select/select.js";
|
|
5
|
+
import { FieldControlGroup } from "../FieldControlGroup/field-control-group.js";
|
|
6
|
+
import { DEFAULT_OPERATOR, getOperatorSpec, getValueShape, OPERATOR_REGISTRY } from "./filter-operators.js";
|
|
7
|
+
import { FilterValuePicker } from "./data-table-filter-value-picker.js";
|
|
8
|
+
function getOperatorOptions(type) {
|
|
9
|
+
const registry = type && OPERATOR_REGISTRY[type] ? OPERATOR_REGISTRY[type] : OPERATOR_REGISTRY.string;
|
|
10
|
+
return registry.map((op) => ({ value: op.op, label: op.label }));
|
|
11
|
+
}
|
|
12
|
+
function getDefaultOperator(type) {
|
|
13
|
+
return type && DEFAULT_OPERATOR[type] || DEFAULT_OPERATOR.string;
|
|
14
|
+
}
|
|
15
|
+
const datePrecisionOf = (col) => (col == null ? void 0 : col.type) === "date" ? col.includeTime ? "ms" : "day" : void 0;
|
|
16
|
+
const CONJ_OPTIONS = [
|
|
17
|
+
{ value: "and", label: "And" },
|
|
18
|
+
{ value: "or", label: "Or" }
|
|
19
|
+
];
|
|
20
|
+
function ConjunctionLabel({
|
|
21
|
+
index,
|
|
22
|
+
conjunction,
|
|
23
|
+
onChange
|
|
24
|
+
}) {
|
|
25
|
+
if (index === 0) {
|
|
26
|
+
return /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0 text-body text-fg-muted px-[var(--field-px)] self-center", children: "Where" });
|
|
27
|
+
}
|
|
28
|
+
if (index >= 2) {
|
|
29
|
+
const label = conjunction === "and" ? "And" : "Or";
|
|
30
|
+
return /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0 text-body text-fg-muted px-[var(--field-px)] self-center", children: label });
|
|
31
|
+
}
|
|
32
|
+
return /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ jsx(
|
|
33
|
+
Select,
|
|
34
|
+
{
|
|
35
|
+
size: "sm",
|
|
36
|
+
options: CONJ_OPTIONS,
|
|
37
|
+
value: conjunction,
|
|
38
|
+
onChange: (v) => onChange(v),
|
|
39
|
+
minRows: 2,
|
|
40
|
+
"aria-label": "連接詞 — 同 group 共用"
|
|
41
|
+
}
|
|
42
|
+
) });
|
|
43
|
+
}
|
|
44
|
+
function FilterRow({
|
|
45
|
+
index,
|
|
46
|
+
condition,
|
|
47
|
+
conjunction,
|
|
48
|
+
filterableColumns,
|
|
49
|
+
fieldOptions,
|
|
50
|
+
onChangeConjunction,
|
|
51
|
+
onChangeField,
|
|
52
|
+
onChangeOp,
|
|
53
|
+
onChangeValue,
|
|
54
|
+
onRemove
|
|
55
|
+
}) {
|
|
56
|
+
const colInfo = filterableColumns.find((c) => c.id === condition.field);
|
|
57
|
+
const operatorOptions = getOperatorOptions(colInfo == null ? void 0 : colInfo.type);
|
|
58
|
+
const hasField = !!condition.field;
|
|
59
|
+
const opSpec = colInfo ? getOperatorSpec(colInfo.type, condition.op) : null;
|
|
60
|
+
const valueShape = colInfo && opSpec ? getValueShape(opSpec, colInfo.type, colInfo.includeTime) : null;
|
|
61
|
+
const hasValueCell = valueShape !== "none";
|
|
62
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
63
|
+
/* @__PURE__ */ jsx(ConjunctionLabel, { index, conjunction, onChange: onChangeConjunction }),
|
|
64
|
+
/* @__PURE__ */ jsxs(FieldControlGroup, { block: true, className: "flex-1 min-w-0", children: [
|
|
65
|
+
/* @__PURE__ */ jsx(
|
|
66
|
+
Select,
|
|
67
|
+
{
|
|
68
|
+
className: "!w-[var(--data-table-filter-field-width)] flex-shrink-0",
|
|
69
|
+
size: "sm",
|
|
70
|
+
options: fieldOptions,
|
|
71
|
+
value: condition.field,
|
|
72
|
+
onChange: onChangeField,
|
|
73
|
+
placeholder: "選擇欄位",
|
|
74
|
+
"aria-label": "篩選欄位"
|
|
75
|
+
}
|
|
76
|
+
),
|
|
77
|
+
/* @__PURE__ */ jsx(
|
|
78
|
+
Select,
|
|
79
|
+
{
|
|
80
|
+
className: hasValueCell ? "!w-[var(--data-table-filter-op-width)] flex-shrink-0" : "!flex-1 !min-w-0",
|
|
81
|
+
size: "sm",
|
|
82
|
+
options: operatorOptions,
|
|
83
|
+
value: condition.op,
|
|
84
|
+
onChange: onChangeOp,
|
|
85
|
+
disabled: !hasField,
|
|
86
|
+
placeholder: "運算子",
|
|
87
|
+
"aria-label": "篩選運算子"
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
hasValueCell && /* @__PURE__ */ jsx(
|
|
91
|
+
FilterValuePicker,
|
|
92
|
+
{
|
|
93
|
+
shape: valueShape,
|
|
94
|
+
value: condition.value,
|
|
95
|
+
onChange: onChangeValue,
|
|
96
|
+
colInfo,
|
|
97
|
+
disabled: !hasField,
|
|
98
|
+
ariaLabel: colInfo ? `${colInfo.label} 篩選值` : "篩選值",
|
|
99
|
+
className: "!flex-1 !min-w-0"
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
] }),
|
|
103
|
+
/* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", iconOnly: true, startIcon: Trash2, "aria-label": "刪除", onClick: onRemove })
|
|
104
|
+
] });
|
|
105
|
+
}
|
|
106
|
+
function GroupBlock({
|
|
107
|
+
index,
|
|
108
|
+
group,
|
|
109
|
+
rootConjunction,
|
|
110
|
+
filterableColumns,
|
|
111
|
+
fieldOptions,
|
|
112
|
+
onChangeRootConjunction,
|
|
113
|
+
onChangeGroupConjunction,
|
|
114
|
+
onChangeCondition,
|
|
115
|
+
onRemoveCondition,
|
|
116
|
+
onAddCondition,
|
|
117
|
+
onRemoveGroup
|
|
118
|
+
}) {
|
|
119
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
120
|
+
/* @__PURE__ */ jsx("div", { className: "pt-2", children: /* @__PURE__ */ jsx(ConjunctionLabel, { index, conjunction: rootConjunction, onChange: onChangeRootConjunction }) }),
|
|
121
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 rounded-md bg-muted p-2 flex flex-col gap-2", children: [
|
|
122
|
+
group.children.map((cond, cIdx) => /* @__PURE__ */ jsx(
|
|
123
|
+
FilterRow,
|
|
124
|
+
{
|
|
125
|
+
index: cIdx,
|
|
126
|
+
condition: cond,
|
|
127
|
+
conjunction: group.conjunction,
|
|
128
|
+
filterableColumns,
|
|
129
|
+
fieldOptions,
|
|
130
|
+
onChangeConjunction: onChangeGroupConjunction,
|
|
131
|
+
onChangeField: (v) => {
|
|
132
|
+
const newCol = filterableColumns.find((c) => c.id === v);
|
|
133
|
+
onChangeCondition(cond.id, { field: v, op: getDefaultOperator(newCol == null ? void 0 : newCol.type), value: "", datePrecision: datePrecisionOf(newCol) });
|
|
134
|
+
},
|
|
135
|
+
onChangeOp: (v) => onChangeCondition(cond.id, { op: v, value: "" }),
|
|
136
|
+
onChangeValue: (v) => onChangeCondition(cond.id, { value: v }),
|
|
137
|
+
onRemove: () => onRemoveCondition(cond.id)
|
|
138
|
+
},
|
|
139
|
+
cond.id
|
|
140
|
+
)),
|
|
141
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
142
|
+
/* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", startIcon: Plus, onClick: onAddCondition, children: "加入巢狀篩選" }),
|
|
143
|
+
group.children.length === 0 && /* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", startIcon: Trash2, danger: true, onClick: onRemoveGroup, children: "移除空群組" })
|
|
144
|
+
] })
|
|
145
|
+
] })
|
|
146
|
+
] });
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
FilterRow,
|
|
150
|
+
GroupBlock,
|
|
151
|
+
datePrecisionOf,
|
|
152
|
+
getDefaultOperator
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=data-table-filter-group.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-table-filter-group.js","sources":["../../../src/components/DataTable/data-table-filter-group.tsx"],"sourcesContent":["// @benchmark-unverified-blanket: file-level retraction per M22 (d) — claims herein not individually URL-cited; treat as unverified visual/usage rumor unless retrofit per-claim. Hook escape preserved(2026-07-14 自 data-table-filter-panel.tsx 拆檔搬移,原檔級 escape 隨段落帶入)。\n// @internal — DS-internal 單元(filter row / nested-group renderer + 共用 column/operator\n// helpers;GroupBlock 為 nested-group renderer,FilterRow / ConjunctionLabel 為其 row 構件、\n// flat mode 共用。只被 data-table-filter-panel.tsx 消費);不隨 index.ts re-export 進 npm public surface。\n// ────────────────────────────── 消費的 SSOT ──────────────────────────────\n// - FieldControlGroup:field + op + value border-collapse 接合(field-control-group.spec.md;\n// FilterValuePicker 為 direct child,className forward 不開 wrapper div — 2026-05-04 #2 fix)\n// - Button(variant=\"text\" iconOnly)row meta trash / Select(size=\"sm\")conjunction・field・op\n// (data-table.spec.md「四、UI canonical」:trash = form-control row text Button;A6 conjunction)\n// - Width tokens:--data-table-filter-field-width / --data-table-filter-op-width(uiSize.css SSOT,\n// 2026-05-23 Phase A.4 Decision 2)\n// - Operator SSOT:./filter-operators.ts OPERATOR_REGISTRY(禁 hardcode op 字串)\n// - Group container 灰底 bg-muted:color.spec.md L651-654 靜態低重要 surface semantic(2026-05-09 Q3 A)\n// 設計規則:./filter-operators.spec.md + ./data-table.spec.md「進階篩選」段\n\nimport { Plus, Trash2 } from 'lucide-react'\nimport { Button } from '@/design-system/components/Button/button'\nimport { Select, type SelectOption } from '@/design-system/components/Select/select'\nimport { FieldControlGroup } from '@/design-system/components/FieldControlGroup/field-control-group'\nimport type { ColumnType } from './column-types'\nimport {\n OPERATOR_REGISTRY,\n DEFAULT_OPERATOR,\n getOperatorSpec,\n getValueShape,\n type ValueShape,\n} from './filter-operators'\nimport type { Conjunction, FilterCondition, FilterGroup } from './filter-tree'\nimport { FilterValuePicker } from './data-table-filter-value-picker'\n\n// ── Shared internal types + operator helpers(panel orchestration 也消費)────\n\nexport interface FilterColumn {\n id: string\n label: string\n type: ColumnType\n options?: Array<{ value: string; label: string }>\n /** People pool for person/multiPerson filter picker(對齊 cell-registry meta.people SSOT)*/\n people?: Array<{ name: string; avatarUrl?: string; description?: string }>\n includeTime?: boolean\n}\n\nfunction getOperatorOptions(type?: ColumnType): SelectOption[] {\n const registry = type && OPERATOR_REGISTRY[type] ? OPERATOR_REGISTRY[type] : OPERATOR_REGISTRY.string\n return registry.map((op) => ({ value: op.op, label: op.label }))\n}\n\nexport function getDefaultOperator(type?: ColumnType): string {\n return (type && DEFAULT_OPERATOR[type]) || DEFAULT_OPERATOR.string\n}\n\n// Q6(2026-07-04):date 欄位把比對精度寫進 condition(includeTime=true → ms,否則 day)。\n// 'time' 欄暫不寫(值非完整日期,truncate 語意不成立;見 filter-operators.spec v3 time 註)。\nexport const datePrecisionOf = (col: FilterColumn | undefined): 'day' | 'ms' | undefined =>\n col?.type === 'date' ? (col.includeTime ? 'ms' : 'day') : undefined\n\n// ── ConjunctionLabel ───────────────────────────────────────────────────\n\nconst CONJ_OPTIONS: SelectOption[] = [\n { value: 'and', label: 'And' },\n { value: 'or', label: 'Or' },\n]\n\nfunction ConjunctionLabel({\n index, conjunction, onChange,\n}: { index: number; conjunction: Conjunction; onChange: (c: Conjunction) => void }) {\n // index === 0:首 row 顯示靜態「Where」label\n // index === 1:**唯一可改**的 AND/OR Select(連動整 group conjunction)\n // index ≥ 2:被連動的 row,read-only 顯示當前 conjunction 文字(同 Where 視覺,A6 canonical)\n // 對齊 Airtable / Notion / Linear 共識 @benchmark-unverified(non-OSS)\n // px-[var(--field-px)] 對齊 Field 內部 padding 12px(Q13)\n if (index === 0) {\n return <div className=\"w-20 shrink-0 text-body text-fg-muted px-[var(--field-px)] self-center\">Where</div>\n }\n if (index >= 2) {\n const label = conjunction === 'and' ? 'And' : 'Or'\n return <div className=\"w-20 shrink-0 text-body text-fg-muted px-[var(--field-px)] self-center\">{label}</div>\n }\n // index === 1:可切換的 AND/OR Select\n // minRows={2} — And/Or 2 選項,顯式縮 menu 高度避免 reserve 3 row 空白(Q5)\n return (\n <div className=\"w-20 shrink-0\">\n <Select\n size=\"sm\"\n options={CONJ_OPTIONS}\n value={conjunction}\n onChange={(v) => onChange(v as Conjunction)}\n minRows={2}\n aria-label=\"連接詞 — 同 group 共用\"\n />\n </div>\n )\n}\n\n// ── FilterRow(flat 用 + group 內共用) ──────────────────────────────\n\nexport function FilterRow({\n index, condition, conjunction, filterableColumns, fieldOptions,\n onChangeConjunction, onChangeField, onChangeOp, onChangeValue, onRemove,\n}: {\n index: number\n condition: FilterCondition\n conjunction: Conjunction\n filterableColumns: FilterColumn[]\n fieldOptions: SelectOption[]\n onChangeConjunction: (c: Conjunction) => void\n onChangeField: (v: string) => void\n onChangeOp: (v: string) => void\n onChangeValue: (v: unknown) => void\n onRemove: () => void\n}) {\n const colInfo = filterableColumns.find((c) => c.id === condition.field)\n const operatorOptions = getOperatorOptions(colInfo?.type)\n const hasField = !!condition.field\n const opSpec = colInfo ? getOperatorSpec(colInfo.type, condition.op) : null\n const valueShape: ValueShape | null = colInfo && opSpec\n ? getValueShape(opSpec, colInfo.type, colInfo.includeTime)\n : null\n // op 'is_set' / 'is_not_set' 等 shape='none' → 無 value cell,op 自動 expand 填剩餘寬\n // 對齊 Notion / Airtable / Linear filter row 行為\n // 注意:valueShape=null(初始無 field 選)時仍 render value cell(disabled placeholder)— 只 'none' 才 fold\n const hasValueCell = valueShape !== 'none'\n\n // FieldControlGroup 接合 field + op + value 視覺(2026-05-04 E refactor + 多輪 fix):\n // - border collapse 取代 3 顆獨立 Select 並排,對齊 Airtable / Linear / Notion filter row idiom\n // - ConjunctionLabel + Trash 在 group 外層(meta actions,不屬 control 一體)\n // - **#5 fix**:row 內水平 gap = `gap-2` (8px),layoutSpace 規則 5 緊密相關\n // - **#9 fix**:cell 用 `min-w-[]`(field 160 / op 120),value flex-1 min-w-0,讓 long label 可撐寬\n // - **#2 fix**:FilterValuePicker 直接是 FieldControlGroup direct child(無 wrapper div),CSS variants 命中正確\n return (\n <div className=\"flex items-center gap-2\">\n <ConjunctionLabel index={index} conjunction={conjunction} onChange={onChangeConjunction} />\n {/* **#9 fix(2026-05-04 v4)**:Field controls trigger `w-full` override 外 className,改用 Tailwind `!`\n important 強制 override(`!w-[160px]` / `!w-[120px]`),value 用 `!flex-1 !min-w-0`。\n Select 元件本身沒 destructure `style` prop 所以 inline style flex-basis 行不通,只能用 className。 */}\n <FieldControlGroup block className=\"flex-1 min-w-0\">\n {/* 2026-05-23 Phase A.4 Decision 2:`!w-[160px]` / `!w-[120px]` → tokens\n `--data-table-filter-field-width` / `--data-table-filter-op-width`(SSOT in uiSize.css)\n Behavior preserved 完好如初:flat + nested 同 width(token 是 design constant) */}\n <Select\n className=\"!w-[var(--data-table-filter-field-width)] flex-shrink-0\"\n size=\"sm\"\n options={fieldOptions}\n value={condition.field}\n onChange={onChangeField}\n placeholder=\"選擇欄位\"\n aria-label=\"篩選欄位\"\n />\n <Select\n className={hasValueCell ? '!w-[var(--data-table-filter-op-width)] flex-shrink-0' : '!flex-1 !min-w-0'}\n size=\"sm\"\n options={operatorOptions}\n value={condition.op}\n onChange={onChangeOp}\n disabled={!hasField}\n placeholder=\"運算子\"\n aria-label=\"篩選運算子\"\n />\n {hasValueCell && (\n <FilterValuePicker\n shape={valueShape}\n value={condition.value}\n onChange={onChangeValue}\n colInfo={colInfo}\n disabled={!hasField}\n ariaLabel={colInfo ? `${colInfo.label} 篩選值` : '篩選值'}\n className=\"!flex-1 !min-w-0\"\n />\n )}\n </FieldControlGroup>\n {/* Trash 用 text Button — filter row 是 form-control row,Field 同高對齊(28 md) */}\n <Button variant=\"text\" size=\"sm\" iconOnly startIcon={Trash2} aria-label=\"刪除\" onClick={onRemove} />\n </div>\n )\n}\n\n// ── GroupBlock(nested 用) ────────────────────────────────────────────\n\nexport function GroupBlock({\n index, group, rootConjunction, filterableColumns, fieldOptions,\n onChangeRootConjunction, onChangeGroupConjunction,\n onChangeCondition, onRemoveCondition, onAddCondition, onRemoveGroup,\n}: {\n index: number\n group: FilterGroup\n rootConjunction: Conjunction\n filterableColumns: FilterColumn[]\n fieldOptions: SelectOption[]\n onChangeRootConjunction: (c: Conjunction) => void\n onChangeGroupConjunction: (c: Conjunction) => void\n onChangeCondition: (condId: string, patch: Partial<FilterCondition>) => void\n onRemoveCondition: (condId: string) => void\n onAddCondition: () => void\n onRemoveGroup: () => void\n}) {\n return (\n <div className=\"flex items-start gap-2\">\n <div className=\"pt-2\">\n <ConjunctionLabel index={index} conjunction={rootConjunction} onChange={onChangeRootConjunction} />\n </div>\n {/* Group container 灰底 — `bg-muted`(`--muted` neutral-2,user 2026-05-09 拍板 Q3 A)。對齊 color.spec.md L651-654 「table header / tab / code block / skeleton」靜態低重要 surface semantic */}\n <div className=\"flex-1 min-w-0 rounded-md bg-muted p-2 flex flex-col gap-2\">\n {group.children.map((cond, cIdx) => (\n <FilterRow\n key={cond.id}\n index={cIdx}\n condition={cond}\n conjunction={group.conjunction}\n filterableColumns={filterableColumns}\n fieldOptions={fieldOptions}\n onChangeConjunction={onChangeGroupConjunction}\n onChangeField={(v) => {\n const newCol = filterableColumns.find((c) => c.id === v)\n onChangeCondition(cond.id, { field: v, op: getDefaultOperator(newCol?.type), value: '', datePrecision: datePrecisionOf(newCol) })\n }}\n onChangeOp={(v) => onChangeCondition(cond.id, { op: v, value: '' })}\n onChangeValue={(v) => onChangeCondition(cond.id, { value: v })}\n onRemove={() => onRemoveCondition(cond.id)}\n />\n ))}\n {/* Q9 — text variant 對齊 inline 派 + 視覺輕量 */}\n <div className=\"flex items-center justify-between\">\n <Button variant=\"text\" size=\"sm\" startIcon={Plus} onClick={onAddCondition}>加入巢狀篩選</Button>\n {group.children.length === 0 && (\n <Button variant=\"text\" size=\"sm\" startIcon={Trash2} danger onClick={onRemoveGroup}>移除空群組</Button>\n )}\n </div>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;;AA0CA,SAAS,mBAAmB,MAAmC;AAC7D,QAAM,WAAW,QAAQ,kBAAkB,IAAI,IAAI,kBAAkB,IAAI,IAAI,kBAAkB;AAC/F,SAAO,SAAS,IAAI,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,OAAO,GAAG,MAAA,EAAQ;AACjE;AAEO,SAAS,mBAAmB,MAA2B;AAC5D,SAAQ,QAAQ,iBAAiB,IAAI,KAAM,iBAAiB;AAC9D;AAIO,MAAM,kBAAkB,CAAC,SAC9B,2BAAK,UAAS,SAAU,IAAI,cAAc,OAAO,QAAS;AAI5D,MAAM,eAA+B;AAAA,EACnC,EAAE,OAAO,OAAO,OAAO,MAAA;AAAA,EACvB,EAAE,OAAO,MAAM,OAAO,KAAA;AACxB;AAEA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EAAO;AAAA,EAAa;AACtB,GAAoF;AAMlF,MAAI,UAAU,GAAG;AACf,WAAO,oBAAC,OAAA,EAAI,WAAU,0EAAyE,UAAA,SAAK;AAAA,EACtG;AACA,MAAI,SAAS,GAAG;AACd,UAAM,QAAQ,gBAAgB,QAAQ,QAAQ;AAC9C,WAAO,oBAAC,OAAA,EAAI,WAAU,0EAA0E,UAAA,OAAM;AAAA,EACxG;AAGA,SACE,oBAAC,OAAA,EAAI,WAAU,iBACb,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,MACP,UAAU,CAAC,MAAM,SAAS,CAAgB;AAAA,MAC1C,SAAS;AAAA,MACT,cAAW;AAAA,IAAA;AAAA,EAAA,GAEf;AAEJ;AAIO,SAAS,UAAU;AAAA,EACxB;AAAA,EAAO;AAAA,EAAW;AAAA,EAAa;AAAA,EAAmB;AAAA,EAClD;AAAA,EAAqB;AAAA,EAAe;AAAA,EAAY;AAAA,EAAe;AACjE,GAWG;AACD,QAAM,UAAU,kBAAkB,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU,KAAK;AACtE,QAAM,kBAAkB,mBAAmB,mCAAS,IAAI;AACxD,QAAM,WAAW,CAAC,CAAC,UAAU;AAC7B,QAAM,SAAS,UAAU,gBAAgB,QAAQ,MAAM,UAAU,EAAE,IAAI;AACvE,QAAM,aAAgC,WAAW,SAC7C,cAAc,QAAQ,QAAQ,MAAM,QAAQ,WAAW,IACvD;AAIJ,QAAM,eAAe,eAAe;AAQpC,SACE,qBAAC,OAAA,EAAI,WAAU,2BACb,UAAA;AAAA,IAAA,oBAAC,kBAAA,EAAiB,OAAc,aAA0B,UAAU,qBAAqB;AAAA,IAIzF,qBAAC,mBAAA,EAAkB,OAAK,MAAC,WAAU,kBAIjC,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,MAAK;AAAA,UACL,SAAS;AAAA,UACT,OAAO,UAAU;AAAA,UACjB,UAAU;AAAA,UACV,aAAY;AAAA,UACZ,cAAW;AAAA,QAAA;AAAA,MAAA;AAAA,MAEb;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAW,eAAe,yDAAyD;AAAA,UACnF,MAAK;AAAA,UACL,SAAS;AAAA,UACT,OAAO,UAAU;AAAA,UACjB,UAAU;AAAA,UACV,UAAU,CAAC;AAAA,UACX,aAAY;AAAA,UACZ,cAAW;AAAA,QAAA;AAAA,MAAA;AAAA,MAEZ,gBACC;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,OAAO;AAAA,UACP,OAAO,UAAU;AAAA,UACjB,UAAU;AAAA,UACV;AAAA,UACA,UAAU,CAAC;AAAA,UACX,WAAW,UAAU,GAAG,QAAQ,KAAK,SAAS;AAAA,UAC9C,WAAU;AAAA,QAAA;AAAA,MAAA;AAAA,IACZ,GAEJ;AAAA,IAEA,oBAAC,QAAA,EAAO,SAAQ,QAAO,MAAK,MAAK,UAAQ,MAAC,WAAW,QAAQ,cAAW,MAAK,SAAS,SAAA,CAAU;AAAA,EAAA,GAClG;AAEJ;AAIO,SAAS,WAAW;AAAA,EACzB;AAAA,EAAO;AAAA,EAAO;AAAA,EAAiB;AAAA,EAAmB;AAAA,EAClD;AAAA,EAAyB;AAAA,EACzB;AAAA,EAAmB;AAAA,EAAmB;AAAA,EAAgB;AACxD,GAYG;AACD,SACE,qBAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,WAAU,QACb,UAAA,oBAAC,kBAAA,EAAiB,OAAc,aAAa,iBAAiB,UAAU,wBAAA,CAAyB,EAAA,CACnG;AAAA,IAEA,qBAAC,OAAA,EAAI,WAAU,8DACZ,UAAA;AAAA,MAAA,MAAM,SAAS,IAAI,CAAC,MAAM,SACzB;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,OAAO;AAAA,UACP,WAAW;AAAA,UACX,aAAa,MAAM;AAAA,UACnB;AAAA,UACA;AAAA,UACA,qBAAqB;AAAA,UACrB,eAAe,CAAC,MAAM;AACpB,kBAAM,SAAS,kBAAkB,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC;AACvD,8BAAkB,KAAK,IAAI,EAAE,OAAO,GAAG,IAAI,mBAAmB,iCAAQ,IAAI,GAAG,OAAO,IAAI,eAAe,gBAAgB,MAAM,GAAG;AAAA,UAClI;AAAA,UACA,YAAY,CAAC,MAAM,kBAAkB,KAAK,IAAI,EAAE,IAAI,GAAG,OAAO,IAAI;AAAA,UAClE,eAAe,CAAC,MAAM,kBAAkB,KAAK,IAAI,EAAE,OAAO,GAAG;AAAA,UAC7D,UAAU,MAAM,kBAAkB,KAAK,EAAE;AAAA,QAAA;AAAA,QAbpC,KAAK;AAAA,MAAA,CAeb;AAAA,MAED,qBAAC,OAAA,EAAI,WAAU,qCACb,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAO,SAAQ,QAAO,MAAK,MAAK,WAAW,MAAM,SAAS,gBAAgB,UAAA,SAAA,CAAM;AAAA,QAChF,MAAM,SAAS,WAAW,KACzB,oBAAC,UAAO,SAAQ,QAAO,MAAK,MAAK,WAAW,QAAQ,QAAM,MAAC,SAAS,eAAe,UAAA,QAAA,CAAK;AAAA,MAAA,EAAA,CAE5F;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-table-filter-panel.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/data-table-filter-panel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"data-table-filter-panel.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/data-table-filter-panel.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAgBtD,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAElB,YAAY,EACZ,oBAAoB,EACpB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACtB,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,GACrB,CAAA;AACD,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAsFvG,MAAM,WAAW,yBAAyB,CAAC,KAAK;IAC9C,yDAAyD;IACzD,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAA;IACvB,0BAA0B;IAC1B,OAAO,EAAE,SAAS,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAA;IAChC,gCAAgC;IAChC,KAAK,EAAE,UAAU,CAAA;IACjB,wBAAwB;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAA;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,UAAU,CAAA;IACzB,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC9B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA8PD,eAAO,MAAM,oBAAoB,EAAkD,CAAC,KAAK,EACvF,KAAK,EAAE,yBAAyB,CAAC,KAAK,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,cAAc,CAAC,CAAA;CAAE,KACnF,KAAK,CAAC,YAAY,CACtB"}
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { RotateCcw, X, Plus
|
|
3
|
+
import { RotateCcw, X, Plus } from "lucide-react";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
5
|
import { Button } from "../Button/button.js";
|
|
6
|
-
import { Select } from "../Select/select.js";
|
|
7
|
-
import { Combobox } from "../Combobox/combobox.js";
|
|
8
|
-
import { Input } from "../Input/input.js";
|
|
9
|
-
import { NumberInput } from "../NumberInput/number-input.js";
|
|
10
|
-
import { DatePickerRange, DatePicker as DatePickerWithRange } from "../DatePicker/date-picker.js";
|
|
11
|
-
import { PeoplePicker } from "../PeoplePicker/people-picker.js";
|
|
12
6
|
import { SurfaceHeader, COMPACT_HEADER_SLOT, SurfaceBody } from "../../patterns/overlay-surface/overlay-surface.js";
|
|
13
7
|
import { PopoverTitle, PopoverClose } from "../Popover/popover.js";
|
|
14
8
|
import { ButtonDivider } from "../Button/button-group.js";
|
|
15
|
-
import { FieldControlGroup } from "../FieldControlGroup/field-control-group.js";
|
|
16
9
|
import { getColumnId, getColumnMeta, getColumnLabel } from "./lib/column-meta.js";
|
|
17
|
-
import {
|
|
10
|
+
import { datePrecisionOf, getDefaultOperator, FilterRow, GroupBlock } from "./data-table-filter-group.js";
|
|
18
11
|
import { isFilterTreeEqual } from "./filter-tree.js";
|
|
19
12
|
import { createEmptyFilterTree, dataTableFilterMatch, evaluateTree, isFilterTreeActive } from "./filter-tree.js";
|
|
20
13
|
let _idSeed = 0;
|
|
@@ -39,14 +32,6 @@ function extractColumns(columns) {
|
|
|
39
32
|
}
|
|
40
33
|
return out;
|
|
41
34
|
}
|
|
42
|
-
function getOperatorOptions(type) {
|
|
43
|
-
const registry = type && OPERATOR_REGISTRY[type] ? OPERATOR_REGISTRY[type] : OPERATOR_REGISTRY.string;
|
|
44
|
-
return registry.map((op) => ({ value: op.op, label: op.label }));
|
|
45
|
-
}
|
|
46
|
-
function getDefaultOperator(type) {
|
|
47
|
-
return type && DEFAULT_OPERATOR[type] || DEFAULT_OPERATOR.string;
|
|
48
|
-
}
|
|
49
|
-
const datePrecisionOf = (col) => (col == null ? void 0 : col.type) === "date" ? col.includeTime ? "ms" : "day" : void 0;
|
|
50
35
|
const newCondition = (firstCol) => ({
|
|
51
36
|
kind: "cond",
|
|
52
37
|
id: newId(),
|
|
@@ -75,181 +60,6 @@ const newEmptyGroup = () => ({
|
|
|
75
60
|
conjunction: "or",
|
|
76
61
|
children: [newEmptyCondition()]
|
|
77
62
|
});
|
|
78
|
-
function FilterValuePicker({
|
|
79
|
-
shape,
|
|
80
|
-
value,
|
|
81
|
-
onChange,
|
|
82
|
-
colInfo,
|
|
83
|
-
disabled,
|
|
84
|
-
ariaLabel,
|
|
85
|
-
className
|
|
86
|
-
}) {
|
|
87
|
-
if (!shape || disabled) {
|
|
88
|
-
return /* @__PURE__ */ jsx(Input, { size: "sm", value: "", onChange: () => {
|
|
89
|
-
}, placeholder: "輸入值…", disabled: true, "aria-label": ariaLabel, className });
|
|
90
|
-
}
|
|
91
|
-
switch (shape) {
|
|
92
|
-
case "none":
|
|
93
|
-
return null;
|
|
94
|
-
case "text":
|
|
95
|
-
return /* @__PURE__ */ jsx(
|
|
96
|
-
Input,
|
|
97
|
-
{
|
|
98
|
-
size: "sm",
|
|
99
|
-
value: String(value ?? ""),
|
|
100
|
-
onChange: (e) => onChange(e.target.value),
|
|
101
|
-
placeholder: "輸入值…",
|
|
102
|
-
"aria-label": ariaLabel,
|
|
103
|
-
className
|
|
104
|
-
}
|
|
105
|
-
);
|
|
106
|
-
case "number":
|
|
107
|
-
return /* @__PURE__ */ jsx(
|
|
108
|
-
NumberInput,
|
|
109
|
-
{
|
|
110
|
-
size: "sm",
|
|
111
|
-
value: typeof value === "number" ? value : null,
|
|
112
|
-
onChange: (v) => onChange(v ?? ""),
|
|
113
|
-
placeholder: "輸入數字…",
|
|
114
|
-
"aria-label": ariaLabel,
|
|
115
|
-
className
|
|
116
|
-
}
|
|
117
|
-
);
|
|
118
|
-
case "date_single":
|
|
119
|
-
return /* @__PURE__ */ jsx(
|
|
120
|
-
DatePickerWithRange,
|
|
121
|
-
{
|
|
122
|
-
size: "sm",
|
|
123
|
-
value: typeof value === "string" ? value : null,
|
|
124
|
-
onChange: (v) => onChange(v ?? ""),
|
|
125
|
-
"aria-label": ariaLabel,
|
|
126
|
-
className
|
|
127
|
-
}
|
|
128
|
-
);
|
|
129
|
-
case "date_range":
|
|
130
|
-
return /* @__PURE__ */ jsx(
|
|
131
|
-
DatePickerRange,
|
|
132
|
-
{
|
|
133
|
-
size: "sm",
|
|
134
|
-
value: Array.isArray(value) && value.length === 2 ? value : null,
|
|
135
|
-
onChange: (v) => onChange(v),
|
|
136
|
-
"aria-label": ariaLabel,
|
|
137
|
-
className
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
case "date_relative": {
|
|
141
|
-
const opts = DATE_RELATIVE_OPTIONS.map((o) => ({
|
|
142
|
-
value: o.value,
|
|
143
|
-
label: o.label,
|
|
144
|
-
group: o.group
|
|
145
|
-
}));
|
|
146
|
-
return /* @__PURE__ */ jsx(
|
|
147
|
-
Select,
|
|
148
|
-
{
|
|
149
|
-
size: "sm",
|
|
150
|
-
options: opts,
|
|
151
|
-
groups: DATE_RELATIVE_GROUPS,
|
|
152
|
-
value: String(value ?? ""),
|
|
153
|
-
onChange: (v) => onChange(v),
|
|
154
|
-
placeholder: "選擇相對日期",
|
|
155
|
-
"aria-label": ariaLabel,
|
|
156
|
-
className
|
|
157
|
-
}
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
case "select_single": {
|
|
161
|
-
const opts = ((colInfo == null ? void 0 : colInfo.options) ?? []).map((o) => ({
|
|
162
|
-
value: o.value,
|
|
163
|
-
label: o.label
|
|
164
|
-
}));
|
|
165
|
-
return /* @__PURE__ */ jsx(
|
|
166
|
-
Select,
|
|
167
|
-
{
|
|
168
|
-
size: "sm",
|
|
169
|
-
options: opts,
|
|
170
|
-
value: String(value ?? ""),
|
|
171
|
-
onChange: (v) => onChange(v),
|
|
172
|
-
placeholder: "選擇值",
|
|
173
|
-
"aria-label": ariaLabel,
|
|
174
|
-
className
|
|
175
|
-
}
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
case "select_multi": {
|
|
179
|
-
const opts = ((colInfo == null ? void 0 : colInfo.options) ?? []).map((o) => ({
|
|
180
|
-
value: o.value,
|
|
181
|
-
label: o.label
|
|
182
|
-
}));
|
|
183
|
-
const arr = Array.isArray(value) ? value : [];
|
|
184
|
-
return /* @__PURE__ */ jsx(
|
|
185
|
-
Combobox,
|
|
186
|
-
{
|
|
187
|
-
size: "sm",
|
|
188
|
-
options: opts,
|
|
189
|
-
value: arr,
|
|
190
|
-
onChange: (v) => onChange(v),
|
|
191
|
-
placeholder: "選擇值…",
|
|
192
|
-
"aria-label": ariaLabel,
|
|
193
|
-
className
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
case "datetime_single":
|
|
198
|
-
return /* @__PURE__ */ jsx(
|
|
199
|
-
DatePickerWithRange,
|
|
200
|
-
{
|
|
201
|
-
size: "sm",
|
|
202
|
-
showTime: true,
|
|
203
|
-
value: typeof value === "string" ? value : null,
|
|
204
|
-
onChange: (v) => onChange(v ?? ""),
|
|
205
|
-
"aria-label": ariaLabel,
|
|
206
|
-
className
|
|
207
|
-
}
|
|
208
|
-
);
|
|
209
|
-
case "datetime_range":
|
|
210
|
-
return /* @__PURE__ */ jsx(
|
|
211
|
-
DatePickerRange,
|
|
212
|
-
{
|
|
213
|
-
size: "sm",
|
|
214
|
-
showTime: true,
|
|
215
|
-
value: Array.isArray(value) && value.length === 2 ? value : null,
|
|
216
|
-
onChange: (v) => onChange(v),
|
|
217
|
-
"aria-label": ariaLabel,
|
|
218
|
-
className
|
|
219
|
-
}
|
|
220
|
-
);
|
|
221
|
-
case "person_single": {
|
|
222
|
-
const v = value;
|
|
223
|
-
return /* @__PURE__ */ jsx(
|
|
224
|
-
PeoplePicker,
|
|
225
|
-
{
|
|
226
|
-
size: "sm",
|
|
227
|
-
value: v ?? null,
|
|
228
|
-
people: (colInfo == null ? void 0 : colInfo.people) ?? [],
|
|
229
|
-
onChange: (next) => onChange(next[0] ?? null),
|
|
230
|
-
"aria-label": ariaLabel,
|
|
231
|
-
className
|
|
232
|
-
}
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
case "person_multi": {
|
|
236
|
-
const v = Array.isArray(value) ? value : [];
|
|
237
|
-
return /* @__PURE__ */ jsx(
|
|
238
|
-
PeoplePicker,
|
|
239
|
-
{
|
|
240
|
-
size: "sm",
|
|
241
|
-
value: v,
|
|
242
|
-
people: (colInfo == null ? void 0 : colInfo.people) ?? [],
|
|
243
|
-
onChange: (next) => onChange(next),
|
|
244
|
-
"aria-label": ariaLabel,
|
|
245
|
-
className
|
|
246
|
-
}
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
default:
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
63
|
function DataTableFilterPanelInner({
|
|
254
64
|
mode,
|
|
255
65
|
columns,
|
|
@@ -465,138 +275,6 @@ function DataTableFilterPanelInner({
|
|
|
465
275
|
}
|
|
466
276
|
const DataTableFilterPanel = React.forwardRef(DataTableFilterPanelInner);
|
|
467
277
|
DataTableFilterPanel.displayName = "DataTableFilterPanel";
|
|
468
|
-
const CONJ_OPTIONS = [
|
|
469
|
-
{ value: "and", label: "And" },
|
|
470
|
-
{ value: "or", label: "Or" }
|
|
471
|
-
];
|
|
472
|
-
function ConjunctionLabel({
|
|
473
|
-
index,
|
|
474
|
-
conjunction,
|
|
475
|
-
onChange
|
|
476
|
-
}) {
|
|
477
|
-
if (index === 0) {
|
|
478
|
-
return /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0 text-body text-fg-muted px-[var(--field-px)] self-center", children: "Where" });
|
|
479
|
-
}
|
|
480
|
-
if (index >= 2) {
|
|
481
|
-
const label = conjunction === "and" ? "And" : "Or";
|
|
482
|
-
return /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0 text-body text-fg-muted px-[var(--field-px)] self-center", children: label });
|
|
483
|
-
}
|
|
484
|
-
return /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ jsx(
|
|
485
|
-
Select,
|
|
486
|
-
{
|
|
487
|
-
size: "sm",
|
|
488
|
-
options: CONJ_OPTIONS,
|
|
489
|
-
value: conjunction,
|
|
490
|
-
onChange: (v) => onChange(v),
|
|
491
|
-
minRows: 2,
|
|
492
|
-
"aria-label": "連接詞 — 同 group 共用"
|
|
493
|
-
}
|
|
494
|
-
) });
|
|
495
|
-
}
|
|
496
|
-
function FilterRow({
|
|
497
|
-
index,
|
|
498
|
-
condition,
|
|
499
|
-
conjunction,
|
|
500
|
-
filterableColumns,
|
|
501
|
-
fieldOptions,
|
|
502
|
-
onChangeConjunction,
|
|
503
|
-
onChangeField,
|
|
504
|
-
onChangeOp,
|
|
505
|
-
onChangeValue,
|
|
506
|
-
onRemove
|
|
507
|
-
}) {
|
|
508
|
-
const colInfo = filterableColumns.find((c) => c.id === condition.field);
|
|
509
|
-
const operatorOptions = getOperatorOptions(colInfo == null ? void 0 : colInfo.type);
|
|
510
|
-
const hasField = !!condition.field;
|
|
511
|
-
const opSpec = colInfo ? getOperatorSpec(colInfo.type, condition.op) : null;
|
|
512
|
-
const valueShape = colInfo && opSpec ? getValueShape(opSpec, colInfo.type, colInfo.includeTime) : null;
|
|
513
|
-
const hasValueCell = valueShape !== "none";
|
|
514
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
515
|
-
/* @__PURE__ */ jsx(ConjunctionLabel, { index, conjunction, onChange: onChangeConjunction }),
|
|
516
|
-
/* @__PURE__ */ jsxs(FieldControlGroup, { block: true, className: "flex-1 min-w-0", children: [
|
|
517
|
-
/* @__PURE__ */ jsx(
|
|
518
|
-
Select,
|
|
519
|
-
{
|
|
520
|
-
className: "!w-[var(--data-table-filter-field-width)] flex-shrink-0",
|
|
521
|
-
size: "sm",
|
|
522
|
-
options: fieldOptions,
|
|
523
|
-
value: condition.field,
|
|
524
|
-
onChange: onChangeField,
|
|
525
|
-
placeholder: "選擇欄位",
|
|
526
|
-
"aria-label": "篩選欄位"
|
|
527
|
-
}
|
|
528
|
-
),
|
|
529
|
-
/* @__PURE__ */ jsx(
|
|
530
|
-
Select,
|
|
531
|
-
{
|
|
532
|
-
className: hasValueCell ? "!w-[var(--data-table-filter-op-width)] flex-shrink-0" : "!flex-1 !min-w-0",
|
|
533
|
-
size: "sm",
|
|
534
|
-
options: operatorOptions,
|
|
535
|
-
value: condition.op,
|
|
536
|
-
onChange: onChangeOp,
|
|
537
|
-
disabled: !hasField,
|
|
538
|
-
placeholder: "運算子",
|
|
539
|
-
"aria-label": "篩選運算子"
|
|
540
|
-
}
|
|
541
|
-
),
|
|
542
|
-
hasValueCell && /* @__PURE__ */ jsx(
|
|
543
|
-
FilterValuePicker,
|
|
544
|
-
{
|
|
545
|
-
shape: valueShape,
|
|
546
|
-
value: condition.value,
|
|
547
|
-
onChange: onChangeValue,
|
|
548
|
-
colInfo,
|
|
549
|
-
disabled: !hasField,
|
|
550
|
-
ariaLabel: colInfo ? `${colInfo.label} 篩選值` : "篩選值",
|
|
551
|
-
className: "!flex-1 !min-w-0"
|
|
552
|
-
}
|
|
553
|
-
)
|
|
554
|
-
] }),
|
|
555
|
-
/* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", iconOnly: true, startIcon: Trash2, "aria-label": "刪除", onClick: onRemove })
|
|
556
|
-
] });
|
|
557
|
-
}
|
|
558
|
-
function GroupBlock({
|
|
559
|
-
index,
|
|
560
|
-
group,
|
|
561
|
-
rootConjunction,
|
|
562
|
-
filterableColumns,
|
|
563
|
-
fieldOptions,
|
|
564
|
-
onChangeRootConjunction,
|
|
565
|
-
onChangeGroupConjunction,
|
|
566
|
-
onChangeCondition,
|
|
567
|
-
onRemoveCondition,
|
|
568
|
-
onAddCondition,
|
|
569
|
-
onRemoveGroup
|
|
570
|
-
}) {
|
|
571
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
|
|
572
|
-
/* @__PURE__ */ jsx("div", { className: "pt-2", children: /* @__PURE__ */ jsx(ConjunctionLabel, { index, conjunction: rootConjunction, onChange: onChangeRootConjunction }) }),
|
|
573
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 rounded-md bg-muted p-2 flex flex-col gap-2", children: [
|
|
574
|
-
group.children.map((cond, cIdx) => /* @__PURE__ */ jsx(
|
|
575
|
-
FilterRow,
|
|
576
|
-
{
|
|
577
|
-
index: cIdx,
|
|
578
|
-
condition: cond,
|
|
579
|
-
conjunction: group.conjunction,
|
|
580
|
-
filterableColumns,
|
|
581
|
-
fieldOptions,
|
|
582
|
-
onChangeConjunction: onChangeGroupConjunction,
|
|
583
|
-
onChangeField: (v) => {
|
|
584
|
-
const newCol = filterableColumns.find((c) => c.id === v);
|
|
585
|
-
onChangeCondition(cond.id, { field: v, op: getDefaultOperator(newCol == null ? void 0 : newCol.type), value: "", datePrecision: datePrecisionOf(newCol) });
|
|
586
|
-
},
|
|
587
|
-
onChangeOp: (v) => onChangeCondition(cond.id, { op: v, value: "" }),
|
|
588
|
-
onChangeValue: (v) => onChangeCondition(cond.id, { value: v }),
|
|
589
|
-
onRemove: () => onRemoveCondition(cond.id)
|
|
590
|
-
},
|
|
591
|
-
cond.id
|
|
592
|
-
)),
|
|
593
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
594
|
-
/* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", startIcon: Plus, onClick: onAddCondition, children: "加入巢狀篩選" }),
|
|
595
|
-
group.children.length === 0 && /* @__PURE__ */ jsx(Button, { variant: "text", size: "sm", startIcon: Trash2, danger: true, onClick: onRemoveGroup, children: "移除空群組" })
|
|
596
|
-
] })
|
|
597
|
-
] })
|
|
598
|
-
] });
|
|
599
|
-
}
|
|
600
278
|
export {
|
|
601
279
|
DataTableFilterPanel,
|
|
602
280
|
createEmptyFilterTree,
|