import { GFCol, parseGenField } from "@/gen/utils"; import { cn } from "@/utils"; import { fields_map } from "@/utils/format-value"; import { useLocal } from "@/utils/use-local"; import { set } from "lib/utils/set"; import get from "lodash.get"; import { AlertTriangle, ChevronDown, ChevronRight, Loader2, Sticker, } from "lucide-react"; import { ChangeEvent, FC, MouseEvent, ReactElement, ReactNode, useEffect, } from "react"; import DataGrid, { ColumnOrColumnGroup, RenderCellProps, Row, SELECT_COLUMN_KEY, SortColumn, } from "react-data-grid"; import "react-data-grid/lib/styles.css"; import { createPortal } from "react-dom"; import { Toaster } from "sonner"; import { call_prasi_events } from "../../.."; import { filterWhere } from "../filter/parser/filter-where"; import { getFilter } from "../filter/utils/get-filter"; import { MDLocal } from "../md/utils/typings"; import { Skeleton } from "../ui/skeleton"; import { toast } from "../ui/toast"; import { sortTree } from "./utils/sort-tree"; import { TLList } from "./TLList"; import { OnRowClick } from "./utils/type"; import { TLSlider } from "./TLSlider"; let EMPTY_SET = new Set() as ReadonlySet; type SelectedRow = (arg: { row: any; rows: any[]; idx: any }) => boolean; type TableListProp = { child: any; PassProp: any; list: { type: string; item_w: string }; name: string; value?: any[]; on_load?: (arg: { reload: () => Promise; orderBy?: Record>; paging: { take: number; skip: number }; mode: "count" | "query"; }) => Promise; on_init: (arg?: any) => any; mode: "table" | "list" | "grid" | "auto"; _item: PrasiItem; __props?: any; gen_fields: string[]; row_click: OnRowClick; selected: SelectedRow; show_header?: boolean; id_parent?: string; feature?: Array; filter_name: string; render_row?: (child: any, data: any) => ReactNode; row_height?: number | ((row: any) => number); render_col?: (arg: { props: RenderCellProps; tbl: any; child: any; }) => ReactNode; gen_table?: string; softdel_type?: string; paging?: boolean; cache_row?: boolean; md?: MDLocal; }; const w = window as any; const selectCellClassname = css` display: flex; align-items: center; justify-content: center; > input { margin: 0; } `; export const TableList: FC = ({ name, on_load, child, PassProp, mode: _mode, on_init, _item, gen_fields, row_click, selected, id_parent, feature, filter_name, row_height: rowHeight, render_col, show_header, list, value, paging, cache_row, __props, md, }) => { let mode = _mode; if (mode === "auto") { if (w.isMobile) { mode = "list"; } else { mode = "table"; } } let ls_sort = localStorage.getItem( `sort-${location.pathname}-${location.hash}-${name}` ) as unknown as { columns: any; orderBy: any }; if (ls_sort) { ls_sort = JSON.parse(ls_sort as any); } const local = useLocal( { times: 0, selectedRows: [] as { pk: string | number; rows: any; }[], el: null as null | HTMLDivElement, width: 0, height: 0, selectedRowIds: [] as (string | number)[], pk: null as null | GFCol, scrolled: false, data: [] as any[], status: "init" as | "loading" | "ready" | "resizing" | "reload" | "init" | "error", where: null as any, firstKey: "", should_toast: true, paging: { take: 100, skip: 0, timeout: null as any, total: 0, last_length: 0, scroll: (currentTarget: HTMLDivElement) => { if ( isEditor || local.data.length < local.paging.take || local.data.length === 0 || local.status !== "ready" || !isAtBottom(currentTarget) || local.reloading ) return; if (local.paging.last_length <= local.data.length) { local.paging.skip = local.data.length; local.reload(); } }, }, grid_ref: null as null | HTMLDivElement, collapsed: new Set(), cached_row: new WeakMap(), filtering: "" as ReactNode | string | true, reloading: null as any, reload: (arg?: { toast: boolean }) => { if (local.reloading) return local.reloading; local.reloading = new Promise(async (done) => { let should_toast = true; if (arg?.toast === false) should_toast = false; local.should_toast = should_toast; local.filtering = ""; if (typeof on_load === "function") { local.status = "loading"; local.render(); const orderBy = local.sort.orderBy || undefined; const where = filterWhere(filter_name, __props); if (where?.OR?.length > 0) { const key = Object.keys(where.OR[0])[0]; if (key && where.OR[0][key]) { let filtering = where.OR[0][key].contains; if (typeof local.filtering === "string") { filtering = filtering.slice(1, -1); } else { filtering = ""; } if (filtering) { local.filtering = (
Searching for:
"{filtering.trim()}"
); } } } if (md) { if (md.header.loading) { await new Promise((resolve) => { const ival = setInterval(() => { if (!md.header.loading) { clearInterval(ival); resolve(); } }, 10); }); } const last = md.params.links[md.params.links.length - 1]; if (last && last.where) { if ((last.name && last.name === md.name) || !last.name) { for (const [k, v] of Object.entries(last.where)) { where[k] = v; } } } } call_prasi_events("tablelist", "where", [ __props?.gen__table, where, ]); const load_args: any = { async reload() {}, orderBy, where, paging: { take: local.paging.take > 0 ? local.paging.take : undefined, skip: local.paging.skip, }, }; if (id_parent) { load_args.paging = {}; } const result = on_load({ ...load_args, mode: "query" }); const callback = (data: any[]) => { if ( id_parent || !local.paging || (local.paging && !local.paging.take) ) { local.data = data; } else { local.data = [...local.data, ...data]; } local.paging.last_length = local.data.length; local.status = "ready"; local.reloading = null; local.render(); done(); setTimeout(() => { if ( local.grid_ref && !id_parent && (paging !== undefined || paging) ) { local.paging.scroll(local.grid_ref); } }, 100); }; if (result instanceof Promise) { (async () => { try { callback(await result); } catch (e) { console.error(e); local.status = "error"; toast.dismiss(); toast.error(
Failed to load data
, { dismissible: true, className: css` background: #ffecec; border: 2px solid red; `, } ); } })(); } else callback(result); } }); return local.reloading; }, sort: { columns: (ls_sort?.columns || []) as SortColumn[], on_change: (cols: SortColumn[]) => { if (feature?.find((e) => e === "sorting")) { local.sort.columns = cols; local.paging.skip = 0; if (cols.length > 0) { let { columnKey, direction } = cols[0]; if (columnKey.includes(".")) { let root: any = {}; set(root, columnKey, direction === "ASC" ? "asc" : "desc"); local.sort.orderBy = root; } else { let should_set = true; const gf = JSON.stringify(gen_fields); const fields = fields_map.get(gf); if (fields) { const rel = fields?.find((e) => e.name === columnKey); if (rel && rel.checked) { should_set = false; if (rel.type === "has-many") { local.sort.orderBy = { [columnKey]: { _count: direction === "ASC" ? "asc" : "desc", }, }; } else { const field = rel.checked.find((e) => !e.is_pk); if (field) { local.sort.orderBy = { [columnKey]: { [field.name]: direction === "ASC" ? "asc" : "desc", }, }; } else if (rel.relation) { local.sort.orderBy = { [columnKey]: { [rel.relation.to.fields[0]]: direction === "ASC" ? "asc" : "desc", }, }; } } } } if (should_set) { local.sort.orderBy = { [columnKey]: direction === "ASC" ? "asc" : "desc", }; } } } else { local.sort.orderBy = null; } localStorage.setItem( `sort-${location.pathname}-${location.hash}-${name}`, JSON.stringify({ columns: local.sort.columns, orderBy: local.sort.orderBy, }) ); local.status = "reload"; local.render(); } }, orderBy: (ls_sort?.orderBy || null) as null | Record< string, "asc" | "desc" | Record >, }, soft_delete: { field: null as any, }, }, ({ setDelayedRender }) => { setDelayedRender(true); } ); const reload = local.reload; if (md) { md.master.list = local; md.master.reload = reload; } if (filter_name) { const f = getFilter(filter_name); if (f) { f.list.ref[_item.id] = { reload }; } } // code ini digunakan untuk mengambil nama dari pk yang akan digunakan sebagai key untuk id const pk = local.pk?.name || "id"; useEffect(() => { if (isEditor || value) { on_init(local); if (isEditor && local.data.length === 0 && local.status === "ready") { reload(); } return; } (async () => { on_init(local); if ( (local.status === "init" || local.status === "reload") && typeof on_load === "function" ) { reload(); } })(); }, [local.status, on_load, local.sort.orderBy]); const raw_childs = get( child, "props.meta.item.component.props.child.content.childs" ); let childs: any[] = []; let sub_name: string[] = []; switch (mode) { case "table": sub_name = ["tbl-col", "table: columns"]; break; case "list": sub_name = ["list-row", "list: fields"]; break; } // passing local data ke variable baru (biar gak panjang nulisnya hehe) let rowData = local.data; // function untuk menghandle checkbox di header (digunakan untuk check all) const headerCheckboxClick = (e: ChangeEvent) => { if (e.target.checked && Array.isArray(rowData)) { // jika checbox checked, maka semua rowData akan dimasukkan ke dalam local selected rows rowData.forEach((data) => { local.selectedRows.push({ pk: data[pk], rows: data, }); }); local.render(); } else { // jika tidak, maka local selected rows akan dikosongkan local.selectedRows = []; local.render(); } }; // function untuk menghandle checkbox pada setiap row (digunakan untuk check setiap rowData) const checkboxClick = (rowId: any) => (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); const checked = !!local.selectedRows.find((data) => data.pk === rowId); if (!checked) { // jika checkbox tercheck, maka rowData akan diambil jika memiliki id yang sama dengan rowId yang dikirim const checkedRowData = rowData.filter((row) => row[pk] === rowId); local.selectedRows.push({ pk: rowId, rows: checkedRowData, }); local.render(); } else { // jika tidak, maka akan dihapus local.selectedRows = local.selectedRows.filter( (data) => data.pk !== rowId ); local.render(); } }; const mode_child = raw_childs.find( (e: any) => sub_name.includes(e.name) || e.name === mode ); if (mode_child) { const tbl = _item.edit.childs[0].edit.childs.find( (e: any) => get(e, "id") === mode_child.id ); const meta = tbl; if (meta && meta.childs) { childs = meta.childs; } } let columns: ColumnOrColumnGroup[] = []; let isCheckbox = false; let isTree = false; try { if (feature?.find((e) => e === "checkbox")) isCheckbox = true; if (feature?.find((e) => e === "tree")) isTree = true; } catch (e) {} if (local.status === "init") { const fields = parseGenField(gen_fields); for (const field of fields) { if (field.is_pk) { local.pk = field; } } } if (typeof value !== "undefined") { local.data = value; local.status = "ready" as any; } else { if (isEditor && local.status !== "ready") { if (local.data.length === 0) { const load_args: any = { async reload() {}, where: {}, paging: { take: local.paging.take > 0 ? local.paging.take : undefined, skip: local.paging.skip, }, }; if (id_parent) load_args.paging = {}; if (typeof on_load === "function") { let res = on_load({ ...load_args, mode: "query" }) as any; if (typeof res === "object" && res instanceof Promise) { res.then((e) => { local.data = e; }); } else { local.data = res; } } } local.status = "ready"; } } let data = Array.isArray(local.data) ? local.data : []; if (typeof local.data === "string") console.error(local.data); if (isEditor) { if (data.length > 0) { w.prasi_table_list_temp_data = data; } else if ( w.prasi_table_list_temp_data && w.prasi_table_list_temp_data.length > 0 ) { data = w.prasi_table_list_temp_data; } } if (isTree && id_parent && local.pk && local.sort.columns.length === 0) { data = sortTree(local.data, id_parent, local.pk.name).filter((e) => { if (local.pk && local.collapsed.has(e?.__parent?.[local.pk.name])) { return false; } return true; }); } if (childs.length && isCheckbox) { columns.push({ key: SELECT_COLUMN_KEY, name: "", width: 35, minWidth: 35, maxWidth: 35, resizable: false, sortable: false, frozen: true, renderHeaderCell(props) { return ; }, renderCell(props) { // digunakan untuk mengecek apakah local selected rows memiliki pk dari props.row.id const isChecked = local.selectedRows.some( (checked) => checked.pk === props.row.id ); return (
); }, headerCellClass: selectCellClassname, cellClass: selectCellClassname, }); } let first = true; for (const child of childs) { let key = getProp(child, "name", {}); const name = getProp(child, "title", ""); const type = getProp(child, "type", ""); const width = parseInt(getProp(child, "width", {})); if (type === "checkbox") { columns.push({ key, name, width: 35, minWidth: 45, resizable: true, sortable: true, frozen: true, renderHeaderCell(props) { return (
{/* */}
); }, renderCell(props) { if (typeof render_col === "function") return render_col({ props, tbl: local, child, }); return ( {child} ); }, }); } else { columns.push({ key, name, width: width > 0 ? width : undefined, resizable: true, sortable: true, renderCell(props) { if (typeof render_col === "function") return render_col({ props, tbl: local, child, }); return ( <> {isTree && local.firstKey === key && local.pk && (
{ if (!local.pk) return; if (props?.row?.__children?.length > 0) { e.stopPropagation(); if (!local.collapsed.has(props.row?.[local.pk.name])) { local.collapsed.add(props.row?.[local.pk.name]); } else { local.collapsed.delete(props.row?.[local.pk.name]); } local.render(); } }} >
{props.row?.__children?.length > 0 && ( <> {local.collapsed.has(props.row?.[local.pk.name]) ? ( ) : ( )} )}
{props.row?.__parent && (props.row?.__children || []).length === 0 && (
)}
)} {child} ); }, }); if (first) { first = false; local.firstKey = key; } } } if (mode === "list") { if (columns.length > 1) columns = columns.slice(0, 0 + 1); } if (!isEditor) { let should_toast = true; if (md) { if (md.props.mode !== "full") { should_toast = false; } } if (should_toast) { if (local.status === "loading") { toast.dismiss(); toast.loading( <> Loading Data ... ); } else { toast.dismiss(); } } } if (document.getElementsByClassName("prasi-toaster").length === 0) { const elemDiv = document.createElement("div"); elemDiv.className = "prasi-toaster"; document.body.appendChild(elemDiv); } const toaster_el = document.getElementsByClassName("prasi-toaster")[0]; if (mode === "table") { if (local.status === "resizing" && !isEditor) { local.status = "ready"; local.render(); return null; } return ( <>
{local.status !== "ready" && (
)}
{ local.el = e; if (e) local.height = e.offsetHeight; }} > {toaster_el && createPortal( , toaster_el )} {local.status === "init" ? ( { local.status = "reload"; local.render(); }, 100); return <>; }, }, ]} rows={genRows(200)} /> ) : ( <> { local.grid_ref = e?.element as any; }} onScroll={(e) => { local.paging.scroll(e.currentTarget); }} selectedRows={EMPTY_SET} onSelectedCellChange={() => {}} onSelectedRowsChange={() => {}} headerRowHeight={show_header === false ? 0 : undefined} renderers={ local.status !== "ready" ? undefined : { renderRow(key, props) { if ( cache_row === true && local.cached_row.has(props.row) ) { return local.cached_row.get(props.row); } const isSelect = selected({ idx: props.rowIdx, row: props.row, rows: local.data, }); const child_row = ( { if ( !isEditor && typeof row_click === "function" ) { row_click({ event: ev, idx: props.rowIdx, row: props.row, rows: local.data, }); } }} isRowSelected={true} className={cx( props.className, (isSelect || (md?.selected?.[local.pk?.name || ""] === props.row[local.pk?.name || ""] && props.row[local.pk?.name || ""])) && "row-selected" )} /> ); if (cache_row) { local.cached_row.set(props.row, child_row); } return child_row; }, noRowsFallback: (
No Data
{local.filtering && (
{local.filtering}
)}
), } } /> )}
); } else if (mode === "list") { return ( <> {toaster_el && createPortal( , toaster_el )} {list.type !== "slider" && list.type !== "grid" && ( )} {list.type === "slider" && ( )} ); } }; const CheckboxList: FC<{ on_click: (e: any) => void; value?: boolean; }> = ({ value, on_click }) => { const local = useLocal({ value: false as boolean, }); return (
{ local.value = !local.value; on_click(local.value); local.render(); }} className="c-flex c-flex-row c-space-x-1 cursor-pointer c-items-center rounded-full p-0.5" > {local.value ? ( ) : ( )}
); }; const genRows = (total: number) => { const result = [] as any[]; for (let i = 0; i < total; i++) { result.push({ _: i }); } return result; }; const dataGridStyle = (local: { el: null | HTMLDivElement }) => { return css` .rdg { display: grid !important; .rdg-cell, .rdg-header-sort-name { display: flex; flex-direction: row; align-items: stretch; &.rdg-header-sort-name { align-items: center; } } } .rdg { height: ${local.el?.clientHeight || 100}px; } div[role="row"]:hover { background: #e2f1ff; .num-edit { display: flex; } .num-idx { display: none; } } div[role="columnheader"] span svg { margin: 12px 2px; /* color: #ffffff */ } div[aria-selected="true"] { outline: none; } div[role="gridcell"] { padding-inline: 0px; } .row-selected { background: #bddfff !important; } `; }; function isAtBottom(currentTarget: HTMLDivElement): boolean { return ( currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight ); } function getProp(child: any, name: string, defaultValue?: any) { const fn = new Function( `return ${get(child, `component.props.${name}.valueBuilt`) || `null`}` ); return fn() || defaultValue; }