diff --git a/comps/form/field/type/FilePreview.tsx b/comps/form/field/type/FilePreview.tsx index 952f180..8d88d4b 100755 --- a/comps/form/field/type/FilePreview.tsx +++ b/comps/form/field/type/FilePreview.tsx @@ -1,12 +1,38 @@ import { ExternalLink } from "lucide-react"; -export const FilePreview = ({ url }: { url: string }) => { +export const FilePreview = ({ + url, + variant, +}: { + url: string; + variant?: "thumb"; +}) => { const file = getFileName(url); + if (typeof file === "string") + return ( +
+ {file} +
+ ); const color = darkenColor(generateRandomColor(file.extension)); let content = (
{ {file.extension}
); + + if (variant === "thumb") { + content = ( +
+ {file.extension} + +
+ +
+
+ ); + } + if (url.startsWith("_file/")) { if ([".png", ".jpeg", ".jpg", ".webp"].find((e) => url.endsWith(e))) { content = ( ); } @@ -36,16 +96,33 @@ export const FilePreview = ({ url }: { url: string }) => { <> {file.extension && (
{ let _url = siteurl(url || ""); window.open(_url, "_blank"); }} > {content} -
- -
+ {variant !== "thumb" && ( +
+ +
+ )}
)} @@ -90,6 +167,17 @@ function generateRandomColor(str: string): string { return color; } const getFileName = (url: string) => { + if (url.startsWith("[")) { + try { + const list = JSON.parse(url); + if (list.length === 0) return "Empty"; + return `${list.length} File${list.length > 1 ? "s" : ""}`; + } catch (e) { + console.error(`Error parsing multi-file: ${url}`); + } + return "Unknown File"; + } + const fileName = url.substring(url.lastIndexOf("/") + 1); const dotIndex = fileName.lastIndexOf("."); const fullname = fileName; diff --git a/comps/form/field/type/TypeInput.tsx b/comps/form/field/type/TypeInput.tsx index f84eb33..d0fd8bd 100755 --- a/comps/form/field/type/TypeInput.tsx +++ b/comps/form/field/type/TypeInput.tsx @@ -63,7 +63,6 @@ export const FieldTypeInput: FC<{ // let value: any = "2024-05-14T05:58:01.376Z" // case untuk date time field.input = input; - if (!field.prop) field.prop = prop; if (["date", "datetime", "datetime-local", "time"].includes(type_field)) { if (typeof value === "string" || value instanceof Date) { let date = parse(value); diff --git a/comps/form/field/type/TypeUpload.tsx b/comps/form/field/type/TypeUpload.tsx index 2900b35..a1aedcc 100755 --- a/comps/form/field/type/TypeUpload.tsx +++ b/comps/form/field/type/TypeUpload.tsx @@ -1,14 +1,8 @@ -import { useLocal } from "@/utils/use-local"; -import get from "lodash.get"; -import { Loader2, Paperclip, Trash2, Upload } from "lucide-react"; import { FC } from "react"; -import * as XLSX from "xlsx"; import { FMLocal, FieldLocal, FieldProp } from "../../typings"; -import { FilePreview } from "./FilePreview"; import { PropTypeInput } from "./TypeInput"; -const w = window as unknown as { - serverurl: string; -}; +import { FieldUploadMulti } from "./TypeUploadMulti"; +import { FieldUploadSingle } from "./TypeUploadSingle"; export const FieldUpload: FC<{ field: FieldLocal; @@ -17,7 +11,11 @@ export const FieldUpload: FC<{ styling?: string; arg: FieldProp; on_change: (e: any) => void | Promise; -}> = ({ field, fm, prop, on_change, arg }) => { - console.log(field.prop.upload); - return <>; +}> = (pass) => { + const { field, fm, prop, on_change, arg } = pass; + let mode = field.prop.upload?.mode || "single-file"; + if (mode === "single-file") { + return ; + } + return ; }; diff --git a/comps/form/field/type/TypeUploadMulti.tsx b/comps/form/field/type/TypeUploadMulti.tsx index e69de29..d70df46 100755 --- a/comps/form/field/type/TypeUploadMulti.tsx +++ b/comps/form/field/type/TypeUploadMulti.tsx @@ -0,0 +1,223 @@ +import { useLocal } from "@/utils/use-local"; +import get from "lodash.get"; +import { Trash2, Upload } from "lucide-react"; +import { ChangeEvent, FC } from "react"; +import { FMLocal, FieldLocal, FieldProp } from "../../typings"; +import { PropTypeInput } from "./TypeInput"; +import { FilePreview } from "./FilePreview"; +import { Spinner } from "lib/comps/ui/field-loading"; +const w = window as unknown as { + serverurl: string; +}; + +export const FieldUploadMulti: FC<{ + field: FieldLocal; + fm: FMLocal; + prop: PropTypeInput; + styling?: string; + arg: FieldProp; + on_change: (e: any) => void | Promise; +}> = ({ field, fm, prop, on_change, arg }) => { + let value: string = (fm.data[field.name] || "").trim(); + // let type_upload = + const input = useLocal({ + value: 0 as any, + display: false as any, + ref: null as any, + drop: false as boolean, + uploading: new Set(), + fase: value ? "preview" : ("start" as "start" | "upload" | "preview"), + style: "inline" as "inline" | "full", + }); + + const parse_list = () => { + let list: string[] = []; + if (value.startsWith("[")) { + try { + list = JSON.parse(value); + } catch (e) {} + } else if (typeof value === "string" && value) { + list.push(value); + } + return list; + }; + + const on_upload = async (event: ChangeEvent) => { + const upload_single = async (file: File) => { + const formData = new FormData(); + formData.append("file", file); + + let url = siteurl("/_upload"); + if ( + location.hostname === "prasi.avolut.com" || + location.host === "localhost:4550" + ) { + const newurl = new URL(location.href); + newurl.pathname = `/_proxy/${url}`; + url = newurl.toString(); + } + 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 = parse_list(); + + for (let i = 0; i < event.target.files.length; i++) { + const file = event.target?.files?.item(i); + if (file) { + input.uploading.add(file); + upload_single(file).then((path) => { + input.uploading.delete(file); + list.push(path); + fm.data[field.name] = JSON.stringify(list); + fm.render(); + }); + } + } + input.render(); + } + if (input.ref) { + input.ref.value = null; + } + }; + + if (isEditor) input.fase = "start"; + + const list = parse_list(); + + return ( +
+
+ {list.map((value, idx) => { + return ( +
{ + e.stopPropagation(); + e.preventDefault(); + }} + > +
+ +
{ + e.preventDefault(); + e.stopPropagation(); + if (confirm("Remove this file ?")) { + list.splice(idx, 1); + fm.data[field.name] = JSON.stringify(list); + fm.render(); + } + }} + className={cx( + "c-flex c-flex-row c-items-center c-px-1 c-rounded c-bg-white c-cursor-pointer hover:c-bg-red-100 c-absolute c-top-0 c-right-0 del transition-all", + css` + border: 1px solid red; + width: 25px; + height: 25px; + margin: 5px; + ` + )} + > + +
+
+
+ ); + })} + {input.uploading.size > 0 && ( +
+
Uploading
+
+ )} +
+
+
+
+ {!isEditor && ( + { + if (!input.ref) { + input.ref = ref; + } + }} + type="file" + multiple={true} + accept={field.prop.upload?.accept} + onChange={on_upload} + className={cx( + "c-absolute c-w-full c-h-full c-cursor-pointer c-top-0 c-left-0 c-opacity-0" + )} + /> + )} +
+
+ +
+
+ Upload File +
+
+
+
+
{ + e.stopPropagation(); + e.preventDefault(); + }} + >
+
+
+ ); +}; diff --git a/comps/form/field/type/TypeUploadSingle.tsx b/comps/form/field/type/TypeUploadSingle.tsx index a3012eb..8279db2 100755 --- a/comps/form/field/type/TypeUploadSingle.tsx +++ b/comps/form/field/type/TypeUploadSingle.tsx @@ -1,7 +1,7 @@ import { useLocal } from "@/utils/use-local"; import get from "lodash.get"; import { Loader2, Paperclip, Trash2, Upload } from "lucide-react"; -import { FC } from "react"; +import { ChangeEvent, FC } from "react"; import * as XLSX from "xlsx"; import { FMLocal, FieldLocal, FieldProp } from "../../typings"; import { FilePreview } from "./FilePreview"; @@ -19,7 +19,6 @@ export const FieldUploadSingle: FC<{ on_change: (e: any) => void | Promise; }> = ({ field, fm, prop, on_change, arg }) => { const styling = arg.upload_style ? arg.upload_style : "full"; - let type_field = prop.sub_type; let value: any = fm.data[field.name]; // let type_upload = const input = useLocal({ @@ -31,30 +30,46 @@ export const FieldUploadSingle: FC<{ style: "inline" as "inline" | "full", }); - const on_upload = async (event: any) => { + const on_upload = async (event: ChangeEvent) => { let file = null; try { - file = event.target.files[0]; + file = event.target?.files?.[0]; } catch (ex) {} - if (type_field === "import") { + if (prop.model_upload === "import") { const reader = new FileReader(); - reader.onload = (e: any) => { - const binaryStr = e.target.result; - const workbook = XLSX.read(binaryStr, { type: "binary" }); + function arrayBufferToBinaryString(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + return String.fromCharCode.apply(null, Array.from(bytes)); + } - const worksheet = workbook.Sheets[workbook.SheetNames[0]]; - const jsonData = XLSX.utils.sheet_to_json(worksheet); - if (typeof on_change === "function") { - const res = on_change({ - value: jsonData, - file: file, - binnary: e.target.result, - }); + reader.onload = (e: ProgressEvent) => { + if (e.target && e.target.result) { + const binaryStr = + typeof e.target.result === "string" + ? e.target.result + : arrayBufferToBinaryString(e.target.result); + const workbook = XLSX.read(binaryStr, { type: "binary" }); + + const worksheet = workbook.Sheets[workbook.SheetNames[0]]; + const jsonData = XLSX.utils.sheet_to_json(worksheet); + if (typeof on_change === "function") { + on_change({ + value: jsonData, + file: file, + binnary: e.target.result, + }); + } } }; - reader.readAsBinaryString(file); - } else { + if (file) { + if (typeof reader.readAsArrayBuffer === "function") { + reader.readAsArrayBuffer(file); + } else { + reader.readAsBinaryString(file); + } + } + } else if (file) { const formData = new FormData(); formData.append("file", file); @@ -132,6 +147,7 @@ export const FieldUploadSingle: FC<{ ref={(ref) => (input.ref = ref)} type="file" multiple={false} + accept={field.prop.upload?.accept} onChange={on_upload} className={cx( "c-absolute c-w-full c-h-full c-cursor-pointer c-top-0 c-left-0 c-opacity-0" @@ -152,7 +168,7 @@ export const FieldUploadSingle: FC<{
-
+
Upload File
@@ -173,7 +189,7 @@ export const FieldUploadSingle: FC<{ }} className={cx( input.drop ? "c-bg-gray-100" : "", - "hover:c-bg-gray-100 c-flex-grow c-m-1 c-relative c-flex-grow c-p-4 c-items-center c-flex c-flex-row c-text-gray-400 c-border c-border-gray-200 c-border-dashed c-rounded c-cursor-pointer" + "hover:c-bg-gray-100 c-m-1 c-relative c-flex-grow c-p-4 c-items-center c-flex c-flex-row c-text-gray-400 c-border c-border-gray-200 c-border-dashed c-rounded c-cursor-pointer" )} >
@@ -217,19 +233,19 @@ export const FieldUploadSingle: FC<{ ) : input.fase === "preview" ? (
-
- { - e.preventDefault(); - e.stopPropagation(); - if (confirm("Clear this file ?")) { - input.fase = "start"; - fm.data[field.name] = null; - fm.render(); - } - }} - /> +
{ + e.preventDefault(); + e.stopPropagation(); + if (confirm("Clear this file ?")) { + input.fase = "start"; + fm.data[field.name] = null; + fm.render(); + } + }} + className="c-flex c-flex-row c-items-center c-border c-px-2 c-rounded c-cursor-pointer hover:c-bg-red-100" + > +
) : ( diff --git a/comps/form/typings.ts b/comps/form/typings.ts index 1a1d81d..e1dd6c3 100755 --- a/comps/form/typings.ts +++ b/comps/form/typings.ts @@ -48,7 +48,7 @@ export type FieldProp = { label: string; desc?: string; props?: any; - upload?: { mode: "single-file" | "multi-file" }; + upload?: { mode: "single-file" | "multi-file"; accept: string }; link: { text: | string @@ -187,7 +187,7 @@ export type FieldInternal = { name: string; fm: FMLocal; }) => void | Promise; - prop?: FieldProp; + prop: FieldProp; max_date?: FieldProp["max_date"]; min_date?: FieldProp["min_date"]; error?: any; diff --git a/comps/form/utils/init.tsx b/comps/form/utils/init.tsx index 8afacad..c749f79 100755 --- a/comps/form/utils/init.tsx +++ b/comps/form/utils/init.tsx @@ -56,7 +56,12 @@ export const formInit = (fm: FMLocal, props: FMProps) => {