seti-ramesesv1
Version:
Reusable components and context for Next.js apps
181 lines (178 loc) • 10.4 kB
JavaScript
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