UNPKG

seti-ramesesv1

Version:

Reusable components and context for Next.js apps

181 lines (178 loc) 10.4 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { forwardRef, useState, useImperativeHandle, useEffect } from 'react'; import DataListBody from './DataListBody.js'; import DataListFooter from './DataListFooter.js'; import DataListHeader from './DataListHeader.js'; import DataListStates from './DataListStates.js'; import DataListToolbar from './DataListToolbar.js'; import Pencil from '../../../node_modules/lucide-react/dist/esm/icons/pencil.js'; import Eye from '../../../node_modules/lucide-react/dist/esm/icons/eye.js'; import Trash from '../../../node_modules/lucide-react/dist/esm/icons/trash.js'; const DataList = forwardRef(({ listHandler, cols, states, handler, orderby, limit = 10, openItem, hideToolbar = false, allowSearch = false, searchMode = "manual", onEdit, onView, onDelete, emptyState, dropdown, select, searchFields, addAction, addToolbar, toolbarActions, error: externalError, }, ref) => { // --- 1️⃣ Grouped data state --- const [dataState, setDataState] = useState({ items: [], loading: false, error: null, states: [], // 🆕 added for sidebar states activeState: null, // 🆕 track currently active state }); // =================================== 2️⃣ Separate UI state =================================== const [start, setStart] = useState(0); const [expandedRowIndex, setExpandedRowIndex] = useState(null); const [searchText, setSearchText] = useState(""); const [appliedFilters, setAppliedFilters] = useState({}); const handlerCols = listHandler?.getColumns?.() ?? []; // Merge handler + prop columns (deduplicate by id) const mergedCols = [...handlerCols, ...(cols ?? [])].filter((col, index, self) => index === self.findIndex((c) => c.id === col.id)); const [visibleCols, setVisibleCols] = useState(mergedCols.filter((c) => c.visible !== false)); const fetcher = listHandler ? listHandler.getList : handler; // --- 3️⃣ Row actions --- const defaultActions = [ onEdit ? { name: "Edit", icon: jsx(Pencil, { size: 16, className: "text-green-500" }), onClick: onEdit, order: 2 } : null, onView ? { name: "View", icon: jsx(Eye, { size: 16, className: "text-blue-600" }), onClick: onView, order: 3 } : null, onDelete ? { name: "Delete", icon: jsx(Trash, { size: 16, className: "text-red-500" }), onClick: onDelete, order: 4 } : null, ].filter(Boolean); const allRowActions = [...(addAction?.map((a) => ({ ...a, order: a.order ? a.order + 1 : 1 })) ?? []), ...defaultActions].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); const showActions = allRowActions.length > 0; const sortedToolbarActions = (toolbarActions ?? []).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); const handleSelectState = (state) => { setDataState((prev) => ({ ...prev, activeState: state.name })); // console.log(state); // TODO: later: trigger reload of list based on selected state // Example: doSearch({ _start: 0, state: state.name }); }; // ================================= 4️⃣ Fetch / Search function ================================= const doSearch = async (params) => { if (!fetcher) return; setDataState((prev) => ({ ...prev, loading: true, error: null })); try { let pkcol = null; mergedCols.filter((c) => { if (!pkcol && c.primary) pkcol = c; }); if (!pkcol) setDataState((prev) => ({ ...prev, error: "Specify a primary column" })); const projection = {}; mergedCols.forEach((c) => { projection[c.id] = 1; }); const uFilter = { ...(params.filters ?? appliedFilters ?? {}), // ...(params.states ? { state: params.states } : {}), }; const query = { start: params._start ?? start, // limit: params.limit ?? limit, cols: params.cols, state: params.state, searchtext: searchText, orderby: orderby ?? null, filter: uFilter, searchfields: searchFields ?? undefined, projection, }; const results = await fetcher(query); const resolvedItems = results == null ? [] : Array.isArray(results) ? results : results.data; //console.log("resolvedItems", resolvedItems); setDataState((prev) => ({ ...prev, items: resolvedItems ?? [] })); } catch (err) { setDataState((prev) => ({ ...prev, error: err?.message ?? "An unexpected error occurred." })); } finally { setDataState((prev) => ({ ...prev, loading: false })); } }; // =================================== 5️⃣ Ref API =================================== useImperativeHandle(ref, () => ({ refresh: () => doSearch({ _start: start }), update: (newItems) => setDataState((prev) => ({ ...prev, items: newItems })), search: () => doSearch({ _start: 0 }), updateItem: (predicate, newItem) => setDataState((prev) => ({ ...prev, items: prev.items.map((item) => (predicate(item) ? { ...item, ...newItem } : item)), })), removeItem: (predicate) => setDataState((prev) => ({ ...prev, items: prev.items.filter((item) => !predicate(item)), })), })); // =================================== 6️⃣ Effects =================================== // Update visible columns if handler or prop columns change useEffect(() => { setVisibleCols(mergedCols.filter((c) => c.visible !== false)); }, [listHandler, cols]); // Inject uiref to listHandler useEffect(() => { if (listHandler) listHandler.uiref = { refresh: () => doSearch({ _start: start }), update: (newItems) => setDataState((prev) => ({ ...prev, items: newItems })), search: () => doSearch({ _start: 0 }), updateItem: (predicate, newItem) => setDataState((prev) => ({ ...prev, items: prev.items.map((item) => (predicate(item) ? { ...item, ...newItem } : item)), })), removeItem: (predicate) => setDataState((prev) => ({ ...prev, items: prev.items.filter((item) => !predicate(item)), })), }; }, [listHandler, start]); // Auto search debounce useEffect(() => { if (searchMode === "auto") { const timeout = setTimeout(() => doSearch({ _start: start }), 300); return () => clearTimeout(timeout); } }, [searchText, appliedFilters, start, searchMode]); // Load states from handler useEffect(() => { const loadStates = async () => { // 1️⃣ If states prop was directly passed if (states && states.length > 0) { const initialState = states[0].name; setDataState((prev) => ({ ...prev, states, activeState: prev.activeState ?? initialState, })); await doSearch({ _start: 0, state: initialState }); return; } // 2️⃣ If states not passed but listHandler can provide them if (listHandler?.getStates) { const result = await listHandler.getStates(); if (Array.isArray(result) && result.length > 0) { const initialState = result[0].name; setDataState((prev) => ({ ...prev, states: result, activeState: initialState, })); await doSearch({ _start: 0, state: initialState }); } else { // 🧩 getStates exists but returned nothing await doSearch({ _start: 0 }); } return; } // 3️⃣ Fallback — no states prop and no listHandler.getStates await doSearch({ _start: 0 }); }; loadStates(); }, [states, listHandler]); const hasNext = dataState.items.length > limit; const finalError = externalError || dataState.error; return (jsxs("div", { className: "flex flex-col font-sans font-sm rounded-md relative", children: [finalError && (jsx("div", { className: "bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-md mb-2 flex items-center justify-between", children: jsx("span", { className: "ml-2 text-sm", children: finalError }) })), !hideToolbar && (jsx(DataListToolbar, { addToolbar: addToolbar, select: select, sortedToolbarActions: sortedToolbarActions, visibleCols: visibleCols, setVisibleCols: setVisibleCols, allowSearch: allowSearch, searchText: searchText, setSearchText: setSearchText, searchMode: searchMode, onSearch: () => doSearch({ _start: 0 }), onRefresh: () => doSearch({ _start: start }), allCols: mergedCols })), jsxs("div", { className: "flex flex-row flex-1 overflow-hidden", children: [dataState.states.length > 0 && (jsx(DataListStates, { states: dataState.states, activeState: dataState.activeState, onSelectState: handleSelectState })), jsx("div", { className: "flex-1 overflow-y-auto relative", children: jsxs("table", { className: "w-full border-collapse table-auto", children: [jsx(DataListHeader, { visibleCols: visibleCols, showActions: showActions, dropdown: !!dropdown }), jsx(DataListBody, { items: dataState.items, visibleCols: visibleCols, showActions: showActions, allRowActions: allRowActions, dropdown: dropdown, expandedRowIndex: expandedRowIndex, setExpandedRowIndex: setExpandedRowIndex, loading: dataState.loading, limit: limit, emptyState: emptyState, openItem: openItem })] }) })] }), jsx(DataListFooter, { start: start, limit: limit, hasNext: hasNext, onChangeStart: (newStart) => { setStart(newStart); doSearch({ _start: newStart }); } })] })); }); export { DataList as default }; //# sourceMappingURL=Datalist.js.map