Merge pull request #10 from Kingsrook/feature/sprint-14

Feature/sprint 14
This commit is contained in:
2022-11-03 10:59:38 -05:00
committed by GitHub
20 changed files with 1246 additions and 271 deletions

View File

@ -13,7 +13,7 @@
"@fullcalendar/interaction": "5.10.0", "@fullcalendar/interaction": "5.10.0",
"@fullcalendar/react": "5.10.0", "@fullcalendar/react": "5.10.0",
"@fullcalendar/timegrid": "5.10.0", "@fullcalendar/timegrid": "5.10.0",
"@kingsrook/qqq-frontend-core": "1.0.25", "@kingsrook/qqq-frontend-core": "1.0.31",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.4.1", "@mui/material": "5.4.1",
"@mui/styled-engine": "5.4.1", "@mui/styled-engine": "5.4.1",
@ -34,6 +34,7 @@
"@types/react": "17.0.38", "@types/react": "17.0.38",
"@types/react-dom": "17.0.11", "@types/react-dom": "17.0.11",
"@types/react-router-hash-link": "2.4.5", "@types/react-router-hash-link": "2.4.5",
"ace-builds": "1.12.3",
"chart.js": "3.4.1", "chart.js": "3.4.1",
"chroma-js": "2.4.2", "chroma-js": "2.4.2",
"datejs": "1.0.0-rc3", "datejs": "1.0.0-rc3",
@ -45,6 +46,7 @@
"html-react-parser": "1.4.8", "html-react-parser": "1.4.8",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.6",
"react": "17.0.2", "react": "17.0.2",
"react-ace": "10.1.0",
"react-chartjs-2": "3.0.4", "react-chartjs-2": "3.0.4",
"react-cookie": "4.1.1", "react-cookie": "4.1.1",
"react-dom": "17.0.2", "react-dom": "17.0.2",
@ -73,7 +75,7 @@
"geff-ham": "rm -rf node_modules/ && rm -rf package-lock.json && npm install --legacy-peer-deps && npm start", "geff-ham": "rm -rf node_modules/ && rm -rf package-lock.json && npm install --legacy-peer-deps && npm start",
"install-legacy-peer-deps": "npm install --legacy-peer-deps", "install-legacy-peer-deps": "npm install --legacy-peer-deps",
"prepublishOnly": "tsc -p ./ --outDir lib/", "prepublishOnly": "tsc -p ./ --outDir lib/",
"start": "react-scripts start", "start": "BROWSER=none react-scripts start",
"test": "react-scripts test", "test": "react-scripts test",
"cypress:open": "cypress open" "cypress:open": "cypress open"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -48,7 +48,8 @@ import Overview from "qqq/pages/dashboards/Overview";
import EntityCreate from "qqq/pages/entity-create"; import EntityCreate from "qqq/pages/entity-create";
import EntityEdit from "qqq/pages/entity-edit"; import EntityEdit from "qqq/pages/entity-edit";
import EntityList from "qqq/pages/entity-list"; import EntityList from "qqq/pages/entity-list";
import EntityView from "qqq/pages/entity-view"; import EntityDeveloperView from "qqq/pages/entity-view/EntityDeveloperView";
import EntityView from "qqq/pages/entity-view/EntityView";
import ProcessRun from "qqq/pages/process-run"; import ProcessRun from "qqq/pages/process-run";
import ReportRun from "qqq/pages/process-run/ReportRun"; import ReportRun from "qqq/pages/process-run/ReportRun";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
@ -262,6 +263,13 @@ export default function App()
component: <EntityEdit table={table} />, component: <EntityEdit table={table} />,
}); });
routeList.push({
name: `${app.label}`,
key: `${app.name}.dev`,
route: `${path}/:id/dev`,
component: <EntityDeveloperView table={table} />,
});
const processesForTable = QProcessUtils.getProcessesForTable(metaData, table.name, true); const processesForTable = QProcessUtils.getProcessesForTable(metaData, table.name, true);
processesForTable.forEach((process) => processesForTable.forEach((process) =>
{ {

View File

@ -0,0 +1,37 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {tooltipClasses, TooltipProps} from "@mui/material";
import {styled} from "@mui/material/styles";
import Tooltip from "@mui/material/Tooltip";
import React from "react";
const CustomWidthTooltip = styled(({className, ...props}: TooltipProps) => (
<Tooltip {...props} classes={{popper: className}} />
))({
[`& .${tooltipClasses.tooltip}`]: {
maxWidth: 500,
textAlign: "left",
},
});
export default CustomWidthTooltip

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 {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
@ -73,6 +74,8 @@ function EntityForm({table, id}: Props): JSX.Element
const [tableSections, setTableSections] = useState(null as QTableSection[]); const [tableSections, setTableSections] = useState(null as QTableSection[]);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [noCapabilityError, setNoCapabilityError] = useState(null as string);
const {pageHeader, setPageHeader} = useContext(QContext); const {pageHeader, setPageHeader} = useContext(QContext);
const navigate = useNavigate(); const navigate = useNavigate();
@ -140,11 +143,21 @@ function EntityForm({table, id}: Props): JSX.Element
}); });
setFormValues(formValues); setFormValues(formValues);
if(!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
{
setNoCapabilityError("You may not edit records in this table");
}
} }
else else
{ {
setFormTitle(`Creating New ${tableMetaData?.label}`); setFormTitle(`Creating New ${tableMetaData?.label}`);
setPageHeader(`Creating New ${tableMetaData?.label}`); setPageHeader(`Creating New ${tableMetaData?.label}`);
if(!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
{
setNoCapabilityError("You may not create records in this table");
}
} }
setInitialValues(initialValues); setInitialValues(initialValues);
@ -168,6 +181,11 @@ function EntityForm({table, id}: Props): JSX.Element
const section = tableSections[i]; const section = tableSections[i];
const sectionDynamicFormFields: any[] = []; const sectionDynamicFormFields: any[] = [];
if(section.isHidden)
{
continue;
}
for (let j = 0; j < section.fieldNames.length; j++) for (let j = 0; j < section.fieldNames.length; j++)
{ {
const fieldName = section.fieldNames[j]; const fieldName = section.fieldNames[j];
@ -277,6 +295,19 @@ function EntityForm({table, id}: Props): JSX.Element
const formId = id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`; const formId = id != null ? `edit-${tableMetaData?.name}-form` : `create-${tableMetaData?.name}-form`;
if(noCapabilityError)
{
return <MDBox mb={3}>
<Grid container spacing={3}>
<Grid item xs={12}>
<MDBox mb={3}>
<Alert severity="error">{noCapabilityError}</Alert>
</MDBox>
</Grid>
</Grid>
</MDBox>;
}
return ( return (
<MDBox mb={3}> <MDBox mb={3}>
<Grid container spacing={3}> <Grid container spacing={3}>

View File

@ -71,7 +71,7 @@ interface QDeleteButtonProps
export function QDeleteButton({onClickHandler}: QDeleteButtonProps): JSX.Element export function QDeleteButton({onClickHandler}: QDeleteButtonProps): JSX.Element
{ {
return ( return (
<MDBox ml={3} mr={3} width={standardWidth}> <MDBox ml={3} width={standardWidth}>
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>}> <MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>}>
Delete Delete
</MDButton> </MDButton>
@ -82,7 +82,7 @@ export function QDeleteButton({onClickHandler}: QDeleteButtonProps): JSX.Element
export function QEditButton(): JSX.Element export function QEditButton(): JSX.Element
{ {
return ( return (
<MDBox width={standardWidth}> <MDBox ml={3} width={standardWidth}>
<Link to="edit"> <Link to="edit">
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}> <MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
Edit Edit

View File

@ -59,6 +59,11 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P
const sidebarEntries = [] as SidebarEntry[]; const sidebarEntries = [] as SidebarEntry[];
tableSections && tableSections.forEach((section, index) => tableSections && tableSections.forEach((section, index) =>
{ {
if(section.isHidden)
{
return;
}
if (index === 1 && widgetMetaDataList) if (index === 1 && widgetMetaDataList)
{ {
widgetMetaDataList.forEach((widget) => widgetMetaDataList.forEach((widget) =>

View File

@ -18,6 +18,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 {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData"; import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppMetaData";
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType"; import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
@ -25,7 +26,7 @@ import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/
import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData"; import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Icon} from "@mui/material"; import {Box, Icon, Typography} from "@mui/material";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
@ -55,6 +56,8 @@ function AppHome({app}: Props): JSX.Element
const [reports, setReports] = useState([] as QReportMetaData[]); const [reports, setReports] = useState([] as QReportMetaData[]);
const [childApps, setChildApps] = useState([] as QAppMetaData[]); const [childApps, setChildApps] = useState([] as QAppMetaData[]);
const [tableCounts, setTableCounts] = useState(new Map<string, { isLoading: boolean, value: number }>()); const [tableCounts, setTableCounts] = useState(new Map<string, { isLoading: boolean, value: number }>());
const [tableCountNumbers, setTableCountNumbers] = useState(new Map<string, string>());
const [tableCountTexts, setTableCountTexts] = useState(new Map<string, string>());
const [updatedTableCounts, setUpdatedTableCounts] = useState(new Date()); const [updatedTableCounts, setUpdatedTableCounts] = useState(new Date());
const [widgets, setWidgets] = useState([] as any[]); const [widgets, setWidgets] = useState([] as any[]);
@ -113,15 +116,41 @@ function AppHome({app}: Props): JSX.Element
setChildApps(newChildApps); setChildApps(newChildApps);
const tableCounts = new Map<string, { isLoading: boolean, value: number }>(); const tableCounts = new Map<string, { isLoading: boolean, value: number }>();
const tableCountNumbers = new Map<string, string>();
const tableCountTexts = new Map<string, string>();
newTables.forEach((table) => newTables.forEach((table) =>
{ {
tableCounts.set(table.name, {isLoading: true, value: null}); tableCounts.set(table.name, {isLoading: true, value: null});
setTimeout(async () => setTimeout(async () =>
{ {
const count = await qController.count(table.name); const tableMetaData = await qController.loadTableMetaData(table.name);
tableCounts.set(table.name, {isLoading: false, value: count}); let countResult = null;
if(tableMetaData.capabilities.has(Capability.TABLE_COUNT))
{
countResult = await qController.count(table.name);
if (countResult !== null && countResult !== undefined)
{
tableCountNumbers.set(table.name, countResult.toLocaleString());
tableCountTexts.set(table.name, countResult === 1 ? "total record" : "total records");
}
else
{
tableCountNumbers.set(table.name, "--");
tableCountTexts.set(table.name, " ");
}
}
else
{
tableCountNumbers.set(table.name, "");
tableCountTexts.set(table.name, " ");
}
tableCounts.set(table.name, {isLoading: false, value: countResult});
setTableCounts(tableCounts); setTableCounts(tableCounts);
setTableCountNumbers(tableCountNumbers);
setTableCountTexts(tableCountTexts);
setUpdatedTableCounts(new Date()); setUpdatedTableCounts(new Date());
}, 1); }, 1);
}); });
@ -169,9 +198,17 @@ function AppHome({app}: Props): JSX.Element
{app.sections.map((section) => ( {app.sections.map((section) => (
<MDBox key={section.name} mb={3}> <MDBox key={section.name} mb={3}>
<Card sx={{overflow: "visible"}}> <Card sx={{overflow: "visible"}}>
<MDBox p={3}> <Box p={3} display="flex" alignItems="center" gap=".5rem">
<MDTypography variant="h5">{section.label}</MDTypography> {
</MDBox> section.icon &&
(
section.icon.path && <img src={section.icon.path} alt={section.label} />
)
}
<Typography variant="h5">
{section.label}
</Typography>
</Box>
{ {
section.processes ? ( section.processes ? (
<MDBox p={3} pl={5} pt={0}> <MDBox p={3} pl={5} pt={0}>
@ -261,8 +298,8 @@ function AppHome({app}: Props): JSX.Element
<MDBox mb={3}> <MDBox mb={3}>
<MiniStatisticsCard <MiniStatisticsCard
title={{fontWeight: "bold", text: table.label}} title={{fontWeight: "bold", text: table.label}}
count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : tableCounts.get(table.name).value.toLocaleString()} count={!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "..." : (tableCountNumbers.get(table.name))}
percentage={{color: "info", text: (!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "" : (tableCounts.get(table.name).value === 1 ? "total record" : "total records"))}} percentage={{color: "info", text: (!tableCounts.has(table.name) || tableCounts.get(table.name).isLoading ? "" : (tableCountTexts.get(table.name)))}}
icon={{color: "info", component: <Icon>{table.iconName || app.iconName}</Icon>}} icon={{color: "info", component: <Icon>{table.iconName || app.iconName}</Icon>}}
direction="right" direction="right"
/> />

View File

@ -20,6 +20,7 @@
*/ */
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType"; import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
@ -219,7 +220,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
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(null);
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 [columnsModel, setColumnsModel] = useState([] as GridColDef[]); const [columnsModel, setColumnsModel] = useState([] as GridColDef[]);
@ -384,12 +385,15 @@ function EntityList({table, launchProcess}: Props): JSX.Element
setLatestQueryId(thisQueryId); setLatestQueryId(thisQueryId);
console.log(`Issuing query: ${thisQueryId}`); console.log(`Issuing query: ${thisQueryId}`);
if (tableMetaData.capabilities.has(Capability.TABLE_COUNT))
{
qController.count(tableName, qFilter).then((count) => qController.count(tableName, qFilter).then((count) =>
{ {
countResults[thisQueryId] = count; countResults[thisQueryId] = count;
setCountResults(countResults); setCountResults(countResults);
setReceivedCountTimestamp(new Date()); setReceivedCountTimestamp(new Date());
}); });
}
qController.query(tableName, qFilter, rowsPerPage, pageNumber * rowsPerPage).then((results) => qController.query(tableName, qFilter, rowsPerPage, pageNumber * rowsPerPage).then((results) =>
{ {
@ -584,7 +588,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
const row: any = {}; const row: any = {};
fields.forEach((field) => fields.forEach((field) =>
{ {
const value = QValueUtils.getDisplayValue(field, record); const value = QValueUtils.getDisplayValue(field, record, "query");
if (typeof value !== "string") if (typeof value !== "string")
{ {
columnsToRender[field.name] = true; columnsToRender[field.name] = true;
@ -592,9 +596,26 @@ function EntityList({table, launchProcess}: Props): JSX.Element
row[field.name] = value; row[field.name] = value;
}); });
if(!row["id"])
{
row["id"] = row[tableMetaData.primaryKeyField];
}
rows.push(row); rows.push(row);
}); });
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// do this secondary check for columnsToRender - in case we didn't have any rows above, and our check for string isn't enough. //
// ... shouldn't this be just based on the field definition anyway... ? plus adornments? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
fields.forEach((field) =>
{
if(field.possibleValueSourceName)
{
columnsToRender[field.name] = true;
}
});
if(columnsModel.length == 0) if(columnsModel.length == 0)
{ {
setupGridColumns(columnsToRender); setupGridColumns(columnsToRender);
@ -877,21 +898,6 @@ function EntityList({table, launchProcess}: Props): JSX.Element
return ""; return "";
} }
function getRecordIdsForProcess(): string | QQueryFilter
{
if (selectFullFilterState === "filter")
{
return (buildQFilter(filterModel));
}
if (selectedIds.length > 0)
{
return (selectedIds.join(","));
}
return "";
}
const openModalProcess = (process: QProcessMetaData = null) => const openModalProcess = (process: QProcessMetaData = null) =>
{ {
if (selectFullFilterState === "filter") if (selectFullFilterState === "filter")
@ -979,7 +985,23 @@ function EntityList({table, launchProcess}: Props): JSX.Element
// @ts-ignore // @ts-ignore
const defaultLabelDisplayedRows = ({from, to, count}) => const defaultLabelDisplayedRows = ({from, to, count}) =>
{ {
if (count !== null && count !== undefined) if(tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// to avoid a non-countable table showing (this is what data-grid did) 91-100 even if there were only 95 records, //
// we'll do this... not quite good enough, but better than the original //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(rows.length > 0 && rows.length < to - from)
{
to = from + rows.length;
}
return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()}`);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// treat -1 as the sentinel that it's set as below -- remember, we did that so that 'to' would have a value in here when there's no count. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (count !== null && count !== undefined && count !== -1)
{ {
if (count === 0) if (count === 0)
{ {
@ -998,7 +1020,9 @@ function EntityList({table, launchProcess}: Props): JSX.Element
return ( return (
<TablePagination <TablePagination
component="div" component="div"
count={totalRecords === null ? 0 : totalRecords} // note - passing null here makes the 'to' param in the defaultLabelDisplayedRows also be null,
// so pass some sentinel value...
count={totalRecords === null ? -1 : totalRecords}
page={pageNumber} page={pageNumber}
rowsPerPageOptions={[ 10, 25, 50, 100, 250 ]} rowsPerPageOptions={[ 10, 25, 50, 100, 250 ]}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
@ -1111,7 +1135,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
selectFullFilterState === "filter" && ( selectFullFilterState === "filter" && (
<div className="selectionTool"> <div className="selectionTool">
All All
<strong>{` ${totalRecords ? totalRecords.toLocaleString() : "All"} `}</strong> <strong>{` ${totalRecords ? totalRecords.toLocaleString() : ""} `}</strong>
records matching this query are selected. records matching this query are selected.
<Button onClick={() => setSelectFullFilterState("checked")}> <Button onClick={() => setSelectFullFilterState("checked")}>
Select the Select the
@ -1144,25 +1168,41 @@ function EntityList({table, launchProcess}: Props): JSX.Element
onClose={closeActionsMenu} onClose={closeActionsMenu}
keepMounted keepMounted
> >
{
table.capabilities.has(Capability.TABLE_INSERT) &&
<MenuItem onClick={bulkLoadClicked}> <MenuItem onClick={bulkLoadClicked}>
<ListItemIcon><Icon>library_add</Icon></ListItemIcon> <ListItemIcon><Icon>library_add</Icon></ListItemIcon>
Bulk Load Bulk Load
</MenuItem> </MenuItem>
}
{
table.capabilities.has(Capability.TABLE_UPDATE) &&
<MenuItem onClick={bulkEditClicked}> <MenuItem onClick={bulkEditClicked}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon> <ListItemIcon><Icon>edit</Icon></ListItemIcon>
Bulk Edit Bulk Edit
</MenuItem> </MenuItem>
}
{
table.capabilities.has(Capability.TABLE_DELETE) &&
<MenuItem onClick={bulkDeleteClicked}> <MenuItem onClick={bulkDeleteClicked}>
<ListItemIcon><Icon>delete</Icon></ListItemIcon> <ListItemIcon><Icon>delete</Icon></ListItemIcon>
Bulk Delete Bulk Delete
</MenuItem> </MenuItem>
{tableProcesses.length > 0 && <Divider />} }
{(table.capabilities.has(Capability.TABLE_INSERT) || table.capabilities.has(Capability.TABLE_UPDATE) || table.capabilities.has(Capability.TABLE_DELETE)) && tableProcesses.length > 0 && <Divider />}
{tableProcesses.map((process) => ( {tableProcesses.map((process) => (
<MenuItem key={process.name} onClick={() => processClicked(process)}> <MenuItem key={process.name} onClick={() => processClicked(process)}>
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon> <ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{process.label} {process.label}
</MenuItem> </MenuItem>
))} ))}
{
tableProcesses.length == 0 && !table.capabilities.has(Capability.TABLE_INSERT) && !table.capabilities.has(Capability.TABLE_UPDATE) && !table.capabilities.has(Capability.TABLE_DELETE) &&
<MenuItem disabled>
<ListItemIcon><Icon>block</Icon></ListItemIcon>
<i>No actions available</i>
</MenuItem>
}
</Menu> </Menu>
); );
@ -1224,7 +1264,10 @@ function EntityList({table, launchProcess}: Props): JSX.Element
{renderActionsMenu} {renderActionsMenu}
</MDBox> </MDBox>
{
table.capabilities.has(Capability.TABLE_INSERT) &&
<QCreateNewButton /> <QCreateNewButton />
}
</MDBox> </MDBox>
<Card> <Card>
@ -1242,6 +1285,7 @@ function EntityList({table, launchProcess}: Props): JSX.Element
disableSelectionOnClick disableSelectionOnClick
autoHeight autoHeight
rows={rows} rows={rows}
// getRowHeight={() => "auto"} // maybe nice? wraps values in cells...
columns={columnsModel} columns={columnsModel}
rowBuffer={10} rowBuffer={10}
rowCount={totalRecords === null ? 0 : totalRecords} rowCount={totalRecords === null ? 0 : totalRecords}

View File

@ -0,0 +1,117 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Typography} from "@mui/material";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import React, {useState} from "react";
import AceEditor from "react-ace";
import {QCancelButton, QSaveButton} from "qqq/components/QButtons";
import QClient from "qqq/utils/QClient";
interface Props
{
tableName: string;
primaryKey: any;
fieldName: string;
titlePrefix: string;
recordLabel: string;
scriptName: string;
code: string;
closeCallback: any;
}
const qController = QClient.getInstance();
function AssociatedScriptEditor({tableName, primaryKey, fieldName, titlePrefix, recordLabel, scriptName, code, closeCallback}: Props): JSX.Element
{
const [closing, setClosing] = useState(false);
const [updatedCode, setUpdatedCode] = useState(code)
const [commitMessage, setCommitMessage] = useState("")
const saveClicked = () =>
{
setClosing(true);
(async () =>
{
const rs = await qController.storeRecordAssociatedScript(tableName, primaryKey, fieldName, updatedCode, commitMessage);
closeCallback(null, "saved", "Saved New " + scriptName);
})();
}
const cancelClicked = () =>
{
setClosing(true);
closeCallback(null, "cancelled");
}
const updateCode = (value: string, event: any) =>
{
setUpdatedCode(value);
}
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
{
setCommitMessage(event.target.value);
}
return (
<Box sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={12}>
<Card sx={{height: "100%", p: 3}}>
<Typography variant="h5" pb={1}>
{`${titlePrefix}: ${recordLabel} - ${scriptName}`}
</Typography>
<AceEditor
mode="javascript"
theme="github"
name="editor"
editorProps={{$blockScrolling: true}}
onChange={updateCode}
width="100%"
height="100%"
value={updatedCode}
style={{border: "1px solid gray"}}
/>
<Box pt={1}>
<Grid container alignItems="flex-end">
<Box width="50%">
<TextField id="commitMessage" label="Commit Message" variant="standard" fullWidth value={commitMessage} onChange={updateCommitMessage} />
</Box>
<Grid container justifyContent="flex-end" spacing={3}>
<QCancelButton disabled={closing} onClickHandler={cancelClicked} />
<QSaveButton disabled={closing} onClickHandler={saveClicked} />
</Grid>
</Grid>
</Box>
</Card>
</Box>
);
}
export default AssociatedScriptEditor;

View File

@ -0,0 +1,654 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Chip, Icon, ListItem, ListItemAvatar, Typography} from "@mui/material";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid";
import List from "@mui/material/List";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Modal from "@mui/material/Modal";
import Snackbar from "@mui/material/Snackbar";
import Tab from "@mui/material/Tab";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import Tabs from "@mui/material/Tabs";
import TextField from "@mui/material/TextField";
import React, {useContext, useReducer, useState} from "react";
import AceEditor from "react-ace";
import {useParams} from "react-router-dom";
import QContext from "QContext";
import BaseLayout from "qqq/components/BaseLayout";
import CustomWidthTooltip from "qqq/components/CustomWidthTooltip/CustomWidthTooltip";
import DataTableBodyCell from "qqq/components/Temporary/DataTable/DataTableBodyCell";
import DataTableHeadCell from "qqq/components/Temporary/DataTable/DataTableHeadCell";
import MDBox from "qqq/components/Temporary/MDBox";
import AssociatedScriptEditor from "qqq/pages/entity-view/AssociatedScriptEditor";
import QClient from "qqq/utils/QClient";
import QValueUtils from "qqq/utils/QValueUtils";
import "ace-builds/src-noconflict/mode-java";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/ext-language_tools";
const qController = QClient.getInstance();
interface TabPanelProps
{
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps)
{
const {children, value, index, ...other} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
// Declaring props types for ViewForm
interface Props
{
table?: QTableMetaData;
}
EntityDeveloperView.defaultProps =
{
table: null,
};
function EntityDeveloperView({table}: Props): JSX.Element
{
const {id} = useParams();
const tableName = table.name;
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [tableMetaData, setTableMetaData] = useState(null);
const [record, setRecord] = useState(null as QRecord);
const [recordJSON, setRecordJSON] = useState("");
const [associatedScripts, setAssociatedScripts] = useState([] as any[]);
const [notFoundMessage, setNotFoundMessage] = useState(null);
const [selectedTabs, setSelectedTabs] = useState({} as any);
const [viewingRevisions, setViewingRevisions] = useState({} as any);
const [scriptLogs, setScriptLogs] = useState({} as any);
const [editingScript, setEditingScript] = useState(null as any);
const [alertText, setAlertText] = useState(null as string);
const {setPageHeader} = useContext(QContext);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
if (!asyncLoadInited)
{
setAsyncLoadInited(true);
(async () =>
{
/////////////////////////////////////////////////////////////////////
// load the full table meta-data (the one we took in is a partial) //
/////////////////////////////////////////////////////////////////////
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
//////////////////////////////
// load top-level meta-data //
//////////////////////////////
const metaData = await qController.loadMetaData();
QValueUtils.qInstance = metaData;
/////////////////////
// load the record //
/////////////////////
let record: QRecord;
try
{
const developerModeData = await qController.getRecordDeveloperMode(tableName, id);
record = new QRecord(developerModeData.record);
console.log("Loaded record developer mode.");
setRecord(record);
setAssociatedScripts(developerModeData.associatedScripts);
const recordJSONObject = {} as any;
for (let key of record.values.keys())
{
recordJSONObject[key] = record.values.get(key);
}
setRecordJSON(JSON.stringify(recordJSONObject, null, 3));
}
catch (e)
{
if (e instanceof QException)
{
if ((e as QException).status === "404")
{
setNotFoundMessage(`${tableMetaData.label} ${id} could not be found.`);
return;
}
}
}
setPageHeader(record.recordLabel + " Developer Mode");
forceUpdate();
})();
}
const revToColor = (fieldName: string, rev: number): string =>
{
let hash = 0;
let idFactor = 1;
try
{
idFactor = Number(id);
}
catch (e)
{
}
const string = `${fieldName} ${90210 * idFactor * rev}`;
for (let i = 0; i < string.length; i += 1)
{
hash = string.charCodeAt(i) + ((hash << 5) - hash);
}
let color = "#";
for (let i = 0; i < 3; i += 1)
{
const value = (hash >> (i * 8)) & 0xff;
color += `00${value.toString(16)}`.slice(-2);
}
return color;
};
const editScript = (fieldName: string, code: string) =>
{
const editingScript = {} as any;
editingScript.fieldName = fieldName;
editingScript.titlePrefix = code ? "Editing Script" : "Creating New Script";
editingScript.code = code;
setEditingScript(editingScript);
};
const closeEditingScript = (event: object, reason: string, alert: string = null) =>
{
if (reason === "backdropClick")
{
return;
}
if (reason === "saved")
{
setAsyncLoadInited(false);
setAssociatedScripts([]);
viewingRevisions[editingScript.fieldName] = null;
setViewingRevisions(viewingRevisions);
forceUpdate();
}
if (alert)
{
setAlertText(alert);
}
setEditingScript(null);
};
const changeTab = (newValue: number, fieldName: string) =>
{
selectedTabs[fieldName] = newValue;
setSelectedTabs(selectedTabs);
forceUpdate();
};
const selectRevision = (fieldName: string, revisionId: number) =>
{
viewingRevisions[fieldName] = revisionId;
setViewingRevisions(viewingRevisions);
scriptLogs[revisionId] = null;
setScriptLogs(scriptLogs);
loadRevisionLogs(fieldName, revisionId)
forceUpdate();
};
const loadRevisionLogs = (fieldName: string, revisionId: number) =>
{
(async () =>
{
const rs = await qController.getRecordAssociatedScriptLogs(tableName, id, fieldName, revisionId);
scriptLogs[revisionId] = [];
if (rs["scriptLogRecords"])
{
scriptLogs[revisionId] = rs["scriptLogRecords"];
}
console.log("Script logs:");
console.log(scriptLogs[revisionId]);
setScriptLogs(scriptLogs);
forceUpdate();
})();
}
function getRevisionsList(scriptRevisions: any, fieldName: any, currentScriptRevisionId: any)
{
return <List sx={{pl: 3, height: "400px", overflow: "auto"}}>
{
scriptRevisions ? <></> :
<Typography variant="body2">
There are not any versions of this script.
</Typography>
}
{
scriptRevisions?.map((revision: any) => (
<React.Fragment key={revision.values.id}>
<ListItem sx={{p: 1}} alignItems="flex-start" selected={viewingRevisions[fieldName] == revision.values.id} onClick={(event) => selectRevision(fieldName, revision.values.id)}>
<ListItemAvatar>
<Avatar sx={{bgcolor: revToColor(fieldName, revision.values.sequenceNo)}}>{`${revision.values.sequenceNo}`}</Avatar>
</ListItemAvatar>
<ListItemText
primaryTypographyProps={{fontSize: "1rem"}}
secondaryTypographyProps={{fontSize: ".85rem"}}
primary={
<div style={{whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}} title={revision.values.commitMessage}>
{revision.values.id == currentScriptRevisionId && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
{revision.values.commitMessage}
</div>
}
secondary={
<>
{QValueUtils.formatDateTime(revision.values.createDate)}
<br />
{revision.values.author}
</>
}
/>
<ListItemIcon sx={{minWidth: "auto", px: 1}}><Icon>settings</Icon></ListItemIcon>
</ListItem>
<Divider sx={{my: 0.5}} variant="inset" component="li" />
</React.Fragment>
))
}
</List>;
}
function getScriptLogs(revisionId: number)
{
const logs = scriptLogs[revisionId] as any[];
if (logs === null || logs === undefined)
{
return <Typography variant="body2" p={3}>Loading...</Typography>;
}
if (logs.length === 0)
{
return <Typography variant="body2" p={3}>No logs available for this version.</Typography>;
}
return (
<TableContainer sx={{boxShadow: "none"}}>
<Table>
<Box component="thead">
<TableRow key="header">
<DataTableHeadCell sorted={false}>Timestamp</DataTableHeadCell>
<DataTableHeadCell sorted={false} align="right">Run Time (ms)</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Had Error?</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Input</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Output</DataTableHeadCell>
<DataTableHeadCell sorted={false}>Logs</DataTableHeadCell>
</TableRow>
</Box>
<TableBody>
{
logs.map((logRecord) =>
{
let logs = "";
if (logRecord.values.scriptLogLine)
{
for (let i = 0; i < logRecord.values.scriptLogLine.length; i++)
{
console.log(" += " + i);
logs += (logRecord.values.scriptLogLine[i].values.text + "\n");
}
}
return (
<TableRow key={logRecord.values.id}>
<DataTableBodyCell>{QValueUtils.formatDateTime(logRecord.values.startTimestamp)}</DataTableBodyCell>
<DataTableBodyCell align="right">{logRecord.values.runTimeMillis?.toLocaleString()}</DataTableBodyCell>
<DataTableBodyCell>
<div style={{color: logRecord.values.hadError ? "red" : "auto"}}>{QValueUtils.formatBoolean(logRecord.values.hadError)}</div>
</DataTableBodyCell>
<DataTableBodyCell>{logRecord.values.input}</DataTableBodyCell>
<DataTableBodyCell>
{logRecord.values.output}
{logRecord.values.error}
</DataTableBodyCell>
<DataTableBodyCell>{logs}</DataTableBodyCell>
</TableRow>
);
})
}
</TableBody>
</Table>
</TableContainer>
);
}
return (
<BaseLayout>
<MDBox>
<Grid container>
<Grid item xs={12}>
<MDBox mb={3}>
{
notFoundMessage
?
<MDBox>{notFoundMessage}</MDBox>
:
<MDBox pb={3}>
{
alertText ? (
<Snackbar open={alertText !== null && alertText !== ""} autoHideDuration={6000} onClose={() => setAlertText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
<Alert color="success" onClose={() => setAlertText(null)}>
{alertText}
</Alert>
</Snackbar>
) : ("")
}
<Grid container spacing={3}>
<Grid item xs={12} mb={3}>
<Card id="jsonView" sx={{mb: 3}}>
<Typography variant="h5" p={2}>Record Raw Values as JSON</Typography>
<AceEditor
mode="json"
theme="github"
name="recordJSON"
editorProps={{$blockScrolling: true}}
value={recordJSON}
readOnly
width="100%"
showPrintMargin={false}
height="200px"
/>
</Card>
{
associatedScripts && associatedScripts.map((object) =>
{
let fieldName = object.associatedScript?.fieldName;
let field = tableMetaData.fields.get(fieldName);
let currentScriptRevisionId = object.script?.values?.currentScriptRevisionId;
if (!selectedTabs[fieldName])
{
selectedTabs[fieldName] = 0;
}
if (!viewingRevisions[fieldName] || viewingRevisions[fieldName] === -1)
{
console.log(`Defaulting revision for ${fieldName} to ${currentScriptRevisionId}`);
viewingRevisions[fieldName] = currentScriptRevisionId;
if(!scriptLogs[currentScriptRevisionId])
{
loadRevisionLogs(fieldName, currentScriptRevisionId);
}
}
const viewingRevisionArray = object.scriptRevisions?.filter((rev: any) => rev?.values?.id === viewingRevisions[fieldName]);
const code = viewingRevisionArray?.length > 0 ? viewingRevisionArray[0].values.contents : "";
const viewingSequenceNo = viewingRevisionArray?.length > 0 ? viewingRevisionArray[0].values.sequenceNo : "";
let editButtonTooltip = "";
let editButtonText = "Create New Script";
if (currentScriptRevisionId)
{
if (currentScriptRevisionId === viewingRevisions[fieldName])
{
editButtonTooltip = "If you make any changes to this script, a new version will be created when you hit Save.";
editButtonText = "Edit";
}
else
{
editButtonTooltip = "If you want to make this previous Version active, bring up the Edit window, make any changes " +
"to the old Version if they are needed, then click Save. A new Version will be created, and set as Current.";
editButtonText = "Edit and Activate";
}
}
return (
<Card key={fieldName} id={`associatedScript.${fieldName}`} sx={{mb: 3}}>
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2}>
<Typography variant="h5" p={2}>{field?.label}</Typography>
<Tabs
sx={{mr: 1}}
value={selectedTabs[fieldName]}
onChange={(event, newValue) => changeTab(newValue, fieldName)}
variant="standard"
>
<Tab label="Code" id="simple-tab-0" aria-controls="simple-tabpanel-0" sx={{width: "100px"}} />
<Tab label="Logs" id="simple-tab-1" aria-controls="simple-tabpanel-1" sx={{width: "100px"}} />
<Tab label="Test" id="simple-tab-2" aria-controls="simple-tabpanel-2" sx={{width: "100px"}} />
<Tab label="Docs" id="simple-tab-3" aria-controls="simple-tabpanel-3" sx={{width: "100px"}} />
</Tabs>
</Box>
<TabPanel index={0} value={selectedTabs[fieldName]}>
<Grid container>
<Grid item xs={4}>
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
<Typography variant="h6" pl={3}>Versions</Typography>
</Box>
{getRevisionsList(object.scriptRevisions, fieldName, currentScriptRevisionId)}
</Grid>
<Grid item xs={8}>
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
{
currentScriptRevisionId &&
<Typography variant="h6">
{
currentScriptRevisionId === viewingRevisions[fieldName]
? (<>Current Version ({viewingSequenceNo})</>)
: (<>Version {viewingSequenceNo}</>)
}
</Typography>
}
<CustomWidthTooltip title={editButtonTooltip}>
<Button sx={{py: 0}} onClick={() => editScript(fieldName, code)}>
{editButtonText}
</Button>
</CustomWidthTooltip>
</Box>
{
code ? (
<>
<AceEditor
mode="javascript"
theme="github"
name={`view-${fieldName}`}
readOnly
editorProps={{$blockScrolling: true}}
width="100%"
height="400px"
value={code}
/>
</>
) : null
}
</Grid>
</Grid>
</TabPanel>
<TabPanel index={1} value={selectedTabs[fieldName]}>
<Grid container height="440px">
<Grid item xs={4}>
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
<Typography variant="h6" pl={3}>Versions</Typography>
</Box>
{getRevisionsList(object.scriptRevisions, fieldName, currentScriptRevisionId)}
</Grid>
<Grid item xs={8}>
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
<Typography variant="h6" pl={3}>Script Logs (Version {viewingSequenceNo})</Typography>
</Box>
<Box height="400px" overflow="auto">
{getScriptLogs(viewingRevisions[fieldName])}
</Box>
</Grid>
</Grid>
</TabPanel>
<TabPanel index={2} value={selectedTabs[fieldName]}>
<Grid container height="440px" spacing={2}>
<Grid item xs={6}>
<Box gap={2} pb={1} height="40px" px={2}>
<Card sx={{width: "100%", height: "400px"}}>
<Box width="100%">
<Typography variant="h6" p={2}>Test Input</Typography>
<Box px={2} pb={2}>
<TextField id="testInput1" label="Ship To Zip" variant="standard" fullWidth sx={{mb: 2}} />
<TextField id="testInput1" label="No of Cartons" variant="standard" fullWidth sx={{mb: 2}} />
</Box>
<div style={{float: "right"}}>
<Button>Submit</Button>
</div>
</Box>
</Card>
</Box>
</Grid>
<Grid item xs={6}>
<Box gap={2} pb={1} height="40px">
<Card sx={{width: "100%", height: "400px"}}>
<Typography variant="h6" pl={3}>Test Output</Typography>
</Card>
</Box>
</Grid>
</Grid>
</TabPanel>
<TabPanel index={3} value={selectedTabs[fieldName]}>
<Grid container height="440px">
<Grid item xs={12}>
<Box gap={2} pb={1} pl={3}>
<Box pb={1}>
<Typography variant="h6">Documentation</Typography>
</Box>
<Box sx={{overflow: "auto"}} className="devDocumentation">
<Typography variant="body2" sx={{maxWidth: "1200px", margin: "auto"}}>
<p>A <b>Deposco Order Optimization Batch Name Script</b> is called when an order is being
optimized for shipping within Deposco. It is responsible for determining the order&apos;s&nbsp;
<b>Batch Name</b> - in other words, an indication of what day the order should be shipped,
and whether or not the order is a line haul.</p>
<p><b>Input</b></p>
<p>The input to this type of script is an object named <code>input</code>, with the following fields:</p>
<ul>
<li><code>warehouseId</code> The id of the warehouse that the order is shipping from. See the <b>Warehouse</b> table for mappings.</li>
<li><code>shipToZipCode</code> The zip code that the order is shipping to.</li>
<li><code>estimatedNoOfCartons</code> The estimated number of cartons that the order will ship in.</li>
</ul>
<p><b>Output</b></p>
<p>The script is responsible only for outputting a single value - a <code>string</code> which will be set as the order&apos;s&nbsp;
<b>Batch Name</b> in Deposco.</p>
<p><b>Example</b></p>
<code style={{whiteSpace: "pre-wrap"}}>
if(today.weekday == 1)
(
return &quot;TUE-Line-Haul&quot;
)
</code>
</Typography>
</Box>
</Box>
</Grid>
</Grid>
</TabPanel>
</Card>
);
})
}
</Grid>
</Grid>
{
editingScript &&
<Modal open={editingScript as boolean} onClose={(event, reason) => closeEditingScript(event, reason)}>
<AssociatedScriptEditor
tableName={tableName}
primaryKey={id}
fieldName={editingScript.fieldName}
titlePrefix={editingScript.titlePrefix}
recordLabel={record.recordLabel}
scriptName={tableMetaData.fields.get(editingScript.fieldName).label}
code={editingScript.code}
closeCallback={closeEditingScript}
/>
</Modal>
}
</MDBox>
}
</MDBox>
</Grid>
</Grid>
</MDBox>
</BaseLayout>
);
}
export default EntityDeveloperView;

View File

@ -20,6 +20,7 @@
*/ */
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException"; import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection"; import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
@ -41,8 +42,9 @@ import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Modal from "@mui/material/Modal"; import Modal from "@mui/material/Modal";
import React, {useContext, useEffect, useReducer, useState} from "react"; import React, {useContext, useEffect, useReducer, useState} from "react";
import {useLocation, useNavigate, useSearchParams} from "react-router-dom"; import {useLocation, useNavigate, useParams, useSearchParams} from "react-router-dom";
import QContext from "QContext"; import QContext from "QContext";
import BaseLayout from "qqq/components/BaseLayout";
import DashboardWidgets from "qqq/components/DashboardWidgets"; import DashboardWidgets from "qqq/components/DashboardWidgets";
import {QActionsMenuButton, QDeleteButton, QEditButton} from "qqq/components/QButtons"; import {QActionsMenuButton, QDeleteButton, QEditButton} from "qqq/components/QButtons";
import QRecordSidebar from "qqq/components/QRecordSidebar"; import QRecordSidebar from "qqq/components/QRecordSidebar";
@ -61,18 +63,20 @@ const qController = QClient.getInstance();
// Declaring props types for ViewForm // Declaring props types for ViewForm
interface Props interface Props
{ {
id: string;
table?: QTableMetaData; table?: QTableMetaData;
launchProcess?: QProcessMetaData; launchProcess?: QProcessMetaData;
} }
ViewContents.defaultProps = { EntityView.defaultProps =
{
table: null, table: null,
launchProcess: null launchProcess: null
}; };
function ViewContents({id, table, launchProcess}: Props): JSX.Element function EntityView({table, launchProcess}: Props): JSX.Element
{ {
const {id} = useParams();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -231,6 +235,11 @@ function ViewContents({id, table, launchProcess}: Props): JSX.Element
for (let i = 0; i < tableSections.length; i++) for (let i = 0; i < tableSections.length; i++)
{ {
const section = tableSections[i]; const section = tableSections[i];
if(section.isHidden)
{
continue;
}
sectionFieldElements.set( sectionFieldElements.set(
section.name, section.name,
<MDBox key={section.name} display="flex" flexDirection="column" py={1} pr={2}> <MDBox key={section.name} display="flex" flexDirection="column" py={1} pr={2}>
@ -241,7 +250,7 @@ function ViewContents({id, table, launchProcess}: Props): JSX.Element
{tableMetaData.fields.get(fieldName).label}: {tableMetaData.fields.get(fieldName).label}:
</MDTypography> </MDTypography>
<MDTypography variant="button" fontWeight="regular" color="text"> <MDTypography variant="button" fontWeight="regular" color="text">
{QValueUtils.getDisplayValue(tableMetaData.fields.get(fieldName), record)} {QValueUtils.getDisplayValue(tableMetaData.fields.get(fieldName), record, "view")}
</MDTypography> </MDTypography>
</MDBox> </MDBox>
)) ))
@ -310,10 +319,15 @@ function ViewContents({id, table, launchProcess}: Props): JSX.Element
onClose={closeActionsMenu} onClose={closeActionsMenu}
keepMounted keepMounted
> >
{
table.capabilities.has(Capability.TABLE_UPDATE) &&
<MenuItem onClick={() => navigate("edit")}> <MenuItem onClick={() => navigate("edit")}>
<ListItemIcon><Icon>edit</Icon></ListItemIcon> <ListItemIcon><Icon>edit</Icon></ListItemIcon>
Edit Edit
</MenuItem> </MenuItem>
}
{
table.capabilities.has(Capability.TABLE_DELETE) &&
<MenuItem onClick={() => <MenuItem onClick={() =>
{ {
setActionsMenu(null); setActionsMenu(null);
@ -323,13 +337,19 @@ function ViewContents({id, table, launchProcess}: Props): JSX.Element
<ListItemIcon><Icon>delete</Icon></ListItemIcon> <ListItemIcon><Icon>delete</Icon></ListItemIcon>
Delete Delete
</MenuItem> </MenuItem>
{tableProcesses.length > 0 && <Divider />} }
{tableProcesses.length > 0 && (table.capabilities.has(Capability.TABLE_UPDATE) || table.capabilities.has(Capability.TABLE_DELETE)) && <Divider />}
{tableProcesses.map((process) => ( {tableProcesses.map((process) => (
<MenuItem key={process.name} onClick={() => processClicked(process)}> <MenuItem key={process.name} onClick={() => processClicked(process)}>
<ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon> <ListItemIcon><Icon>{process.iconName ?? "arrow_forward"}</Icon></ListItemIcon>
{process.label} {process.label}
</MenuItem> </MenuItem>
))} ))}
{tableProcesses.length > 0 && <Divider />}
<MenuItem onClick={() => navigate("dev")}>
<ListItemIcon><Icon>data_object</Icon></ListItemIcon>
Developer Mode
</MenuItem>
</Menu> </Menu>
); );
@ -355,6 +375,12 @@ function ViewContents({id, table, launchProcess}: Props): JSX.Element
}; };
return ( return (
<BaseLayout>
<MDBox>
<Grid container>
<Grid item xs={12}>
<MDBox mb={3}>
{
notFoundMessage notFoundMessage
? ?
<MDBox>{notFoundMessage}</MDBox> <MDBox>{notFoundMessage}</MDBox>
@ -419,8 +445,12 @@ function ViewContents({id, table, launchProcess}: Props): JSX.Element
)) : null} )) : null}
<MDBox component="form" p={3}> <MDBox component="form" p={3}>
<Grid container justifyContent="flex-end" spacing={3}> <Grid container justifyContent="flex-end" spacing={3}>
<QDeleteButton onClickHandler={handleClickDeleteButton} /> {
<QEditButton /> table.capabilities.has(Capability.TABLE_DELETE) && <QDeleteButton onClickHandler={handleClickDeleteButton} />
}
{
table.capabilities.has(Capability.TABLE_UPDATE) && <QEditButton />
}
</Grid> </Grid>
</MDBox> </MDBox>
@ -458,8 +488,13 @@ function ViewContents({id, table, launchProcess}: Props): JSX.Element
} }
</MDBox> </MDBox>
}
</MDBox>
</Grid>
</Grid>
</MDBox>
</BaseLayout>
); );
} }
export default ViewContents; export default EntityView;

View File

@ -1,60 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Grid from "@mui/material/Grid";
import {useParams} from "react-router-dom";
import BaseLayout from "qqq/components/BaseLayout";
import MDBox from "qqq/components/Temporary/MDBox";
import ViewContents from "./components/ViewContents";
interface Props
{
table?: QTableMetaData;
launchProcess?: QProcessMetaData;
}
function EntityView({table, launchProcess}: Props): JSX.Element
{
const {id} = useParams();
return (
<BaseLayout>
<MDBox>
<Grid container>
<Grid item xs={12}>
<MDBox mb={3}>
<ViewContents table={table} id={id} launchProcess={launchProcess}/>
</MDBox>
</Grid>
</Grid>
</MDBox>
</BaseLayout>
);
}
EntityView.defaultProps = {
table: null,
launchProcess: null
};
export default EntityView;

View File

@ -35,6 +35,7 @@ import Tooltip from "@mui/material/Tooltip";
import React, {useState} from "react"; import React, {useState} from "react";
import MDBox from "components/MDBox"; import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography"; import MDTypography from "components/MDTypography";
import CustomWidthTooltip from "qqq/components/CustomWidthTooltip/CustomWidthTooltip";
import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine"; import {ProcessSummaryLine} from "qqq/pages/process-run/model/ProcessSummaryLine";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
import QValueUtils from "qqq/utils/QValueUtils"; import QValueUtils from "qqq/utils/QValueUtils";
@ -87,15 +88,6 @@ function QValidationReview({
setPreviewRecordIndex(newIndex); setPreviewRecordIndex(newIndex);
}; };
const CustomWidthTooltip = styled(({className, ...props}: TooltipProps) => (
<Tooltip {...props} classes={{popper: className}} />
))({
[`& .${tooltipClasses.tooltip}`]: {
maxWidth: 500,
textAlign: "left",
},
});
const buildDoFullValidationRadioListItem = (value: "true" | "false", labelText: string, tooltipHTML: JSX.Element): JSX.Element => const buildDoFullValidationRadioListItem = (value: "true" | "false", labelText: string, tooltipHTML: JSX.Element): JSX.Element =>
{ {
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -261,7 +253,7 @@ function QValidationReview({
{" "} {" "}
&nbsp; &nbsp;
{" "} {" "}
{QValueUtils.getDisplayValue(field, previewRecords[previewRecordIndex])} {QValueUtils.getDisplayValue(field, previewRecords[previewRecordIndex], "view")}
</MDBox> </MDBox>
)) ))
} }

View File

@ -362,8 +362,11 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
{localTableSections.map((section: QTableSection, index: number) => {localTableSections.map((section: QTableSection, index: number) =>
{ {
const name = section.name const name = section.name
console.log(formData);
console.log(section.fieldNames); if(section.isHidden)
{
return ;
}
const sectionFormFields = {}; const sectionFormFields = {};
for(let i = 0; i<section.fieldNames.length; i++) for(let i = 0; i<section.fieldNames.length; i++)
@ -424,7 +427,7 @@ function ProcessRun({process, defaultProcessValues, isModal, recordIds, closeMod
: &nbsp; : &nbsp;
</MDTypography> </MDTypography>
<MDTypography variant="button" fontWeight="regular" color="text"> <MDTypography variant="button" fontWeight="regular" color="text">
{QValueUtils.getValueForDisplay(field, processValues[field.name])} {QValueUtils.getValueForDisplay(field, processValues[field.name], "view")}
</MDTypography> </MDTypography>
</MDBox> </MDBox>
))} ))}

View File

@ -238,3 +238,24 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
color: gray; color: gray;
right: 0.125rem; right: 0.125rem;
} }
.devDocumentation ul>li
{
margin-left: 30px;
}
.devDocumentation *
{
line-height: 1.5;
}
.devDocumentation p
{
margin-top: .5rem;
margin-bottom: .5rem;
}
.devDocumentation code
{
white-space: pre-wrap;
}

View File

@ -25,9 +25,9 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import "datejs"; import "datejs";
import {Chip, Icon} from "@mui/material"; import {Chip, Icon, Typography} from "@mui/material";
import {queryByTestId} from "@testing-library/react";
import React, {Fragment} from "react"; import React, {Fragment} from "react";
import AceEditor from "react-ace";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
@ -67,19 +67,19 @@ class QValueUtils
** When you have a field, and a record - call this method to get a string or ** When you have a field, and a record - call this method to get a string or
** element back to display the field's value. ** element back to display the field's value.
*******************************************************************************/ *******************************************************************************/
public static getDisplayValue(field: QFieldMetaData, record: QRecord): string | JSX.Element public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view"): string | JSX.Element
{ {
const displayValue = record.displayValues ? record.displayValues.get(field.name) : undefined; const displayValue = record.displayValues ? record.displayValues.get(field.name) : undefined;
const rawValue = record.values ? record.values.get(field.name) : undefined; const rawValue = record.values ? record.values.get(field.name) : undefined;
return QValueUtils.getValueForDisplay(field, rawValue, displayValue); return QValueUtils.getValueForDisplay(field, rawValue, displayValue, usage);
} }
/******************************************************************************* /*******************************************************************************
** When you have a field and a value (either just a raw value, or a raw and ** When you have a field and a value (either just a raw value, or a raw and
** display value), call this method to get a string Element to display. ** display value), call this method to get a string Element to display.
*******************************************************************************/ *******************************************************************************/
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue): string | JSX.Element public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view"): string | JSX.Element
{ {
if (field.hasAdornment(AdornmentType.LINK)) if (field.hasAdornment(AdornmentType.LINK))
{ {
@ -100,7 +100,7 @@ class QValueUtils
if (!tablePath.endsWith("/")) if (!tablePath.endsWith("/"))
{ {
tablePath += "/" tablePath += "/";
} }
href = tablePath + rawValue; href = tablePath + rawValue;
} }
@ -120,11 +120,11 @@ class QValueUtils
if (href.startsWith("http")) if (href.startsWith("http"))
{ {
return (<a target={adornment.getValue("target") ?? "_self"} href={href} onClick={(e) => e.stopPropagation()}>{displayValue ?? rawValue}</a>) return (<a target={adornment.getValue("target") ?? "_self"} href={href} onClick={(e) => e.stopPropagation()}>{displayValue ?? rawValue}</a>);
} }
else else
{ {
return (<Link target={adornment.getValue("target") ?? "_self"} to={href} onClick={(e) => e.stopPropagation()}>{displayValue ?? rawValue}</Link>) return (<Link target={adornment.getValue("target") ?? "_self"} to={href} onClick={(e) => e.stopPropagation()}>{displayValue ?? rawValue}</Link>);
} }
} }
@ -136,12 +136,34 @@ class QValueUtils
} }
const adornment = field.getAdornment(AdornmentType.CHIP); const adornment = field.getAdornment(AdornmentType.CHIP);
const color = adornment.getValue("color." + rawValue) ?? "default" const color = adornment.getValue("color." + rawValue) ?? "default";
const iconName = adornment.getValue("icon." + rawValue) ?? null; const iconName = adornment.getValue("icon." + rawValue) ?? null;
const iconElement = iconName ? <Icon>{iconName}</Icon> : null; const iconElement = iconName ? <Icon>{iconName}</Icon> : null;
return (<Chip label={displayValue} color={color} icon={iconElement} size="small" variant="outlined" sx={{fontWeight: 500}} />); return (<Chip label={displayValue} color={color} icon={iconElement} size="small" variant="outlined" sx={{fontWeight: 500}} />);
} }
if (field.hasAdornment(AdornmentType.CODE_EDITOR))
{
if(usage === "view")
{
return (<AceEditor
mode="javascript"
theme="github"
name={field.name}
editorProps={{$blockScrolling: true}}
value={rawValue}
readOnly
width="100%"
showPrintMargin={false}
height="200px"
/>);
}
else
{
return rawValue;
}
}
return (QValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue)); return (QValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
} }
@ -158,8 +180,7 @@ class QValueUtils
return (""); return ("");
} }
const date = new Date(rawValue); const date = new Date(rawValue);
// @ts-ignore return this.formatDateTime(date);
return (`${date.toString("yyyy-MM-dd hh:mm:ss")} ${date.getHours() < 12 ? "AM" : "PM"} ${date.getTimezone()}`);
} }
else if (field.type === QFieldType.DATE) else if (field.type === QFieldType.DATE)
{ {
@ -185,6 +206,29 @@ class QValueUtils
return (returnValue); return (returnValue);
} }
public static formatDateTime(date: Date)
{
if(!(date instanceof Date))
{
date = new Date(date)
}
// @ts-ignore
return (`${date.toString("yyyy-MM-dd hh:mm:ss")} ${date.getHours() < 12 ? "AM" : "PM"} ${date.getTimezone()}`);
}
public static formatBoolean(value: any)
{
if(value === true)
{
return ("Yes");
}
else if(value === false)
{
return ("No");
}
return (null);
}
public static getFormattedNumber(n: number): string public static getFormattedNumber(n: number): string
{ {
try try
@ -206,6 +250,11 @@ class QValueUtils
public static breakTextIntoLines(value: string): JSX.Element public static breakTextIntoLines(value: string): JSX.Element
{ {
if(!value)
{
return <Fragment />;
}
return ( return (
<Fragment> <Fragment>
{value.split(/\n/).map((value: string, index: number) => ( {value.split(/\n/).map((value: string, index: number) => (