refactor: now using MyQueryBuilder (temp name) in Data Explorer pg as it implements zustand state management to aid the transition from retriggering an api call on every new filter added

This commit is contained in:
2026-11-12 09:59:00 -08:00
parent 39ee717a6c
commit 34a2501e5e
5 changed files with 577 additions and 102 deletions
+14 -16
View File
@@ -1,9 +1,11 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { Box, Stack, Separator, Center, Spinner, Alert, Flex } from "@chakra-ui/react"; import { Box, Stack, Separator, Center, Spinner, Alert, Flex } from "@chakra-ui/react";
import { toaster } from "../../components/ui/toaster.jsx"; // Adjust path to your v3 toaster if needed import { toaster } from "../../components/ui/toaster.jsx"; // Adjust path to your v3 toaster if needed
import VdapApi from "../../hooks/useVdapApi.js";
// Import our newly consolidated engine and presentation components // Import our newly consolidated engine and presentation components
import QueryBuilder from "./QueryBuilder.jsx"; import QueryBuilder from "./QueryBuilder.jsx";
import MyQueryBuilder from "./MyQueryBuilder.jsx";
import DataTable from "./DataTable.jsx"; import DataTable from "./DataTable.jsx";
// import { exportToCSV } from "../utils/csvExport.js"; // import { exportToCSV } from "../utils/csvExport.js";
@@ -51,6 +53,7 @@ export default function DataExplorer() {
return params.toString(); return params.toString();
}; };
// pulls list of column names and datatypes of said columns
const getColumns = async (endpoint) => { const getColumns = async (endpoint) => {
setError(null); setError(null);
isReadyRef.current = false; // does this need to be set to true at the end? isReadyRef.current = false; // does this need to be set to true at the end?
@@ -69,6 +72,8 @@ export default function DataExplorer() {
} }
}; };
// pulls the data
const pullTable = async () => { const pullTable = async () => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
@@ -80,7 +85,6 @@ export default function DataExplorer() {
} }
try { try {
// 🚨 Relative pathing for your production API
const response = await fetch(`/api/v3/table_view?${buildUrlParams()}`); const response = await fetch(`/api/v3/table_view?${buildUrlParams()}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
@@ -95,10 +99,11 @@ export default function DataExplorer() {
// Auto-fetch when pagination, columns, or sorting changes (but skip first render) // Auto-fetch when pagination, columns, or sorting changes (but skip first render)
useEffect(() => { useEffect(() => {
if (! isReadyRef.current) { if (! isReadyRef.current) return;
isReadyRef.current = false; // why are we resetting the readyRef back to false since it already is false? // {
return; // isReadyRef.current = false; // why are we resetting the readyRef back to false since it already is false?
} // return;
// }
pullTable(); pullTable();
}, [url.page, url.columns, url.ordering]); }, [url.page, url.columns, url.ordering]);
@@ -122,15 +127,11 @@ export default function DataExplorer() {
} }
}; };
// 5. Render Shell
return ( return (
// 🚨 Stripped hardcoded #FFFBFA background.
// This Flex container will perfectly fill your new Sidebar Route canvas. // This Flex container will perfectly fill your new Sidebar Route canvas.
<Flex p={8} flex="1" direction="column" overflowY="auto" bg="transparent" h="100%"> <Flex direction="column" p={2} gap={3} bg="transparent">
<Stack direction="column" gap={6} align="stretch" w="100%">
{/* The Engine */} {/* The Engine */}
<QueryBuilder <MyQueryBuilder
url={url} url={url}
setUrl={setUrl} setUrl={setUrl}
allColumns={allColumns} allColumns={allColumns}
@@ -140,9 +141,8 @@ export default function DataExplorer() {
isReadyRef={isReadyRef} isReadyRef={isReadyRef}
/> />
{results && <Separator borderColor="widgetBadgeBg" />} <Separator borderColor="widgetBadgeBg" />
{/* Global Loading & Error States */}
{isLoading && ( {isLoading && (
<Center p={10}> <Center p={10}>
<Spinner color="blue.500" size="xl" thickness="4px" /> <Spinner color="blue.500" size="xl" thickness="4px" />
@@ -160,7 +160,7 @@ export default function DataExplorer() {
)} )}
{/* The Display (Table, Pagination, Download, Columns) */} {/* The Display (Table, Pagination, Download, Columns) */}
{results && !isLoading && !error && ( {results && (
<Box w="100%"> <Box w="100%">
{results.results.length > 0 ? ( {results.results.length > 0 ? (
<DataTable <DataTable
@@ -183,8 +183,6 @@ export default function DataExplorer() {
)} )}
</Box> </Box>
)} )}
</Stack>
</Flex> </Flex>
); );
}; };
+2 -2
View File
@@ -31,7 +31,7 @@ const ColumnSelector = ({ allColumns, url, setUrl, isLoading }) => {
</Menu.Trigger> </Menu.Trigger>
<Portal> <Portal>
<Menu.Positioner> <Menu.Positioner>
<Menu.Content bg="widgetItemBg" borderColor="widgetBadgeBg"> <Menu.Content bg="white" borderColor="widgetBadgeBg">
<Menu.ItemGroup> <Menu.ItemGroup>
<Menu.ItemGroupLabel color="widgetTextMuted">Available Columns</Menu.ItemGroupLabel> <Menu.ItemGroupLabel color="widgetTextMuted">Available Columns</Menu.ItemGroupLabel>
{Object.keys(allColumns).map((colName) => ( {Object.keys(allColumns).map((colName) => (
@@ -161,7 +161,7 @@ const DataTable = React.memo(({ data, count, url, setUrl, allColumns, isLoading,
<Table.ScrollArea borderWidth="1px" borderColor="widgetBadgeBg" rounded="md" maxH="65vh" bg="widgetItemBg"> <Table.ScrollArea borderWidth="1px" borderColor="widgetBadgeBg" rounded="md" maxH="65vh" bg="widgetItemBg">
<Table.Root stickyHeader showColumnBorder size="sm"> <Table.Root stickyHeader showColumnBorder size="sm">
<Table.Header> <Table.Header>
<Table.Row bg="widgetItemExpandedBg"> <Table.Row bg="white">
{columns.map((col) => ( {columns.map((col) => (
<Table.ColumnHeader key={col} color="text" py="2"> <Table.ColumnHeader key={col} color="text" py="2">
<Button <Button
@@ -0,0 +1,272 @@
// CHAKRA COMPONENTS
import {
Button,
Stack,
Flex,
Field,
NativeSelect,
Input,
HoverCard,
Text,
NumberInput,
Box,
useSlotRecipe,
Combobox,
Portal,
useFilter,
useListCollection
} from "@chakra-ui/react";
// ICONS
import {
Plus,
CircleMinus,
Files,
ClipboardPaste,
Trash
} from 'lucide-react';
// OTHER
import { useState } from 'react';
import useDataFilterStore from '../../store/useDataFilterStore.jsx';
// ============================================================================
// 1. MAIN QUERY BUILDER (The Engine Wrapper)
// ============================================================================
export default function MyQueryBuilder({ url, setUrl, allColumns, getColumns, pullTable, isLoading, isReadyRef }) {
// STYLING
const dataExplorerRecipe = useSlotRecipe({ key: "dataExplorer" });
const dataExplorerStyle = dataExplorerRecipe();
// THE SELECT DROPDOWN MENU
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: tables,
filter: contains,
})
// STATE MANAGEMENT
const [value, setValue] = useState([]);
const addNewGroup = useDataFilterStore(state => state.addNewGroup);
const filterGroups = useDataFilterStore(state => state.filterGroups);
console.log("filter groups", filterGroups);
return (
<Flex direction="column" gap={2} w="100%" bg="transparent" css={dataExplorerStyle.query_builder}>
{/* Top Controls: Source & Execute */}
<Flex gap={2} align="flex-end">
<Combobox.Root
collection={collection}
onInputValueChange={(e) => filter(e.inputValue)}
value={value}
onValueChange={(e) => {
setValue(e.value);
const selectedView = e.value;
setUrl(prev => ({ ...prev, source: selectedView }));
// need to get all columns as default in param building
const endpoint = "/api/v3/view_dash_event_search_columns";
getColumns(endpoint);
}}
width="320px"
>
<Combobox.Label>Select Table</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
<Button
loading={isLoading}
onClick={() => {
isReadyRef.current = true;
pullTable();
}}
>
Execute Search
</Button>
</Flex>
{/* Filter Builder Engine */}
{url.source && allColumns?.types && (
<Stack w="100%" gap="2">
<Text fontSize="sm" fontWeight="bold" color="text">Filters</Text>
<Box w="100%" maxH="400px" overflow="auto">
<Stack w="100%" gap="2" pr="4">
{filterGroups.map((group) => (
<FilterGroup key={group.id} groupID={group.id} filters={group.filters} columns={allColumns.types} />
))}
<Button onClick={() => addNewGroup()} variant="outline" borderColor="widgetBadgeBg" color="text" _hover={{ bg: "widgetBadgeBg" }}>
New Filter Group <Plus />
</Button>
</Stack>
</Box>
</Stack>
)}
</Flex>
);
}
// LIST OF TABLES USER CAN EXPLORE
const tables = [
{ label: "Events Table", value: "ViewDashEventSearch" },
];
// ============================================================================
// 2. FILTERS GROUPS (The AND logic groups)
// ============================================================================
function FilterGroup({ groupID, filters, columns }) {
const removeGroup = useDataFilterStore(state => state.removeGroup);
const addFilter = useDataFilterStore(state => state.addFilter);
console.log("in filter group. GroupID: ", groupID, "filters: ", filters);
return (
<Stack bg="widgetItemBg" borderWidth="1px" borderColor="widgetBadgeBg" borderRadius="lg" p="3" spacing="3">
<Flex justify="space-between" align="center">
<Input placeholder="Group Name (Optional)" maxW="200px" size="sm" variant="flushed" color="text" />
<Button onClick={() => removeGroup(groupID)} variant="ghost" size="sm" color="red.400">
<Trash />
</Button>
</Flex>
<Stack spacing="2">
{filters?.map((filter) => (
<FilterRow key={filter.id} groupID={groupID} filterID={filter.id} filter={filter} columns={columns} setUrl={setUrl} copiedFilter={copiedFilter} />
))}
<Button onClick={() => addFilter(groupID)} variant="outline" size="sm" color="text" borderColor="widgetBadgeBg" _hover={{ bg: "widgetBadgeBg" }}>
Add Filter <Plus />
</Button>
</Stack>
</Stack>
);
};
// ============================================================================
// 3. FILTER ROW (The individual criteria inputs)
// ============================================================================
function FilterRow({ groupID, filterID, filter, columns }) {
// STATE MANAGEMENT
const removeFilter = useDataFilterStore(state => state.removeFilter);
const updateFilterColumn = useDataFilterStore(state => state.updateFilterColumn);
const updateFilterOp = useDataFilterStore(state => state.updateFilterOps);
const updateFilterValue = useDataFilterStore(state => state.updateFilterValue);
const copyFilter = useDataFilterStore(state => state.copyFilter);
const pasteFilter = useDataFilterStore(state => state.pasteFilter);
// CONSTANTS
const types = {
AutoField: ["=", "<", ">", "<=", ">="],
IntegerField: ["=", "<", ">", "<=", ">="],
CharField: ["=", "contains"],
DateTimeField: ["range"],
};
return (
<Flex gap="2" align="center">
{/* Remove button */}
<HoverCard.Root positioning={{ placement: "top" }}>
<HoverCard.Trigger asChild>
<Button onClick={() => removeFilter(groupID, filterID)} variant="ghost" size="sm" color="red.400">
<CircleMinus />
</Button>
</HoverCard.Trigger>
<HoverCard.Positioner>
<HoverCard.Content p="2" bg="white"><Text fontSize="sm">Remove Filter</Text></HoverCard.Content>
</HoverCard.Positioner>
</HoverCard.Root>
{/* Copy Button */}
<HoverCard.Root positioning={{ placement: "top" }}>
<HoverCard.Trigger asChild>
<Button onClick={() => copyFilter(filter)} variant="ghost" size="sm" color="text">
<Files />
</Button>
</HoverCard.Trigger>
<HoverCard.Positioner>
<HoverCard.Content p="2" bg="white"><Text fontSize="sm">Copy Filter</Text></HoverCard.Content>
</HoverCard.Positioner>
</HoverCard.Root>
{/* paste button */}
<HoverCard.Root positioning={{ placement: "top" }}>
<HoverCard.Trigger asChild>
<Button
onClick={() => pasteFilter(groupID)}
variant="ghost"
size="sm"
color="text"
>
<ClipboardPaste />
</Button>
</HoverCard.Trigger>
<HoverCard.Positioner>
<HoverCard.Content p="2" bg="white"><Text fontSize="sm">Paste Filter</Text></HoverCard.Content>
</HoverCard.Positioner>
</HoverCard.Root>
{/* Column selector */}
<Field.Root w={filter?.type === "DateTimeField" ? "30%" : "30%"}>
<NativeSelect.Root>
<NativeSelect.Field
bg="widgetItemBg"
color="text"
value={filter.col ?? ""}
onChange={(e) => updateFilterColumn(groupID, filterID, e.target.value, columns[e.target.value], null, null)} // e.target.value is the new column
>
<option value="" disabled hidden>Select Column</option>
{Object.keys(columns).map((colName) => (
<option key={colName} value={colName}>{colName}</option>
))}
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
</Field.Root>
{/* Operator selector or start date for range */}
<Field.Root w={filter?.type === "DateTimeField" ? "20%" : "20%"}>
{filter?.type === "DateTimeField" ? (
<Input type="date" bg="widgetItemBg" color="text" value={filter.value?.start || ""} onChange={(e) => updateFilterValue(groupID, filterID, e.target.value, "s")} />
) : (
<NativeSelect.Root>
<NativeSelect.Field bg="widgetItemBg" color="text" value={filter.op ?? ""} onChange={(e) => updateFilterOp(groupID, filterID, e.target.value)}>
<option value="" disabled hidden>Operator</option>
{filter?.type && types[filter?.type].map((op) => <option key={op} value={op}>{op}</option>)} {/* maps over the 'filter.type' corresponding ops from the types dictionary */}
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
)}
</Field.Root>
{/* Value input or end date for range */}
<Field.Root w={filter.type === "DateTimeField" ? "20%" : "50%"}>
{filter?.type === "DateTimeField" ? (
<Input type="date" bg="widgetItemBg" color="text" value={filter.value?.end || ""} onChange={(e) => updateFilterValue(groupID, filterID, e.target.value, "e")} />
) : filter?.type === "IntegerField" || filter?.type === "AutoField" ? (
<NumberInput.Root value={filter.value ?? ""} onValueChange={(e) => updateFilterValue(groupID, filterID, e.value)}>
<NumberInput.Input placeholder="Value" bg="widgetItemBg" color="text" />
</NumberInput.Root>
) : (
<Input placeholder="Value" bg="widgetItemBg" color="text" value={filter.value ?? ""} onChange={(e) => updateFilterValue(groupID, filterID, e.target.value)} />
)}
</Field.Root>
</Flex>
);
};
+89 -32
View File
@@ -1,8 +1,24 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { Button, Stack, Flex, Field, NativeSelect, Input, HoverCard, Text, NumberInput, Box } from "@chakra-ui/react"; import {
Button,
Stack,
Flex,
Field,
NativeSelect,
Input,
HoverCard,
Text,
NumberInput,
Box,
useSlotRecipe,
Combobox,
Portal,
useFilter,
useListCollection
} from "@chakra-ui/react";
import { Plus, CircleMinus, Files, ClipboardPaste, Trash } from 'lucide-react'; import { Plus, CircleMinus, Files, ClipboardPaste, Trash } from 'lucide-react';
// import { GrAdd, GrSubtractCircle } from "react-icons/gr"; import { useState } from 'react';
// import { FaCopy, FaPaste, FaTrash } from "react-icons/fa"; import useDataFilterStore from '../../store/useDataFilterStore.jsx';
// ============================================================================ // ============================================================================
// 1. FILTER ROW (The individual criteria inputs) // 1. FILTER ROW (The individual criteria inputs)
@@ -80,7 +96,7 @@ const FilterRow = React.memo(({ filter, columns, setUrl, bundleId, copiedFilter
</Button> </Button>
</HoverCard.Trigger> </HoverCard.Trigger>
<HoverCard.Positioner> <HoverCard.Positioner>
<HoverCard.Content p="2" bg="widgetItemExpandedBg"><Text fontSize="sm">Remove Filter</Text></HoverCard.Content> <HoverCard.Content p="2" bg="white"><Text fontSize="sm">Remove Filter</Text></HoverCard.Content>
</HoverCard.Positioner> </HoverCard.Positioner>
</HoverCard.Root> </HoverCard.Root>
@@ -92,7 +108,7 @@ const FilterRow = React.memo(({ filter, columns, setUrl, bundleId, copiedFilter
</Button> </Button>
</HoverCard.Trigger> </HoverCard.Trigger>
<HoverCard.Positioner> <HoverCard.Positioner>
<HoverCard.Content p="2" bg="widgetItemExpandedBg"><Text fontSize="sm">Copy Filter</Text></HoverCard.Content> <HoverCard.Content p="2" bg="white"><Text fontSize="sm">Copy Filter</Text></HoverCard.Content>
</HoverCard.Positioner> </HoverCard.Positioner>
</HoverCard.Root> </HoverCard.Root>
@@ -124,7 +140,7 @@ const FilterRow = React.memo(({ filter, columns, setUrl, bundleId, copiedFilter
</Button> </Button>
</HoverCard.Trigger> </HoverCard.Trigger>
<HoverCard.Positioner> <HoverCard.Positioner>
<HoverCard.Content p="2" bg="widgetItemExpandedBg"><Text fontSize="sm">Paste Filter</Text></HoverCard.Content> <HoverCard.Content p="2" bg="white"><Text fontSize="sm">Paste Filter</Text></HoverCard.Content>
</HoverCard.Positioner> </HoverCard.Positioner>
</HoverCard.Root> </HoverCard.Root>
@@ -207,6 +223,8 @@ const FilterBundle = ({ bundle, columns, url, setUrl, copiedFilter }) => {
}) : (b) }) : (b)
), ),
})); }));
}; };
return ( return (
@@ -234,8 +252,25 @@ const FilterBundle = ({ bundle, columns, url, setUrl, copiedFilter }) => {
// 3. MAIN QUERY BUILDER (The Engine Wrapper) // 3. MAIN QUERY BUILDER (The Engine Wrapper)
// ============================================================================ // ============================================================================
export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pullTable, isLoading, isReadyRef }) { export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pullTable, isLoading, isReadyRef }) {
const copiedFilter = useRef(null); // STYLING
const dataExplorerRecipe = useSlotRecipe({ key: "dataExplorer" });
const dataExplorerStyle = dataExplorerRecipe();
// SELECT DROPDOWN MENU
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: tables,
filter: contains,
})
// STATE MANAGEMENT
const copiedFilter = useRef(null);
const [value, setValue] = useState([]);
const addNewGroup = useDataFilterStore(state => state.addNewGroup);
const filterGroups = useDataFilterStore(state => state.filterGroups);
// FUNCTIONS
const handleAddBundle = () => { const handleAddBundle = () => {
setUrl(prev => ({ setUrl(prev => ({
...prev, ...prev,
@@ -253,39 +288,56 @@ export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pull
}, },
], ],
})); }));
// adding zustand state management as a test here
addNewGroup();
}; };
console.log("filter groups", filterGroups);
return ( return (
<Flex direction="column" gap="4" w="100%" p="4" bg="transparent"> <Flex direction="column" gap={2} w="100%" bg="transparent" css={dataExplorerStyle.query_builder}>
{/* Top Controls: Source & Execute */} {/* Top Controls: Source & Execute */}
<Flex gap="4" align="flex-end"> <Flex gap={2} align="flex-end">
<Field.Root maxW="300px"> <Combobox.Root
<Field.Label color="text">Data Source</Field.Label> collection={collection}
<NativeSelect.Root> onInputValueChange={(e) => filter(e.inputValue)}
<NativeSelect.Field value={value}
bg="widgetItemBg" onValueChange={(e) => {
color="text" setValue(e.value);
value={url.source || ""} const selectedView = e.value;
onChange={(e) => {
const selectedView = e.target.value;
setUrl(prev => ({ ...prev, source: selectedView })); setUrl(prev => ({ ...prev, source: selectedView }));
// 🚨 FIXED: Now uses relative pathing for the production API // need to get all columns as default in param building
const endpoint = "/api/v3/view_dash_event_search_columns"; const endpoint = "/api/v3/view_dash_event_search_columns";
getColumns(endpoint); getColumns(endpoint);
}} }}
width="320px"
> >
<option value="" disabled hidden>Select View</option> <Combobox.Label>Select Table</Combobox.Label>
<option value="main">Main View</option> <Combobox.Control>
{/*<option value="test">Test View</option>*/} <Combobox.Input placeholder="Type to search" />
</NativeSelect.Field> <Combobox.IndicatorGroup>
<NativeSelect.Indicator /> <Combobox.ClearTrigger />
</NativeSelect.Root> <Combobox.Trigger />
</Field.Root> </Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
{/*</Field.Root>*/}
<Button <Button
bg="blue.600"
color="white"
_hover={{ bg: "blue.500" }}
loading={isLoading} loading={isLoading}
onClick={() => { onClick={() => {
isReadyRef.current = true; isReadyRef.current = true;
@@ -298,11 +350,11 @@ export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pull
{/* Filter Builder Engine */} {/* Filter Builder Engine */}
{url.source && allColumns?.types && ( {url.source && allColumns?.types && (
<Stack w="100%" gap="4"> <Stack w="100%" gap="2">
<Text fontSize="lg" fontWeight="bold" color="text">Filters</Text> <Text fontSize="sm" fontWeight="bold" color="text">Filters</Text>
<Box w="100%" maxH="400px" overflow="auto"> <Box w="100%" maxH="400px" overflow="auto">
<Stack w="100%" gap="4" pr="4"> <Stack w="100%" gap="2" pr="4">
{url.bundles.map((bundle) => ( {url.bundles.map((bundle) => (
<FilterBundle key={bundle.id} bundle={bundle} columns={allColumns.types} url={url} setUrl={setUrl} copiedFilter={copiedFilter} /> <FilterBundle key={bundle.id} bundle={bundle} columns={allColumns.types} url={url} setUrl={setUrl} copiedFilter={copiedFilter} />
))} ))}
@@ -316,3 +368,8 @@ export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pull
</Flex> </Flex>
); );
} }
// LIST OF TABLES USER CAN EXPLORE
const tables = [
{ label: "Events Table", value: "ViewDashEventSearch" },
]
+148
View File
@@ -0,0 +1,148 @@
import { create } from 'zustand';
const useDataFilterStore = create((set, get) => ({
filterGroups: [],
copiedFilter: {},
addNewGroup: () => {
const newGroup = {
id: crypto.randomUUID(),
filters: []
}
set((state) => ({filterGroups: [...state.filterGroups, newGroup]}));
},
removeGroup: (groupID) => set((state) => ({
filterGroups: state.filterGroups.filter((group) => group.id !== groupID),
})),
addFilter: (groupID) => set((state) => {
return {
filterGroups: state.filterGroups.map(filterGroup => {
return filterGroup.id === groupID ?
{
...filterGroup,
filters: [
...filterGroup.filters,
{
id: crypto.randomUUID(),
col: null,
type: null,
op: null,
value: null
}
]
}
: filterGroup
})
}
}),
updateFilterColumn: (groupID, filterID, col, type, op, value) => set((state) => {
return {
filterGroups: state.filterGroups.map((group) => {
return groupID === group.id ?
{
...group,
filters: group.filters.map(filter => {
return filterID === filter.id ?
{
...filter,
col: col,
type: type,
op: op,
value: value
}
: filter
})
}
: group
}),
}
}),
updateFilterOps: (groupID, filterID, op) => set((state) => {
return {
filterGroups: state.filterGroups.map((group) => {
return groupID === group.id ?
{
...group,
filters: group.filters.map(filter => {
return filterID === filter.id ?
{
...filter,
op: op,
}
: filter
})
}
: group
}),
}
}),
updateFilterValue: (groupID, filterID, value, dateType = undefined) => set((state) => {
return {
filterGroups: state.filterGroups.map((group) => {
return groupID === group.id ?
{
...group,
filters: group.filters.map(filter => {
return filterID === filter.id ?
{
...filter,
value: (dateType === "s") ? {...filter.value, start: value} : (dateType === "e") ? {...filter.value, end: value } : value
}
: filter
})
}
: group
}),
}
}),
removeFilter: (groupID, filterID) => set((state) => ({
filterGroups: state.filterGroups.map((group) => {
return group.id === groupID ?
{
...group,
filters: group.filters.filter((f) => f.id !== filterID)
}
: group
}),
})),
copyFilter: (filter) => set((state) => ({
copiedFilter: state.copiedFilter = {
col: filter.col,
type: filter.type,
op: filter.op,
value: filter.value
}
})),
pasteFilter: (groupID) => set((state) => {
state.filterGroups.map((group) => {
return groupID === group.id ?
{
...group,
filters: [
...group.filters,
{
id: crypto.randomUUID(),
col: state.copiedFilter.col,
type: state.copiedFilter.type,
op: state.copiedFilter.op,
value: state.copiedFilter.value,
}
]
}
: group
})
})
}));
export default useDataFilterStore;