fix typeahead
This commit is contained in:
parent
4433f5fcd1
commit
c7965e81b8
|
|
@ -1,15 +1,16 @@
|
||||||
import { FC, useEffect } from "react";
|
import { FC, useEffect } from "react";
|
||||||
import { BaseField } from "../form/base/BaseField";
|
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 { FieldTypeText } from "../form/field/type/TypeText";
|
||||||
import { FieldModifier } from "./FieldModifier";
|
import { FieldModifier } from "./FieldModifier";
|
||||||
import { useLocal } from "lib/utils/use-local";
|
import { useLocal } from "lib/utils/use-local";
|
||||||
|
import { FieldToggle } from "../form/field/type/TypeToggle";
|
||||||
|
|
||||||
export const FilterField: FC<{
|
export const FilterField: FC<{
|
||||||
filter: FilterLocal;
|
filter: FilterLocal;
|
||||||
name?: string;
|
name?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
type: "text" | "number" | "boolean";
|
type: FilterFieldType;
|
||||||
}> = ({ filter, name, label, type }) => {
|
}> = ({ filter, name, label, type }) => {
|
||||||
const internal = useLocal({ render_timeout: null as any });
|
const internal = useLocal({ render_timeout: null as any });
|
||||||
if (!name) return <>No Name</>;
|
if (!name) return <>No Name</>;
|
||||||
|
|
@ -67,6 +68,33 @@ export const FilterField: FC<{
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{type === "date" && (
|
||||||
|
<>
|
||||||
|
<FieldTypeText
|
||||||
|
{...field}
|
||||||
|
prop={{
|
||||||
|
type: "text",
|
||||||
|
sub_type: "date",
|
||||||
|
prefix: "",
|
||||||
|
suffix: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{filter.modifiers[name] === 'Between' && (
|
||||||
|
<FieldTypeText
|
||||||
|
{...field}
|
||||||
|
prop={{
|
||||||
|
type: "text",
|
||||||
|
sub_type: "date",
|
||||||
|
prefix: "",
|
||||||
|
suffix: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === "boolean" && (
|
||||||
|
<FieldToggle {...field} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</BaseField>
|
</BaseField>
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,71 @@ export const filterWhere = (filter_name: string) => {
|
||||||
{
|
{
|
||||||
if (modifier === "contains")
|
if (modifier === "contains")
|
||||||
where[name] = {
|
where[name] = {
|
||||||
contains: value,
|
contains: "%" + value + "%",
|
||||||
mode: "insensitive",
|
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;
|
break;
|
||||||
case "date": {
|
case "date":
|
||||||
let is_value_valid = false;
|
{
|
||||||
// TODO: pastikan value bisa diparse pakai any-date-parser
|
let is_value_valid = false;
|
||||||
if (is_value_valid) {
|
// TODO: pastikan value bisa diparse pakai any-date-parser
|
||||||
if (modifier === "between") {
|
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]: { gt: value } });
|
||||||
AND.push({ [name]: { lt: 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,22 @@ export const default_filter_local = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const modifiers = {
|
export const modifiers = {
|
||||||
text: { contains: "Contains", ends_with: "Ends With" },
|
text: { contains: "Contains", ends_with: "Ends With", equal: "Equal", not_equal: "Not Equal" },
|
||||||
boolean: {},
|
boolean: {
|
||||||
number: {},
|
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: {
|
date: {
|
||||||
between: "Between",
|
between: "Between",
|
||||||
|
greater_than: "Greater Than",
|
||||||
|
less_than: "Less Than"
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export type FilterModifier = typeof modifiers;
|
export type FilterModifier = typeof modifiers;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,10 @@ export type GFCol = {
|
||||||
fields: GFCol[];
|
fields: GFCol[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export const newField = (arg: GFCol, opt: { parent_table: string, value: Array<string> }) => {
|
export const newField = (
|
||||||
|
arg: GFCol,
|
||||||
|
opt: { parent_table: string; value: Array<string> }
|
||||||
|
) => {
|
||||||
console.log({ arg, opt });
|
console.log({ arg, opt });
|
||||||
let type = "input";
|
let type = "input";
|
||||||
if (["int", "string", "text"].includes(arg.type)) {
|
if (["int", "string", "text"].includes(arg.type)) {
|
||||||
|
|
@ -49,6 +52,82 @@ export const newField = (arg: GFCol, opt: { parent_table: string, value: Array<s
|
||||||
}
|
}
|
||||||
} else if (["timestamptz", "date"].includes(arg.type) && arg.relation) {
|
} else if (["timestamptz", "date"].includes(arg.type) && arg.relation) {
|
||||||
return createItem({
|
return createItem({
|
||||||
|
component: {
|
||||||
|
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
|
||||||
|
props: {
|
||||||
|
name: arg.name,
|
||||||
|
label: formatName(arg.name),
|
||||||
|
type: "date",
|
||||||
|
sub_type: "datetime",
|
||||||
|
child: {
|
||||||
|
childs: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (["has-many", "has-one"].includes(arg.type) && arg.relation) {
|
||||||
|
if (["has-one"].includes(arg.type)) {
|
||||||
|
return createItem({
|
||||||
|
component: {
|
||||||
|
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
|
||||||
|
props: {
|
||||||
|
name: arg.name,
|
||||||
|
label: formatName(arg.name),
|
||||||
|
type: "single-option",
|
||||||
|
sub_type: "dropdown",
|
||||||
|
rel__gen_table: arg.name,
|
||||||
|
rel__gen_fields: [`[${opt.value.join(",")}]`],
|
||||||
|
opt__on_load: [
|
||||||
|
`\
|
||||||
|
() => {
|
||||||
|
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: {
|
component: {
|
||||||
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
|
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -62,74 +141,7 @@ export const newField = (arg: GFCol, opt: { parent_table: string, value: Array<s
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (["has-many", "has-one"].includes(arg.type) && arg.relation) {
|
|
||||||
if(["has-one"].includes(arg.type)){
|
|
||||||
return createItem({
|
|
||||||
component: {
|
|
||||||
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
|
|
||||||
props: {
|
|
||||||
name: arg.name,
|
|
||||||
label: formatName(arg.name),
|
|
||||||
type: "single-option",
|
|
||||||
sub_type: "dropdown",
|
|
||||||
rel__gen_table: arg.name,
|
|
||||||
rel__gen_fields: [`[${opt.value.join(",")}]`],
|
|
||||||
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: {
|
|
||||||
name: arg.name,
|
|
||||||
label: formatName(arg.name),
|
|
||||||
type: "date",
|
|
||||||
sub_type: "datetime",
|
|
||||||
child: {
|
|
||||||
childs: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// type not found,
|
// type not found,
|
||||||
return createItem({
|
return createItem({
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,8 @@ export const generateForm = async (
|
||||||
childs: childs,
|
childs: childs,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
// await item.edit.commit();
|
await item.edit.commit();
|
||||||
|
// console.log("done")
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"c-inline-flex c-items-center c-rounded-full c-border c-px-2.5 c-py-0.5 c-text-xs c-font-semibold c-transition-colors focus:c-outline-none focus:c-ring-2 focus:c-ring-ring focus:c-ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"c-border-transparent c-bg-primary c-text-primary-foreground hover:c-bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"c-border-transparent c-bg-secondary c-text-secondary-foreground hover:c-bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"c-border-transparent c-bg-destructive c-text-destructive-foreground hover:c-bg-destructive/80",
|
||||||
|
outline: "c-text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
|
|
@ -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<HTMLInputElement>(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<string>();
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"c-flex c-cursor-text c-space-x-2 c-flex-wrap c-p-2 c-pb-0 c-items-center c-w-full c-h-full c-flex-1",
|
||||||
|
css`
|
||||||
|
min-height: 40px;
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
input.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{local.value.map((e, idx) => {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={idx}
|
||||||
|
variant={"outline"}
|
||||||
|
className="c-space-x-1 c-mb-2 c-cursor-pointer hover:c-bg-red-100"
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
local.value = local.value.filter((val) => e !== val);
|
||||||
|
local.render();
|
||||||
|
input.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{e}</div>
|
||||||
|
<X size={12} />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<WrapOptions
|
||||||
|
wrap={true}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
ref={input}
|
||||||
|
value={local.search.input}
|
||||||
|
onClick={(e) => {
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</WrapOptions>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
arrow={false}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
placement="bottom-start"
|
||||||
|
content={
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
css`
|
||||||
|
min-width: 150px;
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{options.map((item, idx) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
key={item.value + "_" + idx}
|
||||||
|
className={cx(
|
||||||
|
"c-px-3 c-py-1 cursor-pointer option-item",
|
||||||
|
item.value === selected
|
||||||
|
? "c-bg-blue-600 c-text-white"
|
||||||
|
: "hover:c-bg-blue-50",
|
||||||
|
idx > 0 && "c-border-t"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(item.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{searching ? (
|
||||||
|
<div className="c-px-4 c-w-full c-text-xs c-text-slate-400">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{options.length === 0 && (
|
||||||
|
<div className="c-p-4 c-w-full c-text-center c-text-sm c-text-slate-400">
|
||||||
|
— Empty —
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -97,3 +97,7 @@ export { Profile } from "@/preset/profile/Profile";
|
||||||
export { generateProfile } from "@/preset/profile/utils/generate";
|
export { generateProfile } from "@/preset/profile/utils/generate";
|
||||||
export { ButtonUpload } from "@/preset/profile/ButtonUpload";
|
export { ButtonUpload } from "@/preset/profile/ButtonUpload";
|
||||||
export { longDate, shortDate, timeAgo, formatTime } from "@/utils/date";
|
export { longDate, shortDate, timeAgo, formatTime } from "@/utils/date";
|
||||||
|
|
||||||
|
|
||||||
|
export * from '@/comps/ui/typeahead'
|
||||||
|
export * from '@/comps/ui/input'
|
||||||
Loading…
Reference in New Issue