Merge pull request #28 from Kingsrook/feature/CE-607-mvp-of-transportation-plan-record

CE-607 Support fields from an exposed-join on a view screen.
This commit is contained in:
2023-08-09 12:28:16 -05:00
committed by GitHub
7 changed files with 135 additions and 74 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.79", "@kingsrook/qqq-frontend-core": "1.0.80",
"@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",

View File

@ -346,6 +346,12 @@ function EntityForm(props: Props): JSX.Element
const fieldName = section.fieldNames[j]; const fieldName = section.fieldNames[j];
const field = tableMetaData.fields.get(fieldName); const field = tableMetaData.fields.get(fieldName);
if(!field)
{
console.log(`Omitting un-found field ${fieldName} from form`);
continue;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. // // if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
// || (or) we're on the insert screen in which case, only show editable fields. // // || (or) we're on the insert screen in which case, only show editable fields. //

View File

@ -985,6 +985,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
} }
setQJobRunning(null); setQJobRunning(null);
} }
else
{
console.warn(`Process response was not of an expected type (need an npm clean?) ${JSON.stringify(lastProcessResponse)}`);
}
} }
}, [lastProcessResponse]); }, [lastProcessResponse]);

View File

@ -71,6 +71,7 @@ import DataGridUtils from "qqq/utils/DataGridUtils";
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 ProcessUtils from "qqq/utils/qqq/ProcessUtils"; import ProcessUtils from "qqq/utils/qqq/ProcessUtils";
import TableUtils from "qqq/utils/qqq/TableUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId"; const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
@ -628,6 +629,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
let models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, null, searchParams, filterLocalStorageKey, sortLocalStorageKey); let models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, null, searchParams, filterLocalStorageKey, sortLocalStorageKey);
setFilterModel(models.filter); setFilterModel(models.filter);
setColumnSortModel(models.sort); setColumnSortModel(models.sort);
setWarningAlert(models.warning);
setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, models.filter, models.sort, rowsPerPage)); setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, models.filter, models.sort, rowsPerPage));
return; return;
} }
@ -708,16 +711,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
if (tableMetaData?.exposedJoins) if (tableMetaData?.exposedJoins)
{ {
const visibleJoinTables = getVisibleJoinTables(); const visibleJoinTables = getVisibleJoinTables();
queryJoins = TableUtils.getQueryJoins(tableMetaData, visibleJoinTables);
queryJoins = [];
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{
const join = tableMetaData.exposedJoins[i];
if (visibleJoinTables.has(join.joinTable.name))
{
queryJoins.push(new QueryJoin(join.joinTable.name, true, "LEFT"));
}
}
} }
////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////
@ -1400,6 +1394,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
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);
handleSortChange(models.sort, models.filter); handleSortChange(models.sort, models.filter);
setWarningAlert(models.warning);
localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString()); localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString());
} }
else else
@ -1431,35 +1427,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
return (qRecord); return (qRecord);
} }
const getFieldAndTable = (fieldName: string): [QFieldMetaData, QTableMetaData] =>
{
if(fieldName.indexOf(".") > -1)
{
const nameParts = fieldName.split(".", 2);
for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++)
{
const join = tableMetaData?.exposedJoins[i];
if(join?.joinTable.name == nameParts[0])
{
return ([join.joinTable.fields.get(nameParts[1]), join.joinTable]);
}
}
}
else
{
return ([tableMetaData.fields.get(fieldName), tableMetaData]);
}
return (null);
}
const copyColumnValues = async (column: GridColDef) => const copyColumnValues = async (column: GridColDef) =>
{ {
let data = ""; let data = "";
let counter = 0; let counter = 0;
if (latestQueryResults && latestQueryResults.length) if (latestQueryResults && latestQueryResults.length)
{ {
let [qFieldMetaData, fieldTable] = getFieldAndTable(column.field); let [qFieldMetaData, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, column.field);
for (let i = 0; i < latestQueryResults.length; i++) for (let i = 0; i < latestQueryResults.length; i++)
{ {
let record = latestQueryResults[i] as QRecord; let record = latestQueryResults[i] as QRecord;
@ -1489,7 +1463,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setFilterForColumnStats(buildQFilter(tableMetaData, filterModel)); setFilterForColumnStats(buildQFilter(tableMetaData, filterModel));
setColumnStatsFieldName(column.field); setColumnStatsFieldName(column.field);
const [field, fieldTable] = getFieldAndTable(column.field); const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, column.field);
setColumnStatsField(field); setColumnStatsField(field);
setColumnStatsFieldTableName(fieldTable.name); setColumnStatsFieldTableName(fieldTable.name);
}; };
@ -1962,7 +1936,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{ {
(warningAlert) ? ( (warningAlert) ? (
<Collapse in={Boolean(warningAlert)}> <Collapse in={Boolean(warningAlert)}>
<Alert color="warning" sx={{mb: 3}} onClose={() => setWarningAlert(null)}>{warningAlert}</Alert> <Alert color="warning" icon={<Icon>warning</Icon>} sx={{mb: 3}} onClose={() => setWarningAlert(null)}>{warningAlert}</Alert>
</Collapse> </Collapse>
) : null ) : null
} }

View File

@ -27,6 +27,7 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection"; import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant"; import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
import {Alert, Typography} from "@mui/material"; import {Alert, Typography} from "@mui/material";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
@ -103,7 +104,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]); const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
const [actionsMenu, setActionsMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null);
const [notFoundMessage, setNotFoundMessage] = useState(null as string); const [notFoundMessage, setNotFoundMessage] = useState(null as string);
const [errorMessage, setErrorMessage] = useState(null as string) const [errorMessage, setErrorMessage] = useState(null as string);
const [successMessage, setSuccessMessage] = useState(null as string); const [successMessage, setSuccessMessage] = useState(null as string);
const [warningMessage, setWarningMessage] = useState(null as string); const [warningMessage, setWarningMessage] = useState(null as string);
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData); const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
@ -325,6 +326,31 @@ function RecordView({table, launchProcess}: Props): JSX.Element
reload(); reload();
}, [location.pathname, location.hash]); }, [location.pathname, location.hash]);
const getVisibleJoinTables = (tableMetaData: QTableMetaData): Set<string> =>
{
const visibleJoinTables = new Set<string>();
for (let i = 0; i < tableMetaData?.sections.length; i++)
{
const section = tableMetaData?.sections[i];
if (section.isHidden || !section.fieldNames || !section.fieldNames.length)
{
continue;
}
section.fieldNames.forEach((fieldName) =>
{
const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
if(tableForField && tableForField.name != tableMetaData.name)
{
visibleJoinTables.add(tableForField.name);
}
})
}
return (visibleJoinTables);
};
if (!asyncLoadInited) if (!asyncLoadInited)
{ {
setAsyncLoadInited(true); setAsyncLoadInited(true);
@ -368,13 +394,20 @@ function RecordView({table, launchProcess}: Props): JSX.Element
setActiveModalProcess(launchingProcess); setActiveModalProcess(launchingProcess);
} }
let queryJoins: QueryJoin[] = null;
const visibleJoinTables = getVisibleJoinTables(tableMetaData);
if(visibleJoinTables.size > 0)
{
queryJoins = TableUtils.getQueryJoins(tableMetaData, visibleJoinTables);
}
///////////////////// /////////////////////
// load the record // // load the record //
///////////////////// /////////////////////
let record: QRecord; let record: QRecord;
try try
{ {
record = await qController.get(tableName, id, tableVariant); record = await qController.get(tableName, id, tableVariant, null, queryJoins);
setRecord(record); setRecord(record);
} }
catch (e) catch (e)
@ -465,17 +498,22 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const fields = ( const fields = (
<Box key={section.name} display="flex" flexDirection="column" py={1} pr={2}> <Box key={section.name} display="flex" flexDirection="column" py={1} pr={2}>
{ {
section.fieldNames.map((fieldName: string) => ( section.fieldNames.map((fieldName: string) =>
<Box key={fieldName} flexDirection="row" pr={2}> {
<Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)"> let [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fieldName);
{tableMetaData.fields.get(fieldName).label}: let label = field.label;
<div style={{display: "inline-block", width: 0}}>&nbsp;</div> return (
</Typography> <Box key={fieldName} flexDirection="row" pr={2}>
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)"> <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)">
{ValueUtils.getDisplayValue(tableMetaData.fields.get(fieldName), record, "view")} {label}:
</Typography> <div style={{display: "inline-block", width: 0}}>&nbsp;</div>
</Box> </Typography>
)) <Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)">
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
</Typography>
</Box>
)
})
} }
</Box> </Box>
); );

View File

@ -31,6 +31,7 @@ import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilt
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {ThisOrLastPeriodExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression"; import {ThisOrLastPeriodExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression";
import {GridFilterItem, GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro"; import {GridFilterItem, GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro";
import TableUtils from "qqq/utils/qqq/TableUtils";
import ValueUtils from "qqq/utils/qqq/ValueUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils";
const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId"; const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
@ -375,10 +376,11 @@ class FilterUtils
** Get the default filter to use on the page - either from given filter string, query string param, or ** Get the default filter to use on the page - either from given filter string, query string param, or
** local storage, or a default (empty). ** local storage, or a default (empty).
*******************************************************************************/ *******************************************************************************/
public static async determineFilterAndSortModels(qController: QController, tableMetaData: QTableMetaData, filterString: string, searchParams: URLSearchParams, filterLocalStorageKey: string, sortLocalStorageKey: string): Promise<{ filter: GridFilterModel, sort: GridSortItem[] }> public static async determineFilterAndSortModels(qController: QController, tableMetaData: QTableMetaData, filterString: string, searchParams: URLSearchParams, filterLocalStorageKey: string, sortLocalStorageKey: string): Promise<{ filter: GridFilterModel, sort: GridSortItem[], warning: string }>
{ {
let defaultFilter = {items: []} as GridFilterModel; let defaultFilter = {items: []} as GridFilterModel;
let defaultSort = [] as GridSortItem[]; let defaultSort = [] as GridSortItem[];
let warningParts = [] as string[];
if (tableMetaData && tableMetaData.fields !== undefined) if (tableMetaData && tableMetaData.fields !== undefined)
{ {
@ -396,30 +398,11 @@ class FilterUtils
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];
let fieldTable = tableMetaData; let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
let field = null;
if (criteria.fieldName.indexOf(".") > -1)
{
const nameParts = criteria.fieldName.split(".", 2);
for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++)
{
const joinTable = tableMetaData.exposedJoins[i].joinTable;
if (joinTable.name == nameParts[0])
{
fieldTable = joinTable;
field = joinTable.fields.get(nameParts[1]);
break;
}
}
}
else
{
field = tableMetaData.fields.get(criteria.fieldName);
}
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)
continue; continue;
} }
@ -500,7 +483,7 @@ class FilterUtils
localStorage.setItem(sortLocalStorageKey, JSON.stringify(defaultSort)); localStorage.setItem(sortLocalStorageKey, JSON.stringify(defaultSort));
} }
return ({filter: defaultFilter, sort: defaultSort}); return ({filter: defaultFilter, sort: defaultSort, warning: warningParts.length > 0 ? "Warning: " + warningParts.join("; ") : ""});
} }
catch (e) catch (e)
{ {
@ -551,7 +534,7 @@ class FilterUtils
}); });
} }
return ({filter: defaultFilter, sort: defaultSort}); return ({filter: defaultFilter, sort: defaultSort, warning: warningParts.length > 0 ? "Warning: " + warningParts.join("; ") : ""});
} }

View File

@ -19,8 +19,10 @@
* 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 {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 {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection"; import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QueryJoin} from "@kingsrook/qqq-frontend-core/lib/model/query/QueryJoin";
/******************************************************************************* /*******************************************************************************
** Utility class for working with QQQ Tables ** Utility class for working with QQQ Tables
@ -28,7 +30,6 @@ import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTa
*******************************************************************************/ *******************************************************************************/
class TableUtils class TableUtils
{ {
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -85,6 +86,61 @@ class TableUtils
})]); })]);
} }
} }
/*******************************************************************************
**
*******************************************************************************/
public static getFieldAndTable(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
{
if (fieldName.indexOf(".") > -1)
{
const nameParts = fieldName.split(".", 2);
for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++)
{
const join = tableMetaData?.exposedJoins[i];
if (join?.joinTable.name == nameParts[0])
{
return ([join.joinTable.fields.get(nameParts[1]), join.joinTable]);
}
}
}
else
{
return ([tableMetaData.fields.get(fieldName), tableMetaData]);
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public static getQueryJoins(tableMetaData: QTableMetaData, visibleJoinTables: Set<string>): QueryJoin[]
{
const queryJoins = [];
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
{
const join = tableMetaData.exposedJoins[i];
if (visibleJoinTables.has(join.joinTable.name))
{
let joinName = null;
if (join.joinPath && join.joinPath.length == 1 && join.joinPath[0].name)
{
joinName = join.joinPath[0].name;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - what about a join with a longer path? it would be nice to pass such joinNames through there too, //
// but what, that would actually be multiple queryJoins? needs a fair amount of thought. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
queryJoins.push(new QueryJoin(join.joinTable.name, true, "LEFT", null, null, joinName));
}
}
return queryJoins;
}
} }
export default TableUtils; export default TableUtils;