Merged feature/workflows-support into integration

This commit is contained in:
2025-07-18 11:11:15 -05:00
13 changed files with 199 additions and 120 deletions

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.123", "@kingsrook/qqq-frontend-core": "1.0.124",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",
@ -35,8 +35,8 @@
"html-react-parser": "1.4.8", "html-react-parser": "1.4.8",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.6",
"lodash": "4.17.21",
"jwt-decode": "3.1.2", "jwt-decode": "3.1.2",
"lodash": "4.17.21",
"oidc-client-ts": "2.4.1", "oidc-client-ts": "2.4.1",
"react-oidc-context": "2.3.1", "react-oidc-context": "2.3.1",
"rapidoc": "9.3.4", "rapidoc": "9.3.4",

View File

@ -34,6 +34,8 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@ -64,6 +66,7 @@ public class RunFormAdjusterProcess implements BackendStep, MetaDataProducerInte
{ {
return new QProcessMetaData() return new QProcessMetaData()
.withName(NAME) .withName(NAME)
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.withStep(new QBackendStepMetaData() .withStep(new QBackendStepMetaData()
.withName("execute") .withName("execute")
.withCode(new QCodeReference(getClass()))); .withCode(new QCodeReference(getClass())));

View File

@ -191,6 +191,11 @@ class DynamicFormUtils
props.possibleValueSourceName = field.possibleValueSourceName; props.possibleValueSourceName = field.possibleValueSourceName;
} }
if(field.possibleValueSourceFilter)
{
props.possibleValueSourceFilter = field.possibleValueSourceFilter;
}
dynamicFormFields[field.name].possibleValueProps = props; dynamicFormFields[field.name].possibleValueProps = props;
} }
} }

View File

@ -188,7 +188,16 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, name, fieldLabel, i
} }
else else
{ {
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, null, otherValues, useCase); return await qController.possibleValues(
{
tableName,
processName,
fieldNameOrPossibleValueSourceName: possibleValueSourceName ?? fieldName,
searchTerm: searchTerm ?? "",
values: otherValues,
useCase,
possibleValueSourceFilter: fieldPossibleValueProps.possibleValueSourceFilter
});
} }
}; };

View File

@ -20,6 +20,7 @@
*/ */
import {QExposedJoin} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QExposedJoin";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
@ -27,25 +28,26 @@ import {Box} from "@mui/material";
import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete"; import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import React, {ReactNode, useState} from "react"; import React, {ReactNode, useMemo, useState} from "react";
interface FieldAutoCompleteProps interface FieldAutoCompleteProps
{ {
id: string; id: string,
metaData: QInstance; metaData: QInstance,
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,
forceOpen?: boolean; forceOpen?: boolean,
hiddenFieldNames?: string[]; hiddenFieldNames?: string[],
availableFieldNames?: string[]; availableFieldNames?: string[],
variant?: "standard" | "filled" | "outlined"; variant?: "standard" | "filled" | "outlined",
label?: string; label?: string,
textFieldSX?: any; textFieldSX?: any,
autocompleteSlotProps?: any; autocompleteSlotProps?: any,
hasError?: boolean; hasError?: boolean,
noOptionsText?: string; noOptionsText?: string,
omitExposedJoins?: string[]
} }
FieldAutoComplete.defaultProps = FieldAutoComplete.defaultProps =
@ -88,7 +90,7 @@ function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: a
/******************************************************************************* /*******************************************************************************
** Component for rendering a list of field names from a table as an auto-complete. ** Component for rendering a list of field names from a table as an auto-complete.
*******************************************************************************/ *******************************************************************************/
export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, forceOpen, hiddenFieldNames, availableFieldNames, variant, label, textFieldSX, autocompleteSlotProps, hasError, noOptionsText}: FieldAutoCompleteProps): JSX.Element export default function FieldAutoComplete({id, metaData, tableMetaData, handleFieldChange, defaultValue, autoFocus, forceOpen, hiddenFieldNames, availableFieldNames, variant, label, textFieldSX, autocompleteSlotProps, hasError, noOptionsText, omitExposedJoins}: FieldAutoCompleteProps): JSX.Element
{ {
const [selectedFieldName, setSelectedFieldName] = useState(defaultValue ? defaultValue.fieldName : null); const [selectedFieldName, setSelectedFieldName] = useState(defaultValue ? defaultValue.fieldName : null);
@ -96,11 +98,25 @@ export default function FieldAutoComplete({id, metaData, tableMetaData, handleFi
makeFieldOptionsForTable(tableMetaData, fieldOptions, false, hiddenFieldNames, availableFieldNames, selectedFieldName); makeFieldOptionsForTable(tableMetaData, fieldOptions, false, hiddenFieldNames, availableFieldNames, selectedFieldName);
let fieldsGroupBy = null; let fieldsGroupBy = null;
if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0) const availableExposedJoins = useMemo(() =>
{ {
for (let i = 0; i < tableMetaData.exposedJoins.length; i++) const rs: QExposedJoin[] = []
for(let exposedJoin of tableMetaData.exposedJoins ?? [])
{ {
const exposedJoin = tableMetaData.exposedJoins[i]; if(omitExposedJoins?.indexOf(exposedJoin.joinTable.name) > -1)
{
continue;
}
rs.push(exposedJoin);
}
return (rs);
}, [tableMetaData, omitExposedJoins]);
if (availableExposedJoins && availableExposedJoins.length > 0)
{
for (let i = 0; i < availableExposedJoins.length; i++)
{
const exposedJoin = availableExposedJoins[i];
if (metaData.tables.has(exposedJoin.joinTable.name)) if (metaData.tables.has(exposedJoin.joinTable.name))
{ {
fieldsGroupBy = (option: any) => `${option.table.label} fields`; fieldsGroupBy = (option: any) => `${option.table.label} fields`;
@ -185,7 +201,7 @@ export default function FieldAutoComplete({id, metaData, tableMetaData, handleFi
{originalEndAdornment} {originalEndAdornment}
</Box>; </Box>;
return (<TextField {...params} autoFocus={autoFocus} label={label} variant={variant} sx={textFieldSX} autoComplete="off" type="search" InputProps={inputProps} />) return (<TextField {...params} autoFocus={autoFocus} label={label} variant={variant} sx={textFieldSX} autoComplete="off" type="search" InputProps={inputProps} />);
}} }}
// @ts-ignore // @ts-ignore
defaultValue={defaultValue} defaultValue={defaultValue}

View File

@ -83,6 +83,8 @@ interface BasicAndAdvancedQueryControlsProps
mode: string; mode: string;
setMode: (mode: string) => void; setMode: (mode: string) => void;
omitExposedJoins?: string[];
} }
let debounceTimeout: string | number | NodeJS.Timeout; let debounceTimeout: string | number | NodeJS.Timeout;
@ -627,6 +629,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
handleSelectedField={handleSetSort} handleSelectedField={handleSetSort}
fieldEndAdornment={<Box whiteSpace="nowrap"><Icon>arrow_upward</Icon><Icon>arrow_downward</Icon></Box>} fieldEndAdornment={<Box whiteSpace="nowrap"><Icon>arrow_upward</Icon><Icon>arrow_downward</Icon></Box>}
handleAdornmentClick={handleSetSortArrowClick} handleAdornmentClick={handleSetSortArrowClick}
omitExposedJoins={props.omitExposedJoins}
/>); />);
const filterBuilderMouseEvents = const filterBuilderMouseEvents =
@ -721,6 +724,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
buttonChildren={"Add Filter"} buttonChildren={"Add Filter"}
isModeSelectOne={true} isModeSelectOne={true}
handleSelectedField={handleFieldListMenuSelection} handleSelectedField={handleFieldListMenuSelection}
omitExposedJoins={props.omitExposedJoins}
/> />
} }
</> </>

View File

@ -43,6 +43,7 @@ declare module "@mui/x-data-grid"
metaData: QInstance; metaData: QInstance;
queryFilter: QQueryFilter; queryFilter: QQueryFilter;
updateFilter: (newFilter: QQueryFilter) => void; updateFilter: (newFilter: QQueryFilter) => void;
omitExposedJoins?: string[]
} }
} }
@ -181,6 +182,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)} updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
allowVariables={props.allowVariables} allowVariables={props.allowVariables}
queryScreenUsage={props.queryScreenUsage} queryScreenUsage={props.queryScreenUsage}
omitExposedJoins={props.omitExposedJoins}
/> />
{/*JSON.stringify(criteria)*/} {/*JSON.stringify(criteria)*/}
</Box> </Box>

View File

@ -19,6 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {QExposedJoin} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QExposedJoin";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
@ -31,28 +32,26 @@ import ListItem, {ListItemProps} from "@mui/material/ListItem/ListItem";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import React, {useState} from "react"; import React, {useMemo, useState} from "react";
interface FieldListMenuProps interface FieldListMenuProps
{ {
idPrefix: string; idPrefix: string,
heading?: string; heading?: string,
placeholder?: string; placeholder?: string,
tableMetaData: QTableMetaData; tableMetaData: QTableMetaData,
showTableHeaderEvenIfNoExposedJoins: boolean; showTableHeaderEvenIfNoExposedJoins: boolean,
fieldNamesToHide?: string[]; fieldNamesToHide?: string[],
buttonProps: any; buttonProps: any,
buttonChildren: JSX.Element | string; buttonChildren: JSX.Element | string,
isModeSelectOne?: boolean,
isModeSelectOne?: boolean; handleSelectedField?: (field: QFieldMetaData, table: QTableMetaData) => void,
handleSelectedField?: (field: QFieldMetaData, table: QTableMetaData) => void; isModeToggle?: boolean,
toggleStates?: { [fieldName: string]: boolean },
isModeToggle?: boolean; handleToggleField?: (field: QFieldMetaData, table: QTableMetaData, newValue: boolean) => void,
toggleStates?: {[fieldName: string]: boolean}; fieldEndAdornment?: JSX.Element,
handleToggleField?: (field: QFieldMetaData, table: QTableMetaData, newValue: boolean) => void; handleAdornmentClick?: (field: QFieldMetaData, table: QTableMetaData, event: React.MouseEvent<any>) => void,
omitExposedJoins?: string[]
fieldEndAdornment?: JSX.Element
handleAdornmentClick?: (field: QFieldMetaData, table: QTableMetaData, event: React.MouseEvent<any>) => void;
} }
FieldListMenu.defaultProps = { FieldListMenu.defaultProps = {
@ -71,7 +70,7 @@ interface TableWithFields
** Component to render a list of fields from a table (and its join tables) ** Component to render a list of fields from a table (and its join tables)
** which can be interacted with... ** which can be interacted with...
*******************************************************************************/ *******************************************************************************/
export default function FieldListMenu({idPrefix, heading, placeholder, tableMetaData, showTableHeaderEvenIfNoExposedJoins, buttonProps, buttonChildren, isModeSelectOne, fieldNamesToHide, handleSelectedField, isModeToggle, toggleStates, handleToggleField, fieldEndAdornment, handleAdornmentClick}: FieldListMenuProps): JSX.Element export default function FieldListMenu({idPrefix, heading, placeholder, tableMetaData, showTableHeaderEvenIfNoExposedJoins, buttonProps, buttonChildren, isModeSelectOne, fieldNamesToHide, handleSelectedField, isModeToggle, toggleStates, handleToggleField, fieldEndAdornment, handleAdornmentClick, omitExposedJoins}: FieldListMenuProps): JSX.Element
{ {
const [menuAnchorElement, setMenuAnchorElement] = useState(null); const [menuAnchorElement, setMenuAnchorElement] = useState(null);
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
@ -81,7 +80,21 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
const [collapsedTables, setCollapsedTables] = useState({} as { [tableName: string]: boolean }); const [collapsedTables, setCollapsedTables] = useState({} as { [tableName: string]: boolean });
const [lastMouseOverXY, setLastMouseOverXY] = useState({x: 0, y: 0}); const [lastMouseOverXY, setLastMouseOverXY] = useState({x: 0, y: 0});
const [timeOfLastArrow, setTimeOfLastArrow] = useState(0) const [timeOfLastArrow, setTimeOfLastArrow] = useState(0);
const availableExposedJoins = useMemo(() =>
{
const rs: QExposedJoin[] = []
for(let exposedJoin of tableMetaData.exposedJoins ?? [])
{
if(omitExposedJoins?.indexOf(exposedJoin.joinTable.name) > -1)
{
continue;
}
rs.push(exposedJoin);
}
return (rs);
}, [tableMetaData, omitExposedJoins]);
////////////////// //////////////////
// check usages // // check usages //
@ -113,16 +126,16 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
{ {
collapsedTables[tableMetaData.name] = false; collapsedTables[tableMetaData.name] = false;
if (tableMetaData.exposedJoins?.length > 0) if (availableExposedJoins?.length > 0)
{ {
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we have exposed joins, put the table meta data with its fields, and then all of the join tables & fields too // // if we have exposed joins, put the table meta data with its fields, and then all of the join tables & fields too //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
fieldsByTable.push({table: tableMetaData, fields: getTableFieldsAsAlphabeticalArray(tableMetaData)}); fieldsByTable.push({table: tableMetaData, fields: getTableFieldsAsAlphabeticalArray(tableMetaData)});
for (let i = 0; i < tableMetaData.exposedJoins?.length; i++) for (let i = 0; i < availableExposedJoins?.length; i++)
{ {
const joinTable = tableMetaData.exposedJoins[i].joinTable; const joinTable = availableExposedJoins[i].joinTable;
fieldsByTable.push({table: joinTable, fields: getTableFieldsAsAlphabeticalArray(joinTable)}); fieldsByTable.push({table: joinTable, fields: getTableFieldsAsAlphabeticalArray(joinTable)});
collapsedTables[joinTable.name] = false; collapsedTables[joinTable.name] = false;
@ -159,7 +172,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
{ {
return; return;
} }
fields.push(field) fields.push(field);
}); });
fields.sort((a, b) => a.label.localeCompare(b.label)); fields.sort((a, b) => a.label.localeCompare(b.label));
return (fields); return (fields);
@ -193,7 +206,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
if (index == targetIndex) if (index == targetIndex)
{ {
return {field: tableWithField.fields[j], table: tableWithField.table} return {field: tableWithField.fields[j], table: tableWithField.table};
} }
} }
} }
@ -500,9 +513,9 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
tableToggleStates[tableMetaData.name] = allOn; tableToggleStates[tableMetaData.name] = allOn;
tableToggleCounts[tableMetaData.name] = count; tableToggleCounts[tableMetaData.name] = count;
for (let i = 0; i < tableMetaData.exposedJoins?.length; i++) for (let i = 0; i < availableExposedJoins?.length; i++)
{ {
const join = tableMetaData.exposedJoins[i]; const join = availableExposedJoins[i];
const {allOn, count} = getTableToggleState(join.joinTable, false); const {allOn, count} = getTableToggleState(join.joinTable, false);
tableToggleStates[join.joinTable.name] = allOn; tableToggleStates[join.joinTable.name] = allOn;
tableToggleCounts[join.joinTable.name] = count; tableToggleCounts[join.joinTable.name] = count;
@ -541,7 +554,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
*******************************************************************************/ *******************************************************************************/
function toggleCollapsedTable(tableName: string) function toggleCollapsedTable(tableName: string)
{ {
collapsedTables[tableName] = !collapsedTables[tableName] collapsedTables[tableName] = !collapsedTables[tableName];
setCollapsedTables(Object.assign({}, collapsedTables)); setCollapsedTables(Object.assign({}, collapsedTables));
} }
@ -622,7 +635,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
checked={tableToggleStates[headerTable.name]} checked={tableToggleStates[headerTable.name]}
onChange={(event) => handleTableToggle(event, headerTable)} onChange={(event) => handleTableToggle(event, headerTable)}
/>} />}
label={<span style={{marginTop: "0.25rem", display: "inline-block"}}><b>{headerTable.label} Fields</b>&nbsp;<span style={{fontWeight: 400}}>({tableToggleCounts[headerTable.name]})</span></span>} />) label={<span style={{marginTop: "0.25rem", display: "inline-block"}}><b>{headerTable.label} Fields</b>&nbsp;<span style={{fontWeight: 400}}>({tableToggleCounts[headerTable.name]})</span></span>} />);
} }
if (isModeToggle) if (isModeToggle)
@ -638,7 +651,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
</IconButton> </IconButton>
{headerContents} {headerContents}
</> </>
) );
} }
let marginLeft = "unset"; let marginLeft = "unset";
@ -657,7 +670,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
tableWithFields.fields.map((field) => tableWithFields.fields.map((field) =>
{ {
index++; index++;
const key = `${tableWithFields.table?.name}-${field.name}` const key = `${tableWithFields.table?.name}-${field.name}`;
if (collapsedTables[headerTable.name]) if (collapsedTables[headerTable.name])
{ {
@ -677,7 +690,7 @@ export default function FieldListMenu({idPrefix, heading, placeholder, tableMeta
{ {
closeMenu(); closeMenu();
handleSelectedField(field, tableWithFields.table ?? tableMetaData); handleSelectedField(field, tableWithFields.table ?? tableMetaData);
} };
} }
let label: JSX.Element | string = field.label; let label: JSX.Element | string = field.label;

View File

@ -33,6 +33,7 @@ import MenuItem from "@mui/material/MenuItem";
import Select, {SelectChangeEvent} from "@mui/material/Select/Select"; import Select, {SelectChangeEvent} from "@mui/material/Select/Select";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import {omit} from "lodash";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete"; import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues"; import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues";
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery"; import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
@ -190,17 +191,18 @@ export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: str
interface FilterCriteriaRowProps interface FilterCriteriaRowProps
{ {
id: number; id: number,
index: number; index: number,
tableMetaData: QTableMetaData; tableMetaData: QTableMetaData,
metaData: QInstance; metaData: QInstance,
criteria: QFilterCriteria; criteria: QFilterCriteria,
booleanOperator: "AND" | "OR" | null; booleanOperator: "AND" | "OR" | null,
updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void; updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void,
removeCriteria: () => void; removeCriteria: () => void,
updateBooleanOperator: (newValue: string) => void; updateBooleanOperator: (newValue: string) => void,
queryScreenUsage?: QueryScreenUsage; queryScreenUsage?: QueryScreenUsage,
allowVariables?: boolean; allowVariables?: boolean,
omitExposedJoins?: string[]
} }
FilterCriteriaRow.defaultProps = FilterCriteriaRow.defaultProps =
@ -269,7 +271,7 @@ export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValu
return {criteriaIsValid, criteriaStatusTooltip}; return {criteriaIsValid, criteriaStatusTooltip};
} }
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables}: FilterCriteriaRowProps): JSX.Element export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables, omitExposedJoins}: FilterCriteriaRowProps): JSX.Element
{ {
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`); // console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption); const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
@ -488,7 +490,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
</Box> </Box>
<Box display="inline-block" width={250} className="fieldColumn"> <Box display="inline-block" width={250} className="fieldColumn">
<FieldAutoComplete id={`field-${id}`} metaData={metaData} tableMetaData={tableMetaData} defaultValue={defaultFieldValue} handleFieldChange={handleFieldChange} <FieldAutoComplete id={`field-${id}`} metaData={metaData} tableMetaData={tableMetaData} defaultValue={defaultFieldValue} handleFieldChange={handleFieldChange}
autocompleteSlotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}} omitExposedJoins={omitExposedJoins} autocompleteSlotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
/> />
</Box> </Box>
<Box display="inline-block" width={200} className="operatorColumn"> <Box display="inline-block" width={200} className="operatorColumn">

View File

@ -44,7 +44,7 @@ import RecordQuery from "qqq/pages/records/query/RecordQuery";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
import FilterUtils from "qqq/utils/qqq/FilterUtils"; import FilterUtils from "qqq/utils/qqq/FilterUtils";
import TableUtils from "qqq/utils/qqq/TableUtils"; import TableUtils from "qqq/utils/qqq/TableUtils";
import React, {useContext, useEffect, useRef, useState} from "react"; import React, {useContext, useEffect, useMemo, useRef, useState} from "react";
interface FilterAndColumnsSetupWidgetProps interface FilterAndColumnsSetupWidgetProps
{ {
@ -106,6 +106,8 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
const [warning, setWarning] = useState(null as string); const [warning, setWarning] = useState(null as string);
const [widgetFailureAlertContent, setWidgetFailureAlertContent] = useState(null as string); const [widgetFailureAlertContent, setWidgetFailureAlertContent] = useState(null as string);
const omitExposedJoins: string[] = widgetData?.omitExposedJoins ?? [];
////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////
// we'll actually keep 2 copies of the query filter around here - // // we'll actually keep 2 copies of the query filter around here - //
// the one in the record (as json) is one that the backend likes (e.g., possible values as ids) // // the one in the record (as json) is one that the backend likes (e.g., possible values as ids) //
@ -441,7 +443,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
</Collapse> </Collapse>
<Box pt="0.5rem"> <Box pt="0.5rem">
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="center">
<h5>{label ?? "Query Filter"}</h5> <h5>{label ?? widgetData.label ?? widgetMetaData.label ?? "Query Filter"}</h5>
{!hideSortBy && <Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>} {!hideSortBy && <Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>}
</Box> </Box>
{ {
@ -454,7 +456,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
{ {
isEditable && isEditable &&
<Tooltip title={selectTableFirstTooltipTitle}> <Tooltip title={selectTableFirstTooltipTitle}>
<span><Button disabled={!recordValues["tableName"]} sx={{mb: "0.125rem", ...unborderedButtonSX}} onClick={openEditor}>+ Add Filters</Button></span> <span><Button disabled={tableMetaData == null} sx={{mb: "0.125rem", ...unborderedButtonSX}} onClick={openEditor}>+ Add Filters</Button></span>
</Tooltip> </Tooltip>
} }
{ {
@ -501,6 +503,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
initialQueryFilter={frontendQueryFilter} initialQueryFilter={frontendQueryFilter}
initialColumns={columns} initialColumns={columns}
apiVersion={apiVersion} apiVersion={apiVersion}
omitExposedJoins={omitExposedJoins}
/> />
</Box> </Box>
)} )}
@ -510,7 +513,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
<div> <div>
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}> <Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
<Card sx={{m: "2rem", p: "2rem"}}> <Card sx={{m: "2rem", p: "2rem"}}>
<h3>Edit Filters and Columns</h3> <h3>Edit Filters {hideColumns ? "" : " and Columns"}</h3>
{ {
showHelp("modalSubheader") && showHelp("modalSubheader") &&
<Box color={colors.gray.main} pb={"0.5rem"}> <Box color={colors.gray.main} pb={"0.5rem"}>
@ -527,6 +530,7 @@ export default function FilterAndColumnsSetupWidget({isEditable: isEditableProp,
initialQueryFilter={usingDefaultEmptyFilter ? null : frontendQueryFilter} initialQueryFilter={usingDefaultEmptyFilter ? null : frontendQueryFilter}
initialColumns={columns} initialColumns={columns}
apiVersion={apiVersion} apiVersion={apiVersion}
omitExposedJoins={omitExposedJoins}
/> />
} }

View File

@ -20,6 +20,7 @@
*/ */
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue"; import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
/******************************************************************************* /*******************************************************************************
** Properties attached to a (formik?) form field, to denote how it behaves as ** Properties attached to a (formik?) form field, to denote how it behaves as
@ -34,5 +35,6 @@ export interface FieldPossibleValueProps
tableName?: string; tableName?: string;
processName?: string; processName?: string;
possibleValueSourceName?: string; possibleValueSourceName?: string;
possibleValueSourceFilter?: QQueryFilter;
} }

View File

@ -89,15 +89,16 @@ export type QueryScreenUsage = "queryScreen" | "reportSetup"
interface Props interface Props
{ {
table?: QTableMetaData; table?: QTableMetaData,
apiVersion?: ApiVersion; apiVersion?: ApiVersion,
launchProcess?: QProcessMetaData; launchProcess?: QProcessMetaData,
usage?: QueryScreenUsage; usage?: QueryScreenUsage,
isModal?: boolean; isModal?: boolean,
isPreview?: boolean; isPreview?: boolean,
initialQueryFilter?: QQueryFilter; initialQueryFilter?: QQueryFilter,
initialColumns?: QQueryColumns; initialColumns?: QQueryColumns,
allowVariables?: boolean; allowVariables?: boolean,
omitExposedJoins?: string[]
} }
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
@ -130,7 +131,7 @@ const getLoadingScreen = (isModal: boolean) =>
** **
** Yuge component. The best. Lots of very smart people are saying so. ** Yuge component. The best. Lots of very smart people are saying so.
*******************************************************************************/ *******************************************************************************/
const RecordQuery = forwardRef(({table, apiVersion, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) => const RecordQuery = forwardRef(({table, apiVersion, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns, omitExposedJoins}: Props, ref) =>
{ {
const tableName = table.name; const tableName = table.name;
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -2843,6 +2844,7 @@ const RecordQuery = forwardRef(({table, apiVersion, usage, isModal, isPreview, a
idPrefix="columns" idPrefix="columns"
tableMetaData={tableMetaData} tableMetaData={tableMetaData}
showTableHeaderEvenIfNoExposedJoins={true} showTableHeaderEvenIfNoExposedJoins={true}
omitExposedJoins={omitExposedJoins}
placeholder="Search Fields" placeholder="Search Fields"
buttonProps={{sx: columnMenuButtonStyles}} buttonProps={{sx: columnMenuButtonStyles}}
buttonChildren={<><Icon sx={{mr: "0.5rem"}}>view_week_outline</Icon> Columns ({view.queryColumns.getVisibleColumnCount()}) <Icon sx={{ml: "0.5rem"}}>keyboard_arrow_down</Icon></>} buttonChildren={<><Icon sx={{mr: "0.5rem"}}>view_week_outline</Icon> Columns ({view.queryColumns.getVisibleColumnCount()}) <Icon sx={{ml: "0.5rem"}}>keyboard_arrow_down</Icon></>}
@ -2985,6 +2987,7 @@ const RecordQuery = forwardRef(({table, apiVersion, usage, isModal, isPreview, a
setMode={doSetMode} setMode={doSetMode}
savedViewsComponent={savedViewsComponent} savedViewsComponent={savedViewsComponent}
columnMenuComponent={buildColumnMenu()} columnMenuComponent={buildColumnMenu()}
omitExposedJoins={omitExposedJoins}
/> />
} }
@ -3010,7 +3013,8 @@ const RecordQuery = forwardRef(({table, apiVersion, usage, isModal, isPreview, a
metaData: metaData, metaData: metaData,
queryFilter: queryFilter, queryFilter: queryFilter,
updateFilter: doSetQueryFilter, updateFilter: doSetQueryFilter,
allowVariables: allowVariables allowVariables: allowVariables,
omitExposedJoins: omitExposedJoins,
} }
}} }}
localeText={{ localeText={{

View File

@ -73,10 +73,20 @@ function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridge
for (let field of fields) for (let field of fields)
{ {
initialValues[field.name] = record.values.get(field.name); initialValues[field.name] = record.values.get(field.name);
if(initialValues[field.name] === undefined && field.defaultValue !== undefined)
{
initialValues[field.name] = field.defaultValue;
}
} }
const [lastValues, setLastValues] = useState(initialValues); const [lastValues, setLastValues] = useState(initialValues);
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
///////////////////////////////////////////////////////////////////////////////
// store reference to record display values in a state var - see usage below //
///////////////////////////////////////////////////////////////////////////////
const [recordDisplayValues, setRecordDisplayValues] = useState(record?.displayValues ?? new Map<string, string>())
useEffect(() => useEffect(() =>
{ {
(async () => (async () =>
@ -91,7 +101,12 @@ function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridge
const possibleValues = await qController.possibleValues(null, null, field.possibleValueSourceName, null, [value], [], record.values, "form"); const possibleValues = await qController.possibleValues(null, null, field.possibleValueSourceName, null, [value], [], record.values, "form");
if (possibleValues && possibleValues.length > 0) if (possibleValues && possibleValues.length > 0)
{ {
record.displayValues.set(field.name, possibleValues[0].label); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// originally, we put this in record.displayValues, but, sometimes that would then be empty at the usage point below... //
// this works, so, we'll go with it //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
recordDisplayValues.set(field.name, possibleValues[0].label)
setRecordDisplayValues(recordDisplayValues);
} }
} }
} }
@ -109,7 +124,7 @@ function QFMDBridgeForm({fields, record, handleChange, handleSubmit}: QFMDBridge
dynamicFormFields, dynamicFormFields,
formValidations, formValidations,
} = DynamicFormUtils.getFormData(fields); } = DynamicFormUtils.getFormData(fields);
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fields, null, null, record ? record.displayValues : new Map()); DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fields, null, null, recordDisplayValues);
const otherValuesMap = new Map<string, any>(); const otherValuesMap = new Map<string, any>();
record.values.forEach((value, key) => otherValuesMap.set(key, value)); record.values.forEach((value, key) => otherValuesMap.set(key, value));