lib form
This commit is contained in:
parent
f5f187ff81
commit
a20ae163f7
|
|
@ -57,7 +57,7 @@ export const Form: FC<FMProps> = (props) => {
|
|||
field: null,
|
||||
},
|
||||
has_fields_container: null as any,
|
||||
is_newly_created: false
|
||||
is_newly_created: false,
|
||||
});
|
||||
const form_inner_ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
|
@ -174,6 +174,11 @@ export const Form: FC<FMProps> = (props) => {
|
|||
if (fm.status === "resizing") {
|
||||
fm.status = "ready";
|
||||
}
|
||||
// useEffect(() => {
|
||||
// setTimeout(() => {
|
||||
// fm.render();
|
||||
// }, 100);
|
||||
// }, []);
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export const FieldTypeInput: FC<{
|
|||
case "upload":
|
||||
return (
|
||||
<FieldUpload
|
||||
arg={arg}
|
||||
field={field}
|
||||
fm={fm}
|
||||
prop={prop}
|
||||
|
|
@ -177,6 +178,7 @@ export const FieldTypeInput: FC<{
|
|||
case "import":
|
||||
return (
|
||||
<FieldUpload
|
||||
arg={arg}
|
||||
field={field}
|
||||
fm={fm}
|
||||
prop={prop}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,32 @@
|
|||
import { useLocal } from "@/utils/use-local";
|
||||
import get from "lodash.get";
|
||||
import { FC } from "react";
|
||||
import { FMLocal, FieldLocal } from "../../typings";
|
||||
import { FMLocal, FieldLocal, FieldProp } from "../../typings";
|
||||
import { PropTypeInput } from "./TypeInput";
|
||||
import * as XLSX from "xlsx";
|
||||
import {
|
||||
ArrowDownToLine,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
Paperclip,
|
||||
SquareArrowOutUpRight,
|
||||
Trash2,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import { Spinner } from "lib/comps/ui/field-loading";
|
||||
const w = window as unknown as {
|
||||
serverurl: string
|
||||
}
|
||||
serverurl: string;
|
||||
};
|
||||
|
||||
export const FieldUpload: FC<{
|
||||
field: FieldLocal;
|
||||
fm: FMLocal;
|
||||
prop: PropTypeInput;
|
||||
styling?: string;
|
||||
arg: FieldProp;
|
||||
on_change: (e: any) => void | Promise<void>;
|
||||
}> = ({ 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,12 +35,125 @@ 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 (
|
||||
<div className="c-flex-grow c-flex-row c-flex c-w-full c-h-full">
|
||||
<div className="c-flex-grow c-flex-row c-flex c-w-full c-h-full c-items-center">
|
||||
{input.fase === "start" ? (
|
||||
<>
|
||||
<div className="c-flex c-flex-row c-relative c-flex-grow">
|
||||
<input
|
||||
ref={(ref) => (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" ? (
|
||||
<>
|
||||
<div
|
||||
onClick={() => {
|
||||
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 "
|
||||
>
|
||||
<div className="c-flex c-flex-row c-items-center c-px-2">
|
||||
<Upload className="c-h-4 c-w-4" />
|
||||
</div>
|
||||
<div className="c-flex c-flex-row c-items-center">
|
||||
Upload Your File
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
onDrop={(e: any) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -42,7 +168,7 @@ export const FieldUpload: FC<{
|
|||
}}
|
||||
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"
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<div className="c-flex-row c-flex c-flex-grow c-space-x-2">
|
||||
|
|
@ -65,127 +191,145 @@ export const FieldUpload: FC<{
|
|||
<div className="c-flex c-flex-col">
|
||||
<span className="c-font-medium">
|
||||
Drop Your File or{" "}
|
||||
<span className="c-underline c-text-blue-500">Browse</span>
|
||||
<span className="c-underline c-text-blue-500">
|
||||
Browse
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref={(ref) => (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]");
|
||||
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"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
// console.log({ prop });
|
||||
return (
|
||||
<div className="c-flex-grow c-flex-row c-flex c-w-full c-h-full">
|
||||
<div
|
||||
className={cx(
|
||||
input.display ? "c-hidden" : "",
|
||||
"c-flex-grow c-px-2 c-flex c-flex-row c-items-center"
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : input.fase === "upload" ? (
|
||||
<>
|
||||
<div className="c-px-2">
|
||||
<Loader2 className={cx("c-h-5 c-w-5 c-animate-spin")} />
|
||||
</div>
|
||||
<div className="c-px-2">Uploading</div>
|
||||
</>
|
||||
) : input.fase === "preview" ? (
|
||||
<>
|
||||
<div className="c-flex c-flex-row c-p-2 c-items-center">
|
||||
<IconFile type={getFileName(siteurl(value)).extension} />
|
||||
</div>
|
||||
<div
|
||||
className="c-line-clamp-1 c-flex-grow c-items-center"
|
||||
onClick={() => {
|
||||
if (input.ref) {
|
||||
input.display = !input.display;
|
||||
input.ref.focus();
|
||||
input.render();
|
||||
}
|
||||
let url = siteurl(value);
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
>
|
||||
{formatMoney(Number(value) || 0)}
|
||||
{getFileName(siteurl(value)).fullname}
|
||||
</div>
|
||||
<input
|
||||
ref={(el) => (input.ref = el)}
|
||||
type={"number"}
|
||||
onClick={() => {}}
|
||||
onChange={(ev) => {
|
||||
fm.data[field.name] = ev.currentTarget.value;
|
||||
<div className="c-flex c-flex-row c-items-center">
|
||||
<div className="c-flex c-flex-row c-space-x-1 c-px-2">
|
||||
<SquareArrowOutUpRight
|
||||
className="c-h-5 c-w-5"
|
||||
onClick={() => {
|
||||
let url = siteurl(value);
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
/>
|
||||
<Trash2
|
||||
className="c-text-red-500 c-h-5 c-w-5"
|
||||
onClick={() => {
|
||||
fm.data[field.name] = null;
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="c-flex-grow c-flex-row c-flex c-w-full c-h-full c-items-center">
|
||||
<div className="c-flex c-flex-row c-p-2 c-items-center">
|
||||
<IconFile
|
||||
type={
|
||||
getFileName("https://www.example.com/path/to/your/file.txt")
|
||||
.extension
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="c-line-clamp-1 c-flex-grow c-items-center">
|
||||
{getFileName("https://www.example.com/path/to/your/file.txt").fullname}
|
||||
</div>
|
||||
<div className="c-flex c-flex-row c-items-center">
|
||||
<div className="c-flex c-flex-row c-space-x-1 c-px-2">
|
||||
<SquareArrowOutUpRight
|
||||
className="c-h-5 c-w-5"
|
||||
onClick={() => {
|
||||
let url = siteurl(value);
|
||||
window.open(url, "_blank");
|
||||
}}
|
||||
/>
|
||||
<Trash2
|
||||
className="c-text-red-500 c-h-5 c-w-5"
|
||||
onClick={() => {
|
||||
fm.data[field.name] = null;
|
||||
fm.render();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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 (
|
||||
<div className="c-flex c-flex-row c-text-[#2a801d]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m2.859 2.877l12.57-1.795a.5.5 0 0 1 .571.494v20.848a.5.5 0 0 1-.57.494L2.858 21.123a1 1 0 0 1-.859-.99V3.867a1 1 0 0 1 .859-.99M17 3h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4zm-6.8 9L13 8h-2.4L9 10.286L7.4 8H5l2.8 4L5 16h2.4L9 13.714L10.6 16H13z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="c-flex c-flex-row ">
|
||||
<Paperclip className="c-h-5 c-w-5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="c-flex c-flex-row c-p-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m2.859 2.877l12.57-1.795a.5.5 0 0 1 .571.494v20.848a.5.5 0 0 1-.57.494L2.858 21.123a1 1 0 0 1-.859-.99V3.867a1 1 0 0 1 .859-.99M17 3h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4zm-6.8 9L13 8h-2.4L9 10.286L7.4 8H5l2.8 4L5 16h2.4L9 13.714L10.6 16H13z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ export type FieldProp = {
|
|||
model_upload?: "upload" | "import";
|
||||
max_date?: any;
|
||||
min_date?: any;
|
||||
upload_style?: "inline" | "full"
|
||||
};
|
||||
|
||||
export type FMInternal = {
|
||||
|
|
|
|||
|
|
@ -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<VirtualTableProps> = ({
|
||||
data,
|
||||
columns,
|
||||
estimatedRowHeight,
|
||||
visibleRows,
|
||||
resizableColumns = false,
|
||||
pinnedColumns = [] // Default to no pinned columns
|
||||
}) => {
|
||||
const [start, setStart] = useState(0);
|
||||
const [rowHeights, setRowHeights] = useState<number[]>([]);
|
||||
const [columnWidths, setColumnWidths] = useState<{ [key: string]: number }>(
|
||||
Object.fromEntries(columns.map(column => [column, 100]))
|
||||
);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]);
|
||||
const resizingColumn = useRef<string | null>(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[]) => (
|
||||
<table style={{
|
||||
transform: `translateY(${getOffsetForIndex(start)}px)`,
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse'
|
||||
}}>
|
||||
<thead>
|
||||
<tr>
|
||||
{columnSet.map((column) => (
|
||||
<th key={column} style={{
|
||||
padding: '8px',
|
||||
textAlign: 'left',
|
||||
fontWeight: 'bold',
|
||||
width: columnWidths[column],
|
||||
position: 'relative'
|
||||
}}>
|
||||
{column}
|
||||
{resizableColumns && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '5px',
|
||||
cursor: 'col-resize',
|
||||
background: 'transparent'
|
||||
}}
|
||||
onMouseDown={handleMouseDown(column)}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleData.map((row, index) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
ref={(el) => (rowRefs.current[start + index] = el)}
|
||||
>
|
||||
{columnSet.map((column) => (
|
||||
<td key={column} style={{
|
||||
padding: '8px',
|
||||
width: columnWidths[column],
|
||||
maxWidth: columnWidths[column],
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{row[column]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
height: `${visibleRows * estimatedRowHeight}px`,
|
||||
overflowY: 'auto',
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
{pinnedColumns.length > 0 && (
|
||||
<div style={{
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '2px 0 5px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<div style={{ height: `${getTotalHeight()}px`, position: 'relative' }}>
|
||||
{renderTableContent(pinnedColumns)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
overflowX: 'auto',
|
||||
marginLeft: pinnedColumns.length > 0 ? '5px' : '0'
|
||||
}}>
|
||||
<div style={{ height: `${getTotalHeight()}px`, position: 'relative' }}>
|
||||
{renderTableContent(scrollableColumns)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VirtualTable;
|
||||
|
|
@ -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<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"c-fixed c-inset-0 c-z-50 c-bg-black/80 data-[state=open]:c-animate-in data-[state=closed]:c-animate-out data-[state=closed]:c-fade-out-0 data-[state=open]:c-fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"c-fixed c-left-[50%] c-top-[50%] c-z-50 c-grid c-w-full c-max-w-lg c-translate-x-[-50%] c-translate-y-[-50%] c-gap-4 c-border c-bg-background c-p-6 c-shadow-lg c-duration-200 data-[state=open]:c-animate-in data-[state=closed]:c-animate-out data-[state=closed]:c-fade-out-0 data-[state=open]:c-fade-in-0 data-[state=closed]:c-zoom-out-95 data-[state=open]:c-zoom-in-95 data-[state=closed]:c-slide-out-to-left-1/2 data-[state=closed]:c-slide-out-to-top-[48%] data-[state=open]:c-slide-in-from-left-1/2 data-[state=open]:c-slide-in-from-top-[48%] sm:c-rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"c-flex c-flex-col c-space-y-1.5 c-text-center sm:c-text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"c-flex c-flex-col-reverse sm:c-flex-row sm:c-justify-end sm:c-space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"c-text-lg c-font-semibold c-leading-none c-tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("c-text-sm c-text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
|
|
@ -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<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cx(
|
||||
"c-relative c-h-4 c-w-full c-overflow-hidden c-rounded-full c-bg-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="c-h-full c-w-full c-flex-1 c-bg-primary c-transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue