mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
Checkpoint - new custom filter panel
This commit is contained in:
16
src/App.tsx
16
src/App.tsx
@ -46,6 +46,8 @@ import ReportRun from "qqq/pages/processes/ReportRun";
|
|||||||
import EntityCreate from "qqq/pages/records/create/RecordCreate";
|
import EntityCreate from "qqq/pages/records/create/RecordCreate";
|
||||||
import TableDeveloperView from "qqq/pages/records/developer/TableDeveloperView";
|
import TableDeveloperView from "qqq/pages/records/developer/TableDeveloperView";
|
||||||
import EntityEdit from "qqq/pages/records/edit/RecordEdit";
|
import EntityEdit from "qqq/pages/records/edit/RecordEdit";
|
||||||
|
import FilterPoc from "qqq/pages/records/FilterPoc";
|
||||||
|
import IntersectionMatrix from "qqq/pages/records/IntersectionMatrix";
|
||||||
import RecordQuery from "qqq/pages/records/query/RecordQuery";
|
import RecordQuery from "qqq/pages/records/query/RecordQuery";
|
||||||
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
|
import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView";
|
||||||
import RecordView from "qqq/pages/records/view/RecordView";
|
import RecordView from "qqq/pages/records/view/RecordView";
|
||||||
@ -456,6 +458,20 @@ export default function App()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appRoutesList.push({
|
||||||
|
name: "Intersection Matrix",
|
||||||
|
key: "intersection-matrix",
|
||||||
|
route: "/intersection-matrix",
|
||||||
|
component: <IntersectionMatrix />,
|
||||||
|
});
|
||||||
|
|
||||||
|
appRoutesList.push({
|
||||||
|
name: "Filer POC",
|
||||||
|
key: "filter-poc",
|
||||||
|
route: "/filter-poc",
|
||||||
|
component: <FilterPoc />,
|
||||||
|
});
|
||||||
|
|
||||||
const newSideNavRoutes = [];
|
const newSideNavRoutes = [];
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
newSideNavRoutes.unshift(profileRoutes);
|
newSideNavRoutes.unshift(profileRoutes);
|
||||||
|
167
src/qqq/components/query/CustomFilterPanel.tsx
Normal file
167
src/qqq/components/query/CustomFilterPanel.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||||
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button/Button";
|
||||||
|
import Icon from "@mui/material/Icon/Icon";
|
||||||
|
import {GridFilterPanelProps, GridSlotsComponentsProps} from "@mui/x-data-grid-pro";
|
||||||
|
import React, {forwardRef, useReducer} from "react";
|
||||||
|
import {FilterCriteriaRow} from "qqq/components/query/FilterCriteriaRow";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
declare module "@mui/x-data-grid"
|
||||||
|
{
|
||||||
|
interface FilterPanelPropsOverrides
|
||||||
|
{
|
||||||
|
tableMetaData: QTableMetaData;
|
||||||
|
queryFilter: QQueryFilter;
|
||||||
|
updateFilter: (newFilter: QQueryFilter) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class QFilterCriteriaWithId extends QFilterCriteria
|
||||||
|
{
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let debounceTimeout: string | number | NodeJS.Timeout;
|
||||||
|
let criteriaId = (new Date().getTime()) + 1000;
|
||||||
|
|
||||||
|
export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
|
||||||
|
function MyCustomFilterPanel(props: GridSlotsComponentsProps["filterPanel"], ref)
|
||||||
|
{
|
||||||
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
const queryFilter = props.queryFilter;
|
||||||
|
// console.log(`CustomFilterPanel: filter: ${JSON.stringify(queryFilter)}`);
|
||||||
|
|
||||||
|
function focusLastField()
|
||||||
|
{
|
||||||
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
console.log(`Try to focus ${criteriaId - 1}`);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
document.getElementById(`field-${criteriaId - 1}`).focus();
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log("Error trying to focus field ...", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCriteria = () =>
|
||||||
|
{
|
||||||
|
const qFilterCriteriaWithId = new QFilterCriteriaWithId(null, QCriteriaOperator.EQUALS, [""]);
|
||||||
|
qFilterCriteriaWithId.id = criteriaId++;
|
||||||
|
console.log(`adding criteria id ${qFilterCriteriaWithId.id}`);
|
||||||
|
queryFilter.criteria.push(qFilterCriteriaWithId);
|
||||||
|
props.updateFilter(queryFilter);
|
||||||
|
forceUpdate();
|
||||||
|
|
||||||
|
focusLastField();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!queryFilter.criteria)
|
||||||
|
{
|
||||||
|
queryFilter.criteria = [];
|
||||||
|
addCriteria();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryFilter.criteria.length == 0)
|
||||||
|
{
|
||||||
|
addCriteria();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(queryFilter.criteria.length == 1 && !queryFilter.criteria[0].fieldName)
|
||||||
|
{
|
||||||
|
focusLastField();
|
||||||
|
}
|
||||||
|
|
||||||
|
let booleanOperator: "AND" | "OR" | null = null;
|
||||||
|
if (queryFilter.criteria.length > 1)
|
||||||
|
{
|
||||||
|
booleanOperator = queryFilter.booleanOperator;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// needDebounce param - things like typing in a text field DO need debounce, but changing an operator doesn't //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const updateCriteria = (newCriteria: QFilterCriteria, index: number, needDebounce = false) =>
|
||||||
|
{
|
||||||
|
queryFilter.criteria[index] = newCriteria;
|
||||||
|
|
||||||
|
clearTimeout(debounceTimeout)
|
||||||
|
debounceTimeout = setTimeout(() => props.updateFilter(queryFilter), needDebounce ? 500 : 1);
|
||||||
|
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBooleanOperator = (newValue: string) =>
|
||||||
|
{
|
||||||
|
queryFilter.booleanOperator = newValue;
|
||||||
|
props.updateFilter(queryFilter);
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCriteria = (index: number) =>
|
||||||
|
{
|
||||||
|
queryFilter.criteria.splice(index, 1);
|
||||||
|
props.updateFilter(queryFilter);
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="customFilterPanel">
|
||||||
|
{
|
||||||
|
queryFilter.criteria.map((criteria: QFilterCriteriaWithId, index: number) =>
|
||||||
|
(
|
||||||
|
<Box key={criteria.id}>
|
||||||
|
<FilterCriteriaRow
|
||||||
|
id={criteria.id}
|
||||||
|
index={index}
|
||||||
|
tableMetaData={props.tableMetaData}
|
||||||
|
criteria={criteria}
|
||||||
|
booleanOperator={booleanOperator}
|
||||||
|
updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)}
|
||||||
|
removeCriteria={() => removeCriteria(index)}
|
||||||
|
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
|
||||||
|
/>
|
||||||
|
{/*JSON.stringify(criteria)*/}
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<Box p={1}>
|
||||||
|
<Button onClick={() => addCriteria()} startIcon={<Icon>add</Icon>} size="medium" sx={{px: 0.75}}>Add Condition</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
507
src/qqq/components/query/FilterCriteriaRow.tsx
Normal file
507
src/qqq/components/query/FilterCriteriaRow.tsx
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||||
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
|
import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import FormControl from "@mui/material/FormControl/FormControl";
|
||||||
|
import Icon from "@mui/material/Icon/Icon";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Select, {SelectChangeEvent} from "@mui/material/Select/Select";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import React, {ReactNode, SyntheticEvent, useState} from "react";
|
||||||
|
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
|
||||||
|
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||||
|
|
||||||
|
|
||||||
|
export enum ValueMode
|
||||||
|
{
|
||||||
|
NONE = "NONE",
|
||||||
|
SINGLE = "SINGLE",
|
||||||
|
DOUBLE = "DOUBLE",
|
||||||
|
MULTI = "MULTI",
|
||||||
|
SINGLE_DATE = "SINGLE_DATE",
|
||||||
|
SINGLE_DATE_TIME = "SINGLE_DATE_TIME",
|
||||||
|
PVS_SINGLE = "PVS_SINGLE",
|
||||||
|
PVS_MULTI = "PVS_MULTI",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperatorOption
|
||||||
|
{
|
||||||
|
label: string;
|
||||||
|
value: QCriteriaOperator;
|
||||||
|
implicitValues?: [any];
|
||||||
|
valueMode: ValueMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface FilterCriteriaRowProps
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
index: number;
|
||||||
|
tableMetaData: QTableMetaData;
|
||||||
|
criteria: QFilterCriteria;
|
||||||
|
booleanOperator: "AND" | "OR" | null;
|
||||||
|
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void;
|
||||||
|
removeCriteria: () => void;
|
||||||
|
updateBooleanOperator: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterCriteriaRow.defaultProps = {};
|
||||||
|
|
||||||
|
function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: any[], isJoinTable: boolean)
|
||||||
|
{
|
||||||
|
const sortedFields = [...tableMetaData.fields.values()].sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
for (let i = 0; i < sortedFields.length; i++)
|
||||||
|
{
|
||||||
|
const fieldName = isJoinTable ? `${tableMetaData.name}.${sortedFields[i].name}` : sortedFields[i].name;
|
||||||
|
fieldOptions.push({field: sortedFields[i], table: tableMetaData, fieldName: fieldName});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element
|
||||||
|
{
|
||||||
|
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
|
||||||
|
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
|
||||||
|
const [operatorInputValue, setOperatorInputValue] = useState("")
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// set up the array of options for the fields Autocomplete //
|
||||||
|
// also, a groupBy function, in case there are exposed joins //
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
const fieldOptions: any[] = [];
|
||||||
|
makeFieldOptionsForTable(tableMetaData, fieldOptions, false);
|
||||||
|
let fieldsGroupBy = null;
|
||||||
|
|
||||||
|
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0)
|
||||||
|
{
|
||||||
|
fieldsGroupBy = (option: any) => `${option.table.label} Fields`;
|
||||||
|
|
||||||
|
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||||
|
{
|
||||||
|
const exposedJoin = tableMetaData.exposedJoins[i];
|
||||||
|
makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
// set up array of options for operator dropdown //
|
||||||
|
// only call the function to do it if we have a field set //
|
||||||
|
////////////////////////////////////////////////////////////
|
||||||
|
let operatorOptions: OperatorOption[] = [];
|
||||||
|
|
||||||
|
function setOperatorOptions(fieldName: string)
|
||||||
|
{
|
||||||
|
const [field, fieldTable] = FilterUtils.getField(tableMetaData, fieldName);
|
||||||
|
operatorOptions = [];
|
||||||
|
if (field && fieldTable)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// setup array of options for operator Autocomplete //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
if (field.possibleValueSourceName)
|
||||||
|
{
|
||||||
|
operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.PVS_SINGLE});
|
||||||
|
operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.PVS_SINGLE});
|
||||||
|
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.PVS_MULTI});
|
||||||
|
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.PVS_MULTI});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (field.type)
|
||||||
|
{
|
||||||
|
case QFieldType.DECIMAL:
|
||||||
|
case QFieldType.INTEGER:
|
||||||
|
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "not equals", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "greater than or equals", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "less than", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "less than or equals", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE});
|
||||||
|
operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE});
|
||||||
|
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI});
|
||||||
|
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
|
||||||
|
break;
|
||||||
|
case QFieldType.DATE:
|
||||||
|
operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE});
|
||||||
|
operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE_DATE});
|
||||||
|
operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE});
|
||||||
|
operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE});
|
||||||
|
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
//? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN});
|
||||||
|
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
|
||||||
|
//? operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN});
|
||||||
|
//? operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN});
|
||||||
|
break;
|
||||||
|
case QFieldType.DATE_TIME:
|
||||||
|
operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
|
||||||
|
operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
|
||||||
|
operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
|
||||||
|
operatorOptions.push({label: "is on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
|
||||||
|
operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
|
||||||
|
operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
|
||||||
|
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
//? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN});
|
||||||
|
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
|
||||||
|
break;
|
||||||
|
case QFieldType.BOOLEAN:
|
||||||
|
operatorOptions.push({label: "is yes", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [true]});
|
||||||
|
operatorOptions.push({label: "is no", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [false]});
|
||||||
|
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
/*
|
||||||
|
? is yes or empty (is not no)
|
||||||
|
? is no or empty (is not yes)
|
||||||
|
*/
|
||||||
|
break;
|
||||||
|
case QFieldType.BLOB:
|
||||||
|
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "contains ", value: QCriteriaOperator.CONTAINS, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "does not contain", value: QCriteriaOperator.NOT_CONTAINS, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "starts with", value: QCriteriaOperator.STARTS_WITH, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "does not start with", value: QCriteriaOperator.NOT_STARTS_WITH, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "ends with", value: QCriteriaOperator.ENDS_WITH, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "does not end with", value: QCriteriaOperator.NOT_ENDS_WITH, valueMode: ValueMode.SINGLE});
|
||||||
|
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
|
||||||
|
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI});
|
||||||
|
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// make currently selected values appear in the Autocompletes //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
let defaultFieldValue;
|
||||||
|
let field = null;
|
||||||
|
let fieldTable = null;
|
||||||
|
if(criteria && criteria.fieldName)
|
||||||
|
{
|
||||||
|
[field, fieldTable] = FilterUtils.getField(tableMetaData, criteria.fieldName);
|
||||||
|
if (field && fieldTable)
|
||||||
|
{
|
||||||
|
if (fieldTable.name == tableMetaData.name)
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
defaultFieldValue = {field: field, table: tableMetaData, fieldName: criteria.fieldName};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
defaultFieldValue = {field: field, table: fieldTable, fieldName: criteria.fieldName};
|
||||||
|
}
|
||||||
|
|
||||||
|
setOperatorOptions(criteria.fieldName);
|
||||||
|
|
||||||
|
|
||||||
|
let newOperatorSelectedValue = operatorOptions.filter(option =>
|
||||||
|
{
|
||||||
|
if(option.value == criteria.operator)
|
||||||
|
{
|
||||||
|
if(option.implicitValues)
|
||||||
|
{
|
||||||
|
return (JSON.stringify(option.implicitValues) == JSON.stringify(criteria.values));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (false);
|
||||||
|
})[0];
|
||||||
|
if(newOperatorSelectedValue?.label !== operatorSelectedValue?.label)
|
||||||
|
{
|
||||||
|
setOperatorSelectedValue(newOperatorSelectedValue);
|
||||||
|
setOperatorInputValue(newOperatorSelectedValue?.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
// event handler for booleanOperator Select //
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
const handleBooleanOperatorChange = (event: SelectChangeEvent<"AND" | "OR">, child: ReactNode) =>
|
||||||
|
{
|
||||||
|
updateBooleanOperator(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// event handler for field Autocomplete //
|
||||||
|
//////////////////////////////////////////
|
||||||
|
const handleFieldChange = (event: any, newValue: any, reason: string) =>
|
||||||
|
{
|
||||||
|
criteria.fieldName = newValue ? newValue.fieldName : null;
|
||||||
|
updateCriteria(criteria, false);
|
||||||
|
|
||||||
|
setOperatorOptions(criteria.fieldName)
|
||||||
|
if(operatorOptions.length)
|
||||||
|
{
|
||||||
|
setOperatorSelectedValue(operatorOptions[0]);
|
||||||
|
setOperatorInputValue(operatorOptions[0].label);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setOperatorSelectedValue(null);
|
||||||
|
setOperatorInputValue("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// event handler for operator Autocomplete //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
const handleOperatorChange = (event: any, newValue: any, reason: string) =>
|
||||||
|
{
|
||||||
|
criteria.operator = newValue ? newValue.value : null;
|
||||||
|
|
||||||
|
if(newValue)
|
||||||
|
{
|
||||||
|
setOperatorSelectedValue(newValue);
|
||||||
|
setOperatorInputValue(newValue.label);
|
||||||
|
|
||||||
|
if(newValue.implicitValues)
|
||||||
|
{
|
||||||
|
criteria.values = newValue.implicitValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setOperatorSelectedValue(null);
|
||||||
|
setOperatorInputValue("");
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCriteria(criteria, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
// event handler for value text field //
|
||||||
|
////////////////////////////////////////
|
||||||
|
const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) =>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
const value = newValue ? newValue : event.target.value
|
||||||
|
|
||||||
|
if(!criteria.values)
|
||||||
|
{
|
||||||
|
criteria.values = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(valueIndex == "all")
|
||||||
|
{
|
||||||
|
criteria.values= value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
criteria.values[valueIndex] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCriteria(criteria, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isFieldOptionEqual(option: any, value: any)
|
||||||
|
{
|
||||||
|
return option.fieldName === value.fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldOptionLabel(option: any)
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// note - we're using renderFieldOption below for the actual select-box options, which //
|
||||||
|
// are always jut field label (as they are under groupings that show their table name) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(option && option.field && option.table)
|
||||||
|
{
|
||||||
|
if(option.table.name == tableMetaData.name)
|
||||||
|
{
|
||||||
|
return (option.field.label);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (option.table.label + ": " + option.field.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ("");
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for options, we only want the field label (contrast with what we show in the input box, //
|
||||||
|
// which comes out of getFieldOptionLabel, which is the table-label prefix for join fields) //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
function renderFieldOption(props: React.HTMLAttributes<HTMLLIElement>, option: any, state: AutocompleteRenderOptionState): ReactNode
|
||||||
|
{
|
||||||
|
let label = ""
|
||||||
|
if(option && option.field)
|
||||||
|
{
|
||||||
|
label = (option.field.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<li {...props}>{label}</li>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption)
|
||||||
|
{
|
||||||
|
return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
let criteriaIsValid = true;
|
||||||
|
let criteriaStatusTooltip = "This condition is fully defined and is part of your filter.";
|
||||||
|
|
||||||
|
function isNotSet(value: any)
|
||||||
|
{
|
||||||
|
return (value === null || value == undefined || String(value).trim() === "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!criteria.fieldName)
|
||||||
|
{
|
||||||
|
criteriaIsValid = false;
|
||||||
|
criteriaStatusTooltip = "You must select a field to begin to define this condition.";
|
||||||
|
}
|
||||||
|
else if(!criteria.operator)
|
||||||
|
{
|
||||||
|
criteriaIsValid = false;
|
||||||
|
criteriaStatusTooltip = "You must select an operator to continue to define this condition.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(operatorSelectedValue)
|
||||||
|
{
|
||||||
|
if (operatorSelectedValue.valueMode == ValueMode.NONE || operatorSelectedValue.implicitValues)
|
||||||
|
{
|
||||||
|
//////////////////////////////////
|
||||||
|
// don't need to look at values //
|
||||||
|
//////////////////////////////////
|
||||||
|
}
|
||||||
|
else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE)
|
||||||
|
{
|
||||||
|
if(criteria.values.length < 2)
|
||||||
|
{
|
||||||
|
criteriaIsValid = false;
|
||||||
|
criteriaStatusTooltip = "You must enter two values to complete the definition of this condition.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(operatorSelectedValue.valueMode == ValueMode.MULTI || operatorSelectedValue.valueMode == ValueMode.PVS_MULTI)
|
||||||
|
{
|
||||||
|
if(criteria.values.length < 1 || isNotSet(criteria.values[0]))
|
||||||
|
{
|
||||||
|
criteriaIsValid = false;
|
||||||
|
criteriaStatusTooltip = "You must enter one or more values complete the definition of this condition.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(isNotSet(criteria.values[0]))
|
||||||
|
{
|
||||||
|
criteriaIsValid = false;
|
||||||
|
criteriaStatusTooltip = "You must enter a value to complete the definition of this condition.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box pt={0.5} display="flex" alignItems="flex-end">
|
||||||
|
<Box display="inline-block">
|
||||||
|
<Tooltip title="Remove this condition from your filter" enterDelay={750} placement="left">
|
||||||
|
<IconButton onClick={removeCriteria}><Icon fontSize="small">close</Icon></IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box display="inline-block" width={55} className="booleanOperatorColumn">
|
||||||
|
{booleanOperator && index > 0 ?
|
||||||
|
<FormControl variant="standard" sx={{verticalAlign: "bottom"}} fullWidth>
|
||||||
|
<Select value={booleanOperator} disabled={index > 1} onChange={handleBooleanOperatorChange}>
|
||||||
|
<MenuItem value="AND">And</MenuItem>
|
||||||
|
<MenuItem value="OR">Or</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
: <span />}
|
||||||
|
</Box>
|
||||||
|
<Box display="inline-block" width={250}>
|
||||||
|
<Autocomplete
|
||||||
|
id={`field-${id}`}
|
||||||
|
renderInput={(params) => (<TextField {...params} label={"Field"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||||
|
// @ts-ignore
|
||||||
|
defaultValue={defaultFieldValue}
|
||||||
|
options={fieldOptions}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
isOptionEqualToValue={(option, value) => isFieldOptionEqual(option, value)}
|
||||||
|
groupBy={fieldsGroupBy}
|
||||||
|
getOptionLabel={(option) => getFieldOptionLabel(option)}
|
||||||
|
renderOption={(props, option, state) => renderFieldOption(props, option, state)}
|
||||||
|
autoSelect={true}
|
||||||
|
autoHighlight={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box display="inline-block" width={200}>
|
||||||
|
<Tooltip title={criteria.fieldName == null ? "You must select a field before you can select an operator" : null} enterDelay={750}>
|
||||||
|
<Autocomplete
|
||||||
|
id={"criteriaOperator"}
|
||||||
|
renderInput={(params) => (<TextField {...params} label={"Operator"} variant="standard" autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
|
||||||
|
options={operatorOptions}
|
||||||
|
value={operatorSelectedValue as any}
|
||||||
|
inputValue={operatorInputValue}
|
||||||
|
onChange={handleOperatorChange}
|
||||||
|
onInputChange={(e, value) => setOperatorInputValue(value)}
|
||||||
|
isOptionEqualToValue={(option, value) => isOperatorOptionEqual(option, value)}
|
||||||
|
getOptionLabel={(option: any) => option.label}
|
||||||
|
autoSelect={true}
|
||||||
|
autoHighlight={true}
|
||||||
|
/*disabled={criteria.fieldName == null}*/
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box display="inline-block" width={300} className="filterValuesColumn">
|
||||||
|
<FilterCriteriaRowValues
|
||||||
|
operatorOption={operatorSelectedValue}
|
||||||
|
criteria={{id: id, ...criteria}}
|
||||||
|
fieldType={field?.type}
|
||||||
|
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box display="inline-block" pl={0.5} pr={1}>
|
||||||
|
<Tooltip title={criteriaStatusTooltip} enterDelay={750} placement="right">
|
||||||
|
{
|
||||||
|
criteriaIsValid
|
||||||
|
? <Icon color="success">check</Icon>
|
||||||
|
: <Icon color="disabled">pending</Icon>
|
||||||
|
}
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
129
src/qqq/components/query/FilterCriteriaRowValues.tsx
Normal file
129
src/qqq/components/query/FilterCriteriaRowValues.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
|
import {Chip} from "@mui/material";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import React, {SyntheticEvent} from "react";
|
||||||
|
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||||
|
import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow";
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
operatorOption: OperatorOption;
|
||||||
|
criteria: QFilterCriteriaWithId;
|
||||||
|
fieldType?: QFieldType;
|
||||||
|
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterCriteriaRowValues.defaultProps = {
|
||||||
|
};
|
||||||
|
|
||||||
|
function FilterCriteriaRowValues({operatorOption, criteria, fieldType, valueChangeHandler}: Props): JSX.Element
|
||||||
|
{
|
||||||
|
if(!operatorOption)
|
||||||
|
{
|
||||||
|
return <br />
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix="value-") =>
|
||||||
|
{
|
||||||
|
let type = "search"
|
||||||
|
const inputLabelProps: any = {};
|
||||||
|
|
||||||
|
if(fieldType == QFieldType.INTEGER)
|
||||||
|
{
|
||||||
|
type = "number";
|
||||||
|
}
|
||||||
|
else if(fieldType == QFieldType.DATE)
|
||||||
|
{
|
||||||
|
type = "date";
|
||||||
|
inputLabelProps.shrink = true;
|
||||||
|
}
|
||||||
|
else if(fieldType == QFieldType.DATE_TIME)
|
||||||
|
{
|
||||||
|
type = "datetime-local";
|
||||||
|
inputLabelProps.shrink = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TextField
|
||||||
|
id={`${idPrefix}${criteria.id}`}
|
||||||
|
label={label}
|
||||||
|
variant="standard"
|
||||||
|
autoComplete="off"
|
||||||
|
type={type}
|
||||||
|
onChange={(event) => valueChangeHandler(event, valueIndex)}
|
||||||
|
value={criteria.values[valueIndex]}
|
||||||
|
InputLabelProps={inputLabelProps}
|
||||||
|
fullWidth
|
||||||
|
// todo - x to clear value?
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (operatorOption.valueMode)
|
||||||
|
{
|
||||||
|
case ValueMode.NONE:
|
||||||
|
return <br />
|
||||||
|
case ValueMode.SINGLE:
|
||||||
|
return makeTextField();
|
||||||
|
case ValueMode.SINGLE_DATE:
|
||||||
|
return makeTextField();
|
||||||
|
case ValueMode.SINGLE_DATE_TIME:
|
||||||
|
return makeTextField();
|
||||||
|
case ValueMode.DOUBLE:
|
||||||
|
return <Box>
|
||||||
|
<Box width="50%" display="inline-block">
|
||||||
|
{ makeTextField(0, "From", "from-") }
|
||||||
|
</Box>
|
||||||
|
<Box width="50%" display="inline-block">
|
||||||
|
{ makeTextField(1, "To", "to-") }
|
||||||
|
</Box>
|
||||||
|
</Box>;
|
||||||
|
case ValueMode.MULTI:
|
||||||
|
let values = criteria.values;
|
||||||
|
if(values && values.length == 1 && values[0] == "")
|
||||||
|
{
|
||||||
|
values = [];
|
||||||
|
}
|
||||||
|
return <Autocomplete
|
||||||
|
renderInput={(params) => (<TextField {...params} variant="standard" label="Values" />)}
|
||||||
|
options={[]}
|
||||||
|
multiple
|
||||||
|
freeSolo // todo - no debounce after enter?
|
||||||
|
selectOnFocus
|
||||||
|
clearOnBlur
|
||||||
|
limitTags={5}
|
||||||
|
value={values}
|
||||||
|
onChange={(event, value) => valueChangeHandler(event, "all", value)}
|
||||||
|
/>
|
||||||
|
case ValueMode.PVS_SINGLE:
|
||||||
|
break;
|
||||||
|
case ValueMode.PVS_MULTI:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<br />);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterCriteriaRowValues;
|
75
src/qqq/pages/records/FilterPoc.tsx
Normal file
75
src/qqq/pages/records/FilterPoc.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||||
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {CustomFilterPanel} from "qqq/components/query/CustomFilterPanel";
|
||||||
|
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterPoc.defaultProps = {};
|
||||||
|
|
||||||
|
function FilterPoc({}: Props): JSX.Element
|
||||||
|
{
|
||||||
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData)
|
||||||
|
const [queryFilter, setQueryFilter] = useState(new QQueryFilter())
|
||||||
|
|
||||||
|
const updateFilter = (newFilter: QQueryFilter) =>
|
||||||
|
{
|
||||||
|
setQueryFilter(JSON.parse(JSON.stringify(newFilter)));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
const table = await Client.getInstance().loadTableMetaData("order")
|
||||||
|
setTableMetaData(table);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseLayout>
|
||||||
|
{
|
||||||
|
tableMetaData &&
|
||||||
|
<Box>
|
||||||
|
<Box sx={{background: "white"}} border="1px solid gray">
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<CustomFilterPanel tableMetaData={tableMetaData} queryFilter={queryFilter} updateFilter={updateFilter} />
|
||||||
|
</Box>
|
||||||
|
<pre style={{fontSize: "12px"}}>
|
||||||
|
{JSON.stringify(queryFilter, null, 3)})
|
||||||
|
</pre>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterPoc;
|
123
src/qqq/pages/records/IntersectionMatrix.tsx
Normal file
123
src/qqq/pages/records/IntersectionMatrix.tsx
Normal file
File diff suppressed because one or more lines are too long
@ -59,6 +59,7 @@ import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "
|
|||||||
import MenuButton from "qqq/components/buttons/MenuButton";
|
import MenuButton from "qqq/components/buttons/MenuButton";
|
||||||
import SavedFilters from "qqq/components/misc/SavedFilters";
|
import SavedFilters from "qqq/components/misc/SavedFilters";
|
||||||
import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel";
|
import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel";
|
||||||
|
import {CustomFilterPanel} from "qqq/components/query/CustomFilterPanel";
|
||||||
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
|
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
|
||||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
@ -154,7 +155,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
||||||
|
const [lastFetchedQFilterJSON, setLastFetchedQFilterJSON] = useState("");
|
||||||
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
||||||
|
const [queryFilter, setQueryFilter] = useState(new QQueryFilter());
|
||||||
|
|
||||||
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
|
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
|
||||||
const [shouldSetAllNewJoinFieldsToHidden, setShouldSetAllNewJoinFieldsToHidden] = useState(!didDefaultVisibilityComeFromLocalStorage)
|
const [shouldSetAllNewJoinFieldsToHidden, setShouldSetAllNewJoinFieldsToHidden] = useState(!didDefaultVisibilityComeFromLocalStorage)
|
||||||
const [visibleJoinTables, setVisibleJoinTables] = useState(new Set<string>());
|
const [visibleJoinTables, setVisibleJoinTables] = useState(new Set<string>());
|
||||||
@ -217,7 +221,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const [queryErrors, setQueryErrors] = useState({} as any);
|
const [queryErrors, setQueryErrors] = useState({} as any);
|
||||||
const [receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp] = useState(new Date());
|
const [receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp] = useState(new Date());
|
||||||
|
|
||||||
|
|
||||||
const {setPageHeader} = useContext(QContext);
|
const {setPageHeader} = useContext(QContext);
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
@ -335,6 +338,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
updateColumnVisibilityModel();
|
updateColumnVisibilityModel();
|
||||||
setColumnsModel([]);
|
setColumnsModel([]);
|
||||||
setFilterModel({items: []});
|
setFilterModel({items: []});
|
||||||
|
setQueryFilter(new QQueryFilter());
|
||||||
setDefaultFilterLoaded(false);
|
setDefaultFilterLoaded(false);
|
||||||
setRows([]);
|
setRows([]);
|
||||||
}
|
}
|
||||||
@ -512,6 +516,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
let models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, null, searchParams, filterLocalStorageKey, sortLocalStorageKey);
|
let models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, null, searchParams, filterLocalStorageKey, sortLocalStorageKey);
|
||||||
setFilterModel(models.filter);
|
setFilterModel(models.filter);
|
||||||
setColumnSortModel(models.sort);
|
setColumnSortModel(models.sort);
|
||||||
|
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, models.filter, models.sort, rowsPerPage));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -545,6 +550,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
columnSortModel.splice(i, 1);
|
columnSortModel.splice(i, 1);
|
||||||
setColumnSortModel(columnSortModel);
|
setColumnSortModel(columnSortModel);
|
||||||
|
// todo - need to setQueryFilter?
|
||||||
resetColumnSortModel = true;
|
resetColumnSortModel = true;
|
||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
@ -561,6 +567,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
sort: "desc",
|
sort: "desc",
|
||||||
});
|
});
|
||||||
setColumnSortModel(columnSortModel);
|
setColumnSortModel(columnSortModel);
|
||||||
|
// todo - need to setQueryFilter?
|
||||||
resetColumnSortModel = true;
|
resetColumnSortModel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,6 +624,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLastFetchedQFilterJSON(JSON.stringify(qFilter));
|
||||||
qController.query(tableName, qFilter, queryJoins).then((results) =>
|
qController.query(tableName, qFilter, queryJoins).then((results) =>
|
||||||
{
|
{
|
||||||
console.log(`Received results for query ${thisQueryId}`);
|
console.log(`Received results for query ${thisQueryId}`);
|
||||||
@ -859,6 +867,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const newVisibleJoinTables = getVisibleJoinTables();
|
const newVisibleJoinTables = getVisibleJoinTables();
|
||||||
if (JSON.stringify([...newVisibleJoinTables.keys()]) != JSON.stringify([...visibleJoinTables.keys()]))
|
if (JSON.stringify([...newVisibleJoinTables.keys()]) != JSON.stringify([...visibleJoinTables.keys()]))
|
||||||
{
|
{
|
||||||
|
console.log("calling update table for visible join table change");
|
||||||
updateTable();
|
updateTable();
|
||||||
setVisibleJoinTables(newVisibleJoinTables);
|
setVisibleJoinTables(newVisibleJoinTables);
|
||||||
}
|
}
|
||||||
@ -870,9 +879,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
console.log(columnOrderChangeParams);
|
console.log(columnOrderChangeParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterChange = (filterModel: GridFilterModel) =>
|
const handleFilterChange = (filterModel: GridFilterModel, doSetQueryFilter = true) =>
|
||||||
{
|
{
|
||||||
setFilterModel(filterModel);
|
setFilterModel(filterModel);
|
||||||
|
|
||||||
|
if(doSetQueryFilter)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// someone might have already set the query filter, so, only set it if asked to //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage));
|
||||||
|
}
|
||||||
|
|
||||||
if (filterLocalStorageKey)
|
if (filterLocalStorageKey)
|
||||||
{
|
{
|
||||||
localStorage.setItem(filterLocalStorageKey, JSON.stringify(filterModel));
|
localStorage.setItem(filterLocalStorageKey, JSON.stringify(filterModel));
|
||||||
@ -884,6 +902,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
if (gridSort && gridSort.length > 0)
|
if (gridSort && gridSort.length > 0)
|
||||||
{
|
{
|
||||||
setColumnSortModel(gridSort);
|
setColumnSortModel(gridSort);
|
||||||
|
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, gridSort, rowsPerPage));
|
||||||
localStorage.setItem(sortLocalStorageKey, JSON.stringify(gridSort));
|
localStorage.setItem(sortLocalStorageKey, JSON.stringify(gridSort));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -948,8 +967,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
// construct the url for the export //
|
// construct the url for the export //
|
||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
const d = new Date();
|
const dateString = ValueUtils.formatDateTimeForFileName(new Date());
|
||||||
const dateString = `${d.getFullYear()}-${zp(d.getMonth() + 1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
|
|
||||||
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
|
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
|
||||||
const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel)))}&fields=${visibleFields.join(",")}`;
|
const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel)))}&fields=${visibleFields.join(",")}`;
|
||||||
|
|
||||||
@ -1087,6 +1105,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
newPath.pop();
|
newPath.pop();
|
||||||
navigate(newPath.join("/"));
|
navigate(newPath.join("/"));
|
||||||
|
|
||||||
|
console.log("calling update table for close modal");
|
||||||
updateTable();
|
updateTable();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1196,6 +1215,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<TablePagination
|
<TablePagination
|
||||||
component="div"
|
component="div"
|
||||||
|
sx={{minWidth: "450px"}}
|
||||||
// note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null,
|
// note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null,
|
||||||
// so pass a sentinel value of -1...
|
// so pass a sentinel value of -1...
|
||||||
count={totalRecords === null || totalRecords === undefined ? -1 : totalRecords}
|
count={totalRecords === null || totalRecords === undefined ? -1 : totalRecords}
|
||||||
@ -1257,17 +1277,39 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
return (qRecord);
|
return (qRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getFieldAndTable = (fieldName: string): [QFieldMetaData, QTableMetaData] =>
|
||||||
|
{
|
||||||
|
if(fieldName.indexOf(".") > -1)
|
||||||
|
{
|
||||||
|
const nameParts = fieldName.split(".", 2);
|
||||||
|
for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++)
|
||||||
|
{
|
||||||
|
const join = tableMetaData?.exposedJoins[i];
|
||||||
|
if(join?.joinTable.name == nameParts[0])
|
||||||
|
{
|
||||||
|
return ([join.joinTable.fields.get(nameParts[1]), join.joinTable]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ([tableMetaData.fields.get(fieldName), tableMetaData]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
const copyColumnValues = async (column: GridColDef) =>
|
const copyColumnValues = async (column: GridColDef) =>
|
||||||
{
|
{
|
||||||
let data = "";
|
let data = "";
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
if (latestQueryResults && latestQueryResults.length)
|
if (latestQueryResults && latestQueryResults.length)
|
||||||
{
|
{
|
||||||
let qFieldMetaData = tableMetaData.fields.get(column.field);
|
let [qFieldMetaData, fieldTable] = getFieldAndTable(column.field);
|
||||||
for (let i = 0; i < latestQueryResults.length; i++)
|
for (let i = 0; i < latestQueryResults.length; i++)
|
||||||
{
|
{
|
||||||
let record = latestQueryResults[i] as QRecord;
|
let record = latestQueryResults[i] as QRecord;
|
||||||
const value = ValueUtils.getUnadornedValueForDisplay(qFieldMetaData, record.values.get(qFieldMetaData.name), record.displayValues.get(qFieldMetaData.name));
|
const value = ValueUtils.getUnadornedValueForDisplay(qFieldMetaData, record.values.get(column.field), record.displayValues.get(column.field));
|
||||||
if (value !== null && value !== undefined && String(value) !== "")
|
if (value !== null && value !== undefined && String(value) !== "")
|
||||||
{
|
{
|
||||||
data += value + "\n";
|
data += value + "\n";
|
||||||
@ -1293,24 +1335,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setFilterForColumnStats(buildQFilter(tableMetaData, filterModel));
|
setFilterForColumnStats(buildQFilter(tableMetaData, filterModel));
|
||||||
setColumnStatsFieldName(column.field);
|
setColumnStatsFieldName(column.field);
|
||||||
|
|
||||||
if(column.field.indexOf(".") > -1)
|
const [field, fieldTable] = getFieldAndTable(column.field);
|
||||||
{
|
setColumnStatsField(field);
|
||||||
const nameParts = column.field.split(".", 2);
|
setColumnStatsFieldTableName(fieldTable.name);
|
||||||
for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++)
|
|
||||||
{
|
|
||||||
const join = tableMetaData?.exposedJoins[i];
|
|
||||||
if(join?.joinTable.name == nameParts[0])
|
|
||||||
{
|
|
||||||
setColumnStatsField(join.joinTable.fields.get(nameParts[1]));
|
|
||||||
setColumnStatsFieldTableName(nameParts[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
setColumnStatsField(tableMetaData.fields.get(column.field));
|
|
||||||
setColumnStatsFieldTableName(tableMetaData.name);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomColumnMenu = forwardRef<HTMLUListElement, GridColumnMenuProps>(
|
const CustomColumnMenu = forwardRef<HTMLUListElement, GridColumnMenuProps>(
|
||||||
@ -1474,6 +1501,15 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const doClearFilter = (event: React.KeyboardEvent<HTMLDivElement>, isYesButton: boolean = false) =>
|
||||||
|
{
|
||||||
|
if (isYesButton|| event.key == "Enter")
|
||||||
|
{
|
||||||
|
setShowClearFiltersWarning(false);
|
||||||
|
handleFilterChange({items: []} as GridFilterModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridToolbarContainer>
|
<GridToolbarContainer>
|
||||||
<div>
|
<div>
|
||||||
@ -1488,30 +1524,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<GridToolbarFilterButton nonce={undefined} />
|
<GridToolbarFilterButton nonce={undefined} />
|
||||||
{
|
{
|
||||||
hasValidFilters && (
|
hasValidFilters && (
|
||||||
|
<div id="clearFiltersButton" style={{display: "inline-block", position: "relative", top: "2px", left: "-0.75rem", width: "1rem"}}>
|
||||||
<div id="clearFiltersButton" style={{position: "absolute", left: "84px", top: "6px"}}>
|
<Tooltip title="Clear Filter">
|
||||||
<Tooltip title="Clear All Filters">
|
|
||||||
<Icon sx={{cursor: "pointer"}} onClick={() => setShowClearFiltersWarning(true)}>clear</Icon>
|
<Icon sx={{cursor: "pointer"}} onClick={() => setShowClearFiltersWarning(true)}>clear</Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Dialog open={showClearFiltersWarning} onClose={() => setShowClearFiltersWarning(false)} onKeyPress={(e) =>
|
<Dialog open={showClearFiltersWarning} onClose={() => setShowClearFiltersWarning(false)} onKeyPress={(e) => doClearFilter(e)}>
|
||||||
{
|
|
||||||
if (e.key == "Enter")
|
|
||||||
{
|
|
||||||
setShowClearFiltersWarning(false)
|
|
||||||
handleFilterChange({items: []} as GridFilterModel);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<DialogTitle id="alert-dialog-title">Confirm</DialogTitle>
|
<DialogTitle id="alert-dialog-title">Confirm</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>Are you sure you want to clear all filters?</DialogContentText>
|
<DialogContentText>Are you sure you want to remove all conditions from the current filter?</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<QCancelButton label="No" disabled={false} onClickHandler={() => setShowClearFiltersWarning(false)} />
|
<QCancelButton label="No" disabled={false} onClickHandler={() => setShowClearFiltersWarning(false)} />
|
||||||
<QSaveButton label="Yes" iconName="check" disabled={false} onClickHandler={() =>
|
<QSaveButton label="Yes" iconName="check" disabled={false} onClickHandler={() => doClearFilter(null, true)}/>
|
||||||
{
|
|
||||||
setShowClearFiltersWarning(false);
|
|
||||||
handleFilterChange({items: []} as GridFilterModel);
|
|
||||||
}}/>
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
@ -1676,6 +1700,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
// to avoid both this useEffect and the one below from both doing an "initial query", //
|
// to avoid both this useEffect and the one below from both doing an "initial query", //
|
||||||
// only run this one if at least 1 query has already been ran //
|
// only run this one if at least 1 query has already been ran //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// console.log("calling update table for UE 1");
|
||||||
updateTable();
|
updateTable();
|
||||||
}
|
}
|
||||||
}, [pageNumber, rowsPerPage, columnSortModel, currentSavedFilter]);
|
}, [pageNumber, rowsPerPage, columnSortModel, currentSavedFilter]);
|
||||||
@ -1687,8 +1712,31 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
setTotalRecords(null);
|
setTotalRecords(null);
|
||||||
setDistinctRecords(null);
|
setDistinctRecords(null);
|
||||||
|
// console.log("calling update table for UE 2");
|
||||||
updateTable();
|
updateTable();
|
||||||
}, [columnsModel, tableState, filterModel]);
|
}, [columnsModel, tableState]);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
const currentQFilter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage);
|
||||||
|
currentQFilter.skip = pageNumber * rowsPerPage;
|
||||||
|
const currentQFilterJSON = JSON.stringify(currentQFilter);
|
||||||
|
|
||||||
|
// console.log(`current ${currentQFilterJSON}`);
|
||||||
|
// console.log(`last... ${lastFetchedQFilterJSON}`);
|
||||||
|
if(currentQFilterJSON !== lastFetchedQFilterJSON)
|
||||||
|
{
|
||||||
|
setTotalRecords(null);
|
||||||
|
setDistinctRecords(null);
|
||||||
|
// console.log("calling update table for UE 3");
|
||||||
|
updateTable();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// console.log("NOT calling update table for UE 3!!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [filterModel]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
@ -1696,6 +1744,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
document.scrollingElement.scrollTop = 0;
|
document.scrollingElement.scrollTop = 0;
|
||||||
}, [pageNumber, rowsPerPage]);
|
}, [pageNumber, rowsPerPage]);
|
||||||
|
|
||||||
|
const updateFilter = (newFilter: QQueryFilter): void =>
|
||||||
|
{
|
||||||
|
setQueryFilter(newFilter);
|
||||||
|
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter);
|
||||||
|
handleFilterChange(gridFilterModel, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (tableMetaData && !tableMetaData.readPermission)
|
if (tableMetaData && !tableMetaData.readPermission)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
@ -1769,7 +1824,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
|
table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission &&
|
||||||
<QCreateNewButton tablePath={metaData?.getTablePathByName(tableName)} />
|
<QCreateNewButton tablePath={metaData?.getTablePathByName(tableName)} />
|
||||||
}
|
}
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
<Card>
|
<Card>
|
||||||
<Box height="100%">
|
<Box height="100%">
|
||||||
@ -1779,7 +1833,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
Pagination: CustomPagination,
|
Pagination: CustomPagination,
|
||||||
LoadingOverlay: Loading,
|
LoadingOverlay: Loading,
|
||||||
ColumnMenu: CustomColumnMenu,
|
ColumnMenu: CustomColumnMenu,
|
||||||
ColumnsPanel: CustomColumnsPanel
|
ColumnsPanel: CustomColumnsPanel,
|
||||||
|
FilterPanel: CustomFilterPanel
|
||||||
}}
|
}}
|
||||||
componentsProps={{
|
componentsProps={{
|
||||||
columnsPanel:
|
columnsPanel:
|
||||||
@ -1789,8 +1844,21 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
openGroupsChanger: setColumnChooserOpenGroups,
|
openGroupsChanger: setColumnChooserOpenGroups,
|
||||||
initialFilterText: columnChooserFilterText,
|
initialFilterText: columnChooserFilterText,
|
||||||
filterTextChanger: setColumnChooserFilterText
|
filterTextChanger: setColumnChooserFilterText
|
||||||
|
},
|
||||||
|
filterPanel:
|
||||||
|
{
|
||||||
|
tableMetaData: tableMetaData,
|
||||||
|
queryFilter: queryFilter,
|
||||||
|
updateFilter: updateFilter
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
localeText={{
|
||||||
|
toolbarFilters: "Filter", // label on the filters button. we prefer singular (1 filter has many "conditions" in it).
|
||||||
|
toolbarFiltersLabel: "", // setting these 3 to "" turns off the "Show Filters" and "Hide Filters" tooltip (which can get in the way of the actual filters panel)
|
||||||
|
toolbarFiltersTooltipShow: "",
|
||||||
|
toolbarFiltersTooltipHide: "",
|
||||||
|
toolbarFiltersTooltipActive: count => count !== 1 ? `${count} conditions` : `${count} condition`
|
||||||
|
}}
|
||||||
pinnedColumns={pinnedColumns}
|
pinnedColumns={pinnedColumns}
|
||||||
onPinnedColumnsChange={handlePinnedColumnsChange}
|
onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||||
pagination
|
pagination
|
||||||
@ -1812,7 +1880,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
density={density}
|
density={density}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
filterModel={filterModel}
|
filterModel={filterModel}
|
||||||
onFilterModelChange={handleFilterChange}
|
onFilterModelChange={(model) => handleFilterChange(model)}
|
||||||
columnVisibilityModel={columnVisibilityModel}
|
columnVisibilityModel={columnVisibilityModel}
|
||||||
onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||||
onColumnOrderChange={handleColumnOrderChange}
|
onColumnOrderChange={handleColumnOrderChange}
|
||||||
|
@ -405,4 +405,72 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
height: 15px !important;
|
height: 15px !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blobIcon
|
||||||
|
{
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* move the columns & filter panels on the query screen data grid up to not be below the column headers row */
|
||||||
|
/* todo - add a class to the query screen and qualify this like that */
|
||||||
|
.MuiDataGrid-panel
|
||||||
|
{
|
||||||
|
top: -60px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .MuiAutocomplete-paper
|
||||||
|
{
|
||||||
|
line-height: 1.375;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .MuiAutocomplete-groupLabel
|
||||||
|
{
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .MuiAutocomplete-listbox
|
||||||
|
{
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard,
|
||||||
|
.customFilterPanel .MuiSvgIcon-root
|
||||||
|
{
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .booleanOperatorColumn .MuiSvgIcon-root
|
||||||
|
{
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .booleanOperatorColumn .MuiInputBase-formControl
|
||||||
|
{
|
||||||
|
padding-bottom: calc(0.25rem + 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard
|
||||||
|
{
|
||||||
|
top: calc(50% - 0.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .filterValuesColumn .MuiChip-root
|
||||||
|
{
|
||||||
|
background: none;
|
||||||
|
color: black;
|
||||||
|
border: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .filterValuesColumn .MuiChip-root .MuiChip-deleteIcon
|
||||||
|
{
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag
|
||||||
|
{
|
||||||
|
color: #191919;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
@ -27,7 +27,7 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
|
|||||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro";
|
import {GridFilterItem, GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
|
const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
|
||||||
@ -256,7 +256,7 @@ class FilterUtils
|
|||||||
}
|
}
|
||||||
else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN || operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN)
|
else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN || operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN)
|
||||||
{
|
{
|
||||||
if (value == null && (operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN))
|
if ((value == null || value.length < 2) && (operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN))
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if we send back null, we get a 500 - bad look every time you try to set up a BETWEEN filter //
|
// if we send back null, we get a 500 - bad look every time you try to set up a BETWEEN filter //
|
||||||
@ -538,6 +538,63 @@ class FilterUtils
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** build a grid filter from a qqq filter
|
||||||
|
*******************************************************************************/
|
||||||
|
public static buildGridFilterFromQFilter(tableMetaData: QTableMetaData, queryFilter: QQueryFilter): GridFilterModel
|
||||||
|
{
|
||||||
|
const gridItems: GridFilterItem[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < queryFilter.criteria.length; i++)
|
||||||
|
{
|
||||||
|
const criteria = queryFilter.criteria[i];
|
||||||
|
const [field, fieldTable] = FilterUtils.getField(tableMetaData, criteria.fieldName);
|
||||||
|
if (field)
|
||||||
|
{
|
||||||
|
gridItems.push({columnField: criteria.fieldName, id: i, operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, criteria.values), value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, criteria.values, field)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridFilter: GridFilterModel = {items: gridItems, linkOperator: queryFilter.booleanOperator == "AND" ? GridLinkOperator.And : GridLinkOperator.Or};
|
||||||
|
return (gridFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static getField(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
|
||||||
|
{
|
||||||
|
if (fieldName == null)
|
||||||
|
{
|
||||||
|
return ([null, null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldName.indexOf(".") > -1)
|
||||||
|
{
|
||||||
|
let parts = fieldName.split(".", 2);
|
||||||
|
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||||
|
{
|
||||||
|
const joinTable = tableMetaData.exposedJoins[i].joinTable;
|
||||||
|
if (joinTable.name == parts[0])
|
||||||
|
{
|
||||||
|
return ([joinTable.fields.get(parts[1]), joinTable]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Failed to find join field: ${fieldName}`);
|
||||||
|
return ([null, null]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ([tableMetaData.fields.get(fieldName), tableMetaData]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** build a qqq filter from a grid and column sort model
|
** build a qqq filter from a grid and column sort model
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
Reference in New Issue
Block a user