feat: enhance form handling and loading states across components
This commit is contained in:
parent
66e2cdb966
commit
643659a26c
|
|
@ -219,6 +219,7 @@ export function Popover({
|
|||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={cx(
|
||||
"pointer-events-auto",
|
||||
popoverClassName
|
||||
? popoverClassName
|
||||
: cx(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export const Field: React.FC<{
|
|||
name: string;
|
||||
isBetter?: boolean;
|
||||
tooltip?: string;
|
||||
valueKey?: string;
|
||||
onLoad?: () => Promise<any> | any;
|
||||
onDelete?: (item: any) => Promise<any> | any;
|
||||
type?:
|
||||
| "rating"
|
||||
| "color"
|
||||
|
|
@ -73,6 +75,8 @@ export const Field: React.FC<{
|
|||
allowNew,
|
||||
unique = true,
|
||||
tooltip,
|
||||
valueKey,
|
||||
onDelete,
|
||||
}) => {
|
||||
let result = null;
|
||||
const field = useLocal({
|
||||
|
|
@ -182,6 +186,7 @@ export const Field: React.FC<{
|
|||
"single-checkbox",
|
||||
"radio",
|
||||
"checkbox",
|
||||
"multi-upload",
|
||||
].includes(type) &&
|
||||
css`
|
||||
border: 0px !important;
|
||||
|
|
@ -197,7 +202,7 @@ export const Field: React.FC<{
|
|||
<div
|
||||
// ref={prefixRef}
|
||||
className={cx(
|
||||
"px-1 py-1 items-center flex flex-row flex-grow rounded-l-md h-full",
|
||||
"px-1 py-1 items-center flex flex-row flex-grow rounded-l-md h-full prefix",
|
||||
css`
|
||||
height: 2.13rem;
|
||||
`
|
||||
|
|
@ -231,6 +236,8 @@ export const Field: React.FC<{
|
|||
mode={"upload"}
|
||||
type="multi"
|
||||
disabled={is_disable}
|
||||
valueKey={valueKey}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</>
|
||||
) : ["dropdown"].includes(type) ? (
|
||||
|
|
@ -354,7 +361,7 @@ export const Field: React.FC<{
|
|||
<div
|
||||
// ref={suffixRef}
|
||||
className={cx(
|
||||
"px-1 py-1 items-center flex flex-row flex-grow rounded-r-md h-full",
|
||||
"px-1 py-1 items-center flex flex-row flex-grow rounded-r-md h-full suffix",
|
||||
css`
|
||||
height: 2.13rem;
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -253,6 +253,111 @@ export const FilePreview = ({
|
|||
</>
|
||||
);
|
||||
};
|
||||
export const FilePreviewBetter = ({
|
||||
url,
|
||||
disabled,
|
||||
filename,
|
||||
}: {
|
||||
url: any;
|
||||
disabled?: boolean;
|
||||
filename?: string;
|
||||
}) => {
|
||||
const file: any = extractFileInfo(filename || url);
|
||||
const color = colorOfExtension(file.extension);
|
||||
let content = (
|
||||
<div
|
||||
className={cx(
|
||||
"flex items-center justify-center w-8 h-8 rounded-lg ",
|
||||
css`
|
||||
background: ${color?.background};
|
||||
border: 1px solid ${color?.color};
|
||||
color: ${color?.color};
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
padding: 0px 5px;
|
||||
font-size: 9px;
|
||||
margin-right: 5px;
|
||||
`,
|
||||
"flex items-center"
|
||||
)}
|
||||
>
|
||||
{file.extension}
|
||||
</div>
|
||||
);
|
||||
if (
|
||||
[".png", ".jpeg", ".jpg", ".webp"].find((e) => file?.fullname.endsWith(e))
|
||||
) {
|
||||
content = (
|
||||
<div className="rounded-lg flex-grow overflow-hidden">
|
||||
<img
|
||||
onClick={() => {
|
||||
let _url = siteurl(url || "");
|
||||
window.open(_url, "_blank");
|
||||
}}
|
||||
className={cx(
|
||||
"rounded-md w-8 h-8 object-cover",
|
||||
css`
|
||||
&:hover {
|
||||
outline: 2px solid #1c4ed8;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
background-image: linear-gradient(
|
||||
45deg,
|
||||
#ccc 25%,
|
||||
transparent 25%
|
||||
),
|
||||
linear-gradient(135deg, #ccc 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #ccc 75%),
|
||||
linear-gradient(135deg, transparent 75%, #ccc 75%);
|
||||
background-size: 25px 25px; /* Must be a square */
|
||||
background-position: 0 0, 12.5px 0, 12.5px -12.5px, 0px 12.5px; /* Must be half of one side of the square */
|
||||
`
|
||||
)}
|
||||
src={url}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{file.extension && (
|
||||
<div
|
||||
className={cx(
|
||||
"flex max-w-full rounded items-center px-1 cursor-pointer flex-grow hover:bg-gray-100 gap-x-1 justify-between",
|
||||
"pr-2",
|
||||
css`
|
||||
&:hover {
|
||||
// border: 1px solid #1c4ed8;
|
||||
// outline: 1px solid #1c4ed8;
|
||||
}
|
||||
&:hover {
|
||||
// border-bottom: 1px solid #1c4ed8;
|
||||
// outline: 1px solid #1c4ed8;
|
||||
}
|
||||
`,
|
||||
disabled ? "bg-transparent" : "bg-white"
|
||||
)}
|
||||
onClick={() => {
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row gap-x-1 items-center">
|
||||
<div className=" flex flex-row items-center">{content}</div>
|
||||
<div className="text-xs filename line-clamp-1 break-all">
|
||||
{file?.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ml-2">
|
||||
<ExternalLink size="12px" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
function darkenColor(color: string, factor: number = 0.5): string {
|
||||
const rgb = hexToRgb(color);
|
||||
const r = Math.floor(rgb.r * factor);
|
||||
|
|
@ -314,6 +419,26 @@ const getFileName = (url: string) => {
|
|||
return { name, extension, fullname };
|
||||
};
|
||||
|
||||
const extractFileInfo = (url: string) => {
|
||||
let fileName = url.split("/").pop();
|
||||
if (fileName) {
|
||||
let parts = fileName.split(".");
|
||||
let extension = parts.length > 1 ? parts.pop() : "";
|
||||
let name = parts.join(".");
|
||||
|
||||
return {
|
||||
name: name,
|
||||
fullname: fileName,
|
||||
extension: extension,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: null,
|
||||
fullname: null,
|
||||
extension: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
export const ImgThumb = ({
|
||||
className,
|
||||
url,
|
||||
|
|
@ -361,3 +486,35 @@ export const ImgThumb = ({
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getRandomColorPair = () => {
|
||||
const colors = [
|
||||
{ color: "#dc2626", background: "#fbd5d5" },
|
||||
{ color: "#2563eb", background: "#dbeafe" },
|
||||
{ color: "#16a34a", background: "#dcfce7" },
|
||||
{ color: "#6b7280", background: "#f3f4f6" },
|
||||
{ color: "#7c3aed", background: "#ede9fe" },
|
||||
{ color: "#f97316", background: "#ffedd5" },
|
||||
{ color: "#0d9488", background: "#ccfbf1" },
|
||||
{ color: "#9333ea", background: "#e9d5ff" },
|
||||
{ color: "#eab308", background: "#fef9c3" },
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
const colorOfExtension = (extension: string) => {
|
||||
const colorMap: any = {
|
||||
pdf: { color: "#dc2626", background: "#fbd5d5" },
|
||||
doc: { color: "#2563eb", background: "#dbeafe" },
|
||||
docx: { color: "#2563eb", background: "#dbeafe" },
|
||||
xls: { color: "#16a34a", background: "#dcfce7" },
|
||||
xlsx: { color: "#16a34a", background: "#dcfce7" },
|
||||
txt: { color: "#6b7280", background: "#f3f4f6" },
|
||||
zip: { color: "#7c3aed", background: "#ede9fe" },
|
||||
rar: { color: "#7c3aed", background: "#ede9fe" },
|
||||
mp4: { color: "#f97316", background: "#ffedd5" },
|
||||
mp3: { color: "#0d9488", background: "#ccfbf1" },
|
||||
};
|
||||
|
||||
return colorMap[extension] || getRandomColorPair();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export const TypeUpload: React.FC<any> = ({
|
|||
mode,
|
||||
type,
|
||||
disabled,
|
||||
valueKey = "url",
|
||||
onDelete,
|
||||
}) => {
|
||||
if (type === "multi") {
|
||||
return (
|
||||
|
|
@ -20,6 +22,8 @@ export const TypeUpload: React.FC<any> = ({
|
|||
fm={fm}
|
||||
on_change={on_change}
|
||||
mode={mode}
|
||||
valueKey={valueKey}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import get from "lodash.get";
|
||||
import { Loader2, Paperclip, Trash2, Upload } from "lucide-react";
|
||||
import { Upload } from "lucide-react";
|
||||
import { ChangeEvent, FC } from "react";
|
||||
import * as XLSX from "xlsx";
|
||||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import { siteurl } from "@/lib/utils/siteurl";
|
||||
import { FilePreview } from "./FilePreview";
|
||||
import { Spinner } from "../../ui/spinner";
|
||||
import { FilePreviewBetter } from "./FilePreview";
|
||||
import { MdDelete } from "react-icons/md";
|
||||
|
||||
export const FieldUploadMulti: FC<{
|
||||
field: any;
|
||||
fm: any;
|
||||
on_change: (e: any) => void | Promise<void>;
|
||||
mode?: "upload";
|
||||
}> = ({ field, fm, on_change, mode }) => {
|
||||
valueKey?: string;
|
||||
onDelete?: (e: any) => any | Promise<any>;
|
||||
}> = ({ field, fm, on_change, mode, valueKey = "url", onDelete }) => {
|
||||
const styling = "mini";
|
||||
const disabled = field?.disabled || false;
|
||||
let value: any = fm.data?.[field.name];
|
||||
// let type_upload =
|
||||
const input = useLocal({
|
||||
value: 0 as any,
|
||||
value: [] as any[],
|
||||
display: false as any,
|
||||
ref: null as any,
|
||||
drop: false as boolean,
|
||||
|
|
@ -32,32 +32,6 @@ export const FieldUploadMulti: FC<{
|
|||
try {
|
||||
file = event.target?.files?.[0];
|
||||
} catch (ex) {}
|
||||
const upload_single = async (file: File) => {
|
||||
return { url: `/dog.jpg` };
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const url = "/api/upload";
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const contentType: any = response.headers.get("content-type");
|
||||
let result;
|
||||
if (contentType.includes("application/json")) {
|
||||
result = await response.json();
|
||||
} else if (contentType.includes("text/plain")) {
|
||||
result = await response.text();
|
||||
} else {
|
||||
result = await response.blob();
|
||||
}
|
||||
if (Array.isArray(result)) {
|
||||
return `_file${get(result, "[0]")}`;
|
||||
}
|
||||
}
|
||||
throw new Error("Upload Failed");
|
||||
};
|
||||
|
||||
if (event.target.files) {
|
||||
const list = [] as any[];
|
||||
|
|
@ -70,13 +44,13 @@ export const FieldUploadMulti: FC<{
|
|||
list.push({
|
||||
name: file.name,
|
||||
data: file,
|
||||
[valueKey]: `${URL.createObjectURL(file)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fm.data[field.name] = list;
|
||||
fm.render();
|
||||
on_change(fm.data?.[field.name]);
|
||||
if (typeof on_change === "function") on_change(fm.data?.[field.name]);
|
||||
input.fase = "start";
|
||||
input.render();
|
||||
}
|
||||
|
|
@ -85,7 +59,111 @@ export const FieldUploadMulti: FC<{
|
|||
input.ref.value = null;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex-grow flex-col flex w-full h-full items-stretch relative">
|
||||
{!disabled ? (
|
||||
<>
|
||||
{" "}
|
||||
<div className="flex flex-wrap py-1 pb-2">
|
||||
<div className=" relative flex focus-within:border focus-within:border-primary border border-gray-300 rounded-md ">
|
||||
<div
|
||||
className={cx(
|
||||
"hover:bg-gray-50 text-gray-900 text-md rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-1.5 ",
|
||||
css`
|
||||
input[type="file"],
|
||||
input[type="file"]::-webkit-file-upload-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
disabled && "bg-gray-50"
|
||||
)}
|
||||
>
|
||||
{!disabled && (
|
||||
<input
|
||||
ref={(ref) => {
|
||||
if (ref) input.ref = ref;
|
||||
}}
|
||||
type="file"
|
||||
multiple={true}
|
||||
// accept={field.prop.upload?.accept}
|
||||
accept={"file/**"}
|
||||
onChange={on_upload}
|
||||
className={cx(
|
||||
"absolute w-full h-full cursor-pointer top-0 left-0 opacity-0"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!disabled ? (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (input.ref) {
|
||||
input.ref.click();
|
||||
}
|
||||
}}
|
||||
className="items-center flex text-base px-1 outline-none rounded cursor-pointer flex-row justify-center"
|
||||
>
|
||||
<div className="flex flex-row items-center px-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex flex-row items-center text-sm">
|
||||
Add File
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row items-center px-1.5 text-sm">
|
||||
-
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.isArray(value) && value?.length ? (
|
||||
<>
|
||||
{value.map((e: any, idx: number) => {
|
||||
return (
|
||||
<div className="flex flex-col" key={`files-${name}-${idx}`}>
|
||||
<div className="flex flex-row items-center w-64 p-2 border rounded-lg shadow-sm bg-white">
|
||||
<div className="flex flex-grow flex-row items-center">
|
||||
<div className="flex flex-grow">
|
||||
<FilePreviewBetter
|
||||
url={e?.[valueKey]}
|
||||
filename={e?.name}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="hover:bg-gray-100 p-2 rounded-lg cursor-pointer"
|
||||
onClick={() => {
|
||||
fm.data[field.name] = value.filter(
|
||||
(_, i) => i !== idx
|
||||
);
|
||||
fm.render();
|
||||
if (typeof on_change === "function")
|
||||
on_change(fm.data?.[field.name]);
|
||||
|
||||
if (typeof onDelete === "function") onDelete(e);
|
||||
}}
|
||||
>
|
||||
<MdDelete className="w-4 h-4 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex-grow flex-col flex w-full h-full items-stretch p-1">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,836 @@
|
|||
import {
|
||||
FC,
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import { TypeaheadOptions } from "./typeahead-opt";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { GoChevronDown } from "react-icons/go";
|
||||
import { IoCloseOutline } from "react-icons/io5";
|
||||
import { X } from "lucide-react";
|
||||
import uniqBy from "lodash.uniqby";
|
||||
|
||||
type OptItem = { value: string; label: string; tag?: string };
|
||||
|
||||
export const TypeaheadBetter: FC<{
|
||||
fitur?: "search-add";
|
||||
value?: string[] | null;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
options?: (arg: {
|
||||
search: string;
|
||||
existing: OptItem[];
|
||||
}) => (string | OptItem)[] | Promise<(string | OptItem)[]>;
|
||||
onSelect?: (arg: { search: string; item?: null | OptItem }) => string | false;
|
||||
onChange?: (selected: string[]) => void;
|
||||
unique?: boolean;
|
||||
allowNew?: boolean;
|
||||
className?: string;
|
||||
popupClassName?: string;
|
||||
localSearch?: boolean;
|
||||
autoPopupWidth?: boolean;
|
||||
focusOpen?: boolean;
|
||||
disabled?: boolean;
|
||||
mode?: "multi" | "single";
|
||||
note?: string;
|
||||
disabledSearch?: boolean;
|
||||
onInit?: (e: any) => void;
|
||||
isBetter?: boolean;
|
||||
}> = ({
|
||||
value,
|
||||
fitur,
|
||||
note,
|
||||
options: options_fn,
|
||||
onSelect,
|
||||
unique,
|
||||
allowNew: allow_new,
|
||||
focusOpen: on_focus_open,
|
||||
localSearch: local_search,
|
||||
autoPopupWidth: auto_popup_width,
|
||||
placeholder,
|
||||
mode,
|
||||
disabled,
|
||||
onChange,
|
||||
className,
|
||||
popupClassName,
|
||||
disabledSearch,
|
||||
onInit,
|
||||
isBetter = false,
|
||||
}) => {
|
||||
const maxLength = 4;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [debouncedTerm, setDebouncedTerm] = useState("");
|
||||
const local = useLocal({
|
||||
value: [] as string[],
|
||||
open: false,
|
||||
options: [] as OptItem[],
|
||||
loaded: false,
|
||||
loading: false,
|
||||
selectBetter: {
|
||||
all: false,
|
||||
partial: [] as any[],
|
||||
},
|
||||
search: {
|
||||
input: "",
|
||||
timeout: null as any,
|
||||
searching: false,
|
||||
promise: null as any,
|
||||
result: null as null | OptItem[],
|
||||
},
|
||||
unique: typeof unique === "undefined" ? true : unique,
|
||||
allow_new: typeof allow_new === "undefined" ? false : allow_new,
|
||||
on_focus_open: typeof on_focus_open === "undefined" ? true : on_focus_open,
|
||||
local_search: typeof local_search === "undefined" ? true : local_search,
|
||||
mode: typeof mode === "undefined" ? "multi" : mode,
|
||||
auto_popup_width:
|
||||
typeof auto_popup_width === "undefined" ? false : auto_popup_width,
|
||||
select: null as null | OptItem,
|
||||
});
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
|
||||
let select_found = false;
|
||||
let options = [...(local.search.result || local.options)];
|
||||
|
||||
const added = new Set<string>();
|
||||
if (local.mode === "multi") {
|
||||
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 (Array.isArray(value) && value?.length) {
|
||||
if (!select_found) {
|
||||
local.select = options[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) return;
|
||||
if (options.length === 0) {
|
||||
loadOptions().then(() => {
|
||||
if (typeof value === "object" && value) {
|
||||
local.value = value;
|
||||
local.render();
|
||||
} else if (typeof value === "string") {
|
||||
local.value = [value];
|
||||
local.render();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (typeof value === "object" && value) {
|
||||
local.value = value;
|
||||
local.render();
|
||||
} else {
|
||||
local.value = [];
|
||||
local.render();
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const select = useCallback(
|
||||
(arg: { search: string; item?: null | OptItem }) => {
|
||||
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 (local.mode === "single") {
|
||||
local.value = [];
|
||||
}
|
||||
|
||||
if (typeof onSelect === "function") {
|
||||
const result = onSelect(arg);
|
||||
|
||||
if (result) {
|
||||
local.value.push(result);
|
||||
local.render();
|
||||
|
||||
if (typeof onChange === "function") {
|
||||
onChange(local.value);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
let val = false as any;
|
||||
if (arg.item) {
|
||||
local.value.push(arg.item.value);
|
||||
val = arg.item.value;
|
||||
} else {
|
||||
if (!arg.search) return false;
|
||||
local.value.push(arg.search);
|
||||
val = arg.search;
|
||||
}
|
||||
|
||||
if (typeof onChange === "function") {
|
||||
onChange(local.value);
|
||||
}
|
||||
local.render();
|
||||
return val;
|
||||
}
|
||||
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") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const selected = select({
|
||||
search: local.search.input,
|
||||
item: local.select,
|
||||
});
|
||||
|
||||
if (local.mode === "single") {
|
||||
local.open = false;
|
||||
}
|
||||
if (typeof selected === "string") {
|
||||
if (!allow_new) resetSearch();
|
||||
if (local.mode === "single") {
|
||||
const item = options.find((item) => item.value === selected);
|
||||
if (item) {
|
||||
local.search.input = item.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local.render();
|
||||
return;
|
||||
}
|
||||
if (options.length > 0) {
|
||||
local.open = true;
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const idx = options.findIndex((item) => {
|
||||
if (item.value === local.select?.value) return true;
|
||||
});
|
||||
if (idx >= 0) {
|
||||
if (idx + 1 <= options.length - 1) {
|
||||
local.select = options[idx + 1];
|
||||
} else {
|
||||
local.select = options[0];
|
||||
}
|
||||
} else {
|
||||
local.select = options[0];
|
||||
}
|
||||
local.render();
|
||||
}
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const idx = options.findIndex((item) => {
|
||||
if (item.value === local.select?.value) return true;
|
||||
});
|
||||
if (idx >= 0) {
|
||||
if (idx - 1 >= 0) {
|
||||
local.select = options[idx - 1];
|
||||
} else {
|
||||
local.select = options[options.length - 1];
|
||||
}
|
||||
} else {
|
||||
local.select = options[0];
|
||||
}
|
||||
local.render();
|
||||
}
|
||||
}
|
||||
},
|
||||
[local.value, local.select, select, options, local.search.input]
|
||||
);
|
||||
|
||||
const loadOptions = useCallback(async () => {
|
||||
if (typeof options_fn === "function" && !local.loading) {
|
||||
local.loading = true;
|
||||
local.loaded = false;
|
||||
local.render();
|
||||
const res = options_fn({
|
||||
search: local.search.input,
|
||||
existing: options,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const applyOptions = (result: (string | OptItem)[]) => {
|
||||
local.options = result.map((item) => {
|
||||
if (typeof item === "string") return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.render();
|
||||
};
|
||||
|
||||
if (res instanceof Promise) {
|
||||
const result = await res;
|
||||
applyOptions(result);
|
||||
} else {
|
||||
applyOptions(res);
|
||||
}
|
||||
local.loaded = true;
|
||||
local.loading = false;
|
||||
local.render();
|
||||
}
|
||||
}
|
||||
}, [options_fn]);
|
||||
useEffect(() => {
|
||||
if (typeof onInit === "function") {
|
||||
onInit({
|
||||
reload: async () => {
|
||||
if (typeof options_fn === "function" && !local.loading) {
|
||||
local.loading = true;
|
||||
local.loaded = false;
|
||||
local.render();
|
||||
const res = options_fn({
|
||||
search: local.search.input,
|
||||
existing: options,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const applyOptions = (result: (string | OptItem)[]) => {
|
||||
local.options = result.map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.render();
|
||||
};
|
||||
|
||||
if (res instanceof Promise) {
|
||||
const result = await res;
|
||||
applyOptions(result);
|
||||
} else {
|
||||
applyOptions(res);
|
||||
}
|
||||
local.loaded = true;
|
||||
local.loading = false;
|
||||
local.render();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
// Debounce effect
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedTerm(searchTerm); // Update debounced term after 1 second
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer); // Clear timeout if user types again
|
||||
}, [searchTerm]);
|
||||
|
||||
// Function to handle search
|
||||
useEffect(() => {
|
||||
if (debouncedTerm) {
|
||||
performSearch(debouncedTerm);
|
||||
}
|
||||
}, [debouncedTerm]);
|
||||
|
||||
const performSearch = (value: any) => {
|
||||
if (typeof onSelect === "function") {
|
||||
const result = onSelect({
|
||||
search: value,
|
||||
item: {
|
||||
label: value,
|
||||
value: value,
|
||||
},
|
||||
});
|
||||
|
||||
if (result) {
|
||||
local.value.push(result);
|
||||
local.render();
|
||||
|
||||
if (typeof onChange === "function") {
|
||||
onChange(local.value);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Lakukan pencarian, panggil API, atau filter data di sini
|
||||
};
|
||||
const resetSearch = () => {
|
||||
local.search.searching = false;
|
||||
local.search.input = "";
|
||||
local.search.promise = null;
|
||||
local.search.result = null;
|
||||
local.select = null;
|
||||
clearTimeout(local.search.timeout);
|
||||
};
|
||||
|
||||
if (local.mode === "single" && local.value.length > 1) {
|
||||
local.value = [local.value.pop() || ""];
|
||||
}
|
||||
|
||||
if (local.value.length === 0) {
|
||||
if (local.mode === "single") {
|
||||
if (!local.open && !allow_new) {
|
||||
local.select = null;
|
||||
|
||||
local.search.input = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const valueLabel = uniqBy(
|
||||
local.value?.map((value) => {
|
||||
if (local.mode === "single") {
|
||||
const item = options.find((item) => item.value === value);
|
||||
|
||||
if (!local.open && !allow_new) {
|
||||
local.select = item || null;
|
||||
|
||||
local.search.input = item?.tag || item?.label || "";
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
const item = local.options.find((e) => e.value === value);
|
||||
return item;
|
||||
}),
|
||||
"value"
|
||||
);
|
||||
let inputval = local.search.input;
|
||||
|
||||
if (!local.open && local.mode === "single" && local.value?.length > 0) {
|
||||
const found = options.find((e) => e.value === local.value[0]);
|
||||
if (found) {
|
||||
inputval = found.tag || found.label;
|
||||
} else {
|
||||
inputval = local.value[0];
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (allow_new && local.open) {
|
||||
local.search.input = local.value[0];
|
||||
local.render();
|
||||
}
|
||||
}, [local.open]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-grow w-full relative">
|
||||
<div
|
||||
className={cx(
|
||||
allow_new
|
||||
? "cursor-text"
|
||||
: local.mode === "single"
|
||||
? "cursor-pointer"
|
||||
: "cursor-text",
|
||||
"text-black flex relative flex-wrap py-0 items-center w-full h-full flex-1 ",
|
||||
className
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!disabled) input.current?.focus();
|
||||
}}
|
||||
>
|
||||
{local.mode === "multi" ? (
|
||||
<div
|
||||
className={cx(
|
||||
css`
|
||||
margin-top: 5px;
|
||||
margin-bottom: -3px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{valueLabel.map((e, idx) => {
|
||||
return (
|
||||
<Badge
|
||||
key={idx}
|
||||
variant={"outline"}
|
||||
className={cx(
|
||||
"space-x-1 mr-2 mb-2 bg-white",
|
||||
!disabled &&
|
||||
" cursor-pointer hover:bg-red-100 hover:border-red-100"
|
||||
)}
|
||||
onClick={(ev) => {
|
||||
if (!disabled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
local.value = local.value.filter(
|
||||
(val) => e?.value !== val
|
||||
);
|
||||
local.render();
|
||||
input.current?.focus();
|
||||
|
||||
if (typeof onChange === "function") {
|
||||
onChange(local.value);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="text-xs">
|
||||
{e?.tag || e?.label || <> </>}
|
||||
</div>
|
||||
{!disabled && <IoCloseOutline size={12} />}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<TypeaheadOptions
|
||||
fitur={fitur}
|
||||
popup={true}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
local.select = null;
|
||||
}
|
||||
local.open = open;
|
||||
local.render();
|
||||
|
||||
if (!open) {
|
||||
resetSearch();
|
||||
}
|
||||
}}
|
||||
onRemove={(data) => {
|
||||
local.value = local.value.filter((val) => data?.value !== val);
|
||||
local.render();
|
||||
input.current?.focus();
|
||||
|
||||
if (typeof onChange === "function") {
|
||||
onChange(local.value);
|
||||
}
|
||||
}}
|
||||
onSelectAll={(data: boolean) => {
|
||||
local.value = data ? options.map((e) => e?.value) : [];
|
||||
local.render();
|
||||
input.current?.focus();
|
||||
if (typeof onChange === "function") {
|
||||
onChange(local.value);
|
||||
}
|
||||
}}
|
||||
init={local}
|
||||
isBetter={isBetter}
|
||||
loading={local.loading}
|
||||
showEmpty={!allow_new}
|
||||
className={popupClassName}
|
||||
open={local.open}
|
||||
options={options}
|
||||
searching={local.search.searching}
|
||||
searchText={local.search.input}
|
||||
onSearch={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 (allow_new) {
|
||||
setSearchTerm(val);
|
||||
}
|
||||
if (local.search.searching) {
|
||||
if (local.local_search) {
|
||||
if (!local.loaded) {
|
||||
await loadOptions();
|
||||
}
|
||||
const search = local.search.input.toLowerCase();
|
||||
if (search) {
|
||||
local.search.result = options.filter((e) =>
|
||||
e.label.toLowerCase().includes(search)
|
||||
);
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
!local.search.result.find(
|
||||
(e) => e.value === local.select?.value
|
||||
)
|
||||
) {
|
||||
}
|
||||
} 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: 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;
|
||||
} else {
|
||||
local.search.result = result.map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.search.searching = false;
|
||||
}
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
!local.search.result.find(
|
||||
(e) => e.value === local.select?.value
|
||||
)
|
||||
) {
|
||||
}
|
||||
|
||||
local.render();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onSelect={(value) => {
|
||||
if (!isBetter) local.open = false;
|
||||
resetSearch();
|
||||
const item = options.find((item) => item.value === value);
|
||||
if (item) {
|
||||
let search = local.search.input;
|
||||
if (local.mode === "single") {
|
||||
local.search.input = item.tag || item.label;
|
||||
} else {
|
||||
local.search.input = "";
|
||||
}
|
||||
|
||||
select({
|
||||
search,
|
||||
item,
|
||||
});
|
||||
}
|
||||
|
||||
local.render();
|
||||
}}
|
||||
width={
|
||||
local.auto_popup_width ? input.current?.offsetWidth : undefined
|
||||
}
|
||||
isMulti={local.mode === "multi"}
|
||||
selected={({ item, options, idx }) => {
|
||||
// console.log(local.select);
|
||||
if (isBetter) {
|
||||
const val = local.value?.length ? local.value : [];
|
||||
let isSelect = options.find((e) => {
|
||||
return (
|
||||
e?.value === item?.value &&
|
||||
val.find((ex) => ex === item?.value)
|
||||
);
|
||||
});
|
||||
return isSelect ? true : false;
|
||||
} else if (item.value === local.select?.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
allow_new ? "cursor-text" : "cursor-pointer",
|
||||
"single flex-1 flex-grow flex flex-row"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!disabled) {
|
||||
if (!local.open) {
|
||||
if (local.on_focus_open) {
|
||||
loadOptions();
|
||||
local.open = true;
|
||||
local.render();
|
||||
// if (allow_new) {
|
||||
// local.search.input = inputval;
|
||||
// local.render();
|
||||
// }
|
||||
}
|
||||
}
|
||||
if (local.mode === "single") {
|
||||
if (input && input.current) input.current.select();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isBetter ? (
|
||||
<div className="h-9 flex-grow flex flex-row items-start">
|
||||
<div className="flex flex-grow"></div>
|
||||
<div className="h-9 flex flex-row items-center px-2">
|
||||
<GoChevronDown size={14} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
placeholder={
|
||||
local.mode === "multi"
|
||||
? placeholder
|
||||
: valueLabel[0]?.label || placeholder
|
||||
}
|
||||
type="text"
|
||||
ref={input}
|
||||
value={inputval}
|
||||
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 (allow_new) {
|
||||
setSearchTerm(val);
|
||||
}
|
||||
if (local.search.searching) {
|
||||
if (local.local_search) {
|
||||
if (!local.loaded) {
|
||||
await loadOptions();
|
||||
}
|
||||
const search = local.search.input.toLowerCase();
|
||||
if (search) {
|
||||
local.search.result = options.filter((e) =>
|
||||
e.label.toLowerCase().includes(search)
|
||||
);
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
!local.search.result.find(
|
||||
(e) => e.value === local.select?.value
|
||||
)
|
||||
) {
|
||||
local.select = local.search.result[0];
|
||||
}
|
||||
} 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: 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;
|
||||
} else {
|
||||
local.search.result = result.map((item) => {
|
||||
if (typeof item === "string")
|
||||
return { value: item, label: item };
|
||||
return item;
|
||||
});
|
||||
local.search.searching = false;
|
||||
}
|
||||
|
||||
if (
|
||||
local.search.result.length > 0 &&
|
||||
!local.search.result.find(
|
||||
(e) => e.value === local.select?.value
|
||||
)
|
||||
) {
|
||||
local.select = local.search.result[0];
|
||||
}
|
||||
|
||||
local.render();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={!disabled ? disabledSearch : disabled}
|
||||
spellCheck={false}
|
||||
className={cx(
|
||||
"text-black flex h-9 w-full border-input bg-transparent px-3 py-1 text-base border-none shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm focus:outline-none focus:ring-0",
|
||||
local.mode === "single" ? "cursor-pointer" : ""
|
||||
)}
|
||||
style={{
|
||||
pointerEvents: disabledSearch ? "none" : "auto", // Mencegah input menangkap klik saat disabled
|
||||
}}
|
||||
onKeyDown={keydown}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TypeaheadOptions>
|
||||
</div>
|
||||
|
||||
{local.mode === "single" && fitur !== "search-add" && (
|
||||
<>
|
||||
<div
|
||||
className={cx(
|
||||
"typeahead-arrow absolute z-10 inset-0 left-auto flex items-center ",
|
||||
" justify-center w-6 mr-1 my-2 bg-transparant",
|
||||
disabled ? "hidden" : "cursor-pointer"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
local.value = [];
|
||||
local.render();
|
||||
if (typeof onChange === "function") onChange(local.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{inputval ? <X size={14} /> : <GoChevronDown size={14} />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -41,6 +41,7 @@ export const ListBetter: React.FC<any> = ({
|
|||
paging: 1,
|
||||
maxPage: 1,
|
||||
count: 0 as any,
|
||||
ready: false,
|
||||
addRow: (row: any) => {
|
||||
setData((prev) => [...prev, row]);
|
||||
local.data.push(row);
|
||||
|
|
@ -83,10 +84,13 @@ export const ListBetter: React.FC<any> = ({
|
|||
{"Loading..."}
|
||||
</>
|
||||
);
|
||||
|
||||
local.ready = false;
|
||||
local.render();
|
||||
if (typeof onCount === "function") {
|
||||
const res = await onCount();
|
||||
local.count = res;
|
||||
local.maxPage = Math.ceil(res / take);
|
||||
local.paging = 1;
|
||||
local.render();
|
||||
}
|
||||
if (Array.isArray(onLoad)) {
|
||||
|
|
@ -108,6 +112,8 @@ export const ListBetter: React.FC<any> = ({
|
|||
toast.dismiss();
|
||||
}, 100);
|
||||
}
|
||||
local.ready = true;
|
||||
local.render();
|
||||
},
|
||||
reload: async () => {
|
||||
toast.info(
|
||||
|
|
@ -176,9 +182,10 @@ export const ListBetter: React.FC<any> = ({
|
|||
{"Loading..."}
|
||||
</>
|
||||
);
|
||||
local.ready = false;
|
||||
local.render();
|
||||
if (typeof onCount === "function") {
|
||||
const res = await onCount();
|
||||
console.log(res, take, Math.ceil(res / take));
|
||||
setMaxPage(Math.ceil(res / take));
|
||||
local.maxPage = Math.ceil(res / take);
|
||||
local.count = res;
|
||||
|
|
@ -213,6 +220,8 @@ export const ListBetter: React.FC<any> = ({
|
|||
if (typeof onInit === "function") {
|
||||
onInit(local);
|
||||
}
|
||||
local.ready = true;
|
||||
local.render();
|
||||
setTimeout(() => {
|
||||
toast.dismiss();
|
||||
}, 100);
|
||||
|
|
@ -246,25 +255,33 @@ export const ListBetter: React.FC<any> = ({
|
|||
className="w-full h-full flex flex-col gap-y-4 p-4"
|
||||
reload={reload}
|
||||
>
|
||||
<div className="flex-grow flex flex-col gap-y-4">
|
||||
{Array.isArray(local.data) && local.data?.length ? (
|
||||
local.data?.map((e, idx) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-full"
|
||||
key={`items-${name}-${idx}`}
|
||||
ref={local.data?.length === idx + 1 ? lastPostRef : null}
|
||||
>
|
||||
{typeof content === "function"
|
||||
? content({ item: e, idx, tbl: local })
|
||||
: content}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
{!local.ready ? (
|
||||
<>
|
||||
<div className="flex-grow h-full flex-grow flex flex-row items-center justify-center">
|
||||
<div className="spinner-better"></div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-grow flex flex-col gap-y-4">
|
||||
{Array.isArray(local.data) && local.data?.length ? (
|
||||
local.data?.map((e, idx) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-full"
|
||||
key={`items-${name}-${idx}`}
|
||||
ref={local.data?.length === idx + 1 ? lastPostRef : null}
|
||||
>
|
||||
{typeof content === "function"
|
||||
? content({ item: e, idx, tbl: local })
|
||||
: content}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import { cn } from "@/lib/utils";
|
|||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
||||
classNameIndicator?: any; // Trigger to force height update
|
||||
}
|
||||
>(({ className, value, classNameIndicator, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
|
|
@ -17,7 +19,10 @@ const Progress = React.forwardRef<
|
|||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all indicator"
|
||||
className={cn(
|
||||
"h-full w-full flex-1 bg-linear-progress transition-all indicator rounded-full",
|
||||
classNameIndicator
|
||||
)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import get from "lodash.get";
|
|||
import api from "./axios";
|
||||
|
||||
type apixType = {
|
||||
port: "portal" | "recruitment" | "mpp" | "public";
|
||||
port: "portal" | "recruitment" | "mpp" | "public" | "onboarding";
|
||||
path: string;
|
||||
method?: "get" | "delete" | "post" | "put";
|
||||
data?: any;
|
||||
|
|
@ -36,6 +36,8 @@ export const apix = async ({
|
|||
? process.env.NEXT_PUBLIC_API_RECRUITMENT
|
||||
: port === "mpp"
|
||||
? process.env.NEXT_PUBLIC_API_MPP
|
||||
: port === "onboarding"
|
||||
? process.env.NEXT_PUBLIC_API_ONBOARDING
|
||||
: port === "public"
|
||||
? process.env.NEXT_PUBLIC_BASE_URL
|
||||
: ""
|
||||
|
|
@ -49,10 +51,20 @@ export const apix = async ({
|
|||
const requestData =
|
||||
type === "form" && data
|
||||
? Object.entries(data as any).reduce((formData, [key, value]) => {
|
||||
formData.append(
|
||||
key.includes("certificate") ? key : key.replace(/\[\d+\]/, ""),
|
||||
value as any
|
||||
);
|
||||
if (Array.isArray(value) && value?.length) {
|
||||
value.map((item: any) => {
|
||||
formData.append(key, item as any);
|
||||
});
|
||||
} else if (value instanceof FormData) {
|
||||
value.forEach((value, key) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
} else {
|
||||
formData.append(
|
||||
key.includes("certificate") ? key : key.replace(/\[\d+\]/, ""),
|
||||
value as any
|
||||
);
|
||||
}
|
||||
return formData;
|
||||
}, new FormData())
|
||||
: data;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
export const convertForm = ({
|
||||
data,
|
||||
task,
|
||||
}: {
|
||||
data: any[];
|
||||
task: (item: any, form: any) => void;
|
||||
}) => {
|
||||
const form = new FormData();
|
||||
if (Array.isArray(data) && data?.length) {
|
||||
data.map((item: any) => {
|
||||
task(item, form);
|
||||
});
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue