mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 13:20:43 +00:00
CE-798 - primary working version of tsx for basic vs. advanced query (quick-filters in basic mode)
This commit is contained in:
@ -37,7 +37,7 @@ interface QCreateNewButtonProps
|
|||||||
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} mr={2} width={standardWidth}>
|
<Box ml={3} mr={0} width={standardWidth}>
|
||||||
<Link to={`${tablePath}/create`}>
|
<Link to={`${tablePath}/create`}>
|
||||||
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
||||||
Create New
|
Create New
|
||||||
|
@ -34,14 +34,16 @@ interface FieldAutoCompleteProps
|
|||||||
tableMetaData: QTableMetaData;
|
tableMetaData: QTableMetaData;
|
||||||
handleFieldChange: (event: any, newValue: any, reason: string) => void;
|
handleFieldChange: (event: any, newValue: any, reason: string) => void;
|
||||||
defaultValue?: {field: QFieldMetaData, table: QTableMetaData, fieldName: string};
|
defaultValue?: {field: QFieldMetaData, table: QTableMetaData, fieldName: string};
|
||||||
autoFocus?: boolean
|
autoFocus?: boolean;
|
||||||
hiddenFieldNames?: string[]
|
forceOpen?: boolean;
|
||||||
|
hiddenFieldNames?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
FieldAutoComplete.defaultProps =
|
FieldAutoComplete.defaultProps =
|
||||||
{
|
{
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
|
forceOpen: null,
|
||||||
hiddenFieldNames: []
|
hiddenFieldNames: []
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, hiddenFieldNames}: FieldAutoCompleteProps): JSX.Element
|
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, forceOpen, hiddenFieldNames}: FieldAutoCompleteProps): JSX.Element
|
||||||
{
|
{
|
||||||
const fieldOptions: any[] = [];
|
const fieldOptions: any[] = [];
|
||||||
makeFieldOptionsForTable(tableMetaData, fieldOptions, false, hiddenFieldNames);
|
makeFieldOptionsForTable(tableMetaData, fieldOptions, false, hiddenFieldNames);
|
||||||
@ -124,6 +126,15 @@ export default function FieldAutoComplete({id, metaData, tableMetaData, handleFi
|
|||||||
return option.fieldName === value.fieldName;
|
return option.fieldName === value.fieldName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// seems like, if we always add the open attribute, then if its false or null, then the autocomplete //
|
||||||
|
// doesn't open at all... so, only add the attribute at all, if forceOpen is true //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const alsoOpen: {[key: string]: any} = {}
|
||||||
|
if(forceOpen)
|
||||||
|
{
|
||||||
|
alsoOpen["open"] = forceOpen;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
@ -140,6 +151,7 @@ export default function FieldAutoComplete({id, metaData, tableMetaData, handleFi
|
|||||||
autoSelect={true}
|
autoSelect={true}
|
||||||
autoHighlight={true}
|
autoHighlight={true}
|
||||||
slotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
|
slotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
|
||||||
|
{...alsoOpen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
664
src/qqq/components/query/BasicAndAdvancedQueryControls.tsx
Normal file
664
src/qqq/components/query/BasicAndAdvancedQueryControls.tsx
Normal file
@ -0,0 +1,664 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. 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 {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
|
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 {Badge, ToggleButton, ToggleButtonGroup, Typography} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import Menu from "@mui/material/Menu";
|
||||||
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
|
import {GridFilterModel} from "@mui/x-data-grid-pro";
|
||||||
|
import {GridApiPro} from "@mui/x-data-grid-pro/models/gridApiPro";
|
||||||
|
import React, {forwardRef, useImperativeHandle, useReducer, useState} from "react";
|
||||||
|
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
|
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
|
||||||
|
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||||
|
import QuickFilter from "qqq/components/query/QuickFilter";
|
||||||
|
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||||
|
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||||
|
|
||||||
|
interface BasicAndAdvancedQueryControlsProps
|
||||||
|
{
|
||||||
|
metaData: QInstance;
|
||||||
|
tableMetaData: QTableMetaData;
|
||||||
|
queryFilter: QQueryFilter;
|
||||||
|
gridApiRef: React.MutableRefObject<GridApiPro>
|
||||||
|
|
||||||
|
setQueryFilter: (queryFilter: QQueryFilter) => void;
|
||||||
|
handleFilterChange: (filterModel: GridFilterModel, doSetQueryFilter?: boolean, isChangeFromDataGrid?: boolean) => void;
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// this prop is used as a way to recognize changes in the query filter internal structure, //
|
||||||
|
// since the queryFilter object (reference) doesn't get updated //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
queryFilterJSON: string;
|
||||||
|
|
||||||
|
mode: string;
|
||||||
|
setMode: (mode: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let debounceTimeout: string | number | NodeJS.Timeout;
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Component to provide the basic & advanced query-filter controls for the
|
||||||
|
** RecordQuery screen.
|
||||||
|
**
|
||||||
|
** Done as a forwardRef, so RecordQuery can call some functions, e.g., when user
|
||||||
|
** does things on that screen, that we need to know about in here.
|
||||||
|
*******************************************************************************/
|
||||||
|
const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryControlsProps, ref) =>
|
||||||
|
{
|
||||||
|
const {metaData, tableMetaData, queryFilter, gridApiRef, setQueryFilter, handleFilterChange, queryFilterJSON, mode, setMode} = props
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
// get the quick-filter-field-names from local storage //
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
const QUICK_FILTER_FIELD_NAMES_LOCAL_STORAGE_KEY_ROOT = "qqq.quickFilterFieldNames";
|
||||||
|
const quickFilterFieldNamesLocalStorageKey = `${QUICK_FILTER_FIELD_NAMES_LOCAL_STORAGE_KEY_ROOT}.${tableMetaData.name}`;
|
||||||
|
let defaultQuickFilterFieldNames: Set<string> = new Set<string>();
|
||||||
|
if (localStorage.getItem(quickFilterFieldNamesLocalStorageKey))
|
||||||
|
{
|
||||||
|
defaultQuickFilterFieldNames = new Set<string>(JSON.parse(localStorage.getItem(quickFilterFieldNamesLocalStorageKey)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// state variables //
|
||||||
|
/////////////////////
|
||||||
|
const [quickFilterFieldNames, setQuickFilterFieldNames] = useState(defaultQuickFilterFieldNames);
|
||||||
|
const [addQuickFilterMenu, setAddQuickFilterMenu] = useState(null)
|
||||||
|
const [addQuickFilterOpenCounter, setAddQuickFilterOpenCounter] = useState(0);
|
||||||
|
const [showClearFiltersWarning, setShowClearFiltersWarning] = useState(false);
|
||||||
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// make some functions available to our parent - so it can tell us to do things //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
useImperativeHandle(ref, () =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
ensureAllFilterCriteriaAreActiveQuickFilters(currentFilter: QQueryFilter, reason: string)
|
||||||
|
{
|
||||||
|
ensureAllFilterCriteriaAreActiveQuickFilters(tableMetaData, currentFilter, reason);
|
||||||
|
},
|
||||||
|
addField(fieldName: string)
|
||||||
|
{
|
||||||
|
addQuickFilterField({fieldName: fieldName}, "columnMenu");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** for a given field, set its default operator for quick-filter dropdowns.
|
||||||
|
*******************************************************************************/
|
||||||
|
function getDefaultOperatorForField(field: QFieldMetaData)
|
||||||
|
{
|
||||||
|
// todo - sometimes i want contains instead of equals on strings (client.name, for example...)
|
||||||
|
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
|
||||||
|
if (field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE)
|
||||||
|
{
|
||||||
|
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
||||||
|
}
|
||||||
|
else if (field?.type == QFieldType.BOOLEAN)
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for booleans, if we set a default, since none of them have values, then they are ALWAYS selected, which isn't what we want. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
defaultOperator = null;
|
||||||
|
}
|
||||||
|
return defaultOperator;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Callback passed into the QuickFilter component, to update the criteria
|
||||||
|
** after user makes changes to it or to clear it out.
|
||||||
|
*******************************************************************************/
|
||||||
|
const updateQuickCriteria = (newCriteria: QFilterCriteria, needDebounce = false, doClearCriteria = false) =>
|
||||||
|
{
|
||||||
|
let found = false;
|
||||||
|
let foundIndex = null;
|
||||||
|
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||||
|
{
|
||||||
|
if(queryFilter.criteria[i].fieldName == newCriteria.fieldName)
|
||||||
|
{
|
||||||
|
queryFilter.criteria[i] = newCriteria;
|
||||||
|
found = true;
|
||||||
|
foundIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(doClearCriteria)
|
||||||
|
{
|
||||||
|
if(found)
|
||||||
|
{
|
||||||
|
queryFilter.criteria.splice(foundIndex, 1);
|
||||||
|
setQueryFilter(queryFilter);
|
||||||
|
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter);
|
||||||
|
handleFilterChange(gridFilterModel, false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!found)
|
||||||
|
{
|
||||||
|
if(!queryFilter.criteria)
|
||||||
|
{
|
||||||
|
queryFilter.criteria = [];
|
||||||
|
}
|
||||||
|
queryFilter.criteria.push(newCriteria);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(found)
|
||||||
|
{
|
||||||
|
clearTimeout(debounceTimeout)
|
||||||
|
debounceTimeout = setTimeout(() =>
|
||||||
|
{
|
||||||
|
setQueryFilter(queryFilter);
|
||||||
|
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter);
|
||||||
|
handleFilterChange(gridFilterModel, false);
|
||||||
|
}, needDebounce ? 500 : 1);
|
||||||
|
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Get the QFilterCriteriaWithId object to pass in to the QuickFilter component
|
||||||
|
** for a given field name.
|
||||||
|
*******************************************************************************/
|
||||||
|
const getQuickCriteriaParam = (fieldName: string): QFilterCriteriaWithId | null | "tooComplex" =>
|
||||||
|
{
|
||||||
|
const matches: QFilterCriteriaWithId[] = [];
|
||||||
|
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||||
|
{
|
||||||
|
if(queryFilter.criteria[i].fieldName == fieldName)
|
||||||
|
{
|
||||||
|
matches.push(queryFilter.criteria[i] as QFilterCriteriaWithId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(matches.length == 0)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
else if(matches.length == 1)
|
||||||
|
{
|
||||||
|
return (matches[0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "tooComplex";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** set the quick-filter field names state variable and local-storage
|
||||||
|
*******************************************************************************/
|
||||||
|
const storeQuickFilterFieldNames = () =>
|
||||||
|
{
|
||||||
|
setQuickFilterFieldNames(new Set<string>([...quickFilterFieldNames.values()]));
|
||||||
|
localStorage.setItem(quickFilterFieldNamesLocalStorageKey, JSON.stringify([...quickFilterFieldNames.values()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Event handler for QuickFilter component, to remove a quick filter field from
|
||||||
|
** the screen.
|
||||||
|
*******************************************************************************/
|
||||||
|
const handleRemoveQuickFilterField = (fieldName: string): void =>
|
||||||
|
{
|
||||||
|
if(quickFilterFieldNames.has(fieldName))
|
||||||
|
{
|
||||||
|
//////////////////////////////////////
|
||||||
|
// remove this field from the query //
|
||||||
|
//////////////////////////////////////
|
||||||
|
const criteria = new QFilterCriteria(fieldName, null, []);
|
||||||
|
updateQuickCriteria(criteria, false, true);
|
||||||
|
|
||||||
|
quickFilterFieldNames.delete(fieldName);
|
||||||
|
storeQuickFilterFieldNames();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Event handler for button that opens the add-quick-filter menu
|
||||||
|
*******************************************************************************/
|
||||||
|
const openAddQuickFilterMenu = (event: any) =>
|
||||||
|
{
|
||||||
|
setAddQuickFilterMenu(event.currentTarget);
|
||||||
|
setAddQuickFilterOpenCounter(addQuickFilterOpenCounter + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Handle closing the add-quick-filter menu
|
||||||
|
*******************************************************************************/
|
||||||
|
const closeAddQuickFilterMenu = () =>
|
||||||
|
{
|
||||||
|
setAddQuickFilterMenu(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Add a quick-filter field to the screen, from either the user selecting one,
|
||||||
|
** or from a new query being activated, etc.
|
||||||
|
*******************************************************************************/
|
||||||
|
const addQuickFilterField = (newValue: any, reason: "blur" | "modeToggleClicked" | "defaultFilterLoaded" | "savedFilterSelected" | "columnMenu" | string) =>
|
||||||
|
{
|
||||||
|
console.log(`Adding quick filter field as: ${JSON.stringify(newValue)}`);
|
||||||
|
if (reason == "blur")
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
// this keeps a click out of the menu from selecting the option //
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldName = newValue ? newValue.fieldName : null;
|
||||||
|
if (fieldName)
|
||||||
|
{
|
||||||
|
if (!quickFilterFieldNames.has(fieldName))
|
||||||
|
{
|
||||||
|
/////////////////////////////////
|
||||||
|
// add the field if we need to //
|
||||||
|
/////////////////////////////////
|
||||||
|
quickFilterFieldNames.add(fieldName);
|
||||||
|
storeQuickFilterFieldNames();
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// only do this when user has added the field (e.g., not when adding it because of a selected view or filter-in-url) //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(reason != "modeToggleClicked" && reason != "defaultFilterLoaded" && reason != "savedFilterSelected")
|
||||||
|
{
|
||||||
|
setTimeout(() => document.getElementById(`quickFilter.${fieldName}`)?.click(), 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(reason == "columnMenu")
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if field was already on-screen, but user clicked an option from the columnMenu, then open the quick-filter field //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
setTimeout(() => document.getElementById(`quickFilter.${fieldName}`)?.click(), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAddQuickFilterMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for the Filter Buidler button - e.g., opens the parent's grid's
|
||||||
|
** filter panel
|
||||||
|
*******************************************************************************/
|
||||||
|
const openFilterBuilder = (e: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>) =>
|
||||||
|
{
|
||||||
|
gridApiRef.current.showFilterPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for the clear-filters modal
|
||||||
|
*******************************************************************************/
|
||||||
|
const handleClearFiltersAction = (event: React.KeyboardEvent<HTMLDivElement>, isYesButton: boolean = false) =>
|
||||||
|
{
|
||||||
|
if (isYesButton || event.key == "Enter")
|
||||||
|
{
|
||||||
|
setShowClearFiltersWarning(false);
|
||||||
|
handleFilterChange({items: []} as GridFilterModel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** format the current query as a string for showing on-screen as a preview.
|
||||||
|
*******************************************************************************/
|
||||||
|
const queryToAdvancedString = () =>
|
||||||
|
{
|
||||||
|
if(queryFilter == null || !queryFilter.criteria)
|
||||||
|
{
|
||||||
|
return (<span></span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{queryFilter.criteria.map((criteria, i) =>
|
||||||
|
{
|
||||||
|
if(criteria && criteria.fieldName && criteria.operator)
|
||||||
|
{
|
||||||
|
const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
|
||||||
|
const valuesString = FilterUtils.getValuesString(field, criteria);
|
||||||
|
counter++;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={i}>
|
||||||
|
{counter > 1 ? <span>{queryFilter.booleanOperator} </span> : <span/>}
|
||||||
|
<b>{field.label}</b> {criteria.operator} <span style={{color: "blue"}}>{valuesString}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (<span />);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for toggling between modes - basic & advanced.
|
||||||
|
*******************************************************************************/
|
||||||
|
const modeToggleClicked = (newValue: string | null) =>
|
||||||
|
{
|
||||||
|
if (newValue)
|
||||||
|
{
|
||||||
|
if(newValue == "basic")
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// we're always allowed to go to advanced - //
|
||||||
|
// but if we're trying to go to basic, make sure the filter isn't too complex //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const {canFilterWorkAsBasic} = FilterUtils.canFilterWorkAsBasic(tableMetaData, queryFilter);
|
||||||
|
if (!canFilterWorkAsBasic)
|
||||||
|
{
|
||||||
|
console.log("Query cannot work as basic - so - not allowing toggle to basic.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// when going to basic, make sure all fields in the current query are active as quick-filters //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (queryFilter && queryFilter.criteria)
|
||||||
|
{
|
||||||
|
ensureAllFilterCriteriaAreActiveQuickFilters(tableMetaData, queryFilter, "modeToggleClicked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// note - this is a callback to the parent - as it is responsible for this state... //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
setMode(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** make sure that all fields in the current query are on-screen as quick-filters
|
||||||
|
** (that is, if the query can be basic)
|
||||||
|
*******************************************************************************/
|
||||||
|
const ensureAllFilterCriteriaAreActiveQuickFilters = (tableMetaData: QTableMetaData, queryFilter: QQueryFilter, reason: "modeToggleClicked" | "defaultFilterLoaded" | "savedFilterSelected" | string) =>
|
||||||
|
{
|
||||||
|
if(!tableMetaData || !queryFilter)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {canFilterWorkAsBasic} = FilterUtils.canFilterWorkAsBasic(tableMetaData, queryFilter);
|
||||||
|
if (!canFilterWorkAsBasic)
|
||||||
|
{
|
||||||
|
console.log("query is too complex for basic - so - switching to advanced");
|
||||||
|
modeToggleClicked("advanced");
|
||||||
|
forceUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||||
|
{
|
||||||
|
const criteria = queryFilter.criteria[i];
|
||||||
|
if (criteria && criteria.fieldName)
|
||||||
|
{
|
||||||
|
addQuickFilterField(criteria, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there aren't any quick-filters turned on, get defaults from the table //
|
||||||
|
// only run this block upon a first-render //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
if(firstRender)
|
||||||
|
{
|
||||||
|
setFirstRender(false);
|
||||||
|
|
||||||
|
if (defaultQuickFilterFieldNames == null || defaultQuickFilterFieldNames.size == 0)
|
||||||
|
{
|
||||||
|
defaultQuickFilterFieldNames = new Set<string>();
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// check if there's materialDashboard tableMetaData, and if it has defaultQuickFilterFieldNames //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const mdbMetaData = tableMetaData?.supplementalTableMetaData?.get("materialDashboard");
|
||||||
|
if (mdbMetaData)
|
||||||
|
{
|
||||||
|
if (mdbMetaData?.defaultQuickFilterFieldNames?.length)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < mdbMetaData.defaultQuickFilterFieldNames.length; i++)
|
||||||
|
{
|
||||||
|
defaultQuickFilterFieldNames.add(mdbMetaData.defaultQuickFilterFieldNames[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// if still none, then look for T1 section //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
if (defaultQuickFilterFieldNames.size == 0)
|
||||||
|
{
|
||||||
|
if (tableMetaData.sections)
|
||||||
|
{
|
||||||
|
const t1Sections = tableMetaData.sections.filter((s: QTableSection) => s.tier == "T1");
|
||||||
|
if (t1Sections.length)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < t1Sections.length; i++)
|
||||||
|
{
|
||||||
|
if (t1Sections[i].fieldNames)
|
||||||
|
{
|
||||||
|
for (let j = 0; j < t1Sections[i].fieldNames.length; j++)
|
||||||
|
{
|
||||||
|
defaultQuickFilterFieldNames.add(t1Sections[i].fieldNames[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuickFilterFieldNames(defaultQuickFilterFieldNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// this is being used as a version of like forcing that we get re-rendered if the query filter changes... //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const [lastIndex, setLastIndex] = useState(queryFilterJSON);
|
||||||
|
if(queryFilterJSON != lastIndex)
|
||||||
|
{
|
||||||
|
ensureAllFilterCriteriaAreActiveQuickFilters(tableMetaData, queryFilter, "defaultFilterLoaded");
|
||||||
|
setLastIndex(queryFilterJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
// set some status flags based on current filter //
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
const hasValidFilters = queryFilter && queryFilter.criteria && queryFilter.criteria.length > 0; // todo - should be better (e.g., see if operator & values are set)
|
||||||
|
const {canFilterWorkAsBasic, reasonsWhyItCannot} = FilterUtils.canFilterWorkAsBasic(tableMetaData, queryFilter);
|
||||||
|
let reasonWhyBasicIsDisabled = null;
|
||||||
|
if(reasonsWhyItCannot && reasonsWhyItCannot.length > 0)
|
||||||
|
{
|
||||||
|
reasonWhyBasicIsDisabled = <>
|
||||||
|
Your current Filter cannot be managed using BASIC mode because:
|
||||||
|
<ul style={{marginLeft: "1rem"}}>
|
||||||
|
{reasonsWhyItCannot.map((reason, i) => <li key={i}>{reason}</li>)}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display="flex" alignItems="flex-start" justifyContent="space-between" flexWrap="wrap" position="relative" top={"-0.5rem"} left={"0.5rem"} minHeight="2.5rem">
|
||||||
|
<Box display="flex" alignItems="center" flexShrink={1} flexGrow={1}>
|
||||||
|
{
|
||||||
|
mode == "basic" &&
|
||||||
|
<Box width="100px" flexShrink={1} flexGrow={1}>
|
||||||
|
{
|
||||||
|
tableMetaData &&
|
||||||
|
[...quickFilterFieldNames.values()].map((fieldName) =>
|
||||||
|
{
|
||||||
|
const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
||||||
|
let defaultOperator = getDefaultOperatorForField(field);
|
||||||
|
|
||||||
|
return (
|
||||||
|
field && <QuickFilter
|
||||||
|
key={fieldName}
|
||||||
|
fullFieldName={fieldName}
|
||||||
|
tableMetaData={tableMetaData}
|
||||||
|
updateCriteria={updateQuickCriteria}
|
||||||
|
criteriaParam={getQuickCriteriaParam(fieldName)}
|
||||||
|
fieldMetaData={field}
|
||||||
|
defaultOperator={defaultOperator}
|
||||||
|
handleRemoveQuickFilterField={handleRemoveQuickFilterField} />
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{
|
||||||
|
tableMetaData &&
|
||||||
|
<>
|
||||||
|
<Tooltip enterDelay={500} title="Add a Quick Filter field" placement="top">
|
||||||
|
<Button onClick={(e) => openAddQuickFilterMenu(e)} startIcon={<Icon>add_circle_outline</Icon>} sx={{border: "1px solid gray", whiteSpace: "nowrap", minWidth: "120px"}}>
|
||||||
|
Add Field
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
anchorEl={addQuickFilterMenu}
|
||||||
|
anchorOrigin={{vertical: "bottom", horizontal: "left"}}
|
||||||
|
transformOrigin={{vertical: "top", horizontal: "left"}}
|
||||||
|
transitionDuration={0}
|
||||||
|
open={Boolean(addQuickFilterMenu)}
|
||||||
|
onClose={closeAddQuickFilterMenu}
|
||||||
|
keepMounted
|
||||||
|
>
|
||||||
|
<Box width="250px">
|
||||||
|
<FieldAutoComplete
|
||||||
|
key={addQuickFilterOpenCounter} // use a unique key each time we open it, because we don't want the user's last selection to stick.
|
||||||
|
id={"add-quick-filter-field"}
|
||||||
|
metaData={metaData}
|
||||||
|
tableMetaData={tableMetaData}
|
||||||
|
defaultValue={null}
|
||||||
|
handleFieldChange={(e, newValue, reason) => addQuickFilterField(newValue, reason)}
|
||||||
|
autoFocus={true}
|
||||||
|
forceOpen={Boolean(addQuickFilterMenu)}
|
||||||
|
hiddenFieldNames={[...quickFilterFieldNames.values()]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
metaData && tableMetaData && mode == "advanced" &&
|
||||||
|
<>
|
||||||
|
<Tooltip enterDelay={500} title="Build an advanced Filter" placement="top">
|
||||||
|
<Button onClick={(e) => openFilterBuilder(e)} startIcon={<Badge badgeContent={queryFilter?.criteria?.length} color="warning" sx={{"& .MuiBadge-badge": {color: "#FFFFFF"}}} anchorOrigin={{vertical: "top", horizontal: "left"}}><Icon>filter_list</Icon></Badge>} sx={{width: "180px", minWidth: "180px", border: "1px solid gray"}}>
|
||||||
|
Filter Builder
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<div id="clearFiltersButton" style={{display: "inline-block", position: "relative", top: "2px", left: "-1.5rem", width: "1rem"}}>
|
||||||
|
{
|
||||||
|
hasValidFilters && (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Clear Filter">
|
||||||
|
<Icon sx={{cursor: "pointer"}} onClick={() => setShowClearFiltersWarning(true)}>clear</Icon>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog open={showClearFiltersWarning} onClose={() => setShowClearFiltersWarning(true)} onKeyPress={(e) => handleClearFiltersAction(e)}>
|
||||||
|
<DialogTitle id="alert-dialog-title">Confirm</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>Are you sure you want to remove all conditions from the current filter?</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<QCancelButton label="No" disabled={false} onClickHandler={() => setShowClearFiltersWarning(true)} />
|
||||||
|
<QSaveButton label="Yes" iconName="check" disabled={false} onClickHandler={() => handleClearFiltersAction(null, true)} />
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Box sx={{fontSize: "1rem"}} whiteSpace="nowrap" display="flex" ml={0.25} flexShrink={1} flexGrow={1} alignItems="center">
|
||||||
|
Current Filter:
|
||||||
|
{
|
||||||
|
<Box display="inline-block" border="1px solid gray" borderRadius="0.5rem" whiteSpace="nowrap" overflow="hidden" textOverflow="ellipsis" width="100px" flexShrink={1} flexGrow={1} sx={{fontSize: "1rem"}} minHeight={"2rem"} p={0.25} ml={0.5}>
|
||||||
|
{queryToAdvancedString()}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
{
|
||||||
|
metaData && tableMetaData &&
|
||||||
|
<Box px={1} display="flex" alignItems="center">
|
||||||
|
<Typography display="inline" sx={{fontSize: "1rem"}}>Mode:</Typography>
|
||||||
|
<Tooltip title={reasonWhyBasicIsDisabled}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={mode}
|
||||||
|
exclusive
|
||||||
|
onChange={(event, newValue) => modeToggleClicked(newValue)}
|
||||||
|
size="small"
|
||||||
|
sx={{pl: 0.5}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="basic" disabled={!canFilterWorkAsBasic}>Basic</ToggleButton>
|
||||||
|
<ToggleButton value="advanced">Advanced</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BasicAndAdvancedQueryControls;
|
131
src/qqq/components/query/ExportMenuItem.tsx
Normal file
131
src/qqq/components/query/ExportMenuItem.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. 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 {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import {GridColDef, GridExportMenuItemProps} from "@mui/x-data-grid-pro";
|
||||||
|
import React from "react";
|
||||||
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
|
||||||
|
interface QExportMenuItemProps extends GridExportMenuItemProps<{}>
|
||||||
|
{
|
||||||
|
tableMetaData: QTableMetaData;
|
||||||
|
totalRecords: number
|
||||||
|
columnsModel: GridColDef[];
|
||||||
|
columnVisibilityModel: { [index: string]: boolean };
|
||||||
|
queryFilter: QQueryFilter;
|
||||||
|
format: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Component to serve as an item in the Export menu
|
||||||
|
*******************************************************************************/
|
||||||
|
export default function ExportMenuItem(props: QExportMenuItemProps)
|
||||||
|
{
|
||||||
|
const {format, tableMetaData, totalRecords, columnsModel, columnVisibilityModel, queryFilter, hideMenu} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
disabled={totalRecords === 0}
|
||||||
|
onClick={() =>
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// build the list of visible fields. note, not doing them in-order (in case //
|
||||||
|
// the user did drag & drop), because column order model isn't right yet //
|
||||||
|
// so just doing them to match columns (which were pKey, then sorted) //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
const visibleFields: string[] = [];
|
||||||
|
columnsModel.forEach((gridColumn) =>
|
||||||
|
{
|
||||||
|
const fieldName = gridColumn.field;
|
||||||
|
if (columnVisibilityModel[fieldName] !== false)
|
||||||
|
{
|
||||||
|
visibleFields.push(fieldName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// construct the url for the export //
|
||||||
|
//////////////////////////////////////
|
||||||
|
const dateString = ValueUtils.formatDateTimeForFileName(new Date());
|
||||||
|
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
|
||||||
|
const url = `/data/${tableMetaData.name}/export/${filename}`;
|
||||||
|
|
||||||
|
const encodedFilterJSON = encodeURIComponent(JSON.stringify(queryFilter));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// open a window (tab) with a little page that says the file is being generated. //
|
||||||
|
// then have that page load the url for the export. //
|
||||||
|
// If there's an error, it'll appear in that window. else, the file will download. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const exportWindow = window.open("", "_blank");
|
||||||
|
exportWindow.document.write(`<html lang="en">
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
* { font-family: "SF Pro Display","Roboto","Helvetica","Arial",sans-serif; }
|
||||||
|
</style>
|
||||||
|
<title>${filename}</title>
|
||||||
|
<script>
|
||||||
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// need to encode and decode this value, so set it in the form here, instead of literally below //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
document.getElementById("filter").value = decodeURIComponent("${encodedFilterJSON}");
|
||||||
|
|
||||||
|
document.getElementById("exportForm").submit();
|
||||||
|
}, 1);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||||
|
<form id="exportForm" method="post" action="${url}" >
|
||||||
|
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||||
|
<input type="hidden" name="filter" id="filter">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>`);
|
||||||
|
|
||||||
|
/*
|
||||||
|
// todo - probably better - generate the report in an iframe...
|
||||||
|
// only open question is, giving user immediate feedback, and knowing when the stream has started and/or stopped
|
||||||
|
// maybe a busy-loop that would check iframe's url (e.g., after posting should change, maybe?)
|
||||||
|
const iframe = document.getElementById("exportIFrame");
|
||||||
|
const form = iframe.querySelector("form");
|
||||||
|
form.action = url;
|
||||||
|
form.target = "exportIFrame";
|
||||||
|
(iframe.querySelector("#authorizationInput") as HTMLInputElement).value = qController.getAuthorizationHeaderValue();
|
||||||
|
form.submit();
|
||||||
|
*/
|
||||||
|
|
||||||
|
///////////////////////////////////////////
|
||||||
|
// Hide the export menu after the export //
|
||||||
|
///////////////////////////////////////////
|
||||||
|
hideMenu?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
{` ${format.toUpperCase()}`}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
@ -35,9 +36,10 @@ import React, {SyntheticEvent, useState} from "react";
|
|||||||
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
||||||
import {getDefaultCriteriaValue, getOperatorOptions, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
|
import {getDefaultCriteriaValue, getOperatorOptions, OperatorOption, validateCriteria} from "qqq/components/query/FilterCriteriaRow";
|
||||||
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
|
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
|
||||||
|
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||||
|
|
||||||
type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex";
|
export type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex";
|
||||||
|
|
||||||
interface QuickFilterProps
|
interface QuickFilterProps
|
||||||
{
|
{
|
||||||
@ -47,27 +49,64 @@ interface QuickFilterProps
|
|||||||
criteriaParam: CriteriaParamType;
|
criteriaParam: CriteriaParamType;
|
||||||
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean, doRemoveCriteria: boolean) => void;
|
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean, doRemoveCriteria: boolean) => void;
|
||||||
defaultOperator?: QCriteriaOperator;
|
defaultOperator?: QCriteriaOperator;
|
||||||
toggleQuickFilterField?: (fieldName: string) => void;
|
handleRemoveQuickFilterField?: (fieldName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QuickFilter.defaultProps =
|
||||||
|
{
|
||||||
|
defaultOperator: QCriteriaOperator.EQUALS,
|
||||||
|
handleRemoveQuickFilterField: null
|
||||||
|
};
|
||||||
|
|
||||||
|
let seedId = new Date().getTime() % 173237;
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Test if a CriteriaParamType represents an actual query criteria - or, if it's
|
||||||
|
** null or the "tooComplex" placeholder.
|
||||||
|
*******************************************************************************/
|
||||||
const criteriaParamIsCriteria = (param: CriteriaParamType): boolean =>
|
const criteriaParamIsCriteria = (param: CriteriaParamType): boolean =>
|
||||||
{
|
{
|
||||||
return (param != null && param != "tooComplex");
|
return (param != null && param != "tooComplex");
|
||||||
};
|
};
|
||||||
|
|
||||||
QuickFilter.defaultProps =
|
/*******************************************************************************
|
||||||
|
** Test of an OperatorOption equals a query Criteria - that is - that the
|
||||||
|
** operators within them are equal - AND - if the OperatorOption has implicit
|
||||||
|
** values (e.g., the booleans), then those options equal the criteria's options.
|
||||||
|
*******************************************************************************/
|
||||||
|
const doesOperatorOptionEqualCriteria = (operatorOption: OperatorOption, criteria: QFilterCriteriaWithId): boolean =>
|
||||||
|
{
|
||||||
|
if(operatorOption.value == criteria.operator)
|
||||||
{
|
{
|
||||||
defaultOperator: QCriteriaOperator.EQUALS,
|
if(operatorOption.implicitValues)
|
||||||
toggleQuickFilterField: null
|
{
|
||||||
};
|
if(JSON.stringify(operatorOption.implicitValues) == JSON.stringify(criteria.values))
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let seedId = new Date().getTime() % 173237;
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Get the object to use as the selected OperatorOption (e.g., value for that
|
||||||
|
** autocomplete), given an array of options, the query's active criteria in this
|
||||||
|
** field, and the default operator to use for this field
|
||||||
|
*******************************************************************************/
|
||||||
const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: QFilterCriteriaWithId, defaultOperator: QCriteriaOperator): OperatorOption =>
|
const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: QFilterCriteriaWithId, defaultOperator: QCriteriaOperator): OperatorOption =>
|
||||||
{
|
{
|
||||||
if(criteria)
|
if(criteria)
|
||||||
{
|
{
|
||||||
const filteredOptions = operatorOptions.filter(o => o.value == criteria.operator);
|
const filteredOptions = operatorOptions.filter(o => doesOperatorOptionEqualCriteria(o, criteria));
|
||||||
if(filteredOptions.length > 0)
|
if(filteredOptions.length > 0)
|
||||||
{
|
{
|
||||||
return (filteredOptions[0]);
|
return (filteredOptions[0]);
|
||||||
@ -83,10 +122,14 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
|
|||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, toggleQuickFilterField}: QuickFilterProps): JSX.Element
|
/*******************************************************************************
|
||||||
|
** Component to render a QuickFilter - that is - a button, with a Menu under it,
|
||||||
|
** with Operator and Value controls.
|
||||||
|
*******************************************************************************/
|
||||||
|
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField}: QuickFilterProps): JSX.Element
|
||||||
{
|
{
|
||||||
const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : [];
|
const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : [];
|
||||||
const tableForField = tableMetaData; // todo!! const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName);
|
const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
@ -97,17 +140,9 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
||||||
const [operatorInputValue, setOperatorInputValue] = useState(operatorSelectedValue?.label);
|
const [operatorInputValue, setOperatorInputValue] = useState(operatorSelectedValue?.label);
|
||||||
|
|
||||||
const maybeNewOperatorSelectedValue = getOperatorSelectedValue(operatorOptions, criteria, defaultOperator);
|
const [startIconName, setStartIconName] = useState("filter_alt");
|
||||||
if(JSON.stringify(maybeNewOperatorSelectedValue) !== JSON.stringify(operatorSelectedValue))
|
|
||||||
{
|
|
||||||
setOperatorSelectedValue(maybeNewOperatorSelectedValue)
|
|
||||||
setOperatorInputValue(maybeNewOperatorSelectedValue.label)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!fieldMetaData)
|
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
|
||||||
{
|
|
||||||
return (null);
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// handle a change to the criteria from outside this component (e.g., the prop isn't the same as the state) //
|
// handle a change to the criteria from outside this component (e.g., the prop isn't the same as the state) //
|
||||||
@ -117,16 +152,20 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
const newCriteria = criteriaParam as QFilterCriteriaWithId;
|
const newCriteria = criteriaParam as QFilterCriteriaWithId;
|
||||||
setCriteria(newCriteria);
|
setCriteria(newCriteria);
|
||||||
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
|
const operatorOption = operatorOptions.filter(o => o.value == newCriteria.operator)[0];
|
||||||
|
console.log(`B: setOperatorSelectedValue [${JSON.stringify(operatorOption)}]`);
|
||||||
setOperatorSelectedValue(operatorOption);
|
setOperatorSelectedValue(operatorOption);
|
||||||
setOperatorInputValue(operatorOption.label);
|
setOperatorInputValue(operatorOption.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Test if we need to construct a new criteria object
|
||||||
|
*******************************************************************************/
|
||||||
const criteriaNeedsReset = (): boolean =>
|
const criteriaNeedsReset = (): boolean =>
|
||||||
{
|
{
|
||||||
if(criteria != null && criteriaParam == null)
|
if(criteria != null && criteriaParam == null)
|
||||||
{
|
{
|
||||||
const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
const defaultOperatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
||||||
if(criteria.operator !== defaultOperatorOption.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue()))
|
if(criteria.operator !== defaultOperatorOption?.value || JSON.stringify(criteria.values) !== JSON.stringify(getDefaultCriteriaValue()))
|
||||||
{
|
{
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
@ -135,44 +174,50 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
return (false);
|
return (false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Construct a new criteria object - resetting the values tied to the oprator
|
||||||
|
** autocomplete at the same time.
|
||||||
|
*******************************************************************************/
|
||||||
const makeNewCriteria = (): QFilterCriteria =>
|
const makeNewCriteria = (): QFilterCriteria =>
|
||||||
{
|
{
|
||||||
const operatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
const operatorOption = operatorOptions.filter(o => o.value == defaultOperator)[0];
|
||||||
const criteria = new QFilterCriteriaWithId(fullFieldName, operatorOption.value, getDefaultCriteriaValue());
|
const criteria = new QFilterCriteriaWithId(fullFieldName, operatorOption?.value, getDefaultCriteriaValue());
|
||||||
criteria.id = id;
|
criteria.id = id;
|
||||||
|
console.log(`C: setOperatorSelectedValue [${JSON.stringify(operatorOption)}]`);
|
||||||
setOperatorSelectedValue(operatorOption);
|
setOperatorSelectedValue(operatorOption);
|
||||||
setOperatorInputValue(operatorOption.label);
|
setOperatorInputValue(operatorOption?.label);
|
||||||
setCriteria(criteria);
|
setCriteria(criteria);
|
||||||
return(criteria);
|
return(criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (criteria == null || criteriaNeedsReset())
|
/*******************************************************************************
|
||||||
{
|
** event handler to open the menu in response to the button being clicked.
|
||||||
makeNewCriteria();
|
*******************************************************************************/
|
||||||
}
|
const handleOpenMenu = (event: any) =>
|
||||||
|
|
||||||
const toggleOpen = (event: any) =>
|
|
||||||
{
|
{
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** handler for the Menu when being closed
|
||||||
|
*******************************************************************************/
|
||||||
const closeMenu = () =>
|
const closeMenu = () =>
|
||||||
{
|
{
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
/////////////////////////////////////////////
|
/*******************************************************************************
|
||||||
// event handler for operator Autocomplete //
|
** event handler for operator Autocomplete having its value changed
|
||||||
// todo - too dupe?
|
*******************************************************************************/
|
||||||
/////////////////////////////////////////////
|
|
||||||
const handleOperatorChange = (event: any, newValue: any, reason: string) =>
|
const handleOperatorChange = (event: any, newValue: any, reason: string) =>
|
||||||
{
|
{
|
||||||
criteria.operator = newValue ? newValue.value : null;
|
criteria.operator = newValue ? newValue.value : null;
|
||||||
|
|
||||||
if (newValue)
|
if (newValue)
|
||||||
{
|
{
|
||||||
|
console.log(`D: setOperatorSelectedValue [${JSON.stringify(newValue)}]`);
|
||||||
setOperatorSelectedValue(newValue);
|
setOperatorSelectedValue(newValue);
|
||||||
setOperatorInputValue(newValue.label);
|
setOperatorInputValue(newValue.label);
|
||||||
|
|
||||||
@ -183,6 +228,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
console.log("E: setOperatorSelectedValue [null]");
|
||||||
setOperatorSelectedValue(null);
|
setOperatorSelectedValue(null);
|
||||||
setOperatorInputValue("");
|
setOperatorInputValue("");
|
||||||
}
|
}
|
||||||
@ -190,15 +236,18 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
updateCriteria(criteria, false, false);
|
updateCriteria(criteria, false, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** implementation of isOptionEqualToValue for Autocomplete - compares both the
|
||||||
|
** value (e.g., what operator it is) and the implicitValues within the option
|
||||||
|
*******************************************************************************/
|
||||||
function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption)
|
function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption)
|
||||||
{
|
{
|
||||||
return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues));
|
return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues));
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
/*******************************************************************************
|
||||||
// event handler for value field (of all types) //
|
** event handler for the value field (of all types), when it changes
|
||||||
// todo - too dupe!
|
*******************************************************************************/
|
||||||
//////////////////////////////////////////////////
|
|
||||||
const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) =>
|
const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) =>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -221,48 +270,17 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
updateCriteria(criteria, true, false);
|
updateCriteria(criteria, true, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** a noop event handler, e.g., for a too-complex
|
||||||
|
*******************************************************************************/
|
||||||
const noop = () =>
|
const noop = () =>
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
|
||||||
const getValuesString = (): string =>
|
/*******************************************************************************
|
||||||
{
|
** event handler that responds to 'x' button that removes the criteria from the
|
||||||
let valuesString = "";
|
** quick-filter, resetting it to a new filter.
|
||||||
if (criteria.values && criteria.values.length)
|
*******************************************************************************/
|
||||||
{
|
|
||||||
let labels = [] as string[];
|
|
||||||
|
|
||||||
let maxLoops = criteria.values.length;
|
|
||||||
if (maxLoops > 5)
|
|
||||||
{
|
|
||||||
maxLoops = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < maxLoops; i++)
|
|
||||||
{
|
|
||||||
if (criteria.values[i] && criteria.values[i].label)
|
|
||||||
{
|
|
||||||
labels.push(criteria.values[i].label);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
labels.push(criteria.values[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxLoops < criteria.values.length)
|
|
||||||
{
|
|
||||||
labels.push(" and " + (criteria.values.length - maxLoops) + " other values.");
|
|
||||||
}
|
|
||||||
|
|
||||||
valuesString = (labels.join(", "));
|
|
||||||
}
|
|
||||||
return valuesString;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [startIconName, setStartIconName] = useState("filter_alt");
|
|
||||||
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
|
|
||||||
|
|
||||||
const resetCriteria = (e: React.MouseEvent<HTMLSpanElement>) =>
|
const resetCriteria = (e: React.MouseEvent<HTMLSpanElement>) =>
|
||||||
{
|
{
|
||||||
if(criteriaIsValid)
|
if(criteriaIsValid)
|
||||||
@ -274,6 +292,10 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for mouse-over on the filter icon - that changes to an 'x'
|
||||||
|
** if there's a valid criteria in the quick-filter
|
||||||
|
*******************************************************************************/
|
||||||
const startIconMouseOver = () =>
|
const startIconMouseOver = () =>
|
||||||
{
|
{
|
||||||
if(criteriaIsValid)
|
if(criteriaIsValid)
|
||||||
@ -281,11 +303,56 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
setStartIconName("clear");
|
setStartIconName("clear");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for mouse-out on the filter icon - always resets it.
|
||||||
|
*******************************************************************************/
|
||||||
const startIconMouseOut = () =>
|
const startIconMouseOut = () =>
|
||||||
{
|
{
|
||||||
setStartIconName("filter_alt");
|
setStartIconName("filter_alt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** event handler for clicking the (x) icon that turns off this quick filter field.
|
||||||
|
** hands off control to the function that was passed in (e.g., from RecordQuery).
|
||||||
|
*******************************************************************************/
|
||||||
|
const handleTurningOffQuickFilterField = () =>
|
||||||
|
{
|
||||||
|
closeMenu()
|
||||||
|
handleRemoveQuickFilterField(criteria?.fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if no field was input (e.g., record-query is still loading), return null early //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(!fieldMetaData)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there should be a selected value in the operator autocomplete, and it's different //
|
||||||
|
// from the last selected one, then set the state vars that control that autocomplete //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const maybeNewOperatorSelectedValue = getOperatorSelectedValue(operatorOptions, criteria, defaultOperator);
|
||||||
|
if(JSON.stringify(maybeNewOperatorSelectedValue) !== JSON.stringify(operatorSelectedValue))
|
||||||
|
{
|
||||||
|
console.log(`A: setOperatorSelectedValue [${JSON.stringify(maybeNewOperatorSelectedValue)}]`);
|
||||||
|
setOperatorSelectedValue(maybeNewOperatorSelectedValue)
|
||||||
|
setOperatorInputValue(maybeNewOperatorSelectedValue?.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there wasn't a criteria, or we need to reset it (make a new one), then do so //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (criteria == null || criteriaNeedsReset())
|
||||||
|
{
|
||||||
|
makeNewCriteria();
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// build up the button //
|
||||||
|
/////////////////////////
|
||||||
const tooComplex = criteriaParam == "tooComplex";
|
const tooComplex = criteriaParam == "tooComplex";
|
||||||
const tooltipEnterDelay = 500;
|
const tooltipEnterDelay = 500;
|
||||||
let startIcon = <Badge badgeContent={criteriaIsValid && !tooComplex ? 1 : 0} color="warning" variant="dot" onMouseOver={startIconMouseOver} onMouseOut={startIconMouseOut} onClick={resetCriteria}><Icon>{startIconName}</Icon></Badge>
|
let startIcon = <Badge badgeContent={criteriaIsValid && !tooComplex ? 1 : 0} color="warning" variant="dot" onMouseOver={startIconMouseOver} onMouseOut={startIconMouseOut} onClick={resetCriteria}><Icon>{startIconName}</Icon></Badge>
|
||||||
@ -298,22 +365,29 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
if (criteriaIsValid)
|
if (criteriaIsValid)
|
||||||
{
|
{
|
||||||
buttonContent = (
|
buttonContent = (
|
||||||
<Tooltip title={`${operatorSelectedValue.label} ${getValuesString()}`} enterDelay={tooltipEnterDelay}>
|
<Tooltip title={`${operatorSelectedValue.label} ${FilterUtils.getValuesString(fieldMetaData, criteria)}`} enterDelay={tooltipEnterDelay}>
|
||||||
{buttonContent}
|
{buttonContent}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let button = fieldMetaData && <Button
|
let button = fieldMetaData && <Button
|
||||||
|
id={`quickFilter.${fullFieldName}`}
|
||||||
sx={{mr: "0.25rem", px: "1rem", border: isOpen ? "1px solid gray" : "1px solid transparent"}}
|
sx={{mr: "0.25rem", px: "1rem", border: isOpen ? "1px solid gray" : "1px solid transparent"}}
|
||||||
startIcon={startIcon}
|
startIcon={startIcon}
|
||||||
onClick={tooComplex ? noop : toggleOpen}
|
onClick={tooComplex ? noop : handleOpenMenu}
|
||||||
disabled={tooComplex}
|
disabled={tooComplex}
|
||||||
>{buttonContent}</Button>;
|
>{buttonContent}</Button>;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the criteria on this field is the "tooComplex" sentinel, then wrap the button in a tooltip stating such, and return early. //
|
||||||
|
// note this was part of original design on this widget, but later deprecated... //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (tooComplex)
|
if (tooComplex)
|
||||||
{
|
{
|
||||||
// wrap button in span, so disabled button doesn't cause disabled tooltip
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// wrap button in span, so disabled button doesn't cause disabled tooltip //
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
return (
|
return (
|
||||||
<Tooltip title={`Your current filter is too complex to do a Quick Filter on ${fieldMetaData.label}. Use the Filter button to edit.`} enterDelay={tooltipEnterDelay} slotProps={{popper: {sx: {top: "-0.75rem!important"}}}}>
|
<Tooltip title={`Your current filter is too complex to do a Quick Filter on ${fieldMetaData.label}. Use the Filter button to edit.`} enterDelay={tooltipEnterDelay} slotProps={{popper: {sx: {top: "-0.75rem!important"}}}}>
|
||||||
<span>{button}</span>
|
<span>{button}</span>
|
||||||
@ -321,12 +395,9 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const doToggle = () =>
|
//////////////////////////////
|
||||||
{
|
// return the button & menu //
|
||||||
closeMenu()
|
//////////////////////////////
|
||||||
toggleQuickFilterField(criteria?.fieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const widthAndMaxWidth = 250
|
const widthAndMaxWidth = 250
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -334,9 +405,9 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
{
|
{
|
||||||
isOpen && <Menu open={Boolean(anchorEl)} anchorEl={anchorEl} onClose={closeMenu} sx={{overflow: "visible"}}>
|
isOpen && <Menu open={Boolean(anchorEl)} anchorEl={anchorEl} onClose={closeMenu} sx={{overflow: "visible"}}>
|
||||||
{
|
{
|
||||||
toggleQuickFilterField &&
|
handleRemoveQuickFilterField &&
|
||||||
<Tooltip title={"Remove this field from Quick Filters"} placement="right">
|
<Tooltip title={"Remove this field from Quick Filters"} placement="right">
|
||||||
<IconButton size="small" sx={{position: "absolute", top: "-8px", right: "-8px", zIndex: 1}} onClick={doToggle}><Icon color="secondary">highlight_off</Icon></IconButton>
|
<IconButton size="small" sx={{position: "absolute", top: "-8px", right: "-8px", zIndex: 1}} onClick={handleTurningOffQuickFilterField}><Icon color="secondary">highlight_off</Icon></IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
<Box display="inline-block" width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="operatorColumn">
|
<Box display="inline-block" width={widthAndMaxWidth} maxWidth={widthAndMaxWidth} className="operatorColumn">
|
||||||
|
74
src/qqq/components/query/SelectionSubsetDialog.tsx
Normal file
74
src/qqq/components/query/SelectionSubsetDialog.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. 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 Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Component that is the dialog for the user to enter the selection-subset
|
||||||
|
*******************************************************************************/
|
||||||
|
export default function SelectionSubsetDialog(props: { isOpen: boolean; initialValue: number; closeHandler: (value?: number) => void })
|
||||||
|
{
|
||||||
|
const [value, setValue] = useState(props.initialValue);
|
||||||
|
|
||||||
|
const handleChange = (newValue: string) =>
|
||||||
|
{
|
||||||
|
setValue(parseInt(newValue));
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||||
|
{
|
||||||
|
if (e.key == "Enter" && value)
|
||||||
|
{
|
||||||
|
props.closeHandler(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={props.isOpen} onClose={() => props.closeHandler()} onKeyPress={(e) => keyPressed(e)}>
|
||||||
|
<DialogTitle>Subset of the Query Result</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>How many records do you want to select?</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
name="selection-subset-size"
|
||||||
|
inputProps={{width: "100%", type: "number", min: 1}}
|
||||||
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
|
value={value}
|
||||||
|
sx={{width: "100%"}}
|
||||||
|
onFocus={event => event.target.select()}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<QCancelButton disabled={false} onClickHandler={() => props.closeHandler()} />
|
||||||
|
<QSaveButton label="OK" iconName="check" disabled={value == undefined || isNaN(value)} onClickHandler={() => props.closeHandler(value)} />
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
122
src/qqq/components/query/TableVariantDialog.tsx
Normal file
122
src/qqq/components/query/TableVariantDialog.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. 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 {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Component that is the dialog for the user to select a variant on tables with variant backends //
|
||||||
|
*******************************************************************************/
|
||||||
|
export default function TableVariantDialog(props: { isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void })
|
||||||
|
{
|
||||||
|
const [value, setValue] = useState(null);
|
||||||
|
const [dropDownOpen, setDropDownOpen] = useState(false);
|
||||||
|
const [variants, setVariants] = useState(null);
|
||||||
|
|
||||||
|
const handleVariantChange = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||||
|
{
|
||||||
|
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${props.table.name}`;
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
localStorage.setItem(tableVariantLocalStorageKey, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
localStorage.removeItem(tableVariantLocalStorageKey);
|
||||||
|
}
|
||||||
|
props.closeHandler(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||||
|
{
|
||||||
|
if (e.key == "Enter" && value)
|
||||||
|
{
|
||||||
|
props.closeHandler(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
console.log("queryVariants");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
const variants = await qController.tableVariants(props.table.name);
|
||||||
|
console.log(JSON.stringify(variants));
|
||||||
|
setVariants(variants);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
return variants && (
|
||||||
|
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
||||||
|
<DialogTitle>{props.table.variantTableLabel}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
||||||
|
<Autocomplete
|
||||||
|
id="tableVariantId"
|
||||||
|
sx={{width: "400px", marginTop: "10px"}}
|
||||||
|
open={dropDownOpen}
|
||||||
|
size="small"
|
||||||
|
onOpen={() =>
|
||||||
|
{
|
||||||
|
setDropDownOpen(true);
|
||||||
|
}}
|
||||||
|
onClose={() =>
|
||||||
|
{
|
||||||
|
setDropDownOpen(false);
|
||||||
|
}}
|
||||||
|
// @ts-ignore
|
||||||
|
onChange={handleVariantChange}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
options={variants}
|
||||||
|
renderInput={(params) => <TextField {...params} label={props.table.variantTableLabel} />}
|
||||||
|
getOptionLabel={(option) =>
|
||||||
|
{
|
||||||
|
if (typeof option == "object")
|
||||||
|
{
|
||||||
|
return (option as QTableVariant).name;
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,6 @@
|
|||||||
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
|
||||||
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
|
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
@ -30,19 +29,11 @@ import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTa
|
|||||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
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 {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {Alert, Collapse, TablePagination, Typography} from "@mui/material";
|
import {Alert, Collapse, TablePagination, Typography} from "@mui/material";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Dialog from "@mui/material/Dialog";
|
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
|
||||||
import DialogContentText from "@mui/material/DialogContentText";
|
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
@ -51,22 +42,23 @@ import ListItemIcon from "@mui/material/ListItemIcon";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import Modal from "@mui/material/Modal";
|
import Modal from "@mui/material/Modal";
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector, useGridApiRef, GridPreferencePanelsValue, GridColumnResizeParams} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector, useGridApiRef, GridPreferencePanelsValue, GridColumnResizeParams, ColumnHeaderFilterIconButtonProps} from "@mui/x-data-grid-pro";
|
||||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||||
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QActionsMenuButton, QCancelButton, QCreateNewButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import MenuButton from "qqq/components/buttons/MenuButton";
|
import MenuButton from "qqq/components/buttons/MenuButton";
|
||||||
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
|
|
||||||
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
|
import {GotoRecordButton} from "qqq/components/misc/GotoRecordDialog";
|
||||||
import SavedFilters from "qqq/components/misc/SavedFilters";
|
import SavedFilters from "qqq/components/misc/SavedFilters";
|
||||||
|
import BasicAndAdvancedQueryControls from "qqq/components/query/BasicAndAdvancedQueryControls";
|
||||||
import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel";
|
import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel";
|
||||||
import {CustomFilterPanel, QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel";
|
import {CustomFilterPanel} from "qqq/components/query/CustomFilterPanel";
|
||||||
import QuickFilter from "qqq/components/query/QuickFilter";
|
import ExportMenuItem from "qqq/components/query/ExportMenuItem";
|
||||||
|
import SelectionSubsetDialog from "qqq/components/query/SelectionSubsetDialog";
|
||||||
|
import TableVariantDialog from "qqq/components/query/TableVariantDialog";
|
||||||
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";
|
||||||
@ -89,6 +81,7 @@ const COLUMN_WIDTHS_LOCAL_STORAGE_KEY_ROOT = "qqq.columnWidths";
|
|||||||
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
|
const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables";
|
||||||
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
|
||||||
const QUICK_FILTER_FIELD_NAMES_LOCAL_STORAGE_KEY_ROOT = "qqq.quickFilterFieldNames";
|
const QUICK_FILTER_FIELD_NAMES_LOCAL_STORAGE_KEY_ROOT = "qqq.quickFilterFieldNames";
|
||||||
|
const MODE_LOCAL_STORAGE_KEY_ROOT = "qqq.queryScreenMode";
|
||||||
|
|
||||||
export const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
export const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
||||||
|
|
||||||
@ -104,7 +97,6 @@ RecordQuery.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
let debounceTimeout: string | number | NodeJS.Timeout;
|
|
||||||
|
|
||||||
function RecordQuery({table, launchProcess}: Props): JSX.Element
|
function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
@ -151,7 +143,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
const quickFilterFieldNamesLocalStorageKey = `${QUICK_FILTER_FIELD_NAMES_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
const modeLocalStorageKey = `${MODE_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
|
||||||
let defaultSort = [] as GridSortItem[];
|
let defaultSort = [] as GridSortItem[];
|
||||||
let defaultVisibility = {} as { [index: string]: boolean };
|
let defaultVisibility = {} as { [index: string]: boolean };
|
||||||
let didDefaultVisibilityComeFromLocalStorage = false;
|
let didDefaultVisibilityComeFromLocalStorage = false;
|
||||||
@ -162,7 +154,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
let defaultColumnWidths = {} as {[fieldName: string]: number};
|
let defaultColumnWidths = {} as {[fieldName: string]: number};
|
||||||
let seenJoinTables: {[tableName: string]: boolean} = {};
|
let seenJoinTables: {[tableName: string]: boolean} = {};
|
||||||
let defaultTableVariant: QTableVariant = null;
|
let defaultTableVariant: QTableVariant = null;
|
||||||
let defaultQuickFilterFieldNames: Set<string> = new Set<string>();
|
let defaultMode = "basic";
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
// set the to be not per table (do as above if we want per table) at a later port //
|
// set the to be not per table (do as above if we want per table) at a later port //
|
||||||
@ -206,13 +198,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
defaultTableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
defaultTableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||||
}
|
}
|
||||||
if (localStorage.getItem(quickFilterFieldNamesLocalStorageKey))
|
if (localStorage.getItem(modeLocalStorageKey))
|
||||||
{
|
{
|
||||||
defaultQuickFilterFieldNames = new Set<string>(JSON.parse(localStorage.getItem(quickFilterFieldNamesLocalStorageKey)));
|
defaultMode = localStorage.getItem(modeLocalStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
|
||||||
const [lastFetchedQFilterJSON, setLastFetchedQFilterJSON] = useState("");
|
const [lastFetchedQFilterJSON, setLastFetchedQFilterJSON] = useState("");
|
||||||
|
const [lastFetchedVariant, setLastFetchedVariant] = useState(null);
|
||||||
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
|
||||||
const [queryFilter, setQueryFilter] = useState(new QQueryFilter());
|
const [queryFilter, setQueryFilter] = useState(new QQueryFilter());
|
||||||
const [tableVariant, setTableVariant] = useState(defaultTableVariant);
|
const [tableVariant, setTableVariant] = useState(defaultTableVariant);
|
||||||
@ -254,8 +247,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
||||||
const [gridMouseDownY, setGridMouseDownY] = useState(0);
|
const [gridMouseDownY, setGridMouseDownY] = useState(0);
|
||||||
const [gridPreferencesWindow, setGridPreferencesWindow] = useState(undefined);
|
const [gridPreferencesWindow, setGridPreferencesWindow] = useState(undefined);
|
||||||
const [showClearFiltersWarning, setShowClearFiltersWarning] = useState(false);
|
|
||||||
const [hasValidFilters, setHasValidFilters] = useState(false);
|
|
||||||
const [currentSavedFilter, setCurrentSavedFilter] = useState(null as QRecord);
|
const [currentSavedFilter, setCurrentSavedFilter] = useState(null as QRecord);
|
||||||
|
|
||||||
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
|
||||||
@ -266,9 +257,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const [columnStatsFieldTableName, setColumnStatsFieldTableName] = useState(null as string)
|
const [columnStatsFieldTableName, setColumnStatsFieldTableName] = useState(null as string)
|
||||||
const [filterForColumnStats, setFilterForColumnStats] = useState(null as QQueryFilter);
|
const [filterForColumnStats, setFilterForColumnStats] = useState(null as QQueryFilter);
|
||||||
|
|
||||||
const [addQuickFilterMenu, setAddQuickFilterMenu] = useState(null);
|
const [mode, setMode] = useState(defaultMode);
|
||||||
const [quickFilterFieldNames, setQuickFilterFieldNames] = useState(defaultQuickFilterFieldNames);
|
const basicAndAdvancedQueryControlsRef = useRef();
|
||||||
const [addQuickFilterOpenCounter, setAddQuickFilterOpenCounter] = useState(0);
|
|
||||||
|
|
||||||
const instance = useRef({timer: null});
|
const instance = useRef({timer: null});
|
||||||
|
|
||||||
@ -321,7 +311,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
else if (! e.metaKey && e.key === "r")
|
else if (! e.metaKey && e.key === "r")
|
||||||
{
|
{
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
updateTable();
|
updateTable("'r' keyboard event");
|
||||||
}
|
}
|
||||||
else if (! e.metaKey && e.key === "c")
|
else if (! e.metaKey && e.key === "c")
|
||||||
{
|
{
|
||||||
@ -396,7 +386,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
setCurrentSavedFilter(null);
|
doSetCurrentSavedFilter(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,7 +407,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
const result = processResult as QJobComplete;
|
const result = processResult as QJobComplete;
|
||||||
const qRecord = new QRecord(result.values.savedFilterList[0]);
|
const qRecord = new QRecord(result.values.savedFilterList[0]);
|
||||||
setCurrentSavedFilter(qRecord);
|
doSetCurrentSavedFilter(qRecord);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@ -473,7 +463,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
let filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, limit);
|
let filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, limit);
|
||||||
filter = FilterUtils.convertFilterPossibleValuesToIds(filter);
|
filter = FilterUtils.convertFilterPossibleValuesToIds(filter);
|
||||||
setHasValidFilters(filter.criteria && filter.criteria.length > 0);
|
|
||||||
return (filter);
|
return (filter);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -588,14 +577,15 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<Typography variant="h6" color="text" fontWeight="light">
|
<Typography variant="h6" color="text" fontWeight="light">
|
||||||
{tableMetaData?.variantTableLabel}: {tableVariant?.name}
|
{tableMetaData?.variantTableLabel}: {tableVariant?.name}
|
||||||
<Tooltip title={`Change ${tableMetaData?.variantTableLabel}`}>
|
<Tooltip title={`Change ${tableMetaData?.variantTableLabel}`}>
|
||||||
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'wght' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
|
<IconButton onClick={promptForTableVariantSelection} sx={{p: 0, m: 0, ml: .5, mb: .5, color: "#9f9f9f", fontVariationSettings: "'weight' 100"}}><Icon fontSize="small">settings</Icon></IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTable = () =>
|
const updateTable = (reason?: string) =>
|
||||||
{
|
{
|
||||||
|
console.log(`In updateTable for ${reason}`);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setRows([]);
|
setRows([]);
|
||||||
(async () =>
|
(async () =>
|
||||||
@ -658,14 +648,25 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setColumnSortModel(models.sort);
|
setColumnSortModel(models.sort);
|
||||||
setWarningAlert(models.warning);
|
setWarningAlert(models.warning);
|
||||||
|
|
||||||
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, models.filter, models.sort, rowsPerPage));
|
const newQueryFilter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, models.filter, models.sort, rowsPerPage);
|
||||||
|
setQueryFilter(newQueryFilter);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// this ref may not be defined on the initial render, so, make this call in a timeout //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
basicAndAdvancedQueryControlsRef?.current?.ensureAllFilterCriteriaAreActiveQuickFilters(newQueryFilter, "defaultFilterLoaded")
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTableMetaData(tableMetaData);
|
setTableMetaData(tableMetaData);
|
||||||
setTableLabel(tableMetaData.label);
|
setTableLabel(tableMetaData.label);
|
||||||
|
|
||||||
if(tableMetaData?.usesVariants && ! tableVariant)
|
if (tableMetaData?.usesVariants && !tableVariant)
|
||||||
{
|
{
|
||||||
promptForTableVariantSelection();
|
promptForTableVariantSelection();
|
||||||
return;
|
return;
|
||||||
@ -802,6 +803,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLastFetchedQFilterJSON(JSON.stringify(qFilter));
|
setLastFetchedQFilterJSON(JSON.stringify(qFilter));
|
||||||
|
setLastFetchedVariant(tableVariant);
|
||||||
qController.query(tableName, qFilter, queryJoins, tableVariant).then((results) =>
|
qController.query(tableName, qFilter, queryJoins, tableVariant).then((results) =>
|
||||||
{
|
{
|
||||||
console.log(`Received results for query ${thisQueryId}`);
|
console.log(`Received results for query ${thisQueryId}`);
|
||||||
@ -1021,7 +1023,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
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");
|
console.log("calling update table for visible join table change");
|
||||||
updateTable();
|
updateTable("visible joins change");
|
||||||
setVisibleJoinTables(newVisibleJoinTables);
|
setVisibleJoinTables(newVisibleJoinTables);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1117,111 +1119,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QExportMenuItemProps extends GridExportMenuItemProps<{}>
|
|
||||||
{
|
|
||||||
format: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExportMenuItem(props: QExportMenuItemProps)
|
|
||||||
{
|
|
||||||
const {format, hideMenu} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
disabled={totalRecords === 0}
|
|
||||||
onClick={() =>
|
|
||||||
{
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// build the list of visible fields. note, not doing them in-order (in case //
|
|
||||||
// the user did drag & drop), because column order model isn't right yet //
|
|
||||||
// so just doing them to match columns (which were pKey, then sorted) //
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
const visibleFields: string[] = [];
|
|
||||||
columnsModel.forEach((gridColumn) =>
|
|
||||||
{
|
|
||||||
const fieldName = gridColumn.field;
|
|
||||||
if (columnVisibilityModel[fieldName] !== false)
|
|
||||||
{
|
|
||||||
visibleFields.push(fieldName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
///////////////////////
|
|
||||||
// zero-pad function //
|
|
||||||
///////////////////////
|
|
||||||
const zp = (value: number): string => (value < 10 ? `0${value}` : `${value}`);
|
|
||||||
|
|
||||||
//////////////////////////////////////
|
|
||||||
// construct the url for the export //
|
|
||||||
//////////////////////////////////////
|
|
||||||
const dateString = ValueUtils.formatDateTimeForFileName(new Date());
|
|
||||||
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
|
|
||||||
const url = `/data/${tableMetaData.name}/export/${filename}`;
|
|
||||||
|
|
||||||
const encodedFilterJSON = encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel)));
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// open a window (tab) with a little page that says the file is being generated. //
|
|
||||||
// then have that page load the url for the export. //
|
|
||||||
// If there's an error, it'll appear in that window. else, the file will download. //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
const exportWindow = window.open("", "_blank");
|
|
||||||
exportWindow.document.write(`<html lang="en">
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
* { font-family: "SF Pro Display","Roboto","Helvetica","Arial",sans-serif; }
|
|
||||||
</style>
|
|
||||||
<title>${filename}</title>
|
|
||||||
<script>
|
|
||||||
setTimeout(() =>
|
|
||||||
{
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// need to encode and decode this value, so set it in the form here, instead of literally below //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
document.getElementById("filter").value = decodeURIComponent("${encodedFilterJSON}");
|
|
||||||
|
|
||||||
document.getElementById("exportForm").submit();
|
|
||||||
}, 1);
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
|
||||||
<form id="exportForm" method="post" action="${url}" >
|
|
||||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
|
||||||
<input type="hidden" name="filter" id="filter">
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>`);
|
|
||||||
|
|
||||||
/*
|
|
||||||
// todo - probably better - generate the report in an iframe...
|
|
||||||
// only open question is, giving user immediate feedback, and knowing when the stream has started and/or stopped
|
|
||||||
// maybe a busy-loop that would check iframe's url (e.g., after posting should change, maybe?)
|
|
||||||
const iframe = document.getElementById("exportIFrame");
|
|
||||||
const form = iframe.querySelector("form");
|
|
||||||
form.action = url;
|
|
||||||
form.target = "exportIFrame";
|
|
||||||
(iframe.querySelector("#authorizationInput") as HTMLInputElement).value = qController.getAuthorizationHeaderValue();
|
|
||||||
form.submit();
|
|
||||||
*/
|
|
||||||
|
|
||||||
///////////////////////////////////////////
|
|
||||||
// Hide the export menu after the export //
|
|
||||||
///////////////////////////////////////////
|
|
||||||
hideMenu?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
{` ${format.toUpperCase()}`}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNoOfSelectedRecords()
|
function getNoOfSelectedRecords()
|
||||||
{
|
{
|
||||||
if (selectFullFilterState === "filter")
|
if (selectFullFilterState === "filter")
|
||||||
{
|
{
|
||||||
if(isJoinMany(tableMetaData, getVisibleJoinTables()))
|
if (isJoinMany(tableMetaData, getVisibleJoinTables()))
|
||||||
{
|
{
|
||||||
return (distinctRecords);
|
return (distinctRecords);
|
||||||
}
|
}
|
||||||
@ -1300,8 +1202,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("close modal process");
|
||||||
updateTable();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") =>
|
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") =>
|
||||||
@ -1361,7 +1262,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
more than one record associated with each {tableMetaData?.label}.
|
more than one record associated with each {tableMetaData?.label}.
|
||||||
</>
|
</>
|
||||||
let distinctPart = isJoinMany(tableMetaData, getVisibleJoinTables()) ? (<Box display="inline" component="span" textAlign="right">
|
let distinctPart = isJoinMany(tableMetaData, getVisibleJoinTables()) ? (<Box display="inline" component="span" textAlign="right">
|
||||||
({safeToLocaleString(distinctRecords)} distinct<CustomWidthTooltip title={tooltipHTML}>
|
({ValueUtils.safeToLocaleString(distinctRecords)} distinct<CustomWidthTooltip title={tooltipHTML}>
|
||||||
<IconButton sx={{p: 0, pl: 0.25, mb: 0.25}}><Icon fontSize="small" sx={{fontSize: "1.125rem !important", color: "#9f9f9f"}}>info_outlined</Icon></IconButton>
|
<IconButton sx={{p: 0, pl: 0.25, mb: 0.25}}><Icon fontSize="small" sx={{fontSize: "1.125rem !important", color: "#9f9f9f"}}>info_outlined</Icon></IconButton>
|
||||||
</CustomWidthTooltip>
|
</CustomWidthTooltip>
|
||||||
)
|
)
|
||||||
@ -1431,12 +1332,36 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function doSetCurrentSavedFilter(savedFilter: QRecord)
|
||||||
|
{
|
||||||
|
setCurrentSavedFilter(savedFilter);
|
||||||
|
|
||||||
|
if(savedFilter)
|
||||||
|
{
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
let localTableMetaData = tableMetaData;
|
||||||
|
if(!localTableMetaData)
|
||||||
|
{
|
||||||
|
localTableMetaData = await qController.loadTableMetaData(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = await FilterUtils.determineFilterAndSortModels(qController, localTableMetaData, savedFilter.values.get("filterJson"), null, null, null);
|
||||||
|
const newQueryFilter = FilterUtils.buildQFilterFromGridFilter(localTableMetaData, models.filter, models.sort, rowsPerPage);
|
||||||
|
// todo?? ensureAllFilterCriteriaAreActiveQuickFilters(localTableMetaData, newQueryFilter, "savedFilterSelected")
|
||||||
|
|
||||||
|
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(localTableMetaData, newQueryFilter);
|
||||||
|
handleFilterChange(gridFilterModel, true);
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSavedFilterChange(selectedSavedFilterId: number)
|
async function handleSavedFilterChange(selectedSavedFilterId: number)
|
||||||
{
|
{
|
||||||
if (selectedSavedFilterId != null)
|
if (selectedSavedFilterId != null)
|
||||||
{
|
{
|
||||||
const qRecord = await fetchSavedFilter(selectedSavedFilterId);
|
const qRecord = await fetchSavedFilter(selectedSavedFilterId);
|
||||||
setCurrentSavedFilter(qRecord); // this fixed initial load not showing filter name
|
doSetCurrentSavedFilter(qRecord); // this fixed initial load not showing filter name
|
||||||
|
|
||||||
const models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, qRecord.values.get("filterJson"), null, null, null);
|
const models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, qRecord.values.get("filterJson"), null, null, null);
|
||||||
handleFilterChange(models.filter);
|
handleFilterChange(models.filter);
|
||||||
@ -1533,7 +1458,28 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<GridColumnMenuContainer ref={ref} {...props}>
|
<GridColumnMenuContainer ref={ref} {...props}>
|
||||||
<SortGridMenuItems onClick={hideMenu} column={currentColumn!} />
|
<SortGridMenuItems onClick={hideMenu} column={currentColumn!} />
|
||||||
<GridFilterMenuItem onClick={hideMenu} column={currentColumn!} />
|
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// in advanced mode, use the default GridFilterMenuItem, which punches into the advanced/filter-builder UI //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
mode == "advanced" && <GridFilterMenuItem onClick={hideMenu} column={currentColumn!} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for basic mode, use our own menu item to turn on this field as a quick-filter //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
mode == "basic" && <MenuItem onClick={(e) =>
|
||||||
|
{
|
||||||
|
hideMenu(e);
|
||||||
|
// @ts-ignore !?
|
||||||
|
basicAndAdvancedQueryControlsRef.current.addField(currentColumn.field);
|
||||||
|
}}>
|
||||||
|
Filter (BASIC) TODO edit text
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
|
|
||||||
<HideGridColMenuItem onClick={hideMenu} column={currentColumn!} />
|
<HideGridColMenuItem onClick={hideMenu} column={currentColumn!} />
|
||||||
<GridColumnsMenuItem onClick={hideMenu} column={currentColumn!} />
|
<GridColumnsMenuItem onClick={hideMenu} column={currentColumn!} />
|
||||||
|
|
||||||
@ -1569,16 +1515,37 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CustomColumnHeaderFilterIconButton = forwardRef<any, ColumnHeaderFilterIconButtonProps>(
|
||||||
|
function ColumnHeaderFilterIconButton(props: ColumnHeaderFilterIconButtonProps, ref)
|
||||||
|
{
|
||||||
|
if(mode == "basic")
|
||||||
|
{
|
||||||
|
let showFilter = false;
|
||||||
|
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
||||||
|
{
|
||||||
|
const criteria = queryFilter.criteria[i];
|
||||||
|
if(criteria.fieldName == props.field && criteria.operator)
|
||||||
|
{
|
||||||
|
// todo - test values too right?
|
||||||
|
showFilter = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const safeToLocaleString = (n: Number): string =>
|
if(showFilter)
|
||||||
{
|
{
|
||||||
if(n != null && n != undefined)
|
return (<IconButton size="small" sx={{p: "2px"}} onClick={(event) =>
|
||||||
{
|
{
|
||||||
return (n.toLocaleString());
|
// @ts-ignore !?
|
||||||
|
basicAndAdvancedQueryControlsRef.current.addField(props.field);
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
}}><Icon fontSize="small">filter_alt</Icon></IconButton>);
|
||||||
}
|
}
|
||||||
return ("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (<></>);
|
||||||
|
});
|
||||||
|
|
||||||
function CustomToolbar()
|
function CustomToolbar()
|
||||||
{
|
{
|
||||||
const handleMouseDown: GridEventListener<"cellMouseDown"> = (
|
const handleMouseDown: GridEventListener<"cellMouseDown"> = (
|
||||||
@ -1614,9 +1581,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const joinIsMany = isJoinMany(tableMetaData, visibleJoinTables);
|
const joinIsMany = isJoinMany(tableMetaData, visibleJoinTables);
|
||||||
|
|
||||||
const selectionMenuOptions: string[] = [];
|
const selectionMenuOptions: string[] = [];
|
||||||
selectionMenuOptions.push(`This page (${safeToLocaleString(distinctRecordsOnPageCount)} ${joinIsMany ? "distinct " : ""}record${distinctRecordsOnPageCount == 1 ? "" : "s"})`);
|
selectionMenuOptions.push(`This page (${ValueUtils.safeToLocaleString(distinctRecordsOnPageCount)} ${joinIsMany ? "distinct " : ""}record${distinctRecordsOnPageCount == 1 ? "" : "s"})`);
|
||||||
selectionMenuOptions.push(`Full query result (${joinIsMany ? safeToLocaleString(distinctRecords) + ` distinct record${distinctRecords == 1 ? "" : "s"}` : safeToLocaleString(totalRecords) + ` record${totalRecords == 1 ? "" : "s"}`})`);
|
selectionMenuOptions.push(`Full query result (${joinIsMany ? ValueUtils.safeToLocaleString(distinctRecords) + ` distinct record${distinctRecords == 1 ? "" : "s"}` : ValueUtils.safeToLocaleString(totalRecords) + ` record${totalRecords == 1 ? "" : "s"}`})`);
|
||||||
selectionMenuOptions.push(`Subset of the query result ${selectionSubsetSize ? `(${safeToLocaleString(selectionSubsetSize)} ${joinIsMany ? "distinct " : ""}record${selectionSubsetSize == 1 ? "" : "s"})` : "..."}`);
|
selectionMenuOptions.push(`Subset of the query result ${selectionSubsetSize ? `(${ValueUtils.safeToLocaleString(selectionSubsetSize)} ${joinIsMany ? "distinct " : ""}record${selectionSubsetSize == 1 ? "" : "s"})` : "..."}`);
|
||||||
selectionMenuOptions.push("Clear selection");
|
selectionMenuOptions.push("Clear selection");
|
||||||
|
|
||||||
function programmaticallySelectSomeOrAllRows(max?: number)
|
function programmaticallySelectSomeOrAllRows(max?: number)
|
||||||
@ -1631,11 +1598,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
rows.forEach((value: GridRowModel, index: number) =>
|
rows.forEach((value: GridRowModel, index: number) =>
|
||||||
{
|
{
|
||||||
const primaryKeyValue = latestQueryResults[index].values.get(tableMetaData.primaryKeyField);
|
const primaryKeyValue = latestQueryResults[index].values.get(tableMetaData.primaryKeyField);
|
||||||
if(max)
|
if (max)
|
||||||
{
|
{
|
||||||
if(selectedPrimaryKeys.size < max)
|
if (selectedPrimaryKeys.size < max)
|
||||||
{
|
{
|
||||||
if(!selectedPrimaryKeys.has(primaryKeyValue))
|
if (!selectedPrimaryKeys.has(primaryKeyValue))
|
||||||
{
|
{
|
||||||
rowSelectionModel.push(value.__rowIndex);
|
rowSelectionModel.push(value.__rowIndex);
|
||||||
selectedPrimaryKeys.add(primaryKeyValue as string);
|
selectedPrimaryKeys.add(primaryKeyValue as string);
|
||||||
@ -1676,58 +1643,37 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const doClearFilter = (event: React.KeyboardEvent<HTMLDivElement>, isYesButton: boolean = false) =>
|
const exportMenuItemRestProps =
|
||||||
{
|
{
|
||||||
if (isYesButton|| event.key == "Enter")
|
tableMetaData: tableMetaData,
|
||||||
{
|
totalRecords: totalRecords,
|
||||||
setShowClearFiltersWarning(false);
|
columnsModel: columnsModel,
|
||||||
handleFilterChange({items: []} as GridFilterModel);
|
columnVisibilityModel: columnVisibilityModel,
|
||||||
}
|
queryFilter: queryFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridToolbarContainer>
|
<GridToolbarContainer>
|
||||||
<div>
|
<div>
|
||||||
<Button id="refresh-button" onClick={updateTable} startIcon={<Icon>refresh</Icon>} sx={{pr: "1.25rem"}}>
|
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pr: "1.25rem"}}>
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<GridToolbarColumnsButton nonce={undefined} />
|
<GridToolbarColumnsButton nonce={undefined} />
|
||||||
<div style={{position: "relative"}}>
|
<div style={{position: "relative"}}>
|
||||||
{/* @ts-ignore */}
|
|
||||||
<GridToolbarFilterButton nonce={undefined} />
|
|
||||||
{
|
|
||||||
hasValidFilters && (
|
|
||||||
<div id="clearFiltersButton" style={{display: "inline-block", position: "relative", top: "2px", left: "-0.75rem", width: "1rem"}}>
|
|
||||||
<Tooltip title="Clear Filter">
|
|
||||||
<Icon sx={{cursor: "pointer"}} onClick={() => setShowClearFiltersWarning(true)}>clear</Icon>
|
|
||||||
</Tooltip>
|
|
||||||
<Dialog open={showClearFiltersWarning} onClose={() => setShowClearFiltersWarning(false)} onKeyPress={(e) => doClearFilter(e)}>
|
|
||||||
<DialogTitle id="alert-dialog-title">Confirm</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>Are you sure you want to remove all conditions from the current filter?</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<QCancelButton label="No" disabled={false} onClickHandler={() => setShowClearFiltersWarning(false)} />
|
|
||||||
<QSaveButton label="Yes" iconName="check" disabled={false} onClickHandler={() => doClearFilter(null, true)}/>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<GridToolbarDensitySelector nonce={undefined} />
|
<GridToolbarDensitySelector nonce={undefined} />
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<GridToolbarExportContainer nonce={undefined}>
|
<GridToolbarExportContainer nonce={undefined}>
|
||||||
<ExportMenuItem format="csv" />
|
<ExportMenuItem format="csv" {...exportMenuItemRestProps} />
|
||||||
<ExportMenuItem format="xlsx" />
|
<ExportMenuItem format="xlsx" {...exportMenuItemRestProps} />
|
||||||
<ExportMenuItem format="json" />
|
<ExportMenuItem format="json" {...exportMenuItemRestProps} />
|
||||||
</GridToolbarExportContainer>
|
</GridToolbarExportContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{zIndex: 10}}>
|
<div style={{zIndex: 10}}>
|
||||||
<MenuButton label="Selection" iconName={selectedIds.length == 0 ? "check_box_outline_blank" : "check_box"} disabled={totalRecords == 0} options={selectionMenuOptions} callback={selectionMenuCallback}/>
|
<MenuButton label="Selection" iconName={selectedIds.length == 0 ? "check_box_outline_blank" : "check_box"} disabled={totalRecords == 0} options={selectionMenuOptions} callback={selectionMenuCallback} />
|
||||||
<SelectionSubsetDialog isOpen={selectionSubsetSizePromptOpen} initialValue={selectionSubsetSize} closeHandler={(value) =>
|
<SelectionSubsetDialog isOpen={selectionSubsetSizePromptOpen} initialValue={selectionSubsetSize} closeHandler={(value) =>
|
||||||
{
|
{
|
||||||
setSelectionSubsetSizePromptOpen(false);
|
setSelectionSubsetSizePromptOpen(false);
|
||||||
@ -1778,14 +1724,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
selectFullFilterState === "filterSubset" && (
|
selectFullFilterState === "filterSubset" && (
|
||||||
<div className="selectionTool">
|
<div className="selectionTool">
|
||||||
The <a onClick={() => setSelectionSubsetSizePromptOpen(true)} style={{cursor: "pointer"}}><strong>first {safeToLocaleString(selectionSubsetSize)}</strong></a> {joinIsMany ? "distinct" : ""} record{selectionSubsetSize == 1 ? "" : "s"} matching this query {selectionSubsetSize == 1 ? "is" : "are"} selected.
|
The <a onClick={() => setSelectionSubsetSizePromptOpen(true)} style={{cursor: "pointer"}}><strong>first {ValueUtils.safeToLocaleString(selectionSubsetSize)}</strong></a> {joinIsMany ? "distinct" : ""} record{selectionSubsetSize == 1 ? "" : "s"} matching this query {selectionSubsetSize == 1 ? "is" : "are"} selected.
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
(selectFullFilterState === "n/a" && selectedIds.length > 0) && (
|
(selectFullFilterState === "n/a" && selectedIds.length > 0) && (
|
||||||
<div className="selectionTool">
|
<div className="selectionTool">
|
||||||
<strong>{safeToLocaleString(selectedIds.length)}</strong> {joinIsMany ? "distinct" : ""} {selectedIds.length == 1 ? "record is" : "records are"} selected.
|
<strong>{ValueUtils.safeToLocaleString(selectedIds.length)}</strong> {joinIsMany ? "distinct" : ""} {selectedIds.length == 1 ? "record is" : "records are"} selected.
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1875,34 +1821,27 @@ 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 //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
updateTable();
|
updateTable("useEffect(pageNumber,rowsPerPage,columnSortModel,currentSavedFilter)");
|
||||||
}
|
}
|
||||||
}, [pageNumber, rowsPerPage, columnSortModel, currentSavedFilter]);
|
}, [pageNumber, rowsPerPage, columnSortModel, currentSavedFilter]);
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// for state changes that DO change the filter, call to update the table - and DO clear out the totalRecords //
|
// for state changes that DO change the filter, call to update the table - and DO clear out the totalRecords //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
setTotalRecords(null);
|
|
||||||
setDistinctRecords(null);
|
|
||||||
updateTable();
|
|
||||||
}, [columnsModel, tableState, tableVariant]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
const currentQFilter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage);
|
const currentQFilter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage);
|
||||||
currentQFilter.skip = pageNumber * rowsPerPage;
|
currentQFilter.skip = pageNumber * rowsPerPage;
|
||||||
const currentQFilterJSON = JSON.stringify(currentQFilter);
|
const currentQFilterJSON = JSON.stringify(currentQFilter);
|
||||||
|
const currentVariantJSON = JSON.stringify(tableVariant);
|
||||||
|
|
||||||
if(currentQFilterJSON !== lastFetchedQFilterJSON)
|
if(currentQFilterJSON !== lastFetchedQFilterJSON || currentVariantJSON !== lastFetchedVariant)
|
||||||
{
|
{
|
||||||
setTotalRecords(null);
|
setTotalRecords(null);
|
||||||
setDistinctRecords(null);
|
setDistinctRecords(null);
|
||||||
updateTable();
|
updateTable("useEffect(filterModel)");
|
||||||
}
|
}
|
||||||
|
}, [filterModel, columnsModel, tableState, tableVariant]);
|
||||||
}, [filterModel]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
@ -1963,141 +1902,33 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateQuickCriteria = (newCriteria: QFilterCriteria, needDebounce = false, doClearCriteria = false) =>
|
const doSetMode = (newValue: string) =>
|
||||||
{
|
{
|
||||||
let found = false;
|
setMode(newValue);
|
||||||
let foundIndex = null;
|
localStorage.setItem(modeLocalStorageKey, newValue);
|
||||||
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
|
||||||
{
|
|
||||||
if(queryFilter.criteria[i].fieldName == newCriteria.fieldName)
|
|
||||||
{
|
|
||||||
queryFilter.criteria[i] = newCriteria;
|
|
||||||
found = true;
|
|
||||||
foundIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(doClearCriteria)
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for basic mode, set a custom ColumnHeaderFilterIconButton - w/ action to activate basic-mode quick-filter //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let restOfDataGridProCustomComponents: any = {}
|
||||||
|
if(mode == "basic")
|
||||||
{
|
{
|
||||||
if(found)
|
restOfDataGridProCustomComponents.ColumnHeaderFilterIconButton = CustomColumnHeaderFilterIconButton;
|
||||||
{
|
|
||||||
queryFilter.criteria.splice(foundIndex, 1);
|
|
||||||
setQueryFilter(queryFilter);
|
|
||||||
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter);
|
|
||||||
handleFilterChange(gridFilterModel, false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!found)
|
|
||||||
{
|
|
||||||
if(!queryFilter.criteria)
|
|
||||||
{
|
|
||||||
queryFilter.criteria = [];
|
|
||||||
}
|
|
||||||
queryFilter.criteria.push(newCriteria);
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(found)
|
|
||||||
{
|
|
||||||
clearTimeout(debounceTimeout)
|
|
||||||
debounceTimeout = setTimeout(() =>
|
|
||||||
{
|
|
||||||
setQueryFilter(queryFilter);
|
|
||||||
const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter);
|
|
||||||
handleFilterChange(gridFilterModel, false);
|
|
||||||
}, needDebounce ? 500 : 1);
|
|
||||||
|
|
||||||
forceUpdate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const getQuickCriteriaParam = (fieldName: string): QFilterCriteriaWithId | null | "tooComplex" =>
|
|
||||||
{
|
|
||||||
const matches: QFilterCriteriaWithId[] = [];
|
|
||||||
for (let i = 0; i < queryFilter?.criteria?.length; i++)
|
|
||||||
{
|
|
||||||
if(queryFilter.criteria[i].fieldName == fieldName)
|
|
||||||
{
|
|
||||||
matches.push(queryFilter.criteria[i] as QFilterCriteriaWithId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(matches.length == 0)
|
|
||||||
{
|
|
||||||
return (null);
|
|
||||||
}
|
|
||||||
else if(matches.length == 1)
|
|
||||||
{
|
|
||||||
return (matches[0]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return "tooComplex";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleQuickFilterField = (fieldName: string): void =>
|
|
||||||
{
|
|
||||||
if(quickFilterFieldNames.has(fieldName))
|
|
||||||
{
|
|
||||||
quickFilterFieldNames.delete(fieldName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
quickFilterFieldNames.add(fieldName);
|
|
||||||
}
|
|
||||||
setQuickFilterFieldNames(new Set<string>([...quickFilterFieldNames.values()]))
|
|
||||||
localStorage.setItem(quickFilterFieldNamesLocalStorageKey, JSON.stringify([...quickFilterFieldNames.values()]));
|
|
||||||
|
|
||||||
// damnit, not auto-updating in the filter panel... have to click twice most of the time w/o this hacky hack.
|
|
||||||
setTimeout(() => forceUpdate(), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
const openAddQuickFilterMenu = (event: any) =>
|
|
||||||
{
|
|
||||||
setAddQuickFilterMenu(event.currentTarget);
|
|
||||||
setAddQuickFilterOpenCounter(addQuickFilterOpenCounter + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeAddQuickFilterMenu = () =>
|
|
||||||
{
|
|
||||||
setAddQuickFilterMenu(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addQuickFilterField(event: any, newValue: any, reason: string)
|
|
||||||
{
|
|
||||||
if(reason == "blur")
|
|
||||||
{
|
|
||||||
//////////////////////////////////////////////////////////////////
|
|
||||||
// this keeps a click out of the menu from selecting the option //
|
|
||||||
//////////////////////////////////////////////////////////////////
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldName = newValue ? newValue.fieldName : null
|
|
||||||
if(fieldName)
|
|
||||||
{
|
|
||||||
toggleQuickFilterField(fieldName);
|
|
||||||
closeAddQuickFilterMenu();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<div className="recordQuery">
|
<div className="recordQuery">
|
||||||
{/*
|
{/*
|
||||||
// see above code that would use this
|
// see code in ExportMenuItem that would use this
|
||||||
<iframe id="exportIFrame" name="exportIFrame">
|
<iframe id="exportIFrame" name="exportIFrame">
|
||||||
<form method="post" target="_self">
|
<form method="post" target="_self">
|
||||||
<input type="hidden" id="authorizationInput" name="Authorization" />
|
<input type="hidden" id="authorizationInput" name="Authorization" />
|
||||||
</form>
|
</form>
|
||||||
</iframe>
|
</iframe>
|
||||||
*/}
|
*/}
|
||||||
<Box my={3}>
|
<Box mb={3}>
|
||||||
{alertContent ? (
|
{alertContent ? (
|
||||||
<Box mb={3}>
|
<Box mb={3}>
|
||||||
<Alert
|
<Alert
|
||||||
@ -2151,76 +1982,21 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box display="flex" alignItems="center" flexWrap="wrap" position="relative" top={"-0.5rem"} left={"0.5rem"} minHeight="2.5rem">
|
|
||||||
<Tooltip enterDelay={1000} title={
|
|
||||||
<Box textAlign="left" width="200px">
|
|
||||||
Fields that you frequently use for filter conditions can be added here for quick access.<br /><br />
|
|
||||||
Use the
|
|
||||||
<Icon fontSize="medium" sx={{position: "relative", top: "0.25rem", fontSize: "1rem", mx: "0.25rem"}}>add_circle_outline</Icon>
|
|
||||||
button to add a Quick Filter field.<br /><br />
|
|
||||||
To remove a Quick Filter field, click the field name, and then use the
|
|
||||||
<Icon fontSize="medium" sx={{position: "relative", top: "0.25rem", fontSize: "1rem", mx: "0.25rem"}}>highlight_off</Icon>
|
|
||||||
button.
|
|
||||||
</Box>} placement="left">
|
|
||||||
<Typography variant="h6" sx={{cursor: "default"}}>Quick Filter:</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
{
|
{
|
||||||
metaData && tableMetaData &&
|
metaData && tableMetaData &&
|
||||||
<>
|
<BasicAndAdvancedQueryControls
|
||||||
<Tooltip enterDelay={500} title="Add a Quick Filter field" placement="top">
|
ref={basicAndAdvancedQueryControlsRef}
|
||||||
<IconButton onClick={(e) => openAddQuickFilterMenu(e)} size="small" disableRipple><Icon>add_circle_outline</Icon></IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Menu
|
|
||||||
anchorEl={addQuickFilterMenu}
|
|
||||||
anchorOrigin={{vertical: "bottom", horizontal: "center"}}
|
|
||||||
transformOrigin={{vertical: "top", horizontal: "left"}}
|
|
||||||
open={Boolean(addQuickFilterMenu)}
|
|
||||||
onClose={closeAddQuickFilterMenu}
|
|
||||||
keepMounted
|
|
||||||
>
|
|
||||||
<Box width="250px">
|
|
||||||
<FieldAutoComplete
|
|
||||||
key={addQuickFilterOpenCounter} // use a unique key each time we open it, because we don't want the user's last selection to stick.
|
|
||||||
id={"add-quick-filter-field"}
|
|
||||||
metaData={metaData}
|
metaData={metaData}
|
||||||
tableMetaData={tableMetaData}
|
tableMetaData={tableMetaData}
|
||||||
defaultValue={null}
|
queryFilter={queryFilter}
|
||||||
handleFieldChange={addQuickFilterField}
|
gridApiRef={gridApiRef}
|
||||||
autoFocus={true}
|
setQueryFilter={setQueryFilter}
|
||||||
hiddenFieldNames={[...quickFilterFieldNames.values()]}
|
handleFilterChange={handleFilterChange}
|
||||||
|
queryFilterJSON={JSON.stringify(queryFilter)}
|
||||||
|
mode={mode}
|
||||||
|
setMode={doSetMode}
|
||||||
/>
|
/>
|
||||||
</Box>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
{
|
|
||||||
tableMetaData &&
|
|
||||||
[...quickFilterFieldNames.values()].map((fieldName) =>
|
|
||||||
{
|
|
||||||
// todo - join fields...
|
|
||||||
// todo - sometimes i want contains (client.name, for example...)
|
|
||||||
|
|
||||||
const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
|
|
||||||
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS
|
|
||||||
if(field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE)
|
|
||||||
{
|
|
||||||
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
field && <QuickFilter
|
|
||||||
key={fieldName}
|
|
||||||
fullFieldName={fieldName}
|
|
||||||
tableMetaData={tableMetaData}
|
|
||||||
updateCriteria={updateQuickCriteria}
|
|
||||||
criteriaParam={getQuickCriteriaParam(fieldName)}
|
|
||||||
fieldMetaData={field}
|
|
||||||
defaultOperator={defaultOperator}
|
|
||||||
toggleQuickFilterField={toggleQuickFilterField} />
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Box height="100%">
|
<Box height="100%">
|
||||||
@ -2232,7 +2008,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
LoadingOverlay: Loading,
|
LoadingOverlay: Loading,
|
||||||
ColumnMenu: CustomColumnMenu,
|
ColumnMenu: CustomColumnMenu,
|
||||||
ColumnsPanel: CustomColumnsPanel,
|
ColumnsPanel: CustomColumnsPanel,
|
||||||
FilterPanel: CustomFilterPanel
|
FilterPanel: CustomFilterPanel,
|
||||||
|
... restOfDataGridProCustomComponents
|
||||||
}}
|
}}
|
||||||
componentsProps={{
|
componentsProps={{
|
||||||
columnsPanel:
|
columnsPanel:
|
||||||
@ -2250,9 +2027,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
metaData: metaData,
|
metaData: metaData,
|
||||||
queryFilter: queryFilter,
|
queryFilter: queryFilter,
|
||||||
updateFilter: updateFilterFromFilterPanel,
|
updateFilter: updateFilterFromFilterPanel,
|
||||||
quickFilterFieldNames: quickFilterFieldNames,
|
|
||||||
showQuickFilterPin: true,
|
|
||||||
toggleQuickFilterField: toggleQuickFilterField,
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
localeText={{
|
localeText={{
|
||||||
@ -2296,7 +2070,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
getRowId={(row) => row.__rowIndex}
|
getRowId={(row) => row.__rowIndex}
|
||||||
selectionModel={rowSelectionModel}
|
selectionModel={rowSelectionModel}
|
||||||
hideFooterSelectedRowCount={true}
|
hideFooterSelectedRowCount={true}
|
||||||
sx={{border: 0, height: "calc(100vh - 250px)"}}
|
sx={{border: 0, height: tableMetaData?.usesVariants ? "calc(100vh - 300px)" : "calc(100vh - 270px)"}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
@ -2342,136 +2116,4 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// mini-component that is the dialog for the user to select a variant on tables with variant backends //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function TableVariantDialog(props: {isOpen: boolean; table: QTableMetaData; closeHandler: (value?: QTableVariant) => void})
|
|
||||||
{
|
|
||||||
const [value, setValue] = useState(null)
|
|
||||||
const [dropDownOpen, setDropDownOpen] = useState(false)
|
|
||||||
const [variants, setVariants] = useState(null);
|
|
||||||
|
|
||||||
const handleVariantChange = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
|
||||||
{
|
|
||||||
const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${props.table.name}`;
|
|
||||||
if(value != null)
|
|
||||||
{
|
|
||||||
localStorage.setItem(tableVariantLocalStorageKey, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
localStorage.removeItem(tableVariantLocalStorageKey);
|
|
||||||
}
|
|
||||||
props.closeHandler(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
|
||||||
{
|
|
||||||
if(e.key == "Enter" && value)
|
|
||||||
{
|
|
||||||
props.closeHandler(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
console.log("queryVariants")
|
|
||||||
try
|
|
||||||
{
|
|
||||||
(async () =>
|
|
||||||
{
|
|
||||||
const variants = await qController.tableVariants(props.table.name);
|
|
||||||
console.log(JSON.stringify(variants));
|
|
||||||
setVariants(variants);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
return variants && (
|
|
||||||
<Dialog open={props.isOpen} onKeyPress={(e) => keyPressed(e)}>
|
|
||||||
<DialogTitle>{props.table.variantTableLabel}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>Select the {props.table.variantTableLabel} to be used on this table:</DialogContentText>
|
|
||||||
<Autocomplete
|
|
||||||
id="tableVariantId"
|
|
||||||
sx={{width: "400px", marginTop: "10px"}}
|
|
||||||
open={dropDownOpen}
|
|
||||||
size="small"
|
|
||||||
onOpen={() =>
|
|
||||||
{
|
|
||||||
setDropDownOpen(true);
|
|
||||||
}}
|
|
||||||
onClose={() =>
|
|
||||||
{
|
|
||||||
setDropDownOpen(false);
|
|
||||||
}}
|
|
||||||
// @ts-ignore
|
|
||||||
onChange={handleVariantChange}
|
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
||||||
options={variants}
|
|
||||||
renderInput={(params) => <TextField {...params} label={props.table.variantTableLabel} />}
|
|
||||||
getOptionLabel={(option) =>
|
|
||||||
{
|
|
||||||
if(typeof option == "object")
|
|
||||||
{
|
|
||||||
return (option as QTableVariant).name;
|
|
||||||
}
|
|
||||||
return option;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// mini-component that is the dialog for the user to enter the selection-subset //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
|
||||||
function SelectionSubsetDialog(props: {isOpen: boolean; initialValue: number; closeHandler: (value?: number) => void})
|
|
||||||
{
|
|
||||||
const [value, setValue] = useState(props.initialValue)
|
|
||||||
|
|
||||||
const handleChange = (newValue: string) =>
|
|
||||||
{
|
|
||||||
setValue(parseInt(newValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyPressed = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
|
||||||
{
|
|
||||||
if(e.key == "Enter" && value)
|
|
||||||
{
|
|
||||||
props.closeHandler(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={props.isOpen} onClose={() => props.closeHandler()} onKeyPress={(e) => keyPressed(e)}>
|
|
||||||
<DialogTitle>Subset of the Query Result</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>How many records do you want to select?</DialogContentText>
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
name="selection-subset-size"
|
|
||||||
inputProps={{width: "100%", type: "number", min: 1}}
|
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
|
||||||
value={value}
|
|
||||||
sx={{width: "100%"}}
|
|
||||||
onFocus={event => event.target.select()}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<QCancelButton disabled={false} onClickHandler={() => props.closeHandler()} />
|
|
||||||
<QSaveButton label="OK" iconName="check" disabled={value == undefined || isNaN(value)} onClickHandler={() => props.closeHandler(value)} />
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default RecordQuery;
|
export default RecordQuery;
|
||||||
|
@ -424,6 +424,12 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
top: -60px !important;
|
top: -60px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MuiDataGrid-panel:has(.customFilterPanel)
|
||||||
|
{
|
||||||
|
/* overwrite what the grid tries to do here, where it changes based on density... we always want the same. */
|
||||||
|
transform: translate(274px, 305px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* within the data-grid, the filter-panel's container has a max-height. mirror that, and set an overflow-y */
|
/* within the data-grid, the filter-panel's container has a max-height. mirror that, and set an overflow-y */
|
||||||
.MuiDataGrid-panel .customFilterPanel
|
.MuiDataGrid-panel .customFilterPanel
|
||||||
{
|
{
|
||||||
|
@ -358,9 +358,9 @@ class FilterUtils
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (fieldType === QFieldType.DATE_TIME)
|
if (fieldType === QFieldType.DATE_TIME)
|
||||||
{
|
{
|
||||||
for(let i = 0; i<values.length; i++)
|
for (let i = 0; i < values.length; i++)
|
||||||
{
|
{
|
||||||
if(!values[i].type)
|
if (!values[i].type)
|
||||||
{
|
{
|
||||||
values[i] = ValueUtils.formatDateTimeValueForForm(values[i]);
|
values[i] = ValueUtils.formatDateTimeValueForForm(values[i]);
|
||||||
}
|
}
|
||||||
@ -402,7 +402,7 @@ class FilterUtils
|
|||||||
if (field == null)
|
if (field == null)
|
||||||
{
|
{
|
||||||
console.log("Couldn't find field for filter: " + criteria.fieldName);
|
console.log("Couldn't find field for filter: " + criteria.fieldName);
|
||||||
warningParts.push("Your filter contained an unrecognized field name: " + criteria.fieldName)
|
warningParts.push("Your filter contained an unrecognized field name: " + criteria.fieldName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,11 +432,11 @@ class FilterUtils
|
|||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
// replace objects that look like expressions with expression instances //
|
// replace objects that look like expressions with expression instances //
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
if(values && values.length)
|
if (values && values.length)
|
||||||
{
|
{
|
||||||
for (let i = 0; i < values.length; i++)
|
for (let i = 0; i < values.length; i++)
|
||||||
{
|
{
|
||||||
const expression = this.gridCriteriaValueToExpression(values[i])
|
const expression = this.gridCriteriaValueToExpression(values[i]);
|
||||||
if (expression)
|
if (expression)
|
||||||
{
|
{
|
||||||
values[i] = expression;
|
values[i] = expression;
|
||||||
@ -508,16 +508,16 @@ class FilterUtils
|
|||||||
// if any values in the items are objects, but should be expression instances, //
|
// if any values in the items are objects, but should be expression instances, //
|
||||||
// then convert & replace them. //
|
// then convert & replace them. //
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
if(defaultFilter && defaultFilter.items && defaultFilter.items.length)
|
if (defaultFilter && defaultFilter.items && defaultFilter.items.length)
|
||||||
{
|
{
|
||||||
defaultFilter.items.forEach((item) =>
|
defaultFilter.items.forEach((item) =>
|
||||||
{
|
{
|
||||||
if(item.value && item.value.length)
|
if (item.value && item.value.length)
|
||||||
{
|
{
|
||||||
for (let i = 0; i < item.value.length; i++)
|
for (let i = 0; i < item.value.length; i++)
|
||||||
{
|
{
|
||||||
const expression = this.gridCriteriaValueToExpression(item.value[i])
|
const expression = this.gridCriteriaValueToExpression(item.value[i]);
|
||||||
if(expression)
|
if (expression)
|
||||||
{
|
{
|
||||||
item.value[i] = expression;
|
item.value[i] = expression;
|
||||||
}
|
}
|
||||||
@ -525,8 +525,8 @@ class FilterUtils
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
const expression = this.gridCriteriaValueToExpression(item.value)
|
const expression = this.gridCriteriaValueToExpression(item.value);
|
||||||
if(expression)
|
if (expression)
|
||||||
{
|
{
|
||||||
item.value = expression;
|
item.value = expression;
|
||||||
}
|
}
|
||||||
@ -641,7 +641,7 @@ class FilterUtils
|
|||||||
let incomplete = false;
|
let incomplete = false;
|
||||||
if (item.operatorValue === "between" || item.operatorValue === "notBetween")
|
if (item.operatorValue === "between" || item.operatorValue === "notBetween")
|
||||||
{
|
{
|
||||||
if(!item.value || !item.value.length || item.value.length < 2 || this.isUnset(item.value[0]) || this.isUnset(item.value[1]))
|
if (!item.value || !item.value.length || item.value.length < 2 || this.isUnset(item.value[0]) || this.isUnset(item.value[1]))
|
||||||
{
|
{
|
||||||
incomplete = true;
|
incomplete = true;
|
||||||
}
|
}
|
||||||
@ -747,6 +747,103 @@ class FilterUtils
|
|||||||
return (filter);
|
return (filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static canFilterWorkAsBasic(tableMetaData: QTableMetaData, filter: QQueryFilter): { canFilterWorkAsBasic: boolean; reasonsWhyItCannot?: string[] }
|
||||||
|
{
|
||||||
|
const reasonsWhyItCannot: string[] = [];
|
||||||
|
|
||||||
|
if(filter == null)
|
||||||
|
{
|
||||||
|
return ({canFilterWorkAsBasic: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filter.booleanOperator == "OR")
|
||||||
|
{
|
||||||
|
reasonsWhyItCannot.push("Filter uses the 'OR' operator.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filter.criteria)
|
||||||
|
{
|
||||||
|
const usedFields: {[name: string]: boolean} = {};
|
||||||
|
const warnedFields: {[name: string]: boolean} = {};
|
||||||
|
for (let i = 0; i < filter.criteria.length; i++)
|
||||||
|
{
|
||||||
|
const criteriaName = filter.criteria[i].fieldName;
|
||||||
|
if(!criteriaName)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(usedFields[criteriaName])
|
||||||
|
{
|
||||||
|
if(!warnedFields[criteriaName])
|
||||||
|
{
|
||||||
|
const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, criteriaName);
|
||||||
|
let fieldLabel = field.label;
|
||||||
|
if(tableForField.name != tableMetaData.name)
|
||||||
|
{
|
||||||
|
let fieldLabel = `${tableForField.label}: ${field.label}`;
|
||||||
|
}
|
||||||
|
reasonsWhyItCannot.push(`Filter contains more than 1 condition for the field: ${fieldLabel}`);
|
||||||
|
warnedFields[criteriaName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usedFields[criteriaName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(reasonsWhyItCannot.length == 0)
|
||||||
|
{
|
||||||
|
return ({canFilterWorkAsBasic: true});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ({canFilterWorkAsBasic: false, reasonsWhyItCannot: reasonsWhyItCannot});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** get the values associated with a criteria as a string, e.g., for showing
|
||||||
|
** in a tooltip.
|
||||||
|
*******************************************************************************/
|
||||||
|
public static getValuesString(fieldMetaData: QFieldMetaData, criteria: QFilterCriteria, maxValuesToShow: number = 3): string
|
||||||
|
{
|
||||||
|
let valuesString = "";
|
||||||
|
if (criteria.values && criteria.values.length && fieldMetaData.type !== QFieldType.BOOLEAN)
|
||||||
|
{
|
||||||
|
let labels = [] as string[];
|
||||||
|
|
||||||
|
let maxLoops = criteria.values.length;
|
||||||
|
if (maxLoops > (maxValuesToShow + 2))
|
||||||
|
{
|
||||||
|
maxLoops = maxValuesToShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLoops; i++)
|
||||||
|
{
|
||||||
|
if (criteria.values[i] && criteria.values[i].label)
|
||||||
|
{
|
||||||
|
labels.push(criteria.values[i].label);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
labels.push(criteria.values[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxLoops < criteria.values.length)
|
||||||
|
{
|
||||||
|
labels.push(" and " + (criteria.values.length - maxLoops) + " other values.");
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesString = (labels.join(", "));
|
||||||
|
}
|
||||||
|
return valuesString;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilterUtils;
|
export default FilterUtils;
|
||||||
|
@ -462,6 +462,19 @@ class ValueUtils
|
|||||||
|
|
||||||
return (String(param).replaceAll(/"/g, "\"\""));
|
return (String(param).replaceAll(/"/g, "\"\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static safeToLocaleString(n: Number): string
|
||||||
|
{
|
||||||
|
if (n != null && n != undefined)
|
||||||
|
{
|
||||||
|
return (n.toLocaleString());
|
||||||
|
}
|
||||||
|
return ("");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Reference in New Issue
Block a user