persist a advance filter obj
This commit is contained in:
parent
c51c6978e9
commit
a5bfad7214
119
src/services/pmsGrid/AdvanceFilter.jsx
Normal file
119
src/services/pmsGrid/AdvanceFilter.jsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const OPERATORS = {
|
||||
number: [
|
||||
{ key: "eq", label: "Equals" },
|
||||
{ key: "neq", label: "Not Equal" },
|
||||
{ key: "gt", label: "Greater Than" },
|
||||
{ key: "gte", label: "Greater or Equal" },
|
||||
{ key: "lt", label: "Less Than" },
|
||||
{ key: "lte", label: "Less or Equal" },
|
||||
{ key: "between", label: "Between" },
|
||||
],
|
||||
text: [
|
||||
{ key: "contains", label: "Contains" },
|
||||
{ key: "starts", label: "Starts With" },
|
||||
{ key: "ends", label: "Ends With" },
|
||||
{ key: "eq", label: "Equals" },
|
||||
],
|
||||
date: [
|
||||
{ key: "before", label: "Before" },
|
||||
{ key: "after", label: "After" },
|
||||
{ key: "between", label: "Between" },
|
||||
],
|
||||
};
|
||||
|
||||
export function AdvanceFilter({
|
||||
type = "number",
|
||||
value,
|
||||
onApply,
|
||||
onClear,
|
||||
}) {
|
||||
const [operation, setOperation] = useState("");
|
||||
const [v1, setV1] = useState("");
|
||||
const [v2, setV2] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setOperation("");
|
||||
setV1("");
|
||||
setV2("");
|
||||
return;
|
||||
}
|
||||
|
||||
setOperation(value.operation);
|
||||
setV1(value.value ?? value.from ?? "");
|
||||
setV2(value.to ?? "");
|
||||
}, [value]);
|
||||
|
||||
const apply = () => {
|
||||
if (!operation) return;
|
||||
|
||||
onApply(
|
||||
operation === "between"
|
||||
? { operation, from: v1, to: v2 }
|
||||
: { operation, value: v1 }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: 240 }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Condition</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={operation}
|
||||
onChange={(e) => setOperation(e.target.value)}
|
||||
>
|
||||
<option value="">Select</option>
|
||||
{OPERATORS[type].map((o) => (
|
||||
<option key={o.key} value={o.key}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{operation && (
|
||||
<div className="mb-2">
|
||||
<label className="form-label">Value</label>
|
||||
|
||||
{operation !== "between" ? (
|
||||
<input
|
||||
type={type === "date" ? "date" : "number"}
|
||||
className="form-control form-control-sm"
|
||||
value={v1}
|
||||
onChange={(e) => setV1(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<div className="d-flex gap-2">
|
||||
<input
|
||||
type={type === "date" ? "date" : "number"}
|
||||
className="form-control form-control-sm"
|
||||
placeholder="From"
|
||||
value={v1}
|
||||
onChange={(e) => setV1(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type={type === "date" ? "date" : "number"}
|
||||
className="form-control form-control-sm"
|
||||
placeholder="To"
|
||||
value={v2}
|
||||
onChange={(e) => setV2(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="d-flex justify-content-between mt-3">
|
||||
<button className="btn btn-label-secondary btn-xs" onClick={onClear}>
|
||||
Clear
|
||||
</button>
|
||||
<button className="btn btn-primary btn-xs" onClick={apply}>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -23,7 +23,7 @@ export default function PmsGrid({
|
||||
features = {},
|
||||
renderExpanded,
|
||||
}) {
|
||||
const [isFullScreen,setFullScreen] = useState(false)
|
||||
const [isFullScreen, setFullScreen] = useState(false);
|
||||
const grid = useGridCore({
|
||||
data,
|
||||
serverMode,
|
||||
@ -59,8 +59,11 @@ export default function PmsGrid({
|
||||
expanded,
|
||||
toggleExpand,
|
||||
setColState,
|
||||
onAdvanceFilters,
|
||||
setAdanceFilter,
|
||||
// onAdvanceFilters,
|
||||
// setAdanceFilter,
|
||||
|
||||
advanceFilters,
|
||||
setColumnAdvanceFilter,
|
||||
} = grid;
|
||||
|
||||
// --- Pin / Unpin helpers ---
|
||||
@ -149,7 +152,11 @@ export default function PmsGrid({
|
||||
const currentRows = rows;
|
||||
|
||||
return (
|
||||
<div className={`pms-grid ${isFullScreen ? "card card-action card-fullscreen p-4":""}`}>
|
||||
<div
|
||||
className={`pms-grid ${
|
||||
isFullScreen ? "card card-action card-fullscreen p-4" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="row px-4">
|
||||
<div className="col-8">
|
||||
<div className="d-flex flex-row gap-2 gap-2 ">
|
||||
@ -218,14 +225,22 @@ export default function PmsGrid({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<i className={`bx bx-${isFullScreen ? "exit-fullscreen":"fullscreen"} cursor-pointer`} onClick={()=>setFullScreen(!isFullScreen)}></i>
|
||||
<i
|
||||
className={`bx bx-${
|
||||
isFullScreen ? "exit-fullscreen" : "fullscreen"
|
||||
} cursor-pointer`}
|
||||
onClick={() => setFullScreen(!isFullScreen)}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="d-flex">
|
||||
<FilterApplied groupBy={groupBy} removeGroupby={()=>setGroupBy(null)} advance={onAdvanceFilters} />
|
||||
<FilterApplied
|
||||
groupBy={groupBy}
|
||||
removeGroupby={() => setGroupBy(null)}
|
||||
advance={advanceFilters}
|
||||
/>
|
||||
</div>
|
||||
{/* Table-Start*/}
|
||||
<div
|
||||
@ -322,7 +337,8 @@ export default function PmsGrid({
|
||||
onPinLeft={() => pinColumn(col.key, "left")}
|
||||
onPinRight={() => pinColumn(col.key, "right")}
|
||||
onUnpin={() => unpinColumn(col.key)}
|
||||
onAdvancedFilter={setAdanceFilter}
|
||||
advanceFilters={advanceFilters}
|
||||
setColumnAdvanceFilter={setColumnAdvanceFilter}
|
||||
/>
|
||||
)}
|
||||
{features.resizing && (
|
||||
@ -411,7 +427,8 @@ export default function PmsGrid({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)} <small>{totalRows} rows</small>
|
||||
)}{" "}
|
||||
<small>{totalRows} rows</small>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
|
||||
@ -1,155 +1,42 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useDropdownPosition } from "./useGridCore";
|
||||
import { AdvanceFilter } from "./AdvanceFilter";
|
||||
|
||||
const OPERATORS = {
|
||||
number: [
|
||||
{ key: "eq", label: "Equals" },
|
||||
{ key: "neq", label: "Not Equal" },
|
||||
{ key: "gt", label: "Greater Than" },
|
||||
{ key: "gte", label: "Greater or Equal" },
|
||||
{ key: "lt", label: "Less Than" },
|
||||
{ key: "lte", label: "Less or Equal" },
|
||||
{ key: "between", label: "Between" },
|
||||
],
|
||||
text: [
|
||||
{ key: "contains", label: "Contains" },
|
||||
{ key: "starts", label: "Starts With" },
|
||||
{ key: "ends", label: "Ends With" },
|
||||
{ key: "eq", label: "Equals" },
|
||||
],
|
||||
date: [
|
||||
{ key: "before", label: "Before" },
|
||||
{ key: "after", label: "After" },
|
||||
{ key: "between", label: "Between" },
|
||||
],
|
||||
};
|
||||
|
||||
// ----------- FILTER UI COMPONENT ----------
|
||||
function AdvanceFilter({ type = "number", onApply, onClear }) {
|
||||
const [operation, setOperator] = useState("");
|
||||
const [value1, setValue1] = useState("");
|
||||
const [value2, setValue2] = useState("");
|
||||
|
||||
const ops = OPERATORS[type];
|
||||
|
||||
const apply = () => {
|
||||
if (!operation) return;
|
||||
|
||||
if (operation === "between") {
|
||||
onApply({
|
||||
operation,
|
||||
from: value1,
|
||||
to: value2,
|
||||
});
|
||||
} else {
|
||||
onApply({
|
||||
operation,
|
||||
value: value1,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className=""
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ width: 240 }}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<label className="form-label text-decoration-none">Condition</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={operation}
|
||||
onChange={(e) => setOperator(e.target.value)}
|
||||
>
|
||||
<option value="">Select</option>
|
||||
{ops.map((o) => (
|
||||
<option key={o.key} value={o.key}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Values */}
|
||||
{operation && (
|
||||
<div className="text-decoration-none">
|
||||
<label className="form-label">Value</label>
|
||||
|
||||
{operation !== "between" ? (
|
||||
<input
|
||||
type={type === "date" ? "date" : "number"}
|
||||
className="form-control form-control-sm"
|
||||
value={value1}
|
||||
onChange={(e) => setValue1(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<div className="d-flex gap-2">
|
||||
<input
|
||||
type={type === "date" ? "date" : "number"}
|
||||
className="form-control form-control-sm"
|
||||
placeholder="From"
|
||||
value={value1}
|
||||
onChange={(e) => setValue1(e.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
type={type === "date" ? "date" : "number"}
|
||||
className="form-control form-control-sm"
|
||||
placeholder="To"
|
||||
value={value2}
|
||||
onChange={(e) => setValue2(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="d-flex flex-row mt-3">
|
||||
<button className="btn btn-primary btn-xs" onClick={apply}>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -------- HEADER OPTION ---------------
|
||||
const PmsHeaderOption = ({
|
||||
column,
|
||||
pinned,
|
||||
onPinLeft,
|
||||
onPinRight,
|
||||
onUnpin,
|
||||
onAdvancedFilter,
|
||||
}) => {
|
||||
const { key: columnKey, enableAdvancedFilter } = column;
|
||||
const rootRef = useRef(null);
|
||||
|
||||
// from useGridCore
|
||||
advanceFilters,
|
||||
setColumnAdvanceFilter,
|
||||
}) => {
|
||||
const {
|
||||
key: columnKey,
|
||||
enableAdvancedFilter,
|
||||
filterType = "number",
|
||||
} = column;
|
||||
|
||||
const rootRef = useRef(null);
|
||||
const btnRef = useRef(null);
|
||||
const menuRef = useRef(null);
|
||||
|
||||
const filterBtnRef = useRef(null);
|
||||
const filterMenuRef = useRef(null);
|
||||
|
||||
const pinBtnRef = useRef(null);
|
||||
const pinMenuRef = useRef(null);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openMenu, setOpenMenu] = useState(null);
|
||||
|
||||
// Main dropdown position
|
||||
const mainStyle = useDropdownPosition(btnRef, menuRef, open, 0);
|
||||
|
||||
// Submenus
|
||||
const filterStyle = useDropdownPosition(
|
||||
filterBtnRef,
|
||||
filterMenuRef,
|
||||
openMenu === "filter",
|
||||
1
|
||||
);
|
||||
|
||||
const pinStyle = useDropdownPosition(
|
||||
pinBtnRef,
|
||||
pinMenuRef,
|
||||
@ -159,51 +46,44 @@ const PmsHeaderOption = ({
|
||||
|
||||
const toggleMenu = (name, e) => {
|
||||
e.stopPropagation();
|
||||
setOpenMenu((prev) => (prev === name ? null : name));
|
||||
setOpenMenu((p) => (p === name ? null : name));
|
||||
};
|
||||
|
||||
// ----------------------------
|
||||
// CLOSE WHEN CLICKING OUTSIDE
|
||||
// ----------------------------
|
||||
useEffect(() => {
|
||||
const handleOutside = (e) => {
|
||||
if (!rootRef.current) return;
|
||||
|
||||
if (!rootRef.current.contains(e.target)) {
|
||||
const close = (e) => {
|
||||
if (!rootRef.current?.contains(e.target)) {
|
||||
setOpen(false);
|
||||
setOpenMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) document.addEventListener("mousedown", handleOutside);
|
||||
return () => document.removeEventListener("mousedown", handleOutside);
|
||||
if (open) document.addEventListener("mousedown", close);
|
||||
return () => document.removeEventListener("mousedown", close);
|
||||
}, [open]);
|
||||
|
||||
// {}
|
||||
console.log(advanceFilters);
|
||||
console.log(columnKey);
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className="dropdown "
|
||||
style={{ zIndex: 9999, position: "relative" }}
|
||||
>
|
||||
{/* BUTTON */}
|
||||
<div ref={rootRef} style={{ position: "relative", zIndex: 9999 }}>
|
||||
<button
|
||||
ref={btnRef}
|
||||
className="btn btn-icon btn-text-secondary rounded-pill p-0"
|
||||
class="btn btn-icon btn-text-secondary rounded-pill d-inline-block"
|
||||
onClick={() => setOpen((p) => !p)}
|
||||
>
|
||||
<i className="bx bx-dots-vertical-rounded bx-sm text-muted"></i>
|
||||
<span className="icon-base bx bx-dots-vertical-rounded bx-sm"></span>
|
||||
{advanceFilters[columnKey] && (
|
||||
<span className="badge rounded-pill bg-info badge-dot badge-notifications"></span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* MAIN MENU */}
|
||||
{open && (
|
||||
<ul
|
||||
ref={menuRef}
|
||||
className="dropdown-menu dropdown-menu-end shadow border rounded-3 py-2 show"
|
||||
className="dropdown-menu dropdown-menu-end shadow rounded-3 py-2 show"
|
||||
style={mainStyle}
|
||||
>
|
||||
{/* ADVANCED FILTER */}
|
||||
{enableAdvancedFilter && (
|
||||
<li className="dropdown-submenu dropstart position-relative">
|
||||
<li className="dropdown-submenu dropstart">
|
||||
<button
|
||||
ref={filterBtnRef}
|
||||
className="dropdown-item d-flex justify-content-between py-1 px-2"
|
||||
@ -218,23 +98,22 @@ const PmsHeaderOption = ({
|
||||
{openMenu === "filter" && (
|
||||
<ul
|
||||
ref={filterMenuRef}
|
||||
className="dropdown-menu shadow rounded-3 py-2 p-0 show"
|
||||
className="dropdown-menu shadow rounded-3 p-2 show"
|
||||
style={filterStyle}
|
||||
>
|
||||
<li className="p-2">
|
||||
<AdvanceFilter
|
||||
type="number"
|
||||
onApply={(f) => onAdvancedFilter({ ...f, columnKey })}
|
||||
onClear={() => onAdvancedFilter(null)}
|
||||
type={filterType}
|
||||
value={advanceFilters[columnKey]}
|
||||
onApply={(f) => setColumnAdvanceFilter(columnKey, f)}
|
||||
onClear={() => setColumnAdvanceFilter(columnKey, null)}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* PIN COLUMN */}
|
||||
<li className="dropdown-submenu dropstart position-relative">
|
||||
{/* PIN */}
|
||||
<li className="dropdown-submenu dropstart">
|
||||
<button
|
||||
ref={pinBtnRef}
|
||||
className="dropdown-item d-flex justify-content-between py-1 px-2"
|
||||
@ -249,39 +128,18 @@ const PmsHeaderOption = ({
|
||||
{openMenu === "pin" && (
|
||||
<ul
|
||||
ref={pinMenuRef}
|
||||
className=" dropdown-menu dropdown-menu-end shadow border rounded-3 py-2 show"
|
||||
className="dropdown-menu shadow rounded-3 py-2 show"
|
||||
style={pinStyle}
|
||||
>
|
||||
<li>
|
||||
<button className="dropdown-item py-1 px-2" onClick={onUnpin}>
|
||||
{!pinned && <i className="bx bx-check me-2"></i>}
|
||||
<button className="dropdown-item" onClick={onUnpin}>
|
||||
No Pin
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<button
|
||||
className={`dropdown-item py-1 px-2 ${
|
||||
pinned === "left" ? "active" : ""
|
||||
}`}
|
||||
onClick={onPinLeft}
|
||||
>
|
||||
{pinned === "left" && <i className="bx bx-check me-2"></i>}
|
||||
<button className="dropdown-item" onClick={onPinLeft}>
|
||||
Pin Left
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<button
|
||||
className={`dropdown-item py-1 px-2 ${
|
||||
pinned === "right" ? "active" : ""
|
||||
}`}
|
||||
onClick={onPinRight}
|
||||
>
|
||||
{pinned === "right" && <i className="bx bx-check me-2"></i>}
|
||||
<button className="dropdown-item" onClick={onPinRight}>
|
||||
Pin Right
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@ -6,6 +6,8 @@ import { useState, useMemo, useCallback, useEffect,useLayoutEffect } from "react
|
||||
- rowKey
|
||||
- initialPageSize
|
||||
*/
|
||||
|
||||
|
||||
export function useGridCore({
|
||||
data,
|
||||
serverMode = false,
|
||||
@ -19,47 +21,57 @@ export function useGridCore({
|
||||
const [search, setSearch] = useState("");
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
const [groupBy, setGroupBy] = useState(null);
|
||||
const [serverGroupRows, setServerGroupRows] = useState(null);
|
||||
const [onAdvanceFilter, setAdanceFilter] = useState(null);
|
||||
|
||||
// FIX: store ADVANCED FILTERS PER COLUMN
|
||||
// { amount: { columnKey, operation, value } }
|
||||
const [advanceFilters, setAdvanceFilters] = useState({});
|
||||
|
||||
const [sortBy, setSortBy] = useState({ key: null, dir: "asc" });
|
||||
const [selected, setSelected] = useState(new Set());
|
||||
const [expanded, setExpanded] = useState(new Set());
|
||||
|
||||
const [colState, setColState] = useState(() =>
|
||||
columns.map((c, i) => ({ ...c, visible: c.visible !== false, order: i }))
|
||||
columns.map((c, i) => ({
|
||||
...c,
|
||||
visible: c.visible !== false,
|
||||
order: i,
|
||||
}))
|
||||
);
|
||||
|
||||
const [totalRows, setTotalRows] = useState(data ? data.length : 0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [serverRows, setServerRows] = useState([]);
|
||||
|
||||
/* ---------------- SEARCH (DEBOUNCE) ---------------- */
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearch(search);
|
||||
setPage(1); // Important — when search changes, go back to page 1
|
||||
setPage(1);
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
}, [search]);
|
||||
|
||||
// client-side derived rows
|
||||
/* ---------------- CLIENT MODE ---------------- */
|
||||
const clientFiltered = useMemo(() => {
|
||||
if (!data) return [];
|
||||
const q = (search || "").toLowerCase();
|
||||
const filtered = q
|
||||
? data.filter((r) =>
|
||||
|
||||
let filtered = data;
|
||||
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
filtered = filtered.filter((r) =>
|
||||
Object.values(r).some((v) =>
|
||||
String(v ?? "")
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
String(v ?? "").toLowerCase().includes(q)
|
||||
)
|
||||
)
|
||||
: data;
|
||||
// sort if needed
|
||||
);
|
||||
}
|
||||
|
||||
if (sortBy.key) {
|
||||
const dir = sortBy.dir === "asc" ? 1 : -1;
|
||||
filtered.sort((a, b) => {
|
||||
const A = a[sortBy.key],
|
||||
B = b[sortBy.key];
|
||||
filtered = [...filtered].sort((a, b) => {
|
||||
const A = a[sortBy.key];
|
||||
const B = b[sortBy.key];
|
||||
if (A == null && B == null) return 0;
|
||||
if (A == null) return -1 * dir;
|
||||
if (B == null) return 1 * dir;
|
||||
@ -68,14 +80,17 @@ export function useGridCore({
|
||||
return String(A).localeCompare(String(B)) * dir;
|
||||
});
|
||||
}
|
||||
|
||||
setTotalRows(filtered.length);
|
||||
|
||||
const start = (page - 1) * pageSize;
|
||||
return filtered.slice(start, start + pageSize);
|
||||
}, [data, search, sortBy, page, pageSize]);
|
||||
|
||||
// server-side fetch
|
||||
/* ---------------- SERVER MODE ---------------- */
|
||||
const fetchServer = useCallback(async () => {
|
||||
// sorting column wise
|
||||
if (!serverMode || typeof fetcher !== "function") return;
|
||||
|
||||
const sortFilters = sortBy.key
|
||||
? [
|
||||
{
|
||||
@ -85,22 +100,15 @@ export function useGridCore({
|
||||
]
|
||||
: [];
|
||||
|
||||
// -----------------------------
|
||||
// 3. ADVANCED FILTERS (NUMERIC FILTER POPOVER)
|
||||
// The grid will pass "filter" already shaped like:
|
||||
// filter = { totalAmount: { type: "gt", value: 200 } }
|
||||
// -----------------------------
|
||||
let advanceFilters = [];
|
||||
if (onAdvanceFilter) {
|
||||
advanceFilters.push(onAdvanceFilter);
|
||||
}
|
||||
// convert map → array
|
||||
const advanceFilterArray = Object.values(advanceFilters);
|
||||
|
||||
const filterPayload = JSON.stringify({
|
||||
sortFilters,
|
||||
groupByColumn: groupBy || null,
|
||||
advanceFilters,
|
||||
advanceFilters: advanceFilterArray,
|
||||
});
|
||||
|
||||
if (!serverMode || typeof fetcher !== "function") return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await fetcher({
|
||||
@ -110,9 +118,9 @@ export function useGridCore({
|
||||
search: debouncedSearch,
|
||||
filter: filterPayload,
|
||||
});
|
||||
// expected: { rows: [], total }
|
||||
setServerRows(resp.rows || []);
|
||||
setTotalRows(resp.total || (resp.rows ? resp.rows.length : 0));
|
||||
|
||||
setServerRows(resp?.rows || []);
|
||||
setTotalRows(resp?.total ?? resp?.rows?.length ?? 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -124,14 +132,35 @@ export function useGridCore({
|
||||
sortBy,
|
||||
debouncedSearch,
|
||||
groupBy,
|
||||
onAdvanceFilter,
|
||||
advanceFilters,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (serverMode) fetchServer();
|
||||
}, [serverMode, fetchServer]);
|
||||
|
||||
// selection
|
||||
/* ---------------- ADVANCED FILTER API ---------------- */
|
||||
const setColumnAdvanceFilter = useCallback((columnKey, filter) => {
|
||||
setAdvanceFilters((prev) => {
|
||||
if (!filter) {
|
||||
const copy = { ...prev };
|
||||
delete copy[columnKey];
|
||||
return copy;
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[columnKey]: {
|
||||
columnKey,
|
||||
...filter,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
/* ---------------- SELECTION ---------------- */
|
||||
const toggleSelect = useCallback((id) => {
|
||||
setSelected((prev) => {
|
||||
const s = new Set(prev);
|
||||
@ -162,6 +191,7 @@ export function useGridCore({
|
||||
[rowKey]
|
||||
);
|
||||
|
||||
/* ---------------- EXPAND ---------------- */
|
||||
const toggleExpand = useCallback((id) => {
|
||||
setExpanded((prev) => {
|
||||
const s = new Set(prev);
|
||||
@ -170,72 +200,81 @@ export function useGridCore({
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* ---------------- SORT ---------------- */
|
||||
const changeSort = useCallback((key) => {
|
||||
setSortBy((prev) => {
|
||||
// first click = asc
|
||||
if (prev.key !== key) {
|
||||
return { key, dir: "asc" };
|
||||
}
|
||||
|
||||
// second click = desc
|
||||
if (prev.dir === "asc") {
|
||||
return { key, dir: "desc" };
|
||||
}
|
||||
|
||||
// third click = remove sort
|
||||
if (prev.key !== key) return { key, dir: "asc" };
|
||||
if (prev.dir === "asc") return { key, dir: "desc" };
|
||||
return { key: null, dir: "asc" };
|
||||
});
|
||||
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
/* ---------------- COLUMNS ---------------- */
|
||||
const visibleColumns = useMemo(
|
||||
() => colState.filter((c) => c.visible).sort((a, b) => a.order - b.order),
|
||||
[colState]
|
||||
);
|
||||
|
||||
const rows = serverMode ? serverRows : clientFiltered;
|
||||
const totalPages = Math.max(1, Math.ceil((totalRows || 0) / pageSize));
|
||||
|
||||
// update columns externally (reorder/pin/resize)
|
||||
const updateColumn = useCallback((key, patch) => {
|
||||
setColState((prev) =>
|
||||
prev.map((c) => (c.key === key ? { ...c, ...patch } : c))
|
||||
);
|
||||
}, []);
|
||||
|
||||
/* ---------------- FINAL ---------------- */
|
||||
const rows = serverMode ? serverRows : clientFiltered;
|
||||
const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
|
||||
|
||||
return {
|
||||
// state
|
||||
// paging
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
totalRows,
|
||||
totalPages,
|
||||
|
||||
// loading
|
||||
loading,
|
||||
|
||||
// search
|
||||
search,
|
||||
setSearch,
|
||||
|
||||
// sorting
|
||||
sortBy,
|
||||
changeSort,
|
||||
|
||||
// selection
|
||||
selected,
|
||||
setSelected,
|
||||
toggleSelect,
|
||||
selectAllOnPage,
|
||||
deselectAllOnPage,
|
||||
setSelected,
|
||||
|
||||
// expand
|
||||
expanded,
|
||||
toggleExpand,
|
||||
|
||||
// columns
|
||||
colState,
|
||||
visibleColumns,
|
||||
updateColumn,
|
||||
setColState,
|
||||
|
||||
// grouping
|
||||
groupBy,
|
||||
setGroupBy,
|
||||
onAdvanceFilter,
|
||||
setAdanceFilter,
|
||||
|
||||
// advanced filter
|
||||
advanceFilters,
|
||||
setColumnAdvanceFilter,
|
||||
|
||||
// data
|
||||
rows,
|
||||
|
||||
// mode helpers
|
||||
// mode
|
||||
serverMode,
|
||||
};
|
||||
}
|
||||
@ -247,7 +286,6 @@ export function useGridCore({
|
||||
|
||||
|
||||
|
||||
|
||||
export function useDropdownPosition(btnRef, menuRef, isOpen, level = 0) {
|
||||
const [style, setStyle] = useState({});
|
||||
|
||||
|
||||
@ -40,4 +40,3 @@ function autoPositionInsideContainer(triggerEl, dropdownEl, containerEl) {
|
||||
dropdownEl.classList.remove("open-left");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user