diff --git a/comps/filter/FilterField.tsx b/comps/filter/FilterField.tsx index 1154eb5..d39e144 100755 --- a/comps/filter/FilterField.tsx +++ b/comps/filter/FilterField.tsx @@ -1,15 +1,16 @@ import { FC, useEffect } from "react"; import { BaseField } from "../form/base/BaseField"; -import { FilterLocal, filter_window } from "./utils/types"; +import { FilterFieldType, FilterLocal, filter_window } from "./utils/types"; import { FieldTypeText } from "../form/field/type/TypeText"; import { FieldModifier } from "./FieldModifier"; import { useLocal } from "lib/utils/use-local"; +import { FieldToggle } from "../form/field/type/TypeToggle"; export const FilterField: FC<{ filter: FilterLocal; name?: string; label?: string; - type: "text" | "number" | "boolean"; + type: FilterFieldType; }> = ({ filter, name, label, type }) => { const internal = useLocal({ render_timeout: null as any }); if (!name) return <>No Name; @@ -67,6 +68,33 @@ export const FilterField: FC<{ }} /> )} + {type === "date" && ( + <> + + {filter.modifiers[name] === 'Between' && ( + + )} + + )} + {type === "boolean" && ( + + )} )} diff --git a/comps/filter/utils/filter-where.ts b/comps/filter/utils/filter-where.ts index 515b326..3feb726 100755 --- a/comps/filter/utils/filter-where.ts +++ b/comps/filter/utils/filter-where.ts @@ -17,21 +17,71 @@ export const filterWhere = (filter_name: string) => { { if (modifier === "contains") where[name] = { - contains: value, + contains: "%" + value + "%", mode: "insensitive", }; + else if (modifier === "starts_with") + where[name] = { + contains: value + "%", + mode: "insensitive", + }; + else if (modifier === "ends_with") + where[name] = { + contains: "%" + value, + mode: "insensitive", + }; + else if (modifier === "not_equal") { + where[name] = { + NOT: value, + }; + } else if (modifier === "equal") { + where[name] = { + value, + }; + } } break; - case "date": { - let is_value_valid = false; - // TODO: pastikan value bisa diparse pakai any-date-parser - if (is_value_valid) { - if (modifier === "between") { + case "date": + { + let is_value_valid = false; + // TODO: pastikan value bisa diparse pakai any-date-parser + if (is_value_valid) { + if (modifier === "between") { + AND.push({ [name]: { gt: value } }); + AND.push({ [name]: { lt: value } }); + } else if (modifier === "greater_than") { + AND.push({ [name]: { gt: value } }); + } else if (modifier === "less_than") { + AND.push({ [name]: { lt: value } }); + } + } + } + break; + case "number": + { + if (modifier === "equal") { + AND.push({ [name]: { value } }); + } else if (modifier === "not_equal") { + AND.push({ [name]: { NOT: value } }); + } else if (modifier === "greater_than") { + AND.push({ [name]: { gt: value } }); + } else if (modifier === "less_than") { + AND.push({ [name]: { lt: value } }); + } else if (modifier === "between") { AND.push({ [name]: { gt: value } }); AND.push({ [name]: { lt: value } }); } } - } + break; + case "boolean": + { + if (modifier === "is_true") { + AND.push({ [name]: true }); + } else if (modifier === "is_false") { + AND.push({ [name]: false }); + } + } + break; } } } diff --git a/comps/filter/utils/types.tsx b/comps/filter/utils/types.tsx index b28c501..a5d7cd3 100755 --- a/comps/filter/utils/types.tsx +++ b/comps/filter/utils/types.tsx @@ -14,11 +14,22 @@ export const default_filter_local = { }; export const modifiers = { - text: { contains: "Contains", ends_with: "Ends With" }, - boolean: {}, - number: {}, + text: { contains: "Contains", ends_with: "Ends With", equal: "Equal", not_equal: "Not Equal" }, + boolean: { + is_true: "Is True", + is_false: "Is False" + }, + number: { + equal: "Equal", + not_equal: "Not Equal", + between: "Between", + greater_than: "Greater Than", + less_than: "Less Than" + }, date: { between: "Between", + greater_than: "Greater Than", + less_than: "Less Than" }, }; export type FilterModifier = typeof modifiers; diff --git a/comps/md/gen/form/fields.ts b/comps/md/gen/form/fields.ts index d0979eb..7f140d5 100755 --- a/comps/md/gen/form/fields.ts +++ b/comps/md/gen/form/fields.ts @@ -12,7 +12,10 @@ export type GFCol = { fields: GFCol[]; }; }; -export const newField = (arg: GFCol, opt: { parent_table: string, value: Array }) => { +export const newField = ( + arg: GFCol, + opt: { parent_table: string; value: Array } +) => { console.log({ arg, opt }); let type = "input"; if (["int", "string", "text"].includes(arg.type)) { @@ -49,6 +52,82 @@ export const newField = (arg: GFCol, opt: { parent_table: string, value: Array { + console.log("halo"); + return { + label: "halo", value: "value" + } + } + `, + ], + child: { + childs: [], + }, + }, + }, + }); + // return { + // name: "item", + // type: "item", + // component: { + // id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67", + // props: { + // name: { + // mode: "string", + // value: arg.name + // }, + // label: { + // mode: "string", + // value: formatName(arg.name) + // }, + // type: { + // mode: "string", + // value: "single-option" + // }, + // sub_type: { + // mode: "string", + // value: "dropdown" + // }, + // rel__gen_table: { + // mode: "string", + // value: arg.name + // }, + // rel__gen_fields: { + // mode: "raw", + // value: `${JSON.stringify(opt.val)}` + // } + // }, + // }, + // }; + } else { + return createItem({ component: { id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67", props: { @@ -62,74 +141,7 @@ export const newField = (arg: GFCol, opt: { parent_table: string, value: Array, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/comps/ui/typeahead.tsx b/comps/ui/typeahead.tsx new file mode 100755 index 0000000..fec5fa2 --- /dev/null +++ b/comps/ui/typeahead.tsx @@ -0,0 +1,437 @@ +import { useLocal } from "lib/utils/use-local"; +import { X } from "lucide-react"; +import { FC, KeyboardEvent, useCallback, useEffect, useRef } from "react"; +import { Popover } from "../custom/Popover"; +import { Badge } from "./badge"; + +export const Typeahead: FC<{ + value?: string[]; + options?: (arg: { + search: string; + existing: { value: string; label: string }[]; + }) => + | (string | { value: string; label: string })[] + | Promise<(string | { value: string; label: string })[]>; + onSelect?: (arg: { + search: string; + item?: null | { value: string; label: string }; + }) => string | false; + unique?: boolean; + allowNew?: boolean; + localSearch?: boolean; + focusOpen?: boolean; +}> = ({ + value, + options: options_fn, + onSelect, + unique, + allowNew: allow_new, + focusOpen: on_focus_open, + localSearch: local_search, +}) => { + const local = useLocal({ + value: [] as string[], + open: false, + options: [] as { value: string; label: string }[], + loaded: false, + search: { + input: "", + timeout: null as any, + searching: false, + promise: null as any, + result: null as null | { value: string; label: string }[], + }, + unique: typeof unique === "undefined" ? true : unique, + allow_new: typeof allow_new === "undefined" ? true : allow_new, + on_focus_open: typeof on_focus_open === "undefined" ? false : on_focus_open, + local_search: typeof local_search === "undefined" ? true : local_search, + select: null as null | { value: string; label: string }, + }); + const input = useRef(null); + + let select_found = false; + let options = [...(local.search.result || local.options)]; + if (local.allow_new && local.search.input) { + options.push({ value: local.search.input, label: local.search.input }); + } + const added = new Set(); + options = options.filter((e) => { + if (!added.has(e.value)) added.add(e.value); + else return false; + if (local.select && local.select.value === e.value) select_found = true; + if (local.unique) { + if (local.value.includes(e.value)) { + return false; + } + } + return true; + }); + + if (!select_found) { + local.select = options[0]; + } + + useEffect(() => { + if (typeof value === "object" && value) { + local.value = value; + local.render(); + } + }, [value]); + + const select = useCallback( + (arg: { + search: string; + item?: null | { value: string; label: string }; + }) => { + if (!local.allow_new) { + let found = null; + if (!arg.item) { + found = options.find((e) => e.value === arg.search); + } else { + found = options.find((e) => e.value === arg.item?.value); + } + if (!found) { + return false; + } + } + + if (local.unique) { + let found = local.value.find((e) => { + return e === arg.item?.value || arg.search === e; + }); + if (found) { + return false; + } + } + + if (typeof onSelect === "function") { + const result = onSelect(arg); + + if (result) { + local.value.push(result); + local.render(); + } else { + return false; + } + } else { + if (arg.item) { + local.value.push(arg.item.value); + } else { + if (!arg.search) return false; + local.value.push(arg.search); + } + local.render(); + } + return true; + }, + [onSelect, local.value, options] + ); + + const keydown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Backspace") { + if (local.value.length > 0 && e.currentTarget.selectionStart === 0) { + local.value.pop(); + local.render(); + } + } + if (e.key === "Enter") { + const selected = select({ + search: local.search.input, + item: local.select, + }); + + if (selected) { + resetSearch(); + local.render(); + } + } + if (options.length > 0) { + if (e.key === "ArrowDown") { + e.preventDefault(); + const idx = options.findIndex((item) => { + if (item.value === local.select?.value) return true; + }); + if (idx >= 0) { + if (idx + 1 <= options.length) { + local.select = options[idx + 1]; + } else { + local.select = options[0]; + } + } else { + local.select = options[0]; + } + local.render(); + } + if (e.key === "ArrowUp") { + e.preventDefault(); + + const idx = options.findIndex((item) => { + if (item.value === local.select?.value) return true; + }); + if (idx >= 0) { + if (idx + 1 < options.length) { + local.select = options[idx + 1]; + } else { + local.select = options[0]; + } + } else { + local.select = options[0]; + } + local.render(); + } + } + }, + [local.value, local.select, select, options, local.search.input] + ); + + const openOptions = useCallback(async () => { + if (typeof options_fn === "function") { + local.loaded = true; + const res = options_fn({ + search: local.search.input, + existing: local.options, + }); + if (res) { + const applyOptions = ( + result: (string | { value: string; label: string })[] + ) => { + local.options = result.map((item) => { + if (typeof item === "string") return { value: item, label: item }; + return item; + }); + local.render(); + }; + if (res instanceof Promise) { + applyOptions(await res); + } else { + applyOptions(res); + } + } + } + }, [options_fn]); + + const resetSearch = () => { + local.search.searching = false; + local.search.input = ""; + local.search.promise = null; + local.search.result = null; + local.select = null; + clearTimeout(local.search.timeout); + }; + + return ( +
{ + input.current?.focus(); + }} + > + {local.value.map((e, idx) => { + return ( + { + ev.stopPropagation(); + ev.preventDefault(); + local.value = local.value.filter((val) => e !== val); + local.render(); + input.current?.focus(); + }} + > +
{e}
+ +
+ ); + })} + + { + if (!open) { + local.select = null; + } + local.open = open; + local.render(); + }} + open={local.open} + options={options} + searching={local.search.searching} + onSelect={(value) => { + local.open = false; + local.value.push(value); + resetSearch(); + local.render(); + }} + selected={local.select?.value} + > + { + e.stopPropagation(); + }} + onFocus={(e) => { + if (!local.open) { + if (local.on_focus_open) { + openOptions(); + local.open = true; + local.render(); + } + } + }} + onChange={async (e) => { + const val = e.currentTarget.value; + if (!local.open) { + local.open = true; + } + local.search.input = val; + local.render(); + + if (local.search.promise) { + await local.search.promise; + } + + local.search.searching = true; + local.render(); + + if (local.search.searching) { + if (local.local_search) { + if (!local.loaded) { + await openOptions(); + } + const search = local.search.input.toLowerCase(); + if (search) { + local.search.result = local.options.filter((e) => + e.label.toLowerCase().includes(search) + ); + } else { + local.search.result = null; + } + local.search.searching = false; + local.render(); + } else { + clearTimeout(local.search.timeout); + local.search.timeout = setTimeout(async () => { + const result = options_fn?.({ + search: local.search.input, + existing: local.options, + }); + if (result) { + if (result instanceof Promise) { + local.search.promise = result; + local.search.result = (await result).map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.search.searching = false; + local.search.promise = null; + local.render(); + } else { + local.search.result = result.map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.search.searching = false; + local.render(); + } + } + }, 100); + } + } + }} + spellCheck={false} + className={cx("c-flex-1 c-mb-2 c-text-sm c-outline-none")} + onKeyDown={keydown} + /> + +
+ ); +}; + +const WrapOptions: FC<{ + wrap: boolean; + children: any; + open: boolean; + onOpenChange: (open: boolean) => void; + options: { value: string; label: string }[]; + selected?: string; + onSelect: (value: string) => void; + searching?: boolean; +}> = ({ + wrap, + children, + open, + onOpenChange, + options, + selected, + onSelect, + searching, +}) => { + if (!wrap) return children; + + return ( + + {options.map((item, idx) => { + return ( +
0 && "c-border-t" + )} + onClick={() => { + onSelect(item.value); + }} + > + {item.label} +
+ ); + })} + + {searching ? ( +
+ Loading... +
+ ) : ( + <> + {options.length === 0 && ( +
+ — Empty — +
+ )} + + )} +
+ } + > + {children} + + ); +}; diff --git a/exports.ts b/exports.ts index 973e8be..0a62c11 100755 --- a/exports.ts +++ b/exports.ts @@ -97,3 +97,7 @@ export { Profile } from "@/preset/profile/Profile"; export { generateProfile } from "@/preset/profile/utils/generate"; export { ButtonUpload } from "@/preset/profile/ButtonUpload"; export { longDate, shortDate, timeAgo, formatTime } from "@/utils/date"; + + +export * from '@/comps/ui/typeahead' +export * from '@/comps/ui/input' \ No newline at end of file