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 = {},
|
features = {},
|
||||||
renderExpanded,
|
renderExpanded,
|
||||||
}) {
|
}) {
|
||||||
const [isFullScreen,setFullScreen] = useState(false)
|
const [isFullScreen, setFullScreen] = useState(false);
|
||||||
const grid = useGridCore({
|
const grid = useGridCore({
|
||||||
data,
|
data,
|
||||||
serverMode,
|
serverMode,
|
||||||
@ -59,8 +59,11 @@ export default function PmsGrid({
|
|||||||
expanded,
|
expanded,
|
||||||
toggleExpand,
|
toggleExpand,
|
||||||
setColState,
|
setColState,
|
||||||
onAdvanceFilters,
|
// onAdvanceFilters,
|
||||||
setAdanceFilter,
|
// setAdanceFilter,
|
||||||
|
|
||||||
|
advanceFilters,
|
||||||
|
setColumnAdvanceFilter,
|
||||||
} = grid;
|
} = grid;
|
||||||
|
|
||||||
// --- Pin / Unpin helpers ---
|
// --- Pin / Unpin helpers ---
|
||||||
@ -149,7 +152,11 @@ export default function PmsGrid({
|
|||||||
const currentRows = rows;
|
const currentRows = rows;
|
||||||
|
|
||||||
return (
|
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="row px-4">
|
||||||
<div className="col-8">
|
<div className="col-8">
|
||||||
<div className="d-flex flex-row gap-2 gap-2 ">
|
<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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
<FilterApplied groupBy={groupBy} removeGroupby={()=>setGroupBy(null)} advance={onAdvanceFilters} />
|
<FilterApplied
|
||||||
|
groupBy={groupBy}
|
||||||
|
removeGroupby={() => setGroupBy(null)}
|
||||||
|
advance={advanceFilters}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Table-Start*/}
|
{/* Table-Start*/}
|
||||||
<div
|
<div
|
||||||
@ -322,7 +337,8 @@ export default function PmsGrid({
|
|||||||
onPinLeft={() => pinColumn(col.key, "left")}
|
onPinLeft={() => pinColumn(col.key, "left")}
|
||||||
onPinRight={() => pinColumn(col.key, "right")}
|
onPinRight={() => pinColumn(col.key, "right")}
|
||||||
onUnpin={() => unpinColumn(col.key)}
|
onUnpin={() => unpinColumn(col.key)}
|
||||||
onAdvancedFilter={setAdanceFilter}
|
advanceFilters={advanceFilters}
|
||||||
|
setColumnAdvanceFilter={setColumnAdvanceFilter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{features.resizing && (
|
{features.resizing && (
|
||||||
@ -411,7 +427,8 @@ export default function PmsGrid({
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)} <small>{totalRows} rows</small>
|
)}{" "}
|
||||||
|
<small>{totalRows} rows</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -1,155 +1,42 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useDropdownPosition } from "./useGridCore";
|
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 = ({
|
const PmsHeaderOption = ({
|
||||||
column,
|
column,
|
||||||
pinned,
|
pinned,
|
||||||
onPinLeft,
|
onPinLeft,
|
||||||
onPinRight,
|
onPinRight,
|
||||||
onUnpin,
|
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 btnRef = useRef(null);
|
||||||
const menuRef = useRef(null);
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
const filterBtnRef = useRef(null);
|
const filterBtnRef = useRef(null);
|
||||||
const filterMenuRef = useRef(null);
|
const filterMenuRef = useRef(null);
|
||||||
|
|
||||||
const pinBtnRef = useRef(null);
|
const pinBtnRef = useRef(null);
|
||||||
const pinMenuRef = useRef(null);
|
const pinMenuRef = useRef(null);
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [openMenu, setOpenMenu] = useState(null);
|
const [openMenu, setOpenMenu] = useState(null);
|
||||||
|
|
||||||
// Main dropdown position
|
|
||||||
const mainStyle = useDropdownPosition(btnRef, menuRef, open, 0);
|
const mainStyle = useDropdownPosition(btnRef, menuRef, open, 0);
|
||||||
|
|
||||||
// Submenus
|
|
||||||
const filterStyle = useDropdownPosition(
|
const filterStyle = useDropdownPosition(
|
||||||
filterBtnRef,
|
filterBtnRef,
|
||||||
filterMenuRef,
|
filterMenuRef,
|
||||||
openMenu === "filter",
|
openMenu === "filter",
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
const pinStyle = useDropdownPosition(
|
const pinStyle = useDropdownPosition(
|
||||||
pinBtnRef,
|
pinBtnRef,
|
||||||
pinMenuRef,
|
pinMenuRef,
|
||||||
@ -159,51 +46,44 @@ const PmsHeaderOption = ({
|
|||||||
|
|
||||||
const toggleMenu = (name, e) => {
|
const toggleMenu = (name, e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setOpenMenu((prev) => (prev === name ? null : name));
|
setOpenMenu((p) => (p === name ? null : name));
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------
|
|
||||||
// CLOSE WHEN CLICKING OUTSIDE
|
|
||||||
// ----------------------------
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleOutside = (e) => {
|
const close = (e) => {
|
||||||
if (!rootRef.current) return;
|
if (!rootRef.current?.contains(e.target)) {
|
||||||
|
|
||||||
if (!rootRef.current.contains(e.target)) {
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setOpenMenu(null);
|
setOpenMenu(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (open) document.addEventListener("mousedown", close);
|
||||||
if (open) document.addEventListener("mousedown", handleOutside);
|
return () => document.removeEventListener("mousedown", close);
|
||||||
return () => document.removeEventListener("mousedown", handleOutside);
|
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
// {}
|
||||||
|
console.log(advanceFilters);
|
||||||
|
console.log(columnKey);
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={rootRef} style={{ position: "relative", zIndex: 9999 }}>
|
||||||
ref={rootRef}
|
|
||||||
className="dropdown "
|
|
||||||
style={{ zIndex: 9999, position: "relative" }}
|
|
||||||
>
|
|
||||||
{/* BUTTON */}
|
|
||||||
<button
|
<button
|
||||||
ref={btnRef}
|
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)}
|
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>
|
</button>
|
||||||
|
|
||||||
{/* MAIN MENU */}
|
|
||||||
{open && (
|
{open && (
|
||||||
<ul
|
<ul
|
||||||
ref={menuRef}
|
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}
|
style={mainStyle}
|
||||||
>
|
>
|
||||||
{/* ADVANCED FILTER */}
|
{/* ADVANCED FILTER */}
|
||||||
{enableAdvancedFilter && (
|
{enableAdvancedFilter && (
|
||||||
<li className="dropdown-submenu dropstart position-relative">
|
<li className="dropdown-submenu dropstart">
|
||||||
<button
|
<button
|
||||||
ref={filterBtnRef}
|
ref={filterBtnRef}
|
||||||
className="dropdown-item d-flex justify-content-between py-1 px-2"
|
className="dropdown-item d-flex justify-content-between py-1 px-2"
|
||||||
@ -218,23 +98,22 @@ const PmsHeaderOption = ({
|
|||||||
{openMenu === "filter" && (
|
{openMenu === "filter" && (
|
||||||
<ul
|
<ul
|
||||||
ref={filterMenuRef}
|
ref={filterMenuRef}
|
||||||
className="dropdown-menu shadow rounded-3 py-2 p-0 show"
|
className="dropdown-menu shadow rounded-3 p-2 show"
|
||||||
style={filterStyle}
|
style={filterStyle}
|
||||||
>
|
>
|
||||||
<li className="p-2">
|
|
||||||
<AdvanceFilter
|
<AdvanceFilter
|
||||||
type="number"
|
type={filterType}
|
||||||
onApply={(f) => onAdvancedFilter({ ...f, columnKey })}
|
value={advanceFilters[columnKey]}
|
||||||
onClear={() => onAdvancedFilter(null)}
|
onApply={(f) => setColumnAdvanceFilter(columnKey, f)}
|
||||||
|
onClear={() => setColumnAdvanceFilter(columnKey, null)}
|
||||||
/>
|
/>
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* PIN COLUMN */}
|
{/* PIN */}
|
||||||
<li className="dropdown-submenu dropstart position-relative">
|
<li className="dropdown-submenu dropstart">
|
||||||
<button
|
<button
|
||||||
ref={pinBtnRef}
|
ref={pinBtnRef}
|
||||||
className="dropdown-item d-flex justify-content-between py-1 px-2"
|
className="dropdown-item d-flex justify-content-between py-1 px-2"
|
||||||
@ -249,39 +128,18 @@ const PmsHeaderOption = ({
|
|||||||
{openMenu === "pin" && (
|
{openMenu === "pin" && (
|
||||||
<ul
|
<ul
|
||||||
ref={pinMenuRef}
|
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}
|
style={pinStyle}
|
||||||
>
|
>
|
||||||
<li>
|
<button className="dropdown-item" onClick={onUnpin}>
|
||||||
<button className="dropdown-item py-1 px-2" onClick={onUnpin}>
|
|
||||||
{!pinned && <i className="bx bx-check me-2"></i>}
|
|
||||||
No Pin
|
No Pin
|
||||||
</button>
|
</button>
|
||||||
</li>
|
<button className="dropdown-item" onClick={onPinLeft}>
|
||||||
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
className={`dropdown-item py-1 px-2 ${
|
|
||||||
pinned === "left" ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={onPinLeft}
|
|
||||||
>
|
|
||||||
{pinned === "left" && <i className="bx bx-check me-2"></i>}
|
|
||||||
Pin Left
|
Pin Left
|
||||||
</button>
|
</button>
|
||||||
</li>
|
<button className="dropdown-item" onClick={onPinRight}>
|
||||||
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
className={`dropdown-item py-1 px-2 ${
|
|
||||||
pinned === "right" ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={onPinRight}
|
|
||||||
>
|
|
||||||
{pinned === "right" && <i className="bx bx-check me-2"></i>}
|
|
||||||
Pin Right
|
Pin Right
|
||||||
</button>
|
</button>
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { useState, useMemo, useCallback, useEffect,useLayoutEffect } from "react
|
|||||||
- rowKey
|
- rowKey
|
||||||
- initialPageSize
|
- initialPageSize
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
export function useGridCore({
|
export function useGridCore({
|
||||||
data,
|
data,
|
||||||
serverMode = false,
|
serverMode = false,
|
||||||
@ -19,47 +21,57 @@ export function useGridCore({
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||||
const [groupBy, setGroupBy] = useState(null);
|
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 [sortBy, setSortBy] = useState({ key: null, dir: "asc" });
|
||||||
const [selected, setSelected] = useState(new Set());
|
const [selected, setSelected] = useState(new Set());
|
||||||
const [expanded, setExpanded] = useState(new Set());
|
const [expanded, setExpanded] = useState(new Set());
|
||||||
|
|
||||||
const [colState, setColState] = useState(() =>
|
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 [totalRows, setTotalRows] = useState(data ? data.length : 0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [serverRows, setServerRows] = useState([]);
|
const [serverRows, setServerRows] = useState([]);
|
||||||
|
|
||||||
|
/* ---------------- SEARCH (DEBOUNCE) ---------------- */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = setTimeout(() => {
|
const handler = setTimeout(() => {
|
||||||
setDebouncedSearch(search);
|
setDebouncedSearch(search);
|
||||||
setPage(1); // Important — when search changes, go back to page 1
|
setPage(1);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(handler);
|
return () => clearTimeout(handler);
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
// client-side derived rows
|
/* ---------------- CLIENT MODE ---------------- */
|
||||||
const clientFiltered = useMemo(() => {
|
const clientFiltered = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
const q = (search || "").toLowerCase();
|
|
||||||
const filtered = q
|
let filtered = data;
|
||||||
? data.filter((r) =>
|
|
||||||
|
if (search) {
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
filtered = filtered.filter((r) =>
|
||||||
Object.values(r).some((v) =>
|
Object.values(r).some((v) =>
|
||||||
String(v ?? "")
|
String(v ?? "").toLowerCase().includes(q)
|
||||||
.toLowerCase()
|
|
||||||
.includes(q)
|
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
: data;
|
}
|
||||||
// sort if needed
|
|
||||||
if (sortBy.key) {
|
if (sortBy.key) {
|
||||||
const dir = sortBy.dir === "asc" ? 1 : -1;
|
const dir = sortBy.dir === "asc" ? 1 : -1;
|
||||||
filtered.sort((a, b) => {
|
filtered = [...filtered].sort((a, b) => {
|
||||||
const A = a[sortBy.key],
|
const A = a[sortBy.key];
|
||||||
B = b[sortBy.key];
|
const B = b[sortBy.key];
|
||||||
if (A == null && B == null) return 0;
|
if (A == null && B == null) return 0;
|
||||||
if (A == null) return -1 * dir;
|
if (A == null) return -1 * dir;
|
||||||
if (B == null) return 1 * dir;
|
if (B == null) return 1 * dir;
|
||||||
@ -68,14 +80,17 @@ export function useGridCore({
|
|||||||
return String(A).localeCompare(String(B)) * dir;
|
return String(A).localeCompare(String(B)) * dir;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalRows(filtered.length);
|
setTotalRows(filtered.length);
|
||||||
|
|
||||||
const start = (page - 1) * pageSize;
|
const start = (page - 1) * pageSize;
|
||||||
return filtered.slice(start, start + pageSize);
|
return filtered.slice(start, start + pageSize);
|
||||||
}, [data, search, sortBy, page, pageSize]);
|
}, [data, search, sortBy, page, pageSize]);
|
||||||
|
|
||||||
// server-side fetch
|
/* ---------------- SERVER MODE ---------------- */
|
||||||
const fetchServer = useCallback(async () => {
|
const fetchServer = useCallback(async () => {
|
||||||
// sorting column wise
|
if (!serverMode || typeof fetcher !== "function") return;
|
||||||
|
|
||||||
const sortFilters = sortBy.key
|
const sortFilters = sortBy.key
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -85,22 +100,15 @@ export function useGridCore({
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// -----------------------------
|
// convert map → array
|
||||||
// 3. ADVANCED FILTERS (NUMERIC FILTER POPOVER)
|
const advanceFilterArray = Object.values(advanceFilters);
|
||||||
// The grid will pass "filter" already shaped like:
|
|
||||||
// filter = { totalAmount: { type: "gt", value: 200 } }
|
|
||||||
// -----------------------------
|
|
||||||
let advanceFilters = [];
|
|
||||||
if (onAdvanceFilter) {
|
|
||||||
advanceFilters.push(onAdvanceFilter);
|
|
||||||
}
|
|
||||||
const filterPayload = JSON.stringify({
|
const filterPayload = JSON.stringify({
|
||||||
sortFilters,
|
sortFilters,
|
||||||
groupByColumn: groupBy || null,
|
groupByColumn: groupBy || null,
|
||||||
advanceFilters,
|
advanceFilters: advanceFilterArray,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!serverMode || typeof fetcher !== "function") return;
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const resp = await fetcher({
|
const resp = await fetcher({
|
||||||
@ -110,9 +118,9 @@ export function useGridCore({
|
|||||||
search: debouncedSearch,
|
search: debouncedSearch,
|
||||||
filter: filterPayload,
|
filter: filterPayload,
|
||||||
});
|
});
|
||||||
// expected: { rows: [], total }
|
|
||||||
setServerRows(resp.rows || []);
|
setServerRows(resp?.rows || []);
|
||||||
setTotalRows(resp.total || (resp.rows ? resp.rows.length : 0));
|
setTotalRows(resp?.total ?? resp?.rows?.length ?? 0);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -124,14 +132,35 @@ export function useGridCore({
|
|||||||
sortBy,
|
sortBy,
|
||||||
debouncedSearch,
|
debouncedSearch,
|
||||||
groupBy,
|
groupBy,
|
||||||
onAdvanceFilter,
|
advanceFilters,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serverMode) fetchServer();
|
if (serverMode) fetchServer();
|
||||||
}, [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) => {
|
const toggleSelect = useCallback((id) => {
|
||||||
setSelected((prev) => {
|
setSelected((prev) => {
|
||||||
const s = new Set(prev);
|
const s = new Set(prev);
|
||||||
@ -162,6 +191,7 @@ export function useGridCore({
|
|||||||
[rowKey]
|
[rowKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* ---------------- EXPAND ---------------- */
|
||||||
const toggleExpand = useCallback((id) => {
|
const toggleExpand = useCallback((id) => {
|
||||||
setExpanded((prev) => {
|
setExpanded((prev) => {
|
||||||
const s = new Set(prev);
|
const s = new Set(prev);
|
||||||
@ -170,72 +200,81 @@ export function useGridCore({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/* ---------------- SORT ---------------- */
|
||||||
const changeSort = useCallback((key) => {
|
const changeSort = useCallback((key) => {
|
||||||
setSortBy((prev) => {
|
setSortBy((prev) => {
|
||||||
// first click = asc
|
if (prev.key !== key) return { key, dir: "asc" };
|
||||||
if (prev.key !== key) {
|
if (prev.dir === "asc") return { key, dir: "desc" };
|
||||||
return { key, dir: "asc" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// second click = desc
|
|
||||||
if (prev.dir === "asc") {
|
|
||||||
return { key, dir: "desc" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// third click = remove sort
|
|
||||||
return { key: null, dir: "asc" };
|
return { key: null, dir: "asc" };
|
||||||
});
|
});
|
||||||
|
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/* ---------------- COLUMNS ---------------- */
|
||||||
const visibleColumns = useMemo(
|
const visibleColumns = useMemo(
|
||||||
() => colState.filter((c) => c.visible).sort((a, b) => a.order - b.order),
|
() => colState.filter((c) => c.visible).sort((a, b) => a.order - b.order),
|
||||||
[colState]
|
[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) => {
|
const updateColumn = useCallback((key, patch) => {
|
||||||
setColState((prev) =>
|
setColState((prev) =>
|
||||||
prev.map((c) => (c.key === key ? { ...c, ...patch } : c))
|
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 {
|
return {
|
||||||
// state
|
// paging
|
||||||
page,
|
page,
|
||||||
setPage,
|
setPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
totalRows,
|
totalRows,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
|
||||||
|
// loading
|
||||||
loading,
|
loading,
|
||||||
|
|
||||||
|
// search
|
||||||
search,
|
search,
|
||||||
setSearch,
|
setSearch,
|
||||||
|
|
||||||
|
// sorting
|
||||||
sortBy,
|
sortBy,
|
||||||
changeSort,
|
changeSort,
|
||||||
|
|
||||||
|
// selection
|
||||||
selected,
|
selected,
|
||||||
|
setSelected,
|
||||||
toggleSelect,
|
toggleSelect,
|
||||||
selectAllOnPage,
|
selectAllOnPage,
|
||||||
deselectAllOnPage,
|
deselectAllOnPage,
|
||||||
setSelected,
|
|
||||||
|
// expand
|
||||||
expanded,
|
expanded,
|
||||||
toggleExpand,
|
toggleExpand,
|
||||||
|
|
||||||
|
// columns
|
||||||
colState,
|
colState,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
updateColumn,
|
updateColumn,
|
||||||
setColState,
|
setColState,
|
||||||
|
|
||||||
|
// grouping
|
||||||
groupBy,
|
groupBy,
|
||||||
setGroupBy,
|
setGroupBy,
|
||||||
onAdvanceFilter,
|
|
||||||
setAdanceFilter,
|
// advanced filter
|
||||||
|
advanceFilters,
|
||||||
|
setColumnAdvanceFilter,
|
||||||
|
|
||||||
// data
|
// data
|
||||||
rows,
|
rows,
|
||||||
|
|
||||||
// mode helpers
|
// mode
|
||||||
serverMode,
|
serverMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -247,7 +286,6 @@ export function useGridCore({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function useDropdownPosition(btnRef, menuRef, isOpen, level = 0) {
|
export function useDropdownPosition(btnRef, menuRef, isOpen, level = 0) {
|
||||||
const [style, setStyle] = useState({});
|
const [style, setStyle] = useState({});
|
||||||
|
|
||||||
|
|||||||
@ -40,4 +40,3 @@ function autoPositionInsideContainer(triggerEl, dropdownEl, containerEl) {
|
|||||||
dropdownEl.classList.remove("open-left");
|
dropdownEl.classList.remove("open-left");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user