mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
23 Commits
snapshot-i
...
wip/CE-148
Author | SHA1 | Date | |
---|---|---|---|
9a6fcd8bb1 | |||
d31215f6c0 | |||
262855b9c0 | |||
4d082c3c57 | |||
47fb7cc2e3 | |||
647c63f5a3 | |||
f545649882 | |||
4d4610801f | |||
3ec43fbbd3 | |||
28bc07cce4 | |||
c7d31fa39e | |||
69f1cfe92f | |||
2ed95ff77a | |||
66336a28ed | |||
826bed4537 | |||
40bd83cd96 | |||
ca460e65e1 | |||
122fef152c | |||
d0ed0ce949 | |||
b8aa36455d | |||
a778b7497a | |||
c3503a719f | |||
2afa82c770 |
@ -115,7 +115,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(main|integration.*)/
|
||||
ignore: /(main|dev|integration.*)/
|
||||
tags:
|
||||
ignore: /(version|snapshot)-.*/
|
||||
deploy:
|
||||
@ -124,7 +124,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||
filters:
|
||||
branches:
|
||||
only: /(main|integration.*)/
|
||||
only: /(main|dev|integration.*)/
|
||||
tags:
|
||||
only: /(version|snapshot)-.*/
|
||||
|
||||
|
5750
package-lock.json
generated
5750
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -6,7 +6,7 @@
|
||||
"@auth0/auth0-react": "1.10.2",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/styled": "11.6.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.102",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.104",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
@ -18,8 +18,8 @@
|
||||
"@react-jvectormap/core": "1.0.1",
|
||||
"@react-jvectormap/unitedstates": "1.0.1",
|
||||
"@react-oauth/google": "0.2.8",
|
||||
"@types/prop-types": "^15.7.5",
|
||||
"@types/react": "18.0.0",
|
||||
"@types/prop-types": "15.7.5",
|
||||
"@types/react": "18.2.0",
|
||||
"@types/react-dom": "18.0.0",
|
||||
"@types/react-router-hash-link": "2.4.5",
|
||||
"ace-builds": "1.12.3",
|
||||
@ -33,7 +33,7 @@
|
||||
"form-data": "4.0.0",
|
||||
"formik": "2.2.9",
|
||||
"html-react-parser": "1.4.8",
|
||||
"html-to-text": "^9.0.5",
|
||||
"html-to-text": "9.0.5",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"jwt-decode": "3.1.2",
|
||||
"rapidoc": "9.3.4",
|
||||
@ -46,12 +46,16 @@
|
||||
"react-dom": "18.0.0",
|
||||
"react-ga4": "2.1.0",
|
||||
"react-github-btn": "1.2.1",
|
||||
"react-google-drive-picker": "^1.2.0",
|
||||
"react-google-drive-picker": "1.2.0",
|
||||
"react-markdown": "9.0.1",
|
||||
"react-router-dom": "6.2.1",
|
||||
"react-router-hash-link": "2.4.3",
|
||||
"react-table": "7.7.0",
|
||||
"sass": "1.63.4",
|
||||
"sequential-workflow-designer": "0.22.0",
|
||||
"sequential-workflow-designer-react": "0.22.0",
|
||||
"sequential-workflow-editor": "0.13.2",
|
||||
"sequential-workflow-editor-model": "0.13.2",
|
||||
"ts-md5": "1.2.11",
|
||||
"yup": "0.32.11"
|
||||
},
|
||||
@ -59,7 +63,7 @@
|
||||
"build": "react-scripts build",
|
||||
"clean": "rm -rf node_modules package-lock.json lib",
|
||||
"eject": "react-scripts eject",
|
||||
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps",
|
||||
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
|
||||
"npm-install": "npm install --legacy-peer-deps",
|
||||
"prepublishOnly": "tsc -p ./ --outDir lib/",
|
||||
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
|
||||
|
2
pom.xml
2
pom.xml
@ -29,7 +29,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.20.0-SNAPSHOT</revision>
|
||||
<revision>0.21.0-SNAPSHOT</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
@ -72,7 +72,7 @@ const CommandMenu = ({metaData}: Props) =>
|
||||
const navigate = useNavigate();
|
||||
const pathParts = location.pathname.replace(/\/+$/, "").split("/");
|
||||
|
||||
const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, keyboardHelpOpen, setKeyboardHelpOpen, setTableMetaData, tableProcesses} = useContext(QContext);
|
||||
const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, keyboardHelpOpen, setKeyboardHelpOpen, setTableMetaData, tableProcesses, recordAnalytics} = useContext(QContext);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
@ -87,6 +87,7 @@ const CommandMenu = ({metaData}: Props) =>
|
||||
if (e.key === "." && !keyboardHelpOpen)
|
||||
{
|
||||
e.preventDefault();
|
||||
recordAnalytics({category: "globalEvents", action: "dotMenuKeyboardShortcut"});
|
||||
setDotMenuOpen(true);
|
||||
}
|
||||
else if (e.key === "?" && !dotMenuOpen)
|
||||
@ -423,9 +424,20 @@ const CommandMenu = ({metaData}: Props) =>
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// iterate over the search parts - if any don't match the corresponding value parts, then it's a non-match //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let valueIndex = 0;
|
||||
for (let i = 0; i < searchParts.length; i++)
|
||||
{
|
||||
if (!valueParts[i].includes(searchParts[i]))
|
||||
let foundMatch = false;
|
||||
for (; valueIndex < valueParts.length; valueIndex++)
|
||||
{
|
||||
if (valueParts[valueIndex].includes(searchParts[i]))
|
||||
{
|
||||
foundMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundMatch)
|
||||
{
|
||||
return (0);
|
||||
}
|
||||
|
@ -19,16 +19,17 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {InputAdornment, InputLabel} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import {Box, InputAdornment, InputLabel} from "@mui/material";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import {ErrorMessage, Field, useFormikContext} from "formik";
|
||||
import React, {useState} from "react";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
|
||||
import MDInput from "qqq/components/legacy/MDInput";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {flushSync} from "react-dom";
|
||||
|
||||
// Declaring props types for FormField
|
||||
interface Props
|
||||
@ -85,6 +86,51 @@ function QDynamicFormField({
|
||||
}
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// check the field meta data for behavior that says to do toUpperCase or toLowerCase //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
let isToUpperCase = useMemo(() => DynamicFormUtils.isToUpperCase(formFieldObject?.fieldMetaData), [formFieldObject]);
|
||||
let isToLowerCase = useMemo(() => DynamicFormUtils.isToLowerCase(formFieldObject?.fieldMetaData), [formFieldObject]);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// if the field has a toUpperCase or toLowerCase behavior on it, then //
|
||||
// apply that rule. But also, to avoid the cursor always jumping to //
|
||||
// the end of the input, do some manipulation of the selection. //
|
||||
// See: https://giacomocerquone.com/blog/keep-input-cursor-still //
|
||||
// Note, we only want an onChange handle if we're doing one of these //
|
||||
// behaviors, (because teh flushSync is potentially slow). hence, we //
|
||||
// put the onChange in an object and assign it with a spread //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
let onChange: any = {};
|
||||
if (isToUpperCase || isToLowerCase)
|
||||
{
|
||||
onChange.onChange = (e: any) =>
|
||||
{
|
||||
const beforeStart = e.target.selectionStart;
|
||||
const beforeEnd = e.target.selectionEnd;
|
||||
|
||||
flushSync(() =>
|
||||
{
|
||||
let newValue = e.currentTarget.value;
|
||||
if (isToUpperCase)
|
||||
{
|
||||
newValue = newValue.toUpperCase();
|
||||
}
|
||||
if (isToLowerCase)
|
||||
{
|
||||
newValue = newValue.toLowerCase();
|
||||
}
|
||||
setFieldValue(name, newValue);
|
||||
});
|
||||
|
||||
const input = document.getElementById(name) as HTMLInputElement;
|
||||
if (input)
|
||||
{
|
||||
input.setSelectionRange(beforeStart, beforeEnd);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let field;
|
||||
let getsBulkEditHtmlLabel = true;
|
||||
if (type === "checkbox")
|
||||
@ -133,7 +179,7 @@ function QDynamicFormField({
|
||||
{
|
||||
field = (
|
||||
<>
|
||||
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
|
||||
<Field {...rest} {...onChange} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
|
||||
onKeyPress={(e: any) =>
|
||||
{
|
||||
if (e.key === "Enter")
|
||||
@ -173,7 +219,8 @@ function QDynamicFormField({
|
||||
id={`bulkEditSwitch-${name}`}
|
||||
checked={switchChecked}
|
||||
onClick={bulkEditSwitchChanged}
|
||||
sx={{top: "-4px",
|
||||
sx={{
|
||||
top: "-4px",
|
||||
"& .MuiSwitch-track": {
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
|
@ -214,7 +214,7 @@ class DynamicFormUtils
|
||||
|
||||
if (Array.isArray(disabledFields))
|
||||
{
|
||||
return (disabledFields.indexOf(fieldName) > -1)
|
||||
return (disabledFields.indexOf(fieldName) > -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -222,6 +222,44 @@ class DynamicFormUtils
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* check if a field has the TO_UPPER_CASE behavior on it.
|
||||
***************************************************************************/
|
||||
public static isToUpperCase(fieldMetaData: QFieldMetaData): boolean
|
||||
{
|
||||
return this.hasBehavior(fieldMetaData, "TO_UPPER_CASE");
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* check if a field has the TO_LOWER_CASE behavior on it.
|
||||
***************************************************************************/
|
||||
public static isToLowerCase(fieldMetaData: QFieldMetaData): boolean
|
||||
{
|
||||
return this.hasBehavior(fieldMetaData, "TO_LOWER_CASE");
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* check if a field has a specific behavior name on it.
|
||||
***************************************************************************/
|
||||
private static hasBehavior(fieldMetaData: QFieldMetaData, behaviorName: string): boolean
|
||||
{
|
||||
if (fieldMetaData && fieldMetaData.behaviors)
|
||||
{
|
||||
for (let i = 0; i < fieldMetaData.behaviors.length; i++)
|
||||
{
|
||||
if (fieldMetaData.behaviors[i] == behaviorName)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DynamicFormUtils;
|
||||
|
@ -97,7 +97,7 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
|
||||
borderColor: inputBorderColor
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const qController = Client.getInstance();
|
||||
@ -108,13 +108,13 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState(null);
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))))
|
||||
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))));
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (tableName && processName)
|
||||
{
|
||||
console.log("DynamicSelect - you may not provide both a tableName and a processName")
|
||||
console.log("DynamicSelect - you may not provide both a tableName and a processName");
|
||||
}
|
||||
if (tableName && !fieldName)
|
||||
{
|
||||
@ -233,7 +233,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
||||
})();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
||||
{
|
||||
@ -248,7 +248,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
const handleBlur = (x: any) =>
|
||||
{
|
||||
setSearchTerm(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||
{
|
||||
@ -280,7 +280,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
// get options whose text/label matches the input (e.g., not ids that match) //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
return (options);
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const renderOption = (props: Object, option: any, {selected}) =>
|
||||
@ -289,13 +289,13 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
|
||||
try
|
||||
{
|
||||
const field = tableMetaData?.fields.get(fieldName)
|
||||
const field = tableMetaData?.fields.get(fieldName);
|
||||
if (field)
|
||||
{
|
||||
const adornment = field.getAdornment(AdornmentType.CHIP);
|
||||
if (adornment)
|
||||
{
|
||||
const color = adornment.getValue("color." + option.id) ?? "default"
|
||||
const color = adornment.getValue("color." + option.id) ?? "default";
|
||||
const iconName = adornment.getValue("icon." + option.id) ?? null;
|
||||
const iconElement = iconName ? <Icon>{iconName}</Icon> : null;
|
||||
content = (<Chip label={option.label} color={color} icon={iconElement} size="small" variant="outlined" sx={{fontWeight: 500}} />);
|
||||
@ -303,7 +303,8 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{ }
|
||||
{
|
||||
}
|
||||
|
||||
if (isMultiple)
|
||||
{
|
||||
@ -327,7 +328,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
{content}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const bulkEditSwitchChanged = () =>
|
||||
{
|
||||
@ -382,7 +383,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
option = option[0];
|
||||
}
|
||||
// @ts-ignore
|
||||
return option.label
|
||||
return option.label;
|
||||
}}
|
||||
options={options}
|
||||
loading={loading}
|
||||
@ -446,7 +447,8 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
id={`bulkEditSwitch-${fieldName}`}
|
||||
checked={switchChecked}
|
||||
onClick={bulkEditSwitchChanged}
|
||||
sx={{top: "-4px",
|
||||
sx={{
|
||||
top: "-4px",
|
||||
"& .MuiSwitch-track": {
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
@ -465,7 +467,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
else
|
||||
{
|
||||
return (
|
||||
<Box mb={1.5}>
|
||||
<Box>
|
||||
{autocomplete}
|
||||
</Box>
|
||||
);
|
||||
|
@ -396,15 +396,16 @@ function EntityForm(props: Props): JSX.Element
|
||||
// if the widget metadata specifies a table name, set form values to that so widget knows which to use //
|
||||
// (for the case when it is not being specified by a separate field in the record) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (widgetMetaData?.defaultValues?.has("tableName"))
|
||||
if (widgetData?.tableName)
|
||||
{
|
||||
formValues["tableName"] = widgetMetaData?.defaultValues.get("tableName");
|
||||
formValues["tableName"] = widgetData?.tableName;
|
||||
}
|
||||
|
||||
return <FilterAndColumnsSetupWidget
|
||||
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
|
||||
isEditable={true}
|
||||
widgetMetaData={widgetMetaData}
|
||||
widgetData={widgetData}
|
||||
recordValues={formValues}
|
||||
onSaveCallback={setFormFieldValuesFromWidget}
|
||||
/>;
|
||||
|
@ -85,7 +85,7 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
||||
}
|
||||
|
||||
return (routeToLabel(route));
|
||||
}
|
||||
};
|
||||
|
||||
let pageTitle = branding?.appName ?? "";
|
||||
const fullRoutes: string[] = [];
|
||||
@ -94,9 +94,9 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
||||
{
|
||||
////////////////////////////////////////////////////////
|
||||
// avoid showing "saved view" as a breadcrumb element //
|
||||
// e.g., if at /app/table/savedView/1 (so where i==2) //
|
||||
// e.g., if at /app/table/savedView/1 //
|
||||
////////////////////////////////////////////////////////
|
||||
if(routes[i] === "savedView" && i == 2)
|
||||
if (routes[i] === "savedView" && i == routes.length - 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -19,16 +19,15 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Popper, InputAdornment} from "@mui/material";
|
||||
import {Popper, InputAdornment, Box} from "@mui/material";
|
||||
import AppBar from "@mui/material/AppBar";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
|
||||
@ -45,7 +44,8 @@ interface Props
|
||||
isMini?: boolean;
|
||||
}
|
||||
|
||||
interface HistoryEntry {
|
||||
interface HistoryEntry
|
||||
{
|
||||
id: number;
|
||||
path: string;
|
||||
label: string;
|
||||
@ -64,7 +64,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
const route = useLocation().pathname.split("/").slice(1);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {pageHeader} = useContext(QContext);
|
||||
const {pageHeader, setDotMenuOpen} = useContext(QContext);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
@ -99,7 +99,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
const options = [] as any;
|
||||
history.entries.reverse().forEach((entry, index) =>
|
||||
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
||||
)
|
||||
);
|
||||
setHistory(options);
|
||||
|
||||
// Remove event listener on cleanup
|
||||
@ -111,7 +111,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
const goToHistory = (path: string) =>
|
||||
{
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
|
||||
function buildHistoryEntries()
|
||||
{
|
||||
@ -119,7 +119,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
const options = [] as any;
|
||||
history.entries.reverse().forEach((entry, index) =>
|
||||
options.push({label: entry.label, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
||||
)
|
||||
);
|
||||
setHistory(options);
|
||||
}
|
||||
|
||||
@ -138,7 +138,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
goToHistory(value.path);
|
||||
}
|
||||
setAutocompleteValue(null);
|
||||
}
|
||||
};
|
||||
|
||||
const CustomPopper = function (props: any)
|
||||
{
|
||||
@ -146,8 +146,8 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
{...props}
|
||||
style={{whiteSpace: "nowrap", width: "auto"}}
|
||||
placement="bottom-end"
|
||||
/>)
|
||||
}
|
||||
/>);
|
||||
};
|
||||
|
||||
const renderHistory = () =>
|
||||
{
|
||||
@ -184,7 +184,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Styles for the navbar icons
|
||||
const iconsStyle = ({
|
||||
@ -221,7 +221,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
}
|
||||
|
||||
return (routeToLabel(route));
|
||||
}
|
||||
};
|
||||
|
||||
const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]);
|
||||
|
||||
@ -242,9 +242,14 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
</Box>
|
||||
{isMini ? null : (
|
||||
<Box sx={(theme) => navbarRow(theme, {isMini})}>
|
||||
<Box pr={0} mr={-2}>
|
||||
<Box mt={"-0.25rem"} pb={"0.75rem"} pr={2} mr={-2} sx={{"& *": {cursor: "pointer !important"}}}>
|
||||
{renderHistory()}
|
||||
</Box>
|
||||
<Box mt={"-1rem"}>
|
||||
<IconButton size="small" disableRipple color="inherit" onClick={() => setDotMenuOpen(true)}>
|
||||
<Icon sx={iconsStyle} fontSize="small">search</Icon>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Toolbar>
|
||||
|
74
src/qqq/components/misc/ErrorBoundary.tsx
Normal file
74
src/qqq/components/misc/ErrorBoundary.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {Component, ErrorInfo} from "react";
|
||||
|
||||
interface Props
|
||||
{
|
||||
errorElement?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State
|
||||
{
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Component that you can wrap around other components that might throw an error,
|
||||
** to give some isolation, rather than breaking a whole page.
|
||||
** Credit: https://medium.com/@bobjunior542/how-to-use-error-boundaries-in-react-js-with-typescript-ee90ec814bf1
|
||||
*******************************************************************************/
|
||||
class ErrorBoundary extends Component<Props, State>
|
||||
{
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
constructor(props: Props)
|
||||
{
|
||||
super(props);
|
||||
this.state = {hasError: false};
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo)
|
||||
{
|
||||
console.error("ErrorBoundary caught an error: ", error, errorInfo);
|
||||
this.setState({hasError: true});
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
render()
|
||||
{
|
||||
if (this.state.hasError)
|
||||
{
|
||||
return this.props.errorElement ?? <span>(Error)</span>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
@ -22,6 +22,7 @@
|
||||
import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent";
|
||||
import Box from "@mui/material/Box";
|
||||
import parse from "html-react-parser";
|
||||
import ErrorBoundary from "qqq/components/misc/ErrorBoundary";
|
||||
import React, {useContext} from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import QContext from "QContext";
|
||||
@ -128,6 +129,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
||||
let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles);
|
||||
|
||||
let content = null;
|
||||
let errorContent = "Error rendering help content.";
|
||||
if (helpHelpActive)
|
||||
{
|
||||
if (!selectedHelpContent)
|
||||
@ -135,6 +137,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
||||
selectedHelpContent = new QHelpContent({content: ""});
|
||||
}
|
||||
content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`;
|
||||
errorContent += ` [${helpContentKey ?? "?"}]`;
|
||||
}
|
||||
else if(selectedHelpContent)
|
||||
{
|
||||
@ -148,7 +151,9 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
||||
{
|
||||
return <Box display="inline" className="helpContent">
|
||||
{heading && <span className="header">{heading}</span>}
|
||||
<ErrorBoundary errorElement={<i>{errorContent}</i>}>
|
||||
{formatHelpContent(content, selectedHelpContent.format)}
|
||||
</ErrorBoundary>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
|
@ -79,6 +79,8 @@ interface BasicAndAdvancedQueryControlsProps
|
||||
|
||||
queryScreenUsage: QueryScreenUsage;
|
||||
|
||||
allowVariables?: boolean;
|
||||
|
||||
mode: string;
|
||||
setMode: (mode: string) => void;
|
||||
}
|
||||
@ -676,6 +678,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
||||
|
||||
return (<QuickFilter
|
||||
key={fieldName}
|
||||
allowVariables={props.allowVariables}
|
||||
fullFieldName={fieldName}
|
||||
tableMetaData={tableMetaData}
|
||||
updateCriteria={updateQuickCriteria}
|
||||
@ -701,6 +704,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
||||
updateCriteria={updateQuickCriteria}
|
||||
criteriaParam={getQuickCriteriaParam(fieldName)}
|
||||
fieldMetaData={field}
|
||||
allowVariables={props.allowVariables}
|
||||
defaultOperator={defaultOperator}
|
||||
queryScreenUsage={queryScreenUsage}
|
||||
handleRemoveQuickFilterField={handleRemoveQuickFilterField} />);
|
||||
|
@ -179,6 +179,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
|
||||
updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)}
|
||||
removeCriteria={() => removeCriteria(index)}
|
||||
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
|
||||
allowVariables={props.allowVariables}
|
||||
queryScreenUsage={props.queryScreenUsage}
|
||||
/>
|
||||
{/*JSON.stringify(criteria)*/}
|
||||
|
@ -199,6 +199,7 @@ interface FilterCriteriaRowProps
|
||||
removeCriteria: () => void;
|
||||
updateBooleanOperator: (newValue: string) => void;
|
||||
queryScreenUsage?: QueryScreenUsage;
|
||||
allowVariables?: boolean;
|
||||
}
|
||||
|
||||
FilterCriteriaRow.defaultProps =
|
||||
@ -267,7 +268,7 @@ export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValu
|
||||
return {criteriaIsValid, criteriaStatusTooltip};
|
||||
}
|
||||
|
||||
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage}: FilterCriteriaRowProps): JSX.Element
|
||||
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables}: FilterCriteriaRowProps): JSX.Element
|
||||
{
|
||||
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
|
||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
|
||||
@ -516,6 +517,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
||||
table={fieldTable}
|
||||
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
||||
queryScreenUsage={queryScreenUsage}
|
||||
allowVariables={allowVariables}
|
||||
/>
|
||||
</Box>
|
||||
<Box display="inline-block">
|
||||
|
@ -30,6 +30,7 @@ import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import InputAdornment from "@mui/material/InputAdornment/InputAdornment";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||
import AssignFilterVariable from "qqq/components/query/AssignFilterVariable";
|
||||
import CriteriaDateField, {NoWrapTooltip} from "qqq/components/query/CriteriaDateField";
|
||||
@ -39,7 +40,8 @@ import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster";
|
||||
import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow";
|
||||
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
import React, {SyntheticEvent, useReducer, useState} from "react";
|
||||
import React, {SyntheticEvent, useReducer} from "react";
|
||||
import {flushSync} from "react-dom";
|
||||
|
||||
interface Props
|
||||
{
|
||||
@ -50,6 +52,7 @@ interface Props
|
||||
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
|
||||
initiallyOpenMultiValuePvs?: boolean;
|
||||
queryScreenUsage?: QueryScreenUsage;
|
||||
allowVariables?: boolean;
|
||||
}
|
||||
|
||||
FilterCriteriaRowValues.defaultProps =
|
||||
@ -57,6 +60,10 @@ FilterCriteriaRowValues.defaultProps =
|
||||
initiallyOpenMultiValuePvs: false
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* get the type to use for an <input> from a QFieldMetaData
|
||||
***************************************************************************/
|
||||
export const getTypeForTextField = (field: QFieldMetaData): string =>
|
||||
{
|
||||
let type = "search";
|
||||
@ -77,10 +84,15 @@ export const getTypeForTextField = (field: QFieldMetaData): string =>
|
||||
return (type);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Make an <input type=text> (actually, might be a different type, but that's
|
||||
* the gist of it), for a field.
|
||||
***************************************************************************/
|
||||
export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-", allowVariables = false) =>
|
||||
{
|
||||
const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type;
|
||||
|
||||
const inputId = `${idPrefix}${criteria.id}`;
|
||||
let type = getTypeForTextField(field);
|
||||
const inputLabelProps: any = {};
|
||||
|
||||
@ -95,10 +107,13 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
value = ValueUtils.formatDateTimeValueForForm(value);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* Event handler for the clear 'x'.
|
||||
***************************************************************************/
|
||||
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
|
||||
{
|
||||
valueChangeHandler(event, index, "");
|
||||
document.getElementById(`${idPrefix}${criteria.id}`).focus();
|
||||
document.getElementById(inputId).focus();
|
||||
};
|
||||
|
||||
|
||||
@ -119,6 +134,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* make a version of the text field for when the criteria's value is set to
|
||||
* be a "variable"
|
||||
***************************************************************************/
|
||||
const makeFilterVariableTextField = (expression: FilterVariableExpression, valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
|
||||
{
|
||||
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
|
||||
@ -148,6 +167,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
/></NoWrapTooltip>;
|
||||
};
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// set up an 'x' icon as an end-adornment, to clear value from the field //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
const inputProps: any = {};
|
||||
inputProps.endAdornment = (
|
||||
<InputAdornment position="end">
|
||||
@ -157,18 +180,64 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
</InputAdornment>
|
||||
);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* onChange event handler. deals with, if the field has a to upper/lower
|
||||
* case rule on it, to apply that transform, and adjust the cursor.
|
||||
* See: https://giacomocerquone.com/blog/keep-input-cursor-still
|
||||
***************************************************************************/
|
||||
function onChange(event: any)
|
||||
{
|
||||
const beforeStart = event.target.selectionStart;
|
||||
const beforeEnd = event.target.selectionEnd;
|
||||
|
||||
let isToUpperCase = DynamicFormUtils.isToUpperCase(field);
|
||||
let isToLowerCase = DynamicFormUtils.isToLowerCase(field);
|
||||
|
||||
if (isToUpperCase || isToLowerCase)
|
||||
{
|
||||
flushSync(() =>
|
||||
{
|
||||
let newValue = event.currentTarget.value;
|
||||
|
||||
if (isToUpperCase)
|
||||
{
|
||||
newValue = newValue.toUpperCase();
|
||||
}
|
||||
if (isToLowerCase)
|
||||
{
|
||||
newValue = newValue.toLowerCase();
|
||||
}
|
||||
|
||||
event.currentTarget.value = newValue;
|
||||
});
|
||||
|
||||
const input = document.getElementById(inputId);
|
||||
if (input)
|
||||
{
|
||||
// @ts-ignore
|
||||
input.setSelectionRange(beforeStart, beforeEnd);
|
||||
}
|
||||
}
|
||||
|
||||
valueChangeHandler(event, valueIndex);
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
// return the element //
|
||||
////////////////////////
|
||||
return <Box sx={{margin: 0, padding: 0, display: "flex"}}>
|
||||
{
|
||||
isExpression ? (
|
||||
makeFilterVariableTextField(criteria.values[valueIndex], valueIndex, label, idPrefix)
|
||||
) : (
|
||||
<TextField
|
||||
id={`${idPrefix}${criteria.id}`}
|
||||
id={inputId}
|
||||
label={label}
|
||||
variant="standard"
|
||||
autoComplete="off"
|
||||
type={type}
|
||||
onChange={(event) => valueChangeHandler(event, valueIndex)}
|
||||
onChange={onChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={value}
|
||||
InputLabelProps={inputLabelProps}
|
||||
@ -187,16 +256,23 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
};
|
||||
|
||||
|
||||
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs, queryScreenUsage}: Props): JSX.Element
|
||||
/***************************************************************************
|
||||
* Component that is the "values" portion of a FilterCriteria Row in the
|
||||
* advanced query filter editor.
|
||||
***************************************************************************/
|
||||
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs, queryScreenUsage, allowVariables}: Props): JSX.Element
|
||||
{
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
const [allowVariables, setAllowVariables] = useState(queryScreenUsage == "reportSetup");
|
||||
|
||||
if (!operatorOption)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* Callback for the Save button from the paste-values modal
|
||||
***************************************************************************/
|
||||
function saveNewPasterValues(newValues: any[])
|
||||
{
|
||||
if (criteria.values)
|
||||
@ -222,6 +298,9 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
|
||||
const isExpression = criteria.values && criteria.values[0] && criteria.values[0].type;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// render different form element9s) based on operator option's "value mode" //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
switch (operatorOption.valueMode)
|
||||
{
|
||||
case ValueMode.NONE:
|
||||
@ -320,7 +399,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
initialValues = criteria.values;
|
||||
}
|
||||
}
|
||||
return <Box mb={-1.5}>
|
||||
return <Box>
|
||||
<DynamicSelect
|
||||
tableName={table.name}
|
||||
fieldName={field.name}
|
||||
|
@ -52,6 +52,7 @@ interface QuickFilterProps
|
||||
defaultOperator?: QCriteriaOperator;
|
||||
handleRemoveQuickFilterField?: (fieldName: string) => void;
|
||||
queryScreenUsage?: QueryScreenUsage;
|
||||
allowVariables?: boolean;
|
||||
}
|
||||
|
||||
QuickFilter.defaultProps =
|
||||
@ -141,7 +142,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
|
||||
** Component to render a QuickFilter - that is - a button, with a Menu under it,
|
||||
** with Operator and Value controls.
|
||||
*******************************************************************************/
|
||||
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField, queryScreenUsage}: QuickFilterProps): JSX.Element
|
||||
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField, queryScreenUsage, allowVariables}: QuickFilterProps): JSX.Element
|
||||
{
|
||||
const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : [];
|
||||
const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName);
|
||||
@ -549,6 +550,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
criteria={criteria}
|
||||
field={fieldMetaData}
|
||||
table={tableForField}
|
||||
allowVariables={allowVariables}
|
||||
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
||||
initiallyOpenMultiValuePvs={true} // todo - maybe not?
|
||||
/>
|
||||
|
@ -47,6 +47,7 @@ import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget";
|
||||
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
|
||||
import StepperCard from "qqq/components/widgets/misc/StepperCard";
|
||||
import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
||||
import WorkflowViewer from "qqq/components/widgets/misc/WorkflowViewer";
|
||||
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
||||
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
||||
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
||||
@ -581,6 +582,14 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "workflow" && (
|
||||
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
||||
<Widget widgetMetaData={widgetMetaData}>
|
||||
<WorkflowViewer workflowId={widgetData[i].queryParams.id} />
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "dataBagViewer" && (
|
||||
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
||||
@ -599,8 +608,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "filterAndColumnsSetup" && (
|
||||
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
||||
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
|
||||
widgetData && widgetData[i] &&
|
||||
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
|
||||
{
|
||||
}} />
|
||||
)
|
||||
|
@ -48,6 +48,7 @@ interface FilterAndColumnsSetupWidgetProps
|
||||
{
|
||||
isEditable: boolean;
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
widgetData: any;
|
||||
recordValues: { [name: string]: any };
|
||||
onSaveCallback?: (values: { [name: string]: any }) => void;
|
||||
}
|
||||
@ -82,10 +83,10 @@ const qController = Client.getInstance();
|
||||
/*******************************************************************************
|
||||
** Component for editing the main setup of a report - that is: filter & columns
|
||||
*******************************************************************************/
|
||||
export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, recordValues, onSaveCallback}: FilterAndColumnsSetupWidgetProps): JSX.Element
|
||||
export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, widgetData, recordValues, onSaveCallback}: FilterAndColumnsSetupWidgetProps): JSX.Element
|
||||
{
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [hideColumns, setHideColumns] = useState(widgetMetaData?.defaultValues?.has("hideColumns") && widgetMetaData?.defaultValues?.get("hideColumns"));
|
||||
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||
|
||||
const [alertContent, setAlertContent] = useState(null as string);
|
||||
@ -107,7 +108,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
let columns: QQueryColumns = null;
|
||||
let usingDefaultEmptyFilter = false;
|
||||
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
|
||||
const defaultFilterFields = getDefaultFilterFieldNames(widgetMetaData);
|
||||
const defaultFilterFields = widgetData?.filterDefaultFieldNames;
|
||||
if (!queryFilter)
|
||||
{
|
||||
queryFilter = new QQueryFilter();
|
||||
@ -153,7 +154,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if a default table name specified, use it, otherwise use it from the record values //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
let tableName = widgetMetaData?.defaultValues?.get("tableName");
|
||||
let tableName = widgetData?.tableName;
|
||||
if (!tableName && recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
|
||||
{
|
||||
tableName = recordValues["tableName"];
|
||||
@ -174,27 +175,13 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
}, [JSON.stringify(recordValues)]);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function getDefaultFilterFieldNames(widgetMetaData: QWidgetMetaData)
|
||||
{
|
||||
if (widgetMetaData?.defaultValues?.has("filterDefaultFieldNames"))
|
||||
{
|
||||
return (widgetMetaData.defaultValues.get("filterDefaultFieldNames").split(","));
|
||||
}
|
||||
|
||||
return ([]);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function openEditor()
|
||||
{
|
||||
let missingRequiredFields = [] as string[];
|
||||
getDefaultFilterFieldNames(widgetMetaData)?.forEach((fieldName: string) =>
|
||||
widgetData?.filterDefaultFieldNames?.forEach((fieldName: string) =>
|
||||
{
|
||||
if (!recordValues[fieldName])
|
||||
{
|
||||
@ -430,6 +417,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
}
|
||||
{
|
||||
tableMetaData && <RecordQuery
|
||||
allowVariables={widgetData?.allowVariables}
|
||||
ref={recordQueryRef}
|
||||
table={tableMetaData}
|
||||
usage="reportSetup"
|
||||
|
383
src/qqq/components/widgets/misc/WorkflowViewer.tsx
Normal file
383
src/qqq/components/widgets/misc/WorkflowViewer.tsx
Normal file
@ -0,0 +1,383 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import {Chip} from "@mui/material";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Avatar from "@mui/material/Avatar";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemAvatar from "@mui/material/ListItemAvatar";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import TabPanel from "qqq/components/misc/TabPanel";
|
||||
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
|
||||
import WorkflowEditor, {WorkflowEditorProps} from "qqq/components/workflows/WorkflowEditor";
|
||||
import WorkflowPreview from "qqq/components/workflows/WorkflowPreview";
|
||||
import {LoadingState} from "qqq/models/LoadingState";
|
||||
import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
import "ace-builds/src-noconflict/mode-java";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import React, {useReducer, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import "ace-builds/src-noconflict/ext-language_tools";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
// Declaring props types for ViewForm
|
||||
interface Props
|
||||
{
|
||||
workflowId?: number;
|
||||
}
|
||||
|
||||
WorkflowViewer.defaultProps =
|
||||
{};
|
||||
|
||||
export default function WorkflowViewer({workflowId}: Props): JSX.Element
|
||||
{
|
||||
const [workflowRecord, setWorkflowRecord] = useState(null as QRecord);
|
||||
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
|
||||
const [versionRecordList, setVersionRecordList] = useState(null as QRecord[]);
|
||||
const [selectedRevisionRecord, setSelectedRevisionRecord] = useState(null as QRecord);
|
||||
const [currentVersionId, setCurrentVersionId] = useState(null as number);
|
||||
const [notFoundMessage, setNotFoundMessage] = useState(null);
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
const [editorProps, setEditorProps] = useState(null as WorkflowEditorProps);
|
||||
const [successText, setSuccessText] = useState(null as string);
|
||||
const [failText, setFailText] = useState(null as string);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
const [loadingSelectedVersion, _] = useState(new LoadingState(forceUpdate, "loading"));
|
||||
|
||||
if (!asyncLoadInited)
|
||||
{
|
||||
setAsyncLoadInited(true);
|
||||
|
||||
(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const workflowRecord = await qController.get("workflow", workflowId);
|
||||
setWorkflowRecord(workflowRecord);
|
||||
|
||||
const criteria = [new QFilterCriteria("workflowId", QCriteriaOperator.EQUALS, [workflowId])];
|
||||
const orderBys = [new QFilterOrderBy("sequenceNo", false)];
|
||||
const filter = new QQueryFilter(criteria, orderBys, null, "AND", 0, 25);
|
||||
const versions = await qController.query("workflowRevision", filter);
|
||||
console.log("Fetched versions:");
|
||||
console.log(versions);
|
||||
setVersionRecordList(versions);
|
||||
|
||||
if (versions && versions.length > 0)
|
||||
{
|
||||
setCurrentVersionId(versions[0].values.get("id"));
|
||||
const latestVersion = await qController.get("workflowRevision", versions[0].values.get("id"));
|
||||
console.log("Fetched latestVersion:");
|
||||
console.log(latestVersion);
|
||||
setSelectedRevisionRecord(latestVersion);
|
||||
loadingSelectedVersion.setNotLoading();
|
||||
forceUpdate();
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
if (e instanceof QException)
|
||||
{
|
||||
if ((e as QException).status === 404)
|
||||
{
|
||||
setNotFoundMessage("Workflow data could not be found.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
setNotFoundMessage("Error loading workflow data: " + e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
const editContents = (contents: string) =>
|
||||
{
|
||||
const editorProps = {} as WorkflowEditorProps;
|
||||
editorProps.title = (contents ? "Editing Workflow: " : "Initializing Workflow: ") + workflowRecord?.values?.get("name");
|
||||
editorProps.contents = contents;
|
||||
editorProps.workflowId = workflowId;
|
||||
setEditorProps(editorProps);
|
||||
};
|
||||
|
||||
const closeEditingWorkflow = (event: object, reason: string, alert: string = null) =>
|
||||
{
|
||||
if (reason === "backdropClick" || reason === "escapeKeyDown")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (reason === "saved")
|
||||
{
|
||||
setAsyncLoadInited(false);
|
||||
forceUpdate();
|
||||
|
||||
if (alert)
|
||||
{
|
||||
setSuccessText(alert);
|
||||
}
|
||||
}
|
||||
else if (reason === "failed")
|
||||
{
|
||||
setAsyncLoadInited(false);
|
||||
forceUpdate();
|
||||
|
||||
if (alert)
|
||||
{
|
||||
setFailText(alert);
|
||||
}
|
||||
}
|
||||
|
||||
setEditorProps(null);
|
||||
};
|
||||
|
||||
const changeTab = (newValue: number) =>
|
||||
{
|
||||
setSelectedTab(newValue);
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
const selectVersion = (version: QRecord) =>
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
// fetch the full version
|
||||
setSelectedRevisionRecord(version);
|
||||
loadingSelectedVersion.setLoading();
|
||||
|
||||
const selectedVersion = await qController.get("workflowRevision", version.values.get("id"));
|
||||
console.log("Fetched selectedVersion:");
|
||||
console.log(selectedVersion);
|
||||
setSelectedRevisionRecord(selectedVersion);
|
||||
loadingSelectedVersion.setNotLoading();
|
||||
forceUpdate();
|
||||
})();
|
||||
};
|
||||
|
||||
function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord)
|
||||
{
|
||||
return <List sx={{pl: 3, height: "400px", overflow: "auto"}}>
|
||||
{
|
||||
(versionRecordList == null || versionRecordList.length == 0) ?
|
||||
<Typography variant="body2">
|
||||
There are not any versions of this workflow.
|
||||
</Typography>
|
||||
: <></>
|
||||
}
|
||||
{
|
||||
versionRecordList?.map((version: any) => (
|
||||
<React.Fragment key={version.values.get("id")}>
|
||||
<ListItem sx={{p: 1}} alignItems="flex-start" selected={selectedVersionRecord?.values?.get("id") == version.values.get("id")} onClick={(event) => selectVersion(version)}>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{bgcolor: DeveloperModeUtils.revToColor("", workflowId, version.values.get("sequenceNo"))}}>{`${version.values.get("sequenceNo")}`}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{fontSize: "1rem"}}
|
||||
secondaryTypographyProps={{fontSize: ".85rem"}}
|
||||
primary={
|
||||
<div style={{overflow: "hidden", textOverflow: "ellipsis", maxHeight: "5rem"}} title={version.values.get("commitMessage")}>
|
||||
{currentVersionId == version?.values?.get("id") && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
|
||||
{version.values.get("commitMessage")}
|
||||
</div>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
{ValueUtils.formatDateTime(version.values.get("createDate"))}
|
||||
<br />
|
||||
{version.values.get("author")}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider sx={{my: 0.5}} variant="inset" component="li" />
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</List>;
|
||||
}
|
||||
|
||||
let editButtonTooltip = "";
|
||||
let editButtonText = "Create New Version";
|
||||
if (currentVersionId)
|
||||
{
|
||||
if (currentVersionId === selectedRevisionRecord?.values?.get("id"))
|
||||
{
|
||||
editButtonTooltip = "If you make any changes to this workflow, a new version will be created when you hit Save.";
|
||||
editButtonText = "Edit";
|
||||
}
|
||||
else
|
||||
{
|
||||
editButtonTooltip = "If you want to make this previous Version active, bring up the Edit window, make any changes " +
|
||||
"to the old Version if they are needed, then click Save. A new Version will be created, and set as Current.";
|
||||
editButtonText = "Edit and Activate";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Box>
|
||||
{
|
||||
<Box>
|
||||
{
|
||||
successText ? (
|
||||
<Snackbar open={successText !== null && successText !== ""} autoHideDuration={6000} onClose={() => setSuccessText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
||||
<Alert color="success" onClose={() => setSuccessText(null)}>
|
||||
{successText}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
) : ("")
|
||||
}
|
||||
{
|
||||
failText ? (
|
||||
<Snackbar open={failText !== null && failText !== ""} autoHideDuration={6000} onClose={() => setFailText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
||||
<Alert color="error" onClose={() => setFailText(null)}>
|
||||
{failText}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
) : ("")
|
||||
}
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<>
|
||||
<Tabs
|
||||
sx={{m: 0, mb: 1, mt: 0}}
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => changeTab(newValue)}
|
||||
variant="standard"
|
||||
>
|
||||
<Tab label="Versions" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
||||
<Tab label="Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
||||
<Tab label="Something Else" id="simple-tab-2" aria-controls="simple-tabpanel-2" />
|
||||
</Tabs>
|
||||
|
||||
<TabPanel index={0} value={selectedTab}>
|
||||
<Grid container>
|
||||
<Grid item xs={4}>
|
||||
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
|
||||
<Typography variant="h6" pl={3}>Versions</Typography>
|
||||
</Box>
|
||||
{getVersionsList(versionRecordList, selectedRevisionRecord)}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={8}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} pb={1} height="40px">
|
||||
{
|
||||
selectedRevisionRecord ?
|
||||
<Typography variant="h6">
|
||||
Version {selectedRevisionRecord.values.get("sequenceNo")}
|
||||
{
|
||||
currentVersionId === selectedRevisionRecord.values.get("id")
|
||||
? (<> (Current)</>)
|
||||
: <></>
|
||||
}
|
||||
</Typography>
|
||||
: <></>
|
||||
}
|
||||
<CustomWidthTooltip title={editButtonTooltip}>
|
||||
<Button sx={{py: 0}} onClick={() => editContents(selectedRevisionRecord?.values?.get("contents"))}>
|
||||
{editButtonText}
|
||||
</Button>
|
||||
</CustomWidthTooltip>
|
||||
</Box>
|
||||
<WorkflowPreview />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel index={1} value={selectedTab}>
|
||||
<Grid container height="440px">
|
||||
<Grid item xs={4}>
|
||||
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
|
||||
<Typography variant="h6" pl={3}>Versions</Typography>
|
||||
</Box>
|
||||
{getVersionsList(versionRecordList, selectedRevisionRecord)}
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
|
||||
<Typography variant="h6" pl={3}>Data Preview (Version {selectedRevisionRecord?.values?.get("sequenceNo")})</Typography>
|
||||
</Box>
|
||||
<Box height="400px" overflow="auto" ml={1} fontSize="14px">
|
||||
{
|
||||
loadingSelectedVersion.isNotLoading() && selectedRevisionRecord && selectedRevisionRecord.values.get("contents") ? (
|
||||
<>
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme="github"
|
||||
name={"viewData"}
|
||||
readOnly
|
||||
highlightActiveLine={false}
|
||||
setOptions={{useWorker: false}}
|
||||
editorProps={{$blockScrolling: true}}
|
||||
width="100%"
|
||||
height="400px"
|
||||
value={selectedRevisionRecord?.values?.get("contents")}
|
||||
/>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
{
|
||||
loadingSelectedVersion.isLoadingSlow() && selectedRevisionRecord && <Box fontSize="14px" pl={3}>Loading...</Box>
|
||||
}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
</>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{
|
||||
editorProps &&
|
||||
<Modal open={editorProps !== null} onClose={(event, reason) => closeEditingWorkflow(event, reason)}>
|
||||
<WorkflowEditor
|
||||
closeCallback={closeEditingWorkflow}
|
||||
{...editorProps}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
21
src/qqq/components/workflows/RootEditor.tsx
Normal file
21
src/qqq/components/workflows/RootEditor.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import {ChangeEvent} from "react";
|
||||
import {useRootEditor} from "sequential-workflow-designer-react";
|
||||
import {WorkflowDefinition} from "./model";
|
||||
|
||||
export function RootEditor()
|
||||
{
|
||||
const {properties, setProperty, isReadonly} = useRootEditor<WorkflowDefinition>();
|
||||
|
||||
function onAlfaChanged(e: ChangeEvent)
|
||||
{
|
||||
setProperty("alfa", (e.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Optimization Workflow Editor</h2>
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.<br /><br />Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</>
|
||||
);
|
||||
}
|
69
src/qqq/components/workflows/StepEditor.tsx
Normal file
69
src/qqq/components/workflows/StepEditor.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import {ChangeEvent} from "react";
|
||||
import {useStepEditor} from "sequential-workflow-designer-react";
|
||||
import {SwitchStep, TaskStep, WarehouseOptimizationStep} from "./model";
|
||||
|
||||
export function StepEditor()
|
||||
{
|
||||
const {type, name, step, properties, isReadonly, setName, setProperty, notifyPropertiesChanged, notifyChildrenChanged} =
|
||||
useStepEditor<TaskStep | SwitchStep | WarehouseOptimizationStep>();
|
||||
|
||||
function onNameChanged(e: ChangeEvent)
|
||||
{
|
||||
setName((e.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
function onXChanged(e: ChangeEvent)
|
||||
{
|
||||
setProperty("warehouse", (e.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
function onYChanged(e: ChangeEvent)
|
||||
{
|
||||
properties["wmsConnection"] = (e.target as HTMLInputElement).value;
|
||||
notifyPropertiesChanged();
|
||||
}
|
||||
|
||||
function toggleExtraBranch()
|
||||
{
|
||||
const switchStep = step as SwitchStep;
|
||||
if (switchStep.branches["extra"])
|
||||
{
|
||||
delete switchStep.branches["extra"];
|
||||
}
|
||||
else
|
||||
{
|
||||
switchStep.branches["extra"] = [];
|
||||
}
|
||||
notifyChildrenChanged();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Step Editor</h2>
|
||||
<h3>{type}</h3>
|
||||
|
||||
<h4>Pre-Script</h4>
|
||||
<select>
|
||||
<option>Pre Script #1</option>
|
||||
<option>Pre Script #2</option>
|
||||
<option>Pre Script #3</option>
|
||||
</select>
|
||||
|
||||
<h4>Post-Script</h4>
|
||||
<select>
|
||||
<option>Post Script #1</option>
|
||||
<option>Post Script #2</option>
|
||||
<option>Post Script #3</option>
|
||||
</select>
|
||||
|
||||
{type === "switch" && (
|
||||
<>
|
||||
<h4>Extra branch</h4>
|
||||
<button onClick={toggleExtraBranch} disabled={isReadonly}>
|
||||
Toggle branch
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
214
src/qqq/components/workflows/StepUtils.ts
Normal file
214
src/qqq/components/workflows/StepUtils.ts
Normal file
@ -0,0 +1,214 @@
|
||||
import {Branches, Uid} from "sequential-workflow-designer";
|
||||
import {ContainerStep, OptimizationStepType, SwitchStep, TaskStep, WarehouseOptimizationStep} from "./model";
|
||||
|
||||
export function createTaskStep(): TaskStep
|
||||
{
|
||||
return {
|
||||
id: Uid.next(),
|
||||
componentType: "task",
|
||||
type: "task",
|
||||
name: "blah",
|
||||
properties: {}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//////////////////////
|
||||
// define all steps //
|
||||
//////////////////////
|
||||
export function createDetermineWarehouseRoutingStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Determine Warehouse", "determineWarehouseRouting");
|
||||
}
|
||||
|
||||
export function createDetermineLineHaulLaneStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Determine Line Haul Lane", "determineLineHaulLane");
|
||||
}
|
||||
|
||||
export function createValidateLineItemsStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Validate Line Items", "validateLineItems");
|
||||
}
|
||||
|
||||
export function createDetermineCoolingCategoryStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Determine Cooling Category", "determineCoolingCategory");
|
||||
}
|
||||
|
||||
export function createValidateOptimizationRulesStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Validate Optimization Rules", "validateOptimizationRules");
|
||||
}
|
||||
|
||||
export function createValidateAddressStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Validate Address", "validateAddress");
|
||||
}
|
||||
|
||||
export function createDetermineCarrierServiceStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Determine Carrier Service", "determineCarrierService");
|
||||
}
|
||||
|
||||
export function createDetermineTNTStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Determine TNT ", "determineTNT");
|
||||
}
|
||||
|
||||
export function createDetermineOrderServiceDatesStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Determine Order Service Dates ", "determineOrderServiceDates");
|
||||
}
|
||||
|
||||
export function createOrderMatchesFilterSelectorStep(): WarehouseOptimizationStep
|
||||
{
|
||||
return createStep("Order Matches Filter Selector", "orderMatchesFilterSelector");
|
||||
}
|
||||
|
||||
|
||||
////////////////////////
|
||||
// define all outputs //
|
||||
////////////////////////
|
||||
export function createDetermineWarehouseRoutingOuptut(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {Edison: [], Patterson: [], Stockton: []}));
|
||||
}
|
||||
|
||||
export function createDetermineLineHaulLaneOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {Chicago: [], Dallas: [], Sheboygan: []}));
|
||||
}
|
||||
|
||||
export function createValidateLineItemsOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {"Is Valid": [], "Not Valid": []}));
|
||||
}
|
||||
|
||||
export function createDetermineCoolingCategoryOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {"Ambient": [], "Frozen": [], "Other": []}));
|
||||
}
|
||||
|
||||
export function createValidateOptimizationRulesOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {"Is Valid": [], "Not Valid": []}));
|
||||
}
|
||||
|
||||
export function createAddressValidationOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {"Is Valid": [], "Not Valid": []}));
|
||||
}
|
||||
|
||||
export function createDetermineCarrierServiceOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {"Fedex Ground": [], "UPS Ground": [], "OnTrac Ground": []}));
|
||||
}
|
||||
|
||||
export function createDetermineTNTOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {1: [], 2: [], 3: [], "4+": []}));
|
||||
}
|
||||
|
||||
export function createDetermineOrderServiceDatesOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {Monday: [], Tuesday: [], Wednesday: []}));
|
||||
}
|
||||
|
||||
export function createOrderMatchesFilterSelectorOutput(): SwitchStep
|
||||
{
|
||||
return (createOutput("Output", {"Matches": [], "No Match": []}));
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////
|
||||
// groups of steps + output //
|
||||
//////////////////////////////
|
||||
export function createDetermineWarehouseRoutingGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Determine Warehouse Routing", [createDetermineWarehouseRoutingStep(), createDetermineWarehouseRoutingOuptut()]));
|
||||
}
|
||||
|
||||
export function createDetermineLineHaulLaneGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Determine Line Haul Lane", [createDetermineLineHaulLaneStep(), createDetermineLineHaulLaneOutput()]));
|
||||
}
|
||||
|
||||
export function createValidateLineItemsGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Validate Line Items", [createValidateLineItemsStep(), createValidateLineItemsOutput()]));
|
||||
}
|
||||
|
||||
export function createDetermineCoolingCategoryGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Determine Cooling Category", [createDetermineCoolingCategoryStep(), createDetermineCoolingCategoryOutput()]));
|
||||
}
|
||||
|
||||
export function createValidateOptimizationRulesGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Validate Optimization Rules", [createValidateOptimizationRulesStep(), createValidateOptimizationRulesOutput()]));
|
||||
}
|
||||
|
||||
export function createValidateAddressGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Validate Address", [createValidateAddressStep(), createAddressValidationOutput()]));
|
||||
}
|
||||
|
||||
export function createDetermineCarrierServiceGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Determine Carrier Service", [createDetermineCarrierServiceStep(), createDetermineCarrierServiceOutput()]));
|
||||
}
|
||||
|
||||
export function createDetermineTNTGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Determine TNT", [createDetermineTNTStep(), createDetermineTNTOutput()]));
|
||||
}
|
||||
|
||||
export function createDetermineOrderServiceDatesGroup(): ContainerStep
|
||||
{
|
||||
return (createGroup("Determine Order Service Dates", [createDetermineOrderServiceDatesStep(), createDetermineOrderServiceDatesOutput()]));
|
||||
}
|
||||
|
||||
export function createOrderMatchesFilterSelector(): ContainerStep
|
||||
{
|
||||
return (createGroup("Order Matches Filter Selector", [createOrderMatchesFilterSelectorStep(), createOrderMatchesFilterSelectorOutput()]));
|
||||
}
|
||||
|
||||
|
||||
///////////
|
||||
// utils //
|
||||
///////////
|
||||
export function createStep(name: string, type: OptimizationStepType): WarehouseOptimizationStep
|
||||
{
|
||||
return {
|
||||
id: Uid.next(),
|
||||
componentType: "task",
|
||||
type: type,
|
||||
name: name,
|
||||
properties: {}
|
||||
};
|
||||
}
|
||||
|
||||
export function createOutput(name: string, branches: Branches): SwitchStep
|
||||
{
|
||||
return {
|
||||
id: Uid.next(),
|
||||
componentType: "switch",
|
||||
type: "switch",
|
||||
name: name,
|
||||
properties: {},
|
||||
branches: branches
|
||||
};
|
||||
}
|
||||
|
||||
export function createGroup(name: string, sequence: (WarehouseOptimizationStep | SwitchStep)[]): ContainerStep
|
||||
{
|
||||
return {
|
||||
id: Uid.next(),
|
||||
componentType: "container",
|
||||
type: "container",
|
||||
name: name,
|
||||
properties: {},
|
||||
sequence: sequence
|
||||
};
|
||||
}
|
187
src/qqq/components/workflows/WorkflowEditor.tsx
Normal file
187
src/qqq/components/workflows/WorkflowEditor.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||
import {ToggleButtonGroup, Typography} from "@mui/material";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Box from "@mui/material/Box";
|
||||
import Card from "@mui/material/Card";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Snackbar from "@mui/material/Snackbar";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import FormData from "form-data";
|
||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import WorkflowPreview from "qqq/components/workflows/WorkflowPreview";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useReducer, useState} from "react";
|
||||
|
||||
export interface WorkflowEditorProps
|
||||
{
|
||||
title: string;
|
||||
workflowId: number;
|
||||
contents: string;
|
||||
closeCallback: any;
|
||||
}
|
||||
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function WorkflowEditor({title, workflowId, contents, closeCallback}: WorkflowEditorProps): JSX.Element
|
||||
{
|
||||
const [closing, setClosing] = useState(false);
|
||||
const [updatedCode, setUpdatedCode] = useState(contents);
|
||||
const [commitMessage, setCommitMessage] = useState("");
|
||||
const [openTool, setOpenTool] = useState(null);
|
||||
const [errorAlert, setErrorAlert] = useState("");
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
const changeOpenTool = (event: React.MouseEvent<HTMLElement>, newValue: string | null) =>
|
||||
{
|
||||
setOpenTool(newValue);
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// need this to make Ace recognize new height. //
|
||||
/////////////////////////////////////////////////
|
||||
setTimeout(() =>
|
||||
{
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const saveClicked = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
JSON.parse(updatedCode);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
setErrorAlert("Cannot save Workflow Contents. Invalid json: " + e);
|
||||
return;
|
||||
}
|
||||
|
||||
setClosing(true);
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const formData = new FormData();
|
||||
formData.append("workflowId", workflowId);
|
||||
formData.append("contents", updatedCode);
|
||||
formData.append("commitMessage", commitMessage);
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// we don't want this job to go async, so, pass a large timeout //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
formData.append("_qStepTimeoutMillis", 60 * 1000);
|
||||
|
||||
const formDataHeaders = {
|
||||
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
|
||||
};
|
||||
|
||||
const processResult = await qController.processInit("storeWorkflowVersionProcess", formData, formDataHeaders);
|
||||
if (processResult instanceof QJobError)
|
||||
{
|
||||
const jobError = processResult as QJobError;
|
||||
closeCallback(null, "failed", jobError.userFacingError ?? jobError.error);
|
||||
}
|
||||
console.log("process result");
|
||||
console.log(processResult);
|
||||
|
||||
closeCallback(null, "saved", "Saved New Workflow Version");
|
||||
})();
|
||||
};
|
||||
|
||||
const cancelClicked = () =>
|
||||
{
|
||||
setClosing(true);
|
||||
closeCallback(null, "cancelled");
|
||||
};
|
||||
|
||||
const updateCode = (value: string, event: any) =>
|
||||
{
|
||||
console.log("Updating code");
|
||||
setUpdatedCode(value);
|
||||
forceUpdate();
|
||||
};
|
||||
|
||||
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
setCommitMessage(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
|
||||
<Card sx={{height: "100%", p: 3}}>
|
||||
|
||||
<Snackbar open={errorAlert !== null && errorAlert !== ""} onClose={(event?: React.SyntheticEvent | Event, reason?: string) =>
|
||||
{
|
||||
if (reason === "clickaway")
|
||||
{
|
||||
return;
|
||||
}
|
||||
setErrorAlert("");
|
||||
}} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
||||
<Alert color="error" onClose={() => setErrorAlert("")}>
|
||||
{errorAlert}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h5" pb={1}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
<Box>
|
||||
<Typography variant="body2" display="inline" pr={1}>
|
||||
Tools:
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={openTool}
|
||||
exclusive
|
||||
onChange={changeOpenTool}
|
||||
size="small"
|
||||
sx={{pb: 1}}
|
||||
>
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
||||
<WorkflowPreview />
|
||||
</Box>
|
||||
|
||||
<Box pt={1}>
|
||||
<Grid container alignItems="flex-end">
|
||||
<Box width="50%">
|
||||
<TextField id="commitMessage" label="Commit Message" variant="standard" fullWidth value={commitMessage} onChange={updateCommitMessage} />
|
||||
</Box>
|
||||
<Grid container justifyContent="flex-end" spacing={3}>
|
||||
<QCancelButton disabled={closing} onClickHandler={cancelClicked} />
|
||||
<QSaveButton disabled={closing} onClickHandler={saveClicked} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowEditor;
|
199
src/qqq/components/workflows/WorkflowPreview.tsx
Normal file
199
src/qqq/components/workflows/WorkflowPreview.tsx
Normal file
@ -0,0 +1,199 @@
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {ObjectCloner, Step, StepsConfiguration, ToolboxConfiguration, ValidatorConfiguration} from "sequential-workflow-designer";
|
||||
import {SequentialWorkflowDesigner, useSequentialWorkflowDesignerController, wrapDefinition} from "sequential-workflow-designer-react";
|
||||
import {WorkflowDefinition} from "./model";
|
||||
import {RootEditor} from "./RootEditor";
|
||||
import {StepEditor} from "./StepEditor";
|
||||
import {createDetermineCarrierServiceGroup, createDetermineCoolingCategoryGroup, createDetermineLineHaulLaneGroup, createDetermineOrderServiceDatesGroup, createDetermineTNTGroup, createDetermineWarehouseRoutingGroup, createOrderMatchesFilterSelector, createTaskStep, createValidateAddressGroup, createValidateLineItemsGroup, createValidateOptimizationRulesGroup} from "./StepUtils";
|
||||
|
||||
const startDefinition: WorkflowDefinition = {
|
||||
properties: {
|
||||
alfa: "bravo"
|
||||
},
|
||||
sequence: []
|
||||
};
|
||||
|
||||
|
||||
function WorkflowPreview()
|
||||
{
|
||||
const controller = useSequentialWorkflowDesignerController();
|
||||
const toolboxConfiguration: ToolboxConfiguration = useMemo(
|
||||
() => ({
|
||||
groups: [{
|
||||
name: "Optimization Steps", steps: [
|
||||
createDetermineCarrierServiceGroup(),
|
||||
createDetermineCoolingCategoryGroup(),
|
||||
createDetermineLineHaulLaneGroup(),
|
||||
createDetermineOrderServiceDatesGroup(),
|
||||
createDetermineTNTGroup(),
|
||||
createDetermineWarehouseRoutingGroup()
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Validators", steps: [
|
||||
createValidateAddressGroup(),
|
||||
createValidateLineItemsGroup(),
|
||||
createValidateOptimizationRulesGroup()
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Utilities", steps: [
|
||||
createOrderMatchesFilterSelector()
|
||||
]
|
||||
}
|
||||
]
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const stepsConfiguration: StepsConfiguration = useMemo(
|
||||
() => ({
|
||||
iconUrlProvider: () => null
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const validatorConfiguration: ValidatorConfiguration = useMemo(
|
||||
() => ({
|
||||
step: (step: Step) => Boolean(step.name),
|
||||
root: (definition: WorkflowDefinition) => Boolean(definition.properties.alfa)
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [isToolboxCollapsed, setIsToolboxCollapsed] = useState(false);
|
||||
const [isEditorCollapsed, setIsEditorCollapsed] = useState(false);
|
||||
const [definition, setDefinition] = useState(() => wrapDefinition(startDefinition));
|
||||
const [selectedStepId, setSelectedStepId] = useState<string | null>(null);
|
||||
const [isReadonly, setIsReadonly] = useState(false);
|
||||
const [moveViewportToStep, setMoveViewportToStep] = useState<string | null>(null);
|
||||
const definitionJson = JSON.stringify(definition.value, null, 2);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
console.log(`definition updated, isValid=${definition.isValid}`);
|
||||
}, [definition]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (moveViewportToStep)
|
||||
{
|
||||
if (controller.isReady())
|
||||
{
|
||||
controller.moveViewportToStep(moveViewportToStep);
|
||||
}
|
||||
setMoveViewportToStep(null);
|
||||
}
|
||||
}, [controller, moveViewportToStep]);
|
||||
|
||||
function toggleVisibilityClicked()
|
||||
{
|
||||
setIsVisible(!isVisible);
|
||||
}
|
||||
|
||||
function toggleSelectionClicked()
|
||||
{
|
||||
const id = definition.value.sequence[0].id;
|
||||
setSelectedStepId(selectedStepId ? null : id);
|
||||
}
|
||||
|
||||
function toggleIsReadonlyClicked()
|
||||
{
|
||||
setIsReadonly(!isReadonly);
|
||||
}
|
||||
|
||||
function toggleToolboxClicked()
|
||||
{
|
||||
setIsToolboxCollapsed(!isToolboxCollapsed);
|
||||
}
|
||||
|
||||
function toggleEditorClicked()
|
||||
{
|
||||
setIsEditorCollapsed(!isEditorCollapsed);
|
||||
}
|
||||
|
||||
function moveViewportToFirstStepClicked()
|
||||
{
|
||||
const fistStep = definition.value.sequence[0];
|
||||
if (fistStep)
|
||||
{
|
||||
setMoveViewportToStep(fistStep.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function appendStepClicked()
|
||||
{
|
||||
const newStep = createTaskStep();
|
||||
|
||||
const newDefinition = ObjectCloner.deepClone(definition.value);
|
||||
newDefinition.sequence.push(newStep);
|
||||
// We need to wait for the controller to finish the operation before we can select the new step
|
||||
await controller.replaceDefinition(newDefinition);
|
||||
|
||||
setSelectedStepId(newStep.id);
|
||||
setMoveViewportToStep(newStep.id);
|
||||
}
|
||||
|
||||
function reloadDefinitionClicked()
|
||||
{
|
||||
const newDefinition = ObjectCloner.deepClone(startDefinition);
|
||||
setSelectedStepId(null);
|
||||
setDefinition(wrapDefinition(newDefinition));
|
||||
}
|
||||
|
||||
function yesOrNo(value: boolean)
|
||||
{
|
||||
return value ? "✅ Yes" : "⛔ No";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isVisible && (
|
||||
<SequentialWorkflowDesigner
|
||||
undoStackSize={10}
|
||||
definition={definition}
|
||||
onDefinitionChange={setDefinition}
|
||||
selectedStepId={selectedStepId}
|
||||
isReadonly={isReadonly}
|
||||
onSelectedStepIdChanged={setSelectedStepId}
|
||||
toolboxConfiguration={toolboxConfiguration}
|
||||
isToolboxCollapsed={isToolboxCollapsed}
|
||||
onIsToolboxCollapsedChanged={setIsToolboxCollapsed}
|
||||
stepsConfiguration={stepsConfiguration}
|
||||
validatorConfiguration={validatorConfiguration}
|
||||
controlBar={true}
|
||||
rootEditor={<RootEditor />}
|
||||
stepEditor={<StepEditor />}
|
||||
isEditorCollapsed={isEditorCollapsed}
|
||||
onIsEditorCollapsedChanged={setIsEditorCollapsed}
|
||||
controller={controller}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ul>
|
||||
<li>Definition: {definitionJson.length} bytes</li>
|
||||
<li>Selected step: {selectedStepId}</li>
|
||||
<li>Is readonly: {yesOrNo(isReadonly)}</li>
|
||||
<li>Is valid: {definition.isValid === undefined ? "?" : yesOrNo(definition.isValid)}</li>
|
||||
<li>Is toolbox collapsed: {yesOrNo(isToolboxCollapsed)}</li>
|
||||
<li>Is editor collapsed: {yesOrNo(isEditorCollapsed)}</li>
|
||||
</ul>
|
||||
|
||||
<div>
|
||||
<button onClick={toggleVisibilityClicked}>Toggle visibility</button>
|
||||
<button onClick={reloadDefinitionClicked}>Reload definition</button>
|
||||
<button onClick={toggleSelectionClicked}>Toggle selection</button>
|
||||
<button onClick={toggleIsReadonlyClicked}>Toggle readonly</button>
|
||||
<button onClick={toggleToolboxClicked}>Toggle toolbox</button>
|
||||
<button onClick={toggleEditorClicked}>Toggle editor</button>
|
||||
<button onClick={moveViewportToFirstStepClicked}>Move viewport to first step</button>
|
||||
<button onClick={appendStepClicked}>Append step</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea value={definitionJson} readOnly={true} cols={100} rows={15} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkflowPreview;
|
75
src/qqq/components/workflows/model.ts
Normal file
75
src/qqq/components/workflows/model.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import {BranchedStep, Definition, Step} from "sequential-workflow-designer";
|
||||
|
||||
export interface WorkflowDefinition extends Definition
|
||||
{
|
||||
properties: {
|
||||
alfa?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TaskStep extends Step
|
||||
{
|
||||
componentType: "task";
|
||||
type: "task";
|
||||
properties: {
|
||||
x?: string;
|
||||
y?: string;
|
||||
warehouse?: string;
|
||||
wmsConnection?: string;
|
||||
wmsSystem?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type OptimizationStepType =
|
||||
"determineWarehouseRouting" |
|
||||
"determineLineHaulLane" |
|
||||
"validateLineItems" |
|
||||
"determineCoolingCategory" |
|
||||
"validateOptimizationRules" |
|
||||
"validateAddress" |
|
||||
"determineCarrierService" |
|
||||
"determineTNT" |
|
||||
"determineOrderServiceDates" |
|
||||
"orderMatchesFilterSelector";
|
||||
|
||||
|
||||
export interface WarehouseOptimizationStep extends Step
|
||||
{
|
||||
componentType: "task";
|
||||
type: OptimizationStepType;
|
||||
properties: {
|
||||
x?: string;
|
||||
y?: string;
|
||||
warehouse?: string;
|
||||
wmsConnection?: string;
|
||||
wmsSystem?: string;
|
||||
isValid?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SwitchStep extends BranchedStep
|
||||
{
|
||||
componentType: "switch";
|
||||
type: "switch";
|
||||
properties: {
|
||||
x?: string;
|
||||
y?: string;
|
||||
warehouse?: string;
|
||||
wmsConnection?: string;
|
||||
wmsSystem?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ContainerStep extends Step
|
||||
{
|
||||
componentType: "container";
|
||||
type: "container";
|
||||
properties: {
|
||||
x?: string;
|
||||
y?: string;
|
||||
warehouse?: string;
|
||||
wmsConnection?: string;
|
||||
wmsSystem?: string;
|
||||
};
|
||||
sequence: (WarehouseOptimizationStep | SwitchStep)[];
|
||||
}
|
@ -33,6 +33,7 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo
|
||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
||||
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
|
||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
||||
@ -96,6 +97,8 @@ let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: b
|
||||
{
|
||||
};
|
||||
|
||||
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
|
||||
|
||||
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
||||
{
|
||||
const processNameParam = useParams().processName;
|
||||
@ -443,8 +446,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{
|
||||
if (processValues[key])
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we have a cached possible-value label for this field name (key), then set it as the PV's initialDisplayValue //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (cachedPossibleValueLabels[key] && cachedPossibleValueLabels[key][processValues[key]])
|
||||
{
|
||||
formFields[key].possibleValueProps.initialDisplayValue = cachedPossibleValueLabels[key][processValues[key]];
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// else (and i don't think this should happen?) at least set something... //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
formFields[key].possibleValueProps.initialDisplayValue = processValues[key];
|
||||
}
|
||||
}
|
||||
|
||||
formFields[key].possibleValueProps.otherValues = formFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
||||
Object.keys(formFields).forEach((otherKey) =>
|
||||
@ -865,6 +881,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{
|
||||
dynamicFormFields[fieldName] = dynamicFormValue;
|
||||
initialValues[fieldName] = initialValue;
|
||||
|
||||
if (formikSetFieldValueFunction)
|
||||
{
|
||||
formikSetFieldValueFunction(fieldName, initialValue);
|
||||
}
|
||||
|
||||
formValidations[fieldName] = validation;
|
||||
};
|
||||
|
||||
@ -914,6 +936,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
fullFieldList.forEach((field) =>
|
||||
{
|
||||
initialValues[field.name] = processValues[field.name];
|
||||
|
||||
if (formikSetFieldValueFunction)
|
||||
{
|
||||
formikSetFieldValueFunction(field.name, processValues[field.name]);
|
||||
}
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1072,11 +1099,67 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
setRetryMillis(INITIAL_RETRY_MILLIS);
|
||||
|
||||
if (lastProcessResponse instanceof QJobComplete)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// run an async function here, in case we need to await looking up any possible-value labels //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
(async () =>
|
||||
{
|
||||
const qJobComplete = lastProcessResponse as QJobComplete;
|
||||
const newValues = qJobComplete.values;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let frontendSteps = steps;
|
||||
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
|
||||
if (updatedFrontendStepList)
|
||||
{
|
||||
setSteps(updatedFrontendStepList);
|
||||
frontendSteps = updatedFrontendStepList;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if the next screen has any PVS fields - look up their labels (display values) //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
const nextStepName = qJobComplete.nextStep;
|
||||
let nextStep: QFrontendStepMetaData | null = null;
|
||||
if (frontendSteps && nextStepName)
|
||||
{
|
||||
for (let i = 0; i < frontendSteps.length; i++)
|
||||
{
|
||||
if (frontendSteps[i].name === nextStepName)
|
||||
{
|
||||
nextStep = frontendSteps[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextStep && nextStep.formFields)
|
||||
{
|
||||
for (let i = 0; i < nextStep.formFields.length; i++)
|
||||
{
|
||||
const field = nextStep.formFields[i];
|
||||
const fieldName = field.name;
|
||||
if (field.possibleValueSourceName && newValues && newValues[fieldName])
|
||||
{
|
||||
const results: QPossibleValue[] = await Client.getInstance().possibleValues(null, processName, fieldName, null, [newValues[fieldName]]);
|
||||
if (results && results.length > 0)
|
||||
{
|
||||
if (!cachedPossibleValueLabels[fieldName])
|
||||
{
|
||||
cachedPossibleValueLabels[fieldName] = {};
|
||||
}
|
||||
cachedPossibleValueLabels[fieldName][newValues[fieldName]] = results[0].label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setJobUUID(null);
|
||||
setNewStep(qJobComplete.nextStep);
|
||||
setProcessValues(qJobComplete.values);
|
||||
setNewStep(nextStepName);
|
||||
setProcessValues(newValues);
|
||||
setQJobRunning(null);
|
||||
|
||||
if (formikSetFieldValueFunction)
|
||||
@ -1094,19 +1177,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
|
||||
if (updatedFrontendStepList)
|
||||
{
|
||||
setSteps(updatedFrontendStepList);
|
||||
}
|
||||
|
||||
if (activeStep && activeStep.recordListFields)
|
||||
{
|
||||
setNeedRecords(true);
|
||||
}
|
||||
})();
|
||||
}
|
||||
else if (lastProcessResponse instanceof QJobStarted)
|
||||
{
|
||||
@ -1347,8 +1422,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
const formData = new FormData();
|
||||
Object.keys(values).forEach((key) =>
|
||||
{
|
||||
if (values[key] !== undefined)
|
||||
{
|
||||
formData.append(key, values[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
|
||||
@ -1571,7 +1649,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
);
|
||||
|
||||
const body = (
|
||||
<Box py={3} mb={20}>
|
||||
<Box py={3} mb={20} className="processRun">
|
||||
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
|
||||
<Grid item xs={12} lg={10} xl={8}>
|
||||
{form}
|
||||
|
@ -94,6 +94,7 @@ interface Props
|
||||
isModal?: boolean;
|
||||
initialQueryFilter?: QQueryFilter;
|
||||
initialColumns?: QQueryColumns;
|
||||
allowVariables?: boolean;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
@ -125,7 +126,7 @@ const getLoadingScreen = (isModal: boolean) =>
|
||||
**
|
||||
** Yuge component. The best. Lots of very smart people are saying so.
|
||||
*******************************************************************************/
|
||||
const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, initialColumns}: Props, ref) =>
|
||||
const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
||||
{
|
||||
const tableName = table.name;
|
||||
const [searchParams] = useSearchParams();
|
||||
@ -630,7 +631,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
const type = (e.target as any).type;
|
||||
const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search");
|
||||
|
||||
if (validType && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess)
|
||||
if (validType && !isModal && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess)
|
||||
{
|
||||
if (!e.metaKey && !e.ctrlKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
|
||||
{
|
||||
@ -668,7 +669,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
{
|
||||
document.removeEventListener("keydown", down);
|
||||
};
|
||||
}, [dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess]);
|
||||
}, [isModal, dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess]);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -2884,6 +2885,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
gridApiRef={gridApiRef}
|
||||
mode={mode}
|
||||
queryScreenUsage={usage}
|
||||
allowVariables={allowVariables}
|
||||
setMode={doSetMode}
|
||||
savedViewsComponent={savedViewsComponent}
|
||||
columnMenuComponent={buildColumnMenu()}
|
||||
@ -2912,6 +2914,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
||||
metaData: metaData,
|
||||
queryFilter: queryFilter,
|
||||
updateFilter: doSetQueryFilter,
|
||||
allowVariables: allowVariables
|
||||
}
|
||||
}}
|
||||
localeText={{
|
||||
|
@ -717,6 +717,7 @@ input[type="search"]::-webkit-search-results-decoration
|
||||
background-color: #0062FF !important;
|
||||
}
|
||||
|
||||
/* several styles below here for user-defined alert inside helpContent */
|
||||
.helpContentAlert
|
||||
{
|
||||
padding: 6px 16px;
|
||||
@ -779,3 +780,511 @@ input[type="search"]::-webkit-search-results-decoration
|
||||
{
|
||||
color: #F44335;
|
||||
}
|
||||
|
||||
/* the alert widget, was built with minimal (no?) margins, for embedding in
|
||||
a parent widget; but for using it on a process, give it some breathing room */
|
||||
.processRun .widget .MuiAlert-root
|
||||
{
|
||||
margin: 2rem 1rem;
|
||||
}
|
||||
|
||||
|
||||
.sqd-designer-react {
|
||||
width: 100vw;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.sqd-editor {
|
||||
padding: 10px;
|
||||
}
|
||||
input:read-only {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.sqd-editor {
|
||||
padding: 10px;
|
||||
}
|
||||
input:read-only {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
|
||||
/* internal */
|
||||
.sqd-theme-light .sqd-toolbox {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.sqd-theme-light .sqd-toolbox-header-title {
|
||||
color: #000;
|
||||
}
|
||||
.sqd-theme-light .sqd-toolbox-filter {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #c3c3c3;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.sqd-theme-light .sqd-toolbox-filter:focus {
|
||||
border-color: #939393;
|
||||
}
|
||||
.sqd-theme-light .sqd-toolbox-group-title {
|
||||
color: #000;
|
||||
background: #e5e5e5;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-toolbox-item {
|
||||
color: #000;
|
||||
border: 1px solid #c3c3c3;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.sqd-theme-light .sqd-toolbox-item:hover {
|
||||
border-color: #939393;
|
||||
background: #fff;
|
||||
}
|
||||
.sqd-theme-light .sqd-toolbox-item .sqd-toolbox-item-icon.sqd-no-icon {
|
||||
background: #c6c6c6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-control-bar {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.sqd-theme-light .sqd-control-bar-button {
|
||||
border: 1px solid #c3c3c3;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.sqd-theme-light .sqd-control-bar-button:hover {
|
||||
border-color: #939393;
|
||||
background: #fff;
|
||||
}
|
||||
.sqd-theme-light .sqd-control-bar-button .sqd-icon-path {
|
||||
fill: #000;
|
||||
}
|
||||
.sqd-theme-light .sqd-control-bar-button.sqd-delete .sqd-icon-path {
|
||||
fill: #e01a24;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-smart-editor {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.sqd-theme-light .sqd-smart-editor-toggle {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.sqd-theme-light.sqd-context-menu {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-context-menu-group {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-context-menu-item {
|
||||
color: #000;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-context-menu-item:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.sqd-theme-light.sqd-designer {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-line-grid-path {
|
||||
stroke: #e3e3e3;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-join {
|
||||
stroke-width: 2;
|
||||
stroke: #000;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-region {
|
||||
stroke: #cecece;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 3;
|
||||
}
|
||||
.sqd-theme-light .sqd-region.sqd-selected {
|
||||
stroke: #ed4800;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 0;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-placeholder .sqd-placeholder-rect {
|
||||
fill: #d8d8d8;
|
||||
stroke: #6a6a6a;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 3;
|
||||
}
|
||||
.sqd-theme-light .sqd-placeholder.sqd-hover .sqd-placeholder-rect {
|
||||
fill: #ed4800;
|
||||
}
|
||||
.sqd-theme-light .sqd-placeholder-icon-path {
|
||||
fill: #2b2b2b;
|
||||
}
|
||||
.sqd-theme-light .sqd-placeholder.sqd-hover .sqd-placeholder-icon-path {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-validation-error {
|
||||
fill: #ffa200;
|
||||
}
|
||||
.sqd-theme-light .sqd-validation-error-icon-path {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-root-start-stop-circle {
|
||||
fill: #2c18df;
|
||||
}
|
||||
.sqd-theme-light .sqd-root-start-stop-icon {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-step-task .sqd-step-task-rect {
|
||||
fill: #fff;
|
||||
stroke-width: 1;
|
||||
stroke: #c3c3c3;
|
||||
filter: drop-shadow(0 1.5px 1.5px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
.sqd-theme-light .sqd-step-task .sqd-step-task-rect.sqd-selected {
|
||||
stroke: #ed4800;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-task .sqd-step-task-text {
|
||||
fill: #000;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-task .sqd-step-task-empty-icon {
|
||||
fill: #c6c6c6;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-task .sqd-input {
|
||||
fill: #fff;
|
||||
stroke-width: 2;
|
||||
stroke: #000;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-task .sqd-output {
|
||||
fill: #000;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-step-switch > .sqd-label-primary > .sqd-label-text {
|
||||
fill: #fff;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-switch > .sqd-label-primary > .sqd-label-rect {
|
||||
fill: #2411db;
|
||||
stroke-width: 0;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-switch > .sqd-label-secondary > .sqd-label-rect {
|
||||
fill: #000;
|
||||
stroke-width: 0;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-switch > .sqd-label-secondary > .sqd-label-text {
|
||||
fill: #fff;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-switch > g > .sqd-input {
|
||||
fill: #fff;
|
||||
stroke-width: 2;
|
||||
stroke: #000;
|
||||
}
|
||||
|
||||
.sqd-theme-light .sqd-step-container > .sqd-label > .sqd-label-text {
|
||||
fill: #fff;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-container > .sqd-label > .sqd-label-rect {
|
||||
fill: #2411db;
|
||||
stroke-width: 0;
|
||||
}
|
||||
.sqd-theme-light .sqd-step-container > g > .sqd-input {
|
||||
fill: #fff;
|
||||
stroke-width: 2;
|
||||
stroke: #000;
|
||||
}
|
||||
|
||||
|
||||
/* .sqd-designer */
|
||||
.sqd-designer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sqd-designer,
|
||||
.sqd-drag,
|
||||
.sqd-context-menu {
|
||||
font-size: 13px;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.sqd-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sqd-disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
/* .sqd-toolbox */
|
||||
.sqd-toolbox,
|
||||
.sqd-toolbox-filter {
|
||||
font-size: 11px;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
.sqd-toolbox {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 20;
|
||||
box-sizing: border-box;
|
||||
width: 250px;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sqd-toolbox-header {
|
||||
position: relative;
|
||||
padding: 15px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sqd-toolbox-header-title {
|
||||
display: block;
|
||||
font-size: 1.2em;
|
||||
line-height: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sqd-toolbox-toggle-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -8px 0 0;
|
||||
}
|
||||
|
||||
.sqd-toolbox-header:hover .sqd-toolbox-toggle-icon {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sqd-scrollbox {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sqd-scrollbox-body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sqd-toolbox-filter {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
outline: none;
|
||||
width: 110px;
|
||||
margin: 0 10px 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sqd-toolbox-group-title {
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
margin: 0 10px 10px;
|
||||
}
|
||||
|
||||
.sqd-toolbox-item {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
margin: 0 10px 10px;
|
||||
cursor: move;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.sqd-toolbox-item-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 5px;
|
||||
margin-top: -10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.sqd-toolbox-item-icon-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sqd-toolbox-item-text {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 10px 10px 10px 30px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sqd-drag {
|
||||
position: absolute;
|
||||
z-index: 9999999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* .sqd-control-bar */
|
||||
.sqd-control-bar {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
z-index: 20;
|
||||
padding: 8px 0 8px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sqd-control-bar-button {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sqd-control-bar-button-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.sqd-control-bar-button.sqd-disabled .sqd-control-bar-button-icon {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/* .sqd-smart-editor */
|
||||
.sqd-smart-editor-toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 29;
|
||||
width: 36px;
|
||||
height: 64px;
|
||||
border-bottom-left-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sqd-smart-editor-toggle-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: -12px 0 0 -12px;
|
||||
}
|
||||
|
||||
.sqd-smart-editor-toggle:hover .sqd-smart-editor-toggle-icon {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sqd-smart-editor {
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.sqd-layout-desktop .sqd-smart-editor {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.sqd-layout-desktop .sqd-smart-editor-toggle {
|
||||
right: 300px;
|
||||
}
|
||||
|
||||
.sqd-layout-desktop .sqd-smart-editor-toggle.sqd-collapsed {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sqd-layout-mobile .sqd-smart-editor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 41px;
|
||||
}
|
||||
|
||||
.sqd-layout-mobile .sqd-smart-editor-toggle {
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.sqd-layout-mobile .sqd-smart-editor-toggle.sqd-collapsed {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* .sqd-context-menu */
|
||||
.sqd-context-menu {
|
||||
position: absolute;
|
||||
z-index: 2000000000;
|
||||
overflow: hidden;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.sqd-context-menu-group,
|
||||
.sqd-context-menu-item {
|
||||
width: 130px;
|
||||
padding: 8px 10px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sqd-context-menu-group {
|
||||
font-size: 11px;
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.sqd-context-menu-item {
|
||||
cursor: pointer;
|
||||
transition: background 70ms;
|
||||
}
|
||||
|
||||
/* .sqd-workspace */
|
||||
.sqd-workspace {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: block;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sqd-workspace-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.sqd-label-text {
|
||||
text-anchor: middle;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
||||
.sqd-placeholder .sqd-placeholder-rect {
|
||||
transition: fill 100ms;
|
||||
}
|
||||
|
||||
.sqd-step-task-text {
|
||||
text-anchor: left;
|
||||
dominant-baseline: central;
|
||||
}
|
||||
|
Reference in New Issue
Block a user