mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 15:18:44 +00:00
Compare commits
38 Commits
wip/CE-148
...
version-0.
Author | SHA1 | Date | |
---|---|---|---|
894a9c2afc | |||
fd5055e502 | |||
326367fbe0 | |||
bb6f818457 | |||
1cd6e07907 | |||
e839da6123 | |||
34a4fc19b4 | |||
2cc7e9ebe1 | |||
128a748b63 | |||
1284e3a22c | |||
ae358b9067 | |||
dc20c3d5ec | |||
71a9c6470a | |||
765d40aef1 | |||
d9f1642f0a | |||
858540427d | |||
eecb2d4489 | |||
5a6293cfdf | |||
868022408c | |||
d090a665ff | |||
f112cf5543 | |||
0c2dcb1215 | |||
418f7957a2 | |||
8be8bf367a | |||
1ca1313a25 | |||
4533815535 | |||
4230f34b15 | |||
e08e37222b | |||
0ffada6aec | |||
9f04d897a1 | |||
e604f47231 | |||
93f5bb688c | |||
3fa017e8b9 | |||
9d5af539b9 | |||
97bab57974 | |||
d9de96ea7f | |||
ff839d85fd | |||
45b6b42836 |
5746
package-lock.json
generated
5746
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
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.104",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.105",
|
||||
"@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.2.0",
|
||||
"@types/prop-types": "^15.7.5",
|
||||
"@types/react": "18.0.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,16 +46,12 @@
|
||||
"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"
|
||||
},
|
||||
|
6
pom.xml
6
pom.xml
@ -29,7 +29,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.21.0-SNAPSHOT</revision>
|
||||
<revision>0.22.0</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
@ -66,7 +66,7 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
<version>0.20.0-20240308.165846-65</version>
|
||||
<version>0.21.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
@ -154,11 +154,11 @@
|
||||
<versionTagPrefix>version-</versionTagPrefix>
|
||||
</gitFlowConfig>
|
||||
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
|
||||
<postReleaseGoals>install</postReleaseGoals> <!-- Let CI run deploys -->
|
||||
<commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart>
|
||||
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
|
||||
<versionProperty>revision</versionProperty>
|
||||
<skipUpdateVersion>true</skipUpdateVersion>
|
||||
<skipTestProject>true</skipTestProject> <!-- we allow CI to do the tests -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
|
@ -183,6 +183,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
bulkEditMode={bulkEditMode}
|
||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||
otherValues={otherValuesMap}
|
||||
useCase="form"
|
||||
/>
|
||||
{formattedHelpContent}
|
||||
</Grid>
|
||||
|
@ -53,6 +53,7 @@ interface Props
|
||||
otherValues?: Map<string, any>;
|
||||
variant: "standard" | "outlined";
|
||||
initiallyOpen: boolean;
|
||||
useCase: "form" | "filter";
|
||||
}
|
||||
|
||||
DynamicSelect.defaultProps = {
|
||||
@ -102,7 +103,7 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
|
||||
function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props)
|
||||
{
|
||||
const [open, setOpen] = useState(initiallyOpen);
|
||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||
@ -194,7 +195,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
(async () =>
|
||||
{
|
||||
// console.log(`doing a search with ${searchTerm}`);
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
|
||||
|
||||
if (tableMetaData == null && tableName)
|
||||
{
|
||||
@ -227,7 +228,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
setLoading(true);
|
||||
setOptions([]);
|
||||
console.log("Refreshing possible values...");
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
|
||||
setLoading(false);
|
||||
setOptions([...results]);
|
||||
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
||||
|
@ -25,6 +25,7 @@ import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||
@ -225,6 +226,19 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
|
||||
const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set the right-half of the navbar up so that below the 'md' breakpoint, it just disappears //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const navbarRowRight = (theme: Theme, {isMini}: any) =>
|
||||
{
|
||||
return {
|
||||
[theme.breakpoints.down("md")]: {
|
||||
display: "none",
|
||||
},
|
||||
...navbarRow(theme, isMini)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position={absolute ? "absolute" : navbarType}
|
||||
@ -241,7 +255,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
||||
</Box>
|
||||
{isMini ? null : (
|
||||
<Box sx={(theme) => navbarRow(theme, {isMini})}>
|
||||
<Box sx={(theme) => navbarRowRight(theme, {isMini})}>
|
||||
<Box mt={"-0.25rem"} pb={"0.75rem"} pr={2} mr={-2} sx={{"& *": {cursor: "pointer !important"}}}>
|
||||
{renderHistory()}
|
||||
</Box>
|
||||
|
@ -183,7 +183,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
||||
{
|
||||
// todo - sometimes i want contains instead of equals on strings (client.name, for example...)
|
||||
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
|
||||
if (field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE)
|
||||
if (field?.type == QFieldType.DATE_TIME)
|
||||
{
|
||||
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ export function EvaluatedExpression({field, expression}: EvaluatedExpressionProp
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return <>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</>;
|
||||
return <span style={{fontVariantNumeric: "tabular-nums"}}>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</span>;
|
||||
}
|
||||
|
||||
const HOUR_MS = 60 * 60 * 1000;
|
||||
|
@ -377,6 +377,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
inForm={false}
|
||||
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
||||
variant="standard"
|
||||
useCase="filter"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@ -412,6 +413,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
inForm={false}
|
||||
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
||||
variant="standard"
|
||||
useCase="filter"
|
||||
/>
|
||||
</Box>;
|
||||
}
|
||||
|
@ -440,10 +440,10 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
||||
<Grid container alignItems="flex-end">
|
||||
<Box maxWidth={"50%"} minWidth={300}>
|
||||
<DynamicSelect fieldName={"apiName"} initialValue={apiName} initialDisplayValue={apiNameLabel} fieldLabel={"API Name *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiName} />
|
||||
<DynamicSelect fieldName={"apiName"} initialValue={apiName} initialDisplayValue={apiNameLabel} fieldLabel={"API Name *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiName} useCase="form" />
|
||||
</Box>
|
||||
<Box maxWidth={"50%"} minWidth={300} pl={2}>
|
||||
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} />
|
||||
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} useCase="form" />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box display="flex" sx={{height: "100%"}}>
|
||||
|
@ -397,6 +397,7 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
|
||||
initialDisplayValue={selectedAudienceOption?.label}
|
||||
inForm={false}
|
||||
onChange={handleAudienceChange}
|
||||
useCase="form"
|
||||
/>
|
||||
</Box>
|
||||
{/*
|
||||
|
@ -22,16 +22,19 @@
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {Box, Skeleton} from "@mui/material";
|
||||
import parse from "html-react-parser";
|
||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
||||
import React from "react";
|
||||
|
||||
|
||||
interface CompositeData
|
||||
export interface CompositeData
|
||||
{
|
||||
blocks: BlockData[];
|
||||
styleOverrides?: any;
|
||||
layout?: string;
|
||||
overlayHtml?: string;
|
||||
overlayStyleOverrides?: any;
|
||||
}
|
||||
|
||||
|
||||
@ -97,13 +100,25 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
boxStyle.borderRadius = "0.5rem";
|
||||
boxStyle.background = "#FFFFFF";
|
||||
}
|
||||
|
||||
if (data?.styleOverrides)
|
||||
{
|
||||
boxStyle = {...boxStyle, ...data.styleOverrides};
|
||||
}
|
||||
|
||||
return (<Box sx={boxStyle} className="compositeWidget">
|
||||
let overlayStyle: any = {};
|
||||
|
||||
if (data?.overlayStyleOverrides)
|
||||
{
|
||||
overlayStyle = {...overlayStyle, ...data.overlayStyleOverrides};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
data?.overlayHtml &&
|
||||
<Box sx={overlayStyle} className="blockWidgetOverlay">{parse(data.overlayHtml)}</Box>
|
||||
}
|
||||
<Box sx={boxStyle} className="compositeWidget">
|
||||
{
|
||||
data.blocks.map((block: BlockData, index) => (
|
||||
<React.Fragment key={index}>
|
||||
@ -111,6 +126,8 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</Box>);
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
@ -47,7 +47,6 @@ 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";
|
||||
@ -582,14 +581,6 @@ 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 &&
|
||||
@ -647,8 +638,28 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
|
||||
if (!omitWrappingGridContainer)
|
||||
{
|
||||
// @ts-ignore
|
||||
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
const gridProps: {[key: string]: any} = {};
|
||||
|
||||
for(let size of ["xs", "sm", "md", "lg", "xl", "xxl"])
|
||||
{
|
||||
const key = `gridCols:sizeClass:${size}`
|
||||
if(widgetMetaData?.defaultValues?.has(key))
|
||||
{
|
||||
gridProps[size] = widgetMetaData?.defaultValues.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
if(!gridProps["xxl"])
|
||||
{
|
||||
gridProps["xxl"] = widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12;
|
||||
}
|
||||
|
||||
if(!gridProps["xs"])
|
||||
{
|
||||
gridProps["xs"] = 12;
|
||||
}
|
||||
|
||||
renderedWidget = (<Grid id={widgetMetaData.name} item {...gridProps} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
{renderedWidget}
|
||||
</Grid>);
|
||||
}
|
||||
|
@ -21,18 +21,19 @@
|
||||
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {Tooltip} from "@mui/material";
|
||||
import React, {ReactElement, useContext} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Box, Tooltip} from "@mui/material";
|
||||
import QContext from "QContext";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||
import React, {ReactElement, useContext} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
interface BlockElementWrapperProps
|
||||
{
|
||||
data: BlockData;
|
||||
metaData: QWidgetMetaData;
|
||||
slot: string
|
||||
slot: string;
|
||||
linkProps?: any;
|
||||
children: ReactElement;
|
||||
}
|
||||
@ -47,16 +48,16 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
||||
let link: BlockLink;
|
||||
let tooltip: BlockTooltip;
|
||||
|
||||
if(slot)
|
||||
if (slot)
|
||||
{
|
||||
link = data.linkMap && data.linkMap[slot.toUpperCase()];
|
||||
if(!link)
|
||||
if (!link)
|
||||
{
|
||||
link = data.link;
|
||||
}
|
||||
|
||||
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
|
||||
if(!tooltip)
|
||||
if (!tooltip)
|
||||
{
|
||||
tooltip = data.tooltip;
|
||||
}
|
||||
@ -67,9 +68,9 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
||||
tooltip = data.tooltip;
|
||||
}
|
||||
|
||||
if(!tooltip)
|
||||
if (!tooltip)
|
||||
{
|
||||
const helpRoles = ["ALL_SCREENS"]
|
||||
const helpRoles = ["ALL_SCREENS"];
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the full keys in the helpContent table will look like: //
|
||||
@ -80,26 +81,39 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
||||
const key = data.blockId ? `${data.blockId},${slot}` : slot;
|
||||
const showHelp = helpHelpActive || hasHelpContent(metaData?.helpContent?.get(key), helpRoles);
|
||||
|
||||
if(showHelp)
|
||||
if (showHelp)
|
||||
{
|
||||
const formattedHelpContent = <HelpContent helpContents={metaData?.helpContent?.get(key)} roles={helpRoles} helpContentKey={`widget:${metaData?.name};slot:${key}`} />;
|
||||
tooltip = {title: formattedHelpContent, placement: "bottom"}
|
||||
tooltip = {title: formattedHelpContent, placement: "bottom"};
|
||||
}
|
||||
}
|
||||
|
||||
let rs = children;
|
||||
|
||||
if(link)
|
||||
if (link && link.href)
|
||||
{
|
||||
rs = <Link to={link.href} target={link.target} style={{color: "#546E7A"}} {...linkProps}>{rs}</Link>
|
||||
rs = <Link to={link.href} target={link.target} style={{color: "#546E7A"}} {...linkProps}>{rs}</Link>;
|
||||
}
|
||||
|
||||
if(tooltip)
|
||||
if (tooltip)
|
||||
{
|
||||
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom"
|
||||
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom";
|
||||
|
||||
// @ts-ignore - placement possible values
|
||||
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip>
|
||||
if (tooltip.blockData)
|
||||
{
|
||||
// @ts-ignore - special case for composite type block...
|
||||
rs = <Tooltip title={
|
||||
<Box sx={{width: "200px"}}>
|
||||
<CompositeWidget widgetMetaData={metaData} data={tooltip?.blockData} />
|
||||
</Box>
|
||||
}>{rs}</Tooltip>;
|
||||
}
|
||||
else
|
||||
{
|
||||
// @ts-ignore - placement possible values
|
||||
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip>;
|
||||
}
|
||||
}
|
||||
|
||||
return (rs);
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {CompositeData} from "qqq/components/widgets/CompositeWidget";
|
||||
|
||||
|
||||
export interface BlockData
|
||||
@ -29,8 +30,8 @@ export interface BlockData
|
||||
|
||||
tooltip?: BlockTooltip;
|
||||
link?: BlockLink;
|
||||
tooltipMap?: {[slot: string]: BlockTooltip};
|
||||
linkMap?: {[slot: string]: BlockLink};
|
||||
tooltipMap?: { [slot: string]: BlockTooltip };
|
||||
linkMap?: { [slot: string]: BlockLink };
|
||||
|
||||
values: any;
|
||||
styles?: any;
|
||||
@ -39,6 +40,7 @@ export interface BlockData
|
||||
|
||||
export interface BlockTooltip
|
||||
{
|
||||
blockData?: CompositeData;
|
||||
title: string | JSX.Element;
|
||||
placement: string;
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ export default function UpOrDownNumberBlock({widgetMetaData, data}: StandardBloc
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline"}}>
|
||||
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline", marginLeft: "auto"}}>
|
||||
|
||||
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="number">
|
||||
|
@ -87,6 +87,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
{
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
||||
const [hidePreview, setHidePreview] = useState(widgetData?.hidePreview);
|
||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||
|
||||
const [alertContent, setAlertContent] = useState(null as string);
|
||||
@ -272,7 +273,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function mayShowQueryPreview(): boolean
|
||||
function mayShowQuery(): boolean
|
||||
{
|
||||
if (tableMetaData)
|
||||
{
|
||||
@ -288,7 +289,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function mayShowColumnsPreview(): boolean
|
||||
function mayShowColumns(): boolean
|
||||
{
|
||||
if (tableMetaData)
|
||||
{
|
||||
@ -356,14 +357,14 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
<Box pt="0.5rem">
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<h5>Query Filter</h5>
|
||||
<Box fontSize="0.75rem" fontWeight="700">{mayShowQueryPreview() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
||||
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
||||
</Box>
|
||||
{
|
||||
mayShowQueryPreview() &&
|
||||
mayShowQuery() &&
|
||||
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} />
|
||||
}
|
||||
{
|
||||
!mayShowQueryPreview() &&
|
||||
!mayShowQuery() &&
|
||||
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.5rem"} p={"0.5rem"} pb={"0.125rem"} borderRadius="0.75rem" border={`1px solid ${colors.grayLines.main}`}>
|
||||
{
|
||||
isEditable &&
|
||||
@ -382,11 +383,11 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
<h5>Columns</h5>
|
||||
<Box display="flex" flexWrap="wrap" fontSize="1rem">
|
||||
{
|
||||
mayShowColumnsPreview() &&
|
||||
mayShowColumns() && columns &&
|
||||
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
|
||||
}
|
||||
{
|
||||
!mayShowColumnsPreview() &&
|
||||
!mayShowColumns() &&
|
||||
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
|
||||
{
|
||||
isEditable &&
|
||||
@ -402,6 +403,21 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{!hidePreview && !isEditable && frontendQueryFilter && tableMetaData && (
|
||||
<Box pt="1rem">
|
||||
<h5>Preview</h5>
|
||||
<RecordQuery
|
||||
allowVariables={widgetData?.allowVariables}
|
||||
ref={recordQueryRef}
|
||||
table={tableMetaData}
|
||||
isPreview={true}
|
||||
usage="reportSetup"
|
||||
isModal={true}
|
||||
initialQueryFilter={frontendQueryFilter}
|
||||
initialColumns={columns}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{
|
||||
modalOpen &&
|
||||
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
|
||||
|
@ -1,383 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {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>
|
||||
);
|
||||
}
|
@ -19,15 +19,12 @@
|
||||
*/
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {tooltipClasses, TooltipProps} from "@mui/material";
|
||||
import {Box, tooltipClasses, TooltipProps} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {styled} from "@mui/material/styles";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import parse from "html-react-parser";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
@ -166,7 +163,7 @@ function DataTable({
|
||||
})}
|
||||
>
|
||||
{/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */}
|
||||
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_right"}</Icon>
|
||||
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_left"}</Icon>
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
@ -312,7 +309,7 @@ function DataTable({
|
||||
{
|
||||
boxStyle = isFooter
|
||||
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
|
||||
: {flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
|
||||
: {height: fixedHeight ? `${fixedHeight}px` : "auto", flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
|
||||
}
|
||||
|
||||
let innerBoxStyle = {};
|
||||
@ -321,16 +318,18 @@ function DataTable({
|
||||
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// note - at one point, we had the table's sx including: whiteSpace: "nowrap"... //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
return <Box sx={boxStyle}><Box sx={innerBoxStyle}>
|
||||
<Table {...getTableProps()}>
|
||||
<Table {...getTableProps()} component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: gridTemplateColumns}}>
|
||||
{
|
||||
includeHead && (
|
||||
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}>
|
||||
{headerGroups.map((headerGroup: any, i: number) => (
|
||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", alignItems: "flex-end", gridTemplateColumns: gridTemplateColumns}}>
|
||||
{headerGroup.headers.map((column: any) => (
|
||||
headerGroups.map((headerGroup: any, i: number) => (
|
||||
headerGroup.headers.map((column: any) => (
|
||||
column.type !== "hidden" && (
|
||||
<DataTableHeadCell
|
||||
sx={{position: "sticky", top: 0, background: "white", zIndex: 10, alignItems: "flex-end"}}
|
||||
key={i++}
|
||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||
align={column.align ? column.align : "left"}
|
||||
@ -340,13 +339,10 @@ function DataTable({
|
||||
{column.render("header")}
|
||||
</DataTableHeadCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</Box>
|
||||
))
|
||||
))
|
||||
)
|
||||
}
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map((row: any, key: any) =>
|
||||
{
|
||||
prepareRow(row);
|
||||
@ -384,11 +380,11 @@ function DataTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: background}} key={key} {...row.getRowProps()}>
|
||||
{row.cells.map((cell: any) => (
|
||||
row.cells.map((cell: any) => (
|
||||
cell.column.type !== "hidden" && (
|
||||
<DataTableBodyCell
|
||||
key={key}
|
||||
sx={{verticalAlign: "top", background: background}}
|
||||
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
||||
depth={row.depth}
|
||||
align={cell.column.align ? cell.column.align : "left"}
|
||||
@ -446,18 +442,15 @@ function DataTable({
|
||||
}
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
);
|
||||
})}
|
||||
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer sx={{boxShadow: "none", height: fixedHeight ? `${fixedHeight}px` : "auto"}}>
|
||||
<TableContainer sx={{boxShadow: "none", height: (fixedHeight && !fixedStickyLastRow) ? `${fixedHeight}px` : "auto"}}>
|
||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
||||
|
@ -93,41 +93,25 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
|
||||
/>
|
||||
: noRowsFoundHTML ?
|
||||
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
|
||||
<MDTypography
|
||||
variant="subtitle2"
|
||||
color="secondary"
|
||||
fontWeight="regular"
|
||||
>
|
||||
{
|
||||
noRowsFoundHTML ? (
|
||||
parse(noRowsFoundHTML)
|
||||
) : "No rows found"
|
||||
}
|
||||
<MDTypography variant="subtitle2" color="secondary" fontWeight="regular">
|
||||
{noRowsFoundHTML ? (parse(noRowsFoundHTML)) : "No rows found"}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
:
|
||||
<TableContainer sx={{boxShadow: "none"}}>
|
||||
<Table>
|
||||
<Box component="thead">
|
||||
<TableRow sx={{alignItems: "flex-end"}} key="header">
|
||||
<Table component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr"}}>
|
||||
{Array(8).fill(0).map((_, i) =>
|
||||
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
|
||||
<Skeleton width="100%" />
|
||||
</DataTableHeadCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</Box>
|
||||
<TableBody>
|
||||
{Array(5).fill(0).map((_, i) =>
|
||||
<TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}>
|
||||
{Array(8).fill(0).map((_, j) =>
|
||||
Array(8).fill(0).map((_, j) =>
|
||||
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
||||
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
)}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import {Box} from "@mui/material";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {ReactNode} from "react";
|
||||
@ -30,13 +30,14 @@ interface Props
|
||||
children: ReactNode;
|
||||
noBorder?: boolean;
|
||||
align?: "left" | "right" | "center";
|
||||
sx?: any;
|
||||
}
|
||||
|
||||
function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
||||
function DataTableBodyCell({noBorder, align, sx, children}: Props): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box
|
||||
component="td"
|
||||
component="div"
|
||||
textAlign={align}
|
||||
py={1.5}
|
||||
px={1.5}
|
||||
@ -54,7 +55,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
||||
},
|
||||
"&:last-child": {
|
||||
paddingRight: "1rem"
|
||||
}
|
||||
}, ...sx
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
@ -72,6 +73,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
||||
DataTableBodyCell.defaultProps = {
|
||||
noBorder: false,
|
||||
align: "left",
|
||||
sx: {}
|
||||
};
|
||||
|
||||
export default DataTableBodyCell;
|
||||
|
@ -44,18 +44,14 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="th"
|
||||
component="div"
|
||||
width={width}
|
||||
py={1.5}
|
||||
px={1.5}
|
||||
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
||||
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||
"&:nth-of-type(1)": {
|
||||
paddingLeft: "1rem"
|
||||
},
|
||||
"&:last-child": {
|
||||
paddingRight: "1rem"
|
||||
},
|
||||
position: "sticky", top: 0, background: "white",
|
||||
zIndex: 1 // so if body rows scroll behind it, they don't show through
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
|
@ -1,21 +0,0 @@
|
||||
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.
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
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
|
||||
};
|
||||
}
|
@ -1,187 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {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;
|
@ -1,199 +0,0 @@
|
||||
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;
|
@ -1,75 +0,0 @@
|
||||
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)[];
|
||||
}
|
@ -121,7 +121,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
||||
}
|
||||
|
||||
const valueCounts = [] as QRecord[];
|
||||
for(let i = 0; i < result.values.valueCounts.length; i++)
|
||||
for(let i = 0; i < result.values.valueCounts?.length; i++)
|
||||
{
|
||||
let valueRecord = new QRecord(result.values.valueCounts[i]);
|
||||
|
||||
|
@ -786,6 +786,7 @@ function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData
|
||||
initialDisplayValue={selectedPossibleValue?.label}
|
||||
inForm={false}
|
||||
onChange={handleChange}
|
||||
useCase="filter"
|
||||
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
|
||||
/>
|
||||
</Box>
|
||||
@ -854,6 +855,7 @@ function InputPossibleValueSourceMultiple(tableName: string, field: QFieldMetaDa
|
||||
initialValues={selectedPossibleValues}
|
||||
inForm={false}
|
||||
onChange={handleChange}
|
||||
useCase="filter"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@ -33,8 +33,7 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
|
||||
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 {Alert, Collapse, Menu, Typography} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import {Alert, Box, Collapse, Menu, Typography} from "@mui/material";
|
||||
import Button from "@mui/material/Button";
|
||||
import Card from "@mui/material/Card";
|
||||
import Divider from "@mui/material/Divider";
|
||||
@ -92,6 +91,7 @@ interface Props
|
||||
launchProcess?: QProcessMetaData;
|
||||
usage?: QueryScreenUsage;
|
||||
isModal?: boolean;
|
||||
isPreview?: boolean;
|
||||
initialQueryFilter?: QQueryFilter;
|
||||
initialColumns?: QQueryColumns;
|
||||
allowVariables?: boolean;
|
||||
@ -126,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, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
||||
const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
||||
{
|
||||
const tableName = table.name;
|
||||
const [searchParams] = useSearchParams();
|
||||
@ -884,6 +884,18 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Opens a new query screen in a new window with the current filter
|
||||
*******************************************************************************/
|
||||
const openFilterInNewWindow = () =>
|
||||
{
|
||||
let filterForBackend = JSON.parse(JSON.stringify(view.queryFilter));
|
||||
filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, filterForBackend);
|
||||
const url = `${metaData?.getTablePathByName(tableName)}?filter=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
|
||||
window.open(url);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** This is the method that actually executes a query to update the data in the table.
|
||||
*******************************************************************************/
|
||||
@ -2232,12 +2244,25 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
||||
return (
|
||||
<GridToolbarContainer>
|
||||
<div>
|
||||
<Tooltip title="Refresh Query">
|
||||
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{
|
||||
!isPreview && (
|
||||
<div style={{position: "relative"}}>
|
||||
{/* @ts-ignore */}
|
||||
<GridToolbarDensitySelector nonce={undefined} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isPreview && (
|
||||
<Tooltip title="Open In New Window">
|
||||
<Button id="open-filter-in-new-window-button" onClick={() => openFilterInNewWindow()} startIcon={<Icon>launch</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
usage == "queryScreen" &&
|
||||
@ -2872,7 +2897,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
||||
}
|
||||
|
||||
{
|
||||
metaData && tableMetaData &&
|
||||
!isPreview && metaData && tableMetaData &&
|
||||
<BasicAndAdvancedQueryControls
|
||||
ref={basicAndAdvancedQueryControlsRef}
|
||||
metaData={metaData}
|
||||
|
@ -788,503 +788,19 @@ input[type="search"]::-webkit-search-results-decoration
|
||||
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 {
|
||||
/* default styles for a block widget overlay */
|
||||
.blockWidgetOverlay
|
||||
{
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
top: 15px;
|
||||
height: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
.blockWidgetOverlay a
|
||||
{
|
||||
color: #0062FF !important;
|
||||
}
|
||||
|
@ -268,7 +268,15 @@ class ValueUtils
|
||||
{
|
||||
if (!(date instanceof Date))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// so, a new Date here will interpret the string as being at midnight UTC, but //
|
||||
// the data object will be in the user/browser timezone. //
|
||||
// so "2024-08-22", for a user in US/Central, will be "2024-08-21T19:00:00-0500". //
|
||||
// correct for that by adding the date's timezone offset (converted from minutes //
|
||||
// to millis) back to it //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
date = new Date(date);
|
||||
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000)
|
||||
}
|
||||
// @ts-ignore
|
||||
return (`${date.toString("yyyy-MM-dd")}`);
|
||||
|
@ -56,8 +56,8 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
|
||||
"label": "Sample Table Widget",
|
||||
"footerHTML": "<span class='material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit' aria-hidden='true'><span class='dashboard-schedule-icon'>schedule</span></span>Updated at 2023-10-17 09:11:38 AM CDT",
|
||||
"columns": [
|
||||
{ "type": "html", "header": "Id", "accessor": "id", "width": "1%" },
|
||||
{ "type": "html", "header": "Name", "accessor": "name", "width": "99%" }
|
||||
{ "type": "html", "header": "Id", "accessor": "id", "width": "30px" },
|
||||
{ "type": "html", "header": "Name", "accessor": "name", "width": "1fr" }
|
||||
],
|
||||
"rows": [
|
||||
{ "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" },
|
||||
@ -83,7 +83,7 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
|
||||
// assert that the table widget rendered its header and some contents //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget");
|
||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget table a", "Homer S.");
|
||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget a", "Homer S.");
|
||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT");
|
||||
|
||||
/////////////////////////////
|
||||
|
Reference in New Issue
Block a user