diff --git a/comps/form/Form.tsx b/comps/form/Form.tsx index 4d8c2d7..b0ac730 100755 --- a/comps/form/Form.tsx +++ b/comps/form/Form.tsx @@ -57,7 +57,7 @@ export const Form: FC = (props) => { field: null, }, has_fields_container: null as any, - is_newly_created: false + is_newly_created: false, }); const form_inner_ref = useRef(null); @@ -174,6 +174,11 @@ export const Form: FC = (props) => { if (fm.status === "resizing") { fm.status = "ready"; } + // useEffect(() => { + // setTimeout(() => { + // fm.render(); + // }, 100); + // }, []); return (
{ diff --git a/comps/form/field/type/TypeInput.tsx b/comps/form/field/type/TypeInput.tsx index 796847f..0c86ff9 100755 --- a/comps/form/field/type/TypeInput.tsx +++ b/comps/form/field/type/TypeInput.tsx @@ -168,6 +168,7 @@ export const FieldTypeInput: FC<{ case "upload": return ( void | Promise; -}> = ({ field, fm, prop, on_change }) => { +}> = ({ 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 = @@ -22,170 +35,301 @@ export const FieldUpload: FC<{ display: false as any, ref: null as any, drop: false as boolean, + fase: value ? "preview" : ("start" as "start" | "upload" | "preview"), + style: "inline" as "inline" | "full", }); let display: any = null; const disabled = typeof field.disabled === "function" ? field.disabled() : field.disabled; + const on_upload = async (event: any) => { + let file = null; + try { + file = event.target.files[0]; + } catch (ex) {} + if (type_field === "import") { + const reader = new FileReader(); + + reader.onload = (e: any) => { + const binaryStr = 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") { + const res = on_change({ + value: jsonData, + file: file, + binnary: e.target.result, + }); + } + }; + reader.readAsBinaryString(file); + } else { + 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(); + } + input.fase = "upload"; + input.render(); + try { + 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)) { + fm.data[field.name] = `_file${get(result, "[0]")}`; + fm.render(); + setTimeout(() => { + input.fase = "preview"; + input.render(); + }, 1000); + } else { + input.fase = "start"; + input.render(); + alert("Error upload"); + } + } else { + } + } catch (ex) { + input.fase = "start"; + input.render(); + alert("Error upload"); + } + } + if (input.ref) { + input.ref.value = null; + } + }; return ( -
-
{ - e.preventDefault(); - input.drop = false; - input.render(); - }} - onDragOver={(e: any) => { - // Prevent default behavior (Prevent file from being opened) - e.preventDefault(); - input.drop = true; - input.render(); - }} - className={cx( - input.drop ? "c-bg-gray-100" : "", - "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" - )} - > -
- - - - - - -
- - Drop Your File or{" "} - Browse - +
+ {input.fase === "start" ? ( + <> +
+ (input.ref = ref)} + type="file" + multiple={false} + onChange={on_upload} + className={cx( + "c-absolute c-w-full c-h-full c-cursor-pointer c-top-0 c-left-0 c-hidden" + )} + /> + {styling === "inline" ? ( + <> +
{ + if (input.ref) { + console.log(input.ref) + input.ref.click(); + } + }} + className="c-items-center c-flex c-text-base c-px-5 c-py-3 c-outline-none c-rounded c-cursor-pointer " + > +
+ +
+
+ Upload Your File +
+
+ + ) : ( + <> +
{ + e.preventDefault(); + input.drop = false; + input.render(); + }} + onDragOver={(e: any) => { + // Prevent default behavior (Prevent file from being opened) + e.preventDefault(); + input.drop = true; + input.render(); + }} + 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" + )} + > +
+ + + + + + +
+ + Drop Your File or{" "} + + Browse + + +
+
+
+ + )}
-
- (input.ref = ref)} - type="file" - multiple - onChange={async (event: any) => { - let file = null; - try { - file = event.target.files[0]; - } catch (ex) {} - if (type_field === "import") { - const reader = new FileReader(); - - reader.onload = (e: any) => { - const binaryStr = 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") { - const res = on_change({ value: jsonData }); - } - }; - reader.readAsBinaryString(file); - } else { - 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/${w.serverurl}/_upload`; - 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)) { - fm.data[field.name] = get(result, "[0]"); + + ) : input.fase === "upload" ? ( + <> +
+ +
+
Uploading
+ + ) : input.fase === "preview" ? ( + <> +
+ +
+
{ + let url = siteurl(value); + window.open(url, "_blank"); + }} + > + {getFileName(siteurl(value)).fullname} +
+
+
+ { + let url = siteurl(value); + window.open(url, "_blank"); + }} + /> + { + fm.data[field.name] = null; fm.render(); - } else { - alert("Error upload"); - } - } else { - } - } - }} - className={ - "c-absolute c-w-full c-h-full c-cursor-pointer c-top-0 c-left-0 c-hidden" + }} + /> +
+
+ + ) : ( + <> + )} +
+ ); + return ( +
+
+
-
- ); - // console.log({ prop }); - return ( -
-
{ - if (input.ref) { - input.display = !input.display; - input.ref.focus(); - input.render(); - } - }} - > - {formatMoney(Number(value) || 0)} +
+ {getFileName("https://www.example.com/path/to/your/file.txt").fullname} +
+
+
+ { + let url = siteurl(value); + window.open(url, "_blank"); + }} + /> + { + fm.data[field.name] = null; + fm.render(); + }} + /> +
- (input.ref = el)} - type={"number"} - onClick={() => {}} - onChange={(ev) => { - fm.data[field.name] = ev.currentTarget.value; - fm.render(); - }} - value={value} - disabled={disabled} - className={cx( - !input.display ? "c-hidden" : "", - "c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full" - )} - spellCheck={false} - onFocus={() => { - field.focused = true; - field.render(); - }} - onBlur={() => { - console.log("blur"); - field.focused = false; - input.display = !input.display; - input.render(); - field.render(); - }} - />
); }; -const formatMoney = (res: number) => { - const formattedAmount = new Intl.NumberFormat("id-ID", { - minimumFractionDigits: 0, - }).format(res); - return formattedAmount; +const getFileName = (url: string) => { + const fileName = url.substring(url.lastIndexOf("/") + 1); + const dotIndex = fileName.lastIndexOf("."); + const fullname = fileName; + if (dotIndex === -1) { + return { name: fileName, extension: "", fullname }; + } + const name = fileName.substring(0, dotIndex); + const extension = fileName.substring(dotIndex + 1); + return { name, extension, fullname }; +}; + +const IconFile: FC<{ type: string }> = ({ type }) => { + if (["xlsx"].includes(type)) { + return ( +
+ + + +
+ ); + } else { + return ( +
+ +
+ ); + } + return ( +
+ + + +
+ ); }; diff --git a/comps/form/typings.ts b/comps/form/typings.ts index 7ed1d29..e505763 100755 --- a/comps/form/typings.ts +++ b/comps/form/typings.ts @@ -117,6 +117,7 @@ export type FieldProp = { model_upload?: "upload" | "import"; max_date?: any; min_date?: any; + upload_style?: "inline" | "full" }; export type FMInternal = { diff --git a/comps/list/virtual/virtualized.tsx b/comps/list/virtual/virtualized.tsx new file mode 100755 index 0000000..b759667 --- /dev/null +++ b/comps/list/virtual/virtualized.tsx @@ -0,0 +1,200 @@ +import React, { useState, useEffect, useRef } from 'react'; + +interface RowData { + id: number; + [key: string]: any; +} + +interface VirtualTableProps { + data: RowData[]; + columns: string[]; + estimatedRowHeight: number; + visibleRows: number; + resizableColumns?: boolean; + pinnedColumns?: string[]; // New prop to specify pinned columns +} + +const VirtualTable: React.FC = ({ + data, + columns, + estimatedRowHeight, + visibleRows, + resizableColumns = false, + pinnedColumns = [] // Default to no pinned columns +}) => { + const [start, setStart] = useState(0); + const [rowHeights, setRowHeights] = useState([]); + const [columnWidths, setColumnWidths] = useState<{ [key: string]: number }>( + Object.fromEntries(columns.map(column => [column, 100])) + ); + const containerRef = useRef(null); + const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]); + const resizingColumn = useRef(null); + + const scrollableColumns = columns.filter(col => !pinnedColumns.includes(col)); + + useEffect(() => { + const handleScroll = () => { + if (containerRef.current) { + const scrollTop = containerRef.current.scrollTop; + const newStart = Math.floor(scrollTop / estimatedRowHeight); + setStart(newStart); + } + }; + + containerRef.current?.addEventListener('scroll', handleScroll); + return () => containerRef.current?.removeEventListener('scroll', handleScroll); + }, [estimatedRowHeight]); + + useEffect(() => { + const measureRowHeights = () => { + const newRowHeights = rowRefs.current.map( + (rowRef) => rowRef?.getBoundingClientRect().height || estimatedRowHeight + ); + setRowHeights(newRowHeights); + }; + + measureRowHeights(); + window.addEventListener('resize', measureRowHeights); + return () => window.removeEventListener('resize', measureRowHeights); + }, [data, estimatedRowHeight]); + + const getTotalHeight = () => { + return rowHeights.reduce((sum, height) => sum + height, 0) || data.length * estimatedRowHeight; + }; + + const getOffsetForIndex = (index: number) => { + return rowHeights.slice(0, index).reduce((sum, height) => sum + height, 0); + }; + + const visibleData = data.slice(start, start + visibleRows); + + const handleMouseDown = (column: string) => (e: React.MouseEvent) => { + if (resizableColumns) { + resizingColumn.current = column; + e.preventDefault(); + } + }; + + const handleMouseMove = (e: React.MouseEvent) => { + if (resizableColumns && resizingColumn.current) { + const newWidth = Math.max(50, e.clientX - (e.target as HTMLElement).getBoundingClientRect().left); + setColumnWidths(prev => ({ + ...prev, + [resizingColumn.current!]: newWidth + })); + } + }; + + const handleMouseUp = () => { + if (resizableColumns) { + resizingColumn.current = null; + } + }; + + useEffect(() => { + if (resizableColumns) { + document.addEventListener('mousemove', handleMouseMove as any); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove as any); + document.removeEventListener('mouseup', handleMouseUp); + }; + } + }, [resizableColumns]); + + const renderTableContent = (columnSet: string[]) => ( + + + + {columnSet.map((column) => ( + + ))} + + + + {visibleData.map((row, index) => ( + (rowRefs.current[start + index] = el)} + > + {columnSet.map((column) => ( + + ))} + + ))} + +
+ {column} + {resizableColumns && ( +
+ )} +
+ {row[column]} +
+ ); + + return ( +
+ {pinnedColumns.length > 0 && ( +
+
+ {renderTableContent(pinnedColumns)} +
+
+ )} +
0 ? '5px' : '0' + }}> +
+ {renderTableContent(scrollableColumns)} +
+
+
+ ); +}; + +export default VirtualTable; \ No newline at end of file diff --git a/comps/ui/dialog.tsx b/comps/ui/dialog.tsx new file mode 100755 index 0000000..a597acb --- /dev/null +++ b/comps/ui/dialog.tsx @@ -0,0 +1,115 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "@/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/comps/ui/progress.tsx b/comps/ui/progress.tsx new file mode 100755 index 0000000..3d948b8 --- /dev/null +++ b/comps/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/exports.tsx b/exports.tsx index b75ce03..41a67c9 100755 --- a/exports.tsx +++ b/exports.tsx @@ -11,7 +11,12 @@ export const Accordion = lazify( export const Popover = lazify( async () => (await import("@/comps/custom/Popover")).Popover ); - +export const Progress = lazify( + async () => (await import("@/comps/ui/progress")).Progress +); +export const Dialog = lazify( + async () => (await import("@/comps/ui/dialog")).Dialog +); export const Typeahead = lazify( async () => (await import("@/comps/ui/typeahead")).Typeahead );