mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-23 23:58:43 +00:00
Compare commits
69 Commits
snapshot-f
...
version-0.
Author | SHA1 | Date | |
---|---|---|---|
dc20c3d5ec | |||
71a9c6470a | |||
765d40aef1 | |||
858540427d | |||
eecb2d4489 | |||
868022408c | |||
d090a665ff | |||
f112cf5543 | |||
8be8bf367a | |||
1ca1313a25 | |||
4533815535 | |||
4230f34b15 | |||
e08e37222b | |||
0ffada6aec | |||
9f04d897a1 | |||
e604f47231 | |||
93f5bb688c | |||
3fa017e8b9 | |||
9d5af539b9 | |||
97bab57974 | |||
d9de96ea7f | |||
ff839d85fd | |||
d31215f6c0 | |||
262855b9c0 | |||
4d082c3c57 | |||
45b6b42836 | |||
47fb7cc2e3 | |||
647c63f5a3 | |||
f545649882 | |||
4d4610801f | |||
3ec43fbbd3 | |||
28bc07cce4 | |||
c7d31fa39e | |||
69f1cfe92f | |||
2ed95ff77a | |||
66336a28ed | |||
826bed4537 | |||
40bd83cd96 | |||
ca460e65e1 | |||
122fef152c | |||
d0ed0ce949 | |||
b8aa36455d | |||
a778b7497a | |||
c3503a719f | |||
2afa82c770 | |||
d03e908a9d | |||
dc62f97219 | |||
fe9e20715a | |||
71a1bfaa6b | |||
d9e9a0be08 | |||
aefb282a0e | |||
fb57718c1c | |||
ba213b038b | |||
69daf47021 | |||
1d24b9b40c | |||
f44ba8d6d3 | |||
7b562aea50 | |||
3bf1cea9dd | |||
dc131d5189 | |||
2b5cc1610f | |||
a36bdb1474 | |||
c2926d26e8 | |||
eb42a86655 | |||
b7f715f832 | |||
16a08cfd42 | |||
f5919c66ab | |||
d750ef0930 | |||
267ead925b | |||
f925ad9116 |
@ -115,7 +115,7 @@ workflows:
|
|||||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
ignore: /(main|integration.*)/
|
ignore: /(main|dev|integration.*)/
|
||||||
tags:
|
tags:
|
||||||
ignore: /(version|snapshot)-.*/
|
ignore: /(version|snapshot)-.*/
|
||||||
deploy:
|
deploy:
|
||||||
@ -124,7 +124,7 @@ workflows:
|
|||||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: /(main|integration.*)/
|
only: /(main|dev|integration.*)/
|
||||||
tags:
|
tags:
|
||||||
only: /(version|snapshot)-.*/
|
only: /(version|snapshot)-.*/
|
||||||
|
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import {defineConfig} from "cypress";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
e2e: {
|
|
||||||
viewportHeight: 1000,
|
|
||||||
viewportWidth: 1200,
|
|
||||||
setupNodeEvents(on, config)
|
|
||||||
{
|
|
||||||
// implement node event listeners here
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
20141
package-lock.json
generated
20141
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@auth0/auth0-react": "1.10.2",
|
||||||
"@emotion/react": "11.7.1",
|
"@emotion/react": "11.7.1",
|
||||||
"@emotion/styled": "11.6.0",
|
"@emotion/styled": "11.6.0",
|
||||||
"@kingsrook/qqq-frontend-core": "1.0.100",
|
"@kingsrook/qqq-frontend-core": "1.0.104",
|
||||||
"@mui/icons-material": "5.4.1",
|
"@mui/icons-material": "5.4.1",
|
||||||
"@mui/material": "5.11.1",
|
"@mui/material": "5.11.1",
|
||||||
"@mui/styles": "5.11.1",
|
"@mui/styles": "5.11.1",
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"clean": "rm -rf node_modules package-lock.json lib",
|
"clean": "rm -rf node_modules package-lock.json lib",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps",
|
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
|
||||||
"npm-install": "npm install --legacy-peer-deps",
|
"npm-install": "npm install --legacy-peer-deps",
|
||||||
"prepublishOnly": "tsc -p ./ --outDir lib/",
|
"prepublishOnly": "tsc -p ./ --outDir lib/",
|
||||||
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
|
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start",
|
||||||
|
6
pom.xml
6
pom.xml
@ -29,7 +29,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.20.0-SNAPSHOT</revision>
|
<revision>0.21.0</revision>
|
||||||
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
@ -66,7 +66,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
<groupId>com.kingsrook.qqq</groupId>
|
||||||
<artifactId>qqq-backend-core</artifactId>
|
<artifactId>qqq-backend-core</artifactId>
|
||||||
<version>0.20.0-20240308.165846-65</version>
|
<version>0.21.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
@ -154,11 +154,11 @@
|
|||||||
<versionTagPrefix>version-</versionTagPrefix>
|
<versionTagPrefix>version-</versionTagPrefix>
|
||||||
</gitFlowConfig>
|
</gitFlowConfig>
|
||||||
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
|
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
|
||||||
<postReleaseGoals>install</postReleaseGoals> <!-- Let CI run deploys -->
|
|
||||||
<commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart>
|
<commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart>
|
||||||
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
|
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
|
||||||
<versionProperty>revision</versionProperty>
|
<versionProperty>revision</versionProperty>
|
||||||
<skipUpdateVersion>true</skipUpdateVersion>
|
<skipUpdateVersion>true</skipUpdateVersion>
|
||||||
|
<skipTestProject>true</skipTestProject> <!-- we allow CI to do the tests -->
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ import Icon from "@mui/material/Icon";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import {makeStyles} from "@mui/styles";
|
import {makeStyles} from "@mui/styles";
|
||||||
import {Command} from "cmdk";
|
import {Command} from "cmdk";
|
||||||
import React, {useContext, useEffect, useRef} from "react";
|
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils";
|
import HistoryUtils, {QHistoryEntry} from "qqq/utils/HistoryUtils";
|
||||||
@ -62,16 +62,21 @@ const useStyles = makeStyles((theme: any) => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const A_FIRST = -1;
|
||||||
|
const B_FIRST = 1;
|
||||||
|
|
||||||
const CommandMenu = ({metaData}: Props) =>
|
const CommandMenu = ({metaData}: Props) =>
|
||||||
{
|
{
|
||||||
|
const [searchString, setSearchString] = useState("");
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const pathParts = location.pathname.replace(/\/+$/, "").split("/");
|
const pathParts = location.pathname.replace(/\/+$/, "").split("/");
|
||||||
|
|
||||||
const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, keyboardHelpOpen, setKeyboardHelpOpen, setTableMetaData, tableProcesses} = useContext(QContext);
|
const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, keyboardHelpOpen, setKeyboardHelpOpen, setTableMetaData, tableProcesses, recordAnalytics} = useContext(QContext);
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
function evalueKeyPress(e: KeyboardEvent)
|
function evaluateKeyPress(e: KeyboardEvent)
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// if a dot pressed, not from a "text" element, then toggle command menu //
|
// if a dot pressed, not from a "text" element, then toggle command menu //
|
||||||
@ -82,6 +87,7 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
if (e.key === "." && !keyboardHelpOpen)
|
if (e.key === "." && !keyboardHelpOpen)
|
||||||
{
|
{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
recordAnalytics({category: "globalEvents", action: "dotMenuKeyboardShortcut"});
|
||||||
setDotMenuOpen(true);
|
setDotMenuOpen(true);
|
||||||
}
|
}
|
||||||
else if (e.key === "?" && !dotMenuOpen)
|
else if (e.key === "?" && !dotMenuOpen)
|
||||||
@ -107,20 +113,20 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
|
|
||||||
const down = (e: KeyboardEvent) =>
|
const down = (e: KeyboardEvent) =>
|
||||||
{
|
{
|
||||||
evalueKeyPress(e);
|
evaluateKeyPress(e);
|
||||||
}
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", down)
|
document.addEventListener("keydown", down);
|
||||||
return () =>
|
return () =>
|
||||||
{
|
{
|
||||||
document.removeEventListener("keydown", down)
|
document.removeEventListener("keydown", down);
|
||||||
}
|
};
|
||||||
}, [tableMetaData, dotMenuOpen, keyboardHelpOpen])
|
}, [tableMetaData, dotMenuOpen, keyboardHelpOpen]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setDotMenuOpen(false);
|
setDotMenuOpen(false);
|
||||||
}, [location.pathname])
|
}, [location.pathname]);
|
||||||
|
|
||||||
function goToItem(path: string)
|
function goToItem(path: string)
|
||||||
{
|
{
|
||||||
@ -162,73 +168,117 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** sort a section (e.g, tables, apps).
|
||||||
|
**
|
||||||
|
** put labels that start-with the search word first.
|
||||||
|
*******************************************************************************/
|
||||||
|
function comparator(labelA: string, labelB: string)
|
||||||
|
{
|
||||||
|
if (searchString != "")
|
||||||
|
{
|
||||||
|
let aStartsWith = labelA.toLowerCase().startsWith(searchString.toLowerCase());
|
||||||
|
let bStartsWith = labelB.toLowerCase().startsWith(searchString.toLowerCase());
|
||||||
|
|
||||||
|
if (aStartsWith && !bStartsWith)
|
||||||
|
{
|
||||||
|
return A_FIRST;
|
||||||
|
}
|
||||||
|
else if (bStartsWith && !aStartsWith)
|
||||||
|
{
|
||||||
|
return B_FIRST;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexOfSpace = searchString.indexOf(" ");
|
||||||
|
if (indexOfSpace > 0)
|
||||||
|
{
|
||||||
|
aStartsWith = labelA.toLowerCase().startsWith(searchString.substring(0, indexOfSpace).toLowerCase());
|
||||||
|
bStartsWith = labelB.toLowerCase().startsWith(searchString.substring(0, indexOfSpace).toLowerCase());
|
||||||
|
|
||||||
|
if (aStartsWith && !bStartsWith)
|
||||||
|
{
|
||||||
|
return A_FIRST;
|
||||||
|
}
|
||||||
|
else if (bStartsWith && !aStartsWith)
|
||||||
|
{
|
||||||
|
return B_FIRST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (labelA.localeCompare(labelB));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function ActionsSection()
|
function ActionsSection()
|
||||||
{
|
{
|
||||||
let tableNames : string[]= [];
|
let tableNames: string[] = [];
|
||||||
metaData.tables.forEach((value: QTableMetaData, key: string) =>
|
metaData.tables.forEach((value: QTableMetaData, key: string) =>
|
||||||
{
|
{
|
||||||
tableNames.push(value.name);
|
tableNames.push(value.name);
|
||||||
})
|
});
|
||||||
tableNames = tableNames.sort((a: string, b:string) =>
|
tableNames = tableNames.sort((a: string, b: string) =>
|
||||||
{
|
{
|
||||||
const labelA = metaData.tables.get(a).label ?? "";
|
const labelA = metaData.tables.get(a).label ?? "";
|
||||||
const labelB = metaData.tables.get(b).label ?? "";
|
const labelB = metaData.tables.get(b).label ?? "";
|
||||||
return (labelA.localeCompare(labelB));
|
return comparator(labelA, labelB);
|
||||||
})
|
});
|
||||||
|
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
return tableMetaData && !path.endsWith("/edit") && !path.endsWith("/create") && !path.endsWith("#audit") && ! path.endsWith("copy") &&
|
return tableMetaData && !path.endsWith("/edit") && !path.endsWith("/create") && !path.endsWith("#audit") && !path.endsWith("copy") &&
|
||||||
(
|
(
|
||||||
<Command.Group heading={`${tableMetaData.label} Actions`}>
|
<Command.Group heading={`${tableMetaData.label} Actions`}>
|
||||||
{
|
{
|
||||||
tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission &&
|
tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission &&
|
||||||
<Command.Item onSelect={() => goToItem(`${pathParts.slice(0, -1).join("/")}/create`)} key={`${tableMetaData.label}-new`} value="New"><Icon sx={{color: accentColor}}>add</Icon>New</Command.Item>
|
<Command.Item onSelect={() => goToItem(`${pathParts.slice(0, -1).join("/")}/create`)} key={`${tableMetaData.label}-new`} value="New"><Icon sx={{color: accentColor}}>add</Icon>New</Command.Item>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission &&
|
tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission &&
|
||||||
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/copy`)} key={`${tableMetaData.label}-copy`} value="Copy"><Icon sx={{color: accentColor}}>copy</Icon>Copy</Command.Item>
|
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/copy`)} key={`${tableMetaData.label}-copy`} value="Copy"><Icon sx={{color: accentColor}}>copy</Icon>Copy</Command.Item>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission &&
|
tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission &&
|
||||||
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/edit`)} key={`${tableMetaData.label}-edit`} value="Edit"><Icon sx={{color: accentColor}}>edit</Icon>Edit</Command.Item>
|
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/edit`)} key={`${tableMetaData.label}-edit`} value="Edit"><Icon sx={{color: accentColor}}>edit</Icon>Edit</Command.Item>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
metaData && metaData.tables.has("audit") &&
|
metaData && metaData.tables.has("audit") &&
|
||||||
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}#audit`)} key={`${tableMetaData.label}-audit`} value="Audit"><Icon sx={{color: accentColor}}>checklist</Icon>Audit</Command.Item>
|
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}#audit`)} key={`${tableMetaData.label}-audit`} value="Audit"><Icon sx={{color: accentColor}}>checklist</Icon>Audit</Command.Item>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
tableProcesses && tableProcesses.length > 0 &&
|
tableProcesses && tableProcesses.length > 0 &&
|
||||||
(
|
(
|
||||||
tableProcesses.map((process) => (
|
tableProcesses.map((process) => (
|
||||||
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/${process.name}`)} key={`${process.name}`} value={`${process.label}`}><Icon sx={{color: accentColor}}>{getIconName(process.iconName, "play_arrow")}</Icon>{process.label}</Command.Item>
|
<Command.Item onSelect={() => goToItem(`${pathParts.join("/")}/${process.name}`)} key={`${process.name}`} value={`${process.label}`}><Icon sx={{color: accentColor}}>{getIconName(process.iconName, "play_arrow")}</Icon>{process.label}</Command.Item>
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Command.Separator />
|
<Command.Separator />
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function TablesSection()
|
function TablesSection()
|
||||||
{
|
{
|
||||||
let tableNames : string[]= [];
|
let tableNames: string[] = [];
|
||||||
metaData.tables.forEach((value: QTableMetaData, key: string) =>
|
metaData.tables.forEach((value: QTableMetaData, key: string) =>
|
||||||
{
|
{
|
||||||
tableNames.push(value.name);
|
tableNames.push(value.name);
|
||||||
})
|
});
|
||||||
tableNames = tableNames.sort((a: string, b:string) =>
|
tableNames = tableNames.sort((a: string, b: string) =>
|
||||||
{
|
{
|
||||||
const labelA = metaData.tables.get(a).label ?? "";
|
const labelA = metaData.tables.get(a).label ?? "";
|
||||||
const labelB = metaData.tables.get(b).label ?? "";
|
const labelB = metaData.tables.get(b).label ?? "";
|
||||||
return (labelA.localeCompare(labelB));
|
return comparator(labelA, labelB);
|
||||||
})
|
});
|
||||||
return(
|
return (
|
||||||
<Command.Group heading="Tables">
|
<Command.Group heading="Tables">
|
||||||
{
|
{
|
||||||
tableNames.map((tableName: string, index: number) =>
|
tableNames.map((tableName: string, index: number) =>
|
||||||
@ -243,6 +293,7 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -252,16 +303,16 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
metaData.apps.forEach((value: QAppMetaData, key: string) =>
|
metaData.apps.forEach((value: QAppMetaData, key: string) =>
|
||||||
{
|
{
|
||||||
appNames.push(value.name);
|
appNames.push(value.name);
|
||||||
})
|
});
|
||||||
|
|
||||||
appNames = appNames.sort((a: string, b:string) =>
|
appNames = appNames.sort((a: string, b: string) =>
|
||||||
{
|
{
|
||||||
const labelA = getFullAppLabel(metaData.appTree, a, 1, "") ?? "";
|
const labelA = getFullAppLabel(metaData.appTree, a, 1, "") ?? "";
|
||||||
const labelB = getFullAppLabel(metaData.appTree, b, 1, "") ?? "";
|
const labelB = getFullAppLabel(metaData.appTree, b, 1, "") ?? "";
|
||||||
return (labelA.localeCompare(labelB));
|
return comparator(labelA, labelB);
|
||||||
})
|
});
|
||||||
|
|
||||||
return(
|
return (
|
||||||
<Command.Group heading="Apps">
|
<Command.Group heading="Apps">
|
||||||
{
|
{
|
||||||
appNames.map((appName: string, index: number) =>
|
appNames.map((appName: string, index: number) =>
|
||||||
@ -276,33 +327,37 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
function RecentlyViewedSection()
|
function RecentlyViewedSection()
|
||||||
{
|
{
|
||||||
const history = HistoryUtils.get();
|
const history = HistoryUtils.get();
|
||||||
const options = [] as any;
|
const options = [] as any;
|
||||||
history.entries.reverse().forEach((entry, index) =>
|
history.entries.reverse().forEach((entry, index) =>
|
||||||
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
||||||
)
|
);
|
||||||
|
|
||||||
let appNames: string[] = [];
|
let appNames: string[] = [];
|
||||||
metaData.apps.forEach((value: QAppMetaData, key: string) =>
|
metaData.apps.forEach((value: QAppMetaData, key: string) =>
|
||||||
{
|
{
|
||||||
appNames.push(value.name);
|
appNames.push(value.name);
|
||||||
})
|
});
|
||||||
|
|
||||||
appNames = appNames.sort((a: string, b:string) =>
|
appNames = appNames.sort((a: string, b: string) =>
|
||||||
{
|
{
|
||||||
const labelA = metaData.apps.get(a).label ?? "";
|
const labelA = metaData.apps.get(a).label ?? "";
|
||||||
const labelB = metaData.apps.get(b).label ?? "";
|
const labelB = metaData.apps.get(b).label ?? "";
|
||||||
return (labelA.localeCompare(labelB));
|
return comparator(labelA, labelB);
|
||||||
})
|
});
|
||||||
|
|
||||||
const entryMap = new Map<string, boolean>();
|
const entryMap = new Map<string, boolean>();
|
||||||
return(
|
return (
|
||||||
<Command.Group heading="Recently Viewed Records">
|
<Command.Group heading="Recently Viewed Records">
|
||||||
{
|
{
|
||||||
history.entries.reverse().map((entry: QHistoryEntry, index: number) =>
|
history.entries.reverse().map((entry: QHistoryEntry, index: number) =>
|
||||||
! entryMap.has(entry.label) && entryMap.set(entry.label, true) && (
|
!entryMap.has(entry.label) && entryMap.set(entry.label, true) && (
|
||||||
<Command.Item onSelect={() => goToItem(`${entry.path}`)} key={`${entry.label}-${index}`} value={entry.label}><Icon sx={{color: accentColor}}>{entry.iconName}</Icon>{entry.label}</Command.Item>
|
<Command.Item onSelect={() => goToItem(`${entry.path}`)} key={`${entry.label}-${index}`} value={entry.label}><Icon sx={{color: accentColor}}>{entry.iconName}</Icon>{entry.label}</Command.Item>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -311,29 +366,101 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const containerElement = useRef(null)
|
const containerElement = useRef(null);
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
function closeKeyboardHelp()
|
function closeKeyboardHelp()
|
||||||
{
|
{
|
||||||
setKeyboardHelpOpen(false);
|
setKeyboardHelpOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
function closeDotMenu()
|
function closeDotMenu()
|
||||||
{
|
{
|
||||||
setDotMenuOpen(false);
|
setDotMenuOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** filter function for cmd-k library
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
function doFilter(value: string, search: string)
|
||||||
|
{
|
||||||
|
setSearchString(search);
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// split on spaces //
|
||||||
|
/////////////////////
|
||||||
|
const searchParts = search.toLowerCase().split(" ");
|
||||||
|
if (searchParts.length == 1)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
// if only 1 word, just do an includes test //
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
return (value.toLowerCase().includes(search.toLowerCase()) ? 1 : 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
////////////////////////////////////////
|
||||||
|
// else split the value on spaces too //
|
||||||
|
////////////////////////////////////////
|
||||||
|
const valueParts = value.toLowerCase().split(" ");
|
||||||
|
if (searchParts.length > valueParts.length)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there are more words in the search than in the value, then it can't match //
|
||||||
|
// e.g. "order c" can't ever match, say "order" //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// iterate over the search parts - if any don't match the corresponding value parts, then it's a non-match //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let valueIndex = 0;
|
||||||
|
for (let i = 0; i < searchParts.length; i++)
|
||||||
|
{
|
||||||
|
let foundMatch = false;
|
||||||
|
for (; valueIndex < valueParts.length; valueIndex++)
|
||||||
|
{
|
||||||
|
if (valueParts[valueIndex].includes(searchParts[i]))
|
||||||
|
{
|
||||||
|
foundMatch = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundMatch)
|
||||||
|
{
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////
|
||||||
|
// if no failure, return a hit //
|
||||||
|
/////////////////////////////////
|
||||||
|
return (1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Box ref={containerElement} className="raycast" sx={{position: "relative", zIndex: 10_000}}>
|
<Box ref={containerElement} className="raycast" sx={{position: "relative", zIndex: 10_000}}>
|
||||||
{
|
{
|
||||||
<Dialog open={dotMenuOpen} onClose={closeDotMenu}>
|
<Dialog open={dotMenuOpen} onClose={closeDotMenu}>
|
||||||
<Command.Dialog open={dotMenuOpen} onOpenChange={setDotMenuOpen} container={containerElement.current} label="Test Global Command Menu">
|
<Command.Dialog open={dotMenuOpen} onOpenChange={setDotMenuOpen} container={containerElement.current} filter={(value, search) => doFilter(value, search)}>
|
||||||
<Box sx={{display: "flex"}}>
|
<Box sx={{display: "flex"}}>
|
||||||
<Command.Input placeholder="Search for Tables, Actions, or Recently Viewed Items..."/>
|
<Command.Input placeholder="Search for Tables, Actions, or Recently Viewed Items..." />
|
||||||
<Button onClick={closeDotMenu}><Icon>close</Icon></Button>
|
<Button onClick={closeDotMenu}><Icon>close</Icon></Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Command.Loading />
|
<Command.Loading />
|
||||||
<Command.Separator />
|
<Command.Separator />
|
||||||
<Command.List>
|
<Command.List>
|
||||||
<Command.Empty>No results found.</Command.Empty>
|
<Command.Empty>No results found.</Command.Empty>
|
||||||
@ -381,6 +508,6 @@ const CommandMenu = ({metaData}: Props) =>
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
}
|
}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
export default CommandMenu;
|
export default CommandMenu;
|
||||||
|
@ -19,16 +19,17 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {InputAdornment, InputLabel} from "@mui/material";
|
import {Box, InputAdornment, InputLabel} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
import {ErrorMessage, Field, useFormikContext} from "formik";
|
import {ErrorMessage, Field, useFormikContext} from "formik";
|
||||||
import React, {useState} from "react";
|
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||||
|
import React, {useMemo, useState} from "react";
|
||||||
import AceEditor from "react-ace";
|
import AceEditor from "react-ace";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
|
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
|
||||||
import MDInput from "qqq/components/legacy/MDInput";
|
import MDInput from "qqq/components/legacy/MDInput";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
import {flushSync} from "react-dom";
|
||||||
|
|
||||||
// Declaring props types for FormField
|
// Declaring props types for FormField
|
||||||
interface Props
|
interface Props
|
||||||
@ -85,6 +86,51 @@ function QDynamicFormField({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// check the field meta data for behavior that says to do toUpperCase or toLowerCase //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let isToUpperCase = useMemo(() => DynamicFormUtils.isToUpperCase(formFieldObject?.fieldMetaData), [formFieldObject]);
|
||||||
|
let isToLowerCase = useMemo(() => DynamicFormUtils.isToLowerCase(formFieldObject?.fieldMetaData), [formFieldObject]);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the field has a toUpperCase or toLowerCase behavior on it, then //
|
||||||
|
// apply that rule. But also, to avoid the cursor always jumping to //
|
||||||
|
// the end of the input, do some manipulation of the selection. //
|
||||||
|
// See: https://giacomocerquone.com/blog/keep-input-cursor-still //
|
||||||
|
// Note, we only want an onChange handle if we're doing one of these //
|
||||||
|
// behaviors, (because teh flushSync is potentially slow). hence, we //
|
||||||
|
// put the onChange in an object and assign it with a spread //
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
let onChange: any = {};
|
||||||
|
if (isToUpperCase || isToLowerCase)
|
||||||
|
{
|
||||||
|
onChange.onChange = (e: any) =>
|
||||||
|
{
|
||||||
|
const beforeStart = e.target.selectionStart;
|
||||||
|
const beforeEnd = e.target.selectionEnd;
|
||||||
|
|
||||||
|
flushSync(() =>
|
||||||
|
{
|
||||||
|
let newValue = e.currentTarget.value;
|
||||||
|
if (isToUpperCase)
|
||||||
|
{
|
||||||
|
newValue = newValue.toUpperCase();
|
||||||
|
}
|
||||||
|
if (isToLowerCase)
|
||||||
|
{
|
||||||
|
newValue = newValue.toLowerCase();
|
||||||
|
}
|
||||||
|
setFieldValue(name, newValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = document.getElementById(name) as HTMLInputElement;
|
||||||
|
if (input)
|
||||||
|
{
|
||||||
|
input.setSelectionRange(beforeStart, beforeEnd);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let field;
|
let field;
|
||||||
let getsBulkEditHtmlLabel = true;
|
let getsBulkEditHtmlLabel = true;
|
||||||
if (type === "checkbox")
|
if (type === "checkbox")
|
||||||
@ -102,7 +148,7 @@ function QDynamicFormField({
|
|||||||
else if (type === "ace")
|
else if (type === "ace")
|
||||||
{
|
{
|
||||||
let mode = "text";
|
let mode = "text";
|
||||||
if(formFieldObject && formFieldObject.languageMode)
|
if (formFieldObject && formFieldObject.languageMode)
|
||||||
{
|
{
|
||||||
mode = formFieldObject.languageMode;
|
mode = formFieldObject.languageMode;
|
||||||
}
|
}
|
||||||
@ -133,7 +179,7 @@ function QDynamicFormField({
|
|||||||
{
|
{
|
||||||
field = (
|
field = (
|
||||||
<>
|
<>
|
||||||
<Field {...rest} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
|
<Field {...rest} {...onChange} onWheel={handleOnWheel} name={name} type={type} as={MDInput} variant="outlined" label={label} InputLabelProps={inputLabelProps} InputProps={inputProps} fullWidth disabled={isDisabled}
|
||||||
onKeyPress={(e: any) =>
|
onKeyPress={(e: any) =>
|
||||||
{
|
{
|
||||||
if (e.key === "Enter")
|
if (e.key === "Enter")
|
||||||
@ -173,7 +219,8 @@ function QDynamicFormField({
|
|||||||
id={`bulkEditSwitch-${name}`}
|
id={`bulkEditSwitch-${name}`}
|
||||||
checked={switchChecked}
|
checked={switchChecked}
|
||||||
onClick={bulkEditSwitchChanged}
|
onClick={bulkEditSwitchChanged}
|
||||||
sx={{top: "-4px",
|
sx={{
|
||||||
|
top: "-4px",
|
||||||
"& .MuiSwitch-track": {
|
"& .MuiSwitch-track": {
|
||||||
height: 20,
|
height: 20,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
|
@ -176,7 +176,7 @@ class DynamicFormUtils
|
|||||||
initialDisplayValue: initialDisplayValue,
|
initialDisplayValue: initialDisplayValue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else if(processName)
|
else if (processName)
|
||||||
{
|
{
|
||||||
dynamicFormFields[field.name].possibleValueProps =
|
dynamicFormFields[field.name].possibleValueProps =
|
||||||
{
|
{
|
||||||
@ -214,7 +214,7 @@ class DynamicFormUtils
|
|||||||
|
|
||||||
if (Array.isArray(disabledFields))
|
if (Array.isArray(disabledFields))
|
||||||
{
|
{
|
||||||
return (disabledFields.indexOf(fieldName) > -1)
|
return (disabledFields.indexOf(fieldName) > -1);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -222,6 +222,44 @@ class DynamicFormUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* check if a field has the TO_UPPER_CASE behavior on it.
|
||||||
|
***************************************************************************/
|
||||||
|
public static isToUpperCase(fieldMetaData: QFieldMetaData): boolean
|
||||||
|
{
|
||||||
|
return this.hasBehavior(fieldMetaData, "TO_UPPER_CASE");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* check if a field has the TO_LOWER_CASE behavior on it.
|
||||||
|
***************************************************************************/
|
||||||
|
public static isToLowerCase(fieldMetaData: QFieldMetaData): boolean
|
||||||
|
{
|
||||||
|
return this.hasBehavior(fieldMetaData, "TO_LOWER_CASE");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* check if a field has a specific behavior name on it.
|
||||||
|
***************************************************************************/
|
||||||
|
private static hasBehavior(fieldMetaData: QFieldMetaData, behaviorName: string): boolean
|
||||||
|
{
|
||||||
|
if (fieldMetaData && fieldMetaData.behaviors)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < fieldMetaData.behaviors.length; i++)
|
||||||
|
{
|
||||||
|
if (fieldMetaData.behaviors[i] == behaviorName)
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DynamicFormUtils;
|
export default DynamicFormUtils;
|
||||||
|
@ -97,7 +97,7 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
|
|||||||
borderColor: inputBorderColor
|
borderColor: inputBorderColor
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
@ -108,36 +108,36 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState(null);
|
const [searchTerm, setSearchTerm] = useState(null);
|
||||||
const [firstRender, setFirstRender] = useState(true);
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))))
|
const [otherValuesWhenResultsWereLoaded, setOtherValuesWhenResultsWereLoaded] = useState(JSON.stringify(Object.fromEntries((otherValues))));
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(tableName && processName)
|
if (tableName && processName)
|
||||||
{
|
{
|
||||||
console.log("DynamicSelect - you may not provide both a tableName and a processName")
|
console.log("DynamicSelect - you may not provide both a tableName and a processName");
|
||||||
}
|
}
|
||||||
if(tableName && !fieldName)
|
if (tableName && !fieldName)
|
||||||
{
|
{
|
||||||
console.log("DynamicSelect - if you provide a tableName, you must also provide a fieldName");
|
console.log("DynamicSelect - if you provide a tableName, you must also provide a fieldName");
|
||||||
}
|
}
|
||||||
if(processName && !fieldName)
|
if (processName && !fieldName)
|
||||||
{
|
{
|
||||||
console.log("DynamicSelect - if you provide a processName, you must also provide a fieldName");
|
console.log("DynamicSelect - if you provide a processName, you must also provide a fieldName");
|
||||||
}
|
}
|
||||||
if(!fieldName && !possibleValueSourceName)
|
if (!fieldName && !possibleValueSourceName)
|
||||||
{
|
{
|
||||||
console.log("DynamicSelect - you must provide either a fieldName (and a tableName or processName) or a possibleValueSourceName");
|
console.log("DynamicSelect - you must provide either a fieldName (and a tableName or processName) or a possibleValueSourceName");
|
||||||
}
|
}
|
||||||
if(fieldName && !possibleValueSourceName)
|
if (fieldName && !possibleValueSourceName)
|
||||||
{
|
{
|
||||||
if(!tableName || !processName)
|
if (!tableName || !processName)
|
||||||
{
|
{
|
||||||
console.log("DynamicSelect - if you provide a fieldName and not a possibleValueSourceName, then you must also provide a tableName or processName");
|
console.log("DynamicSelect - if you provide a fieldName and not a possibleValueSourceName, then you must also provide a tableName or processName");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(possibleValueSourceName)
|
if (possibleValueSourceName)
|
||||||
{
|
{
|
||||||
if(tableName || processName)
|
if (tableName || processName)
|
||||||
{
|
{
|
||||||
console.log("DynamicSelect - if you provide a possibleValueSourceName, you should not also provide a tableName or processName");
|
console.log("DynamicSelect - if you provide a possibleValueSourceName, you should not also provide a tableName or processName");
|
||||||
}
|
}
|
||||||
@ -173,7 +173,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if(firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
// console.log("First render, so not searching...");
|
// console.log("First render, so not searching...");
|
||||||
setFirstRender(false);
|
setFirstRender(false);
|
||||||
@ -196,7 +196,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
// console.log(`doing a search with ${searchTerm}`);
|
// 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);
|
||||||
|
|
||||||
if(tableMetaData == null && tableName)
|
if (tableMetaData == null && tableName)
|
||||||
{
|
{
|
||||||
let tableMetaData: QTableMetaData = await qController.loadTableMetaData(tableName);
|
let tableMetaData: QTableMetaData = await qController.loadTableMetaData(tableName);
|
||||||
setTableMetaData(tableMetaData);
|
setTableMetaData(tableMetaData);
|
||||||
@ -207,7 +207,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
// console.log(`${results}`);
|
// console.log(`${results}`);
|
||||||
if (active)
|
if (active)
|
||||||
{
|
{
|
||||||
setOptions([ ...results ]);
|
setOptions([...results]);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@ -215,12 +215,12 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
{
|
{
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [ searchTerm ]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
// todo - finish... call it in onOpen?
|
// todo - finish... call it in onOpen?
|
||||||
const reloadIfOtherValuesAreChanged = () =>
|
const reloadIfOtherValuesAreChanged = () =>
|
||||||
{
|
{
|
||||||
if(JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
||||||
{
|
{
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
@ -229,16 +229,16 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
console.log("Refreshing possible values...");
|
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);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setOptions([ ...results ]);
|
setOptions([...results]);
|
||||||
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
||||||
{
|
{
|
||||||
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
||||||
if(reason !== "reset")
|
if (reason !== "reset")
|
||||||
{
|
{
|
||||||
// console.log(` -> setting search term to ${value}`);
|
// console.log(` -> setting search term to ${value}`);
|
||||||
setSearchTerm(value);
|
setSearchTerm(value);
|
||||||
@ -248,7 +248,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
const handleBlur = (x: any) =>
|
const handleBlur = (x: any) =>
|
||||||
{
|
{
|
||||||
setSearchTerm(null);
|
setSearchTerm(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||||
{
|
{
|
||||||
@ -256,9 +256,9 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
// console.log(value);
|
// console.log(value);
|
||||||
setSearchTerm(null);
|
setSearchTerm(null);
|
||||||
|
|
||||||
if(onChange)
|
if (onChange)
|
||||||
{
|
{
|
||||||
if(isMultiple)
|
if (isMultiple)
|
||||||
{
|
{
|
||||||
onChange(value);
|
onChange(value);
|
||||||
}
|
}
|
||||||
@ -267,7 +267,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
onChange(value ? new QPossibleValue(value) : null);
|
onChange(value ? new QPossibleValue(value) : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(setFieldValueRef && fieldName)
|
else if (setFieldValueRef && fieldName)
|
||||||
{
|
{
|
||||||
setFieldValueRef(fieldName, value ? value.id : null);
|
setFieldValueRef(fieldName, value ? value.id : null);
|
||||||
}
|
}
|
||||||
@ -280,7 +280,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
// get options whose text/label matches the input (e.g., not ids that match) //
|
// get options whose text/label matches the input (e.g., not ids that match) //
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
return (options);
|
return (options);
|
||||||
}
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const renderOption = (props: Object, option: any, {selected}) =>
|
const renderOption = (props: Object, option: any, {selected}) =>
|
||||||
@ -289,23 +289,24 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const field = tableMetaData?.fields.get(fieldName)
|
const field = tableMetaData?.fields.get(fieldName);
|
||||||
if(field)
|
if (field)
|
||||||
{
|
{
|
||||||
const adornment = field.getAdornment(AdornmentType.CHIP);
|
const adornment = field.getAdornment(AdornmentType.CHIP);
|
||||||
if(adornment)
|
if (adornment)
|
||||||
{
|
{
|
||||||
const color = adornment.getValue("color." + option.id) ?? "default"
|
const color = adornment.getValue("color." + option.id) ?? "default";
|
||||||
const iconName = adornment.getValue("icon." + option.id) ?? null;
|
const iconName = adornment.getValue("icon." + option.id) ?? null;
|
||||||
const iconElement = iconName ? <Icon>{iconName}</Icon> : null;
|
const iconElement = iconName ? <Icon>{iconName}</Icon> : null;
|
||||||
content = (<Chip label={option.label} color={color} icon={iconElement} size="small" variant="outlined" sx={{fontWeight: 500}} />);
|
content = (<Chip label={option.label} color={color} icon={iconElement} size="small" variant="outlined" sx={{fontWeight: 500}} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e)
|
catch (e)
|
||||||
{ }
|
{
|
||||||
|
}
|
||||||
|
|
||||||
if(isMultiple)
|
if (isMultiple)
|
||||||
{
|
{
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
@ -327,7 +328,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
{content}
|
{content}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const bulkEditSwitchChanged = () =>
|
const bulkEditSwitchChanged = () =>
|
||||||
{
|
{
|
||||||
@ -357,7 +358,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
{
|
{
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
// console.log("setting open...");
|
// console.log("setting open...");
|
||||||
if(options.length == 0)
|
if (options.length == 0)
|
||||||
{
|
{
|
||||||
// console.log("no options yet, so setting search term to ''...");
|
// console.log("no options yet, so setting search term to ''...");
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
@ -370,19 +371,19 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
isOptionEqualToValue={(option, value) => value !== null && value !== undefined && option.id === value.id}
|
isOptionEqualToValue={(option, value) => value !== null && value !== undefined && option.id === value.id}
|
||||||
getOptionLabel={(option) =>
|
getOptionLabel={(option) =>
|
||||||
{
|
{
|
||||||
if(option === null || option === undefined)
|
if (option === null || option === undefined)
|
||||||
{
|
{
|
||||||
return ("");
|
return ("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if(option && option.length)
|
if (option && option.length)
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
option = option[0];
|
option = option[0];
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return option.label
|
return option.label;
|
||||||
}}
|
}}
|
||||||
options={options}
|
options={options}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -446,7 +447,8 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
id={`bulkEditSwitch-${fieldName}`}
|
id={`bulkEditSwitch-${fieldName}`}
|
||||||
checked={switchChecked}
|
checked={switchChecked}
|
||||||
onClick={bulkEditSwitchChanged}
|
onClick={bulkEditSwitchChanged}
|
||||||
sx={{top: "-4px",
|
sx={{
|
||||||
|
top: "-4px",
|
||||||
"& .MuiSwitch-track": {
|
"& .MuiSwitch-track": {
|
||||||
height: 20,
|
height: 20,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
@ -465,7 +467,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box mb={1.5}>
|
<Box>
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -44,9 +44,9 @@ import MDTypography from "qqq/components/legacy/MDTypography";
|
|||||||
import HelpContent from "qqq/components/misc/HelpContent";
|
import HelpContent from "qqq/components/misc/HelpContent";
|
||||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||||
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
|
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
|
||||||
|
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
|
||||||
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
|
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
|
||||||
import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
|
import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
|
||||||
import ReportSetupWidget from "qqq/components/widgets/misc/ReportSetupWidget";
|
|
||||||
import {FieldRule, FieldRuleAction, FieldRuleTrigger} from "qqq/models/fields/FieldRules";
|
import {FieldRule, FieldRuleAction, FieldRuleTrigger} from "qqq/models/fields/FieldRules";
|
||||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
@ -88,7 +88,7 @@ EntityForm.defaultProps = {
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
||||||
{
|
{
|
||||||
}
|
};
|
||||||
|
|
||||||
function EntityForm(props: Props): JSX.Element
|
function EntityForm(props: Props): JSX.Element
|
||||||
{
|
{
|
||||||
@ -119,11 +119,12 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
|
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
|
||||||
|
const [modalDataChangedCounter, setModalDataChangedCount] = useState(0);
|
||||||
|
|
||||||
const [notAllowedError, setNotAllowedError] = useState(null as string);
|
const [notAllowedError, setNotAllowedError] = useState(null as string);
|
||||||
|
|
||||||
const [formValuesJSON, setFormValuesJSON] = useState("");
|
const [formValuesJSON, setFormValuesJSON] = useState("");
|
||||||
const [formValues, setFormValues] = useState({} as {[name: string]: any});
|
const [formValues, setFormValues] = useState({} as { [name: string]: any });
|
||||||
|
|
||||||
const {pageHeader, setPageHeader} = useContext(QContext);
|
const {pageHeader, setPageHeader} = useContext(QContext);
|
||||||
|
|
||||||
@ -204,7 +205,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const deleteChildRecord = (name: string, widgetData: any, rowIndex: number) =>
|
function deleteChildRecord(name: string, widgetData: any, rowIndex: number)
|
||||||
{
|
{
|
||||||
updateChildRecordList(name, "delete", rowIndex);
|
updateChildRecordList(name, "delete", rowIndex);
|
||||||
};
|
};
|
||||||
@ -282,6 +283,8 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
setRenderedWidgetSections(newRenderedWidgetSections);
|
setRenderedWidgetSections(newRenderedWidgetSections);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
|
|
||||||
|
setModalDataChangedCount(modalDataChangedCounter + 1);
|
||||||
|
|
||||||
setShowEditChildForm(null);
|
setShowEditChildForm(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +294,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
const newRenderedWidgetSections: {[name: string]: JSX.Element} = {};
|
const newRenderedWidgetSections: { [name: string]: JSX.Element } = {};
|
||||||
for (let widgetName in renderedWidgetSections)
|
for (let widgetName in renderedWidgetSections)
|
||||||
{
|
{
|
||||||
const widgetMetaData = metaData.widgets.get(widgetName);
|
const widgetMetaData = metaData.widgets.get(widgetName);
|
||||||
@ -351,12 +354,11 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** if we have a widget that wants to set form-field values, they can take this
|
** if we have a widget that wants to set form-field values, they can take this
|
||||||
** function in as a callback, and then call it with their values.
|
** function in as a callback, and then call it with their values.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function setFormFieldValuesFromWidget(values: {[name: string]: any})
|
function setFormFieldValuesFromWidget(values: { [name: string]: any })
|
||||||
{
|
{
|
||||||
for (let key in values)
|
for (let key in values)
|
||||||
{
|
{
|
||||||
@ -370,13 +372,13 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function getWidgetSection(widgetMetaData: QWidgetMetaData, widgetData: any): JSX.Element
|
function getWidgetSection(widgetMetaData: QWidgetMetaData, widgetData: any): JSX.Element
|
||||||
{
|
{
|
||||||
if(widgetMetaData.type == "childRecordList")
|
if (widgetMetaData.type == "childRecordList")
|
||||||
{
|
{
|
||||||
widgetData.viewAllLink = null;
|
widgetData.viewAllLink = null;
|
||||||
widgetMetaData.showExportButton = false;
|
widgetMetaData.showExportButton = false;
|
||||||
|
|
||||||
return <RecordGridWidget
|
return Object.keys(childListWidgetData).length > 0 && (<RecordGridWidget
|
||||||
key={new Date().getTime()} // added so that editing values actually re-renders...
|
key={`${formValues["tableName"]}-${modalDataChangedCounter}`}
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
data={widgetData}
|
data={widgetData}
|
||||||
disableRowClick
|
disableRowClick
|
||||||
@ -385,21 +387,31 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
addNewRecordCallback={() => openAddChildRecord(widgetMetaData.name, widgetData)}
|
addNewRecordCallback={() => openAddChildRecord(widgetMetaData.name, widgetData)}
|
||||||
editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData, rowIndex)}
|
editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData, rowIndex)}
|
||||||
deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, widgetData, rowIndex)}
|
deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, widgetData, rowIndex)}
|
||||||
/>;
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(widgetMetaData.type == "reportSetup")
|
if (widgetMetaData.type == "filterAndColumnsSetup")
|
||||||
{
|
{
|
||||||
return <ReportSetupWidget
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the widget metadata specifies a table name, set form values to that so widget knows which to use //
|
||||||
|
// (for the case when it is not being specified by a separate field in the record) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (widgetData?.tableName)
|
||||||
|
{
|
||||||
|
formValues["tableName"] = widgetData?.tableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FilterAndColumnsSetupWidget
|
||||||
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
|
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
|
||||||
isEditable={true}
|
isEditable={true}
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
|
widgetData={widgetData}
|
||||||
recordValues={formValues}
|
recordValues={formValues}
|
||||||
onSaveCallback={setFormFieldValuesFromWidget}
|
onSaveCallback={setFormFieldValuesFromWidget}
|
||||||
/>
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(widgetMetaData.type == "pivotTableSetup")
|
if (widgetMetaData.type == "pivotTableSetup")
|
||||||
{
|
{
|
||||||
return <PivotTableSetupWidget
|
return <PivotTableSetupWidget
|
||||||
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
|
key={formValues["tableName"]} // todo, is this good? it was added so that editing values actually re-renders...
|
||||||
@ -407,10 +419,10 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
recordValues={formValues}
|
recordValues={formValues}
|
||||||
onSaveCallback={setFormFieldValuesFromWidget}
|
onSaveCallback={setFormFieldValuesFromWidget}
|
||||||
/>
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(widgetMetaData.type == "dynamicForm")
|
if (widgetMetaData.type == "dynamicForm")
|
||||||
{
|
{
|
||||||
return <DynamicFormWidget
|
return <DynamicFormWidget
|
||||||
key={formValues["savedReportId"]} // todo - pull this from the metaData (could do so above too...)
|
key={formValues["savedReportId"]} // todo - pull this from the metaData (could do so above too...)
|
||||||
@ -420,10 +432,10 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
recordValues={formValues}
|
recordValues={formValues}
|
||||||
record={record}
|
record={record}
|
||||||
onSaveCallback={setFormFieldValuesFromWidget}
|
onSaveCallback={setFormFieldValuesFromWidget}
|
||||||
/>
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<Box>Unsupported widget type: {widgetMetaData.type}</Box>)
|
return (<Box>Unsupported widget type: {widgetMetaData.type}</Box>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -449,12 +461,12 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
function setupFieldRules(tableMetaData: QTableMetaData)
|
function setupFieldRules(tableMetaData: QTableMetaData)
|
||||||
{
|
{
|
||||||
const mdbMetaData = tableMetaData?.supplementalTableMetaData?.get("materialDashboard");
|
const mdbMetaData = tableMetaData?.supplementalTableMetaData?.get("materialDashboard");
|
||||||
if(!mdbMetaData)
|
if (!mdbMetaData)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mdbMetaData.fieldRules)
|
if (mdbMetaData.fieldRules)
|
||||||
{
|
{
|
||||||
const newFieldRules: FieldRule[] = [];
|
const newFieldRules: FieldRule[] = [];
|
||||||
for (let i = 0; i < mdbMetaData.fieldRules.length; i++)
|
for (let i = 0; i < mdbMetaData.fieldRules.length; i++)
|
||||||
@ -469,83 +481,164 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
//////////////////
|
//////////////////
|
||||||
// initial load //
|
// initial load //
|
||||||
//////////////////
|
//////////////////
|
||||||
if (!asyncLoadInited)
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setAsyncLoadInited(true);
|
if (!asyncLoadInited)
|
||||||
(async () =>
|
|
||||||
{
|
{
|
||||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
setAsyncLoadInited(true);
|
||||||
setTableMetaData(tableMetaData);
|
(async () =>
|
||||||
recordAnalytics({location: window.location, title: (props.isCopy ? "Copy" : props.id ? "Edit" : "New") + ": " + tableMetaData.label});
|
|
||||||
|
|
||||||
setupFieldRules(tableMetaData);
|
|
||||||
|
|
||||||
const metaData = await qController.loadMetaData();
|
|
||||||
setMetaData(metaData);
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////
|
|
||||||
// define the sections, e.g., for the left-bar //
|
|
||||||
/////////////////////////////////////////////////
|
|
||||||
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
|
|
||||||
{
|
{
|
||||||
const widget = metaData.widgets.get(section.widgetName);
|
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||||
if(widget)
|
setTableMetaData(tableMetaData);
|
||||||
|
recordAnalytics({location: window.location, title: (props.isCopy ? "Copy" : props.id ? "Edit" : "New") + ": " + tableMetaData.label});
|
||||||
|
|
||||||
|
setupFieldRules(tableMetaData);
|
||||||
|
|
||||||
|
const metaData = await qController.loadMetaData();
|
||||||
|
setMetaData(metaData);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
// define the sections, e.g., for the left-bar //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
|
||||||
{
|
{
|
||||||
if(widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
|
const widget = metaData?.widgets.get(section.widgetName);
|
||||||
|
if (widget)
|
||||||
{
|
{
|
||||||
return (true);
|
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.type == "filterAndColumnsSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm")
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(widget.type == "reportSetup" || widget.type == "pivotTableSetup" || widget.type == "dynamicForm")
|
return (false);
|
||||||
{
|
});
|
||||||
return (true);
|
setTableSections(tableSections);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (false);
|
const fieldArray = [] as QFieldMetaData[];
|
||||||
});
|
const sortedKeys = [...tableMetaData.fields.keys()].sort();
|
||||||
setTableSections(tableSections);
|
sortedKeys.forEach((key) =>
|
||||||
|
|
||||||
const fieldArray = [] as QFieldMetaData[];
|
|
||||||
const sortedKeys = [...tableMetaData.fields.keys()].sort();
|
|
||||||
sortedKeys.forEach((key) =>
|
|
||||||
{
|
|
||||||
const fieldMetaData = tableMetaData.fields.get(key);
|
|
||||||
fieldArray.push(fieldMetaData);
|
|
||||||
});
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// if doing an edit or copy, fetch the record and pre-populate the form values from it //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
let record: QRecord = null;
|
|
||||||
let defaultDisplayValues = new Map<string, string>();
|
|
||||||
if (props.id !== null)
|
|
||||||
{
|
|
||||||
record = await qController.get(tableName, props.id);
|
|
||||||
setRecord(record);
|
|
||||||
recordAnalytics({category: "tableEvents", action: props.isCopy ? "copy" : "edit", label: tableMetaData?.label + " / " + record?.recordLabel});
|
|
||||||
|
|
||||||
const titleVerb = props.isCopy ? "Copy" : "Edit";
|
|
||||||
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
|
|
||||||
|
|
||||||
if (!props.isModal)
|
|
||||||
{
|
{
|
||||||
setPageHeader(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
|
const fieldMetaData = tableMetaData.fields.get(key);
|
||||||
}
|
fieldArray.push(fieldMetaData);
|
||||||
|
|
||||||
tableMetaData.fields.forEach((fieldMetaData, key) =>
|
|
||||||
{
|
|
||||||
if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initialValues[key] = record.values.get(key);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// these checks are only for updating records, if copying, it is actually an insert, which is checked after this block //
|
// if doing an edit or copy, fetch the record and pre-populate the form values from it //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (!props.isCopy)
|
let record: QRecord = null;
|
||||||
|
let defaultDisplayValues = new Map<string, string>();
|
||||||
|
if (props.id !== null)
|
||||||
|
{
|
||||||
|
record = await qController.get(tableName, props.id);
|
||||||
|
setRecord(record);
|
||||||
|
recordAnalytics({category: "tableEvents", action: props.isCopy ? "copy" : "edit", label: tableMetaData?.label + " / " + record?.recordLabel});
|
||||||
|
|
||||||
|
const titleVerb = props.isCopy ? "Copy" : "Edit";
|
||||||
|
setFormTitle(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
|
||||||
|
|
||||||
|
if (!props.isModal)
|
||||||
|
{
|
||||||
|
setPageHeader(`${titleVerb} ${tableMetaData?.label}: ${record?.recordLabel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
tableMetaData.fields.forEach((fieldMetaData, key) =>
|
||||||
|
{
|
||||||
|
if (props.isCopy && fieldMetaData.name == tableMetaData.primaryKeyField)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initialValues[key] = record.values.get(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// these checks are only for updating records, if copying, it is actually an insert, which is checked after this block //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (!props.isCopy)
|
||||||
|
{
|
||||||
|
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
|
||||||
|
{
|
||||||
|
setNotAllowedError("Records may not be edited in this table");
|
||||||
|
}
|
||||||
|
else if (!tableMetaData.editPermission)
|
||||||
|
{
|
||||||
|
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////
|
||||||
|
// else handle preparing to do an insert //
|
||||||
|
///////////////////////////////////////////
|
||||||
|
setFormTitle(`Creating New ${tableMetaData?.label}`);
|
||||||
|
recordAnalytics({category: "tableEvents", action: "new", label: tableMetaData?.label});
|
||||||
|
|
||||||
|
if (!props.isModal)
|
||||||
|
{
|
||||||
|
setPageHeader(`Creating New ${tableMetaData?.label}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if default values were supplied for a new record, then populate initialValues, for formik. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
for (let i = 0; i < fieldArray.length; i++)
|
||||||
|
{
|
||||||
|
const fieldMetaData = fieldArray[i];
|
||||||
|
const fieldName = fieldMetaData.name;
|
||||||
|
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
|
||||||
|
if (defaultValue)
|
||||||
|
{
|
||||||
|
initialValues[fieldName] = defaultValue;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// we need to set the initialDisplayValue for possible value fields with a default value //
|
||||||
|
// so, look them up here now if needed //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (fieldMetaData.possibleValueSourceName)
|
||||||
|
{
|
||||||
|
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
|
||||||
|
if (results && results.length > 0)
|
||||||
|
{
|
||||||
|
defaultDisplayValues.set(fieldName, results[0].label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
// if an override heading was passed in, use it. //
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
if (props.overrideHeading)
|
||||||
|
{
|
||||||
|
setFormTitle(props.overrideHeading);
|
||||||
|
if (!props.isModal)
|
||||||
|
{
|
||||||
|
setPageHeader(props.overrideHeading);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// check capabilities & permissions //
|
||||||
|
//////////////////////////////////////
|
||||||
|
if (props.isCopy || !props.id)
|
||||||
|
{
|
||||||
|
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
|
||||||
|
{
|
||||||
|
setNotAllowedError("Records may not be created in this table");
|
||||||
|
}
|
||||||
|
else if (!tableMetaData.insertPermission)
|
||||||
|
{
|
||||||
|
setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
|
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
|
||||||
{
|
{
|
||||||
@ -556,201 +649,123 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
|
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
///////////////////////////////////////////
|
|
||||||
// else handle preparing to do an insert //
|
|
||||||
///////////////////////////////////////////
|
|
||||||
setFormTitle(`Creating New ${tableMetaData?.label}`);
|
|
||||||
recordAnalytics({category: "tableEvents", action: "new", label: tableMetaData?.label});
|
|
||||||
|
|
||||||
if (!props.isModal)
|
/////////////////////////////////////////////////////////////////////
|
||||||
{
|
// make sure all initialValues are properly formatted for the form //
|
||||||
setPageHeader(`Creating New ${tableMetaData?.label}`);
|
/////////////////////////////////////////////////////////////////////
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// if default values were supplied for a new record, then populate initialValues, for formik. //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
for (let i = 0; i < fieldArray.length; i++)
|
for (let i = 0; i < fieldArray.length; i++)
|
||||||
{
|
{
|
||||||
const fieldMetaData = fieldArray[i];
|
const fieldMetaData = fieldArray[i];
|
||||||
const fieldName = fieldMetaData.name;
|
if (fieldMetaData.type == QFieldType.DATE_TIME && initialValues[fieldMetaData.name])
|
||||||
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
|
|
||||||
if (defaultValue)
|
|
||||||
{
|
{
|
||||||
initialValues[fieldName] = defaultValue;
|
initialValues[fieldMetaData.name] = ValueUtils.formatDateTimeValueForForm(initialValues[fieldMetaData.name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
setInitialValues(initialValues);
|
||||||
// we need to set the initialDisplayValue for possible value fields with a default value //
|
|
||||||
// so, look them up here now if needed //
|
/////////////////////////////////////////////////////////
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
// get formField and formValidation objects for Formik //
|
||||||
if (fieldMetaData.possibleValueSourceName)
|
/////////////////////////////////////////////////////////
|
||||||
|
const {
|
||||||
|
dynamicFormFields,
|
||||||
|
formValidations,
|
||||||
|
} = DynamicFormUtils.getFormData(fieldArray, disabledFields);
|
||||||
|
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, null, record ? record.displayValues : defaultDisplayValues);
|
||||||
|
|
||||||
|
/////////////////////////////////////
|
||||||
|
// group the formFields by section //
|
||||||
|
/////////////////////////////////////
|
||||||
|
const dynamicFormFieldsBySection = new Map<string, any>();
|
||||||
|
let t1sectionName;
|
||||||
|
let t1section;
|
||||||
|
const nonT1Sections: QTableSection[] = [];
|
||||||
|
const newRenderedWidgetSections: { [name: string]: JSX.Element } = {};
|
||||||
|
const newChildListWidgetData: { [name: string]: ChildRecordListData } = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < tableSections.length; i++)
|
||||||
|
{
|
||||||
|
const section = tableSections[i];
|
||||||
|
const sectionDynamicFormFields: any[] = [];
|
||||||
|
|
||||||
|
if (section.isHidden)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFields = section.fieldNames && section.fieldNames.length > 0;
|
||||||
|
if (hasFields)
|
||||||
|
{
|
||||||
|
for (let j = 0; j < section.fieldNames.length; j++)
|
||||||
{
|
{
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
|
const fieldName = section.fieldNames[j];
|
||||||
if (results && results.length > 0)
|
const field = tableMetaData.fields.get(fieldName);
|
||||||
|
|
||||||
|
if (!field)
|
||||||
{
|
{
|
||||||
defaultDisplayValues.set(fieldName, results[0].label);
|
console.log(`Omitting un-found field ${fieldName} from form`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
|
||||||
|
// || (or) we're on the insert screen in which case, only show editable fields. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if ((props.id !== null && !props.isCopy) || field.isEditable)
|
||||||
|
{
|
||||||
|
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////
|
if (sectionDynamicFormFields.length === 0)
|
||||||
// if an override heading was passed in, use it. //
|
|
||||||
///////////////////////////////////////////////////
|
|
||||||
if (props.overrideHeading)
|
|
||||||
{
|
|
||||||
setFormTitle(props.overrideHeading);
|
|
||||||
if (!props.isModal)
|
|
||||||
{
|
|
||||||
setPageHeader(props.overrideHeading);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////////////////////////////
|
|
||||||
// check capabilities & permissions //
|
|
||||||
//////////////////////////////////////
|
|
||||||
if (props.isCopy || !props.id)
|
|
||||||
{
|
|
||||||
if (!tableMetaData.capabilities.has(Capability.TABLE_INSERT))
|
|
||||||
{
|
|
||||||
setNotAllowedError("Records may not be created in this table");
|
|
||||||
}
|
|
||||||
else if (!tableMetaData.insertPermission)
|
|
||||||
{
|
|
||||||
setNotAllowedError(`You do not have permission to create ${tableMetaData.label} records`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!tableMetaData.capabilities.has(Capability.TABLE_UPDATE))
|
|
||||||
{
|
|
||||||
setNotAllowedError("Records may not be edited in this table");
|
|
||||||
}
|
|
||||||
else if (!tableMetaData.editPermission)
|
|
||||||
{
|
|
||||||
setNotAllowedError(`You do not have permission to edit ${tableMetaData.label} records`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////
|
|
||||||
// make sure all initialValues are properly formatted for the form //
|
|
||||||
/////////////////////////////////////////////////////////////////////
|
|
||||||
for (let i = 0; i < fieldArray.length; i++)
|
|
||||||
{
|
|
||||||
const fieldMetaData = fieldArray[i];
|
|
||||||
if (fieldMetaData.type == QFieldType.DATE_TIME && initialValues[fieldMetaData.name])
|
|
||||||
{
|
|
||||||
initialValues[fieldMetaData.name] = ValueUtils.formatDateTimeValueForForm(initialValues[fieldMetaData.name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInitialValues(initialValues);
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////
|
|
||||||
// get formField and formValidation objects for Formik //
|
|
||||||
/////////////////////////////////////////////////////////
|
|
||||||
const {
|
|
||||||
dynamicFormFields,
|
|
||||||
formValidations,
|
|
||||||
} = DynamicFormUtils.getFormData(fieldArray, disabledFields);
|
|
||||||
DynamicFormUtils.addPossibleValueProps(dynamicFormFields, fieldArray, tableName, null, record ? record.displayValues : defaultDisplayValues);
|
|
||||||
|
|
||||||
/////////////////////////////////////
|
|
||||||
// group the formFields by section //
|
|
||||||
/////////////////////////////////////
|
|
||||||
const dynamicFormFieldsBySection = new Map<string, any>();
|
|
||||||
let t1sectionName;
|
|
||||||
let t1section;
|
|
||||||
const nonT1Sections: QTableSection[] = [];
|
|
||||||
const newRenderedWidgetSections: { [name: string]: JSX.Element } = {};
|
|
||||||
const newChildListWidgetData: { [name: string]: ChildRecordListData } = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < tableSections.length; i++)
|
|
||||||
{
|
|
||||||
const section = tableSections[i];
|
|
||||||
const sectionDynamicFormFields: any[] = [];
|
|
||||||
|
|
||||||
if (section.isHidden)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasFields = section.fieldNames && section.fieldNames.length > 0;
|
|
||||||
if(hasFields)
|
|
||||||
{
|
|
||||||
for (let j = 0; j < section.fieldNames.length; j++)
|
|
||||||
{
|
|
||||||
const fieldName = section.fieldNames[j];
|
|
||||||
const field = tableMetaData.fields.get(fieldName);
|
|
||||||
|
|
||||||
if (!field)
|
|
||||||
{
|
{
|
||||||
console.log(`Omitting un-found field ${fieldName} from form`);
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// in case there are no active fields in this section, remove it from the tableSections array //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
tableSections.splice(i, 1);
|
||||||
|
i--;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// if id !== null (and we're not copying) - means we're on the edit screen -- show all fields on the edit screen. //
|
|
||||||
// || (or) we're on the insert screen in which case, only show editable fields. //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
if ((props.id !== null && !props.isCopy) || field.isEditable)
|
|
||||||
{
|
{
|
||||||
sectionDynamicFormFields.push(dynamicFormFields[fieldName]);
|
dynamicFormFieldsBySection.set(section.name, sectionDynamicFormFields);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sectionDynamicFormFields.length === 0)
|
|
||||||
{
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// in case there are no active fields in this section, remove it from the tableSections array //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tableSections.splice(i, 1);
|
|
||||||
i--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dynamicFormFieldsBySection.set(section.name, sectionDynamicFormFields);
|
const widgetMetaData = metaData?.widgets.get(section.widgetName);
|
||||||
|
const widgetData = await qController.widget(widgetMetaData.name, makeQueryStringWithIdAndObject(tableMetaData, defaultValues));
|
||||||
|
|
||||||
|
newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData);
|
||||||
|
newChildListWidgetData[section.widgetName] = widgetData;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// capture the tier1 section's name //
|
||||||
|
//////////////////////////////////////
|
||||||
|
if (section.tier === "T1")
|
||||||
|
{
|
||||||
|
t1sectionName = section.name;
|
||||||
|
t1section = section;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nonT1Sections.push(section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
const widgetMetaData = metaData.widgets.get(section.widgetName);
|
|
||||||
const widgetData = await qController.widget(widgetMetaData.name, makeQueryStringWithIdAndObject(tableMetaData, defaultValues));
|
|
||||||
|
|
||||||
newRenderedWidgetSections[section.widgetName] = getWidgetSection(widgetMetaData, widgetData);
|
setT1SectionName(t1sectionName);
|
||||||
newChildListWidgetData[section.widgetName] = widgetData;
|
setT1Section(t1section);
|
||||||
}
|
setNonT1Sections(nonT1Sections);
|
||||||
|
setFormFields(dynamicFormFieldsBySection);
|
||||||
|
setValidations(Yup.object().shape(formValidations));
|
||||||
|
setRenderedWidgetSections(newRenderedWidgetSections);
|
||||||
|
setChildListWidgetData(newChildListWidgetData);
|
||||||
|
|
||||||
//////////////////////////////////////
|
forceUpdate();
|
||||||
// capture the tier1 section's name //
|
})();
|
||||||
//////////////////////////////////////
|
}
|
||||||
if (section.tier === "T1")
|
}, []);
|
||||||
{
|
|
||||||
t1sectionName = section.name;
|
|
||||||
t1section = section;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
nonT1Sections.push(section);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setT1SectionName(t1sectionName);
|
|
||||||
setT1Section(t1section);
|
|
||||||
setNonT1Sections(nonT1Sections);
|
|
||||||
setFormFields(dynamicFormFieldsBySection);
|
|
||||||
setValidations(Yup.object().shape(formValidations));
|
|
||||||
setRenderedWidgetSections(newRenderedWidgetSections);
|
|
||||||
setChildListWidgetData(newChildListWidgetData);
|
|
||||||
|
|
||||||
forceUpdate();
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
@ -870,16 +885,28 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
let haveAssociationsToPost = false;
|
let haveAssociationsToPost = false;
|
||||||
for (let name of Object.keys(childListWidgetData))
|
for (let name of Object.keys(childListWidgetData))
|
||||||
{
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if cannot find association name, continue loop, since cannot tell backend which association this is for //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const manageAssociationName = metaData.widgets.get(name)?.defaultValues?.get("manageAssociationName");
|
const manageAssociationName = metaData.widgets.get(name)?.defaultValues?.get("manageAssociationName");
|
||||||
if (!manageAssociationName)
|
if (!manageAssociationName)
|
||||||
{
|
{
|
||||||
console.log(`Cannot send association data to backend - missing a manageAssociationName defaultValue in widget meta data for widget name ${name}`);
|
console.log(`Cannot send association data to backend - missing a manageAssociationName defaultValue in widget meta data for widget name ${name}`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
associationsToPost[manageAssociationName] = [];
|
|
||||||
haveAssociationsToPost = true;
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
for (let i = 0; i < childListWidgetData[name].queryOutput?.records?.length; i++)
|
// if the records array exists, add to associations to post - note: even if empty list, the backend will expect this //
|
||||||
|
// association name to be present if it is to act on it (for the case when all associations have been deleted) //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (childListWidgetData[name].queryOutput.records)
|
||||||
{
|
{
|
||||||
associationsToPost[manageAssociationName].push(childListWidgetData[name].queryOutput.records[i].values);
|
associationsToPost[manageAssociationName] = [];
|
||||||
|
haveAssociationsToPost = true;
|
||||||
|
for (let i = 0; i < childListWidgetData[name].queryOutput?.records?.length; i++)
|
||||||
|
{
|
||||||
|
associationsToPost[manageAssociationName].push(childListWidgetData[name].queryOutput.records[i].values);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (haveAssociationsToPost)
|
if (haveAssociationsToPost)
|
||||||
@ -1000,19 +1027,19 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function makeQueryStringWithIdAndObject(tableMetaData: QTableMetaData, object: {[key: string]: any})
|
function makeQueryStringWithIdAndObject(tableMetaData: QTableMetaData, object: { [key: string]: any })
|
||||||
{
|
{
|
||||||
const queryParamsArray: string[] = [];
|
const queryParamsArray: string[] = [];
|
||||||
if(props.id)
|
if (props.id)
|
||||||
{
|
{
|
||||||
queryParamsArray.push(`${tableMetaData.primaryKeyField}=${encodeURIComponent(props.id)}`)
|
queryParamsArray.push(`${tableMetaData.primaryKeyField}=${encodeURIComponent(props.id)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(object)
|
if (object)
|
||||||
{
|
{
|
||||||
for (let key in object)
|
for (let key in object)
|
||||||
{
|
{
|
||||||
queryParamsArray.push(`${key}=${encodeURIComponent(object[key])}`)
|
queryParamsArray.push(`${key}=${encodeURIComponent(object[key])}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1023,7 +1050,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
async function reloadWidget(widgetName: string, additionalQueryParamsForWidget: {[key: string]: any })
|
async function reloadWidget(widgetName: string, additionalQueryParamsForWidget: { [key: string]: any })
|
||||||
{
|
{
|
||||||
const widgetData = await qController.widget(widgetName, makeQueryStringWithIdAndObject(tableMetaData, additionalQueryParamsForWidget));
|
const widgetData = await qController.widget(widgetName, makeQueryStringWithIdAndObject(tableMetaData, additionalQueryParamsForWidget));
|
||||||
const widgetMetaData = metaData.widgets.get(widgetName);
|
const widgetMetaData = metaData.widgets.get(widgetName);
|
||||||
@ -1045,11 +1072,11 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** process a form-field having a changed value (e.g., apply field rules).
|
** process a form-field having a changed value (e.g., apply field rules).
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function handleChangedFieldValue(fieldName: string, oldValue: any, newValue: any, valueChangesToMake: {[fieldName: string]: any})
|
function handleChangedFieldValue(fieldName: string, oldValue: any, newValue: any, valueChangesToMake: { [fieldName: string]: any })
|
||||||
{
|
{
|
||||||
for (let fieldRule of fieldRules)
|
for (let fieldRule of fieldRules)
|
||||||
{
|
{
|
||||||
if(fieldRule.trigger == FieldRuleTrigger.ON_CHANGE && fieldRule.sourceField == fieldName)
|
if (fieldRule.trigger == FieldRuleTrigger.ON_CHANGE && fieldRule.sourceField == fieldName)
|
||||||
{
|
{
|
||||||
switch (fieldRule.action)
|
switch (fieldRule.action)
|
||||||
{
|
{
|
||||||
@ -1058,7 +1085,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
valueChangesToMake[fieldRule.targetField] = null;
|
valueChangesToMake[fieldRule.targetField] = null;
|
||||||
break;
|
break;
|
||||||
case FieldRuleAction.RELOAD_WIDGET:
|
case FieldRuleAction.RELOAD_WIDGET:
|
||||||
const additionalQueryParamsForWidget: {[key: string]: any} = {};
|
const additionalQueryParamsForWidget: { [key: string]: any } = {};
|
||||||
additionalQueryParamsForWidget[fieldRule.sourceField] = newValue;
|
additionalQueryParamsForWidget[fieldRule.sourceField] = newValue;
|
||||||
reloadWidget(fieldRule.targetWidget, additionalQueryParamsForWidget);
|
reloadWidget(fieldRule.targetWidget, additionalQueryParamsForWidget);
|
||||||
}
|
}
|
||||||
@ -1148,21 +1175,21 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
/////////////////////////////////////////////////
|
/////////////////////////////////////////////////
|
||||||
// if we have values from formik, look at them //
|
// if we have values from formik, look at them //
|
||||||
/////////////////////////////////////////////////
|
/////////////////////////////////////////////////
|
||||||
if(values)
|
if (values)
|
||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
// use stringified values as cheap/easy way to see if any are changed //
|
// use stringified values as cheap/easy way to see if any are changed //
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
const newFormValuesJSON = JSON.stringify(values);
|
const newFormValuesJSON = JSON.stringify(values);
|
||||||
if(formValuesJSON != newFormValuesJSON)
|
if (formValuesJSON != newFormValuesJSON)
|
||||||
{
|
{
|
||||||
const valueChangesToMake: {[fieldName: string]: any} = {};
|
const valueChangesToMake: { [fieldName: string]: any } = {};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////
|
||||||
// if the form is dirty (e.g., we're not doing the initial load), //
|
// if the form is dirty (e.g., we're not doing the initial load), //
|
||||||
// then process rules for any changed fields //
|
// then process rules for any changed fields //
|
||||||
////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////
|
||||||
if(dirty)
|
if (dirty)
|
||||||
{
|
{
|
||||||
for (let fieldName in values)
|
for (let fieldName in values)
|
||||||
{
|
{
|
||||||
@ -1194,7 +1221,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
setFieldValue(fieldName, valueChangesToMake[fieldName], false);
|
setFieldValue(fieldName, valueChangesToMake[fieldName], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormValues(formValues)
|
setFormValues(formValues);
|
||||||
setFormValuesJSON(JSON.stringify(values));
|
setFormValuesJSON(JSON.stringify(values));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
|||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
// strip away empty elements of the route (e.g., trailing slash(es)) //
|
// strip away empty elements of the route (e.g., trailing slash(es)) //
|
||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
if(route.length)
|
if (route.length)
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
route = route.filter(r => r != "");
|
route = route.filter(r => r != "");
|
||||||
@ -74,18 +74,18 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
|||||||
|
|
||||||
const fullPathToLabel = (fullPath: string, route: string): string =>
|
const fullPathToLabel = (fullPath: string, route: string): string =>
|
||||||
{
|
{
|
||||||
if(fullPath.endsWith("/"))
|
if (fullPath.endsWith("/"))
|
||||||
{
|
{
|
||||||
fullPath = fullPath.replace(/\/+$/, "");
|
fullPath = fullPath.replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(pathToLabelMap && pathToLabelMap[fullPath])
|
if (pathToLabelMap && pathToLabelMap[fullPath])
|
||||||
{
|
{
|
||||||
return pathToLabelMap[fullPath];
|
return pathToLabelMap[fullPath];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (routeToLabel(route));
|
return (routeToLabel(route));
|
||||||
}
|
};
|
||||||
|
|
||||||
let pageTitle = branding?.appName ?? "";
|
let pageTitle = branding?.appName ?? "";
|
||||||
const fullRoutes: string[] = [];
|
const fullRoutes: string[] = [];
|
||||||
@ -94,9 +94,9 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
// avoid showing "saved view" as a breadcrumb element //
|
// avoid showing "saved view" as a breadcrumb element //
|
||||||
// e.g., if at /app/table/savedView/1 (so where i==2) //
|
// e.g., if at /app/table/savedView/1 //
|
||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
if(routes[i] === "savedView" && i == 2)
|
if (routes[i] === "savedView" && i == routes.length - 1)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -106,12 +106,12 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element
|
|||||||
// e.g., when at /app/table/savedView/1 (so where i==1) //
|
// e.g., when at /app/table/savedView/1 (so where i==1) //
|
||||||
// we want to just be showing "App" //
|
// we want to just be showing "App" //
|
||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
if(i < routes.length - 1 && routes[i+1] == "savedView" && i == 1)
|
if (i < routes.length - 1 && routes[i + 1] == "savedView" && i == 1)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(routes[i] === "")
|
if (routes[i] === "")
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -19,16 +19,16 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Popper, InputAdornment} from "@mui/material";
|
import {Popper, InputAdornment, Box} from "@mui/material";
|
||||||
import AppBar from "@mui/material/AppBar";
|
import AppBar from "@mui/material/AppBar";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
import {Theme} from "@mui/material/styles";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import React, {useContext, useEffect, useState} from "react";
|
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||||
import {useLocation, useNavigate} from "react-router-dom";
|
import {useLocation, useNavigate} from "react-router-dom";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
|
import QBreadcrumbs, {routeToLabel} from "qqq/components/horseshoe/Breadcrumbs";
|
||||||
@ -45,7 +45,8 @@ interface Props
|
|||||||
isMini?: boolean;
|
isMini?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HistoryEntry {
|
interface HistoryEntry
|
||||||
|
{
|
||||||
id: number;
|
id: number;
|
||||||
path: string;
|
path: string;
|
||||||
label: string;
|
label: string;
|
||||||
@ -64,7 +65,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
const route = useLocation().pathname.split("/").slice(1);
|
const route = useLocation().pathname.split("/").slice(1);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const {pageHeader} = useContext(QContext);
|
const {pageHeader, setDotMenuOpen} = useContext(QContext);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
@ -99,7 +100,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
const options = [] as any;
|
const options = [] as any;
|
||||||
history.entries.reverse().forEach((entry, index) =>
|
history.entries.reverse().forEach((entry, index) =>
|
||||||
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
options.push({label: `${entry.label} index`, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
||||||
)
|
);
|
||||||
setHistory(options);
|
setHistory(options);
|
||||||
|
|
||||||
// Remove event listener on cleanup
|
// Remove event listener on cleanup
|
||||||
@ -111,7 +112,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
const goToHistory = (path: string) =>
|
const goToHistory = (path: string) =>
|
||||||
{
|
{
|
||||||
navigate(path);
|
navigate(path);
|
||||||
}
|
};
|
||||||
|
|
||||||
function buildHistoryEntries()
|
function buildHistoryEntries()
|
||||||
{
|
{
|
||||||
@ -119,7 +120,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
const options = [] as any;
|
const options = [] as any;
|
||||||
history.entries.reverse().forEach((entry, index) =>
|
history.entries.reverse().forEach((entry, index) =>
|
||||||
options.push({label: entry.label, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
options.push({label: entry.label, id: index, key: index, path: entry.path, iconName: entry.iconName})
|
||||||
)
|
);
|
||||||
setHistory(options);
|
setHistory(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,12 +134,12 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
|
|
||||||
const handleAutocompleteOnChange = (event: any, value: any, reason: any, details: any) =>
|
const handleAutocompleteOnChange = (event: any, value: any, reason: any, details: any) =>
|
||||||
{
|
{
|
||||||
if(value)
|
if (value)
|
||||||
{
|
{
|
||||||
goToHistory(value.path);
|
goToHistory(value.path);
|
||||||
}
|
}
|
||||||
setAutocompleteValue(null);
|
setAutocompleteValue(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
const CustomPopper = function (props: any)
|
const CustomPopper = function (props: any)
|
||||||
{
|
{
|
||||||
@ -146,8 +147,8 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
{...props}
|
{...props}
|
||||||
style={{whiteSpace: "nowrap", width: "auto"}}
|
style={{whiteSpace: "nowrap", width: "auto"}}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
/>)
|
/>);
|
||||||
}
|
};
|
||||||
|
|
||||||
const renderHistory = () =>
|
const renderHistory = () =>
|
||||||
{
|
{
|
||||||
@ -166,7 +167,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
PopperComponent={CustomPopper}
|
PopperComponent={CustomPopper}
|
||||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
sx={recentlyViewedMenu}
|
sx={recentlyViewedMenu}
|
||||||
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" InputProps={{
|
renderInput={(params) => <TextField {...params} label="Recently Viewed Records" InputProps={{
|
||||||
...params.InputProps,
|
...params.InputProps,
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
@ -184,7 +185,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Styles for the navbar icons
|
// Styles for the navbar icons
|
||||||
const iconsStyle = ({
|
const iconsStyle = ({
|
||||||
@ -210,21 +211,34 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
const {pathToLabelMap} = useContext(QContext);
|
const {pathToLabelMap} = useContext(QContext);
|
||||||
const fullPathToLabel = (fullPath: string, route: string): string =>
|
const fullPathToLabel = (fullPath: string, route: string): string =>
|
||||||
{
|
{
|
||||||
if(fullPath.endsWith("/"))
|
if (fullPath.endsWith("/"))
|
||||||
{
|
{
|
||||||
fullPath = fullPath.replace(/\/+$/, "");
|
fullPath = fullPath.replace(/\/+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(pathToLabelMap && pathToLabelMap[fullPath])
|
if (pathToLabelMap && pathToLabelMap[fullPath])
|
||||||
{
|
{
|
||||||
return pathToLabelMap[fullPath];
|
return pathToLabelMap[fullPath];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (routeToLabel(route));
|
return (routeToLabel(route));
|
||||||
}
|
};
|
||||||
|
|
||||||
const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]);
|
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 (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position={absolute ? "absolute" : navbarType}
|
position={absolute ? "absolute" : navbarType}
|
||||||
@ -241,10 +255,15 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
||||||
</Box>
|
</Box>
|
||||||
{isMini ? null : (
|
{isMini ? null : (
|
||||||
<Box sx={(theme) => navbarRow(theme, {isMini})}>
|
<Box sx={(theme) => navbarRowRight(theme, {isMini})}>
|
||||||
<Box pr={0} mr={-2}>
|
<Box mt={"-0.25rem"} pb={"0.75rem"} pr={2} mr={-2} sx={{"& *": {cursor: "pointer !important"}}}>
|
||||||
{renderHistory()}
|
{renderHistory()}
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box mt={"-1rem"}>
|
||||||
|
<IconButton size="small" disableRipple color="inherit" onClick={() => setDotMenuOpen(true)}>
|
||||||
|
<Icon sx={iconsStyle} fontSize="small">search</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
74
src/qqq/components/misc/ErrorBoundary.tsx
Normal file
74
src/qqq/components/misc/ErrorBoundary.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {Component, ErrorInfo} from "react";
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
errorElement?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State
|
||||||
|
{
|
||||||
|
hasError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Component that you can wrap around other components that might throw an error,
|
||||||
|
** to give some isolation, rather than breaking a whole page.
|
||||||
|
** Credit: https://medium.com/@bobjunior542/how-to-use-error-boundaries-in-react-js-with-typescript-ee90ec814bf1
|
||||||
|
*******************************************************************************/
|
||||||
|
class ErrorBoundary extends Component<Props, State>
|
||||||
|
{
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
constructor(props: Props)
|
||||||
|
{
|
||||||
|
super(props);
|
||||||
|
this.state = {hasError: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo)
|
||||||
|
{
|
||||||
|
console.error("ErrorBoundary caught an error: ", error, errorInfo);
|
||||||
|
this.setState({hasError: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
*
|
||||||
|
***************************************************************************/
|
||||||
|
render()
|
||||||
|
{
|
||||||
|
if (this.state.hasError)
|
||||||
|
{
|
||||||
|
return this.props.errorElement ?? <span>(Error)</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
@ -358,7 +358,7 @@ export function GotoRecordButton(props: GotoRecordButtonProps): JSX.Element
|
|||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{
|
{
|
||||||
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto}>Go To...</Button>
|
props.buttonVisible && hasGotoFieldNames(props.tableMetaData) && <Button onClick={openGoto} sx={{whiteSpace: "nowrap"}}>Go To...</Button>
|
||||||
}
|
}
|
||||||
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} tableVariant={props.tableVariant} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} subHeader={props.subHeader} />
|
<GotoRecordDialog metaData={props.metaData} tableMetaData={props.tableMetaData} tableVariant={props.tableVariant} isOpen={gotoIsOpen} closeHandler={closeGoto} mayClose={props.mayClose} subHeader={props.subHeader} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent";
|
import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
|
import ErrorBoundary from "qqq/components/misc/ErrorBoundary";
|
||||||
import React, {useContext} from "react";
|
import React, {useContext} from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
@ -128,6 +129,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
|||||||
let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles);
|
let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles);
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
|
let errorContent = "Error rendering help content.";
|
||||||
if (helpHelpActive)
|
if (helpHelpActive)
|
||||||
{
|
{
|
||||||
if (!selectedHelpContent)
|
if (!selectedHelpContent)
|
||||||
@ -135,6 +137,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
|||||||
selectedHelpContent = new QHelpContent({content: ""});
|
selectedHelpContent = new QHelpContent({content: ""});
|
||||||
}
|
}
|
||||||
content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`;
|
content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`;
|
||||||
|
errorContent += ` [${helpContentKey ?? "?"}]`;
|
||||||
}
|
}
|
||||||
else if(selectedHelpContent)
|
else if(selectedHelpContent)
|
||||||
{
|
{
|
||||||
@ -148,7 +151,9 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
|||||||
{
|
{
|
||||||
return <Box display="inline" className="helpContent">
|
return <Box display="inline" className="helpContent">
|
||||||
{heading && <span className="header">{heading}</span>}
|
{heading && <span className="header">{heading}</span>}
|
||||||
{formatHelpContent(content, selectedHelpContent.format)}
|
<ErrorBoundary errorElement={<i>{errorContent}</i>}>
|
||||||
|
{formatHelpContent(content, selectedHelpContent.format)}
|
||||||
|
</ErrorBoundary>
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import Box from "@mui/material/Box";
|
import {Box} from "@mui/material";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {Theme} from "@mui/material/styles";
|
import {Theme} from "@mui/material/styles";
|
||||||
@ -76,12 +76,12 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "auto", maxHeight: "calc(100vh - 2rem)"}}>
|
<Card sx={{borderRadius: "0.75rem", position: "sticky", top: stickyTop, overflow: "hidden", maxHeight: "calc(100vh - 2rem)"}}>
|
||||||
<Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none"}}>
|
<Box component="ul" display="flex" flexDirection="column" p={2} m={0} sx={{listStyle: "none", overflow: "auto", height: "100%"}}>
|
||||||
{
|
{
|
||||||
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
|
sidebarEntries ? sidebarEntries.map((entry: SidebarEntry, key: number) => (
|
||||||
|
|
||||||
<HashLink key={`section-link-${entry.name}`} to={`#${entry.name}`}>
|
<Box key={`section-link-${entry.name}`} onClick={() => document.getElementById(entry.name).scrollIntoView()} sx={{cursor: "pointer"}}>
|
||||||
<Box key={`section-${entry.name}`} component="li" pt={key === 0 ? 0 : 1}>
|
<Box key={`section-${entry.name}`} component="li" pt={key === 0 ? 0 : 1}>
|
||||||
<MDTypography
|
<MDTypography
|
||||||
variant="button"
|
variant="button"
|
||||||
@ -112,7 +112,7 @@ function QRecordSidebar({tableSections, widgetMetaDataList, light, stickyTop}: P
|
|||||||
|
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
</HashLink>
|
</Box>
|
||||||
)) : null
|
)) : null
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -25,7 +25,8 @@ import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QT
|
|||||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {Alert, Box, Button} from "@mui/material";
|
import {Alert, Button} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import Dialog from "@mui/material/Dialog";
|
import Dialog from "@mui/material/Dialog";
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
@ -94,12 +95,12 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, tab
|
|||||||
|
|
||||||
const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext);
|
const {accentColor, accentColorLight, userId: currentUserId} = useContext(QContext);
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// this component is used by <RecordQuery> - but that component has different usages - //
|
// this component is used by <RecordQuery> - but that component has different usages - //
|
||||||
// e.g., the full-fledged query screen, but also, within other screens (e.g., a modal //
|
// e.g., the full-fledged query screen, but also, within other screens (e.g., a modal //
|
||||||
// under the ReportSetupWidget). So, there are some behaviors we only want when we're //
|
// under the FilterAndColumnsSetupWidget). So, there are some behaviors we only want when //
|
||||||
// on the full-fledged query screen, such as changing the URL with saved view ids. //
|
// we're on the full-fledged query screen, such as changing the URL with saved view ids. //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const isQueryScreen = queryScreenUsage == "queryScreen";
|
const isQueryScreen = queryScreenUsage == "queryScreen";
|
||||||
|
|
||||||
const openSavedViewsMenu = (event: any) => setSavedViewsMenu(event.currentTarget);
|
const openSavedViewsMenu = (event: any) => setSavedViewsMenu(event.currentTarget);
|
||||||
|
@ -79,6 +79,8 @@ interface BasicAndAdvancedQueryControlsProps
|
|||||||
|
|
||||||
queryScreenUsage: QueryScreenUsage;
|
queryScreenUsage: QueryScreenUsage;
|
||||||
|
|
||||||
|
allowVariables?: boolean;
|
||||||
|
|
||||||
mode: string;
|
mode: string;
|
||||||
setMode: (mode: string) => void;
|
setMode: (mode: string) => void;
|
||||||
}
|
}
|
||||||
@ -676,6 +678,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
|||||||
|
|
||||||
return (<QuickFilter
|
return (<QuickFilter
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
|
allowVariables={props.allowVariables}
|
||||||
fullFieldName={fieldName}
|
fullFieldName={fieldName}
|
||||||
tableMetaData={tableMetaData}
|
tableMetaData={tableMetaData}
|
||||||
updateCriteria={updateQuickCriteria}
|
updateCriteria={updateQuickCriteria}
|
||||||
@ -701,6 +704,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
|||||||
updateCriteria={updateQuickCriteria}
|
updateCriteria={updateQuickCriteria}
|
||||||
criteriaParam={getQuickCriteriaParam(fieldName)}
|
criteriaParam={getQuickCriteriaParam(fieldName)}
|
||||||
fieldMetaData={field}
|
fieldMetaData={field}
|
||||||
|
allowVariables={props.allowVariables}
|
||||||
defaultOperator={defaultOperator}
|
defaultOperator={defaultOperator}
|
||||||
queryScreenUsage={queryScreenUsage}
|
queryScreenUsage={queryScreenUsage}
|
||||||
handleRemoveQuickFilterField={handleRemoveQuickFilterField} />);
|
handleRemoveQuickFilterField={handleRemoveQuickFilterField} />);
|
||||||
|
@ -179,6 +179,7 @@ export const CustomFilterPanel = forwardRef<any, GridFilterPanelProps>(
|
|||||||
updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)}
|
updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)}
|
||||||
removeCriteria={() => removeCriteria(index)}
|
removeCriteria={() => removeCriteria(index)}
|
||||||
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
|
updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)}
|
||||||
|
allowVariables={props.allowVariables}
|
||||||
queryScreenUsage={props.queryScreenUsage}
|
queryScreenUsage={props.queryScreenUsage}
|
||||||
/>
|
/>
|
||||||
{/*JSON.stringify(criteria)*/}
|
{/*JSON.stringify(criteria)*/}
|
||||||
|
@ -199,6 +199,7 @@ interface FilterCriteriaRowProps
|
|||||||
removeCriteria: () => void;
|
removeCriteria: () => void;
|
||||||
updateBooleanOperator: (newValue: string) => void;
|
updateBooleanOperator: (newValue: string) => void;
|
||||||
queryScreenUsage?: QueryScreenUsage;
|
queryScreenUsage?: QueryScreenUsage;
|
||||||
|
allowVariables?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterCriteriaRow.defaultProps =
|
FilterCriteriaRow.defaultProps =
|
||||||
@ -267,7 +268,7 @@ export function validateCriteria(criteria: QFilterCriteria, operatorSelectedValu
|
|||||||
return {criteriaIsValid, criteriaStatusTooltip};
|
return {criteriaIsValid, criteriaStatusTooltip};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage}: FilterCriteriaRowProps): JSX.Element
|
export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator, queryScreenUsage, allowVariables}: FilterCriteriaRowProps): JSX.Element
|
||||||
{
|
{
|
||||||
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
|
// console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`);
|
||||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
|
const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption);
|
||||||
@ -516,6 +517,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
|
|||||||
table={fieldTable}
|
table={fieldTable}
|
||||||
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
||||||
queryScreenUsage={queryScreenUsage}
|
queryScreenUsage={queryScreenUsage}
|
||||||
|
allowVariables={allowVariables}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="inline-block">
|
<Box display="inline-block">
|
||||||
|
@ -30,6 +30,7 @@ import Icon from "@mui/material/Icon";
|
|||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import InputAdornment from "@mui/material/InputAdornment/InputAdornment";
|
import InputAdornment from "@mui/material/InputAdornment/InputAdornment";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
|
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||||
import AssignFilterVariable from "qqq/components/query/AssignFilterVariable";
|
import AssignFilterVariable from "qqq/components/query/AssignFilterVariable";
|
||||||
import CriteriaDateField, {NoWrapTooltip} from "qqq/components/query/CriteriaDateField";
|
import CriteriaDateField, {NoWrapTooltip} from "qqq/components/query/CriteriaDateField";
|
||||||
@ -39,7 +40,8 @@ import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster";
|
|||||||
import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow";
|
import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow";
|
||||||
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
|
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
import React, {SyntheticEvent, useReducer, useState} from "react";
|
import React, {SyntheticEvent, useReducer} from "react";
|
||||||
|
import {flushSync} from "react-dom";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -50,6 +52,7 @@ interface Props
|
|||||||
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
|
valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void;
|
||||||
initiallyOpenMultiValuePvs?: boolean;
|
initiallyOpenMultiValuePvs?: boolean;
|
||||||
queryScreenUsage?: QueryScreenUsage;
|
queryScreenUsage?: QueryScreenUsage;
|
||||||
|
allowVariables?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterCriteriaRowValues.defaultProps =
|
FilterCriteriaRowValues.defaultProps =
|
||||||
@ -57,6 +60,10 @@ FilterCriteriaRowValues.defaultProps =
|
|||||||
initiallyOpenMultiValuePvs: false
|
initiallyOpenMultiValuePvs: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* get the type to use for an <input> from a QFieldMetaData
|
||||||
|
***************************************************************************/
|
||||||
export const getTypeForTextField = (field: QFieldMetaData): string =>
|
export const getTypeForTextField = (field: QFieldMetaData): string =>
|
||||||
{
|
{
|
||||||
let type = "search";
|
let type = "search";
|
||||||
@ -77,10 +84,15 @@ export const getTypeForTextField = (field: QFieldMetaData): string =>
|
|||||||
return (type);
|
return (type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* Make an <input type=text> (actually, might be a different type, but that's
|
||||||
|
* the gist of it), for a field.
|
||||||
|
***************************************************************************/
|
||||||
export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-", allowVariables = false) =>
|
export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-", allowVariables = false) =>
|
||||||
{
|
{
|
||||||
const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type;
|
const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type;
|
||||||
|
const inputId = `${idPrefix}${criteria.id}`;
|
||||||
let type = getTypeForTextField(field);
|
let type = getTypeForTextField(field);
|
||||||
const inputLabelProps: any = {};
|
const inputLabelProps: any = {};
|
||||||
|
|
||||||
@ -95,10 +107,13 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
|||||||
value = ValueUtils.formatDateTimeValueForForm(value);
|
value = ValueUtils.formatDateTimeValueForForm(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* Event handler for the clear 'x'.
|
||||||
|
***************************************************************************/
|
||||||
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
|
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
|
||||||
{
|
{
|
||||||
valueChangeHandler(event, index, "");
|
valueChangeHandler(event, index, "");
|
||||||
document.getElementById(`${idPrefix}${criteria.id}`).focus();
|
document.getElementById(inputId).focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -119,6 +134,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* make a version of the text field for when the criteria's value is set to
|
||||||
|
* be a "variable"
|
||||||
|
***************************************************************************/
|
||||||
const makeFilterVariableTextField = (expression: FilterVariableExpression, valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
|
const makeFilterVariableTextField = (expression: FilterVariableExpression, valueIndex: number = 0, label = "Value", idPrefix = "value-") =>
|
||||||
{
|
{
|
||||||
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
|
const clearValue = (event: React.MouseEvent<HTMLAnchorElement> | React.MouseEvent<HTMLButtonElement>, index: number) =>
|
||||||
@ -148,6 +167,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
|||||||
/></NoWrapTooltip>;
|
/></NoWrapTooltip>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// set up an 'x' icon as an end-adornment, to clear value from the field //
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
const inputProps: any = {};
|
const inputProps: any = {};
|
||||||
inputProps.endAdornment = (
|
inputProps.endAdornment = (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
@ -157,18 +180,64 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
|||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* onChange event handler. deals with, if the field has a to upper/lower
|
||||||
|
* case rule on it, to apply that transform, and adjust the cursor.
|
||||||
|
* See: https://giacomocerquone.com/blog/keep-input-cursor-still
|
||||||
|
***************************************************************************/
|
||||||
|
function onChange(event: any)
|
||||||
|
{
|
||||||
|
const beforeStart = event.target.selectionStart;
|
||||||
|
const beforeEnd = event.target.selectionEnd;
|
||||||
|
|
||||||
|
let isToUpperCase = DynamicFormUtils.isToUpperCase(field);
|
||||||
|
let isToLowerCase = DynamicFormUtils.isToLowerCase(field);
|
||||||
|
|
||||||
|
if (isToUpperCase || isToLowerCase)
|
||||||
|
{
|
||||||
|
flushSync(() =>
|
||||||
|
{
|
||||||
|
let newValue = event.currentTarget.value;
|
||||||
|
|
||||||
|
if (isToUpperCase)
|
||||||
|
{
|
||||||
|
newValue = newValue.toUpperCase();
|
||||||
|
}
|
||||||
|
if (isToLowerCase)
|
||||||
|
{
|
||||||
|
newValue = newValue.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
event.currentTarget.value = newValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
if (input)
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
input.setSelectionRange(beforeStart, beforeEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
valueChangeHandler(event, valueIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////
|
||||||
|
// return the element //
|
||||||
|
////////////////////////
|
||||||
return <Box sx={{margin: 0, padding: 0, display: "flex"}}>
|
return <Box sx={{margin: 0, padding: 0, display: "flex"}}>
|
||||||
{
|
{
|
||||||
isExpression ? (
|
isExpression ? (
|
||||||
makeFilterVariableTextField(criteria.values[valueIndex], valueIndex, label, idPrefix)
|
makeFilterVariableTextField(criteria.values[valueIndex], valueIndex, label, idPrefix)
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
id={`${idPrefix}${criteria.id}`}
|
id={inputId}
|
||||||
label={label}
|
label={label}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
type={type}
|
type={type}
|
||||||
onChange={(event) => valueChangeHandler(event, valueIndex)}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
value={value}
|
value={value}
|
||||||
InputLabelProps={inputLabelProps}
|
InputLabelProps={inputLabelProps}
|
||||||
@ -187,16 +256,23 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs, queryScreenUsage}: Props): JSX.Element
|
/***************************************************************************
|
||||||
|
* Component that is the "values" portion of a FilterCriteria Row in the
|
||||||
|
* advanced query filter editor.
|
||||||
|
***************************************************************************/
|
||||||
|
function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs, queryScreenUsage, allowVariables}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
const [allowVariables, setAllowVariables] = useState(queryScreenUsage == "reportSetup");
|
|
||||||
|
|
||||||
if (!operatorOption)
|
if (!operatorOption)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
* Callback for the Save button from the paste-values modal
|
||||||
|
***************************************************************************/
|
||||||
function saveNewPasterValues(newValues: any[])
|
function saveNewPasterValues(newValues: any[])
|
||||||
{
|
{
|
||||||
if (criteria.values)
|
if (criteria.values)
|
||||||
@ -222,6 +298,9 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
|
|
||||||
const isExpression = criteria.values && criteria.values[0] && criteria.values[0].type;
|
const isExpression = criteria.values && criteria.values[0] && criteria.values[0].type;
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// render different form element9s) based on operator option's "value mode" //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
switch (operatorOption.valueMode)
|
switch (operatorOption.valueMode)
|
||||||
{
|
{
|
||||||
case ValueMode.NONE:
|
case ValueMode.NONE:
|
||||||
@ -320,7 +399,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
initialValues = criteria.values;
|
initialValues = criteria.values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <Box mb={-1.5}>
|
return <Box>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={table.name}
|
tableName={table.name}
|
||||||
fieldName={field.name}
|
fieldName={field.name}
|
||||||
|
@ -52,6 +52,7 @@ interface QuickFilterProps
|
|||||||
defaultOperator?: QCriteriaOperator;
|
defaultOperator?: QCriteriaOperator;
|
||||||
handleRemoveQuickFilterField?: (fieldName: string) => void;
|
handleRemoveQuickFilterField?: (fieldName: string) => void;
|
||||||
queryScreenUsage?: QueryScreenUsage;
|
queryScreenUsage?: QueryScreenUsage;
|
||||||
|
allowVariables?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
QuickFilter.defaultProps =
|
QuickFilter.defaultProps =
|
||||||
@ -141,7 +142,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
|
|||||||
** Component to render a QuickFilter - that is - a button, with a Menu under it,
|
** Component to render a QuickFilter - that is - a button, with a Menu under it,
|
||||||
** with Operator and Value controls.
|
** with Operator and Value controls.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField, queryScreenUsage}: QuickFilterProps): JSX.Element
|
export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData, criteriaParam, updateCriteria, defaultOperator, handleRemoveQuickFilterField, queryScreenUsage, allowVariables}: QuickFilterProps): JSX.Element
|
||||||
{
|
{
|
||||||
const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : [];
|
const operatorOptions = fieldMetaData ? getOperatorOptions(tableMetaData, fullFieldName) : [];
|
||||||
const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName);
|
const [_, tableForField] = TableUtils.getFieldAndTable(tableMetaData, fullFieldName);
|
||||||
@ -549,6 +550,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
criteria={criteria}
|
criteria={criteria}
|
||||||
field={fieldMetaData}
|
field={fieldMetaData}
|
||||||
table={tableForField}
|
table={tableForField}
|
||||||
|
allowVariables={allowVariables}
|
||||||
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
valueChangeHandler={(event, valueIndex, newValue) => handleValueChange(event, valueIndex, newValue)}
|
||||||
initiallyOpenMultiValuePvs={true} // todo - maybe not?
|
initiallyOpenMultiValuePvs={true} // todo - maybe not?
|
||||||
/>
|
/>
|
||||||
|
@ -22,16 +22,19 @@
|
|||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {Box, Skeleton} from "@mui/material";
|
import {Box, Skeleton} from "@mui/material";
|
||||||
import React from "react";
|
import parse from "html-react-parser";
|
||||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
|
||||||
interface CompositeData
|
export interface CompositeData
|
||||||
{
|
{
|
||||||
blocks: BlockData[];
|
blocks: BlockData[];
|
||||||
styleOverrides?: any;
|
styleOverrides?: any;
|
||||||
layout?: string
|
layout?: string;
|
||||||
|
overlayHtml?: string;
|
||||||
|
overlayStyleOverrides?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -57,7 +60,14 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
|||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
let layout = data?.layout;
|
let layout = data?.layout;
|
||||||
let boxStyle: any = {};
|
let boxStyle: any = {};
|
||||||
if (layout == "FLEX_ROW_WRAPPED")
|
if (layout == "FLEX_COLUMN")
|
||||||
|
{
|
||||||
|
boxStyle.display = "flex";
|
||||||
|
boxStyle.flexDirection = "column";
|
||||||
|
boxStyle.flexWrap = "wrap";
|
||||||
|
boxStyle.gap = "0.5rem";
|
||||||
|
}
|
||||||
|
else if (layout == "FLEX_ROW_WRAPPED")
|
||||||
{
|
{
|
||||||
boxStyle.display = "flex";
|
boxStyle.display = "flex";
|
||||||
boxStyle.flexDirection = "row";
|
boxStyle.flexDirection = "row";
|
||||||
@ -68,7 +78,7 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
|||||||
{
|
{
|
||||||
boxStyle.display = "flex";
|
boxStyle.display = "flex";
|
||||||
boxStyle.flexDirection = "row";
|
boxStyle.flexDirection = "row";
|
||||||
boxStyle.justifyContent = "space-between"
|
boxStyle.justifyContent = "space-between";
|
||||||
boxStyle.gap = "0.25rem";
|
boxStyle.gap = "0.25rem";
|
||||||
}
|
}
|
||||||
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
||||||
@ -90,20 +100,34 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
|||||||
boxStyle.borderRadius = "0.5rem";
|
boxStyle.borderRadius = "0.5rem";
|
||||||
boxStyle.background = "#FFFFFF";
|
boxStyle.background = "#FFFFFF";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.styleOverrides)
|
if (data?.styleOverrides)
|
||||||
{
|
{
|
||||||
boxStyle = {...boxStyle, ...data.styleOverrides};
|
boxStyle = {...boxStyle, ...data.styleOverrides};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<Box sx={boxStyle} className="compositeWidget">
|
let overlayStyle: any = {};
|
||||||
{
|
|
||||||
data.blocks.map((block: BlockData, index) => (
|
if (data?.overlayStyleOverrides)
|
||||||
<React.Fragment key={index}>
|
{
|
||||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
|
overlayStyle = {...overlayStyle, ...data.overlayStyleOverrides};
|
||||||
</React.Fragment>
|
}
|
||||||
))
|
|
||||||
}
|
return (
|
||||||
</Box>);
|
<>
|
||||||
|
{
|
||||||
|
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}>
|
||||||
|
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,17 +40,17 @@ import DataBagViewer from "qqq/components/widgets/misc/DataBagViewer";
|
|||||||
import DividerWidget from "qqq/components/widgets/misc/Divider";
|
import DividerWidget from "qqq/components/widgets/misc/Divider";
|
||||||
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
|
import DynamicFormWidget from "qqq/components/widgets/misc/DynamicFormWidget";
|
||||||
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget";
|
import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidget";
|
||||||
|
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
|
||||||
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
|
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
|
||||||
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
|
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
|
||||||
import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget";
|
import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget";
|
||||||
import ReportSetupWidget from "qqq/components/widgets/misc/ReportSetupWidget";
|
|
||||||
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
|
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
|
||||||
import StepperCard from "qqq/components/widgets/misc/StepperCard";
|
import StepperCard from "qqq/components/widgets/misc/StepperCard";
|
||||||
import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
||||||
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
||||||
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
||||||
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
||||||
import Widget, {HeaderIcon, LabelComponent, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget";
|
import Widget, {HeaderIcon, LabelComponent, WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT, WidgetData} from "qqq/components/widgets/Widget";
|
||||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
@ -258,11 +258,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
** helper function, to convert values from a QRecord values map to a regular old
|
** helper function, to convert values from a QRecord values map to a regular old
|
||||||
** js object
|
** js object
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function convertQRecordValuesFromMapToObject(record: QRecord): {[name: string]: any}
|
function convertQRecordValuesFromMapToObject(record: QRecord): { [name: string]: any }
|
||||||
{
|
{
|
||||||
const rs: {[name: string]: any} = {};
|
const rs: { [name: string]: any } = {};
|
||||||
|
|
||||||
if(record && record.values)
|
if (record && record.values)
|
||||||
{
|
{
|
||||||
record.values.forEach((value, key) => rs[key] = value);
|
record.values.forEach((value, key) => rs[key] = value);
|
||||||
}
|
}
|
||||||
@ -293,7 +293,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={`${widgetMetaData.name}-${i}`} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%"}}>
|
<Box key={`${widgetMetaData.name}-${i}`} sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px", width: "100%", height: "100%", flexDirection: widgetMetaData.type == "multiTable" ? "column" : "row"}}>
|
||||||
{
|
{
|
||||||
haveLoadedParams && widgetMetaData.type === "parentWidget" && (
|
haveLoadedParams && widgetMetaData.type === "parentWidget" && (
|
||||||
<ParentWidget
|
<ParentWidget
|
||||||
@ -343,6 +343,20 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
widgetMetaData.type === "multiTable" && (
|
||||||
|
widgetData[i]?.tableDataList?.map((tableData: WidgetData, index: number) =>
|
||||||
|
<Box pb={3} key={`${widgetMetaData.type}-${index}`}>
|
||||||
|
<TableWidget
|
||||||
|
widgetMetaData={widgetMetaData}
|
||||||
|
widgetData={tableData}
|
||||||
|
reloadWidgetCallback={(data) => reloadWidget(i, data)}
|
||||||
|
isChild={areChildren}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "stackedBarChart" && (
|
widgetMetaData.type === "stackedBarChart" && (
|
||||||
<Widget
|
<Widget
|
||||||
@ -584,17 +598,19 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "reportSetup" && (
|
widgetMetaData.type === "filterAndColumnsSetup" && (
|
||||||
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
widgetData && widgetData[i] &&
|
||||||
<ReportSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
|
<FilterAndColumnsSetupWidget isEditable={false} widgetMetaData={widgetMetaData} widgetData={widgetData[i]} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
|
||||||
{}} />
|
{
|
||||||
|
}} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "pivotTableSetup" && (
|
widgetMetaData.type === "pivotTableSetup" && (
|
||||||
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
||||||
<PivotTableSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
|
<PivotTableSetupWidget isEditable={false} widgetMetaData={widgetMetaData} recordValues={convertQRecordValuesFromMapToObject(record)} onSaveCallback={() =>
|
||||||
{}} />
|
{
|
||||||
|
}} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@ -622,8 +638,28 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
|
|
||||||
if (!omitWrappingGridContainer)
|
if (!omitWrappingGridContainer)
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
const gridProps: {[key: string]: any} = {};
|
||||||
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
|
||||||
|
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}
|
{renderedWidget}
|
||||||
</Grid>);
|
</Grid>);
|
||||||
}
|
}
|
||||||
|
@ -21,14 +21,16 @@
|
|||||||
|
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import React from "react";
|
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import {WidgetData} from "qqq/components/widgets/Widget";
|
import {WidgetData} from "qqq/components/widgets/Widget";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
import React from "react";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Utility class used by Widgets
|
** Utility class used by Widgets
|
||||||
@ -51,6 +53,17 @@ export class WidgetUtils
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static generateLabelLink = (linkText: string, linkURL: string): JSX.Element =>
|
||||||
|
{
|
||||||
|
return (<Box key={1} fontSize="1rem" pl={1} display="inline" position="relative">
|
||||||
|
(<Link to={linkURL}>{linkText}</Link>)
|
||||||
|
</Box>);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -97,4 +110,4 @@ export class WidgetUtils
|
|||||||
return (fileName);
|
return (fileName);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -21,18 +21,19 @@
|
|||||||
|
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {Tooltip} from "@mui/material";
|
import {Box, Tooltip} from "@mui/material";
|
||||||
import React, {ReactElement, useContext} from "react";
|
|
||||||
import {Link} from "react-router-dom";
|
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||||
import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
|
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
|
interface BlockElementWrapperProps
|
||||||
{
|
{
|
||||||
data: BlockData;
|
data: BlockData;
|
||||||
metaData: QWidgetMetaData;
|
metaData: QWidgetMetaData;
|
||||||
slot: string
|
slot: string;
|
||||||
linkProps?: any;
|
linkProps?: any;
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
}
|
}
|
||||||
@ -47,16 +48,16 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
|||||||
let link: BlockLink;
|
let link: BlockLink;
|
||||||
let tooltip: BlockTooltip;
|
let tooltip: BlockTooltip;
|
||||||
|
|
||||||
if(slot)
|
if (slot)
|
||||||
{
|
{
|
||||||
link = data.linkMap && data.linkMap[slot.toUpperCase()];
|
link = data.linkMap && data.linkMap[slot.toUpperCase()];
|
||||||
if(!link)
|
if (!link)
|
||||||
{
|
{
|
||||||
link = data.link;
|
link = data.link;
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
|
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
|
||||||
if(!tooltip)
|
if (!tooltip)
|
||||||
{
|
{
|
||||||
tooltip = data.tooltip;
|
tooltip = data.tooltip;
|
||||||
}
|
}
|
||||||
@ -67,9 +68,9 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
|||||||
tooltip = data.tooltip;
|
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: //
|
// 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 key = data.blockId ? `${data.blockId},${slot}` : slot;
|
||||||
const showHelp = helpHelpActive || hasHelpContent(metaData?.helpContent?.get(key), helpRoles);
|
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}`} />;
|
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;
|
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
|
// @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);
|
return (rs);
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
|
import {CompositeData} from "qqq/components/widgets/CompositeWidget";
|
||||||
|
|
||||||
|
|
||||||
export interface BlockData
|
export interface BlockData
|
||||||
@ -29,8 +30,8 @@ export interface BlockData
|
|||||||
|
|
||||||
tooltip?: BlockTooltip;
|
tooltip?: BlockTooltip;
|
||||||
link?: BlockLink;
|
link?: BlockLink;
|
||||||
tooltipMap?: {[slot: string]: BlockTooltip};
|
tooltipMap?: { [slot: string]: BlockTooltip };
|
||||||
linkMap?: {[slot: string]: BlockLink};
|
linkMap?: { [slot: string]: BlockLink };
|
||||||
|
|
||||||
values: any;
|
values: any;
|
||||||
styles?: any;
|
styles?: any;
|
||||||
@ -39,6 +40,7 @@ export interface BlockData
|
|||||||
|
|
||||||
export interface BlockTooltip
|
export interface BlockTooltip
|
||||||
{
|
{
|
||||||
|
blockData?: CompositeData;
|
||||||
title: string | JSX.Element;
|
title: string | JSX.Element;
|
||||||
placement: string;
|
placement: string;
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ export default function NumberIconBadgeBlock({widgetMetaData, data}: StandardBlo
|
|||||||
{
|
{
|
||||||
data.values.iconName &&
|
data.values.iconName &&
|
||||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="icon">
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="icon">
|
||||||
<Icon style={{color: data.styles.color, fontSize: "1rem", position: "relative", top: "3px"}}>{data.values.iconName}</Icon>
|
<Icon style={{color: data.styles.color, fontSize: "1rem", marginLeft: "2px", position: "relative", top: "4px"}}>{data.values.iconName}</Icon>
|
||||||
</BlockElementWrapper>
|
</BlockElementWrapper>
|
||||||
}
|
}
|
||||||
</div>);
|
</div>);
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
|
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
||||||
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {Alert, Collapse} from "@mui/material";
|
import {Alert, Collapse} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
@ -42,15 +44,16 @@ import Client from "qqq/utils/qqq/Client";
|
|||||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||||
|
|
||||||
interface ReportSetupWidgetProps
|
interface FilterAndColumnsSetupWidgetProps
|
||||||
{
|
{
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData;
|
||||||
|
widgetData: any;
|
||||||
recordValues: { [name: string]: any };
|
recordValues: { [name: string]: any };
|
||||||
onSaveCallback?: (values: { [name: string]: any }) => void;
|
onSaveCallback?: (values: { [name: string]: any }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportSetupWidget.defaultProps = {
|
FilterAndColumnsSetupWidget.defaultProps = {
|
||||||
onSaveCallback: null
|
onSaveCallback: null
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -80,9 +83,10 @@ const qController = Client.getInstance();
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Component for editing the main setup of a report - that is: filter & columns
|
** Component for editing the main setup of a report - that is: filter & columns
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export default function ReportSetupWidget({isEditable, widgetMetaData, recordValues, onSaveCallback}: ReportSetupWidgetProps): JSX.Element
|
export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, widgetData, recordValues, onSaveCallback}: FilterAndColumnsSetupWidgetProps): JSX.Element
|
||||||
{
|
{
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
||||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
|
|
||||||
const [alertContent, setAlertContent] = useState(null as string);
|
const [alertContent, setAlertContent] = useState(null as string);
|
||||||
@ -101,15 +105,42 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// load values from record //
|
// load values from record //
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
|
let columns: QQueryColumns = null;
|
||||||
let usingDefaultEmptyFilter = false;
|
let usingDefaultEmptyFilter = false;
|
||||||
|
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
|
||||||
|
const defaultFilterFields = widgetData?.filterDefaultFieldNames;
|
||||||
if (!queryFilter)
|
if (!queryFilter)
|
||||||
{
|
{
|
||||||
queryFilter = new QQueryFilter();
|
queryFilter = new QQueryFilter();
|
||||||
usingDefaultEmptyFilter = true;
|
if (defaultFilterFields?.length == 0)
|
||||||
|
{
|
||||||
|
usingDefaultEmptyFilter = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
queryFilter = Object.assign(new QQueryFilter(), queryFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there are default fields from which a query should be seeded, add/update the filter with them //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (defaultFilterFields?.length > 0)
|
||||||
|
{
|
||||||
|
defaultFilterFields.forEach((fieldName: string) =>
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if a value for the default field exists, remove the criteria for it in our query first //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
queryFilter.criteria = queryFilter.criteria?.filter(c => c.fieldName != fieldName);
|
||||||
|
|
||||||
|
if (recordValues[fieldName])
|
||||||
|
{
|
||||||
|
queryFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, [recordValues[fieldName]]));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let columns: QQueryColumns = null;
|
|
||||||
if (recordValues["columnsJson"])
|
if (recordValues["columnsJson"])
|
||||||
{
|
{
|
||||||
columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]);
|
columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]);
|
||||||
@ -120,11 +151,20 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if a default table name specified, use it, otherwise use it from the record values //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let tableName = widgetData?.tableName;
|
||||||
|
if (!tableName && recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
|
||||||
|
{
|
||||||
|
tableName = recordValues["tableName"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableName)
|
||||||
{
|
{
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const tableMetaData = await qController.loadTableMetaData(recordValues["tableName"]);
|
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||||
setTableMetaData(tableMetaData);
|
setTableMetaData(tableMetaData);
|
||||||
|
|
||||||
const queryFilterForFrontend = Object.assign({}, queryFilter);
|
const queryFilterForFrontend = Object.assign({}, queryFilter);
|
||||||
@ -132,7 +172,7 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
setFrontendQueryFilter(queryFilterForFrontend);
|
setFrontendQueryFilter(queryFilterForFrontend);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
}, [recordValues]);
|
}, [JSON.stringify(recordValues)]);
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -140,8 +180,27 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function openEditor()
|
function openEditor()
|
||||||
{
|
{
|
||||||
|
let missingRequiredFields = [] as string[];
|
||||||
|
widgetData?.filterDefaultFieldNames?.forEach((fieldName: string) =>
|
||||||
|
{
|
||||||
|
if (!recordValues[fieldName])
|
||||||
|
{
|
||||||
|
missingRequiredFields.push(tableMetaData.fields.get(fieldName).label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
// display an alert and return if any required fields are missing //
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
if (missingRequiredFields.length > 0)
|
||||||
|
{
|
||||||
|
setAlertContent("The following fields must first be selected to edit the filter: '" + missingRequiredFields.join(", ") + "'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (recordValues["tableName"])
|
if (recordValues["tableName"])
|
||||||
{
|
{
|
||||||
|
setAlertContent(null);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -272,7 +331,14 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
const labelAdditionalElementsRight: JSX.Element[] = [];
|
const labelAdditionalElementsRight: JSX.Element[] = [];
|
||||||
if (isEditable)
|
if (isEditable)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsRight.push(<HeaderLinkButtonComponent key="filterAndColumnsHeader" label="Edit Filters and Columns" onClickCallback={openEditor} disabled={tableMetaData == null} disabledTooltip={selectTableFirstTooltipTitle} />);
|
if (!hideColumns)
|
||||||
|
{
|
||||||
|
labelAdditionalElementsRight.push(<HeaderLinkButtonComponent key="filterAndColumnsHeader" label="Edit Filters and Columns" onClickCallback={openEditor} disabled={tableMetaData == null} disabledTooltip={selectTableFirstTooltipTitle} />);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
labelAdditionalElementsRight.push(<HeaderLinkButtonComponent key="filterAndColumnsHeader" label="Edit Filters" onClickCallback={openEditor} disabled={tableMetaData == null} disabledTooltip={selectTableFirstTooltipTitle} />);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -306,34 +372,36 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!isEditable && <Box color={colors.gray.main}>Your report has no filters.</Box>
|
!isEditable && <Box color={colors.gray.main}>No filters are configured.</Box>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
<Box pt="1rem">
|
{!hideColumns && (
|
||||||
<h5>Columns</h5>
|
<Box pt="1rem">
|
||||||
<Box display="flex" flexWrap="wrap" fontSize="1rem">
|
<h5>Columns</h5>
|
||||||
{
|
<Box display="flex" flexWrap="wrap" fontSize="1rem">
|
||||||
mayShowColumnsPreview() &&
|
{
|
||||||
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
|
mayShowColumnsPreview() &&
|
||||||
}
|
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
|
||||||
{
|
}
|
||||||
!mayShowColumnsPreview() &&
|
{
|
||||||
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
|
!mayShowColumnsPreview() &&
|
||||||
{
|
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
|
||||||
isEditable &&
|
{
|
||||||
<Tooltip title={selectTableFirstTooltipTitle}>
|
isEditable &&
|
||||||
<span><Button disabled={!recordValues["tableName"]} sx={unborderedButtonSX} onClick={openEditor}>+ Add Columns</Button></span>
|
<Tooltip title={selectTableFirstTooltipTitle}>
|
||||||
</Tooltip>
|
<span><Button disabled={!recordValues["tableName"]} sx={unborderedButtonSX} onClick={openEditor}>+ Add Columns</Button></span>
|
||||||
}
|
</Tooltip>
|
||||||
{
|
}
|
||||||
!isEditable && <Box color={colors.gray.main}>Your report has no columns.</Box>
|
{
|
||||||
}
|
!isEditable && <Box color={colors.gray.main}>No columns are selected.</Box>
|
||||||
</Box>
|
}
|
||||||
}
|
</Box>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
{
|
{
|
||||||
modalOpen &&
|
modalOpen &&
|
||||||
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
|
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
|
||||||
@ -349,6 +417,7 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
tableMetaData && <RecordQuery
|
tableMetaData && <RecordQuery
|
||||||
|
allowVariables={widgetData?.allowVariables}
|
||||||
ref={recordQueryRef}
|
ref={recordQueryRef}
|
||||||
table={tableMetaData}
|
table={tableMetaData}
|
||||||
usage="reportSetup"
|
usage="reportSetup"
|
@ -39,9 +39,9 @@ import colors from "qqq/assets/theme/base/colors";
|
|||||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
|
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
|
||||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||||
|
import {buttonSX, unborderedButtonSX} from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
|
||||||
import {PivotTableGroupByElement} from "qqq/components/widgets/misc/PivotTableGroupByElement";
|
import {PivotTableGroupByElement} from "qqq/components/widgets/misc/PivotTableGroupByElement";
|
||||||
import {PivotTableValueElement} from "qqq/components/widgets/misc/PivotTableValueElement";
|
import {PivotTableValueElement} from "qqq/components/widgets/misc/PivotTableValueElement";
|
||||||
import {buttonSX, unborderedButtonSX} from "qqq/components/widgets/misc/ReportSetupWidget";
|
|
||||||
import Widget, {HeaderToggleComponent} from "qqq/components/widgets/Widget";
|
import Widget, {HeaderToggleComponent} from "qqq/components/widgets/Widget";
|
||||||
import {PivotObjectKey, PivotTableDefinition, PivotTableFunction, pivotTableFunctionLabels, PivotTableGroupBy, PivotTableValue} from "qqq/models/misc/PivotTableDefinitionModels";
|
import {PivotObjectKey, PivotTableDefinition, PivotTableFunction, pivotTableFunctionLabels, PivotTableGroupBy, PivotTableValue} from "qqq/models/misc/PivotTableDefinitionModels";
|
||||||
import QQueryColumns from "qqq/models/query/QQueryColumns";
|
import QQueryColumns from "qqq/models/query/QQueryColumns";
|
||||||
|
@ -40,14 +40,14 @@ import {Link, useNavigate} from "react-router-dom";
|
|||||||
export interface ChildRecordListData extends WidgetData
|
export interface ChildRecordListData extends WidgetData
|
||||||
{
|
{
|
||||||
title: string;
|
title: string;
|
||||||
queryOutput: {records: {values: any}[]}
|
queryOutput: { records: { values: any }[] };
|
||||||
childTableMetaData: QTableMetaData;
|
childTableMetaData: QTableMetaData;
|
||||||
tablePath: string;
|
tablePath: string;
|
||||||
viewAllLink: string;
|
viewAllLink: string;
|
||||||
totalRows: number;
|
totalRows: number;
|
||||||
canAddChildRecord: boolean;
|
canAddChildRecord: boolean;
|
||||||
defaultValuesForNewChildRecords: {[fieldName: string]: any};
|
defaultValuesForNewChildRecords: { [fieldName: string]: any };
|
||||||
disabledFieldsForNewChildRecords: {[fieldName: string]: any};
|
disabledFieldsForNewChildRecords: { [fieldName: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
@ -75,9 +75,9 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
{
|
{
|
||||||
const instance = useRef({timer: null});
|
const instance = useRef({timer: null});
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
const [records, setRecords] = useState([] as QRecord[])
|
const [records, setRecords] = useState([] as QRecord[]);
|
||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
const [allColumns, setAllColumns] = useState([])
|
const [allColumns, setAllColumns] = useState([]);
|
||||||
const [csv, setCsv] = useState(null as string);
|
const [csv, setCsv] = useState(null as string);
|
||||||
const [fileName, setFileName] = useState(null as string);
|
const [fileName, setFileName] = useState(null as string);
|
||||||
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
const [gridMouseDownX, setGridMouseDownX] = useState(0);
|
||||||
@ -110,20 +110,20 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
// capture all-columns to use for the export (before we might splice some away from the on-screen display) //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const allColumns = [... columns];
|
const allColumns = [...columns];
|
||||||
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
setAllColumns(JSON.parse(JSON.stringify(columns)));
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
// do not not show the foreign-key column of the parent table //
|
// do not not show the foreign-key column of the parent table //
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
if(data.defaultValuesForNewChildRecords)
|
if (data.defaultValuesForNewChildRecords)
|
||||||
{
|
{
|
||||||
for (let i = 0; i < columns.length; i++)
|
for (let i = 0; i < columns.length; i++)
|
||||||
{
|
{
|
||||||
if(data.defaultValuesForNewChildRecords[columns[i].field])
|
if (data.defaultValuesForNewChildRecords[columns[i].field])
|
||||||
{
|
{
|
||||||
columns.splice(i, 1);
|
columns.splice(i, 1);
|
||||||
i--
|
i--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +131,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
////////////////////////////////////
|
////////////////////////////////////
|
||||||
// add actions cell, if available //
|
// add actions cell, if available //
|
||||||
////////////////////////////////////
|
////////////////////////////////////
|
||||||
if(allowRecordEdit || allowRecordDelete)
|
if (allowRecordEdit || allowRecordDelete)
|
||||||
{
|
{
|
||||||
columns.unshift({
|
columns.unshift({
|
||||||
field: "_actions",
|
field: "_actions",
|
||||||
@ -145,19 +145,19 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
return <Box>
|
return <Box>
|
||||||
{allowRecordEdit && <IconButton onClick={() => editRecordCallback(params.row.__rowIndex)}><Icon>edit</Icon></IconButton>}
|
{allowRecordEdit && <IconButton onClick={() => editRecordCallback(params.row.__rowIndex)}><Icon>edit</Icon></IconButton>}
|
||||||
{allowRecordDelete && <IconButton onClick={() => deleteRecordCallback(params.row.__rowIndex)}><Icon>delete</Icon></IconButton>}
|
{allowRecordDelete && <IconButton onClick={() => deleteRecordCallback(params.row.__rowIndex)}><Icon>delete</Icon></IconButton>}
|
||||||
</Box>
|
</Box>;
|
||||||
})
|
})
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setRows(rows);
|
setRows(rows);
|
||||||
setRecords(records)
|
setRecords(records);
|
||||||
setColumns(columns);
|
setColumns(columns);
|
||||||
|
|
||||||
let csv = "";
|
let csv = "";
|
||||||
for (let i = 0; i < allColumns.length; i++)
|
for (let i = 0; i < allColumns.length; i++)
|
||||||
{
|
{
|
||||||
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`
|
csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"`;
|
||||||
}
|
}
|
||||||
csv += "\n";
|
csv += "\n";
|
||||||
|
|
||||||
@ -165,8 +165,8 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
{
|
{
|
||||||
for (let j = 0; j < allColumns.length; j++)
|
for (let j = 0; j < allColumns.length; j++)
|
||||||
{
|
{
|
||||||
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field)
|
const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field);
|
||||||
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`
|
csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"`;
|
||||||
}
|
}
|
||||||
csv += "\n";
|
csv += "\n";
|
||||||
}
|
}
|
||||||
@ -182,13 +182,13 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
// view all link //
|
// view all link //
|
||||||
///////////////////
|
///////////////////
|
||||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||||
if(data && data.viewAllLink)
|
if (data && data.viewAllLink)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsLeft.push(
|
labelAdditionalElementsLeft.push(
|
||||||
<Typography key={"viewAllLink"} variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative">
|
<Typography key={"viewAllLink"} variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative">
|
||||||
<Link to={data.viewAllLink}>View All</Link>
|
<Link to={data.viewAllLink}>View All</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
@ -200,10 +200,10 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
{
|
{
|
||||||
isExportDisabled = false;
|
isExportDisabled = false;
|
||||||
|
|
||||||
if(data.totalRows && data.queryOutput.records.length < data.totalRows)
|
if (data.totalRows && data.queryOutput.records.length < data.totalRows)
|
||||||
{
|
{
|
||||||
tooltipTitle = "Export these " + data.queryOutput.records.length + " records."
|
tooltipTitle = "Export these " + data.queryOutput.records.length + " records.";
|
||||||
if(data.viewAllLink)
|
if (data.viewAllLink)
|
||||||
{
|
{
|
||||||
tooltipTitle += "\nClick View All to export all records.";
|
tooltipTitle += "\nClick View All to export all records.";
|
||||||
}
|
}
|
||||||
@ -212,17 +212,17 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
|
|
||||||
const onExportClick = () =>
|
const onExportClick = () =>
|
||||||
{
|
{
|
||||||
if(csv)
|
if (csv)
|
||||||
{
|
{
|
||||||
HtmlUtils.download(fileName, csv);
|
HtmlUtils.download(fileName, csv);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
alert("There is no data available to export.")
|
alert("There is no data available to export.");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if(widgetMetaData?.showExportButton)
|
if (widgetMetaData?.showExportButton)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsLeft.push(
|
labelAdditionalElementsLeft.push(
|
||||||
<Typography key={"exportButton"} variant="body2" px={0} display="inline" position="relative">
|
<Typography key={"exportButton"} variant="body2" px={0} display="inline" position="relative">
|
||||||
@ -234,15 +234,15 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
////////////////////
|
////////////////////
|
||||||
// add new button //
|
// add new button //
|
||||||
////////////////////
|
////////////////////
|
||||||
const labelAdditionalComponentsRight: LabelComponent[] = []
|
const labelAdditionalComponentsRight: LabelComponent[] = [];
|
||||||
if(data && data.canAddChildRecord)
|
if (data && data.canAddChildRecord)
|
||||||
{
|
{
|
||||||
let disabledFields = data.disabledFieldsForNewChildRecords;
|
let disabledFields = data.disabledFieldsForNewChildRecords;
|
||||||
if(!disabledFields)
|
if (!disabledFields)
|
||||||
{
|
{
|
||||||
disabledFields = data.defaultValuesForNewChildRecords;
|
disabledFields = data.defaultValuesForNewChildRecords;
|
||||||
}
|
}
|
||||||
labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback))
|
labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -251,16 +251,16 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
/////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////
|
||||||
const handleRowClick = (params: GridRowParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) =>
|
const handleRowClick = (params: GridRowParams, event: MuiEvent<React.MouseEvent>, details: GridCallbackDetails) =>
|
||||||
{
|
{
|
||||||
if(disableRowClick)
|
if (disableRowClick)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const qInstance = await qController.loadMetaData()
|
const qInstance = await qController.loadMetaData();
|
||||||
let tablePath = qInstance.getTablePathByName(data.childTableMetaData.name)
|
let tablePath = qInstance.getTablePathByName(data.childTableMetaData.name);
|
||||||
if(tablePath)
|
if (tablePath)
|
||||||
{
|
{
|
||||||
tablePath = `${tablePath}/${params.row[data.childTableMetaData.primaryKeyField]}`;
|
tablePath = `${tablePath}/${params.row[data.childTableMetaData.primaryKeyField]}`;
|
||||||
DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance);
|
DataGridUtils.handleRowClick(tablePath, event, gridMouseDownX, gridMouseDownY, navigate, instance);
|
||||||
@ -276,7 +276,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function CustomToolbar()
|
function CustomToolbar()
|
||||||
{
|
{
|
||||||
const handleMouseDown: GridEventListener<"cellMouseDown"> = ( params, event, details ) =>
|
const handleMouseDown: GridEventListener<"cellMouseDown"> = (params, event, details) =>
|
||||||
{
|
{
|
||||||
setGridMouseDownX(event.clientX);
|
setGridMouseDownX(event.clientX);
|
||||||
setGridMouseDownY(event.clientY);
|
setGridMouseDownY(event.clientY);
|
||||||
@ -304,8 +304,8 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
|
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
|
||||||
>
|
>
|
||||||
<Box mx={-2} mb={-3}>
|
<Box mx={-3} mb={-3}>
|
||||||
<Box className="recordGridWidget">
|
<Box>
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
autoHeight
|
autoHeight
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -19,19 +19,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
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 Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {styled} from "@mui/material/styles";
|
import {styled} from "@mui/material/styles";
|
||||||
import Table from "@mui/material/Table";
|
import Table from "@mui/material/Table";
|
||||||
import TableBody from "@mui/material/TableBody";
|
|
||||||
import TableContainer from "@mui/material/TableContainer";
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
import TableRow from "@mui/material/TableRow";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import React, {useEffect, useMemo, useState} from "react";
|
|
||||||
import {useAsyncDebounce, useExpanded, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
|
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import MDInput from "qqq/components/legacy/MDInput";
|
import MDInput from "qqq/components/legacy/MDInput";
|
||||||
import MDPagination from "qqq/components/legacy/MDPagination";
|
import MDPagination from "qqq/components/legacy/MDPagination";
|
||||||
@ -43,6 +38,8 @@ import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell";
|
|||||||
import ImageCell from "qqq/components/widgets/tables/cells/ImageCell";
|
import ImageCell from "qqq/components/widgets/tables/cells/ImageCell";
|
||||||
import {TableDataInput} from "qqq/components/widgets/tables/TableCard";
|
import {TableDataInput} from "qqq/components/widgets/tables/TableCard";
|
||||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
||||||
|
import React, {useEffect, useMemo, useState} from "react";
|
||||||
|
import {useAsyncDebounce, useExpanded, useGlobalFilter, usePagination, useSortBy, useTable} from "react-table";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -106,17 +103,17 @@ function DataTable({
|
|||||||
entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"];
|
entries = entriesPerPageOptions ? entriesPerPageOptions : ["10", "25", "50", "100"];
|
||||||
|
|
||||||
let widths = [];
|
let widths = [];
|
||||||
for(let i = 0; i<table.columns.length; i++)
|
for (let i = 0; i < table.columns.length; i++)
|
||||||
{
|
{
|
||||||
const column = table.columns[i];
|
const column = table.columns[i];
|
||||||
if(column.type !== "hidden")
|
if (column.type !== "hidden")
|
||||||
{
|
{
|
||||||
widths.push(table.columns[i].width ?? "1fr");
|
widths.push(table.columns[i].width ?? "1fr");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let showExpandColumn = false;
|
let showExpandColumn = false;
|
||||||
if(table.rows)
|
if (table.rows)
|
||||||
{
|
{
|
||||||
for (let i = 0; i < table.rows.length; i++)
|
for (let i = 0; i < table.rows.length; i++)
|
||||||
{
|
{
|
||||||
@ -129,7 +126,7 @@ function DataTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columnsToMemo = [...table.columns];
|
const columnsToMemo = [...table.columns];
|
||||||
if(showExpandColumn)
|
if (showExpandColumn)
|
||||||
{
|
{
|
||||||
widths.push("60px");
|
widths.push("60px");
|
||||||
columnsToMemo.push(
|
columnsToMemo.push(
|
||||||
@ -166,18 +163,18 @@ 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... */}
|
{/* 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>
|
</span>
|
||||||
) : null,
|
) : null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(table.columnHeaderTooltips)
|
if (table.columnHeaderTooltips)
|
||||||
{
|
{
|
||||||
for (let column of columnsToMemo)
|
for (let column of columnsToMemo)
|
||||||
{
|
{
|
||||||
if(table.columnHeaderTooltips[column.accessor])
|
if (table.columnHeaderTooltips[column.accessor])
|
||||||
{
|
{
|
||||||
column.tooltip = table.columnHeaderTooltips[column.accessor];
|
column.tooltip = table.columnHeaderTooltips[column.accessor];
|
||||||
}
|
}
|
||||||
@ -297,7 +294,7 @@ function DataTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let visibleFooterRows = 1;
|
let visibleFooterRows = 1;
|
||||||
if(expanded && expanded[`${table.rows.length-1}`])
|
if (expanded && expanded[`${table.rows.length - 1}`])
|
||||||
{
|
{
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// todo - should count how many are expanded... //
|
// todo - should count how many are expanded... //
|
||||||
@ -308,156 +305,152 @@ function DataTable({
|
|||||||
function getTable(includeHead: boolean, rows: any, isFooter: boolean)
|
function getTable(includeHead: boolean, rows: any, isFooter: boolean)
|
||||||
{
|
{
|
||||||
let boxStyle = {};
|
let boxStyle = {};
|
||||||
if(fixedStickyLastRow)
|
if (fixedStickyLastRow)
|
||||||
{
|
{
|
||||||
boxStyle = isFooter
|
boxStyle = isFooter
|
||||||
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
|
? {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 = {};
|
let innerBoxStyle = {};
|
||||||
if(fixedStickyLastRow && isFooter)
|
if (fixedStickyLastRow && isFooter)
|
||||||
{
|
{
|
||||||
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
|
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}>
|
return <Box sx={boxStyle}><Box sx={innerBoxStyle}>
|
||||||
<Table {...getTableProps()}>
|
<Table {...getTableProps()} component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: gridTemplateColumns}}>
|
||||||
{
|
{
|
||||||
includeHead && (
|
includeHead && (
|
||||||
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}>
|
headerGroups.map((headerGroup: any, i: number) => (
|
||||||
{headerGroups.map((headerGroup: any, i: number) => (
|
headerGroup.headers.map((column: any) => (
|
||||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", gridTemplateColumns: gridTemplateColumns}}>
|
column.type !== "hidden" && (
|
||||||
{headerGroup.headers.map((column: any) => (
|
<DataTableHeadCell
|
||||||
column.type !== "hidden" && (
|
sx={{position: "sticky", top: 0, background: "white", zIndex: 10, alignItems: "flex-end"}}
|
||||||
<DataTableHeadCell
|
key={i++}
|
||||||
key={i++}
|
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
align={column.align ? column.align : "left"}
|
||||||
align={column.align ? column.align : "left"}
|
sorted={setSortedValue(column)}
|
||||||
sorted={setSortedValue(column)}
|
tooltip={column.tooltip}
|
||||||
tooltip={column.tooltip}
|
>
|
||||||
>
|
{column.render("header")}
|
||||||
{column.render("header")}
|
</DataTableHeadCell>
|
||||||
</DataTableHeadCell>
|
)
|
||||||
)
|
))
|
||||||
))}
|
))
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<TableBody {...getTableBodyProps()}>
|
{rows.map((row: any, key: any) =>
|
||||||
{rows.map((row: any, key: any) =>
|
{
|
||||||
|
prepareRow(row);
|
||||||
|
|
||||||
|
let overrideNoEndBorder = false;
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// don't do an end-border on nested rows - unless they're the last one in a set //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (row.depth > 0)
|
||||||
{
|
{
|
||||||
prepareRow(row);
|
overrideNoEndBorder = true;
|
||||||
|
if (key + 1 < rows.length && rows[key + 1].depth == 0)
|
||||||
let overrideNoEndBorder = false;
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// don't do an end-border on nested rows - unless they're the last one in a set //
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////
|
|
||||||
if(row.depth > 0)
|
|
||||||
{
|
{
|
||||||
overrideNoEndBorder = true;
|
overrideNoEndBorder = false;
|
||||||
if(key + 1 < rows.length && rows[key + 1].depth == 0)
|
|
||||||
{
|
|
||||||
overrideNoEndBorder = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
// don't do end-border on the footer //
|
// don't do end-border on the footer //
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
if(isFooter)
|
if (isFooter)
|
||||||
{
|
{
|
||||||
overrideNoEndBorder = true;
|
overrideNoEndBorder = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let background = "initial";
|
let background = "initial";
|
||||||
if(isFooter)
|
if (isFooter)
|
||||||
{
|
{
|
||||||
background = "#EEEEEE";
|
background = "#EEEEEE";
|
||||||
}
|
}
|
||||||
else if(row.depth > 0 || row.isExpanded)
|
else if (row.depth > 0 || row.isExpanded)
|
||||||
{
|
{
|
||||||
background = "#FAFAFA";
|
background = "#FAFAFA";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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" && (
|
||||||
cell.column.type !== "hidden" && (
|
<DataTableBodyCell
|
||||||
<DataTableBodyCell
|
key={key}
|
||||||
key={key}
|
sx={{verticalAlign: "top", background: background}}
|
||||||
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
||||||
depth={row.depth}
|
depth={row.depth}
|
||||||
align={cell.column.align ? cell.column.align : "left"}
|
align={cell.column.align ? cell.column.align : "left"}
|
||||||
{...cell.getCellProps()}
|
{...cell.getCellProps()}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
cell.column.type === "default" && (
|
cell.column.type === "default" && (
|
||||||
cell.value && "number" === typeof cell.value ? (
|
cell.value && "number" === typeof cell.value ? (
|
||||||
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
|
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
|
||||||
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
|
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "htmlAndTooltip" && (
|
cell.column.type === "htmlAndTooltip" && (
|
||||||
<DefaultCell isFooter={isFooter}>
|
<DefaultCell isFooter={isFooter}>
|
||||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||||
<Box>
|
<Box>
|
||||||
{parse(cell.value)}
|
{parse(cell.value)}
|
||||||
</Box>
|
</Box>
|
||||||
</NoMaxWidthTooltip>
|
</NoMaxWidthTooltip>
|
||||||
</DefaultCell>
|
</DefaultCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "html" && (
|
cell.column.type === "html" && (
|
||||||
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
|
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "composite" && (
|
cell.column.type === "composite" && (
|
||||||
<DefaultCell isFooter={isFooter}>
|
<DefaultCell isFooter={isFooter}>
|
||||||
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
|
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
|
||||||
</DefaultCell>
|
</DefaultCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "block" && (
|
cell.column.type === "block" && (
|
||||||
<DefaultCell isFooter={isFooter}>
|
<DefaultCell isFooter={isFooter}>
|
||||||
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
|
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
|
||||||
</DefaultCell>
|
</DefaultCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
cell.column.type === "image" && row.values["imageTotal"] && (
|
||||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "image" && !row.values["imageTotal"] && (
|
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
(cell.column.id === "__expander") && cell.render("cell")
|
(cell.column.id === "__expander") && cell.render("cell")
|
||||||
}
|
}
|
||||||
</DataTableBodyCell>
|
</DataTableBodyCell>
|
||||||
)
|
)
|
||||||
))}
|
))
|
||||||
</TableRow>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
</Table>
|
||||||
</Box></Box>
|
</Box></Box>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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) ? (
|
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||||
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
||||||
|
@ -28,13 +28,13 @@ import TableBody from "@mui/material/TableBody";
|
|||||||
import TableContainer from "@mui/material/TableContainer";
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
import TableRow from "@mui/material/TableRow";
|
import TableRow from "@mui/material/TableRow";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import React, {useEffect, useState} from "react";
|
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
import DataTableBodyCell from "qqq/components/widgets/tables/cells/DataTableBodyCell";
|
import DataTableBodyCell from "qqq/components/widgets/tables/cells/DataTableBodyCell";
|
||||||
import DataTableHeadCell from "qqq/components/widgets/tables/cells/DataTableHeadCell";
|
import DataTableHeadCell from "qqq/components/widgets/tables/cells/DataTableHeadCell";
|
||||||
import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell";
|
import DefaultCell from "qqq/components/widgets/tables/cells/DefaultCell";
|
||||||
import DataTable from "qqq/components/widgets/tables/DataTable";
|
import DataTable from "qqq/components/widgets/tables/DataTable";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////////////
|
//////////////////////////////////////
|
||||||
@ -43,7 +43,7 @@ import Client from "qqq/utils/qqq/Client";
|
|||||||
export interface TableDataInput
|
export interface TableDataInput
|
||||||
{
|
{
|
||||||
columns: { [key: string]: any }[];
|
columns: { [key: string]: any }[];
|
||||||
columnHeaderTooltips?: { [columnName: string]: string | JSX.Element }
|
columnHeaderTooltips?: { [columnName: string]: string | JSX.Element };
|
||||||
rows: { [key: string]: any }[];
|
rows: { [key: string]: any }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +63,7 @@ interface Props
|
|||||||
}
|
}
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight, widgetMetaData}: Props): JSX.Element
|
function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown, fixedStickyLastRow, fixedHeight, widgetMetaData}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [qInstance, setQInstance] = useState(null as QInstance);
|
const [qInstance, setQInstance] = useState(null as QInstance);
|
||||||
@ -92,41 +93,25 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
|
|||||||
/>
|
/>
|
||||||
: noRowsFoundHTML ?
|
: noRowsFoundHTML ?
|
||||||
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
|
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
|
||||||
<MDTypography
|
<MDTypography variant="subtitle2" color="secondary" fontWeight="regular">
|
||||||
variant="subtitle2"
|
{noRowsFoundHTML ? (parse(noRowsFoundHTML)) : "No rows found"}
|
||||||
color="secondary"
|
|
||||||
fontWeight="regular"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
noRowsFoundHTML ? (
|
|
||||||
parse(noRowsFoundHTML)
|
|
||||||
) : "No rows found"
|
|
||||||
}
|
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
:
|
:
|
||||||
<TableContainer sx={{boxShadow: "none"}}>
|
<TableContainer sx={{boxShadow: "none"}}>
|
||||||
<Table>
|
<Table component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr"}}>
|
||||||
<Box component="thead">
|
{Array(8).fill(0).map((_, i) =>
|
||||||
<TableRow key="header">
|
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
|
||||||
{Array(8).fill(0).map((_, i) =>
|
<Skeleton width="100%" />
|
||||||
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
|
</DataTableHeadCell>
|
||||||
<Skeleton width="100%" />
|
)}
|
||||||
</DataTableHeadCell>
|
{Array(5).fill(0).map((_, i) =>
|
||||||
)}
|
Array(8).fill(0).map((_, j) =>
|
||||||
</TableRow>
|
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
||||||
</Box>
|
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
|
||||||
<TableBody>
|
</DataTableBodyCell>
|
||||||
{Array(5).fill(0).map((_, i) =>
|
)
|
||||||
<TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}>
|
)}
|
||||||
{Array(8).fill(0).map((_, j) =>
|
|
||||||
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
|
||||||
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
|
|
||||||
</DataTableBodyCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import {htmlToText} from "html-to-text";
|
import {htmlToText} from "html-to-text";
|
||||||
import React, {useContext, useEffect, useState} from "react";
|
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||||
import TableCard from "qqq/components/widgets/tables/TableCard";
|
import TableCard from "qqq/components/widgets/tables/TableCard";
|
||||||
@ -31,6 +30,7 @@ import Widget, {WidgetData} from "qqq/components/widgets/Widget";
|
|||||||
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
|
import {WidgetUtils} from "qqq/components/widgets/WidgetUtils";
|
||||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||||
|
import React, {useContext, useEffect, useState} from "react";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -40,8 +40,7 @@ interface Props
|
|||||||
isChild?: boolean;
|
isChild?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
TableWidget.defaultProps = {
|
TableWidget.defaultProps = {};
|
||||||
};
|
|
||||||
|
|
||||||
function TableWidget(props: Props): JSX.Element
|
function TableWidget(props: Props): JSX.Element
|
||||||
{
|
{
|
||||||
@ -86,7 +85,7 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
|
|
||||||
const cell = rows[i][columns[j].accessor];
|
const cell = rows[i][columns[j].accessor];
|
||||||
let text = cell;
|
let text = cell;
|
||||||
if(columns[j].type != "default")
|
if (columns[j].type != "default")
|
||||||
{
|
{
|
||||||
text = htmlToText(cell,
|
text = htmlToText(cell,
|
||||||
{
|
{
|
||||||
@ -105,7 +104,7 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
setCsv(csv);
|
setCsv(csv);
|
||||||
|
|
||||||
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
||||||
setFileName(fileName)
|
setFileName(fileName);
|
||||||
|
|
||||||
console.log(`useEffect, setting fileName ${fileName}`);
|
console.log(`useEffect, setting fileName ${fileName}`);
|
||||||
}
|
}
|
||||||
@ -114,24 +113,28 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
|
|
||||||
const onExportClick = () =>
|
const onExportClick = () =>
|
||||||
{
|
{
|
||||||
if(props.widgetData?.csvData)
|
if (props.widgetData?.csvData)
|
||||||
{
|
{
|
||||||
const csv = WidgetUtils.widgetCsvDataToString(props.widgetData);
|
const csv = WidgetUtils.widgetCsvDataToString(props.widgetData);
|
||||||
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
const fileName = WidgetUtils.makeExportFileName(props.widgetData, props.widgetMetaData);
|
||||||
HtmlUtils.download(fileName, csv);
|
HtmlUtils.download(fileName, csv);
|
||||||
}
|
}
|
||||||
else if(csv)
|
else if (csv)
|
||||||
{
|
{
|
||||||
HtmlUtils.download(fileName, csv);
|
HtmlUtils.download(fileName, csv);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
alert("There is no data available to export.")
|
alert("There is no data available to export.");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
const labelAdditionalElementsLeft: JSX.Element[] = [];
|
||||||
if(props.widgetMetaData?.showExportButton)
|
if (props.widgetData?.linkText && props.widgetData?.linkURL)
|
||||||
|
{
|
||||||
|
labelAdditionalElementsLeft.push(WidgetUtils.generateLabelLink(props.widgetData?.linkText, props.widgetData?.linkURL));
|
||||||
|
}
|
||||||
|
if (props.widgetMetaData?.showExportButton)
|
||||||
{
|
{
|
||||||
labelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
|
labelAdditionalElementsLeft.push(WidgetUtils.generateExportButton(onExportClick));
|
||||||
}
|
}
|
||||||
@ -139,14 +142,14 @@ function TableWidget(props: Props): JSX.Element
|
|||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
// look for column-header tooltips from helpContent //
|
// look for column-header tooltips from helpContent //
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
const columnHeaderTooltips: {[columnName: string]: JSX.Element} = {}
|
const columnHeaderTooltips: { [columnName: string]: JSX.Element } = {};
|
||||||
for (let column of props.widgetData?.columns ?? [])
|
for (let column of props.widgetData?.columns ?? [])
|
||||||
{
|
{
|
||||||
const helpRoles = ["ALL_SCREENS"]
|
const helpRoles = ["ALL_SCREENS"];
|
||||||
const slotName = `columnHeader=${column.accessor}`;
|
const slotName = `columnHeader=${column.accessor}`;
|
||||||
const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles);
|
const showHelp = helpHelpActive || hasHelpContent(props.widgetMetaData?.helpContent?.get(slotName), helpRoles);
|
||||||
|
|
||||||
if(showHelp)
|
if (showHelp)
|
||||||
{
|
{
|
||||||
const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />;
|
const formattedHelpContent = <HelpContent helpContents={props.widgetMetaData?.helpContent?.get(slotName)} roles={helpRoles} helpContentKey={`widget:${props.widgetMetaData?.name};slot:${slotName}`} />;
|
||||||
columnHeaderTooltips[column.accessor] = formattedHelpContent;
|
columnHeaderTooltips[column.accessor] = formattedHelpContent;
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Box from "@mui/material/Box";
|
import {Box} from "@mui/material";
|
||||||
import {Theme} from "@mui/material/styles";
|
import {Theme} from "@mui/material/styles";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
@ -30,13 +30,14 @@ interface Props
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
align?: "left" | "right" | "center";
|
align?: "left" | "right" | "center";
|
||||||
|
sx?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
function DataTableBodyCell({noBorder, align, sx, children}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component="td"
|
component="div"
|
||||||
textAlign={align}
|
textAlign={align}
|
||||||
py={1.5}
|
py={1.5}
|
||||||
px={1.5}
|
px={1.5}
|
||||||
@ -54,7 +55,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
|||||||
},
|
},
|
||||||
"&:last-child": {
|
"&:last-child": {
|
||||||
paddingRight: "1rem"
|
paddingRight: "1rem"
|
||||||
}
|
}, ...sx
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@ -72,6 +73,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
|||||||
DataTableBodyCell.defaultProps = {
|
DataTableBodyCell.defaultProps = {
|
||||||
noBorder: false,
|
noBorder: false,
|
||||||
align: "left",
|
align: "left",
|
||||||
|
sx: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DataTableBodyCell;
|
export default DataTableBodyCell;
|
||||||
|
@ -44,18 +44,14 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component="th"
|
component="div"
|
||||||
width={width}
|
width={width}
|
||||||
py={1.5}
|
py={1.5}
|
||||||
px={1.5}
|
px={1.5}
|
||||||
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
||||||
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||||
"&:nth-of-type(1)": {
|
position: "sticky", top: 0, background: "white",
|
||||||
paddingLeft: "1rem"
|
zIndex: 1 // so if body rows scroll behind it, they don't show through
|
||||||
},
|
|
||||||
"&:last-child": {
|
|
||||||
paddingRight: "1rem"
|
|
||||||
},
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
@ -33,6 +33,7 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo
|
|||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
||||||
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
|
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
|
||||||
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
||||||
@ -54,7 +55,7 @@ import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
|||||||
import MDButton from "qqq/components/legacy/MDButton";
|
import MDButton from "qqq/components/legacy/MDButton";
|
||||||
import MDProgress from "qqq/components/legacy/MDProgress";
|
import MDProgress from "qqq/components/legacy/MDProgress";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
import HelpContent from "qqq/components/misc/HelpContent";
|
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||||
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||||
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
|
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
|
||||||
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
||||||
@ -94,7 +95,9 @@ const BACKOFF_AMOUNT = 1.5;
|
|||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
||||||
{
|
{
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
|
||||||
|
|
||||||
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
@ -134,9 +137,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
||||||
const [showFullHelpText, setShowFullHelpText] = useState(false);
|
const [showFullHelpText, setShowFullHelpText] = useState(false);
|
||||||
|
|
||||||
const [renderedWidgets, setRenderedWidgets] = useState({} as {[step: string]: {[widgetName: string]: any}});
|
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
|
||||||
|
|
||||||
const {pageHeader, recordAnalytics, setPageHeader} = useContext(QContext);
|
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// for setting the processError state - call this function, which will also set the isUserFacingError state //
|
// for setting the processError state - call this function, which will also set the isUserFacingError state //
|
||||||
@ -238,15 +241,15 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
setShowFullHelpText(!showFullHelpText);
|
setShowFullHelpText(!showFullHelpText);
|
||||||
};
|
};
|
||||||
|
|
||||||
const download = (processValues: {[key: string]: string}) =>
|
const download = (processValues: { [key: string]: string }) =>
|
||||||
{
|
{
|
||||||
let url;
|
let url;
|
||||||
let fileName = processValues.downloadFileName;
|
let fileName = processValues.downloadFileName;
|
||||||
if(processValues.serverFilePath)
|
if (processValues.serverFilePath)
|
||||||
{
|
{
|
||||||
url = `/download/${encodeURIComponent(processValues.downloadFileName)}?filePath=${encodeURIComponent(processValues.serverFilePath)}`;
|
url = `/download/${encodeURIComponent(processValues.downloadFileName)}?filePath=${encodeURIComponent(processValues.serverFilePath)}`;
|
||||||
}
|
}
|
||||||
else if(processValues.storageTableName && processValues.storageReference)
|
else if (processValues.storageTableName && processValues.storageReference)
|
||||||
{
|
{
|
||||||
url = `/download/${encodeURIComponent(processValues.downloadFileName)}?storageTableName=${encodeURIComponent(processValues.storageTableName)}&storageReference=${encodeURIComponent(processValues.storageReference)}`;
|
url = `/download/${encodeURIComponent(processValues.downloadFileName)}?storageTableName=${encodeURIComponent(processValues.storageTableName)}&storageReference=${encodeURIComponent(processValues.storageReference)}`;
|
||||||
}
|
}
|
||||||
@ -291,19 +294,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function renderWidget(widgetName: string)
|
function renderWidget(widgetName: string)
|
||||||
{
|
{
|
||||||
if(!renderedWidgets[activeStep.name])
|
if (!renderedWidgets[activeStep.name])
|
||||||
{
|
{
|
||||||
renderedWidgets[activeStep.name] = {};
|
renderedWidgets[activeStep.name] = {};
|
||||||
setRenderedWidgets(renderedWidgets);
|
setRenderedWidgets(renderedWidgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(renderedWidgets[activeStep.name][widgetName])
|
if (renderedWidgets[activeStep.name][widgetName])
|
||||||
{
|
{
|
||||||
return renderedWidgets[activeStep.name][widgetName];
|
return renderedWidgets[activeStep.name][widgetName];
|
||||||
}
|
}
|
||||||
|
|
||||||
const widgetMetaData = qInstance.widgets.get(widgetName);
|
const widgetMetaData = qInstance.widgets.get(widgetName);
|
||||||
if(!widgetMetaData)
|
if (!widgetMetaData)
|
||||||
{
|
{
|
||||||
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
|
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
|
||||||
}
|
}
|
||||||
@ -311,12 +314,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const queryStringParts: string[] = [];
|
const queryStringParts: string[] = [];
|
||||||
for (let name in processValues)
|
for (let name in processValues)
|
||||||
{
|
{
|
||||||
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`)
|
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderedWidget = (<Box m={-2}>
|
const renderedWidget = (<Box m={-2}>
|
||||||
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} />
|
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} />
|
||||||
</Box>)
|
</Box>);
|
||||||
renderedWidgets[activeStep.name][widgetName] = renderedWidget;
|
renderedWidgets[activeStep.name][widgetName] = renderedWidget;
|
||||||
return renderedWidget;
|
return renderedWidget;
|
||||||
}
|
}
|
||||||
@ -367,8 +370,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
</MDTypography>
|
</MDTypography>
|
||||||
<Box component="div" py={3}>
|
<Box component="div" py={3}>
|
||||||
<Grid container justifyContent="flex-end" spacing={3}>
|
<Grid container justifyContent="flex-end" spacing={3}>
|
||||||
{isModal ? <QCancelButton onClickHandler={handleCancelClicked} disabled={false} label="Close" />
|
{isModal ? <QCancelButton onClickHandler={() => handleCancelClicked(true)} disabled={false} label="Close" />
|
||||||
: !isWidget && <QCancelButton onClickHandler={handleCancelClicked} disabled={false} />
|
: !isWidget && <QCancelButton onClickHandler={() => handleCancelClicked(true)} disabled={false} />
|
||||||
}
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@ -443,7 +446,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
{
|
{
|
||||||
if (processValues[key])
|
if (processValues[key])
|
||||||
{
|
{
|
||||||
formFields[key].possibleValueProps.initialDisplayValue = processValues[key];
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if we have a cached possible-value label for this field name (key), then set it as the PV's initialDisplayValue //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (cachedPossibleValueLabels[key] && cachedPossibleValueLabels[key][processValues[key]])
|
||||||
|
{
|
||||||
|
formFields[key].possibleValueProps.initialDisplayValue = cachedPossibleValueLabels[key][processValues[key]];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// else (and i don't think this should happen?) at least set something... //
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
formFields[key].possibleValueProps.initialDisplayValue = processValues[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formFields[key].possibleValueProps.otherValues = formFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
formFields[key].possibleValueProps.otherValues = formFields[key].possibleValueProps.otherValues ?? new Map<string, any>();
|
||||||
@ -455,14 +471,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo not commit - not ready - need process (or screen) meta-data to have helpContents...
|
/////////////////////////////////////
|
||||||
/*
|
// screen(step)-level help content //
|
||||||
///////////////////////////////
|
/////////////////////////////////////
|
||||||
// screen-level help content //
|
|
||||||
///////////////////////////////
|
|
||||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||||
const formattedHelpContent = <HelpContent helpContents={process.helpContents} roles={helpRoles} helpContentKey={`table:${tableName};section:${section.name}`} />;
|
const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles);
|
||||||
*/
|
const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -479,13 +493,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
/*
|
showHelp &&
|
||||||
// todo not commit - not ready - need process (or screen) meta-data to have helpContents...
|
<Box fontSize={"0.875rem"} color={colors.blueGray.main} pb={2}>
|
||||||
formattedHelpContent &&
|
|
||||||
<Box px={"1.5rem"} fontSize={"0.875rem"} color={colors.blueGray.main}>
|
|
||||||
{formattedHelpContent}
|
{formattedHelpContent}
|
||||||
</Box>
|
</Box>
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -505,7 +516,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
// edit the formData object to just include those. //
|
// edit the formData object to just include those. //
|
||||||
//////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////
|
||||||
let formDataToUse = formData;
|
let formDataToUse = formData;
|
||||||
if(component.values && component.values.includeFieldNames)
|
if (component.values && component.values.includeFieldNames)
|
||||||
{
|
{
|
||||||
formDataToUse = Object.assign({}, formData);
|
formDataToUse = Object.assign({}, formData);
|
||||||
|
|
||||||
@ -613,21 +624,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
component.type === QComponentType.EDIT_FORM &&
|
component.type === QComponentType.EDIT_FORM &&
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
component.values?.sectionLabel ?
|
component.values?.sectionLabel ?
|
||||||
<Box py={1.5}>
|
<Box py={1.5}>
|
||||||
<Card sx={{scrollMarginTop: "20px"}}>
|
<Card sx={{scrollMarginTop: "20px"}}>
|
||||||
<MDTypography variant="h5" p={3} pl={2} pb={1}>
|
<MDTypography variant="h5" p={3} pl={2} pb={1}>
|
||||||
{component.values?.sectionLabel}
|
{component.values?.sectionLabel}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
<Box pt={0} p={2}>
|
<Box pt={0} p={2}>
|
||||||
<QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
<QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Box> : <QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
</Box> : <QDynamicForm formData={formDataToUse} helpRoles={helpRoles} helpContentKeyPrefix={`process:${processName};`} />
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
component.type === QComponentType.VIEW_FORM && step.viewFields && (
|
component.type === QComponentType.VIEW_FORM && step.viewFields && (
|
||||||
@ -870,6 +881,12 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
{
|
{
|
||||||
dynamicFormFields[fieldName] = dynamicFormValue;
|
dynamicFormFields[fieldName] = dynamicFormValue;
|
||||||
initialValues[fieldName] = initialValue;
|
initialValues[fieldName] = initialValue;
|
||||||
|
|
||||||
|
if (formikSetFieldValueFunction)
|
||||||
|
{
|
||||||
|
formikSetFieldValueFunction(fieldName, initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
formValidations[fieldName] = validation;
|
formValidations[fieldName] = validation;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -919,6 +936,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
fullFieldList.forEach((field) =>
|
fullFieldList.forEach((field) =>
|
||||||
{
|
{
|
||||||
initialValues[field.name] = processValues[field.name];
|
initialValues[field.name] = processValues[field.name];
|
||||||
|
|
||||||
|
if (formikSetFieldValueFunction)
|
||||||
|
{
|
||||||
|
formikSetFieldValueFunction(field.name, processValues[field.name]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -1018,7 +1040,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, fullFieldList, tableMetaData.name, null, null);
|
DynamicFormUtils.addPossibleValueProps(newDynamicFormFields, fullFieldList, tableMetaData?.name, null, null);
|
||||||
|
|
||||||
setFormFields(newDynamicFormFields);
|
setFormFields(newDynamicFormFields);
|
||||||
setValidationScheme(Yup.object().shape(newFormValidations));
|
setValidationScheme(Yup.object().shape(newFormValidations));
|
||||||
@ -1078,36 +1100,88 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
if (lastProcessResponse instanceof QJobComplete)
|
if (lastProcessResponse instanceof QJobComplete)
|
||||||
{
|
{
|
||||||
const qJobComplete = lastProcessResponse as QJobComplete;
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
setJobUUID(null);
|
// run an async function here, in case we need to await looking up any possible-value labels //
|
||||||
setNewStep(qJobComplete.nextStep);
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
setProcessValues(qJobComplete.values);
|
(async () =>
|
||||||
setQJobRunning(null);
|
|
||||||
|
|
||||||
if(formikSetFieldValueFunction)
|
|
||||||
{
|
{
|
||||||
//////////////////////////////////
|
const qJobComplete = lastProcessResponse as QJobComplete;
|
||||||
// reset field values in formik //
|
const newValues = qJobComplete.values;
|
||||||
//////////////////////////////////
|
|
||||||
for (let key in qJobComplete.values)
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let frontendSteps = steps;
|
||||||
|
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
|
||||||
|
if (updatedFrontendStepList)
|
||||||
{
|
{
|
||||||
formikSetFieldValueFunction(key, qJobComplete.values[key]);
|
setSteps(updatedFrontendStepList);
|
||||||
|
frontendSteps = updatedFrontendStepList;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
|
// if the next screen has any PVS fields - look up their labels (display values) //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
|
const nextStepName = qJobComplete.nextStep;
|
||||||
if(updatedFrontendStepList)
|
let nextStep: QFrontendStepMetaData | null = null;
|
||||||
{
|
if (frontendSteps && nextStepName)
|
||||||
setSteps(updatedFrontendStepList);
|
{
|
||||||
}
|
for (let i = 0; i < frontendSteps.length; i++)
|
||||||
|
{
|
||||||
|
if (frontendSteps[i].name === nextStepName)
|
||||||
|
{
|
||||||
|
nextStep = frontendSteps[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (activeStep && activeStep.recordListFields)
|
if (nextStep && nextStep.formFields)
|
||||||
{
|
{
|
||||||
setNeedRecords(true);
|
for (let i = 0; i < nextStep.formFields.length; i++)
|
||||||
}
|
{
|
||||||
|
const field = nextStep.formFields[i];
|
||||||
|
const fieldName = field.name;
|
||||||
|
if (field.possibleValueSourceName && newValues && newValues[fieldName])
|
||||||
|
{
|
||||||
|
const results: QPossibleValue[] = await Client.getInstance().possibleValues(null, processName, fieldName, null, [newValues[fieldName]]);
|
||||||
|
if (results && results.length > 0)
|
||||||
|
{
|
||||||
|
if (!cachedPossibleValueLabels[fieldName])
|
||||||
|
{
|
||||||
|
cachedPossibleValueLabels[fieldName] = {};
|
||||||
|
}
|
||||||
|
cachedPossibleValueLabels[fieldName][newValues[fieldName]] = results[0].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setJobUUID(null);
|
||||||
|
setNewStep(nextStepName);
|
||||||
|
setProcessValues(newValues);
|
||||||
|
setQJobRunning(null);
|
||||||
|
|
||||||
|
if (formikSetFieldValueFunction)
|
||||||
|
{
|
||||||
|
//////////////////////////////////
|
||||||
|
// reset field values in formik //
|
||||||
|
//////////////////////////////////
|
||||||
|
for (let key in qJobComplete.values)
|
||||||
|
{
|
||||||
|
if (Object.hasOwn(formFields, key))
|
||||||
|
{
|
||||||
|
console.log(`(re)setting form field [${key}] to [${qJobComplete.values[key]}]`);
|
||||||
|
formikSetFieldValueFunction(key, qJobComplete.values[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeStep && activeStep.recordListFields)
|
||||||
|
{
|
||||||
|
setNeedRecords(true);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
else if (lastProcessResponse instanceof QJobStarted)
|
else if (lastProcessResponse instanceof QJobStarted)
|
||||||
{
|
{
|
||||||
@ -1349,7 +1423,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
Object.keys(values).forEach((key) =>
|
Object.keys(values).forEach((key) =>
|
||||||
{
|
{
|
||||||
formData.append(key, values[key]);
|
if (values[key] !== undefined)
|
||||||
|
{
|
||||||
|
formData.append(key, values[key]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
|
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
|
||||||
@ -1402,8 +1479,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelClicked = () =>
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
const handleCancelClicked = (isClose: boolean) =>
|
||||||
{
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
// unless this is a 'close', then tell backend we're cancelling //
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
if (!isClose)
|
||||||
|
{
|
||||||
|
Client.getInstance().processCancel(processName, processUUID);
|
||||||
|
}
|
||||||
|
|
||||||
if (isModal && closeModalHandler)
|
if (isModal && closeModalHandler)
|
||||||
{
|
{
|
||||||
closeModalHandler(null, "cancelClicked");
|
closeModalHandler(null, "cancelClicked");
|
||||||
@ -1416,6 +1505,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
navigate(path, {replace: true});
|
navigate(path, {replace: true});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const mainCardStyles: any = {};
|
const mainCardStyles: any = {};
|
||||||
const formStyles: any = {};
|
const formStyles: any = {};
|
||||||
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
|
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
|
||||||
@ -1491,8 +1581,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
<Box p={3}>
|
<Box p={3}>
|
||||||
<Box pb={isWidget ? 6 : "initial"}>
|
<Box pb={isWidget ? 6 : "initial"}>
|
||||||
{/***************************************************************************
|
{/***************************************************************************
|
||||||
** step content - e.g., the appropriate form or other screen for the step **
|
** step content - e.g., the appropriate form or other screen for the step **
|
||||||
***************************************************************************/}
|
***************************************************************************/}
|
||||||
{getDynamicStepContent(
|
{getDynamicStepContent(
|
||||||
activeStepIndex,
|
activeStepIndex,
|
||||||
activeStep,
|
activeStep,
|
||||||
@ -1508,8 +1598,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
setFieldValue,
|
setFieldValue,
|
||||||
)}
|
)}
|
||||||
{/********************************
|
{/********************************
|
||||||
** back &| next/submit buttons **
|
** back &| next/submit buttons **
|
||||||
********************************/}
|
********************************/}
|
||||||
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
||||||
{true || activeStepIndex === 0 ? (
|
{true || activeStepIndex === 0 ? (
|
||||||
<Box />
|
<Box />
|
||||||
@ -1527,7 +1617,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
)}
|
)}
|
||||||
{
|
{
|
||||||
noMoreSteps && <QCancelButton
|
noMoreSteps && <QCancelButton
|
||||||
onClickHandler={handleCancelClicked}
|
onClickHandler={() => handleCancelClicked(true)}
|
||||||
label={isModal ? "Close" : "Return"}
|
label={isModal ? "Close" : "Return"}
|
||||||
iconName={isModal ? "cancel" : "arrow_back"}
|
iconName={isModal ? "cancel" : "arrow_back"}
|
||||||
disabled={isSubmitting} />
|
disabled={isSubmitting} />
|
||||||
@ -1538,7 +1628,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
<Grid container justifyContent="flex-end" spacing={3}>
|
<Grid container justifyContent="flex-end" spacing={3}>
|
||||||
{
|
{
|
||||||
!isWidget && (
|
!isWidget && (
|
||||||
<QCancelButton onClickHandler={handleCancelClicked} disabled={isSubmitting} />
|
<QCancelButton onClickHandler={() => handleCancelClicked(false)} disabled={isSubmitting} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
|
<QSubmitButton label={nextButtonLabel} iconName={nextButtonIcon} disabled={isSubmitting} />
|
||||||
@ -1553,13 +1643,13 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
);
|
||||||
}}
|
}}
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<Box py={3} mb={20}>
|
<Box py={3} mb={20} className="processRun">
|
||||||
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
|
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
|
||||||
<Grid item xs={12} lg={10} xl={8}>
|
<Grid item xs={12} lg={10} xl={8}>
|
||||||
{form}
|
{form}
|
||||||
|
@ -121,7 +121,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const valueCounts = [] as QRecord[];
|
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]);
|
let valueRecord = new QRecord(result.values.valueCounts[i]);
|
||||||
|
|
||||||
|
@ -94,6 +94,7 @@ interface Props
|
|||||||
isModal?: boolean;
|
isModal?: boolean;
|
||||||
initialQueryFilter?: QQueryFilter;
|
initialQueryFilter?: QQueryFilter;
|
||||||
initialColumns?: QQueryColumns;
|
initialColumns?: QQueryColumns;
|
||||||
|
allowVariables?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
@ -125,7 +126,7 @@ const getLoadingScreen = (isModal: boolean) =>
|
|||||||
**
|
**
|
||||||
** Yuge component. The best. Lots of very smart people are saying so.
|
** Yuge component. The best. Lots of very smart people are saying so.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, initialColumns}: Props, ref) =>
|
const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
||||||
{
|
{
|
||||||
const tableName = table.name;
|
const tableName = table.name;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@ -630,7 +631,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
|||||||
const type = (e.target as any).type;
|
const type = (e.target as any).type;
|
||||||
const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search");
|
const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search");
|
||||||
|
|
||||||
if (validType && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess)
|
if (validType && !isModal && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess)
|
||||||
{
|
{
|
||||||
if (!e.metaKey && !e.ctrlKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
|
if (!e.metaKey && !e.ctrlKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
|
||||||
{
|
{
|
||||||
@ -668,7 +669,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
|||||||
{
|
{
|
||||||
document.removeEventListener("keydown", down);
|
document.removeEventListener("keydown", down);
|
||||||
};
|
};
|
||||||
}, [dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess]);
|
}, [isModal, dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess]);
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -2884,6 +2885,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
|||||||
gridApiRef={gridApiRef}
|
gridApiRef={gridApiRef}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
queryScreenUsage={usage}
|
queryScreenUsage={usage}
|
||||||
|
allowVariables={allowVariables}
|
||||||
setMode={doSetMode}
|
setMode={doSetMode}
|
||||||
savedViewsComponent={savedViewsComponent}
|
savedViewsComponent={savedViewsComponent}
|
||||||
columnMenuComponent={buildColumnMenu()}
|
columnMenuComponent={buildColumnMenu()}
|
||||||
@ -2912,6 +2914,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, init
|
|||||||
metaData: metaData,
|
metaData: metaData,
|
||||||
queryFilter: queryFilter,
|
queryFilter: queryFilter,
|
||||||
updateFilter: doSetQueryFilter,
|
updateFilter: doSetQueryFilter,
|
||||||
|
allowVariables: allowVariables
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
localeText={{
|
localeText={{
|
||||||
|
@ -535,7 +535,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
|||||||
|
|
||||||
setPageHeader(record.recordLabel);
|
setPageHeader(record.recordLabel);
|
||||||
|
|
||||||
if (!launchingProcess)
|
if (!launchingProcess && !activeModalProcess)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -421,6 +421,14 @@ input[type="search"]::-webkit-search-results-decoration
|
|||||||
font-size: 2rem !important;
|
font-size: 2rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-table-actions-icon
|
||||||
|
{
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
position: relative;
|
||||||
|
top: -5px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-schedule-icon
|
.dashboard-schedule-icon
|
||||||
{
|
{
|
||||||
font-size: 1.1rem !important;
|
font-size: 1.1rem !important;
|
||||||
@ -653,6 +661,11 @@ input[type="search"]::-webkit-search-results-decoration
|
|||||||
min-height: unset !important;
|
min-height: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.MuiDataGrid-columnHeaders
|
||||||
|
{
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
/* new style for toggle buttons */
|
/* new style for toggle buttons */
|
||||||
.MuiToggleButtonGroup-root
|
.MuiToggleButtonGroup-root
|
||||||
{
|
{
|
||||||
@ -698,13 +711,96 @@ input[type="search"]::-webkit-search-results-decoration
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recordView .widget .recordGridWidget
|
|
||||||
{
|
|
||||||
margin: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MuiPickersDay-root.Mui-selected, .MuiPickersDay-root.MuiPickersDay-dayWithMargin:hover
|
.MuiPickersDay-root.Mui-selected, .MuiPickersDay-root.MuiPickersDay-dayWithMargin:hover
|
||||||
{
|
{
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #0062FF !important;
|
background-color: #0062FF !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* several styles below here for user-defined alert inside helpContent */
|
||||||
|
.helpContentAlert
|
||||||
|
{
|
||||||
|
padding: 6px 16px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.6;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert .MuiAlert-icon
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
margin-right: 12px;
|
||||||
|
padding: 7px 0;
|
||||||
|
font-size: 22px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert .MuiAlert-icon .material-icons-round
|
||||||
|
{
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert .MuiAlert-message
|
||||||
|
{
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert.success
|
||||||
|
{
|
||||||
|
background-color: rgb(240, 248, 241);
|
||||||
|
color: rgb(44, 76, 46);
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert.success .MuiAlert-icon .material-icons-round
|
||||||
|
{
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert.warning
|
||||||
|
{
|
||||||
|
background-color: rgb(254, 245, 234);
|
||||||
|
color: rgb(100, 65, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert.warning .MuiAlert-icon .material-icons-round
|
||||||
|
{
|
||||||
|
color: #fb8c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert.error
|
||||||
|
{
|
||||||
|
background-color: rgb(254, 239, 238);
|
||||||
|
color: rgb(98, 41, 37);
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert.error .MuiAlert-icon .material-icons-round
|
||||||
|
{
|
||||||
|
color: #F44335;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the alert widget, was built with minimal (no?) margins, for embedding in
|
||||||
|
a parent widget; but for using it on a process, give it some breathing room */
|
||||||
|
.processRun .widget .MuiAlert-root
|
||||||
|
{
|
||||||
|
margin: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default styles for a block widget overlay */
|
||||||
|
.blockWidgetOverlay
|
||||||
|
{
|
||||||
|
font-weight: 400;
|
||||||
|
position: relative;
|
||||||
|
top: 15px;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.blockWidgetOverlay a
|
||||||
|
{
|
||||||
|
color: #0062FF !important;
|
||||||
|
}
|
||||||
|
@ -56,8 +56,8 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
|
|||||||
"label": "Sample Table Widget",
|
"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",
|
"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": [
|
"columns": [
|
||||||
{ "type": "html", "header": "Id", "accessor": "id", "width": "1%" },
|
{ "type": "html", "header": "Id", "accessor": "id", "width": "30px" },
|
||||||
{ "type": "html", "header": "Name", "accessor": "name", "width": "99%" }
|
{ "type": "html", "header": "Name", "accessor": "name", "width": "1fr" }
|
||||||
],
|
],
|
||||||
"rows": [
|
"rows": [
|
||||||
{ "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" },
|
{ "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 //
|
// assert that the table widget rendered its header and some contents //
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget");
|
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");
|
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT");
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
@ -822,7 +822,7 @@
|
|||||||
"reportSetupWidget": {
|
"reportSetupWidget": {
|
||||||
"name": "reportSetupWidget",
|
"name": "reportSetupWidget",
|
||||||
"label": "Filters and Columns",
|
"label": "Filters and Columns",
|
||||||
"type": "reportSetup",
|
"type": "filterAndColumnsSetup",
|
||||||
"isCard": true,
|
"isCard": true,
|
||||||
"storeDropdownSelections": false,
|
"storeDropdownSelections": false,
|
||||||
"showReloadButton": true,
|
"showReloadButton": true,
|
||||||
|
Reference in New Issue
Block a user