Skip to content

Commit 86213e1

Browse files
interacseanclaude
andcommitted
feat(data-table): friendlier date filter operators (exact date / after / before / between)
Date columns now offer a slimmer, plainer operator set instead of the numeric comparators: - eq → "exact date" - gte → "after" (inclusive) - lte → "before" (inclusive) - between → "between" - dropped gt, lt, and ne (the inclusive after/before cover the intent) Scoped to `date` columns only — number/datetime/time keep the full numeric operator set and labels. Labels added for en + ja; chip labels and both the add-filter and edit-chip operator selects use them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 77ea85e commit 86213e1

3 files changed

Lines changed: 81 additions & 14 deletions

File tree

packages/core/src/components/data-table/i18n.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export const dataTableLabels = defineI18nLabels({
5050
filterOperator_between: "between",
5151
filterOperator_in: "in",
5252
filterOperator_nin: "not in",
53+
// Date-specific operator labels (date filters drop gt/lt/ne and treat the
54+
// boundary as inclusive).
55+
filterDateOperator_eq: "exact date",
56+
filterDateOperator_gte: "after",
57+
filterDateOperator_lte: "before",
5358
filterBetweenFrom: "From",
5459
filterBetweenTo: "To",
5560
filterBetweenMin: "Min",
@@ -102,6 +107,9 @@ export const dataTableLabels = defineI18nLabels({
102107
filterOperator_between: "の範囲内",
103108
filterOperator_in: "次のいずれか",
104109
filterOperator_nin: "次のいずれでもない",
110+
filterDateOperator_eq: "指定日",
111+
filterDateOperator_gte: "以降",
112+
filterDateOperator_lte: "以前",
105113
filterBetweenFrom: "開始",
106114
filterBetweenTo: "終了",
107115
filterBetweenMin: "最小",

packages/core/src/components/data-table/toolbar.test.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ describe("DateFilterEditor", () => {
502502
wrapper,
503503
});
504504

505-
await user.click(screen.getByRole("button", { name: /Created At equals 2025-01-01/ }));
505+
await user.click(screen.getByRole("button", { name: /Created At exact date 2025-01-01/ }));
506506

507507
expect(await screen.findByRole("button", { name: "Apply" })).toBeDefined();
508508
});
@@ -514,7 +514,7 @@ describe("DateFilterEditor", () => {
514514
});
515515
render(<TestFilters control={control} columns={[dateColumn]} />, { wrapper });
516516

517-
await user.click(screen.getByRole("button", { name: /Created At equals 2025-01-01/ }));
517+
await user.click(screen.getByRole("button", { name: /Created At exact date 2025-01-01/ }));
518518

519519
// The editor uses the app-shell DatePicker: a labelled group of spinbuttons.
520520
expect(
@@ -529,7 +529,7 @@ describe("DateFilterEditor", () => {
529529
});
530530
render(<TestFilters control={control} columns={[dateColumn]} />, { wrapper });
531531

532-
await user.click(screen.getByRole("button", { name: /Created At equals 2025-01-01/ }));
532+
await user.click(screen.getByRole("button", { name: /Created At exact date 2025-01-01/ }));
533533
await screen.findByRole("button", { name: "Apply" });
534534

535535
await typeDateInto(user, 0, "2026-06-15");
@@ -545,14 +545,51 @@ describe("DateFilterEditor", () => {
545545
});
546546
render(<TestFilters control={control} columns={[dateColumn]} />, { wrapper });
547547

548-
await user.click(screen.getByRole("button", { name: /Created At equals 2025-01-01/ }));
548+
await user.click(screen.getByRole("button", { name: /Created At exact date 2025-01-01/ }));
549549
await screen.findByRole("button", { name: "Apply" });
550550

551551
await clearDateIn(user, 0);
552552
await user.click(screen.getByRole("button", { name: "Apply" }));
553553

554554
expect(control.removeFilter).toHaveBeenCalledWith("createdAt");
555555
});
556+
557+
it("labels date operators as exact date / after / before", () => {
558+
const control = makeControl({
559+
filters: [
560+
{ field: "createdAt", operator: "eq", value: "2025-01-01" },
561+
{ field: "createdAt", operator: "gte", value: "2025-02-01" },
562+
{ field: "createdAt", operator: "lte", value: "2025-03-01" },
563+
],
564+
});
565+
// Render each via its own chip; assert the friendly date labels appear and
566+
// the numeric labels do not.
567+
const { rerender } = render(
568+
<TestFilters
569+
control={makeControl({ filters: [control.filters[0]] })}
570+
columns={[dateColumn]}
571+
/>,
572+
{ wrapper },
573+
);
574+
expect(screen.getByText(/Created At exact date 2025-01-01/)).toBeDefined();
575+
576+
rerender(
577+
<TestFilters
578+
control={makeControl({ filters: [control.filters[1]] })}
579+
columns={[dateColumn]}
580+
/>,
581+
);
582+
expect(screen.getByText(/Created At after 2025-02-01/)).toBeDefined();
583+
expect(screen.queryByText(/greater than/)).toBeNull();
584+
585+
rerender(
586+
<TestFilters
587+
control={makeControl({ filters: [control.filters[2]] })}
588+
columns={[dateColumn]}
589+
/>,
590+
);
591+
expect(screen.getByText(/Created At before 2025-03-01/)).toBeDefined();
592+
});
556593
});
557594

558595
// ---------------------------------------------------------------------------

packages/core/src/components/data-table/toolbar.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ const DEFAULT_OPERATOR: Record<FilterConfig["type"], FilterOperator> = {
5454
const NUMERIC_TEMPORAL_OPERATORS = ["eq", "ne", "gt", "gte", "lt", "lte", "between"] as const;
5555
type NumericTemporalOperator = (typeof NUMERIC_TEMPORAL_OPERATORS)[number];
5656

57+
// Date filters use a slimmer, friendlier operator set: an exact date, inclusive
58+
// "after"/"before" (gte/lte), and a between range. No gt/lt/ne.
59+
const DATE_OPERATORS = ["eq", "gte", "lte", "between"] as const;
60+
61+
/** Operators offered for a temporal editor, narrowed for `date` columns. */
62+
function temporalOperatorsFor(type: FilterConfig["type"]): readonly NumericTemporalOperator[] {
63+
return type === "date" ? DATE_OPERATORS : NUMERIC_TEMPORAL_OPERATORS;
64+
}
65+
5766
/** String operators available in the operator selector. */
5867
const STRING_OPERATORS = ["eq", "ne", "contains", "notContains", "hasPrefix", "hasSuffix"] as const;
5968
type StringOperator = (typeof STRING_OPERATORS)[number];
@@ -489,7 +498,7 @@ function AddFilterPopover({
489498
}}
490499
mapItem={(op) => ({
491500
value: op,
492-
label: getOperatorLabel(op, t),
501+
label: getOperatorLabel(op, t, selectedColumn?.filter.type),
493502
})}
494503
className="astw:h-8 astw:text-sm"
495504
/>
@@ -920,8 +929,9 @@ function NumericFilterEditor({
920929
onClose: () => void;
921930
}) {
922931
const t = useDataTableT();
932+
const operatorItems = temporalOperatorsFor(config.type);
923933
const [localOp, setLocalOp] = useState<NumericTemporalOperator>(
924-
NUMERIC_TEMPORAL_OPERATORS.includes(filter.operator as NumericTemporalOperator)
934+
operatorItems.includes(filter.operator as NumericTemporalOperator)
925935
? (filter.operator as NumericTemporalOperator)
926936
: "eq",
927937
);
@@ -985,12 +995,12 @@ function NumericFilterEditor({
985995
className="astw:flex astw:flex-col astw:gap-2 astw:p-2"
986996
>
987997
<Select
988-
items={[...NUMERIC_TEMPORAL_OPERATORS]}
998+
items={[...operatorItems]}
989999
value={localOp}
9901000
onValueChange={(v) => {
9911001
if (v) setLocalOp(v);
9921002
}}
993-
mapItem={(op) => ({ value: op, label: t(`filterOperator_${op}`) })}
1003+
mapItem={(op) => ({ value: op, label: getOperatorLabel(op, t, config.type) })}
9941004
className="astw:h-8 astw:text-sm"
9951005
/>
9961006
{localOp === "between" ? (
@@ -1036,8 +1046,9 @@ function TemporalFilterEditor({
10361046
onClose: () => void;
10371047
}) {
10381048
const t = useDataTableT();
1049+
const operatorItems = temporalOperatorsFor(config.type);
10391050
const [localOp, setLocalOp] = useState<NumericTemporalOperator>(
1040-
NUMERIC_TEMPORAL_OPERATORS.includes(filter.operator as NumericTemporalOperator)
1051+
operatorItems.includes(filter.operator as NumericTemporalOperator)
10411052
? (filter.operator as NumericTemporalOperator)
10421053
: "eq",
10431054
);
@@ -1151,12 +1162,12 @@ function TemporalFilterEditor({
11511162
className="astw:flex astw:flex-col astw:gap-2 astw:p-2"
11521163
>
11531164
<Select
1154-
items={[...NUMERIC_TEMPORAL_OPERATORS]}
1165+
items={[...operatorItems]}
11551166
value={localOp}
11561167
onValueChange={(v) => {
11571168
if (v) setLocalOp(v);
11581169
}}
1159-
mapItem={(op) => ({ value: op, label: t(`filterOperator_${op}`) })}
1170+
mapItem={(op) => ({ value: op, label: getOperatorLabel(op, t, config.type) })}
11601171
className="astw:h-8 astw:text-sm"
11611172
/>
11621173
{valueInput}
@@ -1175,9 +1186,10 @@ function getAddFilterOperators(type: FilterConfig["type"]): FilterOperator[] {
11751186
switch (type) {
11761187
case "string":
11771188
return [...STRING_OPERATORS];
1189+
case "date":
1190+
return [...DATE_OPERATORS];
11781191
case "number":
11791192
case "datetime":
1180-
case "date":
11811193
case "time":
11821194
return [...NUMERIC_TEMPORAL_OPERATORS];
11831195
case "enum":
@@ -1311,7 +1323,17 @@ function getTemporalInputProps(type: "datetime" | "date" | "time") {
13111323
}
13121324
}
13131325

1314-
function getOperatorLabel(operator: FilterOperator, t: ReturnType<typeof useDataTableT>): string {
1326+
function getOperatorLabel(
1327+
operator: FilterOperator,
1328+
t: ReturnType<typeof useDataTableT>,
1329+
type?: FilterConfig["type"],
1330+
): string {
1331+
// Date columns relabel the comparison operators (exact date / after / before).
1332+
if (type === "date") {
1333+
if (operator === "eq") return t("filterDateOperator_eq");
1334+
if (operator === "gte") return t("filterDateOperator_gte");
1335+
if (operator === "lte") return t("filterDateOperator_lte");
1336+
}
13151337
switch (operator) {
13161338
case "eq":
13171339
return t("filterOperator_eq");
@@ -1397,7 +1419,7 @@ function getChipDisplayLabel(
13971419
const valueLabel = formatFilterValue(filter, config, t);
13981420
if (!valueLabel) return columnLabel;
13991421

1400-
const operatorLabel = getOperatorLabel(filter.operator, t);
1422+
const operatorLabel = getOperatorLabel(filter.operator, t, config.type);
14011423
const ciSuffix = filter.caseSensitive ? " (Aa)" : "";
14021424

14031425
if (config.type === "enum") {

0 commit comments

Comments
 (0)