From 5a0327c93085518f238b25e7b4cbe6046a1ad730 Mon Sep 17 00:00:00 2001 From: ewandor Date: Tue, 13 May 2025 03:37:06 +0200 Subject: [PATCH] Dynamic Filters --- gui/rpk-gui/package-lock.json | 249 ++++++++---------- gui/rpk-gui/package.json | 3 +- .../src/lib/crud/components/crud-filters.tsx | 71 ++++- .../crud/providers/jsonschema-provider.tsx | 116 +++++--- .../filter-form/components/filter-form.tsx | 184 +++++++++++++ gui/rpk-gui/src/pages/firm/base-page/List.tsx | 33 ++- gui/rpk-gui/src/providers/data-provider.tsx | 8 +- 7 files changed, 481 insertions(+), 183 deletions(-) create mode 100644 gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx diff --git a/gui/rpk-gui/package-lock.json b/gui/rpk-gui/package-lock.json index 8c2087c..2e06f76 100644 --- a/gui/rpk-gui/package-lock.json +++ b/gui/rpk-gui/package-lock.json @@ -14,7 +14,7 @@ "@mui/lab": "^6.0.0-beta.14", "@mui/material": "^6.1.7", "@mui/x-data-grid": "^7.22.2", - "@react-querybuilder/material": "^8.6.1", + "@mui/x-date-pickers": "^8.3.0", "@refinedev/cli": "^2.16.21", "@refinedev/core": "^4.47.1", "@refinedev/devtools": "^1.1.32", @@ -47,7 +47,6 @@ "react-error-boundary": "^6.0.0", "react-hook-form": "^7.30.0", "react-i18next": "^15.5.1", - "react-querybuilder": "^8.6.1", "react-router": "^7.0.2" }, "devDependencies": { @@ -1988,6 +1987,122 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.3.0.tgz", + "integrity": "sha512-N4Rw/Q6bAHBj7BpLEGE84MG05B56wWQStZch9NqA0U8vTt9TGvkj27fQaboOIikk6ERQTHM2mMoLZgwQAvlhIw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@mui/utils": "^7.0.2", + "@mui/x-internals": "8.3.0", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/utils": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@mui/types": "^7.4.2", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/x-internals": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.3.0.tgz", + "integrity": "sha512-wSVxg5aSO9xvJT7oarhsXqr03NeP355Whm7Qn6z3VvxdGwNc7K7vKpez3E+2KMxtdvywOmragwlSdTaO1K6qkg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@mui/utils": "^7.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/x-internals": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.29.0.tgz", @@ -2226,46 +2341,6 @@ "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==", "license": "MIT" }, - "node_modules/@react-querybuilder/material": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/@react-querybuilder/material/-/material-8.6.1.tgz", - "integrity": "sha512-7dXj9z4JC0O+t7UsTfuDkODoTRdKO0mM6Fp6XGIXLsBlfuG9osBTnL8PY+wK/lrI6JSqbaaBZ/aBZegVEn9clA==", - "license": "MIT", - "peerDependencies": { - "@emotion/react": "^11", - "@emotion/styled": "^11", - "@mui/icons-material": ">=5", - "@mui/material": ">=5", - "react": ">=18", - "react-querybuilder": "8.6.1" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.1.tgz", - "integrity": "sha512-GLjHS13LiBdiuxSJvfWs3+Cx5yt97mCbuVlDteTusS6VRksPhoWviO8L1e3Re1G94m6lkw/l4pjEEyyNaGf19g==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^10.0.3", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, "node_modules/@refinedev/cli": { "version": "2.16.46", "resolved": "https://registry.npmjs.org/@refinedev/cli/-/cli-2.16.46.tgz", @@ -2954,18 +3029,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, "node_modules/@tanstack/query-core": { "version": "4.36.1", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", @@ -6667,16 +6730,6 @@ "node": ">= 4" } }, - "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -8268,15 +8321,6 @@ "node": ">=8" } }, - "node_modules/numeric-quantity": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/numeric-quantity/-/numeric-quantity-2.0.1.tgz", - "integrity": "sha512-+Bt2X6YxM5bg8XIBl76NVeG2eL0Y5VQRoyz6GLYrZXW/TDh7We+tGeX4/WZWhaVGOg5ZjNBEOZt9a86slMhOJA==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9346,27 +9390,6 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "license": "MIT" }, - "node_modules/react-querybuilder": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/react-querybuilder/-/react-querybuilder-8.6.1.tgz", - "integrity": "sha512-1A+eQgTM3WRCNo5+Rvfdyq9fn+QH2pC3dCbrW+hj9Pas5sgNzeXDCJXPKg3Pu5Ujo70dBMBn5K5mrl8/YEp3tg==", - "license": "MIT", - "dependencies": { - "@reduxjs/toolkit": "^2.7.0", - "immer": "^10.1.1", - "numeric-quantity": "^2.0.1", - "react-redux": "^9.2.0" - }, - "peerDependencies": { - "json-logic-js": "^2", - "react": ">=18" - }, - "peerDependenciesMeta": { - "json-logic-js": { - "optional": true - } - } - }, "node_modules/react-reconciler": { "version": "0.29.2", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", @@ -9383,29 +9406,6 @@ "react": "^18.3.1" } }, - "node_modules/react-redux": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", - "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -9526,21 +9526,6 @@ "esprima": "~4.0.0" } }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, "node_modules/remark-gfm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz", diff --git a/gui/rpk-gui/package.json b/gui/rpk-gui/package.json index eb9ab05..2fd76b6 100644 --- a/gui/rpk-gui/package.json +++ b/gui/rpk-gui/package.json @@ -10,7 +10,7 @@ "@mui/lab": "^6.0.0-beta.14", "@mui/material": "^6.1.7", "@mui/x-data-grid": "^7.22.2", - "@react-querybuilder/material": "^8.6.1", + "@mui/x-date-pickers": "^8.3.0", "@refinedev/cli": "^2.16.21", "@refinedev/core": "^4.47.1", "@refinedev/devtools": "^1.1.32", @@ -43,7 +43,6 @@ "react-error-boundary": "^6.0.0", "react-hook-form": "^7.30.0", "react-i18next": "^15.5.1", - "react-querybuilder": "^8.6.1", "react-router": "^7.0.2" }, "devDependencies": { diff --git a/gui/rpk-gui/src/lib/crud/components/crud-filters.tsx b/gui/rpk-gui/src/lib/crud/components/crud-filters.tsx index b229c44..5301f19 100644 --- a/gui/rpk-gui/src/lib/crud/components/crud-filters.tsx +++ b/gui/rpk-gui/src/lib/crud/components/crud-filters.tsx @@ -1,22 +1,32 @@ import { useEffect, useState } from "react"; import { jsonschemaProvider } from "../providers/jsonschema-provider"; -import { CircularProgress } from "@mui/material"; -import { QueryBuilderMaterial } from "@react-querybuilder/material"; -import { QueryBuilder, type RuleGroupType } from 'react-querybuilder'; +import { Accordion, AccordionDetails, AccordionSummary, CircularProgress } from "@mui/material"; +import FilterForm from "../../filter-form/components/filter-form"; +import TextField from "@mui/material/TextField"; +import { useSearchParams } from "react-router"; +import { GridExpandMoreIcon } from "@mui/x-data-grid"; + +export type OnChangeValue = { + search: string | null + filters: {[filter: string]: {[op: string]: string}} +} type CrudFiltersProps = { resourceName: string resourcePath: string + onChange: (value: OnChangeValue) => void } const CrudFilters = (props: CrudFiltersProps) => { - const { resourceName, resourcePath } = props + const { resourceName, resourcePath, onChange } = props + const [hasSearch, setHasSearch] = useState(false) const [filtersSchema, setFiltersSchema] = useState([]) const [filtersLoading, setFiltersLoading] = useState(true); useEffect(() => { const fetchSchema = async () => { try { + setHasSearch(await jsonschemaProvider.hasSearch(resourcePath)) const resourceFilters = await jsonschemaProvider.getListFilters(resourceName, resourcePath) setFiltersSchema(resourceFilters); console.log(resourceFilters); @@ -29,20 +39,57 @@ const CrudFilters = (props: CrudFiltersProps) => { fetchSchema(); }, []); - const [query, setQuery] = useState({ combinator: 'and', rules: [] }); - if (filtersLoading) { return } + let currentValue = { + search: "", + filters: {} + } + return ( - - - + <> + {hasSearch && + { + currentValue.search = value; + onChange(currentValue); + }} + /> + } + + Advanced filters} + /> + + { + currentValue.filters = value; + onChange(currentValue); + }} + /> + + + ) } export default CrudFilters; + +type SearchFilter = { + value: string + onChange: (value: any) => void +} + +const SearchFilter = (props: SearchFilter) => { + const {value, onChange} = props; + const [searchParams, setSearchParams] = useSearchParams(); + + return ( + onChange(event.target.value)} + defaultValue={value} + /> + ) +} diff --git a/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx b/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx index 06a1bdc..2c93d37 100644 --- a/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx +++ b/gui/rpk-gui/src/lib/crud/providers/jsonschema-provider.tsx @@ -3,7 +3,6 @@ import i18n from '../../../i18n' import { JSONSchema7Definition } from "json-schema"; import { GridColDef } from "@mui/x-data-grid"; import { GridColType } from "@mui/x-data-grid/models/colDef/gridColType"; -import { Field as FilterField } from "react-querybuilder"; const API_URL = "/api/v1"; @@ -72,6 +71,10 @@ export const jsonschemaProvider = { getListFilters: async (resourceName: string, resourcePath: string): Promise => { return getFilters(resourceName, resourcePath); + }, + + hasSearch: async (resourcePath: string): Promise => { + return hasSearch(resourcePath); } } @@ -88,36 +91,30 @@ function shortenResourceName(resourceName: string) { return convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, "")); } -async function getFilters(resourceName: string, resourcePath: string): Promise { - const shortResourceName = shortenResourceName(resourceName); - const rawSchemas = await getJsonschema() - const resourceParts = `/${resourcePath}/`.split("/") - let pathSchema: PathSchema|undefined; - for (const path in rawSchemas.paths) { - if (rawSchemas.paths[path].hasOwnProperty("get")) { - const pathParts = path.split("/") - if (pathParts.length == resourceParts.length) { - for (let i = 0; i < pathParts.length; i++) { - const isVariable = pathParts[i].slice(0,1) == "{" && pathParts[i].slice(-1) == "}"; - if (! isVariable && pathParts[i] != resourceParts[i] ) { - break; - } +type FilterField = { + name: string, + label: string, + type: string, + operators: { name: string, label: string }[] +} - if (i == pathParts.length - 1) { - pathSchema = rawSchemas.paths[path].get - } - } - } - if (pathSchema !== undefined) { - break - } +async function hasSearch(resourcePath: string): Promise { + const jst = new JsonSchemaTraverser(await getJsonschema()); + const pathSchema = jst.getPath(resourcePath); + for (const param of pathSchema.parameters) { + if (param.name == "search") { + return true } } - if (pathSchema === undefined) { - throw ("Path not found in schema"); - } - const jst = new JsonSchemaTraverser(rawSchemas) + return false +} + +async function getFilters(resourceName: string, resourcePath: string): Promise { + const shortResourceName = shortenResourceName(resourceName); + const jst = new JsonSchemaTraverser(await getJsonschema()); + const pathSchema = jst.getPath(resourcePath); + const seen: { [k: string]: number } = {}; let filters: FilterField[] = [] for (const param of pathSchema.parameters) { @@ -126,10 +123,10 @@ async function getFilters(resourceName: string, resourcePath: string): Promise { + if (fieldName == "created_by" || fieldName == "updated_by") { + return "author"; + } else if (Array.isArray(field)) { + let enumValues = []; + for (const f of field) { + enumValues.push(f.const) + } + return `enum(${enumValues.join("|")})` + } else if (field.hasOwnProperty('type')) { + if (field.type == "string" && field.format == "date-time") { + return "dateTime"; + } + return field.type as string; + } else if (field.hasOwnProperty('anyOf') && field.anyOf) { + for (const prop of field.anyOf) { + if (typeof prop != "boolean" && prop.type != "null") { + return prop.type as string; + } + + } + return "null"; + } + throw "Unimplemented field type" +} + const getColumns = async (resourceName: string): Promise => { return buildColumns(await getJsonschema(), resourceName) } @@ -383,6 +406,35 @@ const JsonSchemaTraverser = class { return this.rawSchemas.components.schemas[resourceName] } + public getPath = (resourcePath: string) => { + const resourceParts = `/${resourcePath}/`.split("/"); + let pathSchema: PathSchema|undefined; + for (const path in this.rawSchemas.paths) { + if (this.rawSchemas.paths[path].hasOwnProperty("get")) { + const pathParts = path.split("/") + if (pathParts.length == resourceParts.length) { + for (let i = 0; i < pathParts.length; i++) { + const isVariable = pathParts[i].slice(0,1) == "{" && pathParts[i].slice(-1) == "}"; + if (! isVariable && pathParts[i] != resourceParts[i] ) { + break; + } + + if (i == pathParts.length - 1) { + pathSchema = this.rawSchemas.paths[path].get + } + } + } + if (pathSchema !== undefined) { + break + } + } + } + if (pathSchema === undefined) { + throw ("Path not found in schema"); + } + return pathSchema + } + public hasDescendant = (resource: RJSFSchema, property_name: string): boolean => { if (is_array(resource)) { return property_name == 'items'; @@ -406,7 +458,7 @@ const JsonSchemaTraverser = class { throw new Error("Jsonschema format not implemented in property finder"); } - public getDescendant = (resource: RJSFSchema, property_name: string): RJSFSchema => { + public getDescendant = (resource: RJSFSchema, property_name: string): RJSFSchema | RJSFSchema[] => { if (is_array(resource) && property_name == 'items') { return resource.items as RJSFSchema; } else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) { @@ -416,11 +468,15 @@ const JsonSchemaTraverser = class { let subresource = this.getResource(subresourceName); return this.getDescendant(subresource, property_name); } else if (is_union(resource)) { + let descendants: RJSFSchema[] = []; for (const ref of resource.oneOf!) { if (this.hasDescendant(ref as RJSFSchema, property_name)) { - return this.getDescendant(ref as RJSFSchema, property_name); + descendants.push(this.getDescendant(ref as RJSFSchema, property_name)); } } + if (descendants.length > 0) { + return descendants; + } } else if (is_enum(resource)) { for (const ref of resource.allOf!) { if (this.hasDescendant(ref as RJSFSchema, property_name)) { diff --git a/gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx b/gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx new file mode 100644 index 0000000..877f7b3 --- /dev/null +++ b/gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx @@ -0,0 +1,184 @@ +import TextField from "@mui/material/TextField"; +import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; +import Autocomplete from "@mui/material/Autocomplete"; +import { FirmContext } from "../../../contexts/FirmContext"; +import { useContext } from "react"; +import { Box, Grid2, styled } from "@mui/material"; +import Stack from "@mui/material/Stack"; +import dayjs from "dayjs"; + +export type FilterField = { + name: string + label: string + type: string +} + +type FilterFormProps = { + values: {[field_name: string]: { [operator: string]: string }} + fields: FilterField[] + onChange: (value: any) => void +} + +const FilterForm = (props: FilterFormProps) => { + const { fields, values, onChange } = props; + + let currentValue = values + + const formField = fields.filter(f => f.name != "search").map((f, index) => + <> + { f.name == "created_at" && } + + { + for (const op in value) { + if (value[op] == null || value[op] == "") { + if (currentValue.hasOwnProperty(f.name)) { + if (currentValue[f.name].hasOwnProperty(op)) { + delete currentValue[f.name][op]; + } + if (Object.entries(currentValue[f.name]).length == 0) { + delete currentValue[f.name]; + } + } + } else { + if (! currentValue.hasOwnProperty(f.name)) { + currentValue[f.name] = {}; + } + currentValue[f.name][op] = value[op]; + } + } + onChange(currentValue); + }} + /> + + + ); + + return ( +
+ + {formField} + +
+ ); +} + +type OperatorValue = { [operator: string]: string } + +type FilterFormFieldProps = { + field: FilterField + value: OperatorValue + onChange: (value: { [operator: string]: string | null }) => void +} + +const FilterFormField = (props: FilterFormFieldProps) => { + const { field, value, onChange } = props; + + if (field.type == "string") { + const defaultValue = value.hasOwnProperty('ilike') ? value['ilike'] : undefined + return ( + onChange({"ilike": event.target.value})} + /> + ) + } else if (field.type == "dateTime") { + return ( + + ) + } else if (field.type == "author") { + return ( + + ); + } else if (field.type.slice(0, 4) == "enum") { + const values = field.type.slice(5,-1).split("|"); + const defaultValue = value.hasOwnProperty("in") ? [{ + value: value["in"], + label: value["in"] + }] : undefined; + return ( + { + return { + value: opt, + label: opt + } + })} + onChange={(event, value) => onChange({ "in": value.map(v => v.value).join(",") })} + getOptionLabel={(option) => option.label} + defaultValue={defaultValue} + renderInput={(params) => ( + + )} + /> + ) + } + throw("Unsupported field filter type"); +} + + +type FilterFieldAuthorProp = FilterFormFieldProps + +const FilterFieldAuthor = (props: FilterFieldAuthorProp) => { + const { field, onChange } = props; + const { partnerMap } = useContext(FirmContext) + + if (partnerMap == undefined) { + throw "Can't use author filter outside of the context of a firm"; + } + + let options = [] + for(let key of Array.from(partnerMap.keys()) ) { + options.push({ + id: key, + label: partnerMap.get(key) + }); + } + + return ( + } + options={options} + onChange={(event, value) => onChange({ "in": value.length == 0 ? null : value.map(v => v.id).join(",") })} + /> + ) +} + +const FilterFieldDateRange = (props: FilterFormFieldProps) => { + const { field, value, onChange } = props; + + const defaultAfterValue = value.hasOwnProperty('gte') ? dayjs(value['gte']) : undefined; + const defaultBeforeValue = value.hasOwnProperty('lte') ? dayjs(value['lte']) : undefined; + + return ( + + onChange({'gte': value === null ? null : value.toJSON()})} + /> + onChange({'lte': value === null ? null : value.toJSON()})} + /> + + ); +} + +export default FilterForm; + diff --git a/gui/rpk-gui/src/pages/firm/base-page/List.tsx b/gui/rpk-gui/src/pages/firm/base-page/List.tsx index 2eb69a3..267e9d3 100644 --- a/gui/rpk-gui/src/pages/firm/base-page/List.tsx +++ b/gui/rpk-gui/src/pages/firm/base-page/List.tsx @@ -1,13 +1,14 @@ import React, { useContext } from "react"; import { Link, useNavigate } from "react-router" import { UiSchema } from "@rjsf/utils"; -import { useTranslation } from "@refinedev/core"; +import { CrudFilter, useTranslation } from "@refinedev/core"; import { List as RefineList, useDataGrid } from "@refinedev/mui"; import { Button } from "@mui/material"; import { GridColDef, GridValidRowModel } from "@mui/x-data-grid"; import { FirmContext } from "../../../contexts/FirmContext"; import CrudList from "../../../lib/crud/components/crud-list"; -import CrudFilters from "../../../lib/crud/components/crud-filters"; +import CrudFilters, { OnChangeValue } from "../../../lib/crud/components/crud-filters"; +import { CrudOperators } from "@refinedev/core/src/contexts/data/types"; type ListProps = { resource: string, @@ -28,13 +29,34 @@ const List = (props: ListProps) => { const { currentFirm } = useContext(FirmContext); const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}` - const { dataGridProps, tableQueryResult } = useDataGrid({ + const { dataGridProps, tableQuery, setFilters } = useDataGrid({ resource: `${resourceBasePath}/${resource}`, }); const navigate = useNavigate(); - if (tableQueryResult.error?.status == 404) { - throw tableQueryResult.error + if (tableQuery.error?.status == 404) { + throw tableQuery.error + } + + const onFilterChange = (value: OnChangeValue) => { + let newFilters: CrudFilter[] = [] + if (value.search != null) { + newFilters.push({ + field: "search", + operator: "eq", + value: value.search + }) + } + for (const filterName in value.filters) { + for (const operator in value.filters[filterName]) { + newFilters.push({ + field: filterName, + operator: operator as Exclude, + value: value.filters[filterName][operator] + }) + } + } + setFilters(newFilters); } return ( @@ -45,6 +67,7 @@ const List = (props: ListProps) => { 0) { filters.forEach((filter) => { - if ("field" in filter && filter.value && filter.operator === "contains") { - params.append(filter.field + "__ilike", filter.value); + if ("field" in filter) { + if (filter.field == "search") { + params.append("search", filter.value); + } else { + params.append(`${filter.field.replace(".", "__")}__${filter.operator}`, filter.value); + } } }); }