Add sections and autocomplete (QDynamicSelect) on bulk-edit

This commit is contained in:
2022-10-12 17:32:29 -05:00
parent 0edc07a1c2
commit 0d16b82ad0
11 changed files with 311 additions and 119 deletions

View File

@ -155,6 +155,7 @@ function EntityForm({table, id}: Props): JSX.Element
dynamicFormFields, dynamicFormFields,
formValidations, formValidations,
} = DynamicFormUtils.getFormData(fieldArray); } = DynamicFormUtils.getFormData(fieldArray);
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, record?.displayValues);
///////////////////////////////////// /////////////////////////////////////
// group the formFields by section // // group the formFields by section //
@ -180,24 +181,6 @@ function EntityForm({table, id}: Props): JSX.Element
{ {
sectionDynamicFormFields.push(dynamicFormFields[fieldName]); sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
} }
/////////////////////////////////////////
// add props for possible value fields //
/////////////////////////////////////////
if(field.possibleValueSourceName)
{
let initialDisplayValue = null;
if(record && record.displayValues)
{
initialDisplayValue = record.displayValues.get(field.name);
}
dynamicFormFields[fieldName].possibleValueProps =
{
isPossibleValue: true,
tableName: tableName,
initialDisplayValue: initialDisplayValue,
};
}
} }
if (sectionDynamicFormFields.length === 0) if (sectionDynamicFormFields.length === 0)

View File

@ -136,6 +136,8 @@ function QDynamicForm(props: Props): JSX.Element
fieldLabel={field.label} fieldLabel={field.label}
initialValue={values[fieldName]} initialValue={values[fieldName]}
initialDisplayValue={field.possibleValueProps.initialDisplayValue} initialDisplayValue={field.possibleValueProps.initialDisplayValue}
bulkEditMode={bulkEditMode}
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
/> />
</Grid> </Grid>
); );

View File

@ -95,6 +95,33 @@ class DynamicFormUtils
} }
return (null); return (null);
} }
public static addPossibleValueProps(dynamicFormFields: any, qFields: QFieldMetaData[], tableName: string, displayValues: Map<string, string>)
{
for (let i = 0; i < qFields.length; i++)
{
const field = qFields[i];
/////////////////////////////////////////
// add props for possible value fields //
/////////////////////////////////////////
if (field.possibleValueSourceName && dynamicFormFields[field.name])
{
let initialDisplayValue = null;
if (displayValues)
{
initialDisplayValue = displayValues.get(field.name);
}
dynamicFormFields[field.name].possibleValueProps =
{
isPossibleValue: true,
tableName: tableName,
initialDisplayValue: initialDisplayValue,
};
}
}
}
} }
export default DynamicFormUtils; export default DynamicFormUtils;

View File

@ -123,7 +123,7 @@ function QDynamicFormField({
onClick={bulkEditSwitchChanged} onClick={bulkEditSwitchChanged}
/> />
</Box> </Box>
<Box width="100%"> <Box width="100%" sx={{background: (type == "checkbox" && isDisabled) ? "#f0f2f5!important" : "initial"}}>
{/* for checkboxes, if we put the whole thing in a label, we get bad overly aggressive toggling of the outer switch... */} {/* for checkboxes, if we put the whole thing in a label, we get bad overly aggressive toggling of the outer switch... */}
{(type == "checkbox" ? {(type == "checkbox" ?
field() : field() :

View File

@ -22,9 +22,12 @@
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue"; import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {CircularProgress, FilterOptionsState} from "@mui/material"; import {CircularProgress, FilterOptionsState} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete"; import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {useFormikContext} from "formik"; import {useFormikContext} from "formik";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import MDBox from "qqq/components/Temporary/MDBox";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
interface Props interface Props
@ -35,7 +38,10 @@ interface Props
inForm: boolean; inForm: boolean;
initialValue?: any; initialValue?: any;
initialDisplayValue?: string; initialDisplayValue?: string;
onChange?: any onChange?: any;
isEditable?: boolean;
bulkEditMode?: boolean;
bulkEditSwitchChangeHandler?: any;
} }
QDynamicSelect.defaultProps = { QDynamicSelect.defaultProps = {
@ -43,11 +49,16 @@ QDynamicSelect.defaultProps = {
initialValue: null, initialValue: null,
initialDisplayValue: null, initialDisplayValue: null,
onChange: null, onChange: null,
isEditable: true,
bulkEditMode: false,
bulkEditSwitchChangeHandler: () =>
{
},
}; };
const qController = QClient.getInstance(); const qController = QClient.getInstance();
function QDynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue, initialDisplayValue, onChange}: Props) function QDynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue, initialDisplayValue, onChange, isEditable, bulkEditMode, bulkEditSwitchChangeHandler}: Props)
{ {
const [ open, setOpen ] = useState(false); const [ open, setOpen ] = useState(false);
const [ options, setOptions ] = useState<readonly QPossibleValue[]>([]); const [ options, setOptions ] = useState<readonly QPossibleValue[]>([]);
@ -56,6 +67,8 @@ function QDynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue,
const [defaultValue, _] = useState(initialValue && initialDisplayValue ? {id: initialValue, label: initialDisplayValue} : null); const [defaultValue, _] = useState(initialValue && initialDisplayValue ? {id: initialValue, label: initialDisplayValue} : null);
// const loading = open && options.length === 0; // const loading = open && options.length === 0;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [ switchChecked, setSwitchChecked ] = useState(false);
const [ isDisabled, setIsDisabled ] = useState(!isEditable || bulkEditMode);
let setFieldValueRef: (field: string, value: any, shouldValidate?: boolean) => void = null; let setFieldValueRef: (field: string, value: any, shouldValidate?: boolean) => void = null;
if(inForm) if(inForm)
@ -152,9 +165,18 @@ function QDynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue,
); );
} }
return ( const bulkEditSwitchChanged = () =>
{
const newSwitchValue = !switchChecked;
setSwitchChecked(newSwitchValue);
setIsDisabled(!newSwitchValue);
bulkEditSwitchChangeHandler(fieldName, newSwitchValue);
};
const autocomplete = (
<Autocomplete <Autocomplete
id={fieldName} id={fieldName}
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}}
open={open} open={open}
fullWidth fullWidth
onOpen={() => onOpen={() =>
@ -190,6 +212,7 @@ function QDynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue,
}} }}
renderOption={renderOption} renderOption={renderOption}
filterOptions={filterOptions} filterOptions={filterOptions}
disabled={isDisabled}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
@ -210,6 +233,35 @@ function QDynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue,
)} )}
/> />
); );
if (bulkEditMode)
{
return (
<Box mb={1.5} display="flex" flexDirection="row">
<Box alignItems="baseline" pt={1}>
<Switch
id={`bulkEditSwitch-${fieldName}`}
checked={switchChecked}
onClick={bulkEditSwitchChanged}
/>
</Box>
<Box width="100%">
{autocomplete}
</Box>
</Box>
);
}
else
{
return (
<MDBox mb={1.5}>
{autocomplete}
</MDBox>
);
}
} }
export default QDynamicSelect; export default QDynamicSelect;

View File

@ -36,8 +36,14 @@ interface Props
metaData?: QTableMetaData; metaData?: QTableMetaData;
widgetMetaDataList?: QWidgetMetaData[]; widgetMetaDataList?: QWidgetMetaData[];
light?: boolean; light?: boolean;
stickyTop?: string;
} }
QRecordSidebar.defaultProps = {
light: false,
stickyTop: "100px",
};
interface SidebarEntry interface SidebarEntry
{ {
iconName: string; iconName: string;
@ -45,7 +51,7 @@ interface SidebarEntry
label: string; label: string;
} }
function QRecordSidebar({tableSections, widgetMetaDataList, light}: Props): JSX.Element function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: Props): JSX.Element
{ {
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////
// insert widgets after identity (first) table section // // insert widgets after identity (first) table section //
@ -65,7 +71,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light}: Props): JSX.
return ( return (
<Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: "100px"}}> <Card sx={{borderRadius: ({borders: {borderRadius}}) => borderRadius.lg, position: "sticky", top: stickyTop}}>
<MDBox component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}> <MDBox component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
{ {
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => ( sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
@ -109,8 +115,4 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light}: Props): JSX.
); );
} }
QRecordSidebar.defaultProps = {
light: false,
};
export default QRecordSidebar; export default QRecordSidebar;

View File

@ -40,7 +40,6 @@ import Modal from "@mui/material/Modal";
import { import {
DataGridPro, DataGridPro,
getGridDateOperators, getGridDateOperators,
getGridNumericOperators,
GridCallbackDetails, GridCallbackDetails,
GridColDef, GridColDef,
GridColumnOrderChangeParams, GridColumnOrderChangeParams,
@ -116,12 +115,12 @@ async function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URL
const defaultFilter = {items: []} as GridFilterModel; const defaultFilter = {items: []} as GridFilterModel;
let id = 1; let id = 1;
for(let i = 0; i < qQueryFilter.criteria.length; i++) for (let i = 0; i < qQueryFilter.criteria.length; i++)
{ {
const criteria = qQueryFilter.criteria[i]; const criteria = qQueryFilter.criteria[i];
const field = tableMetaData.fields.get(criteria.fieldName); const field = tableMetaData.fields.get(criteria.fieldName);
let values = criteria.values; let values = criteria.values;
if(field.possibleValueSourceName) if (field.possibleValueSourceName)
{ {
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
// possible-values in query-string are expected to only be their id values. // // possible-values in query-string are expected to only be their id values. //
@ -129,7 +128,7 @@ async function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URL
// but we need them to be possibleValue objects (w/ id & label) so the label // // but we need them to be possibleValue objects (w/ id & label) so the label //
// can be shown in the filter dropdown. So, make backend call to look them up. // // can be shown in the filter dropdown. So, make backend call to look them up. //
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
if(values && values.length > 0) if (values && values.length > 0)
{ {
values = await qController.possibleValues(tableMetaData.name, field.name, "", values); values = await qController.possibleValues(tableMetaData.name, field.name, "", values);
} }
@ -144,7 +143,7 @@ async function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URL
} }
defaultFilter.linkOperator = GridLinkOperator.And; defaultFilter.linkOperator = GridLinkOperator.And;
if(qQueryFilter.booleanOperator === "OR") if (qQueryFilter.booleanOperator === "OR")
{ {
defaultFilter.linkOperator = GridLinkOperator.Or; defaultFilter.linkOperator = GridLinkOperator.Or;
} }
@ -171,7 +170,7 @@ async function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URL
function EntityList({table, launchProcess}: Props): JSX.Element function EntityList({table, launchProcess}: Props): JSX.Element
{ {
const tableName = table.name; const tableName = table.name;
const [searchParams] = useSearchParams(); const [ searchParams ] = useSearchParams();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -202,10 +201,10 @@ function EntityList({table, launchProcess}: Props): JSX.Element
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey)); defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
} }
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel); const [ filterModel, setFilterModel ] = useState({items: []} as GridFilterModel);
const [columnSortModel, setColumnSortModel] = useState(defaultSort); const [ columnSortModel, setColumnSortModel ] = useState(defaultSort);
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility); const [ columnVisibilityModel, setColumnVisibilityModel ] = useState(defaultVisibility);
const [rowsPerPage, setRowsPerPage] = useState(defaultRowsPerPage); const [ rowsPerPage, setRowsPerPage ] = useState(defaultRowsPerPage);
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
// for some reason, if we set the filterModel to what is in local storage, an onChange event // // for some reason, if we set the filterModel to what is in local storage, an onChange event //
@ -213,47 +212,47 @@ function EntityList({table, launchProcess}: Props): JSX.Element
// when that happens put the default back - it needs to be in state // // when that happens put the default back - it needs to be in state //
// const [defaultFilter1] = useState(defaultFilter); // // const [defaultFilter1] = useState(defaultFilter); //
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
const [defaultFilter] = useState({items: []} as GridFilterModel); const [ defaultFilter ] = useState({items: []} as GridFilterModel);
const [tableState, setTableState] = useState(""); const [ tableState, setTableState ] = useState("");
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [ tableMetaData, setTableMetaData ] = useState(null as QTableMetaData);
const [defaultFilterLoaded, setDefaultFilterLoaded] = useState(false); const [ defaultFilterLoaded, setDefaultFilterLoaded ] = useState(false);
const [, setFiltersMenu] = useState(null); const [ , setFiltersMenu ] = useState(null);
const [actionsMenu, setActionsMenu] = useState(null); const [ actionsMenu, setActionsMenu ] = useState(null);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [ tableProcesses, setTableProcesses ] = useState([] as QProcessMetaData[]);
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]); const [ allTableProcesses, setAllTableProcesses ] = useState([] as QProcessMetaData[]);
const [pageNumber, setPageNumber] = useState(0); const [ pageNumber, setPageNumber ] = useState(0);
const [totalRecords, setTotalRecords] = useState(0); const [ totalRecords, setTotalRecords ] = useState(0);
const [selectedIds, setSelectedIds] = useState([] as string[]); const [ selectedIds, setSelectedIds ] = useState([] as string[]);
const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter"); const [ selectFullFilterState, setSelectFullFilterState ] = useState("n/a" as "n/a" | "checked" | "filter");
const [columns, setColumns] = useState([] as GridColDef[]); const [ columns, setColumns ] = useState([] as GridColDef[]);
const [rows, setRows] = useState([] as GridRowsProp[]); const [ rows, setRows ] = useState([] as GridRowsProp[]);
const [loading, setLoading] = useState(true); const [ loading, setLoading ] = useState(true);
const [alertContent, setAlertContent] = useState(""); const [ alertContent, setAlertContent ] = useState("");
const [tableLabel, setTableLabel] = useState(""); const [ tableLabel, setTableLabel ] = useState("");
const [gridMouseDownX, setGridMouseDownX] = useState(0); const [ gridMouseDownX, setGridMouseDownX ] = useState(0);
const [gridMouseDownY, setGridMouseDownY] = useState(0); const [ gridMouseDownY, setGridMouseDownY ] = useState(0);
const [pinnedColumns, setPinnedColumns] = useState({left: ["__check__", "id"]}); const [ pinnedColumns, setPinnedColumns ] = useState({left: [ "__check__", "id" ]});
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData) const [ activeModalProcess, setActiveModalProcess ] = useState(null as QProcessMetaData);
const [launchingProcess, setLaunchingProcess] = useState(launchProcess); const [ launchingProcess, setLaunchingProcess ] = useState(launchProcess);
const instance = useRef({timer: null}); const instance = useRef({timer: null});
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// use all these states to avoid showing results from an "old" query, that finishes loading after a newer one // // use all these states to avoid showing results from an "old" query, that finishes loading after a newer one //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const [latestQueryId, setLatestQueryId] = useState(0); const [ latestQueryId, setLatestQueryId ] = useState(0);
const [countResults, setCountResults] = useState({} as any); const [ countResults, setCountResults ] = useState({} as any);
const [receivedCountTimestamp, setReceivedCountTimestamp] = useState(new Date()); const [ receivedCountTimestamp, setReceivedCountTimestamp ] = useState(new Date());
const [queryResults, setQueryResults] = useState({} as any); const [ queryResults, setQueryResults ] = useState({} as any);
const [receivedQueryTimestamp, setReceivedQueryTimestamp] = useState(new Date()); const [ receivedQueryTimestamp, setReceivedQueryTimestamp ] = useState(new Date());
const [queryErrors, setQueryErrors] = useState({} as any); const [ queryErrors, setQueryErrors ] = useState({} as any);
const [receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp] = useState(new Date()); const [ receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp ] = useState(new Date());
const {pageHeader, setPageHeader} = useContext(QContext); const {pageHeader, setPageHeader} = useContext(QContext);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [ , forceUpdate ] = useReducer((x) => x + 1, 0);
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
@ -269,11 +268,11 @@ function EntityList({table, launchProcess}: Props): JSX.Element
// the path for a process looks like: .../table/process // // the path for a process looks like: .../table/process //
// so if our tableName is in the -2 index, try to open process // // so if our tableName is in the -2 index, try to open process //
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
if(pathParts[pathParts.length - 2] === tableName) if (pathParts[pathParts.length - 2] === tableName)
{ {
const processName = pathParts[pathParts.length - 1]; const processName = pathParts[pathParts.length - 1];
const processList = allTableProcesses.filter(p => p.name.endsWith(processName)); const processList = allTableProcesses.filter(p => p.name.endsWith(processName));
if(processList.length > 0) if (processList.length > 0)
{ {
setActiveModalProcess(processList[0]); setActiveModalProcess(processList[0]);
return; return;
@ -284,7 +283,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
} }
} }
} }
catch(e) catch (e)
{ {
console.log(e); console.log(e);
} }
@ -294,7 +293,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
setActiveModalProcess(null); setActiveModalProcess(null);
}, [location]); }, [ location ]);
const buildQFilter = (filterModel: GridFilterModel) => const buildQFilter = (filterModel: GridFilterModel) =>
{ {
@ -320,7 +319,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
}); });
qFilter.booleanOperator = "AND"; qFilter.booleanOperator = "AND";
if(filterModel.linkOperator == "or") if (filterModel.linkOperator == "or")
{ {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// by default qFilter uses AND - so only if we see linkOperator=or do we need to set it // // by default qFilter uses AND - so only if we see linkOperator=or do we need to set it //
@ -350,7 +349,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
if (!defaultFilterLoaded) if (!defaultFilterLoaded)
{ {
setDefaultFilterLoaded(true); setDefaultFilterLoaded(true);
localFilterModel = await getDefaultFilter(tableMetaData, searchParams, filterLocalStorageKey) localFilterModel = await getDefaultFilter(tableMetaData, searchParams, filterLocalStorageKey);
setFilterModel(localFilterModel); setFilterModel(localFilterModel);
return; return;
} }
@ -364,7 +363,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
}); });
setColumnSortModel(columnSortModel); setColumnSortModel(columnSortModel);
} }
setPinnedColumns({left: ["__check__", tableMetaData.primaryKeyField]}); setPinnedColumns({left: [ "__check__", tableMetaData.primaryKeyField ]});
const qFilter = buildQFilter(localFilterModel); const qFilter = buildQFilter(localFilterModel);
@ -433,7 +432,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
} }
setTotalRecords(countResults[latestQueryId]); setTotalRecords(countResults[latestQueryId]);
delete countResults[latestQueryId]; delete countResults[latestQueryId];
}, [receivedCountTimestamp]); }, [ receivedCountTimestamp ]);
/////////////////////////// ///////////////////////////
@ -455,7 +454,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
const results = queryResults[latestQueryId]; const results = queryResults[latestQueryId];
delete queryResults[latestQueryId]; delete queryResults[latestQueryId];
const fields = [...tableMetaData.fields.values()]; const fields = [ ...tableMetaData.fields.values() ];
const rows = [] as any[]; const rows = [] as any[];
const columnsToRender = {} as any; const columnsToRender = {} as any;
results.forEach((record: QRecord) => results.forEach((record: QRecord) =>
@ -539,12 +538,12 @@ function EntityList({table, launchProcess}: Props): JSX.Element
const sizeAdornment = field.getAdornment(AdornmentType.SIZE); const sizeAdornment = field.getAdornment(AdornmentType.SIZE);
const width: string = sizeAdornment.getValue("width"); const width: string = sizeAdornment.getValue("width");
const widths: Map<string, number> = new Map<string, number>([ const widths: Map<string, number> = new Map<string, number>([
["small", 100], [ "small", 100 ],
["medium", 200], [ "medium", 200 ],
["large", 400], [ "large", 400 ],
["xlarge", 600] [ "xlarge", 600 ]
]); ]);
if(widths.has(width)) if (widths.has(width))
{ {
columnWidth = widths.get(width); columnWidth = widths.get(width);
} }
@ -588,7 +587,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
setLoading(false); setLoading(false);
setAlertContent(null); setAlertContent(null);
forceUpdate(); forceUpdate();
}, [receivedQueryTimestamp]); }, [ receivedQueryTimestamp ]);
///////////////////////// /////////////////////////
// display query error // // display query error //
@ -610,7 +609,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
setLoading(false); setLoading(false);
setAlertContent(errorMessage); setAlertContent(errorMessage);
}, [receivedQueryErrorTimestamp]); }, [ receivedQueryErrorTimestamp ]);
const handlePageChange = (page: number) => const handlePageChange = (page: number) =>
@ -737,7 +736,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
setAllTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks) setAllTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
if(launchingProcess) if (launchingProcess)
{ {
setLaunchingProcess(null); setLaunchingProcess(null);
setActiveModalProcess(launchingProcess); setActiveModalProcess(launchingProcess);
@ -850,7 +849,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
return ""; return "";
} }
function getRecordIdsForProcess() : string | QQueryFilter function getRecordIdsForProcess(): string | QQueryFilter
{ {
if (selectFullFilterState === "filter") if (selectFullFilterState === "filter")
{ {
@ -873,7 +872,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
const closeModalProcess = (event: object, reason: string) => const closeModalProcess = (event: object, reason: string) =>
{ {
if(reason === "backdropClick") if (reason === "backdropClick")
{ {
return; return;
} }
@ -886,12 +885,12 @@ function EntityList({table, launchProcess}: Props): JSX.Element
navigate(newPath.join("/")); navigate(newPath.join("/"));
updateTable(); updateTable();
} };
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") => const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") =>
{ {
const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`)); const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`));
if(processList.length > 0) if (processList.length > 0)
{ {
openModalProcess(processList[0]); openModalProcess(processList[0]);
} }
@ -899,7 +898,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
{ {
setAlertContent(`Could not find Bulk ${processLabelPart} process for this table.`); setAlertContent(`Could not find Bulk ${processLabelPart} process for this table.`);
} }
} };
const bulkLoadClicked = () => const bulkLoadClicked = () =>
{ {
@ -960,7 +959,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
component="div" component="div"
count={totalRecords === null ? 0 : totalRecords} count={totalRecords === null ? 0 : totalRecords}
page={pageNumber} page={pageNumber}
rowsPerPageOptions={[10, 25, 50, 100, 250]} rowsPerPageOptions={[ 10, 25, 50, 100, 250 ]}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
onPageChange={(event, value) => handlePageChange(value)} onPageChange={(event, value) => handlePageChange(value)}
onRowsPerPageChange={(event) => handleRowsPerPageChange(Number(event.target.value))} onRowsPerPageChange={(event) => handleRowsPerPageChange(Number(event.target.value))}
@ -1077,7 +1076,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
useEffect(() => useEffect(() =>
{ {
updateTable(); updateTable();
}, [pageNumber, rowsPerPage, columnSortModel]); }, [ pageNumber, rowsPerPage, columnSortModel ]);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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 //
@ -1086,13 +1085,13 @@ function EntityList({table, launchProcess}: Props): JSX.Element
{ {
setTotalRecords(null); setTotalRecords(null);
updateTable(); updateTable();
}, [tableState, filterModel]); }, [ tableState, filterModel ]);
useEffect(() => useEffect(() =>
{ {
document.documentElement.scrollTop = 0; document.documentElement.scrollTop = 0;
document.scrollingElement.scrollTop = 0; document.scrollingElement.scrollTop = 0;
}, [pageNumber, rowsPerPage]); }, [ pageNumber, rowsPerPage ]);
return ( return (
<DashboardLayout> <DashboardLayout>
@ -1160,7 +1159,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
onColumnOrderChange={handleColumnOrderChange} onColumnOrderChange={handleColumnOrderChange}
onSelectionModelChange={selectionChanged} onSelectionModelChange={selectionChanged}
onSortModelChange={handleSortChange} onSortModelChange={handleSortChange}
sortingOrder={["asc", "desc"]} sortingOrder={[ "asc", "desc" ]}
sortModel={columnSortModel} sortModel={columnSortModel}
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
/> />
@ -1171,7 +1170,9 @@ function EntityList({table, launchProcess}: Props): JSX.Element
{ {
activeModalProcess && activeModalProcess &&
<Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}> <Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}>
<div className="modalProcess">
<ProcessRun process={activeModalProcess} isModal={true} recordIds={getRecordIdsForProcess()} closeModalHandler={closeModalProcess} /> <ProcessRun process={activeModalProcess} isModal={true} recordIds={getRecordIdsForProcess()} closeModalHandler={closeModalProcess} />
</div>
</Modal> </Modal>
} }

View File

@ -451,7 +451,9 @@ function ViewContents({id, table, launchProcess}: Props): JSX.Element
{ {
activeModalProcess && activeModalProcess &&
<Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}> <Modal open={activeModalProcess !== null} onClose={(event, reason) => closeModalProcess(event, reason)}>
<div className="modalProcess">
<ProcessRun process={activeModalProcess} isModal={true} recordIds={id} closeModalHandler={closeModalProcess} /> <ProcessRun process={activeModalProcess} isModal={true} recordIds={id} closeModalHandler={closeModalProcess} />
</div>
</Modal> </Modal>
} }

View File

@ -26,6 +26,7 @@ import {QFrontendComponent} from "@kingsrook/qqq-frontend-core/lib/model/metaDat
import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData"; import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
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 {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
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 {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning"; import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
@ -52,6 +53,7 @@ import BaseLayout from "qqq/components/BaseLayout";
import {QCancelButton, QSubmitButton} from "qqq/components/QButtons"; import {QCancelButton, QSubmitButton} from "qqq/components/QButtons";
import QDynamicForm from "qqq/components/QDynamicForm"; import QDynamicForm from "qqq/components/QDynamicForm";
import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils"; import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
import QRecordSidebar from "qqq/components/QRecordSidebar";
import MDBox from "qqq/components/Temporary/MDBox"; import MDBox from "qqq/components/Temporary/MDBox";
import MDButton from "qqq/components/Temporary/MDButton"; import MDButton from "qqq/components/Temporary/MDButton";
import MDProgress from "qqq/components/Temporary/MDProgress"; import MDProgress from "qqq/components/Temporary/MDProgress";
@ -59,6 +61,7 @@ import MDTypography from "qqq/components/Temporary/MDTypography";
import {QGoogleDriveFolderPickerWrapper} from "qqq/pages/process-run/components/QGoogleDriveFolderPickerWrapper"; import {QGoogleDriveFolderPickerWrapper} from "qqq/pages/process-run/components/QGoogleDriveFolderPickerWrapper";
import QValidationReview from "qqq/pages/process-run/components/QValidationReview"; import QValidationReview from "qqq/pages/process-run/components/QValidationReview";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
import QTableUtils from "qqq/utils/QTableUtils";
import QValueUtils from "qqq/utils/QValueUtils"; import QValueUtils from "qqq/utils/QValueUtils";
import QProcessSummaryResults from "./components/QProcessSummaryResults"; import QProcessSummaryResults from "./components/QProcessSummaryResults";
@ -66,9 +69,9 @@ interface Props
{ {
process?: QProcessMetaData; process?: QProcessMetaData;
defaultProcessValues?: any; defaultProcessValues?: any;
isModal?: boolean isModal?: boolean;
recordIds?: string | QQueryFilter recordIds?: string | QQueryFilter;
closeModalHandler?: (event: object, reason: string) => void closeModalHandler?: (event: object, reason: string) => void;
} }
const INITIAL_RETRY_MILLIS = 1_500; const INITIAL_RETRY_MILLIS = 1_500;
@ -95,6 +98,7 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
const [needInitialLoad, setNeedInitialLoad] = useState(true); const [needInitialLoad, setNeedInitialLoad] = useState(true);
const [processMetaData, setProcessMetaData] = useState(null); const [processMetaData, setProcessMetaData] = useState(null);
const [tableMetaData, setTableMetaData] = useState(null); const [tableMetaData, setTableMetaData] = useState(null);
const [tableSections, setTableSections] = useState(null as QTableSection[]);
const [qInstance, setQInstance] = useState(null as QInstance); const [qInstance, setQInstance] = useState(null as QInstance);
const [processValues, setProcessValues] = useState({} as any); const [processValues, setProcessValues] = useState({} as any);
const [processError, _setProcessError] = useState(null as string); const [processError, _setProcessError] = useState(null as string);
@ -303,6 +307,17 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
); );
} }
const {formFields, values, errors, touched} = formData;
let localTableSections = tableSections;
if(localTableSections == null)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////
// if the table sections (ones that actually have fields to edit) haven't been built yet, do so now //
//////////////////////////////////////////////////////////////////////////////////////////////////////
localTableSections = tableMetaData ? QTableUtils.getSectionsForRecordSidebar(tableMetaData, Object.keys(formFields)) : null;
setTableSections(localTableSections);
}
return ( return (
<> <>
<MDTypography variation="h5" component="div" fontWeight="bold"> <MDTypography variation="h5" component="div" fontWeight="bold">
@ -337,7 +352,61 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
} }
{ {
component.type === QComponentType.BULK_EDIT_FORM && ( component.type === QComponentType.BULK_EDIT_FORM && (
<QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} /> tableMetaData && localTableSections ?
<Grid container spacing={3} mt={2}>
<Grid item xs={12} lg={3}>
<QRecordSidebar tableSections={localTableSections} stickyTop="20px" />
</Grid>
<Grid item xs={12} lg={9}>
{localTableSections.map((section: QTableSection, index: number) =>
{
const name = section.name
console.log(formData);
console.log(section.fieldNames);
const sectionFormFields = {};
for(let i = 0; i<section.fieldNames.length; i++)
{
const fieldName = section.fieldNames[i];
if(formFields[fieldName])
{
// @ts-ignore
sectionFormFields[fieldName] = formFields[fieldName];
}
}
if(Object.keys(sectionFormFields).length > 0)
{
const sectionFormData = {
formFields: sectionFormFields,
values: values,
errors: errors,
touched: touched
};
return (
<Box key={name} pb={3}>
<Card id={name} sx={{scrollMarginTop: "20px"}} elevation={5}>
<MDTypography variant="h5" p={3} pb={1}>
{section.label}
</MDTypography>
<Box px={2}>
<QDynamicForm formData={sectionFormData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
</Box>
</Card>
</Box>
);
}
else
{
return (<br />);
}
}
)}
</Grid>
</Grid>
: <QDynamicForm formData={formData} bulkEditMode bulkEditSwitchChangeHandler={bulkEditSwitchChanged} />
) )
} }
{ {
@ -505,7 +574,7 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
} }
return (rs); return (rs);
} };
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle moving to another step in the process - e.g., after the backend told us what screen to show next. // // handle moving to another step in the process - e.g., after the backend told us what screen to show next. //
@ -517,7 +586,7 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
console.log("No process meta data yet, so returning early"); console.log("No process meta data yet, so returning early");
return; return;
} }
setPageHeader(processMetaData.label) setPageHeader(processMetaData.label);
let newIndex = null; let newIndex = null;
if (typeof newStep === "number") if (typeof newStep === "number")
@ -560,6 +629,8 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
{ {
let fullFieldList = getFullFieldList(activeStep, processValues); let fullFieldList = getFullFieldList(activeStep, processValues);
const formData = DynamicFormUtils.getFormData(fullFieldList); const formData = DynamicFormUtils.getFormData(fullFieldList);
DynamicFormUtils.addPossibleValueProps(formData.dynamicFormFields, fullFieldList, tableMetaData.name, null);
dynamicFormFields = formData.dynamicFormFields; dynamicFormFields = formData.dynamicFormFields;
formValidations = formData.formValidations; formValidations = formData.formValidations;
@ -592,7 +663,7 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
dynamicFormFields[fieldName] = dynamicFormValue; dynamicFormFields[fieldName] = dynamicFormValue;
initialValues[fieldName] = initialValue; initialValues[fieldName] = initialValue;
formValidations[fieldName] = validation; formValidations[fieldName] = validation;
} };
if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN)) if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN))
{ {
@ -602,9 +673,9 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
if (doesStepHaveComponent(activeStep, QComponentType.GOOGLE_DRIVE_SELECT_FOLDER)) if (doesStepHaveComponent(activeStep, QComponentType.GOOGLE_DRIVE_SELECT_FOLDER))
{ {
addField("googleDriveAccessToken", {type: "hidden", omitFromQDynamicForm: true}, null, null); addField("googleDriveAccessToken", {type: "hidden", omitFromQDynamicForm: true}, "", null);
addField("googleDriveFolderId", {type: "hidden", omitFromQDynamicForm: true}, null, null); addField("googleDriveFolderId", {type: "hidden", omitFromQDynamicForm: true}, "", null);
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, null, null); addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null);
} }
if (Object.keys(dynamicFormFields).length > 0) if (Object.keys(dynamicFormFields).length > 0)
@ -673,6 +744,8 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
} }
}); });
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, fullFieldList, tableMetaData.name, null);
setFormFields(newDynamicFormFields); setFormFields(newDynamicFormFields);
setValidationScheme(Yup.object().shape(newFormValidations)); setValidationScheme(Yup.object().shape(newFormValidations));
} }
@ -853,9 +926,9 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
// queryStringPairsForInit.push("recordsParam=filterId"); // queryStringPairsForInit.push("recordsParam=filterId");
// queryStringPairsForInit.push(`filterId=${urlSearchParams.get("filterId")}`); // queryStringPairsForInit.push(`filterId=${urlSearchParams.get("filterId")}`);
// } // }
else if(recordIds) else if (recordIds)
{ {
if(typeof recordIds === "string") if (typeof recordIds === "string")
{ {
queryStringPairsForInit.push("recordsParam=recordIds"); queryStringPairsForInit.push("recordsParam=recordIds");
queryStringPairsForInit.push(`recordIds=${recordIds}`); queryStringPairsForInit.push(`recordIds=${recordIds}`);
@ -991,7 +1064,7 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
const handleCancelClicked = () => const handleCancelClicked = () =>
{ {
if(isModal && closeModalHandler) if (isModal && closeModalHandler)
{ {
closeModalHandler(null, "cancelClicked"); closeModalHandler(null, "cancelClicked");
return; return;
@ -1121,7 +1194,7 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
</MDBox> </MDBox>
); );
if(isModal) if (isModal)
{ {
return ( return (
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}> <Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>

View File

@ -177,3 +177,9 @@ input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button, input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration { display: none; } input[type="search"]::-webkit-search-results-decoration { display: none; }
/* Shrink the big margin-bottom on modal processes */
.modalProcess>.MuiBox-root>.MuiBox-root
{
margin-bottom: 24px;
}

View File

@ -28,16 +28,60 @@ import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTa
*******************************************************************************/ *******************************************************************************/
class QTableUtils class QTableUtils
{ {
public static getSectionsForRecordSidebar(tableMetaData: QTableMetaData): QTableSection[]
/*******************************************************************************
**
*******************************************************************************/
public static getSectionsForRecordSidebar(tableMetaData: QTableMetaData, allowedKeys: any = null): QTableSection[]
{ {
if (tableMetaData.sections) if (tableMetaData.sections)
{ {
return (tableMetaData.sections); if (allowedKeys)
{
const allowedKeySet = new Set<string>();
allowedKeys.forEach((k: string) => allowedKeySet.add(k));
const allowedSections: QTableSection[] = [];
for (let i = 0; i < tableMetaData.sections.length; i++)
{
const section = tableMetaData.sections[i];
if (section.fieldNames)
{
for (let j = 0; j < section.fieldNames.length; j++)
{
if (allowedKeySet.has(section.fieldNames[j]))
{
allowedSections.push(section);
break;
}
}
}
}
return (allowedSections);
} }
else else
{ {
return (tableMetaData.sections);
}
}
else
{
let fieldNames = [...tableMetaData.fields.keys()];
if (allowedKeys)
{
fieldNames = [];
for (const fieldName in tableMetaData.fields.keys())
{
if (allowedKeys[fieldName])
{
fieldNames.push(fieldName);
}
}
}
return ([new QTableSection({ return ([new QTableSection({
iconName: "description", label: "All Fields", name: "allFields", fieldNames: [...tableMetaData.fields.keys()], iconName: "description", label: "All Fields", name: "allFields", fieldNames: [...fieldNames],
})]); })]);
} }
} }