|
1 |
| -import { valibotResolver } from "@hookform/resolvers/valibot"; |
2 |
| -import { NativeField as Field, Form } from "components/form"; |
3 |
| -import { Modal } from "components/modal"; |
4 |
| -import { FilterIcon } from "lucide-react"; |
5 |
| -import { useState } from "react"; |
| 1 | +import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; |
| 2 | +import { |
| 3 | + NativeCheckField as CheckField, |
| 4 | + NativeField as Field, |
| 5 | +} from "components/form"; |
| 6 | +import { ListFilterIcon } from "lucide-react"; |
6 | 7 | import { useForm } from "react-hook-form";
|
7 |
| -import * as v from "valibot"; |
8 |
| - |
9 |
| -type Val = [number, number]; |
10 |
| -interface Init { |
11 |
| - min: number; |
12 |
| - max: number; |
13 |
| -} |
14 | 8 |
|
15 | 9 | interface Props {
|
16 |
| - value: Val | Init; |
17 |
| - onChange: (value: [number, number]) => void; |
| 10 | + label: string; |
| 11 | + _key: string; |
| 12 | + values?: (k: string) => string[]; |
| 13 | + onChange?: (v: string[], k: string) => void; |
18 | 14 | classes?: string;
|
19 | 15 | }
|
20 | 16 |
|
21 |
| -const amnt = v.pipe( |
22 |
| - v.string(), |
23 |
| - v.transform((x) => +x), |
24 |
| - v.minValue(0), |
25 |
| - v.transform((x) => +x) |
26 |
| -); |
27 |
| -const schema = v.pipe( |
28 |
| - v.object({ |
29 |
| - min: v.optional(amnt), |
30 |
| - max: v.optional(amnt), |
31 |
| - }), |
32 |
| - v.forward( |
33 |
| - v.partialCheck( |
34 |
| - [["min"], ["max"]], |
35 |
| - ({ min, max }) => (min && max ? min <= max : true), |
36 |
| - "min must be less than or equal to max" |
37 |
| - ), |
38 |
| - ["min"] |
39 |
| - ) |
40 |
| -); |
41 |
| - |
42 |
| -interface Fv extends v.InferInput<typeof schema> {} |
43 |
| -interface FvParsed extends v.InferOutput<typeof schema> {} |
44 |
| - |
45 | 17 | export function RangeFilter(props: Props) {
|
46 |
| - const [open, setOpen] = useState(false); |
47 |
| - const isInit = !props.value || !Array.isArray(props.value); |
48 |
| - |
49 |
| - const vmin = |
50 |
| - props.value && Array.isArray(props.value) |
51 |
| - ? props.value[0] |
52 |
| - : props.value?.min; |
53 |
| - const vmax = |
54 |
| - props.value && Array.isArray(props.value) |
55 |
| - ? props.value[1] |
56 |
| - : props.value?.max; |
| 18 | + const raw = props.values?.(props._key) || []; |
| 19 | + const is_active = raw.length > 0; |
| 20 | + const is_blank = raw.includes("blank"); |
| 21 | + const is_exists = raw.includes("exists"); |
| 22 | + const range = raw.filter((t) => t !== "blank" && t !== "exists"); |
57 | 23 |
|
58 | 24 | const {
|
59 |
| - handleSubmit, |
60 | 25 | register,
|
61 |
| - formState: { errors }, |
62 |
| - } = useForm<Fv>({ |
63 |
| - resolver: valibotResolver(schema), |
64 |
| - defaultValues: { min: vmin?.toString(), max: vmax?.toString() }, |
| 26 | + handleSubmit, |
| 27 | + reset, |
| 28 | + formState: { isDirty, errors }, |
| 29 | + watch, |
| 30 | + } = useForm({ |
| 31 | + values: { |
| 32 | + blank: is_blank, |
| 33 | + exists: is_exists, |
| 34 | + min: range[0] || "", |
| 35 | + max: range[1] || "", |
| 36 | + }, |
65 | 37 | });
|
| 38 | + |
| 39 | + const min = watch("min"); |
66 | 40 | return (
|
67 |
| - <div className={`flex items-center ${props.classes}`}> |
68 |
| - <button type="button"> |
69 |
| - <FilterIcon |
70 |
| - size={16} |
71 |
| - className={`${isInit ? "text-gray-l1" : "text-blue-d1"}`} |
| 41 | + <Popover className="relative flex items-start justify-between gap-x-2"> |
| 42 | + <p>{props.label}</p> |
| 43 | + <PopoverButton className="mt-1"> |
| 44 | + <ListFilterIcon |
| 45 | + size={14} |
| 46 | + className={`${is_active ? "text-green stroke-3" : ""}`} |
72 | 47 | />
|
73 |
| - </button> |
74 |
| - <Modal |
75 |
| - open={open} |
76 |
| - onClose={() => setOpen(false)} |
77 |
| - classes="max-w-xl rounded" |
| 48 | + </PopoverButton> |
| 49 | + <PopoverPanel |
| 50 | + as="form" |
| 51 | + anchor={{ to: "bottom", gap: 8 }} |
| 52 | + className="bg-white w-max border border-gray-l3 p-2 grid rounded-sm gap-2" |
| 53 | + onReset={(e) => { |
| 54 | + e.preventDefault(); |
| 55 | + reset(undefined, { keepDirtyValues: false }); |
| 56 | + props.onChange?.([], props._key); |
| 57 | + }} |
| 58 | + onSubmit={handleSubmit(({ blank, exists, ...texts }) => { |
| 59 | + const text = Object.values(texts) as string[]; |
| 60 | + const vals = text |
| 61 | + .concat([blank ? "blank" : "", exists ? "exists" : ""]) |
| 62 | + .filter((x) => x); |
| 63 | + props.onChange?.(vals, props._key); |
| 64 | + })} |
78 | 65 | >
|
79 |
| - <Form |
80 |
| - onSubmit={handleSubmit((fv) => { |
81 |
| - const { min, max } = fv as FvParsed; |
| 66 | + <CheckField {...register("blank")} classes="text-xs"> |
| 67 | + Blank |
| 68 | + </CheckField> |
| 69 | + <CheckField {...register("exists")} classes="mb-2 text-xs"> |
| 70 | + Exists |
| 71 | + </CheckField> |
82 | 72 |
|
83 |
| - props.onChange([min, max]); |
84 |
| - setOpen(false); |
85 |
| - })} |
86 |
| - className="rounded p-4" |
87 |
| - > |
88 |
| - <Field label="Min" {...register("min")} error={errors.min?.message} /> |
89 |
| - <Field label="Max" {...register("min")} error={errors.max?.message} /> |
90 |
| - <button className="btn btn-blue px-4 py-1 text-sm">Apply</button> |
91 |
| - </Form> |
92 |
| - </Modal> |
93 |
| - </div> |
| 73 | + <Field |
| 74 | + label="Min" |
| 75 | + type="number" |
| 76 | + error={errors.min?.message} |
| 77 | + {...register("min", { min: 0 })} |
| 78 | + classes={{ input: "text-xs py-1 px-2", label: "mb-0! text-xs" }} |
| 79 | + /> |
| 80 | + <Field |
| 81 | + label="Max" |
| 82 | + type="number" |
| 83 | + error={errors.max?.message} |
| 84 | + {...register("max", { min, max: 10_000_000_000 })} |
| 85 | + classes={{ input: "text-xs py-1 px-2", label: "mb-0! text-xs" }} |
| 86 | + /> |
| 87 | + <div className="flex justify-end space-x-2 mt-4"> |
| 88 | + <button type="reset" className="btn btn-outline text-xs px-2 py-1"> |
| 89 | + clear |
| 90 | + </button> |
| 91 | + <button |
| 92 | + disabled={!isDirty} |
| 93 | + type="submit" |
| 94 | + className="btn btn-blue text-xs px-2 py-1" |
| 95 | + > |
| 96 | + apply |
| 97 | + </button> |
| 98 | + </div> |
| 99 | + </PopoverPanel> |
| 100 | + </Popover> |
94 | 101 | );
|
95 | 102 | }
|
0 commit comments