mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
Merge branch 'feature/sprint-21' into dev
This commit is contained in:
@ -64,6 +64,8 @@ commands:
|
|||||||
paths:
|
paths:
|
||||||
- ~/.m2
|
- ~/.m2
|
||||||
key: v1-dependencies-{{ checksum "pom.xml" }}
|
key: v1-dependencies-{{ checksum "pom.xml" }}
|
||||||
|
- store_artifacts:
|
||||||
|
path: /tmp/QSeleniumScreenshots
|
||||||
|
|
||||||
mvn_deploy:
|
mvn_deploy:
|
||||||
steps:
|
steps:
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,6 +18,7 @@ yalc.lock
|
|||||||
/build
|
/build
|
||||||
/lib
|
/lib
|
||||||
/target
|
/target
|
||||||
|
/log
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
1261
package-lock.json
generated
1261
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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.52",
|
"@kingsrook/qqq-frontend-core": "1.0.54",
|
||||||
"@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",
|
||||||
|
12
pom.xml
12
pom.xml
@ -94,6 +94,18 @@
|
|||||||
<version>20220924</version>
|
<version>20220924</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
|
<artifactId>log4j-api</artifactId>
|
||||||
|
<version>2.17.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.logging.log4j</groupId>
|
||||||
|
<artifactId>log4j-core</artifactId>
|
||||||
|
<version>2.17.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -131,6 +131,7 @@ function QDynamicForm(props: Props): JSX.Element
|
|||||||
<Grid item xs={12} sm={6} key={fieldName}>
|
<Grid item xs={12} sm={6} key={fieldName}>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={field.possibleValueProps.tableName}
|
tableName={field.possibleValueProps.tableName}
|
||||||
|
processName={field.possibleValueProps.processName}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
isEditable={field.isEditable}
|
isEditable={field.isEditable}
|
||||||
fieldLabel={field.label}
|
fieldLabel={field.label}
|
||||||
|
@ -109,7 +109,7 @@ class DynamicFormUtils
|
|||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static addPossibleValueProps(dynamicFormFields: any, qFields: QFieldMetaData[], tableName: string, displayValues: Map<string, string>)
|
public static addPossibleValueProps(dynamicFormFields: any, qFields: QFieldMetaData[], tableName: string, processName: string, displayValues: Map<string, string>)
|
||||||
{
|
{
|
||||||
for (let i = 0; i < qFields.length; i++)
|
for (let i = 0; i < qFields.length; i++)
|
||||||
{
|
{
|
||||||
@ -126,12 +126,24 @@ class DynamicFormUtils
|
|||||||
initialDisplayValue = displayValues.get(field.name);
|
initialDisplayValue = displayValues.get(field.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicFormFields[field.name].possibleValueProps =
|
if (tableName)
|
||||||
{
|
{
|
||||||
isPossibleValue: true,
|
dynamicFormFields[field.name].possibleValueProps =
|
||||||
tableName: tableName,
|
{
|
||||||
initialDisplayValue: initialDisplayValue,
|
isPossibleValue: true,
|
||||||
};
|
tableName: tableName,
|
||||||
|
initialDisplayValue: initialDisplayValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dynamicFormFields[field.name].possibleValueProps =
|
||||||
|
{
|
||||||
|
isPossibleValue: true,
|
||||||
|
processName: processName,
|
||||||
|
initialDisplayValue: initialDisplayValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,8 @@ import Client from "qqq/utils/qqq/Client";
|
|||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
tableName: string;
|
tableName?: string;
|
||||||
|
processName?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
fieldLabel: string;
|
fieldLabel: string;
|
||||||
inForm: boolean;
|
inForm: boolean;
|
||||||
@ -50,6 +51,8 @@ interface Props
|
|||||||
}
|
}
|
||||||
|
|
||||||
DynamicSelect.defaultProps = {
|
DynamicSelect.defaultProps = {
|
||||||
|
tableName: null,
|
||||||
|
processName: null,
|
||||||
inForm: true,
|
inForm: true,
|
||||||
initialValue: null,
|
initialValue: null,
|
||||||
initialDisplayValue: null,
|
initialDisplayValue: null,
|
||||||
@ -65,7 +68,7 @@ DynamicSelect.defaultProps = {
|
|||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function DynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler}: Props)
|
function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, 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[]>([]);
|
||||||
@ -109,9 +112,9 @@ function DynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue,
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
// console.log(`doing a search with ${searchTerm}`);
|
// console.log(`doing a search with ${searchTerm}`);
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, fieldName, searchTerm ?? "");
|
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, fieldName, searchTerm ?? "");
|
||||||
|
|
||||||
if(tableMetaData == null)
|
if(tableMetaData == null && tableName)
|
||||||
{
|
{
|
||||||
let tableMetaData: QTableMetaData = await qController.loadTableMetaData(tableName);
|
let tableMetaData: QTableMetaData = await qController.loadTableMetaData(tableName);
|
||||||
setTableMetaData(tableMetaData);
|
setTableMetaData(tableMetaData);
|
||||||
@ -134,7 +137,7 @@ function DynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue,
|
|||||||
|
|
||||||
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
||||||
{
|
{
|
||||||
console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
||||||
if(reason !== "reset")
|
if(reason !== "reset")
|
||||||
{
|
{
|
||||||
// console.log(` -> setting search term to ${value}`);
|
// console.log(` -> setting search term to ${value}`);
|
||||||
@ -186,7 +189,7 @@ function DynamicSelect({tableName, fieldName, fieldLabel, inForm, initialValue,
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const field = tableMetaData.fields.get(fieldName)
|
const field = tableMetaData?.fields.get(fieldName)
|
||||||
if(field)
|
if(field)
|
||||||
{
|
{
|
||||||
const adornment = field.getAdornment(AdornmentType.CHIP);
|
const adornment = field.getAdornment(AdornmentType.CHIP);
|
||||||
|
@ -236,7 +236,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (fieldMetaData.possibleValueSourceName)
|
if (fieldMetaData.possibleValueSourceName)
|
||||||
{
|
{
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, fieldName, null, [initialValues[fieldName]]);
|
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
|
||||||
if (results && results.length > 0)
|
if (results && results.length > 0)
|
||||||
{
|
{
|
||||||
defaultDisplayValues.set(fieldName, results[0].label);
|
defaultDisplayValues.set(fieldName, results[0].label);
|
||||||
@ -268,7 +268,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
dynamicFormFields,
|
dynamicFormFields,
|
||||||
formValidations,
|
formValidations,
|
||||||
} = DynamicFormUtils.getFormData(fieldArray);
|
} = DynamicFormUtils.getFormData(fieldArray);
|
||||||
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, record ? record.displayValues : defaultDisplayValues);
|
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, null, record ? record.displayValues : defaultDisplayValues);
|
||||||
|
|
||||||
if(disabledFields)
|
if(disabledFields)
|
||||||
{
|
{
|
||||||
|
@ -62,7 +62,7 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
|||||||
const routes: string[] | any = route.slice(0, -1);
|
const routes: string[] | any = route.slice(0, -1);
|
||||||
const {pageHeader, setPageHeader} = useContext(QContext);
|
const {pageHeader, setPageHeader} = useContext(QContext);
|
||||||
|
|
||||||
let pageTitle = "Nutrifresh One";
|
let pageTitle = "ColdTrack Live";
|
||||||
const fullRoutes: string[] = [];
|
const fullRoutes: string[] = [];
|
||||||
let accumulatedPath = "";
|
let accumulatedPath = "";
|
||||||
for (let i = 0; i < routes.length; i++)
|
for (let i = 0; i < routes.length; i++)
|
||||||
|
@ -98,7 +98,7 @@ function SavedFilters({qController, metaData, tableMetaData, currentSavedFilter,
|
|||||||
{
|
{
|
||||||
if (currentSavedFilter != null)
|
if (currentSavedFilter != null)
|
||||||
{
|
{
|
||||||
let qFilter = FilterUtils.buildQFilterFromGridFilter(filterModel, columnSortModel);
|
let qFilter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel);
|
||||||
setFilterIsModified(JSON.stringify(qFilter) !== currentSavedFilter.values.get("filterJson"));
|
setFilterIsModified(JSON.stringify(qFilter) !== currentSavedFilter.values.get("filterJson"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +200,7 @@ function SavedFilters({qController, metaData, tableMetaData, currentSavedFilter,
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
formData.append("tableName", tableMetaData.name);
|
formData.append("tableName", tableMetaData.name);
|
||||||
formData.append("filterJson", JSON.stringify(FilterUtils.buildQFilterFromGridFilter(filterModel, columnSortModel)));
|
formData.append("filterJson", JSON.stringify(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel)));
|
||||||
|
|
||||||
if (isSaveFilterAs || isRenameFilter || currentSavedFilter == null)
|
if (isSaveFilterAs || isRenameFilter || currentSavedFilter == null)
|
||||||
{
|
{
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {Map} from "@mui/icons-material";
|
|
||||||
import {Typography} from "@mui/material";
|
import {Typography} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
@ -75,7 +74,6 @@ function ScriptTestForm({scriptDefinition, tableName, fieldName, recordId, code}
|
|||||||
|
|
||||||
const testScript = () =>
|
const testScript = () =>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
|
||||||
const inputValues = new Map<string, any>();
|
const inputValues = new Map<string, any>();
|
||||||
if (scriptDefinition.testInputFields)
|
if (scriptDefinition.testInputFields)
|
||||||
{
|
{
|
||||||
|
@ -85,6 +85,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
setWidgetData([]);
|
||||||
for (let i = 0; i < widgetMetaDataList.length; i++)
|
for (let i = 0; i < widgetMetaDataList.length; i++)
|
||||||
{
|
{
|
||||||
const widgetMetaData = widgetMetaDataList[i];
|
const widgetMetaData = widgetMetaDataList[i];
|
||||||
@ -96,17 +97,21 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
widgetData[i] = await qController.widget(widgetMetaData.name, urlParams);
|
widgetData[i] = await qController.widget(widgetMetaData.name, urlParams);
|
||||||
|
setWidgetData(widgetData);
|
||||||
setWidgetCounter(widgetCounter + 1);
|
setWidgetCounter(widgetCounter + 1);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
setWidgetData(widgetData);
|
|
||||||
}, [widgetMetaDataList]);
|
}, [widgetMetaDataList]);
|
||||||
|
|
||||||
const reloadWidget = async (index: number, data: string) =>
|
const reloadWidget = async (index: number, data: string) =>
|
||||||
{
|
{
|
||||||
widgetData[index] = await qController.widget(widgetMetaDataList[index].name, getQueryParams(null, data));
|
(async() =>
|
||||||
forceUpdate();
|
{
|
||||||
|
widgetData[index] = await qController.widget(widgetMetaDataList[index].name, getQueryParams(null, data));
|
||||||
|
setWidgetData(widgetData);
|
||||||
|
forceUpdate();
|
||||||
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string
|
||||||
@ -224,7 +229,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData[i]}
|
widgetData={widgetData[i]}
|
||||||
reloadWidgetCallback={(data) => reloadWidget(i, data)}>
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}>
|
||||||
<div>
|
<div className="widgetProcessMidDiv" style={{height: "100%"}}>
|
||||||
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
|
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
|
||||||
</div>
|
</div>
|
||||||
</Widget>
|
</Widget>
|
||||||
@ -271,20 +276,18 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "statistics" && (
|
widgetMetaData.type === "statistics" && (
|
||||||
widgetData && widgetData[i] && (
|
<Widget
|
||||||
<Widget
|
widgetMetaData={widgetMetaData}
|
||||||
widgetMetaData={widgetMetaData}
|
widgetData={widgetData[i]}
|
||||||
widgetData={widgetData[i]}
|
isChild={areChildren}
|
||||||
isChild={areChildren}
|
|
||||||
|
|
||||||
// reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
// reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
>
|
>
|
||||||
<StatisticsCard
|
<StatisticsCard
|
||||||
data={widgetData[i]}
|
data={widgetData[i]}
|
||||||
increaseIsGood={true}
|
increaseIsGood={true}
|
||||||
/>
|
/>
|
||||||
</Widget>
|
</Widget>
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
@ -285,7 +285,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
|
|||||||
|
|
||||||
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
|
||||||
const widgetContent =
|
const widgetContent =
|
||||||
<Box sx={{width: "100%"}}>
|
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
|
||||||
<Box pr={3} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}}>
|
<Box pr={3} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}}>
|
||||||
<Box pt={2}>
|
<Box pt={2}>
|
||||||
{
|
{
|
||||||
|
@ -85,7 +85,7 @@ function PieChart({description, chartData}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1}}>
|
<Card sx={{minHeight: "400px", boxShadow: "none", height: "100%", width: "100%", display: "flex", flexGrow: 1}}>
|
||||||
<Box mt={3}>
|
<Box mt={3}>
|
||||||
<Grid container alignItems="center">
|
<Grid container alignItems="center">
|
||||||
<Grid item xs={12} justifyContent="center">
|
<Grid item xs={12} justifyContent="center">
|
||||||
|
@ -55,6 +55,10 @@ StatisticsCard.defaultProps = {
|
|||||||
|
|
||||||
function StatisticsCard({data, increaseIsGood}: Props): JSX.Element
|
function StatisticsCard({data, increaseIsGood}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
|
if(! data)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const {count, percentageAmount, percentageLabel} = data;
|
const {count, percentageAmount, percentageLabel} = data;
|
||||||
|
|
||||||
let percentageString = "";
|
let percentageString = "";
|
||||||
@ -82,7 +86,7 @@ function StatisticsCard({data, increaseIsGood}: Props): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Box mt={0} sx={{height: "100%", flexGrow: 1, flexDirection: "column", display: "flex", paddingTop: "0px"}}>
|
<Box mt={0} sx={{minHeight: "112px", height: "100%", flexGrow: 1, flexDirection: "column", display: "flex", paddingTop: "0px"}}>
|
||||||
<Box mt={0} display="flex" justifyContent="center">
|
<Box mt={0} display="flex" justifyContent="center">
|
||||||
{
|
{
|
||||||
count !== undefined ? (
|
count !== undefined ? (
|
||||||
@ -96,7 +100,7 @@ function StatisticsCard({data, increaseIsGood}: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<CircularProgress sx={{marginTop: "1rem", marginBottom: "20px"}} color="inherit" size={data?.countFontSize ? data.countFontSize : 30}/>
|
<CircularProgress sx={{marginTop: "1rem", paddingBottom: "25px"}} color="inherit" size={data?.countFontSize ? data.countFontSize : 23}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -38,7 +38,6 @@ import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/mater
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import Link from "@mui/material/Link";
|
|
||||||
import Step from "@mui/material/Step";
|
import Step from "@mui/material/Step";
|
||||||
import StepLabel from "@mui/material/StepLabel";
|
import StepLabel from "@mui/material/StepLabel";
|
||||||
import Stepper from "@mui/material/Stepper";
|
import Stepper from "@mui/material/Stepper";
|
||||||
@ -46,6 +45,7 @@ import Typography from "@mui/material/Typography";
|
|||||||
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import {Form, Formik} from "formik";
|
import {Form, Formik} from "formik";
|
||||||
|
import parse from "html-react-parser";
|
||||||
import React, {useContext, useEffect, useState} from "react";
|
import React, {useContext, useEffect, useState} from "react";
|
||||||
import {useLocation, useNavigate, useParams} from "react-router-dom";
|
import {useLocation, useNavigate, useParams} from "react-router-dom";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
@ -75,7 +75,7 @@ interface Props
|
|||||||
recordIds?: string | QQueryFilter;
|
recordIds?: string | QQueryFilter;
|
||||||
closeModalHandler?: (event: object, reason: string) => void;
|
closeModalHandler?: (event: object, reason: string) => void;
|
||||||
forceReInit?: number;
|
forceReInit?: number;
|
||||||
overrideLabel?: string
|
overrideLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_RETRY_MILLIS = 1_500;
|
const INITIAL_RETRY_MILLIS = 1_500;
|
||||||
@ -225,12 +225,12 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
xhr.open("POST", url);
|
xhr.open("POST", url);
|
||||||
xhr.responseType = "blob";
|
xhr.responseType = "blob";
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append("Authorization", qController.getAuthorizationHeaderValue())
|
formData.append("Authorization", qController.getAuthorizationHeaderValue());
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
|
|
||||||
xhr.onload = function(e)
|
xhr.onload = function (e)
|
||||||
{
|
{
|
||||||
if (this.status == 200)
|
if (this.status == 200)
|
||||||
{
|
{
|
||||||
@ -247,7 +247,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
setProcessError("Error downloading file", true)
|
setProcessError("Error downloading file", true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -299,7 +299,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
<Box component="div" py={3}>
|
<Box component="div" py={3}>
|
||||||
<Grid container justifyContent="flex-end" spacing={3}>
|
<Grid container justifyContent="flex-end" spacing={3}>
|
||||||
{isModal ? <QCancelButton onClickHandler={handleCancelClicked} disabled={false} label="Close" />
|
{isModal ? <QCancelButton onClickHandler={handleCancelClicked} disabled={false} label="Close" />
|
||||||
: <QCancelButton onClickHandler={handleCancelClicked} disabled={false} />
|
: !isWidget && <QCancelButton onClickHandler={handleCancelClicked} disabled={false} />
|
||||||
}
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@ -350,7 +350,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
|
|
||||||
const {formFields, values, errors, touched} = formData;
|
const {formFields, values, errors, touched} = formData;
|
||||||
let localTableSections = tableSections;
|
let localTableSections = tableSections;
|
||||||
if(localTableSections == null)
|
if (localTableSections == null)
|
||||||
{
|
{
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if the table sections (ones that actually have fields to edit) haven't been built yet, do so now //
|
// if the table sections (ones that actually have fields to edit) haven't been built yet, do so now //
|
||||||
@ -359,6 +359,23 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
setTableSections(localTableSections);
|
setTableSections(localTableSections);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there are any fields that are possible values, they need to know what their //
|
||||||
|
// initial value to display should be. //
|
||||||
|
// this **needs to be** the actual PVS LABEL - not the raw value (e.g, PVS ID) //
|
||||||
|
// but our first use case, they're the same, so... this needs fixed. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(formFields && processValues)
|
||||||
|
{
|
||||||
|
Object.keys(formFields).forEach((key) =>
|
||||||
|
{
|
||||||
|
if(formFields[key].possibleValueProps && processValues[key])
|
||||||
|
{
|
||||||
|
formFields[key].possibleValueProps.initialDisplayValue = processValues[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
@ -420,25 +437,25 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
|
|
||||||
{localTableSections.map((section: QTableSection, index: number) =>
|
{localTableSections.map((section: QTableSection, index: number) =>
|
||||||
{
|
{
|
||||||
const name = section.name
|
const name = section.name;
|
||||||
|
|
||||||
if(section.isHidden)
|
if (section.isHidden)
|
||||||
{
|
{
|
||||||
return ;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sectionFormFields = {};
|
const sectionFormFields = {};
|
||||||
for(let i = 0; i<section.fieldNames.length; i++)
|
for (let i = 0; i < section.fieldNames.length; i++)
|
||||||
{
|
{
|
||||||
const fieldName = section.fieldNames[i];
|
const fieldName = section.fieldNames[i];
|
||||||
if(formFields[fieldName])
|
if (formFields[fieldName])
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
sectionFormFields[fieldName] = formFields[fieldName];
|
sectionFormFields[fieldName] = formFields[fieldName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Object.keys(sectionFormFields).length > 0)
|
if (Object.keys(sectionFormFields).length > 0)
|
||||||
{
|
{
|
||||||
const sectionFormData = {
|
const sectionFormData = {
|
||||||
formFields: sectionFormFields,
|
formFields: sectionFormFields,
|
||||||
@ -589,6 +606,14 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
component.type === QComponentType.HTML && (
|
||||||
|
processValues[`${step.name}.html`] &&
|
||||||
|
<Box fontSize="1rem">
|
||||||
|
{parse(processValues[`${step.name}.html`])}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)))}
|
)))}
|
||||||
</>
|
</>
|
||||||
@ -655,7 +680,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(! isWidget)
|
if (!isWidget)
|
||||||
{
|
{
|
||||||
setPageHeader(overrideLabel ?? processMetaData.label);
|
setPageHeader(overrideLabel ?? processMetaData.label);
|
||||||
}
|
}
|
||||||
@ -701,10 +726,9 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
{
|
{
|
||||||
let fullFieldList = getFullFieldList(activeStep, processValues);
|
let fullFieldList = getFullFieldList(activeStep, processValues);
|
||||||
const formData = DynamicFormUtils.getFormData(fullFieldList);
|
const formData = DynamicFormUtils.getFormData(fullFieldList);
|
||||||
if(tableMetaData)
|
|
||||||
{
|
const possibleValueDisplayValues = new Map<string, string>();
|
||||||
DynamicFormUtils.addPossibleValueProps(formData.dynamicFormFields, fullFieldList, tableMetaData.name, null);
|
DynamicFormUtils.addPossibleValueProps(formData.dynamicFormFields, fullFieldList, tableMetaData?.name, processName, possibleValueDisplayValues);
|
||||||
}
|
|
||||||
|
|
||||||
dynamicFormFields = formData.dynamicFormFields;
|
dynamicFormFields = formData.dynamicFormFields;
|
||||||
formValidations = formData.formValidations;
|
formValidations = formData.formValidations;
|
||||||
@ -819,7 +843,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, fullFieldList, tableMetaData.name, null);
|
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, fullFieldList, tableMetaData.name, null, null);
|
||||||
|
|
||||||
setFormFields(newDynamicFormFields);
|
setFormFields(newDynamicFormFields);
|
||||||
setValidationScheme(Yup.object().shape(newFormValidations));
|
setValidationScheme(Yup.object().shape(newFormValidations));
|
||||||
@ -981,11 +1005,11 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
{
|
{
|
||||||
if ((e as QException).status === "403")
|
if ((e as QException).status === "403")
|
||||||
{
|
{
|
||||||
setProcessError(`You do not have permission to run this ${isReport ? "report" : "process"}.`, true)
|
setProcessError(`You do not have permission to run this ${isReport ? "report" : "process"}.`, true);
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
return (false);
|
return (false);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -1175,7 +1199,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
mainCardStyles.background = "none";
|
mainCardStyles.background = "none";
|
||||||
mainCardStyles.boxShadow = "none";
|
mainCardStyles.boxShadow = "none";
|
||||||
}
|
}
|
||||||
if(isWidget)
|
if (isWidget)
|
||||||
{
|
{
|
||||||
mainCardStyles.background = "none";
|
mainCardStyles.background = "none";
|
||||||
mainCardStyles.boxShadow = "none";
|
mainCardStyles.boxShadow = "none";
|
||||||
@ -1231,10 +1255,10 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
}
|
}
|
||||||
|
|
||||||
<Box p={3}>
|
<Box p={3}>
|
||||||
<Box>
|
<Box pb={isWidget ? 6 : "initial"}>
|
||||||
{/***************************************************************************
|
{/***************************************************************************
|
||||||
** step content - e.g., the appropriate form or other screen for the step **
|
** step content - e.g., the appropriate form or other screen for the step **
|
||||||
***************************************************************************/}
|
***************************************************************************/}
|
||||||
{getDynamicStepContent(
|
{getDynamicStepContent(
|
||||||
activeStepIndex,
|
activeStepIndex,
|
||||||
activeStep,
|
activeStep,
|
||||||
@ -1250,9 +1274,9 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
setFieldValue,
|
setFieldValue,
|
||||||
)}
|
)}
|
||||||
{/********************************
|
{/********************************
|
||||||
** back &| next/submit buttons **
|
** back &| next/submit buttons **
|
||||||
********************************/}
|
********************************/}
|
||||||
<Box mt={6} width="100%" display="flex" justifyContent="space-between">
|
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
||||||
{true || activeStepIndex === 0 ? (
|
{true || activeStepIndex === 0 ? (
|
||||||
<Box />
|
<Box />
|
||||||
) : (
|
) : (
|
||||||
@ -1279,7 +1303,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
<Box component="div" py={3}>
|
<Box component="div" py={3}>
|
||||||
<Grid container justifyContent="flex-end" spacing={3}>
|
<Grid container justifyContent="flex-end" spacing={3}>
|
||||||
{
|
{
|
||||||
! isWidget && (
|
!isWidget && (
|
||||||
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
|
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1320,7 +1344,7 @@ function ProcessRun({process, defaultProcessValues, isModal, isWidget, isReport,
|
|||||||
else if (isWidget)
|
else if (isWidget)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}>
|
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", height: "100%"}}>
|
||||||
{form}
|
{form}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -486,9 +486,46 @@ const stringNotEndWithOperator: GridFilterOperator = {
|
|||||||
InputComponent: GridFilterInputValue,
|
InputComponent: GridFilterInputValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getListValueString = (value: GridFilterItem["value"]): string =>
|
||||||
|
{
|
||||||
|
if (value && value.length)
|
||||||
|
{
|
||||||
|
let labels = [] as string[];
|
||||||
|
|
||||||
|
let maxLoops = value.length;
|
||||||
|
if(maxLoops > 5)
|
||||||
|
{
|
||||||
|
maxLoops = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLoops; i++)
|
||||||
|
{
|
||||||
|
labels.push(value[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(maxLoops < value.length)
|
||||||
|
{
|
||||||
|
labels.push(" and " + (value.length - maxLoops) + " other values.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (labels.join(", "));
|
||||||
|
}
|
||||||
|
return (value);
|
||||||
|
};
|
||||||
|
|
||||||
const stringIsAnyOfOperator: GridFilterOperator = {
|
const stringIsAnyOfOperator: GridFilterOperator = {
|
||||||
label: "is any of",
|
label: "is any of",
|
||||||
value: "isAnyOf",
|
value: "isAnyOf",
|
||||||
|
getValueAsString: getListValueString,
|
||||||
|
getApplyFilterFn: () => null,
|
||||||
|
// @ts-ignore
|
||||||
|
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("text", props)
|
||||||
|
};
|
||||||
|
|
||||||
|
const stringIsNoneOfOperator: GridFilterOperator = {
|
||||||
|
label: "is none of",
|
||||||
|
value: "isNone",
|
||||||
|
getValueAsString: getListValueString,
|
||||||
getApplyFilterFn: () => null,
|
getApplyFilterFn: () => null,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("text", props)
|
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("text", props)
|
||||||
@ -504,7 +541,7 @@ let endsWith = gridStringOperators.splice(0, 1)[0];
|
|||||||
// remove default isany operator //
|
// remove default isany operator //
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
gridStringOperators.splice(2, 1)[0];
|
gridStringOperators.splice(2, 1)[0];
|
||||||
gridStringOperators = [equals, stringNotEqualsOperator, contains, stringNotContainsOperator, startsWith, stringNotStartsWithOperator, endsWith, stringNotEndWithOperator, ...gridStringOperators, stringIsAnyOfOperator];
|
gridStringOperators = [equals, stringNotEqualsOperator, contains, stringNotContainsOperator, startsWith, stringNotStartsWithOperator, endsWith, stringNotEndWithOperator, ...gridStringOperators, stringIsAnyOfOperator, stringIsNoneOfOperator];
|
||||||
|
|
||||||
export const QGridStringOperators = gridStringOperators;
|
export const QGridStringOperators = gridStringOperators;
|
||||||
|
|
||||||
@ -620,6 +657,16 @@ const numericIsAnyOfOperator: GridFilterOperator = {
|
|||||||
label: "is any of",
|
label: "is any of",
|
||||||
value: "isAnyOf",
|
value: "isAnyOf",
|
||||||
getApplyFilterFn: () => null,
|
getApplyFilterFn: () => null,
|
||||||
|
getValueAsString: getListValueString,
|
||||||
|
// @ts-ignore
|
||||||
|
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("number", props)
|
||||||
|
};
|
||||||
|
|
||||||
|
const numericIsNoneOfOperator: GridFilterOperator = {
|
||||||
|
label: "is none of",
|
||||||
|
value: "isNone",
|
||||||
|
getApplyFilterFn: () => null,
|
||||||
|
getValueAsString: getListValueString,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("number", props)
|
InputComponent: (props: GridFilterInputMultipleValueProps<GridApiCommunity>) => CustomIsAnyInput("number", props)
|
||||||
};
|
};
|
||||||
@ -629,7 +676,7 @@ const numericIsAnyOfOperator: GridFilterOperator = {
|
|||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
let gridNumericOperators = getGridNumericOperators();
|
let gridNumericOperators = getGridNumericOperators();
|
||||||
gridNumericOperators.splice(8, 1)[0];
|
gridNumericOperators.splice(8, 1)[0];
|
||||||
export const QGridNumericOperators = [...gridNumericOperators, betweenOperator, notBetweenOperator, numericIsAnyOfOperator];
|
export const QGridNumericOperators = [...gridNumericOperators, betweenOperator, notBetweenOperator, numericIsAnyOfOperator, numericIsNoneOfOperator];
|
||||||
|
|
||||||
///////////////////////
|
///////////////////////
|
||||||
// boolean operators //
|
// boolean operators //
|
||||||
@ -800,11 +847,17 @@ function InputPossibleValueSourceMultiple(tableName: string, field: QFieldMetaDa
|
|||||||
|
|
||||||
const getPvsValueString = (value: GridFilterItem["value"]): string =>
|
const getPvsValueString = (value: GridFilterItem["value"]): string =>
|
||||||
{
|
{
|
||||||
console.log("get pvs value", value);
|
|
||||||
if (value && value.length)
|
if (value && value.length)
|
||||||
{
|
{
|
||||||
let labels = [] as string[];
|
let labels = [] as string[];
|
||||||
for (let i = 0; i < value.length; i++)
|
|
||||||
|
let maxLoops = value.length;
|
||||||
|
if(maxLoops > 5)
|
||||||
|
{
|
||||||
|
maxLoops = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLoops; i++)
|
||||||
{
|
{
|
||||||
if(value[i] && value[i].label)
|
if(value[i] && value[i].label)
|
||||||
{
|
{
|
||||||
@ -815,6 +868,12 @@ const getPvsValueString = (value: GridFilterItem["value"]): string =>
|
|||||||
labels.push(value);
|
labels.push(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(maxLoops < value.length)
|
||||||
|
{
|
||||||
|
labels.push(" and " + (value.length - maxLoops) + " other values.");
|
||||||
|
}
|
||||||
|
|
||||||
return (labels.join(", "));
|
return (labels.join(", "));
|
||||||
}
|
}
|
||||||
else if (value && value.label)
|
else if (value && value.label)
|
||||||
|
@ -27,7 +27,7 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo
|
|||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {Alert, TablePagination} from "@mui/material";
|
import {Alert, Collapse, TablePagination} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
@ -44,9 +44,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 Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenu, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
import React, {useContext, useEffect, useReducer, useRef, useState} from "react";
|
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||||
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import {QActionsMenuButton, QCreateNewButton} from "qqq/components/buttons/DefaultButtons";
|
import {QActionsMenuButton, QCreateNewButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
@ -57,6 +57,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 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";
|
||||||
const COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT = "qqq.columnVisibility";
|
const COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT = "qqq.columnVisibility";
|
||||||
@ -83,8 +84,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
const tableName = table.name;
|
const tableName = table.name;
|
||||||
const [ searchParams ] = useSearchParams();
|
const [ searchParams ] = useSearchParams();
|
||||||
|
|
||||||
const [showSuccessfullyDeletedAlert, setShowSuccessfullyDeletedAlert] = useState(searchParams.has("deleteSuccess"));
|
const [showSuccessfullyDeletedAlert, setShowSuccessfullyDeletedAlert] = useState(searchParams.has("deleteSuccess"));
|
||||||
|
const [successAlert, setSuccessAlert] = useState(null as string)
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -175,6 +177,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
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 [ latestQueryResults, setLatestQueryResults ] = useState(null as QRecord[]);
|
||||||
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());
|
||||||
@ -221,7 +224,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
const parts = location.pathname.split("/");
|
const parts = location.pathname.split("/");
|
||||||
currentSavedFilterId = Number.parseInt(parts[parts.length - 1]);
|
currentSavedFilterId = Number.parseInt(parts[parts.length - 1]);
|
||||||
}
|
}
|
||||||
else
|
else if(!searchParams.has("filter"))
|
||||||
{
|
{
|
||||||
if (localStorage.getItem(currentSavedFilterLocalStorageKey))
|
if (localStorage.getItem(currentSavedFilterLocalStorageKey))
|
||||||
{
|
{
|
||||||
@ -280,7 +283,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
const buildQFilter = (filterModel: GridFilterModel) =>
|
const buildQFilter = (filterModel: GridFilterModel) =>
|
||||||
{
|
{
|
||||||
const filter = FilterUtils.buildQFilterFromGridFilter(filterModel, columnSortModel);
|
const filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel);
|
||||||
setHasValidFilters(filter.criteria && filter.criteria.length > 0);
|
setHasValidFilters(filter.criteria && filter.criteria.length > 0);
|
||||||
return(filter);
|
return(filter);
|
||||||
};
|
};
|
||||||
@ -425,6 +428,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
console.log(`Outputting results for query ${latestQueryId}...`);
|
console.log(`Outputting results for query ${latestQueryId}...`);
|
||||||
const results = queryResults[latestQueryId];
|
const results = queryResults[latestQueryId];
|
||||||
delete queryResults[latestQueryId];
|
delete queryResults[latestQueryId];
|
||||||
|
setLatestQueryResults(results);
|
||||||
|
|
||||||
const {rows, columnsToRender} = DataGridUtils.makeRows(results, tableMetaData);
|
const {rows, columnsToRender} = DataGridUtils.makeRows(results, tableMetaData);
|
||||||
|
|
||||||
@ -573,10 +577,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
setFilterModel(filterModel);
|
setFilterModel(filterModel);
|
||||||
if (filterLocalStorageKey)
|
if (filterLocalStorageKey)
|
||||||
{
|
{
|
||||||
localStorage.setItem(
|
localStorage.setItem(filterLocalStorageKey, JSON.stringify(filterModel));
|
||||||
filterLocalStorageKey,
|
|
||||||
JSON.stringify(filterModel),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -877,7 +878,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
if(selectedSavedFilterId != null)
|
if(selectedSavedFilterId != null)
|
||||||
{
|
{
|
||||||
const qRecord = await fetchSavedFilter(selectedSavedFilterId);
|
const qRecord = await fetchSavedFilter(selectedSavedFilterId);
|
||||||
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);
|
handleSortChange(models.sort);
|
||||||
localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString());
|
localStorage.setItem(currentSavedFilterLocalStorageKey, selectedSavedFilterId.toString());
|
||||||
@ -920,6 +921,71 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
return(qRecord);
|
return(qRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyColumnValues = async (column: GridColDef) =>
|
||||||
|
{
|
||||||
|
let data = "";
|
||||||
|
if(latestQueryResults && latestQueryResults.length)
|
||||||
|
{
|
||||||
|
let qFieldMetaData = tableMetaData.fields.get(column.field);
|
||||||
|
for(let i = 0; i < latestQueryResults.length; i++)
|
||||||
|
{
|
||||||
|
let record = latestQueryResults[i] as QRecord;
|
||||||
|
const value = ValueUtils.getUnadornedValueForDisplay(qFieldMetaData, record.values.get(qFieldMetaData.name), record.displayValues.get(qFieldMetaData.name));
|
||||||
|
data += value + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(data)
|
||||||
|
setSuccessAlert("Copied " + latestQueryResults.length + " " + qFieldMetaData.label + " values.");
|
||||||
|
setTimeout(() => setSuccessAlert(null), 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomColumnMenu = forwardRef<HTMLUListElement, GridColumnMenuProps>(
|
||||||
|
function GridColumnMenu(props: GridColumnMenuProps, ref)
|
||||||
|
{
|
||||||
|
const {hideMenu, currentColumn} = props;
|
||||||
|
|
||||||
|
/*
|
||||||
|
const [copyMoreMenu, setCopyMoreMenu] = useState(null)
|
||||||
|
const openCopyMoreMenu = (event: any) =>
|
||||||
|
{
|
||||||
|
setCopyMoreMenu(event.currentTarget);
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
const closeCopyMoreMenu = () => setCopyMoreMenu(null);
|
||||||
|
*/
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridColumnMenuContainer ref={ref} {...props}>
|
||||||
|
<SortGridMenuItems onClick={hideMenu} column={currentColumn!} />
|
||||||
|
<GridFilterMenuItem onClick={hideMenu} column={currentColumn!} />
|
||||||
|
<HideGridColMenuItem onClick={hideMenu} column={currentColumn!} />
|
||||||
|
<GridColumnsMenuItem onClick={hideMenu} column={currentColumn!} />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<GridColumnPinningMenuItems onClick={hideMenu} column={currentColumn!} />
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<MenuItem sx={{justifyContent: "space-between"}} onClick={(e) =>
|
||||||
|
{
|
||||||
|
hideMenu(e);
|
||||||
|
copyColumnValues(currentColumn)
|
||||||
|
}}>
|
||||||
|
Copy values
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<Button sx={{minHeight: "auto", minWidth: "auto", padding: 0}} onClick={(e) => openCopyMoreMenu(e)}>...</Button>
|
||||||
|
<Menu anchorEl={copyMoreMenu} anchorOrigin={{vertical: "top", horizontal: "right"}} transformOrigin={{vertical: "top", horizontal: "left"}} open={Boolean(copyMoreMenu)} onClose={closeCopyMoreMenu} keepMounted>
|
||||||
|
<MenuItem>Oh</MenuItem>
|
||||||
|
<MenuItem>My</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
*/}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
</GridColumnMenuContainer>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function CustomToolbar()
|
function CustomToolbar()
|
||||||
{
|
{
|
||||||
const handleMouseDown: GridEventListener<"cellMouseDown"> = (
|
const handleMouseDown: GridEventListener<"cellMouseDown"> = (
|
||||||
@ -1167,6 +1233,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
(successAlert) ? (
|
||||||
|
<Collapse in={Boolean(successAlert)}>
|
||||||
|
<Alert color="success" sx={{mb: 3}} onClose={() =>
|
||||||
|
{
|
||||||
|
setSuccessAlert(null);
|
||||||
|
}}>
|
||||||
|
{successAlert}
|
||||||
|
</Alert>
|
||||||
|
</Collapse>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
<Box display="flex" justifyContent="flex-end" alignItems="flex-start" mb={2}>
|
<Box display="flex" justifyContent="flex-end" alignItems="flex-start" mb={2}>
|
||||||
<Box display="flex" marginRight="auto">
|
<Box display="flex" marginRight="auto">
|
||||||
<SavedFilters qController={qController} metaData={metaData} tableMetaData={tableMetaData} currentSavedFilter={currentSavedFilter} filterModel={filterModel} columnSortModel={columnSortModel} filterOnChangeCallback={handleSavedFilterChange}/>
|
<SavedFilters qController={qController} metaData={metaData} tableMetaData={tableMetaData} currentSavedFilter={currentSavedFilter} filterModel={filterModel} columnSortModel={columnSortModel} filterOnChangeCallback={handleSavedFilterChange}/>
|
||||||
@ -1185,7 +1263,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
|||||||
<Card>
|
<Card>
|
||||||
<Box height="100%">
|
<Box height="100%">
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading}}
|
components={{Toolbar: CustomToolbar, Pagination: CustomPagination, LoadingOverlay: Loading, ColumnMenu: CustomColumnMenu}}
|
||||||
pinnedColumns={pinnedColumns}
|
pinnedColumns={pinnedColumns}
|
||||||
onPinnedColumnsChange={handlePinnedColumnsChange}
|
onPinnedColumnsChange={handlePinnedColumnsChange}
|
||||||
pagination
|
pagination
|
||||||
|
@ -254,6 +254,17 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
background: orange;
|
background: orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* make tags in filter forms not be black bg w/ white text */
|
||||||
|
.MuiDataGrid-filterForm .MuiAutocomplete-tag
|
||||||
|
{
|
||||||
|
background-color: initial !important;
|
||||||
|
color: initial !important;
|
||||||
|
}
|
||||||
|
.MuiDataGrid-filterForm .MuiAutocomplete-tag .MuiSvgIcon-root
|
||||||
|
{
|
||||||
|
color: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
.MuiTablePagination-root .MuiTablePagination-toolbar .MuiTablePagination-select
|
.MuiTablePagination-root .MuiTablePagination-toolbar .MuiTablePagination-select
|
||||||
{
|
{
|
||||||
padding-right: 1.125rem !important;
|
padding-right: 1.125rem !important;
|
||||||
|
@ -30,6 +30,8 @@ import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryF
|
|||||||
import {GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro";
|
import {GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro";
|
||||||
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";
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Utility class for working with QQQ Filters
|
** Utility class for working with QQQ Filters
|
||||||
**
|
**
|
||||||
@ -236,7 +238,7 @@ class FilterUtils
|
|||||||
** for non-values (e.g., blank), set it to null.
|
** for non-values (e.g., blank), set it to null.
|
||||||
** for list-values, it's already in an array, so don't wrap it.
|
** for list-values, it's already in an array, so don't wrap it.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static gridCriteriaValueToQQQ = (operator: QCriteriaOperator, value: any, gridOperatorValue: string): any[] =>
|
public static gridCriteriaValueToQQQ = (operator: QCriteriaOperator, value: any, gridOperatorValue: string, fieldMetaData: QFieldMetaData): any[] =>
|
||||||
{
|
{
|
||||||
if (gridOperatorValue === "isTrue")
|
if (gridOperatorValue === "isTrue")
|
||||||
{
|
{
|
||||||
@ -261,18 +263,34 @@ class FilterUtils
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
return ([null, null]);
|
return ([null, null]);
|
||||||
}
|
}
|
||||||
return (FilterUtils.extractIdsFromPossibleValueList(value));
|
return (FilterUtils.prepFilterValuesForBackend(value, fieldMetaData));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (FilterUtils.extractIdsFromPossibleValueList([value]));
|
return (FilterUtils.prepFilterValuesForBackend([value], fieldMetaData));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static zeroPad = (n: number): string =>
|
||||||
|
{
|
||||||
|
if (n < 10)
|
||||||
|
{
|
||||||
|
return ("0" + n);
|
||||||
|
}
|
||||||
|
return (`${n}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Helper method - take a list of values, which may be possible values, and
|
** Helper method - take a list of values, which may be possible values, and
|
||||||
** either return the original list, or a new list that is just the ids of the
|
** either return the original list, or a new list that is just the ids of the
|
||||||
** possible values (if it was a list of possible values)
|
** possible values (if it was a list of possible values).
|
||||||
|
**
|
||||||
|
** Or, if the values are date-times, convert them to UTC.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static extractIdsFromPossibleValueList = (param: any[]): number[] | string[] =>
|
private static prepFilterValuesForBackend = (param: any[], fieldMetaData: QFieldMetaData): number[] | string[] =>
|
||||||
{
|
{
|
||||||
if (param === null || param === undefined)
|
if (param === null || param === undefined)
|
||||||
{
|
{
|
||||||
@ -292,7 +310,27 @@ class FilterUtils
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
rs.push(param[i]);
|
if (fieldMetaData?.type == QFieldType.DATE_TIME)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
let localDate = new Date(param[i]);
|
||||||
|
let month = (1 + localDate.getUTCMonth());
|
||||||
|
let zp = FilterUtils.zeroPad;
|
||||||
|
let toPush = localDate.getUTCFullYear() + "-" + zp(month) + "-" + zp(localDate.getUTCDate()) + "T" + zp(localDate.getUTCHours()) + ":" + zp(localDate.getUTCMinutes()) + ":" + zp(localDate.getUTCSeconds()) + "Z";
|
||||||
|
console.log(`Input date was ${localDate}. Sending to backend as ${toPush}`);
|
||||||
|
rs.push(toPush);
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log("Error converting date-time to UTC: ", e);
|
||||||
|
rs.push(param[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rs.push(param[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (rs);
|
return (rs);
|
||||||
@ -366,7 +404,7 @@ class FilterUtils
|
|||||||
//////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
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, null, field.name, "", values);
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
@ -454,6 +492,16 @@ class FilterUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (searchParams && searchParams.has("filter"))
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if we're setting the filter based on a filter query-string param, then make sure we don't have a currentSavedFilter in local storage. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
localStorage.removeItem(`${CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT}.${tableMetaData.name}`);
|
||||||
|
localStorage.setItem(filterLocalStorageKey, JSON.stringify(defaultFilter));
|
||||||
|
localStorage.setItem(sortLocalStorageKey, JSON.stringify(defaultSort));
|
||||||
|
}
|
||||||
|
|
||||||
return ({filter: defaultFilter, sort: defaultSort});
|
return ({filter: defaultFilter, sort: defaultSort});
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
@ -482,7 +530,7 @@ class FilterUtils
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** build a qqq filter from a grid and column sort model
|
** build a qqq filter from a grid and column sort model
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static buildQFilterFromGridFilter(filterModel: GridFilterModel, columnSortModel: GridSortItem[]): QQueryFilter
|
public static buildQFilterFromGridFilter(tableMetaData: QTableMetaData, filterModel: GridFilterModel, columnSortModel: GridSortItem[]): QQueryFilter
|
||||||
{
|
{
|
||||||
console.log("Building q filter with model:");
|
console.log("Building q filter with model:");
|
||||||
console.log(filterModel);
|
console.log(filterModel);
|
||||||
@ -521,8 +569,10 @@ class FilterUtils
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fieldMetadata = tableMetaData?.fields.get(item.columnField);
|
||||||
|
|
||||||
const operator = FilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue);
|
const operator = FilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue);
|
||||||
const values = FilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
|
const values = FilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue, fieldMetadata);
|
||||||
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
|
||||||
foundFilter = true;
|
foundFilter = true;
|
||||||
});
|
});
|
||||||
|
@ -199,7 +199,7 @@ class ValueUtils
|
|||||||
** After we know there's no element to be returned (e.g., because no adornment),
|
** After we know there's no element to be returned (e.g., because no adornment),
|
||||||
** this method does the string formatting.
|
** this method does the string formatting.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static getUnadornedValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any): string | JSX.Element
|
public static getUnadornedValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any): string | JSX.Element
|
||||||
{
|
{
|
||||||
if(! displayValue && field.defaultValue)
|
if(! displayValue && field.defaultValue)
|
||||||
{
|
{
|
||||||
@ -260,7 +260,7 @@ class ValueUtils
|
|||||||
date = new Date(date)
|
date = new Date(date)
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return (`${date.toString("yyyy-MM-ddThh:mm:ssZ")}`);
|
return (`${date.toString("yyyy-MM-ddTHH:mm:ssZ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getFullWeekday(date: Date)
|
public static getFullWeekday(date: Date)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package com.kingsrook.qqq.materialdashbaord.lib;
|
package com.kingsrook.qqq.materialdashboard.lib;
|
||||||
|
|
||||||
|
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.javalin.QSeleniumJavalin;
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.TestInfo;
|
||||||
import org.openqa.selenium.Dimension;
|
import org.openqa.selenium.Dimension;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
import org.openqa.selenium.chrome.ChromeDriver;
|
import org.openqa.selenium.chrome.ChromeDriver;
|
||||||
@ -19,7 +20,7 @@ public class QBaseSeleniumTest
|
|||||||
{
|
{
|
||||||
private static ChromeOptions chromeOptions;
|
private static ChromeOptions chromeOptions;
|
||||||
|
|
||||||
private WebDriver driver;
|
protected WebDriver driver;
|
||||||
protected QSeleniumJavalin qSeleniumJavalin;
|
protected QSeleniumJavalin qSeleniumJavalin;
|
||||||
protected QSeleniumLib qSeleniumLib;
|
protected QSeleniumLib qSeleniumLib;
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ public class QBaseSeleniumTest
|
|||||||
void beforeEach()
|
void beforeEach()
|
||||||
{
|
{
|
||||||
driver = new ChromeDriver(chromeOptions);
|
driver = new ChromeDriver(chromeOptions);
|
||||||
driver.manage().window().setSize(new Dimension(1600, 1200));
|
driver.manage().window().setSize(new Dimension(1700, 1300));
|
||||||
qSeleniumLib = new QSeleniumLib(driver);
|
qSeleniumLib = new QSeleniumLib(driver);
|
||||||
|
|
||||||
qSeleniumJavalin = new QSeleniumJavalin();
|
qSeleniumJavalin = new QSeleniumJavalin();
|
||||||
@ -81,8 +82,10 @@ public class QBaseSeleniumTest
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void afterEach()
|
void afterEach(TestInfo testInfo)
|
||||||
{
|
{
|
||||||
|
qSeleniumLib.takeScreenshotToFile(getClass().getSimpleName() + "/" + testInfo.getDisplayName());
|
||||||
|
|
||||||
if(driver != null)
|
if(driver != null)
|
||||||
{
|
{
|
||||||
driver.quit();
|
driver.quit();
|
@ -1,4 +1,4 @@
|
|||||||
package com.kingsrook.qqq.materialdashbaord.lib;
|
package com.kingsrook.qqq.materialdashboard.lib;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
@ -1,4 +1,4 @@
|
|||||||
package com.kingsrook.qqq.materialdashbaord.lib;
|
package com.kingsrook.qqq.materialdashboard.lib;
|
||||||
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -6,6 +6,8 @@ import java.time.Duration;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.OutputType;
|
import org.openqa.selenium.OutputType;
|
||||||
import org.openqa.selenium.StaleElementReferenceException;
|
import org.openqa.selenium.StaleElementReferenceException;
|
||||||
@ -23,12 +25,14 @@ import static org.junit.jupiter.api.Assertions.fail;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class QSeleniumLib
|
public class QSeleniumLib
|
||||||
{
|
{
|
||||||
|
Logger LOG = LogManager.getLogger(QSeleniumLib.class);
|
||||||
|
|
||||||
public final WebDriver driver;
|
public final WebDriver driver;
|
||||||
|
|
||||||
private long WAIT_SECONDS = 10;
|
private long WAIT_SECONDS = 10;
|
||||||
private String BASE_URL = "https://localhost:3001";
|
private String BASE_URL = "https://localhost:3001";
|
||||||
private boolean SCREENSHOTS_ENABLED = true;
|
private boolean SCREENSHOTS_ENABLED = true;
|
||||||
private String SCREENSHOTS_PATH = "/tmp/";
|
private String SCREENSHOTS_PATH = "/tmp/QSeleniumScreenshots/";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -118,7 +122,7 @@ public class QSeleniumLib
|
|||||||
{
|
{
|
||||||
// todo - if env says we're in CIRCLECI, then... just do a hard fail (or just not wait forever?)
|
// todo - if env says we're in CIRCLECI, then... just do a hard fail (or just not wait forever?)
|
||||||
|
|
||||||
System.out.println("Going into a waitForever...");
|
LOG.warn("Going into a waitForever...");
|
||||||
new WebDriverWait(driver, Duration.ofHours(1))
|
new WebDriverWait(driver, Duration.ofHours(1))
|
||||||
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".wontEverBePresent")));
|
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".wontEverBePresent")));
|
||||||
}
|
}
|
||||||
@ -131,13 +135,11 @@ public class QSeleniumLib
|
|||||||
public void gotoAndWaitForBreadcrumbHeader(String path, String headerText)
|
public void gotoAndWaitForBreadcrumbHeader(String path, String headerText)
|
||||||
{
|
{
|
||||||
driver.get(BASE_URL + path);
|
driver.get(BASE_URL + path);
|
||||||
String title = driver.getTitle();
|
|
||||||
System.out.println("Page Title: " + title);
|
|
||||||
|
|
||||||
WebElement header = new WebDriverWait(driver, Duration.ofSeconds(WAIT_SECONDS))
|
WebElement header = new WebDriverWait(driver, Duration.ofSeconds(WAIT_SECONDS))
|
||||||
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER)));
|
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER)));
|
||||||
|
|
||||||
System.out.println("Breadcrumb Header: " + header.getText());
|
LOG.debug("Navigated to [" + path + "]. Breadcrumb Header: " + header.getText());
|
||||||
assertEquals(headerText, header.getText());
|
assertEquals(headerText, header.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +160,7 @@ public class QSeleniumLib
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public List<WebElement> waitForSelectorAll(String cssSelector, int minCount)
|
public List<WebElement> waitForSelectorAll(String cssSelector, int minCount)
|
||||||
{
|
{
|
||||||
System.out.println("Waiting for element matching selector [" + cssSelector + "]");
|
LOG.debug("Waiting for element matching selector [" + cssSelector + "]");
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
do
|
do
|
||||||
@ -166,7 +168,7 @@ public class QSeleniumLib
|
|||||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||||
if(elements.size() >= minCount)
|
if(elements.size() >= minCount)
|
||||||
{
|
{
|
||||||
System.out.println("Found [" + elements.size() + "] element(s) matching selector [" + cssSelector + "]");
|
LOG.debug("Found [" + elements.size() + "] element(s) matching selector [" + cssSelector + "]");
|
||||||
return (elements);
|
return (elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,6 +182,64 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void waitForSelectorToNotExist(String cssSelector)
|
||||||
|
{
|
||||||
|
LOG.debug("Waiting for non-existence of element matching selector [" + cssSelector + "]");
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||||
|
if(elements.size() == 0)
|
||||||
|
{
|
||||||
|
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleepABit();
|
||||||
|
}
|
||||||
|
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||||
|
|
||||||
|
fail("Failed for non-existence of element matching selector [" + cssSelector + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void waitForSelectorContainingToNotExist(String cssSelector, String textContains)
|
||||||
|
{
|
||||||
|
LOG.debug("Waiting for non-existence of element matching selector [" + cssSelector + "] containing text [" + textContains + "]");
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
||||||
|
if(elements.size() == 0)
|
||||||
|
{
|
||||||
|
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(elements.stream().noneMatch(e -> e.getText().toLowerCase().contains(textContains)))
|
||||||
|
{
|
||||||
|
LOG.debug("Found non-existence of element(s) matching selector [" + cssSelector + "] containing text [" + textContains + "]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sleepABit();
|
||||||
|
}
|
||||||
|
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||||
|
|
||||||
|
fail("Failed for non-existence of element matching selector [" + cssSelector + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -208,24 +268,24 @@ public class QSeleniumLib
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public <T> T waitLoop(String message, Code<T> c)
|
public boolean waitForCondition(String message, Code<Boolean> c)
|
||||||
{
|
{
|
||||||
System.out.println("Waiting for: " + message);
|
LOG.debug("Waiting for condition: " + message);
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
T t = c.run();
|
Boolean b = c.run();
|
||||||
if(t != null)
|
if(b != null && b)
|
||||||
{
|
{
|
||||||
System.out.println("Found: " + message);
|
LOG.debug("Condition became true: " + message);
|
||||||
return (t);
|
return (true);
|
||||||
}
|
}
|
||||||
|
|
||||||
sleepABit();
|
sleepABit();
|
||||||
}
|
}
|
||||||
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||||
System.out.println("Failed to match while waiting for: " + message);
|
LOG.warn("Failed for condition to become true: " + message);
|
||||||
return (null);
|
return (false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -235,7 +295,7 @@ public class QSeleniumLib
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public WebElement waitForSelectorContaining(String cssSelector, String textContains)
|
public WebElement waitForSelectorContaining(String cssSelector, String textContains)
|
||||||
{
|
{
|
||||||
System.out.println("Waiting for element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
LOG.debug("Waiting for element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
do
|
do
|
||||||
@ -247,7 +307,7 @@ public class QSeleniumLib
|
|||||||
{
|
{
|
||||||
if(element.getText() != null && element.getText().toLowerCase().contains(textContains.toLowerCase()))
|
if(element.getText() != null && element.getText().toLowerCase().contains(textContains.toLowerCase()))
|
||||||
{
|
{
|
||||||
System.out.println("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
LOG.debug("Found element matching selector [" + cssSelector + "] containing text [" + textContains + "].");
|
||||||
Actions actions = new Actions(driver);
|
Actions actions = new Actions(driver);
|
||||||
actions.moveToElement(element);
|
actions.moveToElement(element);
|
||||||
return (element);
|
return (element);
|
||||||
@ -255,12 +315,11 @@ public class QSeleniumLib
|
|||||||
}
|
}
|
||||||
catch(StaleElementReferenceException sere)
|
catch(StaleElementReferenceException sere)
|
||||||
{
|
{
|
||||||
System.err.println("Caught a StaleElementReferenceException - will retry.");
|
LOG.debug("Caught a StaleElementReferenceException - will retry.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sleepABit();
|
sleepABit();
|
||||||
|
|
||||||
}
|
}
|
||||||
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||||
|
|
||||||
@ -270,34 +329,6 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public WebElement waitForSelectorContainingV2(String cssSelector, String textContains)
|
|
||||||
{
|
|
||||||
return (waitLoop("element matching selector [" + cssSelector + "] containing text [" + textContains + "].", () ->
|
|
||||||
{
|
|
||||||
List<WebElement> elements = driver.findElements(By.cssSelector(cssSelector));
|
|
||||||
for(WebElement element : elements)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(element.getText() != null && element.getText().toLowerCase().contains(textContains.toLowerCase()))
|
|
||||||
{
|
|
||||||
return (element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(StaleElementReferenceException sere)
|
|
||||||
{
|
|
||||||
System.err.println("Caught a StaleElementReferenceException - will retry.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (null);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Take a screenshot, putting it in the SCREENSHOTS_PATH, with a subdirectory
|
** Take a screenshot, putting it in the SCREENSHOTS_PATH, with a subdirectory
|
||||||
** for the test class simple name, filename = methodName.png.
|
** for the test class simple name, filename = methodName.png.
|
||||||
@ -314,6 +345,7 @@ public class QSeleniumLib
|
|||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Take a screenshot, and give it a path/name of your choosing (under SCREENSHOTS_PATH)
|
** Take a screenshot, and give it a path/name of your choosing (under SCREENSHOTS_PATH)
|
||||||
|
** - note - .png will be appended.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void takeScreenshotToFile(String filePathSuffix)
|
public void takeScreenshotToFile(String filePathSuffix)
|
||||||
{
|
{
|
||||||
@ -322,18 +354,18 @@ public class QSeleniumLib
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
File outputFile = driver.findElement(By.cssSelector("html")).getScreenshotAs(OutputType.FILE);
|
File outputFile = driver.findElement(By.cssSelector("html")).getScreenshotAs(OutputType.FILE);
|
||||||
File destFile = new File(SCREENSHOTS_PATH + filePathSuffix);
|
File destFile = new File(SCREENSHOTS_PATH + filePathSuffix + ".png");
|
||||||
destFile.mkdirs();
|
destFile.mkdirs();
|
||||||
if(destFile.exists())
|
if(destFile.exists())
|
||||||
{
|
{
|
||||||
destFile.delete();
|
destFile.delete();
|
||||||
}
|
}
|
||||||
FileUtils.moveFile(outputFile, destFile);
|
FileUtils.moveFile(outputFile, destFile);
|
||||||
System.out.println("Made screenshot at: " + destFile);
|
LOG.info("Made screenshot at: " + destFile);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
System.err.println("Error taking screenshot to file: " + e.getMessage());
|
LOG.warn("Error taking screenshot to file: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,20 +375,22 @@ public class QSeleniumLib
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void assertElementHasFocus(WebElement element)
|
public void waitForElementToHaveFocus(WebElement element)
|
||||||
{
|
{
|
||||||
|
LOG.debug("Waiting for element [" + element + "] to have focus.");
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if(Objects.equals(driver.switchTo().activeElement(), element))
|
if(Objects.equals(driver.switchTo().activeElement(), element))
|
||||||
{
|
{
|
||||||
|
LOG.debug("Element [" + element + "] has focus.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sleepABit();
|
sleepABit();
|
||||||
}
|
}
|
||||||
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
while(start + (1000 * WAIT_SECONDS) > System.currentTimeMillis());
|
||||||
|
|
||||||
fail("Failed to see that element [" + element + "] has focus.");
|
fail("Failed to see that element [" + element + "] has focus after [" + WAIT_SECONDS + "] seconds.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -391,7 +425,7 @@ public class QSeleniumLib
|
|||||||
{
|
{
|
||||||
if(i < noOfTries - 1)
|
if(i < noOfTries - 1)
|
||||||
{
|
{
|
||||||
System.out.println("On try [" + i + " of " + noOfTries + "] caught: " + e.getMessage());
|
LOG.debug("On try [" + i + " of " + noOfTries + "] caught: " + e.getMessage());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
|
package com.kingsrook.qqq.materialdashboard.lib.javalin;
|
||||||
|
|
||||||
|
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
@ -1,8 +1,10 @@
|
|||||||
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
|
package com.kingsrook.qqq.materialdashboard.lib.javalin;
|
||||||
|
|
||||||
|
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
import io.javalin.http.Handler;
|
import io.javalin.http.Handler;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -11,6 +13,8 @@ import io.javalin.http.Handler;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class CapturingHandler implements Handler
|
public class CapturingHandler implements Handler
|
||||||
{
|
{
|
||||||
|
Logger LOG = LogManager.getLogger(CapturingHandler.class);
|
||||||
|
|
||||||
private final QSeleniumJavalin qSeleniumJavalin;
|
private final QSeleniumJavalin qSeleniumJavalin;
|
||||||
|
|
||||||
|
|
||||||
@ -34,12 +38,12 @@ public class CapturingHandler implements Handler
|
|||||||
{
|
{
|
||||||
if(qSeleniumJavalin.capturing)
|
if(qSeleniumJavalin.capturing)
|
||||||
{
|
{
|
||||||
System.out.println("Capturing request for path [" + context.path() + "]");
|
LOG.info("Capturing request for path [" + context.path() + "]");
|
||||||
qSeleniumJavalin.captured.add(new CapturedContext(context));
|
qSeleniumJavalin.captured.add(new CapturedContext(context));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
System.out.println("Not capturing request for path [" + context.path() + "]");
|
LOG.trace("Not capturing request for path [" + context.path() + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +1,14 @@
|
|||||||
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
|
package com.kingsrook.qqq.materialdashboard.lib.javalin;
|
||||||
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.QSeleniumLib;
|
import com.kingsrook.qqq.materialdashboard.lib.QSeleniumLib;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.eclipse.jetty.server.Connector;
|
import org.eclipse.jetty.server.Connector;
|
||||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||||
import static io.javalin.apibuilder.ApiBuilder.get;
|
import static io.javalin.apibuilder.ApiBuilder.get;
|
||||||
@ -19,10 +21,12 @@ import static org.junit.jupiter.api.Assertions.fail;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class QSeleniumJavalin
|
public class QSeleniumJavalin
|
||||||
{
|
{
|
||||||
|
Logger LOG = LogManager.getLogger(QSeleniumJavalin.class);
|
||||||
|
|
||||||
private long WAIT_SECONDS = 10;
|
private long WAIT_SECONDS = 10;
|
||||||
|
|
||||||
private List<Pair<String, String>> routesToFiles;
|
private List<Pair<String, String>> routesToFiles = new ArrayList<>();
|
||||||
private List<Pair<String, String>> routesToStrings;
|
private List<Pair<String, String>> routesToStrings = new ArrayList<>();
|
||||||
|
|
||||||
private Javalin javalin;
|
private Javalin javalin;
|
||||||
|
|
||||||
@ -48,17 +52,28 @@ public class QSeleniumJavalin
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void clearRoutes()
|
||||||
|
{
|
||||||
|
this.routesToFiles.clear();
|
||||||
|
this.routesToStrings.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Fluent setter for routeToFile
|
** Fluent setter for routeToFile
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public QSeleniumJavalin withRouteToFile(String path, String file)
|
public QSeleniumJavalin withRouteToFile(String path, String fixtureFilePath)
|
||||||
{
|
{
|
||||||
if(this.routesToFiles == null)
|
if(this.routesToFiles == null)
|
||||||
{
|
{
|
||||||
this.routesToFiles = new ArrayList<>();
|
this.routesToFiles = new ArrayList<>();
|
||||||
}
|
}
|
||||||
this.routesToFiles.add(Pair.of(path, file));
|
this.routesToFiles.add(Pair.of(path, fixtureFilePath));
|
||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +107,7 @@ public class QSeleniumJavalin
|
|||||||
{
|
{
|
||||||
for(Pair<String, String> routeToFile : routesToFiles)
|
for(Pair<String, String> routeToFile : routesToFiles)
|
||||||
{
|
{
|
||||||
System.out.println("Setting up route for [" + routeToFile.getKey() + "] => [" + routeToFile.getValue() + "]");
|
LOG.debug("Setting up route for [" + routeToFile.getKey() + "] => [" + routeToFile.getValue() + "]");
|
||||||
get(routeToFile.getKey(), new RouteFromFileHandler(this, routeToFile));
|
get(routeToFile.getKey(), new RouteFromFileHandler(this, routeToFile));
|
||||||
post(routeToFile.getKey(), new RouteFromFileHandler(this, routeToFile));
|
post(routeToFile.getKey(), new RouteFromFileHandler(this, routeToFile));
|
||||||
}
|
}
|
||||||
@ -105,7 +120,7 @@ public class QSeleniumJavalin
|
|||||||
{
|
{
|
||||||
for(Pair<String, String> routeToString : routesToStrings)
|
for(Pair<String, String> routeToString : routesToStrings)
|
||||||
{
|
{
|
||||||
System.out.println("Setting up route for [" + routeToString.getKey() + "] => [" + routeToString.getValue() + "]");
|
LOG.debug("Setting up route for [" + routeToString.getKey() + "] => [" + routeToString.getValue() + "]");
|
||||||
get(routeToString.getKey(), new RouteFromStringHandler(this, routeToString));
|
get(routeToString.getKey(), new RouteFromStringHandler(this, routeToString));
|
||||||
post(routeToString.getKey(), new RouteFromStringHandler(this, routeToString));
|
post(routeToString.getKey(), new RouteFromStringHandler(this, routeToString));
|
||||||
}
|
}
|
||||||
@ -115,7 +130,7 @@ public class QSeleniumJavalin
|
|||||||
javalin.before(new CapturingHandler(this));
|
javalin.before(new CapturingHandler(this));
|
||||||
|
|
||||||
javalin.error(404, context -> {
|
javalin.error(404, context -> {
|
||||||
System.out.println("Returning 404 for [" + context.method() + " " + context.path() + "]");
|
LOG.warn("Returning 404 for [" + context.method() + " " + context.path() + "]");
|
||||||
pathsThat404ed.add(context.path());
|
pathsThat404ed.add(context.path());
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -143,21 +158,33 @@ public class QSeleniumJavalin
|
|||||||
if(javalin != null)
|
if(javalin != null)
|
||||||
{
|
{
|
||||||
javalin.stop();
|
javalin.stop();
|
||||||
|
javalin = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void restart()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void report()
|
public void report()
|
||||||
{
|
{
|
||||||
System.out.println("Paths that 404'ed:");
|
LOG.info("Paths that 404'ed:");
|
||||||
pathsThat404ed.forEach(s -> System.out.println(" - " + s));
|
pathsThat404ed.forEach(s -> LOG.info(" - " + s));
|
||||||
|
|
||||||
System.out.println("Routes served as static files:");
|
LOG.info("Routes served as static files:");
|
||||||
routeFilesServed.forEach(s -> System.out.println(" - " + s));
|
routeFilesServed.forEach(s -> LOG.info(" - " + s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -167,7 +194,7 @@ public class QSeleniumJavalin
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void beginCapture()
|
public void beginCapture()
|
||||||
{
|
{
|
||||||
System.out.println("Beginning to capture requests now");
|
LOG.info("Beginning to capture requests now");
|
||||||
capturing = true;
|
capturing = true;
|
||||||
captured.clear();
|
captured.clear();
|
||||||
}
|
}
|
||||||
@ -179,7 +206,7 @@ public class QSeleniumJavalin
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void endCapture()
|
public void endCapture()
|
||||||
{
|
{
|
||||||
System.out.println("Ending capturing of requests now");
|
LOG.info("Ending capturing of requests now");
|
||||||
capturing = false;
|
capturing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,17 +227,17 @@ public class QSeleniumJavalin
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public CapturedContext waitForCapturedPath(String path)
|
public CapturedContext waitForCapturedPath(String path)
|
||||||
{
|
{
|
||||||
System.out.println("Waiting for captured request for path [" + path + "]");
|
LOG.debug("Waiting for captured request for path [" + path + "]");
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
// System.out.println(" captured paths: " + captured.stream().map(CapturedContext::getPath).collect(Collectors.joining(",")));
|
// LOG.debug(" captured paths: " + captured.stream().map(CapturedContext::getPath).collect(Collectors.joining(",")));
|
||||||
for(CapturedContext context : captured)
|
for(CapturedContext context : captured)
|
||||||
{
|
{
|
||||||
if(context.getPath().equals(path))
|
if(context.getPath().equals(path))
|
||||||
{
|
{
|
||||||
System.out.println("Found captured request for path [" + path + "]");
|
LOG.debug("Found captured request for path [" + path + "]");
|
||||||
return (context);
|
return (context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,19 +257,19 @@ public class QSeleniumJavalin
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public CapturedContext waitForCapturedPathWithBodyContaining(String path, String bodyContaining)
|
public CapturedContext waitForCapturedPathWithBodyContaining(String path, String bodyContaining)
|
||||||
{
|
{
|
||||||
System.out.println("Waiting for captured request for path [" + path + "] with body containing [" + bodyContaining + "]");
|
LOG.debug("Waiting for captured request for path [" + path + "] with body containing [" + bodyContaining + "]");
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
// System.out.println(" captured paths: " + captured.stream().map(CapturedContext::getPath).collect(Collectors.joining(",")));
|
// LOG.debug(" captured paths: " + captured.stream().map(CapturedContext::getPath).collect(Collectors.joining(",")));
|
||||||
for(CapturedContext context : captured)
|
for(CapturedContext context : captured)
|
||||||
{
|
{
|
||||||
if(context.getPath().equals(path))
|
if(context.getPath().equals(path))
|
||||||
{
|
{
|
||||||
if(context.getBody() != null && context.getBody().contains(bodyContaining))
|
if(context.getBody() != null && context.getBody().contains(bodyContaining))
|
||||||
{
|
{
|
||||||
System.out.println("Found captured request for path [" + path + "] with body containing [" + bodyContaining + "]");
|
LOG.debug("Found captured request for path [" + path + "] with body containing [" + bodyContaining + "]");
|
||||||
return (context);
|
return (context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,4 +282,5 @@ public class QSeleniumJavalin
|
|||||||
fail("Failed to capture a request for path [" + path + "] with body containing [" + bodyContaining + "] after [" + WAIT_SECONDS + "] seconds.");
|
fail("Failed to capture a request for path [" + path + "] with body containing [" + bodyContaining + "] after [" + WAIT_SECONDS + "] seconds.");
|
||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
|
package com.kingsrook.qqq.materialdashboard.lib.javalin;
|
||||||
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -7,6 +7,8 @@ import io.javalin.http.Context;
|
|||||||
import io.javalin.http.Handler;
|
import io.javalin.http.Handler;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -14,6 +16,8 @@ import org.apache.commons.lang3.tuple.Pair;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class RouteFromFileHandler implements Handler
|
public class RouteFromFileHandler implements Handler
|
||||||
{
|
{
|
||||||
|
Logger LOG = LogManager.getLogger(RouteFromFileHandler.class);
|
||||||
|
|
||||||
private final String route;
|
private final String route;
|
||||||
private final String filePath;
|
private final String filePath;
|
||||||
private final QSeleniumJavalin qSeleniumJavalin;
|
private final QSeleniumJavalin qSeleniumJavalin;
|
||||||
@ -42,7 +46,7 @@ public class RouteFromFileHandler implements Handler
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
qSeleniumJavalin.routeFilesServed.add(this.route);
|
qSeleniumJavalin.routeFilesServed.add(this.route);
|
||||||
System.out.println("Serving route [" + this.route + "] via file [" + this.filePath + "]");
|
LOG.debug("Serving route [" + this.route + "] via file [" + this.filePath + "]");
|
||||||
List<String> lines = IOUtils.readLines(getClass().getResourceAsStream("/fixtures/" + this.filePath), StandardCharsets.UTF_8);
|
List<String> lines = IOUtils.readLines(getClass().getResourceAsStream("/fixtures/" + this.filePath), StandardCharsets.UTF_8);
|
||||||
context.result(String.join("\n", lines));
|
context.result(String.join("\n", lines));
|
||||||
}
|
}
|
@ -1,9 +1,11 @@
|
|||||||
package com.kingsrook.qqq.materialdashbaord.lib.javalin;
|
package com.kingsrook.qqq.materialdashboard.lib.javalin;
|
||||||
|
|
||||||
|
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
import io.javalin.http.Handler;
|
import io.javalin.http.Handler;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -11,6 +13,8 @@ import org.apache.commons.lang3.tuple.Pair;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class RouteFromStringHandler implements Handler
|
public class RouteFromStringHandler implements Handler
|
||||||
{
|
{
|
||||||
|
Logger LOG = LogManager.getLogger(RouteFromStringHandler.class);
|
||||||
|
|
||||||
private final String route;
|
private final String route;
|
||||||
private final String responseString;
|
private final String responseString;
|
||||||
private final QSeleniumJavalin qSeleniumJavalin;
|
private final QSeleniumJavalin qSeleniumJavalin;
|
||||||
@ -37,7 +41,7 @@ public class RouteFromStringHandler implements Handler
|
|||||||
public void handle(Context context)
|
public void handle(Context context)
|
||||||
{
|
{
|
||||||
qSeleniumJavalin.routeFilesServed.add(this.route);
|
qSeleniumJavalin.routeFilesServed.add(this.route);
|
||||||
System.out.println("Serving route [" + this.route + "] via static String");
|
LOG.debug("Serving route [" + this.route + "] via static String");
|
||||||
context.result(this.responseString);
|
context.result(this.responseString);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,12 +19,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kingsrook.qqq.materialdashbaord.tests;
|
package com.kingsrook.qqq.materialdashboard.tests;
|
||||||
|
|
||||||
|
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.QBaseSeleniumTest;
|
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.QQQMaterialDashboardSelectors;
|
import com.kingsrook.qqq.materialdashboard.lib.QQQMaterialDashboardSelectors;
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.javalin.QSeleniumJavalin;
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +60,6 @@ public class AppPageNavTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/", "Greetings App");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/", "Greetings App");
|
||||||
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.SIDEBAR_ITEM, "People App").click();
|
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.SIDEBAR_ITEM, "People App").click();
|
||||||
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.SIDEBAR_ITEM, "Greetings App").click();
|
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.SIDEBAR_ITEM, "Greetings App").click();
|
||||||
qSeleniumLib.takeScreenshotToFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -74,7 +73,6 @@ public class AppPageNavTest extends QBaseSeleniumTest
|
|||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp", "Greetings App");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp", "Greetings App");
|
||||||
qSeleniumLib.tryMultiple(3, () -> qSeleniumLib.waitForSelectorContaining("a", "Person").click());
|
qSeleniumLib.tryMultiple(3, () -> qSeleniumLib.waitForSelectorContaining("a", "Person").click());
|
||||||
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER, "Person");
|
qSeleniumLib.waitForSelectorContaining(QQQMaterialDashboardSelectors.BREADCRUMB_HEADER, "Person");
|
||||||
qSeleniumLib.takeScreenshotToFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
157
src/test/java/com/kingsrook/qqq/materialdashboard/tests/AuditTest.java
Executable file
157
src/test/java/com/kingsrook/qqq/materialdashboard/tests/AuditTest.java
Executable file
@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.materialdashboard.tests;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.CapturedContext;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Test for the audit screen (e.g., modal)
|
||||||
|
*******************************************************************************/
|
||||||
|
public class AuditTest extends QBaseSeleniumTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
||||||
|
{
|
||||||
|
super.addJavalinRoutes(qSeleniumJavalin);
|
||||||
|
qSeleniumJavalin
|
||||||
|
.withRouteToFile("/data/person/1701", "data/person/1701.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testOpenAuditsFromRecordWithNoAuditsFoundThenClose()
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
// setup route for empty audits - then assert we show such message //
|
||||||
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
qSeleniumJavalin.withRouteToFile("/data/audit/query", "data/audit/query-empty.json");
|
||||||
|
qSeleniumJavalin.restart();
|
||||||
|
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person/1701", "John Doe");
|
||||||
|
|
||||||
|
qSeleniumLib.waitForSelectorContaining("BUTTON", "Actions").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "Audit").click();
|
||||||
|
qSeleniumLib.waitForSelector(".audit");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Audit for Person: John Doe");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "No audits were found for this record");
|
||||||
|
|
||||||
|
///////////////////////////////////////
|
||||||
|
// make sure we can close the dialog //
|
||||||
|
///////////////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining("BUTTON", "Close").click();
|
||||||
|
qSeleniumLib.waitForSelectorToNotExist(".audit");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testOpenAuditsFromRecordWithSomeAuditsFound()
|
||||||
|
{
|
||||||
|
String auditQueryPath = "/data/audit/query";
|
||||||
|
qSeleniumJavalin.withRouteToFile(auditQueryPath, "data/audit/query.json");
|
||||||
|
qSeleniumJavalin.restart();
|
||||||
|
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person/1701", "John Doe");
|
||||||
|
|
||||||
|
qSeleniumLib.waitForSelectorContaining("BUTTON", "Actions").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "Audit").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Audit for Person: John Doe");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Showing all 5 audits for this record");
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// assertions about the different styles of detail messages (set a value, cleared a value, etc) //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "First Name: Set to John");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("B", "John");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "Last Name: Removed value Doe");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "clientId: Changed from BetaMax to ACME");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("B", "ACME");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Audit message here");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "This is a detail message");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testOpenAuditsFromRecordReSortList()
|
||||||
|
{
|
||||||
|
String auditQueryPath = "/data/audit/query";
|
||||||
|
qSeleniumJavalin.withRouteToFile(auditQueryPath, "data/audit/query.json");
|
||||||
|
qSeleniumJavalin.restart();
|
||||||
|
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person/1701", "John Doe");
|
||||||
|
|
||||||
|
qSeleniumLib.waitForSelectorContaining("BUTTON", "Actions").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "Audit").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Audit for Person: John Doe");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// make sure clicking the re-sort buttons works (fires a new request w/ opposite sort) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
qSeleniumJavalin.beginCapture();
|
||||||
|
WebElement sortAscButton = qSeleniumLib.waitForSelectorContaining("BUTTON", "arrow_upward");
|
||||||
|
assertEquals("false", sortAscButton.getAttribute("aria-pressed"));
|
||||||
|
sortAscButton.click();
|
||||||
|
qSeleniumJavalin.waitForCapturedPath(auditQueryPath);
|
||||||
|
qSeleniumJavalin.endCapture();
|
||||||
|
List<CapturedContext> captured = qSeleniumJavalin.getCaptured();
|
||||||
|
captured = captured.stream().filter(cc -> cc.getPath().equals(auditQueryPath)).toList();
|
||||||
|
assertEquals(1, captured.size());
|
||||||
|
assertThat(captured.get(0).getBody()).contains("\"isAscending\":true");
|
||||||
|
|
||||||
|
sortAscButton = qSeleniumLib.waitForSelectorContaining("BUTTON", "arrow_upward");
|
||||||
|
assertEquals("true", sortAscButton.getAttribute("aria-pressed"));
|
||||||
|
|
||||||
|
qSeleniumJavalin.beginCapture();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("BUTTON", "arrow_downward").click();
|
||||||
|
qSeleniumJavalin.waitForCapturedPath(auditQueryPath);
|
||||||
|
qSeleniumJavalin.endCapture();
|
||||||
|
captured = qSeleniumJavalin.getCaptured();
|
||||||
|
captured = captured.stream().filter(cc -> cc.getPath().equals(auditQueryPath)).toList();
|
||||||
|
assertEquals(1, captured.size());
|
||||||
|
assertThat(captured.get(0).getBody()).contains("\"isAscending\":false");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -19,13 +19,14 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kingsrook.qqq.materialdashbaord.tests;
|
package com.kingsrook.qqq.materialdashboard.tests;
|
||||||
|
|
||||||
|
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.QBaseSeleniumTest;
|
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.QQQMaterialDashboardSelectors;
|
import com.kingsrook.qqq.materialdashboard.lib.QQQMaterialDashboardSelectors;
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.javalin.CapturedContext;
|
import com.kingsrook.qqq.materialdashboard.lib.QSeleniumLib;
|
||||||
import com.kingsrook.qqq.materialdashbaord.lib.javalin.QSeleniumJavalin;
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.CapturedContext;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.openqa.selenium.By;
|
import org.openqa.selenium.By;
|
||||||
import org.openqa.selenium.WebElement;
|
import org.openqa.selenium.WebElement;
|
||||||
@ -56,19 +57,18 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
// @RepeatedTest(10)
|
|
||||||
@Test
|
@Test
|
||||||
void testBasicQueryAndClearFilters()
|
void testBasicQueryAndClearFilters()
|
||||||
{
|
{
|
||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
|
||||||
qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
|
qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
|
||||||
qSeleniumLib.waitForSelectorContaining("BUTTON", "Filters").click();
|
qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filters").click();
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
// open the filter window, enter a value, wait for query to re-run //
|
// open the filter window, enter a value, wait for query to re-run //
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
WebElement filterInput = qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_FILTER_INPUT);
|
WebElement filterInput = qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_FILTER_INPUT);
|
||||||
qSeleniumLib.assertElementHasFocus(filterInput);
|
qSeleniumLib.waitForElementToHaveFocus(filterInput);
|
||||||
qSeleniumJavalin.beginCapture();
|
qSeleniumJavalin.beginCapture();
|
||||||
filterInput.sendKeys("1");
|
filterInput.sendKeys("1");
|
||||||
|
|
||||||
@ -105,9 +105,6 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
assertThat(capturedCount).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
|
assertThat(capturedCount).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
|
||||||
assertThat(capturedQuery).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
|
assertThat(capturedQuery).extracting("body").asString().doesNotContain(idEquals1FilterSubstring);
|
||||||
qSeleniumJavalin.endCapture();
|
qSeleniumJavalin.endCapture();
|
||||||
|
|
||||||
qSeleniumLib.takeScreenshotToFile();
|
|
||||||
// qSeleniumLib.waitForever(); // todo not commit - in fact, build in linting that makes sure we never do?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -115,17 +112,16 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
// @RepeatedTest(10)
|
|
||||||
@Test
|
@Test
|
||||||
void testMultiCriteriaQueryWithOr()
|
void testMultiCriteriaQueryWithOr()
|
||||||
{
|
{
|
||||||
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
|
||||||
qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
|
qSeleniumLib.waitForSelector(QQQMaterialDashboardSelectors.QUERY_GRID_CELL);
|
||||||
qSeleniumLib.waitForSelectorContaining("BUTTON", "Filters").click();
|
qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filters").click();
|
||||||
|
|
||||||
addQueryFilterInput(0, "First Name", "contains", "Dar", "Or");
|
addQueryFilterInput(qSeleniumLib, 0, "First Name", "contains", "Dar", "Or");
|
||||||
qSeleniumJavalin.beginCapture();
|
qSeleniumJavalin.beginCapture();
|
||||||
addQueryFilterInput(1, "First Name", "contains", "Jam", "Or");
|
addQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
|
||||||
|
|
||||||
String expectedFilterContents0 = """
|
String expectedFilterContents0 = """
|
||||||
{"fieldName":"firstName","operator":"CONTAINS","values":["Dar"]}""";
|
{"fieldName":"firstName","operator":"CONTAINS","values":["Dar"]}""";
|
||||||
@ -138,9 +134,6 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents1);
|
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents1);
|
||||||
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents2);
|
qSeleniumJavalin.waitForCapturedPathWithBodyContaining("/data/person/query", expectedFilterContents2);
|
||||||
qSeleniumJavalin.endCapture();
|
qSeleniumJavalin.endCapture();
|
||||||
|
|
||||||
qSeleniumLib.takeScreenshotToFile();
|
|
||||||
// qSeleniumLib.waitForever(); // todo not commit - in fact, build in linting that makes sure we never do?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -148,7 +141,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private void addQueryFilterInput(int index, String fieldlabel, String operator, String value, String booleanOperator)
|
static void addQueryFilterInput(QSeleniumLib qSeleniumLib, int index, String fieldLabel, String operator, String value, String booleanOperator)
|
||||||
{
|
{
|
||||||
if(index > 0)
|
if(index > 0)
|
||||||
{
|
{
|
||||||
@ -164,7 +157,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
Select fieldSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormColumnInput SELECT")));
|
Select fieldSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormColumnInput SELECT")));
|
||||||
fieldSelect.selectByVisibleText(fieldlabel);
|
fieldSelect.selectByVisibleText(fieldLabel);
|
||||||
|
|
||||||
Select operatorSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormOperatorInput SELECT")));
|
Select operatorSelect = new Select(subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormOperatorInput SELECT")));
|
||||||
operatorSelect.selectByVisibleText(operator);
|
operatorSelect.selectByVisibleText(operator);
|
||||||
@ -172,6 +165,7 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
WebElement valueInput = subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormValueInput INPUT"));
|
WebElement valueInput = subFormForField.findElement(By.cssSelector(".MuiDataGrid-filterFormValueInput INPUT"));
|
||||||
valueInput.click();
|
valueInput.click();
|
||||||
valueInput.sendKeys(value);
|
valueInput.sendKeys(value);
|
||||||
|
qSeleniumLib.waitForSeconds(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
179
src/test/java/com/kingsrook/qqq/materialdashboard/tests/SavedFiltersTest.java
Executable file
179
src/test/java/com/kingsrook/qqq/materialdashboard/tests/SavedFiltersTest.java
Executable file
@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.materialdashboard.tests;
|
||||||
|
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.QBaseSeleniumTest;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.CapturedContext;
|
||||||
|
import com.kingsrook.qqq.materialdashboard.lib.javalin.QSeleniumJavalin;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import static com.kingsrook.qqq.materialdashboard.tests.QueryScreenTest.addQueryFilterInput;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Test for Saved Filters functionality on the Query screen.
|
||||||
|
*******************************************************************************/
|
||||||
|
public class SavedFiltersTest extends QBaseSeleniumTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
protected void addJavalinRoutes(QSeleniumJavalin qSeleniumJavalin)
|
||||||
|
{
|
||||||
|
addStandardRoutesForThisTest(qSeleniumJavalin);
|
||||||
|
qSeleniumJavalin.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void addStandardRoutesForThisTest(QSeleniumJavalin qSeleniumJavalin)
|
||||||
|
{
|
||||||
|
super.addJavalinRoutes(qSeleniumJavalin);
|
||||||
|
qSeleniumJavalin.withRouteToFile("/data/person/count", "data/person/count.json");
|
||||||
|
qSeleniumJavalin.withRouteToFile("/data/person/query", "data/person/index.json");
|
||||||
|
qSeleniumJavalin.withRouteToFile("/data/person/*", "data/person/1701.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testNavigatingBackAndForth()
|
||||||
|
{
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person", "Person");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("BUTTON", "Saved Filters").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "Some People");
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
// need to only return id=2 next time //
|
||||||
|
////////////////////////////////////////
|
||||||
|
qSeleniumJavalin.stop();
|
||||||
|
qSeleniumJavalin.clearRoutes();
|
||||||
|
addStandardRoutesForThisTest(qSeleniumJavalin);
|
||||||
|
qSeleniumJavalin.withRouteToFile("/processes/querySavedFilter/init", "processes/querySavedFilter/init-id=2.json");
|
||||||
|
qSeleniumJavalin.restart();
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// go to a specific filter - assert that it's loaded //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "Some People").click();
|
||||||
|
qSeleniumLib.waitForCondition("Current URL should have filter id", () -> driver.getCurrentUrl().endsWith("/person/savedFilter/2"));
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Current Filter: Some People");
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// click into a view screen //
|
||||||
|
//////////////////////////////
|
||||||
|
qSeleniumLib.takeScreenshotToFile("before-johnny-click");
|
||||||
|
qSeleniumLib.waitForSeconds(1); // wait for the filters menu to fully disappear? if this doesn't work, try a different word to look for...
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "jdoe@kingsrook.com").click();
|
||||||
|
qSeleniumLib.takeScreenshotToFile("after-johnny-click");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("H5", "Viewing Person: John Doe");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////
|
||||||
|
// take breadcrumb back to table query //
|
||||||
|
// assert the previously selected filter is loaded //
|
||||||
|
/////////////////////////////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining("A", "Person").click();
|
||||||
|
qSeleniumLib.waitForCondition("Current URL should have filter id", () -> driver.getCurrentUrl().endsWith("/person/savedFilter/2"));
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Current Filter: Some People");
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "1");
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// modify the query //
|
||||||
|
//////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiDataGrid-toolbarContainer BUTTON", "Filters").click();
|
||||||
|
addQueryFilterInput(qSeleniumLib, 1, "First Name", "contains", "Jam", "Or");
|
||||||
|
qSeleniumLib.waitForSelectorContaining("H5", "Person").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Current Filter: Some People")
|
||||||
|
.findElement(By.cssSelector("CIRCLE"));
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "2");
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// click into a view screen //
|
||||||
|
//////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "jdoe@kingsrook.com").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("H5", "Viewing Person: John Doe");
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// take breadcrumb back to table query //
|
||||||
|
// assert the previously selected filter, with modification, is still loaded //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
qSeleniumJavalin.beginCapture();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("A", "Person").click();
|
||||||
|
qSeleniumLib.waitForCondition("Current URL should have filter id", () -> driver.getCurrentUrl().endsWith("/person/savedFilter/2"));
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "Current Filter: Some People")
|
||||||
|
.findElement(By.cssSelector("CIRCLE"));
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "2");
|
||||||
|
CapturedContext capturedContext = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
|
||||||
|
assertTrue(capturedContext.getBody().contains("Jam"));
|
||||||
|
qSeleniumJavalin.endCapture();
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
// navigate to the table with a filter in the URL //
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
String filter = """
|
||||||
|
{
|
||||||
|
"criteria":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fieldName": "id",
|
||||||
|
"operator": "LESS_THAN",
|
||||||
|
"values": [10]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".replace('\n', ' ').replaceAll(" ", "");
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeader("/peopleApp/greetingsApp/person?filter=" + URLEncoder.encode(filter, StandardCharsets.UTF_8), "Person");
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "1");
|
||||||
|
qSeleniumLib.waitForSelectorContainingToNotExist("DIV", "Current Filter");
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// click into a view screen //
|
||||||
|
//////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining("DIV", "jdoe@kingsrook.com").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("H5", "Viewing Person: John Doe");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// take breadcrumb back to table query //
|
||||||
|
// assert the filter previously given on the URL is what is loaded & requested //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
|
qSeleniumJavalin.beginCapture();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("A", "Person").click();
|
||||||
|
qSeleniumLib.waitForCondition("Current URL should not have filter id", () -> !driver.getCurrentUrl().endsWith("/person/savedFilter/2"));
|
||||||
|
qSeleniumLib.waitForSelectorContaining(".MuiBadge-badge", "1");
|
||||||
|
capturedContext = qSeleniumJavalin.waitForCapturedPath("/data/person/query");
|
||||||
|
assertTrue(capturedContext.getBody().matches("(?s).*id.*LESS_THAN.*10.*"));
|
||||||
|
qSeleniumJavalin.endCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
3
src/test/resources/fixtures/data/audit/query-empty.json
Normal file
3
src/test/resources/fixtures/data/audit/query-empty.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"records": []
|
||||||
|
}
|
245
src/test/resources/fixtures/data/audit/query.json
Normal file
245
src/test/resources/fixtures/data/audit/query.json
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
{
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 623577,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Record was Inserted",
|
||||||
|
"timestamp": "2023-02-17T14:11:16Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 278660,
|
||||||
|
"auditDetail.auditId": 623577,
|
||||||
|
"auditDetail.message": "Set First Name to John",
|
||||||
|
"auditDetail.fieldName": "firstName",
|
||||||
|
"auditDetail.newValue": "John"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "623577",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Record was Inserted",
|
||||||
|
"timestamp": "2023-02-17T14:11:16Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 623577,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Record was Inserted",
|
||||||
|
"timestamp": "2023-02-17T14:11:16Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 278661,
|
||||||
|
"auditDetail.auditId": 623577,
|
||||||
|
"auditDetail.message": "Removed Doe from Last Name",
|
||||||
|
"auditDetail.fieldName": "lastName",
|
||||||
|
"auditDetail.oldValue": "Doe"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "623577",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Record was Inserted",
|
||||||
|
"timestamp": "2023-02-17T14:11:16Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 623577,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Record was Inserted",
|
||||||
|
"timestamp": "2023-02-17T14:11:16Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 278662,
|
||||||
|
"auditDetail.auditId": 623577,
|
||||||
|
"auditDetail.message": "Set Client to ACME",
|
||||||
|
"auditDetail.fieldName": "clientId",
|
||||||
|
"auditDetail.oldValue": "BetaMax",
|
||||||
|
"auditDetail.newValue": "ACME"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "623577",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Record was Inserted",
|
||||||
|
"timestamp": "2023-02-17T14:11:16Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 624804,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Record was Edited",
|
||||||
|
"timestamp": "2023-02-17T14:13:16Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 278990,
|
||||||
|
"auditDetail.auditId": 624804,
|
||||||
|
"auditDetail.message": "Set SLA Expected Service Days to 2",
|
||||||
|
"auditDetail.fieldName": "slaExpectedServiceDays",
|
||||||
|
"auditDetail.newValue": "2"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "624804",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Record was Edited",
|
||||||
|
"timestamp": "2023-02-17T14:13:16Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 624804,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Record was Edited",
|
||||||
|
"timestamp": "2023-02-17T14:13:16Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 278991,
|
||||||
|
"auditDetail.auditId": 624804,
|
||||||
|
"auditDetail.message": "Set SLA Status to \"Pending\"",
|
||||||
|
"auditDetail.fieldName": "slaStatusId",
|
||||||
|
"auditDetail.newValue": "Pending"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "624804",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Record was Edited",
|
||||||
|
"timestamp": "2023-02-17T14:13:16Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 624809,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Audit message here",
|
||||||
|
"timestamp": "2023-02-17T14:13:16Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 279000,
|
||||||
|
"auditDetail.auditId": 624809,
|
||||||
|
"auditDetail.message": "This is a detail message"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "624809",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Audit message here",
|
||||||
|
"timestamp": "2023-02-17T14:13:16Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 737694,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Record was Edited",
|
||||||
|
"timestamp": "2023-02-17T17:22:08Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 299222,
|
||||||
|
"auditDetail.auditId": 737694,
|
||||||
|
"auditDetail.message": "Set Estimated Delivery Date Time to 2023-02-18 07:00:00 PM EST",
|
||||||
|
"auditDetail.fieldName": "estimatedDeliveryDateTime",
|
||||||
|
"auditDetail.newValue": "2023-02-18 07:00:00 PM EST"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "737694",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Record was Edited",
|
||||||
|
"timestamp": "2023-02-17T17:22:08Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 737694,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Record was Edited",
|
||||||
|
"timestamp": "2023-02-17T17:22:08Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 299223,
|
||||||
|
"auditDetail.auditId": 737694,
|
||||||
|
"auditDetail.message": "Changed Parcel Tracking Status from \"Unknown\" to \"Pre Transit\"",
|
||||||
|
"auditDetail.fieldName": "parcelTrackingStatusId",
|
||||||
|
"auditDetail.oldValue": "Unknown",
|
||||||
|
"auditDetail.newValue": "Pre Transit"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "737694",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Record was Edited",
|
||||||
|
"timestamp": "2023-02-17T17:22:08Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "audit",
|
||||||
|
"recordLabel": "Parcel 1191682",
|
||||||
|
"values": {
|
||||||
|
"id": 737695,
|
||||||
|
"auditTableId": 4,
|
||||||
|
"auditUserId": 2,
|
||||||
|
"recordId": 1191682,
|
||||||
|
"message": "Updating Parcel based on updated tracking details",
|
||||||
|
"timestamp": "2023-02-17T17:22:09Z",
|
||||||
|
"clientId": 107,
|
||||||
|
"auditDetail.id": 299224,
|
||||||
|
"auditDetail.auditId": 737695,
|
||||||
|
"auditDetail.message": "Set Parcel Tracking Status to Pre Transit based on most recent tracking update: Shipment information sent to FedEx"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"auditTableId": "Parcel",
|
||||||
|
"auditUserId": "QQQ User",
|
||||||
|
"clientId": "ACME",
|
||||||
|
"id": "737695",
|
||||||
|
"recordId": "1191682",
|
||||||
|
"message": "Updating Parcel based on updated tracking details",
|
||||||
|
"timestamp": "2023-02-17T17:22:09Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
src/test/resources/fixtures/data/person/1701.json
Normal file
16
src/test/resources/fixtures/data/person/1701.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"tableName": "person",
|
||||||
|
"recordLabel": "John Doe",
|
||||||
|
"values": {
|
||||||
|
"name": "John Doe",
|
||||||
|
"id": 1710,
|
||||||
|
"createDate": "2022-08-30T00:31:00Z",
|
||||||
|
"modifyDate": "2022-08-30T00:31:00Z"
|
||||||
|
},
|
||||||
|
"displayValues": {
|
||||||
|
"name": "John Doe",
|
||||||
|
"id": 1710,
|
||||||
|
"createDate": "2022-08-30T00:31:00Z",
|
||||||
|
"modifyDate": "2022-08-30T00:31:00Z"
|
||||||
|
}
|
||||||
|
}
|
@ -53,6 +53,21 @@
|
|||||||
"TABLE_INSERT",
|
"TABLE_INSERT",
|
||||||
"TABLE_DELETE"
|
"TABLE_DELETE"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"audit": {
|
||||||
|
"name": "audit",
|
||||||
|
"label": "Audits",
|
||||||
|
"isHidden": true,
|
||||||
|
"iconName": "location_city",
|
||||||
|
"deletePermission": false,
|
||||||
|
"editPermission": false,
|
||||||
|
"insertPermission": false,
|
||||||
|
"readPermission": true,
|
||||||
|
"capabilities": [
|
||||||
|
"TABLE_COUNT",
|
||||||
|
"TABLE_GET",
|
||||||
|
"TABLE_QUERY"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"processes": {
|
"processes": {
|
||||||
@ -87,10 +102,23 @@
|
|||||||
"label": "Sleep Interactive",
|
"label": "Sleep Interactive",
|
||||||
"isHidden": false
|
"isHidden": false
|
||||||
},
|
},
|
||||||
"simpleThrow": {
|
"querySavedFilter": {
|
||||||
"name": "simpleThrow",
|
"name": "querySavedFilter",
|
||||||
"label": "Simple Throw",
|
"label": "Query Saved Filter",
|
||||||
"isHidden": false
|
"isHidden": false,
|
||||||
|
"hasPermission": true
|
||||||
|
},
|
||||||
|
"storeSavedFilter": {
|
||||||
|
"name": "storeSavedFilter",
|
||||||
|
"label": "Store Saved Filter",
|
||||||
|
"isHidden": false,
|
||||||
|
"hasPermission": true
|
||||||
|
},
|
||||||
|
"deleteSavedFilter": {
|
||||||
|
"name": "deleteSavedFilter",
|
||||||
|
"label": "Delete Saved Filter",
|
||||||
|
"isHidden": false,
|
||||||
|
"hasPermission": true
|
||||||
},
|
},
|
||||||
"carrier.bulkInsert": {
|
"carrier.bulkInsert": {
|
||||||
"name": "carrier.bulkInsert",
|
"name": "carrier.bulkInsert",
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"values": {
|
||||||
|
"_qStepTimeoutMillis": "60000",
|
||||||
|
"savedFilterList": [
|
||||||
|
{
|
||||||
|
"tableName": "savedFilter",
|
||||||
|
"values": {
|
||||||
|
"label": "Some People",
|
||||||
|
"id": 2,
|
||||||
|
"createDate": "2023-02-20T18:40:58Z",
|
||||||
|
"modifyDate": "2023-02-20T18:40:58Z",
|
||||||
|
"tableName": "person",
|
||||||
|
"filterJson": "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"STARTS_WITH\",\"values\":[\"D\"]}],\"orderBys\":[{\"fieldName\":\"id\",\"isAscending\":false}],\"booleanOperator\":\"AND\"}",
|
||||||
|
"userId": "darin.kelkhoff@kingsrook.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tableName": "person"
|
||||||
|
},
|
||||||
|
"processUUID": "4eaaea82-2d09-4254-90f8-e5b6948ef0b3"
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"values": {
|
||||||
|
"_qStepTimeoutMillis": "60000",
|
||||||
|
"savedFilterList": [
|
||||||
|
{
|
||||||
|
"tableName": "savedFilter",
|
||||||
|
"values": {
|
||||||
|
"label": "All People",
|
||||||
|
"id": 1,
|
||||||
|
"createDate": "2023-02-20T18:39:11Z",
|
||||||
|
"modifyDate": "2023-02-20T18:39:11Z",
|
||||||
|
"tableName": "person",
|
||||||
|
"filterJson": "{\"orderBys\":[{\"fieldName\":\"id\",\"isAscending\":false}],\"booleanOperator\":\"AND\"}",
|
||||||
|
"userId": "darin.kelkhoff@kingsrook.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "savedFilter",
|
||||||
|
"values": {
|
||||||
|
"label": "Some People",
|
||||||
|
"id": 2,
|
||||||
|
"createDate": "2023-02-20T18:40:58Z",
|
||||||
|
"modifyDate": "2023-02-20T18:40:58Z",
|
||||||
|
"tableName": "person",
|
||||||
|
"filterJson": "{\"criteria\":[{\"fieldName\":\"firstName\",\"operator\":\"STARTS_WITH\",\"values\":[\"D\"]}],\"orderBys\":[{\"fieldName\":\"id\",\"isAscending\":false}],\"booleanOperator\":\"AND\"}",
|
||||||
|
"userId": "darin.kelkhoff@kingsrook.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tableName": "person"
|
||||||
|
},
|
||||||
|
"processUUID": "4eaaea82-2d09-4254-90f8-e5b6948ef0b3"
|
||||||
|
}
|
21
src/test/resources/log4j2.xml
Normal file
21
src/test/resources/log4j2.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration>
|
||||||
|
<Appenders>
|
||||||
|
<Console name="SystemOutAppender" target="SYSTEM_OUT">
|
||||||
|
<LevelRangeFilter minLevel="ERROR" maxLevel="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||||
|
<PatternLayout pattern="%highlight{%date{ISO8601} | %level | %threadName | %logger{1} | %message%n}"/>
|
||||||
|
</Console>
|
||||||
|
<File name="LogFileAppender" fileName="log/qqq.log">
|
||||||
|
<LevelRangeFilter minLevel="ERROR" maxLevel="all" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||||
|
<PatternLayout pattern="%date{ISO8601} | %relative | %level | %threadName | %logger{1} | %message%n"/>
|
||||||
|
</File>
|
||||||
|
</Appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Logger name="org.apache.log4j.xml" additivity="false">
|
||||||
|
</Logger>
|
||||||
|
<Root level="all">
|
||||||
|
<AppenderRef ref="SystemOutAppender"/>
|
||||||
|
<!-- <AppenderRef ref="LogFileAppender"/> -->
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
Reference in New Issue
Block a user