mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-26 13:28:44 +00:00
Compare commits
64 Commits
snapshot-f
...
feature/18
Author | SHA1 | Date | |
---|---|---|---|
f5a3b9eb42
|
|||
461855dc3c
|
|||
1fd4780ea4
|
|||
ad0b9698b1
|
|||
d41f5f8339 | |||
4d30eb3060 | |||
d4a675e952 | |||
633c97b710 | |||
c70ef3dae8 | |||
5c69ae666c | |||
2e5aba6c16 | |||
185775ca4d | |||
cbcb3b505e | |||
ce91f68088 | |||
81da1a4627 | |||
b279a04b43 | |||
1f2e57d688 | |||
52bb7ba411 | |||
34c6f650b5 | |||
d792c23035 | |||
e3d30633f1 | |||
a6ee682671 | |||
c62252075f | |||
debc6f3ebf | |||
679375ba63 | |||
fb10dad803 | |||
c9a618c7f6 | |||
f654208769 | |||
3dacab8d60 | |||
13ce684d23 | |||
b67eea7d87 | |||
8ae3b95105 | |||
5a309e5628 | |||
67e1e78817 | |||
214b6b8af4 | |||
8ec0ce5455 | |||
07cb6fd323 | |||
3bb8451671 | |||
6076c4ddfd | |||
44a8810260 | |||
c69a4b8203 | |||
7db4f34ddd | |||
71dc3f3f65 | |||
ce22db2f89 | |||
aacb239164 | |||
219458ec63 | |||
59fdc72455 | |||
5c3ddb7dec | |||
d65c1fb5d8 | |||
19a63d6956 | |||
40f5b55307 | |||
7320b19fbb | |||
3f8a3e7e4d | |||
3ef2d64327 | |||
d793c23861 | |||
d0201d96e1 | |||
7b66ece466 | |||
02c163899a | |||
8fafe16a95 | |||
722c8d3bcf | |||
85acb612c9 | |||
74c634414a | |||
f8368b030c | |||
dda4ea4f4b |
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,6 +5,7 @@
|
|||||||
.yalc*
|
.yalc*
|
||||||
yalc.lock
|
yalc.lock
|
||||||
.env
|
.env
|
||||||
|
/certs
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
@ -30,3 +31,4 @@ yalc.lock
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
/src/main/resources/material-dashboard/
|
||||||
|
@ -154,7 +154,7 @@ material-dashboard-2-pro-react-ts
|
|||||||
│ │ ├── Cards
|
│ │ ├── Cards
|
||||||
│ │ ├── Charts
|
│ │ ├── Charts
|
||||||
│ │ ├── Configurator
|
│ │ ├── Configurator
|
||||||
│ │ ├── Footer
|
│ │ ├── FooterCard
|
||||||
│ │ ├── Items
|
│ │ ├── Items
|
||||||
│ │ ├── LayoutContainers
|
│ │ ├── LayoutContainers
|
||||||
│ │ ├── Lists
|
│ │ ├── Lists
|
||||||
|
23732
package-lock.json
generated
23732
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@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.113",
|
"@kingsrook/qqq-frontend-core": "1.0.122",
|
||||||
"@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",
|
||||||
@ -23,6 +23,8 @@
|
|||||||
"@types/react-dom": "18.0.0",
|
"@types/react-dom": "18.0.0",
|
||||||
"@types/react-router-hash-link": "2.4.5",
|
"@types/react-router-hash-link": "2.4.5",
|
||||||
"ace-builds": "1.12.3",
|
"ace-builds": "1.12.3",
|
||||||
|
"ajv": "^8.11.0",
|
||||||
|
"ajv-keywords": "^5.1.0",
|
||||||
"chart.js": "3.4.1",
|
"chart.js": "3.4.1",
|
||||||
"chroma-js": "2.4.2",
|
"chroma-js": "2.4.2",
|
||||||
"cmdk": "0.2.0",
|
"cmdk": "0.2.0",
|
||||||
@ -36,6 +38,8 @@
|
|||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
"jwt-decode": "3.1.2",
|
"jwt-decode": "3.1.2",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"oidc-client-ts": "2.4.1",
|
||||||
"rapidoc": "9.3.4",
|
"rapidoc": "9.3.4",
|
||||||
"react": "18.0.0",
|
"react": "18.0.0",
|
||||||
"react-ace": "10.1.0",
|
"react-ace": "10.1.0",
|
||||||
@ -49,6 +53,7 @@
|
|||||||
"react-github-btn": "1.2.1",
|
"react-github-btn": "1.2.1",
|
||||||
"react-google-drive-picker": "^1.2.0",
|
"react-google-drive-picker": "^1.2.0",
|
||||||
"react-markdown": "9.0.1",
|
"react-markdown": "9.0.1",
|
||||||
|
"react-oidc-context": "2.3.1",
|
||||||
"react-router-dom": "6.2.1",
|
"react-router-dom": "6.2.1",
|
||||||
"react-router-hash-link": "2.4.3",
|
"react-router-hash-link": "2.4.3",
|
||||||
"react-table": "7.7.0",
|
"react-table": "7.7.0",
|
||||||
@ -57,14 +62,15 @@
|
|||||||
"yup": "0.32.11"
|
"yup": "0.32.11"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "react-scripts build",
|
"build": "PUBLIC_URL=. 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 && npm dedupe --force",
|
"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": "PUBLIC_URL=. BROWSER=none react-scripts --max-http-header-size=65535 start",
|
||||||
"test": "react-scripts test"
|
"test": "react-scripts test",
|
||||||
|
"export": "rm -rf dist && PUBLIC_URL=. react-scripts build && rm -rf src/main/resources/material-dashboard && mkdir -p src/main/resources/material-dashboard && cp -r build/* src/main/resources/material-dashboard"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
4
pom.xml
4
pom.xml
@ -29,7 +29,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.24.0-SNAPSHOT</revision>
|
<revision>0.26.0-SNAPSHOT</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.21.0</version>
|
<version>0.26.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
|
228
src/App.tsx
228
src/App.tsx
@ -19,7 +19,6 @@
|
|||||||
* 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 {useAuth0} from "@auth0/auth0-react";
|
|
||||||
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
import {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
||||||
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
|
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
|
||||||
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
|
import {QAppTreeNode} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppTreeNode";
|
||||||
@ -29,16 +28,20 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
|
|||||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import Avatar from "@mui/material/Avatar";
|
import Avatar from "@mui/material/Avatar";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {ThemeProvider} from "@mui/material/styles";
|
import {ThemeProvider} from "@mui/material/styles";
|
||||||
import {LicenseInfo} from "@mui/x-license-pro";
|
import {LicenseInfo} from "@mui/x-license-pro";
|
||||||
import CommandMenu from "CommandMenu";
|
import CommandMenu from "CommandMenu";
|
||||||
import jwt_decode from "jwt-decode";
|
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
|
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||||
|
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||||
|
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||||
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
import Sidenav from "qqq/components/horseshoe/sidenav/SideNav";
|
||||||
import theme from "qqq/components/legacy/Theme";
|
import theme from "qqq/components/legacy/Theme";
|
||||||
import {setMiniSidenav, setOpenConfigurator, useMaterialUIController} from "qqq/context";
|
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||||
|
import {setMiniSidenav, useMaterialUIController} from "qqq/context";
|
||||||
import AppHome from "qqq/pages/apps/Home";
|
import AppHome from "qqq/pages/apps/Home";
|
||||||
import NoApps from "qqq/pages/apps/NoApps";
|
import NoApps from "qqq/pages/apps/NoApps";
|
||||||
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
import ProcessRun from "qqq/pages/processes/ProcessRun";
|
||||||
@ -62,10 +65,14 @@ import {Md5} from "ts-md5/dist/md5";
|
|||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
export const SESSION_UUID_COOKIE_NAME = "sessionUUID";
|
||||||
|
|
||||||
export default function App()
|
interface Props
|
||||||
{
|
{
|
||||||
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
authenticationMetaData: QAuthenticationMetaData;
|
||||||
const {user, getAccessTokenSilently, logout} = useAuth0();
|
}
|
||||||
|
|
||||||
|
export default function App({authenticationMetaData}: Props)
|
||||||
|
{
|
||||||
|
const [, , removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
const [loadingToken, setLoadingToken] = useState(false);
|
const [loadingToken, setLoadingToken] = useState(false);
|
||||||
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
const [isFullyAuthenticated, setIsFullyAuthenticated] = useState(false);
|
||||||
const [profileRoutes, setProfileRoutes] = useState({});
|
const [profileRoutes, setProfileRoutes] = useState({});
|
||||||
@ -74,68 +81,20 @@ export default function App()
|
|||||||
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
const [needLicenseKey, setNeedLicenseKey] = useState(true);
|
||||||
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
const [loggedInUser, setLoggedInUser] = useState({} as { name?: string, email?: string });
|
||||||
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
const [defaultRoute, setDefaultRoute] = useState("/no-apps");
|
||||||
|
const [earlyReturnForAuth, setEarlyReturnForAuth] = useState(null as JSX.Element);
|
||||||
|
|
||||||
|
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||||
|
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext: authenticationMetaData.type === "OAUTH2"});
|
||||||
|
const {setupSession: anonymousSetupSession, logout: anonymousLogout} = useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////
|
||||||
// tell the client how to do a logout if it sees a 401 //
|
// tell the client how to do a logout if it sees a 401 //
|
||||||
/////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////
|
||||||
Client.setUnauthorizedCallback(() =>
|
Client.setUnauthorizedCallback(() => doLogout());
|
||||||
{
|
|
||||||
logout();
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
|
||||||
{
|
|
||||||
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
|
||||||
{
|
|
||||||
console.log("No session uuid cookie - so we should store a new one.");
|
|
||||||
return (true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oldToken)
|
|
||||||
{
|
|
||||||
console.log("No accessToken in localStorage - so we should store a new one.");
|
|
||||||
return (true);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const oldJSON: any = jwt_decode(oldToken);
|
|
||||||
const newJSON: any = jwt_decode(newToken);
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// if the old (local storage) token is expired, then we need to store the new one //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
const oldExp = oldJSON["exp"];
|
|
||||||
if (oldExp * 1000 < (new Date().getTime()))
|
|
||||||
{
|
|
||||||
console.log("Access token in local storage was expired - so we should store a new one.");
|
|
||||||
return (true);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
|
||||||
// note, this is only deleting them from what we compare, not from what we'd store. //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
delete newJSON["exp"];
|
|
||||||
delete newJSON["iat"];
|
|
||||||
delete oldJSON["exp"];
|
|
||||||
delete oldJSON["iat"];
|
|
||||||
|
|
||||||
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
|
||||||
if (different)
|
|
||||||
{
|
|
||||||
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
|
||||||
}
|
|
||||||
return (different);
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
console.log("Caught in shouldStoreNewToken: " + e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////
|
||||||
|
// deal with making sure user is authenticated //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (loadingToken)
|
if (loadingToken)
|
||||||
@ -146,65 +105,17 @@ export default function App()
|
|||||||
|
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData();
|
|
||||||
|
|
||||||
if (authenticationMetaData.type === "AUTH_0")
|
if (authenticationMetaData.type === "AUTH_0")
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////
|
await auth0SetupSession();
|
||||||
// use auth0 if auth type is ... auth0 //
|
|
||||||
/////////////////////////////////////////
|
|
||||||
try
|
|
||||||
{
|
|
||||||
console.log("Loading token from auth0...");
|
|
||||||
const accessToken = await getAccessTokenSilently();
|
|
||||||
|
|
||||||
const lsAccessToken = localStorage.getItem("accessToken");
|
|
||||||
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
|
||||||
{
|
|
||||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
|
||||||
const {uuid: newSessionUuid, values} = await qController.manageSession(accessToken, null);
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// the request to the backend should send a header to set the cookie, so we don't need to do it ourselves. //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// setCookie(SESSION_UUID_COOKIE_NAME, newSessionUuid, {path: "/"});
|
|
||||||
|
|
||||||
localStorage.setItem("accessToken", accessToken);
|
|
||||||
localStorage.setItem("sessionValues", JSON.stringify(values));
|
|
||||||
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
|
||||||
}
|
}
|
||||||
else
|
else if (authenticationMetaData.type === "OAUTH2")
|
||||||
{
|
{
|
||||||
console.log("Using existing sessionUUID cookie");
|
await oauth2SetupSession();
|
||||||
}
|
|
||||||
|
|
||||||
setIsFullyAuthenticated(true);
|
|
||||||
qController.setGotAuthentication();
|
|
||||||
|
|
||||||
setLoggedInUser(user);
|
|
||||||
console.log("Token load complete.");
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
|
||||||
qController.clearAuthenticationMetaDataLocalStorage();
|
|
||||||
localStorage.removeItem("accessToken");
|
|
||||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
|
||||||
logout();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////
|
await anonymousSetupSession();
|
||||||
// use a random token if anonymous or mock //
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
console.log("Generating random token...");
|
|
||||||
setIsFullyAuthenticated(true);
|
|
||||||
qController.setGotAuthentication();
|
|
||||||
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
|
||||||
console.log("Token generation complete.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -220,13 +131,36 @@ export default function App()
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const metaData: QInstance = await qController.loadMetaData();
|
const metaData: QInstance = await qController.loadMetaData();
|
||||||
LicenseInfo.setLicenseKey(metaData.environmentValues.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
|
LicenseInfo.setLicenseKey(metaData.environmentValues?.get("MATERIAL_UI_LICENSE_KEY") || process.env.REACT_APP_MATERIAL_UI_LICENSE_KEY);
|
||||||
setNeedLicenseKey(false);
|
setNeedLicenseKey(false);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** call appropriate logout function based on authentication meta data type
|
||||||
|
***************************************************************************/
|
||||||
|
function doLogout()
|
||||||
|
{
|
||||||
|
if (authenticationMetaData?.type === "AUTH_0")
|
||||||
|
{
|
||||||
|
auth0Logout();
|
||||||
|
}
|
||||||
|
else if (authenticationMetaData?.type === "OAUTH2")
|
||||||
|
{
|
||||||
|
oauth2Logout();
|
||||||
|
}
|
||||||
|
else if (authenticationMetaData?.type === "FULLY_ANONYMOUS" || authenticationMetaData?.type === "MOCK")
|
||||||
|
{
|
||||||
|
anonymousLogout();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log(`No logout callback for authentication type [${authenticationMetaData?.type}].`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [controller, dispatch] = useMaterialUIController();
|
const [controller, dispatch] = useMaterialUIController();
|
||||||
const {miniSidenav, direction, layout, openConfigurator, sidenavColor} = controller;
|
const {miniSidenav, direction, sidenavColor} = controller;
|
||||||
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
const [onMouseEnter, setOnMouseEnter] = useState(false);
|
||||||
const {pathname} = useLocation();
|
const {pathname} = useLocation();
|
||||||
const [queryParams] = useSearchParams();
|
const [queryParams] = useSearchParams();
|
||||||
@ -519,11 +453,10 @@ export default function App()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let profileRoutes = {};
|
|
||||||
const gravatarBase = "https://www.gravatar.com/avatar/";
|
const gravatarBase = "https://www.gravatar.com/avatar/";
|
||||||
const hash = Md5.hashStr(loggedInUser?.email || "user");
|
const hash = Md5.hashStr(loggedInUser?.email || "user");
|
||||||
const profilePicture = `${gravatarBase}${hash}`;
|
const profilePicture = `${gravatarBase}${hash}`;
|
||||||
profileRoutes = {
|
const profileRoutes = {
|
||||||
type: "collapse",
|
type: "collapse",
|
||||||
name: loggedInUser?.name ?? "Anonymous",
|
name: loggedInUser?.name ?? "Anonymous",
|
||||||
key: "username",
|
key: "username",
|
||||||
@ -592,10 +525,7 @@ export default function App()
|
|||||||
localStorage.removeItem("accessToken");
|
localStorage.removeItem("accessToken");
|
||||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
|
||||||
//////////////////////////////////////////////////////
|
doLogout();
|
||||||
// todo - this is auth0 logout... make more generic //
|
|
||||||
//////////////////////////////////////////////////////
|
|
||||||
logout();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -603,7 +533,9 @@ export default function App()
|
|||||||
})();
|
})();
|
||||||
}, [needToLoadRoutes, isFullyAuthenticated]);
|
}, [needToLoadRoutes, isFullyAuthenticated]);
|
||||||
|
|
||||||
// Open sidenav when mouse enter on mini sidenav
|
///////////////////////////////////////////////////
|
||||||
|
// Open sidenav when mouse enter on mini sidenav //
|
||||||
|
///////////////////////////////////////////////////
|
||||||
const handleOnMouseEnter = () =>
|
const handleOnMouseEnter = () =>
|
||||||
{
|
{
|
||||||
if (miniSidenav && !onMouseEnter)
|
if (miniSidenav && !onMouseEnter)
|
||||||
@ -613,7 +545,9 @@ export default function App()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close sidenav when mouse leave mini sidenav
|
/////////////////////////////////////////////////
|
||||||
|
// Close sidenav when mouse leave mini sidenav //
|
||||||
|
/////////////////////////////////////////////////
|
||||||
const handleOnMouseLeave = () =>
|
const handleOnMouseLeave = () =>
|
||||||
{
|
{
|
||||||
if (onMouseEnter)
|
if (onMouseEnter)
|
||||||
@ -623,16 +557,14 @@ export default function App()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Change the openConfigurator state
|
|
||||||
const handleConfiguratorOpen = () => setOpenConfigurator(dispatch, !openConfigurator);
|
|
||||||
|
|
||||||
// Setting the dir attribute for the body element
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
document.body.setAttribute("dir", direction);
|
document.body.setAttribute("dir", direction);
|
||||||
}, [direction]);
|
}, [direction]);
|
||||||
|
|
||||||
// Setting page scroll to 0 when changing the route
|
//////////////////////////////////////////////////////
|
||||||
|
// Setting page scroll to 0 when changing the route //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
document.documentElement.scrollTop = 0;
|
document.documentElement.scrollTop = 0;
|
||||||
@ -672,12 +604,12 @@ export default function App()
|
|||||||
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
const [dotMenuOpen, setDotMenuOpen] = useState(false);
|
||||||
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
|
||||||
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
const [helpHelpActive] = useState(queryParams.has("helpHelp"));
|
||||||
const [userId, setUserId] = useState(user?.email);
|
const [userId, setUserId] = useState(loggedInUser?.email);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setUserId(user?.email)
|
setUserId(loggedInUser?.email);
|
||||||
}, [user]);
|
}, [loggedInUser]);
|
||||||
|
|
||||||
|
|
||||||
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
|
const [googleAnalyticsUtils] = useState(new GoogleAnalyticsUtils());
|
||||||
@ -687,9 +619,35 @@ export default function App()
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function recordAnalytics(model: AnalyticsModel)
|
function recordAnalytics(model: AnalyticsModel)
|
||||||
{
|
{
|
||||||
googleAnalyticsUtils.recordAnalytics(model)
|
googleAnalyticsUtils.recordAnalytics(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
// if any of the auth/session setup code determined that we need //
|
||||||
|
// to render something and return early - then do so here. //
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
if (earlyReturnForAuth)
|
||||||
|
{
|
||||||
|
return (earlyReturnForAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function banner(): JSX.Element | null
|
||||||
|
{
|
||||||
|
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_SITE");
|
||||||
|
|
||||||
|
if (!banner)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", position: "sticky", top: "0", zIndex: 1, ...getBannerStyles(banner)}}>
|
||||||
|
{makeBannerContent(banner)}
|
||||||
|
</Box>);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
@ -718,6 +676,7 @@ export default function App()
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<CommandMenu metaData={metaData} />
|
<CommandMenu metaData={metaData} />
|
||||||
|
{banner()}
|
||||||
<Sidenav
|
<Sidenav
|
||||||
color={sidenavColor}
|
color={sidenavColor}
|
||||||
icon={branding.icon}
|
icon={branding.icon}
|
||||||
@ -727,6 +686,7 @@ export default function App()
|
|||||||
routes={sideNavRoutes}
|
routes={sideNavRoutes}
|
||||||
onMouseEnter={handleOnMouseEnter}
|
onMouseEnter={handleOnMouseEnter}
|
||||||
onMouseLeave={handleOnMouseLeave}
|
onMouseLeave={handleOnMouseLeave}
|
||||||
|
logout={doLogout}
|
||||||
/>
|
/>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="*" element={<Navigate to={defaultRoute} />} />
|
<Route path="*" element={<Navigate to={defaultRoute} />} />
|
||||||
|
149
src/index.tsx
149
src/index.tsx
@ -19,116 +19,111 @@
|
|||||||
* 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 {Auth0Provider} from "@auth0/auth0-react";
|
|
||||||
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
import React from "react";
|
|
||||||
import {createRoot} from "react-dom/client";
|
|
||||||
import {BrowserRouter, useNavigate, useSearchParams} from "react-router-dom";
|
|
||||||
import App from "App";
|
import App from "App";
|
||||||
import "qqq/styles/qqq-override-styles.css";
|
import "qqq/styles/qqq-override-styles.css";
|
||||||
import "qqq/styles/globals.scss";
|
import "qqq/styles/globals.scss";
|
||||||
import "qqq/styles/raycast.scss";
|
import "qqq/styles/raycast.scss";
|
||||||
import HandleAuthorizationError from "HandleAuthorizationError";
|
import useAnonymousAuthenticationModule from "qqq/authorization/anonymous/useAnonymousAuthenticationModule";
|
||||||
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0AuthenticationModule";
|
||||||
|
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
||||||
import {MaterialUIControllerProvider} from "qqq/context";
|
import {MaterialUIControllerProvider} from "qqq/context";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import React from "react";
|
||||||
|
import {createRoot} from "react-dom/client";
|
||||||
|
import {BrowserRouter} from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
if(document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
function getBasePath(): string
|
||||||
{
|
{
|
||||||
qController.clearAuthenticationMetaDataLocalStorage()
|
// You can change this logic depending on how you detect your mount point
|
||||||
|
const path = window.location.pathname;
|
||||||
|
|
||||||
|
console.warn("Using hacked base path for QQQ application, please update this code to be better : path ["+ path +"].");
|
||||||
|
|
||||||
|
// Example: If app is deployed at /admin or /portal
|
||||||
|
if (path.startsWith("/admin")) return "/admin";
|
||||||
|
if (path.startsWith("/portal")) return "/portal"; // TODO: This is all temporary, we need to fix this properly
|
||||||
|
|
||||||
|
return "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData()
|
if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
|
||||||
|
{
|
||||||
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticationMetaDataPromise: Promise<QAuthenticationMetaData> = qController.getAuthenticationMetaData();
|
||||||
|
|
||||||
authenticationMetaDataPromise.then((authenticationMetaData) =>
|
authenticationMetaDataPromise.then((authenticationMetaData) =>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
|
||||||
function Auth0ProviderWithRedirectCallback({children, ...props})
|
|
||||||
{
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
// @ts-ignore
|
/***************************************************************************
|
||||||
const onRedirectCallback = (appState) =>
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function Auth0RouterBody()
|
||||||
{
|
{
|
||||||
navigate((appState && appState.returnTo) || window.location.pathname);
|
const {renderAppWrapper} = useAuth0AuthenticationModule({});
|
||||||
};
|
return (renderAppWrapper(authenticationMetaData));
|
||||||
if (searchParams.get("error"))
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function OAuth2RouterBody()
|
||||||
{
|
{
|
||||||
return (
|
const {renderAppWrapper} = useOAuth2AuthenticationModule({inOAuthContext: false});
|
||||||
// @ts-ignore
|
return (renderAppWrapper(authenticationMetaData, (
|
||||||
<Auth0Provider {...props}>
|
<MaterialUIControllerProvider>
|
||||||
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
|
<App authenticationMetaData={authenticationMetaData} />
|
||||||
</Auth0Provider>
|
</MaterialUIControllerProvider>
|
||||||
);
|
)));
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function AnonymousRouterBody()
|
||||||
{
|
{
|
||||||
return (
|
const {renderAppWrapper} = useAnonymousAuthenticationModule({});
|
||||||
// @ts-ignore
|
return (renderAppWrapper(authenticationMetaData, (
|
||||||
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
|
<MaterialUIControllerProvider>
|
||||||
{children}
|
<App authenticationMetaData={authenticationMetaData} />
|
||||||
</Auth0Provider>
|
</MaterialUIControllerProvider>
|
||||||
);
|
)));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
if (authenticationMetaData.type === "AUTH_0")
|
if (authenticationMetaData.type === "AUTH_0")
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
root.render(<BrowserRouter basename={getBasePath()}>
|
||||||
let domain: string = authenticationMetaData.data.baseUrl;
|
<Auth0RouterBody />
|
||||||
|
</BrowserRouter>);
|
||||||
// @ts-ignore
|
|
||||||
const clientId = authenticationMetaData.data.clientId;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const audience = authenticationMetaData.data.audience;
|
|
||||||
|
|
||||||
if(!domain || !clientId)
|
|
||||||
{
|
|
||||||
root.render(
|
|
||||||
<div>Error: AUTH0 authenticationMetaData is missing domain [{domain}] and/or clientId [{clientId}].</div>
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else if (authenticationMetaData.type === "OAUTH2")
|
||||||
if(domain.endsWith("/"))
|
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
root.render(<BrowserRouter basename={getBasePath()}>
|
||||||
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
|
<OAuth2RouterBody />
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
</BrowserRouter>);
|
||||||
domain = domain.replace(/\/$/, "");
|
|
||||||
}
|
}
|
||||||
|
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
|
||||||
root.render(
|
{
|
||||||
<BrowserRouter>
|
root.render(<BrowserRouter basename={getBasePath()}>
|
||||||
<Auth0ProviderWithRedirectCallback
|
<AnonymousRouterBody />
|
||||||
domain={domain}
|
</BrowserRouter>);
|
||||||
clientId={clientId}
|
|
||||||
audience={audience}
|
|
||||||
redirectUri={`${window.location.origin}/`}
|
|
||||||
>
|
|
||||||
<MaterialUIControllerProvider>
|
|
||||||
<ProtectedRoute component={App} />
|
|
||||||
</MaterialUIControllerProvider>
|
|
||||||
</Auth0ProviderWithRedirectCallback>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
root.render(
|
root.render(<div>
|
||||||
<BrowserRouter>
|
Error: Unknown authenticationMetaData type: [{authenticationMetaData.type}].
|
||||||
<MaterialUIControllerProvider>
|
</div>);
|
||||||
<App />
|
|
||||||
</MaterialUIControllerProvider>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* QQQ - Low-code Application Framework for Engineers.
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
* contact@kingsrook.com
|
* contact@kingsrook.com
|
||||||
* https://github.com/Kingsrook/
|
* https://github.com/Kingsrook/
|
||||||
@ -19,20 +19,18 @@
|
|||||||
* 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 {useAuth0} from "@auth0/auth0-react";
|
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
||||||
import {Button} from "@mui/material";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
function AuthenticationButton()
|
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.branding.BannerSlot;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public enum MaterialDashboardBannerSlots implements BannerSlot
|
||||||
{
|
{
|
||||||
const {loginWithRedirect, logout, isAuthenticated} = useAuth0();
|
QFMD_TOP_OF_SITE,
|
||||||
|
QFMD_TOP_OF_BODY,
|
||||||
if (isAuthenticated)
|
QFMD_SIDE_NAV_UNDER_LOGO
|
||||||
{
|
|
||||||
return <Button onClick={() => logout({returnTo: window.location.origin})}>Log Out</Button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Button onClick={() => loginWithRedirect()}>Log In</Button>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AuthenticationButton;
|
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. 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 {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
|
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import {useCookies} from "react-cookie";
|
||||||
|
import {Md5} from "ts-md5/dist/md5";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||||
|
setLoggedInUser?: (user: any) => void;
|
||||||
|
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** hook for working with the anonymous authentication module
|
||||||
|
***************************************************************************/
|
||||||
|
export default function useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
|
||||||
|
{
|
||||||
|
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const setupSession = async () =>
|
||||||
|
{
|
||||||
|
console.log("Generating random token...");
|
||||||
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
setCookie(SESSION_UUID_COOKIE_NAME, Md5.hashStr(`${new Date()}`), {path: "/"});
|
||||||
|
console.log("Token generation complete.");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const logout = () =>
|
||||||
|
{
|
||||||
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||||
|
{
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
setupSession,
|
||||||
|
logout,
|
||||||
|
renderAppWrapper
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
252
src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
Normal file
252
src/qqq/authorization/auth0/useAuth0AuthenticationModule.tsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. 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 {Auth0Provider, useAuth0} from "@auth0/auth0-react";
|
||||||
|
import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
|
import App, {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
import HandleAuthorizationError from "HandleAuthorizationError";
|
||||||
|
import jwt_decode from "jwt-decode";
|
||||||
|
import ProtectedRoute from "qqq/authorization/auth0/ProtectedRoute";
|
||||||
|
import {MaterialUIControllerProvider} from "qqq/context";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import {useCookies} from "react-cookie";
|
||||||
|
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||||
|
setLoggedInUser?: (user: any) => void;
|
||||||
|
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** hook for working with the Auth0 authentication module
|
||||||
|
***************************************************************************/
|
||||||
|
export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser}: Props)
|
||||||
|
{
|
||||||
|
const {user: auth0User, getAccessTokenSilently: auth0GetAccessTokenSilently, logout: useAuth0Logout} = useAuth0();
|
||||||
|
|
||||||
|
const [cookies, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const shouldStoreNewToken = (newToken: string, oldToken: string): boolean =>
|
||||||
|
{
|
||||||
|
if (!cookies[SESSION_UUID_COOKIE_NAME])
|
||||||
|
{
|
||||||
|
console.log("No session uuid cookie - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oldToken)
|
||||||
|
{
|
||||||
|
console.log("No accessToken in localStorage - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const oldJSON: any = jwt_decode(oldToken);
|
||||||
|
const newJSON: any = jwt_decode(newToken);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the old (local storage) token is expired, then we need to store the new one //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const oldExp = oldJSON["exp"];
|
||||||
|
if (oldExp * 1000 < (new Date().getTime()))
|
||||||
|
{
|
||||||
|
console.log("Access token in local storage was expired - so we should store a new one.");
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// remove the exp & iat values from what we compare - as they are always different from auth0 //
|
||||||
|
// note, this is only deleting them from what we compare, not from what we'd store. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
delete newJSON["exp"];
|
||||||
|
delete newJSON["iat"];
|
||||||
|
delete oldJSON["exp"];
|
||||||
|
delete oldJSON["iat"];
|
||||||
|
|
||||||
|
const different = JSON.stringify(newJSON) !== JSON.stringify(oldJSON);
|
||||||
|
if (different)
|
||||||
|
{
|
||||||
|
console.log("Latest access token from auth0 has changed vs localStorage - so we should store a new one.");
|
||||||
|
}
|
||||||
|
return (different);
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log("Caught in shouldStoreNewToken: " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const setupSession = async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
console.log("Loading token from auth0...");
|
||||||
|
const accessToken = await auth0GetAccessTokenSilently();
|
||||||
|
|
||||||
|
const lsAccessToken = localStorage.getItem("accessToken");
|
||||||
|
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||||
|
{
|
||||||
|
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||||
|
const {uuid: values} = await qController.manageSession(accessToken, null);
|
||||||
|
|
||||||
|
localStorage.setItem("accessToken", accessToken);
|
||||||
|
localStorage.setItem("sessionValues", JSON.stringify(values));
|
||||||
|
console.log("Got new sessionUUID from backend, and stored new accessToken");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log("Using existing sessionUUID cookie");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
|
||||||
|
setLoggedInUser(auth0User);
|
||||||
|
console.log("Token load complete.");
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||||
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
localStorage.removeItem("accessToken");
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
useAuth0Logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const logout = () =>
|
||||||
|
{
|
||||||
|
useAuth0Logout({returnTo: window.location.origin});
|
||||||
|
};
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
// @ts-ignore
|
||||||
|
function Auth0ProviderWithRedirectCallback({children, ...props})
|
||||||
|
{
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const onRedirectCallback = (appState) =>
|
||||||
|
{
|
||||||
|
navigate((appState && appState.returnTo) || window.location.pathname);
|
||||||
|
};
|
||||||
|
if (searchParams.get("error"))
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<Auth0Provider {...props}>
|
||||||
|
<HandleAuthorizationError errorMessage={searchParams.get("error_description")} />
|
||||||
|
</Auth0Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<Auth0Provider onRedirectCallback={onRedirectCallback} {...props}>
|
||||||
|
{children}
|
||||||
|
</Auth0Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData): JSX.Element =>
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
let domain: string = authenticationMetaData.data.baseUrl;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const clientId = authenticationMetaData.data.clientId;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const audience = authenticationMetaData.data.audience;
|
||||||
|
|
||||||
|
if (!domain || !clientId)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div>Error: AUTH0 authenticationMetaData is missing baseUrl [{domain}] and/or clientId [{clientId}].</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.endsWith("/"))
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// auth0 lib fails if we have a trailing slash. be a bit more graceful than that. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
domain = domain.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** simple Functional Component to wrap the <App> and pass the authentication-
|
||||||
|
** MetaData prop in, so a simple Component can be passed into ProtectedRoute
|
||||||
|
***************************************************************************/
|
||||||
|
function WrappedApp()
|
||||||
|
{
|
||||||
|
return <App authenticationMetaData={authenticationMetaData} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Auth0ProviderWithRedirectCallback
|
||||||
|
domain={domain}
|
||||||
|
clientId={clientId}
|
||||||
|
audience={audience}
|
||||||
|
redirectUri={`${window.location.origin}/`}>
|
||||||
|
<MaterialUIControllerProvider>
|
||||||
|
<ProtectedRoute component={WrappedApp} />
|
||||||
|
</MaterialUIControllerProvider>
|
||||||
|
</Auth0ProviderWithRedirectCallback>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
setupSession,
|
||||||
|
logout,
|
||||||
|
renderAppWrapper
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
188
src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
Normal file
188
src/qqq/authorization/oauth2/useOAuth2AuthenticationModule.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. 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 {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAuthenticationMetaData";
|
||||||
|
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import {useCookies} from "react-cookie";
|
||||||
|
import {AuthContextProps, AuthProvider, useAuth} from "react-oidc-context";
|
||||||
|
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
|
interface Props
|
||||||
|
{
|
||||||
|
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||||
|
setLoggedInUser?: (user: any) => void;
|
||||||
|
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||||
|
inOAuthContext: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** hook for working with the OAuth2 authentication module
|
||||||
|
***************************************************************************/
|
||||||
|
export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext}: Props)
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// the useAuth hook should only be called if we're inside the <AuthProvider> element //
|
||||||
|
// so on the page that uses this hook to call renderAppWrapper, we aren't in that //
|
||||||
|
// element/context, thus, don't call that hook. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const authOidc: AuthContextProps | null = inOAuthContext ? useAuth() : null;
|
||||||
|
|
||||||
|
const [cookies, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const setupSession = async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const preSigninRedirectPathnameKey = "oauth2.preSigninRedirect.pathname";
|
||||||
|
if (window.location.pathname == "/token")
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// if we're at a path of /token, get code & state params, look up values //
|
||||||
|
// from that state in local storage, and make a post to the backend to //
|
||||||
|
// with these values - which will itself talk to the identity provider //
|
||||||
|
// to get an access token, and ultimately a session. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
const code = searchParams.get("code");
|
||||||
|
const state = searchParams.get("state");
|
||||||
|
const oidcString = localStorage.getItem(`oidc.${state}`);
|
||||||
|
if (oidcString)
|
||||||
|
{
|
||||||
|
const oidcObject = JSON.parse(oidcString) as { [name: string]: any };
|
||||||
|
console.log(oidcObject);
|
||||||
|
const manageSessionRequestBody = {code: code, codeVerifier: oidcObject.code_verifier, redirectUri: oidcObject.redirect_uri};
|
||||||
|
const {uuid: newSessionUuid, values} = await qController.manageSession(null, null, manageSessionRequestBody);
|
||||||
|
console.log(`we have new session UUID: ${newSessionUuid}`);
|
||||||
|
|
||||||
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
|
||||||
|
setLoggedInUser(values?.user);
|
||||||
|
console.log("Token load complete.");
|
||||||
|
|
||||||
|
const preSigninRedirectPathname = localStorage.getItem(preSigninRedirectPathnameKey);
|
||||||
|
localStorage.removeItem(preSigninRedirectPathname);
|
||||||
|
navigate(preSigninRedirectPathname ?? "/", {replace: true});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// if unrecognized state, render an error //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
setEarlyReturnForAuth(<div>Login error: Unrecognized state. Refresh to try again.</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// if we have a sessionUUID cookie, try to validate it with the backend //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
const sessionUuid = cookies[SESSION_UUID_COOKIE_NAME];
|
||||||
|
if (sessionUuid)
|
||||||
|
{
|
||||||
|
console.log(`we have session UUID: ${sessionUuid} - validating it...`);
|
||||||
|
const {values} = await qController.manageSession(null, sessionUuid, null);
|
||||||
|
|
||||||
|
setIsFullyAuthenticated(true);
|
||||||
|
qController.setGotAuthentication();
|
||||||
|
|
||||||
|
setLoggedInUser(values?.user);
|
||||||
|
console.log("Token load complete.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// else no cookie, and not a token url, we need to redirect to the provider's login page //
|
||||||
|
// capture the path the user was trying to access in local storage, to redirect back to later. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
console.log("Loading token from OAuth2 provider...");
|
||||||
|
console.log(authOidc);
|
||||||
|
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
|
||||||
|
setEarlyReturnForAuth(<div>Signing in...</div>);
|
||||||
|
authOidc?.signinRedirect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log(`Error loading token: ${JSON.stringify(e)}`);
|
||||||
|
logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const logout = () =>
|
||||||
|
{
|
||||||
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
|
authOidc?.signoutRedirect();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
||||||
|
{
|
||||||
|
const authority: string = authenticationMetaData.data.baseUrl;
|
||||||
|
const clientId = authenticationMetaData.data.clientId;
|
||||||
|
|
||||||
|
if (!authority || !clientId)
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<div>Error: OAuth2 authenticationMetaData is missing baseUrl [{authority}] and/or clientId [{clientId}].</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oidcConfig =
|
||||||
|
{
|
||||||
|
authority: authority,
|
||||||
|
client_id: clientId,
|
||||||
|
redirect_uri: `${window.location.origin}/token`,
|
||||||
|
response_type: "code",
|
||||||
|
scope: "openid profile email offline_access",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (<AuthProvider {...oidcConfig}>
|
||||||
|
{children}
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
setupSession,
|
||||||
|
logout,
|
||||||
|
renderAppWrapper
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -23,8 +23,10 @@ import {Chip} from "@mui/material";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import {makeStyles} from "@mui/styles";
|
import {makeStyles} from "@mui/styles";
|
||||||
import Downshift from "downshift";
|
import Downshift from "downshift";
|
||||||
|
import {debounce} from "lodash";
|
||||||
import {arrayOf, func, string} from "prop-types";
|
import {arrayOf, func, string} from "prop-types";
|
||||||
import React, {useEffect, useState} from "react";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: any) => ({
|
const useStyles = makeStyles((theme: any) => ({
|
||||||
chip: {
|
chip: {
|
||||||
@ -34,21 +36,99 @@ const useStyles = makeStyles((theme: any) => ({
|
|||||||
|
|
||||||
function ChipTextField({...props})
|
function ChipTextField({...props})
|
||||||
{
|
{
|
||||||
|
const qController = Client.getInstance();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const {handleChipChange, label, chipType, disabled, placeholder, chipData, multiline, rows, ...other} = props;
|
const {table, field, handleChipChange, label, chipType, disabled, placeholder, chipData, multiline, rows, ...other} = props;
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [chips, setChips] = useState([]);
|
const [chips, setChips] = useState([]);
|
||||||
|
const [chipColors, setChipColors] = useState([]);
|
||||||
|
const [chipValidity, setChipValidity] = useState([] as boolean[]);
|
||||||
|
const [chipPVSIds, setChipPVSIds] = useState([] as any[]);
|
||||||
|
const [isMakingRequest, setIsMakingRequest] = useState(false);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
// these refs are used for the async api call for possible values //
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
const chipsRef = useRef<string[]>([]);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// use debounce library to not flood server as user types, wait a second before requesting //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
async function fetchPVSLabelsAndColorChips()
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// make a request for the possible value labels (chips) //
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
setIsMakingRequest(true);
|
||||||
|
const currentChips = chipsRef.current;
|
||||||
|
setChipColors([]);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Determine chip colors based on whether each chip value appears in results //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
const newChipColors = [] as string[];
|
||||||
|
const chipValidity = [] as boolean[];
|
||||||
|
const chipPVSIds = [] as any[];
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// make the request for all 'chips' with pagination to handle large sizes //
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
const BATCH_SIZE = 250;
|
||||||
|
for (let i = 0; i < currentChips.length; i += BATCH_SIZE)
|
||||||
|
{
|
||||||
|
const batch = currentChips.slice(i, i + BATCH_SIZE);
|
||||||
|
const page = await qController.possibleValues(
|
||||||
|
table.name,
|
||||||
|
null,
|
||||||
|
field.name,
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
batch
|
||||||
|
);
|
||||||
|
for (let j = 0; j < batch.length; j++)
|
||||||
|
{
|
||||||
|
let found = false;
|
||||||
|
for (let k = 0; k < page.length; k++)
|
||||||
|
{
|
||||||
|
const result = page[k];
|
||||||
|
if (result.label.toLowerCase() === batch[j].toLowerCase())
|
||||||
|
{
|
||||||
|
chipPVSIds.push(result.id);
|
||||||
|
newChipColors.push("info");
|
||||||
|
chipValidity.push(true);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
{
|
||||||
|
chipPVSIds.push(null);
|
||||||
|
chipValidity.push(false);
|
||||||
|
newChipColors.push("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setChipPVSIds(chipPVSIds);
|
||||||
|
setChipColors(newChipColors);
|
||||||
|
setChipValidity(chipValidity);
|
||||||
|
setIsMakingRequest(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedApiCall = useRef(debounce(fetchPVSLabelsAndColorChips, 500)).current;
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
setChips(chipData);
|
setChips(chipData);
|
||||||
}, [chipData]);
|
chipsRef.current = chipData;
|
||||||
|
determineChipColors();
|
||||||
|
}, [JSON.stringify(chipData)]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
handleChipChange(chips);
|
handleChipChange(isMakingRequest, chipValidity, chipPVSIds);
|
||||||
}, [chips, handleChipChange]);
|
}, [chipValidity, chipPVSIds, isMakingRequest]);
|
||||||
|
|
||||||
function handleKeyDown(event: any)
|
function handleKeyDown(event: any)
|
||||||
{
|
{
|
||||||
@ -64,13 +144,16 @@ function ChipTextField({...props})
|
|||||||
setInputValue("");
|
setInputValue("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!event.target.value.replace(/\s/g, "").length) return;
|
if (!event.target.value.replace(/\s/g, "").length)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInputValue("");
|
||||||
newChipList.push(event.target.value.trim());
|
newChipList.push(event.target.value.trim());
|
||||||
setChips(newChipList);
|
setChips(newChipList);
|
||||||
setInputValue("");
|
|
||||||
}
|
}
|
||||||
else if (chips.length && !inputValue.length && event.key === "Backspace" )
|
else if (chips.length && !inputValue.length && event.key === "Backspace")
|
||||||
{
|
{
|
||||||
setChips(chips.slice(0, chips.length - 1));
|
setChips(chips.slice(0, chips.length - 1));
|
||||||
}
|
}
|
||||||
@ -87,18 +170,26 @@ function ChipTextField({...props})
|
|||||||
setChips(newChipList);
|
setChips(newChipList);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = (item: any) => () =>
|
|
||||||
{
|
|
||||||
const newChipList = [...chips];
|
|
||||||
newChipList.splice(newChipList.indexOf(item), 1);
|
|
||||||
setChips(newChipList);
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleInputChange(event: { target: { value: React.SetStateAction<string>; }; })
|
function handleInputChange(event: { target: { value: React.SetStateAction<string>; }; })
|
||||||
{
|
{
|
||||||
setInputValue(event.target.value);
|
setInputValue(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function determineChipColors(): any
|
||||||
|
{
|
||||||
|
if (chipType === "pvs")
|
||||||
|
{
|
||||||
|
debouncedApiCall();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const newChipColors = chips.map((chip, i) =>
|
||||||
|
(chipType !== "number" || !Number.isNaN(Number(chips[i]))) ? "info" : "error"
|
||||||
|
);
|
||||||
|
setChipColors(newChipColors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@ -116,7 +207,7 @@ function ChipTextField({...props})
|
|||||||
});
|
});
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return (
|
return (
|
||||||
<div id="chip-text-field-container" style={{flexWrap: "wrap", display:"flex"}}>
|
<div id="chip-text-field-container" style={{flexWrap: "wrap", display: "flex"}}>
|
||||||
<TextField
|
<TextField
|
||||||
sx={{width: "99%"}}
|
sx={{width: "99%"}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -125,16 +216,16 @@ function ChipTextField({...props})
|
|||||||
startAdornment:
|
startAdornment:
|
||||||
<div style={{overflowY: "auto", overflowX: "hidden", margin: "-10px", width: "calc(100% + 20px)", padding: "10px", marginBottom: "-20px", height: "calc(100% + 10px)"}}>
|
<div style={{overflowY: "auto", overflowX: "hidden", margin: "-10px", width: "calc(100% + 20px)", padding: "10px", marginBottom: "-20px", height: "calc(100% + 10px)"}}>
|
||||||
{
|
{
|
||||||
chips.map((item, i) => (
|
chips.map((item, index) => (
|
||||||
<Chip
|
<Chip
|
||||||
color={(chipType !== "number" || ! Number.isNaN(Number(item))) ? "info" : "error"}
|
onChange={determineChipColors}
|
||||||
key={`${item}-${i}`}
|
color={chipColors[index]}
|
||||||
|
key={`${item}-${index}`}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
label={item}
|
label={item}
|
||||||
className={classes.chip}
|
className={classes.chip}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>,
|
</div>,
|
||||||
@ -158,6 +249,7 @@ function ChipTextField({...props})
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ChipTextField.defaultProps = {
|
ChipTextField.defaultProps = {
|
||||||
chipData: []
|
chipData: []
|
||||||
};
|
};
|
||||||
@ -166,4 +258,4 @@ ChipTextField.propTypes = {
|
|||||||
chipData: arrayOf(string)
|
chipData: arrayOf(string)
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChipTextField
|
export default ChipTextField;
|
||||||
|
@ -57,7 +57,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
|||||||
<MDTypography variant="h5">{formLabel}</MDTypography>
|
<MDTypography variant="h5">{formLabel}</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box mt={1.625}>
|
<Box mt={1.625}>
|
||||||
<Grid container spacing={3}>
|
<Grid container lg={12} display="flex" spacing={3}>
|
||||||
{formFields
|
{formFields
|
||||||
&& Object.keys(formFields).length > 0
|
&& Object.keys(formFields).length > 0
|
||||||
&& Object.keys(formFields).map((fieldName: any) =>
|
&& Object.keys(formFields).map((fieldName: any) =>
|
||||||
@ -74,13 +74,14 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
|||||||
}
|
}
|
||||||
|
|
||||||
let formattedHelpContent = <HelpContent helpContents={field?.fieldMetaData?.helpContents} roles={helpRoles} helpContentKey={`${helpContentKeyPrefix ?? ""}field:${fieldName}`} />;
|
let formattedHelpContent = <HelpContent helpContents={field?.fieldMetaData?.helpContents} roles={helpRoles} helpContentKey={`${helpContentKeyPrefix ?? ""}field:${fieldName}`} />;
|
||||||
if(formattedHelpContent)
|
if (formattedHelpContent)
|
||||||
{
|
{
|
||||||
formattedHelpContent = <Box color="#757575" fontSize="0.875rem" mt="-0.25rem">{formattedHelpContent}</Box>
|
formattedHelpContent = <Box color="#757575" fontSize="0.875rem" mt="-0.25rem">{formattedHelpContent}</Box>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelElement = <DynamicFormFieldLabel name={field.name} label={field.label} />;
|
const labelElement = <DynamicFormFieldLabel name={field.name} label={field.label} />;
|
||||||
|
|
||||||
|
let itemLG = (field?.fieldMetaData?.gridColumns && field?.fieldMetaData?.gridColumns > 0) ? field.fieldMetaData.gridColumns : 6;
|
||||||
let itemXS = 12;
|
let itemXS = 12;
|
||||||
let itemSM = 6;
|
let itemSM = 6;
|
||||||
|
|
||||||
@ -92,13 +93,14 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
|||||||
const fileUploadAdornment = field.fieldMetaData?.getAdornment(AdornmentType.FILE_UPLOAD);
|
const fileUploadAdornment = field.fieldMetaData?.getAdornment(AdornmentType.FILE_UPLOAD);
|
||||||
const width = fileUploadAdornment?.values?.get("width") ?? "half";
|
const width = fileUploadAdornment?.values?.get("width") ?? "half";
|
||||||
|
|
||||||
if(width == "full")
|
if (width == "full")
|
||||||
{
|
{
|
||||||
itemSM = 12;
|
itemSM = 12;
|
||||||
|
itemLG = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid item xs={itemXS} sm={itemSM} key={fieldName}>
|
<Grid item lg={itemLG} xs={itemXS} sm={itemSM} flexDirection="column" key={fieldName}>
|
||||||
{labelElement}
|
{labelElement}
|
||||||
<FileInputField field={field} record={record} errorMessage={errors[fieldName]} />
|
<FileInputField field={field} record={record} errorMessage={errors[fieldName]} />
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -114,10 +116,10 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
|||||||
Object.keys(values).forEach((key) =>
|
Object.keys(values).forEach((key) =>
|
||||||
{
|
{
|
||||||
otherValuesMap.set(key, values[key]);
|
otherValuesMap.set(key, values[key]);
|
||||||
})
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid item xs={itemXS} sm={itemSM} key={fieldName}>
|
<Grid item lg={itemLG} xs={itemXS} sm={itemSM} key={fieldName}>
|
||||||
{labelElement}
|
{labelElement}
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
fieldPossibleValueProps={field.possibleValueProps}
|
fieldPossibleValueProps={field.possibleValueProps}
|
||||||
@ -138,7 +140,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
|||||||
// everything else!! //
|
// everything else!! //
|
||||||
///////////////////////
|
///////////////////////
|
||||||
return (
|
return (
|
||||||
<Grid item xs={itemXS} sm={itemSM} key={fieldName}>
|
<Grid item lg={itemLG} xs={itemXS} sm={itemSM} key={fieldName}>
|
||||||
{labelElement}
|
{labelElement}
|
||||||
<QDynamicFormField
|
<QDynamicFormField
|
||||||
id={field.name}
|
id={field.name}
|
||||||
|
@ -20,17 +20,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
import {Box, InputAdornment, InputLabel} from "@mui/material";
|
import {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 DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
|
||||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
|
||||||
import React, {useMemo, useState} from "react";
|
|
||||||
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 DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||||
|
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||||
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 React, {useMemo, useState} from "react";
|
||||||
|
import AceEditor from "react-ace";
|
||||||
import {flushSync} from "react-dom";
|
import {flushSync} from "react-dom";
|
||||||
|
|
||||||
// Declaring props types for FormField
|
// Declaring props types for FormField
|
||||||
@ -83,10 +84,10 @@ function QDynamicFormField({
|
|||||||
|
|
||||||
if (placeholder)
|
if (placeholder)
|
||||||
{
|
{
|
||||||
inputProps.placeholder = placeholder
|
inputProps.placeholder = placeholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(backgroundColor)
|
if (backgroundColor)
|
||||||
{
|
{
|
||||||
inputProps.sx = {
|
inputProps.sx = {
|
||||||
"&.MuiInputBase-root": {
|
"&.MuiInputBase-root": {
|
||||||
@ -124,7 +125,7 @@ function QDynamicFormField({
|
|||||||
{
|
{
|
||||||
onChange.onChange = (e: any) =>
|
onChange.onChange = (e: any) =>
|
||||||
{
|
{
|
||||||
if(isToUpperCase || isToLowerCase)
|
if (isToUpperCase || isToLowerCase)
|
||||||
{
|
{
|
||||||
const beforeStart = e.target.selectionStart;
|
const beforeStart = e.target.selectionStart;
|
||||||
const beforeEnd = e.target.selectionEnd;
|
const beforeEnd = e.target.selectionEnd;
|
||||||
@ -141,7 +142,10 @@ function QDynamicFormField({
|
|||||||
newValue = newValue.toLowerCase();
|
newValue = newValue.toLowerCase();
|
||||||
}
|
}
|
||||||
setFieldValue(name, newValue);
|
setFieldValue(name, newValue);
|
||||||
|
if (onChangeCallback)
|
||||||
|
{
|
||||||
onChangeCallback(newValue);
|
onChangeCallback(newValue);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = document.getElementById(name) as HTMLInputElement;
|
const input = document.getElementById(name) as HTMLInputElement;
|
||||||
@ -150,7 +154,7 @@ function QDynamicFormField({
|
|||||||
input.setSelectionRange(beforeStart, beforeEnd);
|
input.setSelectionRange(beforeStart, beforeEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(onChangeCallback)
|
else if (onChangeCallback)
|
||||||
{
|
{
|
||||||
onChangeCallback(e.currentTarget.value);
|
onChangeCallback(e.currentTarget.value);
|
||||||
}
|
}
|
||||||
@ -162,15 +166,15 @@ function QDynamicFormField({
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function dynamicSelectOnChange(newValue?: QPossibleValue)
|
function dynamicSelectOnChange(newValue?: QPossibleValue)
|
||||||
{
|
{
|
||||||
if(onChangeCallback)
|
if (onChangeCallback)
|
||||||
{
|
{
|
||||||
onChangeCallback(newValue == null ? null : newValue.id)
|
onChangeCallback(newValue == null ? null : newValue.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let field;
|
let field;
|
||||||
let getsBulkEditHtmlLabel = true;
|
let getsBulkEditHtmlLabel = true;
|
||||||
if(formFieldObject.possibleValueProps)
|
if (formFieldObject.possibleValueProps)
|
||||||
{
|
{
|
||||||
field = (<DynamicSelect
|
field = (<DynamicSelect
|
||||||
name={name}
|
name={name}
|
||||||
@ -183,7 +187,7 @@ function QDynamicFormField({
|
|||||||
onChange={dynamicSelectOnChange}
|
onChange={dynamicSelectOnChange}
|
||||||
// otherValues={otherValuesMap}
|
// otherValues={otherValuesMap}
|
||||||
useCase="form"
|
useCase="form"
|
||||||
/>)
|
/>);
|
||||||
}
|
}
|
||||||
else if (type === "checkbox")
|
else if (type === "checkbox")
|
||||||
{
|
{
|
||||||
@ -217,7 +221,7 @@ function QDynamicFormField({
|
|||||||
onChange={(value: string, event: any) =>
|
onChange={(value: string, event: any) =>
|
||||||
{
|
{
|
||||||
setFieldValue(name, value, false);
|
setFieldValue(name, value, false);
|
||||||
if(onChangeCallback)
|
if (onChangeCallback)
|
||||||
{
|
{
|
||||||
onChangeCallback(value);
|
onChangeCallback(value);
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, name, fieldLabel, i
|
|||||||
const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] =>
|
const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] =>
|
||||||
{
|
{
|
||||||
return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm));
|
return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm));
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
@ -182,15 +182,15 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, name, fieldLabel, i
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
const loadResults = async (): Promise<QPossibleValue[]> =>
|
const loadResults = async (): Promise<QPossibleValue[]> =>
|
||||||
{
|
{
|
||||||
if(possibleValues)
|
if (possibleValues)
|
||||||
{
|
{
|
||||||
return filterInlinePossibleValues(searchTerm, possibleValues)
|
return filterInlinePossibleValues(searchTerm, possibleValues);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
|
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, null, otherValues, useCase);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
|
@ -66,7 +66,7 @@ interface Props
|
|||||||
defaultValues: { [key: string]: string };
|
defaultValues: { [key: string]: string };
|
||||||
disabledFields: { [key: string]: boolean } | string[];
|
disabledFields: { [key: string]: boolean } | string[];
|
||||||
isCopy?: boolean;
|
isCopy?: boolean;
|
||||||
onSubmitCallback?: (values: any) => void;
|
onSubmitCallback?: (values: any, tableName: string) => void;
|
||||||
overrideHeading?: string;
|
overrideHeading?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +173,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function openAddChildRecord(name: string, widgetData: any)
|
function openAddChildRecord(name: string, widgetData: any)
|
||||||
{
|
{
|
||||||
let defaultValues = widgetData.defaultValuesForNewChildRecords;
|
let defaultValues = widgetData.defaultValuesForNewChildRecords || {};
|
||||||
|
|
||||||
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
|
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
|
||||||
if (!disabledFields)
|
if (!disabledFields)
|
||||||
@ -181,6 +181,18 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
disabledFields = widgetData.defaultValuesForNewChildRecords;
|
disabledFields = widgetData.defaultValuesForNewChildRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// copy values from specified fields in the parent record down into the child record //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (widgetData.defaultValuesForNewChildRecordsFromParentFields)
|
||||||
|
{
|
||||||
|
for (let childField in widgetData.defaultValuesForNewChildRecordsFromParentFields)
|
||||||
|
{
|
||||||
|
const parentField = widgetData.defaultValuesForNewChildRecordsFromParentFields[childField];
|
||||||
|
defaultValues[childField] = formValues[parentField];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
doOpenEditChildForm(name, widgetData.childTableMetaData, null, defaultValues, disabledFields);
|
doOpenEditChildForm(name, widgetData.childTableMetaData, null, defaultValues, disabledFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,7 +220,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
function deleteChildRecord(name: string, widgetData: any, rowIndex: number)
|
function deleteChildRecord(name: string, widgetData: any, rowIndex: number)
|
||||||
{
|
{
|
||||||
updateChildRecordList(name, "delete", rowIndex);
|
updateChildRecordList(name, "delete", rowIndex);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -243,16 +255,16 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function submitEditChildForm(values: any)
|
function submitEditChildForm(values: any, tableName: string)
|
||||||
{
|
{
|
||||||
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
|
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values, tableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
async function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any)
|
async function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any, childTableName?: string)
|
||||||
{
|
{
|
||||||
const metaData = await qController.loadMetaData();
|
const metaData = await qController.loadMetaData();
|
||||||
const widgetMetaData = metaData.widgets.get(widgetName);
|
const widgetMetaData = metaData.widgets.get(widgetName);
|
||||||
@ -263,13 +275,38 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
newChildListWidgetData[widgetName].queryOutput.records = [];
|
newChildListWidgetData[widgetName].queryOutput.records = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// build a map of display values for the new record, specifically, for any possible-values that need translated. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const displayValues: { [fieldName: string]: string } = {};
|
||||||
|
if (childTableName && values)
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// this function internally memoizes, so, we could potentially avoid an await here, but, seems ok... //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const childTableMetaData = await qController.loadTableMetaData(childTableName);
|
||||||
|
for (let key in values)
|
||||||
|
{
|
||||||
|
const value = values[key];
|
||||||
|
const field = childTableMetaData.fields.get(key);
|
||||||
|
if (field.possibleValueSourceName)
|
||||||
|
{
|
||||||
|
const possibleValues = await qController.possibleValues(childTableName, null, field.name, null, [value], null, objectToMap(values), "form");
|
||||||
|
if (possibleValues && possibleValues.length > 0)
|
||||||
|
{
|
||||||
|
displayValues[key] = possibleValues[0].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (action)
|
switch (action)
|
||||||
{
|
{
|
||||||
case "insert":
|
case "insert":
|
||||||
newChildListWidgetData[widgetName].queryOutput.records.push({values: values});
|
newChildListWidgetData[widgetName].queryOutput.records.push({values: values, displayValues: displayValues});
|
||||||
break;
|
break;
|
||||||
case "edit":
|
case "edit":
|
||||||
newChildListWidgetData[widgetName].queryOutput.records[rowIndex] = {values: values};
|
newChildListWidgetData[widgetName].queryOutput.records[rowIndex] = {values: values, displayValues: displayValues};
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
newChildListWidgetData[widgetName].queryOutput.records.splice(rowIndex, 1);
|
newChildListWidgetData[widgetName].queryOutput.records.splice(rowIndex, 1);
|
||||||
@ -407,6 +444,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
widgetData={widgetData}
|
widgetData={widgetData}
|
||||||
recordValues={formValues}
|
recordValues={formValues}
|
||||||
|
label={tableMetaData?.fields.get(widgetData?.filterFieldName ?? "queryFilterJson")?.label}
|
||||||
onSaveCallback={setFormFieldValuesFromWidget}
|
onSaveCallback={setFormFieldValuesFromWidget}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
@ -478,6 +516,25 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function objectToMap(object: { [key: string]: any }): Map<string, any>
|
||||||
|
{
|
||||||
|
if (object == null)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rs = new Map<string, any>();
|
||||||
|
for (let key in object)
|
||||||
|
{
|
||||||
|
rs.set(key, object[key]);
|
||||||
|
}
|
||||||
|
return rs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//////////////////
|
//////////////////
|
||||||
// initial load //
|
// initial load //
|
||||||
//////////////////
|
//////////////////
|
||||||
@ -595,14 +652,21 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
if (defaultValue)
|
if (defaultValue)
|
||||||
{
|
{
|
||||||
initialValues[fieldName] = defaultValue;
|
initialValues[fieldName] = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// we need to set the initialDisplayValue for possible value fields with a default value //
|
// do a second loop, this time looking up display-values for any possible-value fields with a default value //
|
||||||
// so, look them up here now if needed //
|
// do it in a second loop, to pass in all the other values (from initialValues), in case there's a PVS filter that needs them. //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (fieldMetaData.possibleValueSourceName)
|
for (let i = 0; i < fieldArray.length; i++)
|
||||||
{
|
{
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], undefined, "form");
|
const fieldMetaData = fieldArray[i];
|
||||||
|
const fieldName = fieldMetaData.name;
|
||||||
|
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
|
||||||
|
if (defaultValue && fieldMetaData.possibleValueSourceName)
|
||||||
|
{
|
||||||
|
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], null, objectToMap(initialValues), "form");
|
||||||
if (results && results.length > 0)
|
if (results && results.length > 0)
|
||||||
{
|
{
|
||||||
defaultDisplayValues.set(fieldName, results[0].label);
|
defaultDisplayValues.set(fieldName, results[0].label);
|
||||||
@ -610,7 +674,6 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
// if an override heading was passed in, use it. //
|
// if an override heading was passed in, use it. //
|
||||||
@ -823,7 +886,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (props.onSubmitCallback)
|
if (props.onSubmitCallback)
|
||||||
{
|
{
|
||||||
props.onSubmitCallback(values);
|
props.onSubmitCallback(values, tableName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +270,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
pageHeader &&
|
pageHeader &&
|
||||||
<Box display="flex" justifyContent="space-between">
|
<Box display="flex" justifyContent="space-between">
|
||||||
<MDTypography pb="0.5rem" textTransform="capitalize" variant="h3" color={light ? "white" : "dark"} noWrap>
|
<MDTypography pb="0.5rem" variant="h3" color={light ? "white" : "dark"} noWrap>
|
||||||
{pageHeader}
|
{pageHeader}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -20,21 +20,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||||
|
import {Button} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import {ReactNode, useEffect, useReducer, useState} from "react";
|
|
||||||
import {NavLink, useLocation} from "react-router-dom";
|
|
||||||
import AuthenticationButton from "qqq/components/buttons/AuthenticationButton";
|
|
||||||
import SideNavCollapse from "qqq/components/horseshoe/sidenav/SideNavCollapse";
|
import SideNavCollapse from "qqq/components/horseshoe/sidenav/SideNavCollapse";
|
||||||
import SideNavItem from "qqq/components/horseshoe/sidenav/SideNavItem";
|
import SideNavItem from "qqq/components/horseshoe/sidenav/SideNavItem";
|
||||||
import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList";
|
import SideNavList from "qqq/components/horseshoe/sidenav/SideNavList";
|
||||||
import SidenavRoot from "qqq/components/horseshoe/sidenav/SideNavRoot";
|
import SidenavRoot from "qqq/components/horseshoe/sidenav/SideNavRoot";
|
||||||
import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
|
import sidenavLogoLabel from "qqq/components/horseshoe/sidenav/styles/SideNav";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||||
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
|
import {setMiniSidenav, setTransparentSidenav, setWhiteSidenav, useMaterialUIController,} from "qqq/context";
|
||||||
|
import {ReactNode, useEffect, useReducer, useState} from "react";
|
||||||
|
import {NavLink, useLocation} from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
@ -44,6 +45,7 @@ interface Props
|
|||||||
logo?: string;
|
logo?: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
branding?: QBrandingMetaData;
|
branding?: QBrandingMetaData;
|
||||||
|
logout: () => void;
|
||||||
routes: {
|
routes: {
|
||||||
[key: string]:
|
[key: string]:
|
||||||
| ReactNode
|
| ReactNode
|
||||||
@ -66,7 +68,7 @@ interface Props
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props): JSX.Element
|
function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
|
const [openCollapse, setOpenCollapse] = useState<boolean | string>(false);
|
||||||
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
|
const [openNestedCollapse, setOpenNestedCollapse] = useState<boolean | string>(false);
|
||||||
@ -257,7 +259,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
|||||||
active={key === collapseName}
|
active={key === collapseName}
|
||||||
open={openCollapse === key}
|
open={openCollapse === key}
|
||||||
noCollapse={noCollapse}
|
noCollapse={noCollapse}
|
||||||
onClick={() => (! noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null) }
|
onClick={() => (!noCollapse ? (openCollapse === key ? setOpenCollapse(false) : setOpenCollapse(key)) : null)}
|
||||||
>
|
>
|
||||||
{collapse ? renderCollapse(collapse) : null}
|
{collapse ? renderCollapse(collapse) : null}
|
||||||
</SideNavCollapse>
|
</SideNavCollapse>
|
||||||
@ -300,6 +302,30 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function EnvironmentBanner({branding}: { branding: QBrandingMetaData }): JSX.Element | null
|
||||||
|
{
|
||||||
|
// deprecated!
|
||||||
|
if (branding && branding.environmentBannerText)
|
||||||
|
{
|
||||||
|
return <Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
|
||||||
|
{branding.environmentBannerText}
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO");
|
||||||
|
if (banner)
|
||||||
|
{
|
||||||
|
return <Box className={getBannerClassName(banner)} mt={2} borderRadius={2} sx={getBannerStyles(banner)}>
|
||||||
|
{makeBannerContent(banner)}
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidenavRoot
|
<SidenavRoot
|
||||||
{...rest}
|
{...rest}
|
||||||
@ -330,12 +356,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
{
|
<EnvironmentBanner branding={branding} />
|
||||||
branding && branding.environmentBannerText &&
|
|
||||||
<Box mt={2} bgcolor={branding.environmentBannerColor} borderRadius={2}>
|
|
||||||
{branding.environmentBannerText}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
</Box>
|
</Box>
|
||||||
<Divider
|
<Divider
|
||||||
light={
|
light={
|
||||||
@ -350,7 +371,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props)
|
|||||||
(darkMode && !transparentSidenav && whiteSidenav)
|
(darkMode && !transparentSidenav && whiteSidenav)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<AuthenticationButton />
|
<Button onClick={logout}>Log Out</Button>
|
||||||
</SidenavRoot>
|
</SidenavRoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,7 @@ export default styled(Drawer)(({theme, ownerState}: { theme?: Theme | any; owner
|
|||||||
margin: "0",
|
margin: "0",
|
||||||
borderRadius: "0",
|
borderRadius: "0",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
top: "unset",
|
||||||
|
|
||||||
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
|
...(miniSidenav ? drawerCloseStyles() : drawerOpenStyles()),
|
||||||
},
|
},
|
||||||
|
97
src/qqq/components/misc/Banners.tsx
Normal file
97
src/qqq/components/misc/Banners.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2025. 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 {Banner} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Banner";
|
||||||
|
import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData";
|
||||||
|
import parse from "html-react-parser";
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// One may render a banner using the functions in this file as: //
|
||||||
|
// //
|
||||||
|
// const banner = getBanner(branding, "QFMD_SIDE_NAV_UNDER_LOGO"); //
|
||||||
|
// return (<Box className={getBannerClassName(banner)} sx={{padding: "1rem", ...getBannerStyles(banner)}}> //
|
||||||
|
// {makeBannerContent(banner)} //
|
||||||
|
// </Box>); //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
export function getBanner(branding: QBrandingMetaData, slot: string): Banner | null
|
||||||
|
{
|
||||||
|
if (branding?.banners?.has(slot))
|
||||||
|
{
|
||||||
|
return (branding.banners.get(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
export function getBannerStyles(banner: Banner)
|
||||||
|
{
|
||||||
|
let bgColor = "";
|
||||||
|
let color = "";
|
||||||
|
|
||||||
|
if (banner)
|
||||||
|
{
|
||||||
|
if (banner.backgroundColor)
|
||||||
|
{
|
||||||
|
bgColor = banner.backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (banner.textColor)
|
||||||
|
{
|
||||||
|
bgColor = banner.textColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rest = banner?.additionalStyles ?? {};
|
||||||
|
|
||||||
|
return ({
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
color: color,
|
||||||
|
...rest
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
export function getBannerClassName(banner: Banner)
|
||||||
|
{
|
||||||
|
return `banner ${banner?.severity?.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
export function makeBannerContent(banner: Banner): JSX.Element
|
||||||
|
{
|
||||||
|
return <>{banner?.messageHTML ? parse(banner?.messageHTML) : banner?.messageText}</>;
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||||
@ -35,12 +36,11 @@ import DialogTitle from "@mui/material/DialogTitle";
|
|||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import {any} from "prop-types";
|
|
||||||
import React, {useState} from "react";
|
|
||||||
import {useNavigate} from "react-router-dom";
|
|
||||||
import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
|
import {QCancelButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import MDButton from "qqq/components/legacy/MDButton";
|
import MDButton from "qqq/components/legacy/MDButton";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -182,18 +182,18 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
|
|
||||||
options[optionIndex].forEach((field) =>
|
options[optionIndex].forEach((field) =>
|
||||||
{
|
{
|
||||||
if(values[field.name])
|
if (values[field.name])
|
||||||
{
|
{
|
||||||
anyFieldsInThisOptionHaveAValue = true;
|
anyFieldsInThisOptionHaveAValue = true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
if(!anyFieldsInThisOptionHaveAValue)
|
if (!anyFieldsInThisOptionHaveAValue)
|
||||||
{
|
{
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
return (false);
|
return (false);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
@ -207,9 +207,13 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
const queryStringParts: string[] = [];
|
const queryStringParts: string[] = [];
|
||||||
options[optionIndex].forEach((field) =>
|
options[optionIndex].forEach((field) =>
|
||||||
{
|
{
|
||||||
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]))
|
if (field.type == QFieldType.STRING && !values[field.name])
|
||||||
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`)
|
{
|
||||||
})
|
return;
|
||||||
|
}
|
||||||
|
criteria.push(new QFilterCriteria(field.name, QCriteriaOperator.EQUALS, [values[field.name]]));
|
||||||
|
queryStringParts.push(`${field.name}=${encodeURIComponent(values[field.name])}`);
|
||||||
|
});
|
||||||
|
|
||||||
const filter = new QQueryFilter(criteria, null, null, "AND", null, 10);
|
const filter = new QQueryFilter(criteria, null, null, "AND", null, 10);
|
||||||
|
|
||||||
@ -223,7 +227,7 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
else if (queryResult.length == 1)
|
else if (queryResult.length == 1)
|
||||||
{
|
{
|
||||||
if(options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
|
if (options[optionIndex].length == 1 && options[optionIndex][0].name == pkey?.name)
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////
|
/////////////////////////////////////////////////
|
||||||
// navigate by pkey, if that's how we searched //
|
// navigate by pkey, if that's how we searched //
|
||||||
|
@ -21,12 +21,11 @@
|
|||||||
|
|
||||||
|
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {Checkbox, FormControlLabel, Radio} from "@mui/material";
|
import {Checkbox, FormControlLabel, Radio, Tooltip} from "@mui/material";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import RadioGroup from "@mui/material/RadioGroup";
|
import RadioGroup from "@mui/material/RadioGroup";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import {useFormikContext} from "formik";
|
import {useFormikContext} from "formik";
|
||||||
@ -34,6 +33,7 @@ import colors from "qqq/assets/theme/base/colors";
|
|||||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||||
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||||
import {BulkLoadField, FileDescription} from "qqq/models/processes/BulkLoadModels";
|
import {BulkLoadField, FileDescription} from "qqq/models/processes/BulkLoadModels";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
|
|
||||||
interface BulkLoadMappingFieldProps
|
interface BulkLoadMappingFieldProps
|
||||||
@ -45,6 +45,29 @@ interface BulkLoadMappingFieldProps
|
|||||||
forceParentUpdate?: () => void,
|
forceParentUpdate?: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const xIconButtonSX =
|
||||||
|
{
|
||||||
|
border: `1px solid ${colors.grayLines.main} !important`,
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
textTransform: "none",
|
||||||
|
fontSize: "1rem",
|
||||||
|
fontWeight: "400",
|
||||||
|
width: "30px",
|
||||||
|
minWidth: "30px",
|
||||||
|
height: "2rem",
|
||||||
|
minHeight: "2rem",
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
marginRight: "0.5rem",
|
||||||
|
marginTop: "0.5rem",
|
||||||
|
color: colors.error.main,
|
||||||
|
"&:hover": {color: colors.error.main},
|
||||||
|
"&:focus": {color: colors.error.main},
|
||||||
|
"&:focus:not(:hover)": {color: colors.error.main},
|
||||||
|
};
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** row for a single field on the bulk load mapping screen.
|
** row for a single field on the bulk load mapping screen.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@ -54,6 +77,11 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
|
|
||||||
const [valueType, setValueType] = useState(bulkLoadField.valueType);
|
const [valueType, setValueType] = useState(bulkLoadField.valueType);
|
||||||
const [selectedColumn, setSelectedColumn] = useState({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex});
|
const [selectedColumn, setSelectedColumn] = useState({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex});
|
||||||
|
const [selectedColumnInputValue, setSelectedColumnInputValue] = useState(columnNames[bulkLoadField.columnIndex]);
|
||||||
|
|
||||||
|
const [doingInitialLoadOfPossibleValue, setDoingInitialLoadOfPossibleValue] = useState(false);
|
||||||
|
const [everDidInitialLoadOfPossibleValue, setEverDidInitialLoadOfPossibleValue] = useState(false);
|
||||||
|
const [possibleValueInitialDisplayValue, setPossibleValueInitialDisplayValue] = useState(null as string);
|
||||||
|
|
||||||
const fieldMetaData = new QFieldMetaData(bulkLoadField.field);
|
const fieldMetaData = new QFieldMetaData(bulkLoadField.field);
|
||||||
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
|
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
|
||||||
@ -61,18 +89,68 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
dynamicFieldInObject[fieldMetaData["name"]] = dynamicField;
|
dynamicFieldInObject[fieldMetaData["name"]] = dynamicField;
|
||||||
DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [fieldMetaData], bulkLoadField.tableStructure.tableName, null, null);
|
DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [fieldMetaData], bulkLoadField.tableStructure.tableName, null, null);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// deal with dynamically loading the initial default value for a possible value... //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let actuallyDoingInitialLoadOfPossibleValue = doingInitialLoadOfPossibleValue;
|
||||||
|
if (dynamicField.possibleValueProps && bulkLoadField.defaultValue && !doingInitialLoadOfPossibleValue && !everDidInitialLoadOfPossibleValue)
|
||||||
|
{
|
||||||
|
actuallyDoingInitialLoadOfPossibleValue = true;
|
||||||
|
setDoingInitialLoadOfPossibleValue(true);
|
||||||
|
setEverDidInitialLoadOfPossibleValue(true);
|
||||||
|
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const possibleValues = await qController.possibleValues(bulkLoadField.tableStructure.tableName, null, fieldMetaData.name, null, [bulkLoadField.defaultValue], undefined, null, "filter");
|
||||||
|
if (possibleValues && possibleValues.length > 0)
|
||||||
|
{
|
||||||
|
setPossibleValueInitialDisplayValue(possibleValues[0].label);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setPossibleValueInitialDisplayValue(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log(`Error loading possible value: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
actuallyDoingInitialLoadOfPossibleValue = false;
|
||||||
|
setDoingInitialLoadOfPossibleValue(false);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dynamicField.possibleValueProps && possibleValueInitialDisplayValue)
|
||||||
|
{
|
||||||
|
dynamicField.possibleValueProps.initialDisplayValue = possibleValueInitialDisplayValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// build array of options for the columns drop down //
|
||||||
|
// don't allow duplicates //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
const columnOptions: { value: number, label: string }[] = [];
|
const columnOptions: { value: number, label: string }[] = [];
|
||||||
|
const usedLabels: { [label: string]: boolean } = {};
|
||||||
for (let i = 0; i < columnNames.length; i++)
|
for (let i = 0; i < columnNames.length; i++)
|
||||||
{
|
{
|
||||||
columnOptions.push({label: columnNames[i], value: i});
|
const label = columnNames[i];
|
||||||
|
if (!usedLabels[label])
|
||||||
|
{
|
||||||
|
columnOptions.push({label: label, value: i});
|
||||||
|
usedLabels[label] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// try to pick up changes in the hasHeaderRow toggle from way above //
|
// try to pick up changes in the hasHeaderRow toggle from way above //
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
if(bulkLoadField.columnIndex != null && bulkLoadField.columnIndex != undefined && selectedColumn.label && columnNames[bulkLoadField.columnIndex] != selectedColumn.label)
|
if (bulkLoadField.columnIndex != null && bulkLoadField.columnIndex != undefined && selectedColumn.label && columnNames[bulkLoadField.columnIndex] != selectedColumn.label)
|
||||||
{
|
{
|
||||||
setSelectedColumn({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex})
|
setSelectedColumn({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex});
|
||||||
|
setSelectedColumnInputValue(columnNames[bulkLoadField.columnIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainFontSize = "0.875rem";
|
const mainFontSize = "0.875rem";
|
||||||
@ -98,6 +176,8 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
function columnChanged(event: any, newValue: any, reason: string)
|
function columnChanged(event: any, newValue: any, reason: string)
|
||||||
{
|
{
|
||||||
setSelectedColumn(newValue);
|
setSelectedColumn(newValue);
|
||||||
|
setSelectedColumnInputValue(newValue == null ? "" : newValue.label);
|
||||||
|
|
||||||
bulkLoadField.columnIndex = newValue == null ? null : newValue.value;
|
bulkLoadField.columnIndex = newValue == null ? null : newValue.value;
|
||||||
|
|
||||||
if (fileDescription.hasHeaderRow)
|
if (fileDescription.hasHeaderRow)
|
||||||
@ -106,6 +186,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
}
|
}
|
||||||
|
|
||||||
bulkLoadField.error = null;
|
bulkLoadField.error = null;
|
||||||
|
bulkLoadField.warning = null;
|
||||||
forceParentUpdate && forceParentUpdate();
|
forceParentUpdate && forceParentUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +199,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
setFieldValue(`${bulkLoadField.field.name}.defaultValue`, newValue);
|
setFieldValue(`${bulkLoadField.field.name}.defaultValue`, newValue);
|
||||||
bulkLoadField.defaultValue = newValue;
|
bulkLoadField.defaultValue = newValue;
|
||||||
bulkLoadField.error = null;
|
bulkLoadField.error = null;
|
||||||
|
bulkLoadField.warning = null;
|
||||||
forceParentUpdate && forceParentUpdate();
|
forceParentUpdate && forceParentUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +213,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
bulkLoadField.valueType = newValueType;
|
bulkLoadField.valueType = newValueType;
|
||||||
setValueType(newValueType);
|
setValueType(newValueType);
|
||||||
bulkLoadField.error = null;
|
bulkLoadField.error = null;
|
||||||
|
bulkLoadField.warning = null;
|
||||||
forceParentUpdate && forceParentUpdate();
|
forceParentUpdate && forceParentUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +227,15 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
forceParentUpdate && forceParentUpdate();
|
forceParentUpdate && forceParentUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<Box py="0.5rem" sx={{borderBottom: "1px solid lightgray", width: "100%", overflow: "auto"}}>
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function changeSelectedColumnInputValue(e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>)
|
||||||
|
{
|
||||||
|
setSelectedColumnInputValue(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<Box py="0.5rem" sx={{borderBottom: "1px solid lightgray", width: "100%", overflow: "auto"}} id={`blfmf-${bulkLoadField.field.name}`}>
|
||||||
<Box display="grid" gridTemplateColumns="200px 400px auto" fontSize="1rem" gap="0.5rem" sx={
|
<Box display="grid" gridTemplateColumns="200px 400px auto" fontSize="1rem" gap="0.5rem" sx={
|
||||||
{
|
{
|
||||||
"& .MuiFormControlLabel-label": {ml: "0 !important", fontWeight: "normal !important", fontSize: mainFontSize}
|
"& .MuiFormControlLabel-label": {ml: "0 !important", fontWeight: "normal !important", fontSize: mainFontSize}
|
||||||
@ -152,7 +243,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
|
|
||||||
<Box display="flex" alignItems="flex-start">
|
<Box display="flex" alignItems="flex-start">
|
||||||
{
|
{
|
||||||
(!isRequired) && <IconButton onClick={() => removeFieldCallback()} sx={{pt: "0.75rem"}}><Icon fontSize="small">remove_circle</Icon></IconButton>
|
(!isRequired) && <Tooltip placement="bottom" title="Remove this field from your mapping.">
|
||||||
|
<Button sx={xIconButtonSX} onClick={() => removeFieldCallback()}><Icon>clear</Icon></Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
<Box pt="0.625rem">
|
<Box pt="0.625rem">
|
||||||
{bulkLoadField.getQualifiedLabel()}
|
{bulkLoadField.getQualifiedLabel()}
|
||||||
@ -167,13 +260,13 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
valueType == "column" && <Box width="100%">
|
valueType == "column" && <Box width="100%">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
id={bulkLoadField.field.name}
|
id={bulkLoadField.field.name}
|
||||||
renderInput={(params) => (<TextField {...params} label={""} value={selectedColumn?.label} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
|
renderInput={(params) => (<TextField {...params} label={""} value={selectedColumnInputValue} onChange={e => changeSelectedColumnInputValue(e)} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
|
||||||
fullWidth
|
fullWidth
|
||||||
options={columnOptions}
|
options={columnOptions}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
defaultValue={selectedColumn}
|
defaultValue={selectedColumn}
|
||||||
value={selectedColumn}
|
value={selectedColumn}
|
||||||
inputValue={selectedColumn?.label}
|
inputValue={selectedColumnInputValue}
|
||||||
onChange={columnChanged}
|
onChange={columnChanged}
|
||||||
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
||||||
isOptionEqualToValue={(option, value) => option == null && value == null || option.value == value.value}
|
isOptionEqualToValue={(option, value) => option == null && value == null || option.value == value.value}
|
||||||
@ -186,7 +279,10 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
<Box display="flex" alignItems="center" sx={{height: "45px"}}>
|
<Box display="flex" alignItems="center" sx={{height: "45px"}}>
|
||||||
<FormControlLabel value="defaultValue" control={<Radio size="small" onChange={(event, checked) => valueTypeChanged(!checked)} />} label={"Default value"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
<FormControlLabel value="defaultValue" control={<Radio size="small" onChange={(event, checked) => valueTypeChanged(!checked)} />} label={"Default value"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||||
{
|
{
|
||||||
valueType == "defaultValue" && <Box width="100%">
|
valueType == "defaultValue" && actuallyDoingInitialLoadOfPossibleValue && <Box width="100%">Loading...</Box>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
valueType == "defaultValue" && !actuallyDoingInitialLoadOfPossibleValue && <Box width="100%">
|
||||||
<QDynamicFormField
|
<QDynamicFormField
|
||||||
name={`${bulkLoadField.field.name}.defaultValue`}
|
name={`${bulkLoadField.field.name}.defaultValue`}
|
||||||
displayFormat={""}
|
displayFormat={""}
|
||||||
@ -200,9 +296,15 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
{
|
||||||
|
bulkLoadField.warning &&
|
||||||
|
<Box fontSize={smallerFontSize} color={colors.warning.main} ml="145px" className="bulkLoadFieldError">
|
||||||
|
{bulkLoadField.warning}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
{
|
{
|
||||||
bulkLoadField.error &&
|
bulkLoadField.error &&
|
||||||
<Box fontSize={smallerFontSize} color={colors.error.main} ml="145px">
|
<Box fontSize={smallerFontSize} color={colors.error.main} ml="145px" className="bulkLoadFieldError">
|
||||||
{bulkLoadField.error}
|
{bulkLoadField.error}
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
|||||||
{
|
{
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
const [forceRerender, setForceRerender] = useState(0);
|
const [forceHierarchyAutoCompleteRerender, setForceHierarchyAutoCompleteRerender] = useState(0);
|
||||||
|
|
||||||
////////////////////////////////////////////
|
////////////////////////////////////////////
|
||||||
// build list of fields that can be added //
|
// build list of fields that can be added //
|
||||||
@ -98,8 +98,9 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
|||||||
|
|
||||||
setAddFieldsDisableStates(newDisableStates);
|
setAddFieldsDisableStates(newDisableStates);
|
||||||
setTooltips(newTooltips);
|
setTooltips(newTooltips);
|
||||||
|
setForceHierarchyAutoCompleteRerender(forceHierarchyAutoCompleteRerender + 1);
|
||||||
|
|
||||||
}, [bulkLoadMapping]);
|
}, [bulkLoadMapping, bulkLoadMapping.layout]);
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////
|
///////////////////////////////////////////////
|
||||||
@ -140,9 +141,6 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function removeField(bulkLoadField: BulkLoadField)
|
function removeField(bulkLoadField: BulkLoadField)
|
||||||
{
|
{
|
||||||
// addFieldsToggleStates[bulkLoadField.getQualifiedName()] = false;
|
|
||||||
// setAddFieldsToggleStates(Object.assign({}, addFieldsToggleStates));
|
|
||||||
|
|
||||||
addFieldsDisableStates[bulkLoadField.getQualifiedName()] = false;
|
addFieldsDisableStates[bulkLoadField.getQualifiedName()] = false;
|
||||||
setAddFieldsDisableStates(Object.assign({}, addFieldsDisableStates));
|
setAddFieldsDisableStates(Object.assign({}, addFieldsDisableStates));
|
||||||
|
|
||||||
@ -160,7 +158,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
|||||||
bulkLoadMapping.removeField(bulkLoadField);
|
bulkLoadMapping.removeField(bulkLoadField);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
forceParentUpdate();
|
forceParentUpdate();
|
||||||
setForceRerender(forceRerender + 1);
|
setForceHierarchyAutoCompleteRerender(forceHierarchyAutoCompleteRerender + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
@ -297,7 +295,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
|||||||
isModeSelectOne
|
isModeSelectOne
|
||||||
keepOpenAfterSelectOne
|
keepOpenAfterSelectOne
|
||||||
handleSelectedOption={handleAddField}
|
handleSelectedOption={handleAddField}
|
||||||
forceRerender={forceRerender}
|
forceRerender={forceHierarchyAutoCompleteRerender}
|
||||||
disabledStates={addFieldsDisableStates}
|
disabledStates={addFieldsDisableStates}
|
||||||
tooltips={tooltips}
|
tooltips={tooltips}
|
||||||
/>
|
/>
|
||||||
|
@ -20,45 +20,56 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
|
import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
|
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
|
import {Badge, Icon} from "@mui/material";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
import {useFormikContext} from "formik";
|
import {useFormikContext} from "formik";
|
||||||
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm";
|
import {DynamicFormFieldLabel} from "qqq/components/forms/DynamicForm";
|
||||||
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
import HelpContent from "qqq/components/misc/HelpContent";
|
||||||
import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles";
|
import SavedBulkLoadProfiles from "qqq/components/misc/SavedBulkLoadProfiles";
|
||||||
import BulkLoadFileMappingFields from "qqq/components/processes/BulkLoadFileMappingFields";
|
import BulkLoadFileMappingFields from "qqq/components/processes/BulkLoadFileMappingFields";
|
||||||
import {BulkLoadField, BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels";
|
import {BulkLoadField, BulkLoadMapping, BulkLoadProfile, BulkLoadTableStructure, FileDescription, Wrapper} from "qqq/models/processes/BulkLoadModels";
|
||||||
import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun";
|
import {SubFormPreSubmitCallbackResultType} from "qqq/pages/processes/ProcessRun";
|
||||||
import React, {forwardRef, useEffect, useImperativeHandle, useReducer, useState} from "react";
|
import React, {forwardRef, useImperativeHandle, useReducer, useState} from "react";
|
||||||
import ProcessViewForm from "./ProcessViewForm";
|
import ProcessViewForm from "./ProcessViewForm";
|
||||||
|
|
||||||
|
|
||||||
interface BulkLoadMappingFormProps
|
interface BulkLoadMappingFormProps
|
||||||
{
|
{
|
||||||
processValues: any;
|
processValues: any,
|
||||||
tableMetaData: QTableMetaData;
|
tableMetaData: QTableMetaData,
|
||||||
metaData: QInstance;
|
metaData: QInstance,
|
||||||
setActiveStepLabel: (label: string) => void;
|
setActiveStepLabel: (label: string) => void,
|
||||||
|
frontendStep: QFrontendStepMetaData,
|
||||||
|
processMetaData: QProcessMetaData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** process component - screen where user does a bulk-load file mapping.
|
** process component - screen where user does a bulk-load file mapping.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaData, setActiveStepLabel}: BulkLoadMappingFormProps, ref) =>
|
const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaData, setActiveStepLabel, frontendStep, processMetaData}: BulkLoadMappingFormProps, ref) =>
|
||||||
{
|
{
|
||||||
const {setFieldValue} = useFormikContext();
|
const {setFieldValue} = useFormikContext();
|
||||||
|
|
||||||
const [currentSavedBulkLoadProfile, setCurrentSavedBulkLoadProfile] = useState(null as QRecord);
|
const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord;
|
||||||
|
const [currentSavedBulkLoadProfile, setCurrentSavedBulkLoadProfile] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue));
|
||||||
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(currentSavedBulkLoadProfile));
|
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(currentSavedBulkLoadProfile));
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState({} as { [fieldName: string]: string });
|
const [fieldErrors, setFieldErrors] = useState({} as { [fieldName: string]: string });
|
||||||
|
const [noMappedFieldsError, setNoMappedFieldsError] = useState(null as string);
|
||||||
|
|
||||||
const [suggestedBulkLoadProfile] = useState(processValues.suggestedBulkLoadProfile as BulkLoadProfile);
|
const [suggestedBulkLoadProfile] = useState(processValues.suggestedBulkLoadProfile as BulkLoadProfile);
|
||||||
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
|
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
|
||||||
@ -119,18 +130,31 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
}
|
}
|
||||||
setFieldErrors(fieldErrors);
|
setFieldErrors(fieldErrors);
|
||||||
|
|
||||||
|
if(wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0)
|
||||||
|
{
|
||||||
|
setNoMappedFieldsError("You must have at least 1 field.");
|
||||||
|
haveLocalErrors = true;
|
||||||
|
setTimeout(() => setNoMappedFieldsError(null), 2500);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setNoMappedFieldsError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(haveProfileErrors)
|
||||||
|
{
|
||||||
|
setTimeout(() =>
|
||||||
|
{
|
||||||
|
document.querySelector(".bulkLoadFieldError")?.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
return {maySubmit: !haveProfileErrors && !haveLocalErrors, values};
|
return {maySubmit: !haveProfileErrors && !haveLocalErrors, values};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
console.log("@dk has header row changed!");
|
|
||||||
}, [bulkLoadMapping.hasHeaderRow]);
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@ -214,6 +238,8 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
tableStructure={tableStructure}
|
tableStructure={tableStructure}
|
||||||
fileName={processValues.fileBaseName}
|
fileName={processValues.fileBaseName}
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
|
frontendStep={frontendStep}
|
||||||
|
processMetaData={processMetaData}
|
||||||
forceParentUpdate={() => forceUpdate()}
|
forceParentUpdate={() => forceUpdate()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -221,8 +247,15 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
<BulkLoadFileMappingFields
|
<BulkLoadFileMappingFields
|
||||||
bulkLoadMapping={bulkLoadMapping}
|
bulkLoadMapping={bulkLoadMapping}
|
||||||
fileDescription={fileDescription}
|
fileDescription={fileDescription}
|
||||||
forceParentUpdate={() => forceUpdate()}
|
forceParentUpdate={() =>
|
||||||
|
{
|
||||||
|
setRerenderHeader(rerenderHeader + 1);
|
||||||
|
forceUpdate();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
noMappedFieldsError && <Box color={colors.error.main} textAlign="right">{noMappedFieldsError}</Box>
|
||||||
|
}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</Box>);
|
</Box>);
|
||||||
@ -232,8 +265,6 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
export default BulkLoadFileMappingForm;
|
export default BulkLoadFileMappingForm;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface BulkLoadMappingHeaderProps
|
interface BulkLoadMappingHeaderProps
|
||||||
{
|
{
|
||||||
fileDescription: FileDescription,
|
fileDescription: FileDescription,
|
||||||
@ -241,13 +272,15 @@ interface BulkLoadMappingHeaderProps
|
|||||||
bulkLoadMapping?: BulkLoadMapping,
|
bulkLoadMapping?: BulkLoadMapping,
|
||||||
fieldErrors: { [fieldName: string]: string },
|
fieldErrors: { [fieldName: string]: string },
|
||||||
tableStructure: BulkLoadTableStructure,
|
tableStructure: BulkLoadTableStructure,
|
||||||
forceParentUpdate?: () => void
|
forceParentUpdate?: () => void,
|
||||||
|
frontendStep: QFrontendStepMetaData,
|
||||||
|
processMetaData: QProcessMetaData,
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** private subcomponent - the header section of the bulk load file mapping screen.
|
** private subcomponent - the header section of the bulk load file mapping screen.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate}: BulkLoadMappingHeaderProps): JSX.Element
|
function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData}: BulkLoadMappingHeaderProps): JSX.Element
|
||||||
{
|
{
|
||||||
const viewFields = [
|
const viewFields = [
|
||||||
new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}),
|
new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}),
|
||||||
@ -261,8 +294,6 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
|||||||
|
|
||||||
const hasHeaderRowFormField = {name: "hasHeaderRow", label: "Does the file have a header row?", type: "checkbox", isRequired: true, isEditable: true};
|
const hasHeaderRowFormField = {name: "hasHeaderRow", label: "Does the file have a header row?", type: "checkbox", isRequired: true, isEditable: true};
|
||||||
|
|
||||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
|
||||||
|
|
||||||
const layoutOptions = [
|
const layoutOptions = [
|
||||||
{label: "Flat", id: "FLAT"},
|
{label: "Flat", id: "FLAT"},
|
||||||
{label: "Tall", id: "TALL"},
|
{label: "Tall", id: "TALL"},
|
||||||
@ -276,27 +307,55 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
|||||||
|
|
||||||
const selectedLayout = layoutOptions.filter(o => o.id == bulkLoadMapping.layout)[0] ?? null;
|
const selectedLayout = layoutOptions.filter(o => o.id == bulkLoadMapping.layout)[0] ?? null;
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
function hasHeaderRowChanged(newValue: any)
|
function hasHeaderRowChanged(newValue: any)
|
||||||
{
|
{
|
||||||
bulkLoadMapping.hasHeaderRow = newValue;
|
bulkLoadMapping.hasHeaderRow = newValue;
|
||||||
fileDescription.hasHeaderRow = newValue;
|
fileDescription.hasHeaderRow = newValue;
|
||||||
|
|
||||||
|
bulkLoadMapping.handleChangeToHasHeaderRow(newValue, fileDescription);
|
||||||
|
|
||||||
fieldErrors.hasHeaderRow = null;
|
fieldErrors.hasHeaderRow = null;
|
||||||
forceParentUpdate();
|
forceParentUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
function layoutChanged(event: any, newValue: any)
|
function layoutChanged(event: any, newValue: any)
|
||||||
{
|
{
|
||||||
bulkLoadMapping.layout = newValue ? newValue.id : null;
|
bulkLoadMapping.switchLayout(newValue ? newValue.id : null);
|
||||||
fieldErrors.layout = null;
|
fieldErrors.layout = null;
|
||||||
forceParentUpdate();
|
forceParentUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function getFormattedHelpContent(fieldName: string): JSX.Element
|
||||||
|
{
|
||||||
|
const field = frontendStep?.formFields?.find(f => f.name == fieldName);
|
||||||
|
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||||
|
|
||||||
|
let formattedHelpContent = <HelpContent helpContents={field?.helpContents} roles={helpRoles} helpContentKey={`process:${processMetaData?.name};field:${fieldName}`} />;
|
||||||
|
if (formattedHelpContent)
|
||||||
|
{
|
||||||
|
const mt = field && field.type == QFieldType.BOOLEAN ? "-0.5rem" : "0.5rem";
|
||||||
|
|
||||||
|
return <Box color="#757575" fontSize="0.875rem" mt={mt}>{formattedHelpContent}</Box>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<h5>File Details</h5>
|
<h5>File Details</h5>
|
||||||
<Box ml="1rem">
|
<Box ml="1rem">
|
||||||
<ProcessViewForm fields={viewFields} values={viewValues} columns={2} />
|
<ProcessViewForm fields={viewFields} values={viewValues} columns={2} />
|
||||||
<BulkLoadMappingFilePreview fileDescription={fileDescription} />
|
<BulkLoadMappingFilePreview fileDescription={fileDescription} bulkLoadMapping={bulkLoadMapping} />
|
||||||
<Grid container pt="1rem">
|
<Grid container pt="1rem">
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<DynamicFormFieldLabel name={hasHeaderRowFormField.name} label={`${hasHeaderRowFormField.label} *`} />
|
<DynamicFormFieldLabel name={hasHeaderRowFormField.name} label={`${hasHeaderRowFormField.label} *`} />
|
||||||
@ -307,6 +366,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
|||||||
{<div className="fieldErrorMessage">{fieldErrors.hasHeaderRow}</div>}
|
{<div className="fieldErrorMessage">{fieldErrors.hasHeaderRow}</div>}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
}
|
}
|
||||||
|
{getFormattedHelpContent("hasHeaderRow")}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
|
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
|
||||||
@ -320,6 +380,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
|||||||
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
||||||
isOptionEqualToValue={(option, value) => option == null && value == null || option.id == value.id}
|
isOptionEqualToValue={(option, value) => option == null && value == null || option.id == value.id}
|
||||||
renderOption={(props, option, state) => (<li {...props}>{option?.label ?? ""}</li>)}
|
renderOption={(props, option, state) => (<li {...props}>{option?.label ?? ""}</li>)}
|
||||||
|
disableClearable
|
||||||
sx={{"& .MuiOutlinedInput-root": {padding: "0"}}}
|
sx={{"& .MuiOutlinedInput-root": {padding: "0"}}}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
@ -328,6 +389,7 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
|||||||
{<div className="fieldErrorMessage">{fieldErrors.layout}</div>}
|
{<div className="fieldErrorMessage">{fieldErrors.layout}</div>}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
}
|
}
|
||||||
|
{getFormattedHelpContent("layout")}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@ -336,16 +398,16 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface BulkLoadMappingFilePreviewProps
|
interface BulkLoadMappingFilePreviewProps
|
||||||
{
|
{
|
||||||
fileDescription: FileDescription;
|
fileDescription: FileDescription,
|
||||||
|
bulkLoadMapping?: BulkLoadMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** private subcomponent - the file-preview section of the bulk load file mapping screen.
|
** private subcomponent - the file-preview section of the bulk load file mapping screen.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function BulkLoadMappingFilePreview({fileDescription}: BulkLoadMappingFilePreviewProps): JSX.Element
|
function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoadMappingFilePreviewProps): JSX.Element
|
||||||
{
|
{
|
||||||
const rows: number[] = [];
|
const rows: number[] = [];
|
||||||
for (let i = 0; i < fileDescription.bodyValuesPreview[0].length; i++)
|
for (let i = 0; i < fileDescription.bodyValuesPreview[0].length; i++)
|
||||||
@ -353,25 +415,145 @@ function BulkLoadMappingFilePreview({fileDescription}: BulkLoadMappingFilePrevie
|
|||||||
rows.push(i);
|
rows.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function getValue(i: number, j: number)
|
||||||
|
{
|
||||||
|
const value = fileDescription.bodyValuesPreview[j][i];
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// this was useful at one point in time when we had an object coming back for xlsx files with many different data types //
|
||||||
|
// we'd see a .string attribute, which would have the value we'd want to show. not using it now, but keep in case //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// @ts-ignore
|
||||||
|
if (value && value.string)
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
return (value.string);
|
||||||
|
}
|
||||||
|
return `${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function getHeaderColor(count: number): string
|
||||||
|
{
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
return "blue";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "black";
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function getCursor(count: number): string
|
||||||
|
{
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
return "pointer";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function getColumnTooltip(fields: BulkLoadField[])
|
||||||
|
{
|
||||||
|
return (<Box>
|
||||||
|
This column is mapped to the field{fields.length == 1 ? "" : "s"}:
|
||||||
|
<ul style={{marginLeft: "1rem"}}>
|
||||||
|
{fields.map((field, i) => <li key={i}>{field.getQualifiedLabel()}</li>)}
|
||||||
|
</ul>
|
||||||
|
</Box>);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{"& table, & td": {border: "1px solid black", borderCollapse: "collapse", padding: "0 0.25rem", fontSize: "0.875rem", whiteSpace: "nowrap"}}}>
|
<Box sx={{"& table, & td": {border: "1px solid black", borderCollapse: "collapse", padding: "0 0.25rem", fontSize: "0.875rem", whiteSpace: "nowrap"}}}>
|
||||||
<Box sx={{width: "100%", overflow: "auto"}}>
|
<Box sx={{width: "100%", overflow: "auto"}}>
|
||||||
<table cellSpacing="0" width="100%">
|
<table cellSpacing="0" width="100%">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{backgroundColor: "#d3d3d3"}}>
|
<tr style={{backgroundColor: "#d3d3d3", height: "1.75rem"}}>
|
||||||
<td></td>
|
<td></td>
|
||||||
{fileDescription.headerLetters.map((letter) => <td key={letter} style={{textAlign: "center"}}>{letter}</td>)}
|
{fileDescription.headerLetters.map((letter, index) =>
|
||||||
|
{
|
||||||
|
const fields = bulkLoadMapping.getFieldsForColumnIndex(index);
|
||||||
|
const count = fields.length;
|
||||||
|
|
||||||
|
let dupeWarning = <></>
|
||||||
|
if(fileDescription.hasHeaderRow && fileDescription.duplicateHeaderIndexes[index])
|
||||||
|
{
|
||||||
|
dupeWarning = <Tooltip title="This column header is a duplicate. Only the first occurrance of it will be used." placement="top" enterDelay={500}>
|
||||||
|
<Icon color="warning" sx={{p: "0.125rem", mr: "0.25rem"}}>warning</Icon>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<td key={letter} style={{textAlign: "center", color: getHeaderColor(count), cursor: getCursor(count)}}>
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
count > 0 &&
|
||||||
|
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}>
|
||||||
|
<Box>
|
||||||
|
{dupeWarning}
|
||||||
|
{letter}
|
||||||
|
<Badge badgeContent={count} variant={"standard"} color="secondary" sx={{marginTop: ".75rem"}}><Icon></Icon></Badge>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
count == 0 && <Box>{dupeWarning}{letter}</Box>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
</td>);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style={{backgroundColor: "#d3d3d3", textAlign: "center"}}>1</td>
|
<td style={{backgroundColor: "#d3d3d3", textAlign: "center"}}>1</td>
|
||||||
{fileDescription.headerValues.map((value) => <td key={value} style={{backgroundColor: fileDescription.hasHeaderRow ? "#ebebeb" : ""}}>{value}</td>)}
|
|
||||||
|
{fileDescription.headerValues.map((value, index) =>
|
||||||
|
{
|
||||||
|
const fields = bulkLoadMapping.getFieldsForColumnIndex(index);
|
||||||
|
const count = fields.length;
|
||||||
|
const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""};
|
||||||
|
|
||||||
|
if(fileDescription.hasHeaderRow)
|
||||||
|
{
|
||||||
|
tdStyle.backgroundColor = "#ebebeb";
|
||||||
|
|
||||||
|
if(count > 0)
|
||||||
|
{
|
||||||
|
return <td key={value} style={tdStyle}>
|
||||||
|
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}><Box>{value}</Box></Tooltip>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return <td key={value} style={tdStyle}>{value}</td>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return <td key={value} style={tdStyle}>{value}</td>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
{rows.map((i) => (
|
{rows.map((i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td style={{backgroundColor: "#d3d3d3", textAlign: "center"}}>{i + 2}</td>
|
<td style={{backgroundColor: "#d3d3d3", textAlign: "center"}}>{i + 2}</td>
|
||||||
{fileDescription.headerLetters.map((letter, j) => <td key={j}>{fileDescription.bodyValuesPreview[j][i]}</td>)}
|
{fileDescription.headerLetters.map((letter, j) => <td key={j}>{getValue(i, j)}</td>)}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -266,76 +266,6 @@ function ValidationReview({
|
|||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
/***************************************************************************
|
|
||||||
**
|
|
||||||
***************************************************************************/
|
|
||||||
function previewRecordUsingTableLayout(record: QRecord)
|
|
||||||
{
|
|
||||||
if (!previewTableMetaData)
|
|
||||||
{
|
|
||||||
return (<Box>Loading...</Box>);
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedSections: JSX.Element[] = [];
|
|
||||||
const tableSections = TableUtils.getSectionsForRecordSidebar(previewTableMetaData);
|
|
||||||
const previewRecord = previewRecords[previewRecordIndex];
|
|
||||||
|
|
||||||
for (let i = 0; i < tableSections.length; i++)
|
|
||||||
{
|
|
||||||
const section = tableSections[i];
|
|
||||||
if (section.isHidden)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (section.fieldNames)
|
|
||||||
{
|
|
||||||
renderedSections.push(<Box mb="1rem">
|
|
||||||
<Box><h4>{section.label}</h4></Box>
|
|
||||||
<Box ml="1rem">
|
|
||||||
{renderSectionOfFields(section.name, section.fieldNames, previewTableMetaData, false, previewRecord, undefined, {label: {fontWeight: "500"}})}
|
|
||||||
</Box>
|
|
||||||
</Box>);
|
|
||||||
}
|
|
||||||
else if (section.widgetName)
|
|
||||||
{
|
|
||||||
const widget = qInstance.widgets.get(section.widgetName);
|
|
||||||
if (widget)
|
|
||||||
{
|
|
||||||
let data: ChildRecordListData = null;
|
|
||||||
if (associationPreviewsByWidgetName[section.widgetName])
|
|
||||||
{
|
|
||||||
const associationPreview = associationPreviewsByWidgetName[section.widgetName];
|
|
||||||
const associationRecords = previewRecord.associatedRecords.get(associationPreview.associationName) ?? [];
|
|
||||||
data = {
|
|
||||||
canAddChildRecord: false,
|
|
||||||
childTableMetaData: childTableMetaData[associationPreview.tableName],
|
|
||||||
defaultValuesForNewChildRecords: {},
|
|
||||||
disabledFieldsForNewChildRecords: {},
|
|
||||||
queryOutput: {records: associationRecords},
|
|
||||||
totalRows: associationRecords.length,
|
|
||||||
tablePath: "",
|
|
||||||
title: "",
|
|
||||||
viewAllLink: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
renderedSections.push(<Box mb="1rem">
|
|
||||||
{
|
|
||||||
data && <Box>
|
|
||||||
<Box mb="0.5rem"><h4>{section.label}</h4></Box>
|
|
||||||
<Box pl="1rem">
|
|
||||||
<RecordGridWidget data={data} widgetMetaData={widget} disableRowClick gridOnly={true} gridDensity={"compact"} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
</Box>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderedSections;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordPreviewWidget = step.recordListFields && (
|
const recordPreviewWidget = step.recordListFields && (
|
||||||
<Box border="1px solid rgb(70%, 70%, 70%)" borderRadius="10px" p={2} mt={2}>
|
<Box border="1px solid rgb(70%, 70%, 70%)" borderRadius="10px" p={2} mt={2}>
|
||||||
@ -405,7 +335,15 @@ function ValidationReview({
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
previewRecords && processValues.formatPreviewRecordUsingTableLayout && previewRecords[previewRecordIndex] && previewRecordUsingTableLayout(previewRecords[previewRecordIndex])
|
previewRecords && processValues.formatPreviewRecordUsingTableLayout && previewRecords[previewRecordIndex] &&
|
||||||
|
<PreviewRecordUsingTableLayout
|
||||||
|
index={previewRecordIndex}
|
||||||
|
record={previewRecords[previewRecordIndex]}
|
||||||
|
tableMetaData={previewTableMetaData}
|
||||||
|
qInstance={qInstance}
|
||||||
|
associationPreviewsByWidgetName={associationPreviewsByWidgetName}
|
||||||
|
childTableMetaData={childTableMetaData}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@ -441,4 +379,84 @@ function ValidationReview({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface PreviewRecordUsingTableLayoutProps
|
||||||
|
{
|
||||||
|
index: number
|
||||||
|
record: QRecord,
|
||||||
|
tableMetaData: QTableMetaData,
|
||||||
|
qInstance: QInstance,
|
||||||
|
associationPreviewsByWidgetName: { [widgetName: string]: AssociationPreview },
|
||||||
|
childTableMetaData: { [name: string]: QTableMetaData },
|
||||||
|
}
|
||||||
|
|
||||||
|
function PreviewRecordUsingTableLayout({record, tableMetaData, qInstance, associationPreviewsByWidgetName, childTableMetaData, index}: PreviewRecordUsingTableLayoutProps): JSX.Element
|
||||||
|
{
|
||||||
|
if (!tableMetaData)
|
||||||
|
{
|
||||||
|
return (<i>Loading...</i>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedSections: JSX.Element[] = [];
|
||||||
|
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData);
|
||||||
|
|
||||||
|
for (let i = 0; i < tableSections.length; i++)
|
||||||
|
{
|
||||||
|
const section = tableSections[i];
|
||||||
|
if (section.isHidden)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.fieldNames)
|
||||||
|
{
|
||||||
|
renderedSections.push(<Box mb="1rem">
|
||||||
|
<Box><h4>{section.label}</h4></Box>
|
||||||
|
<Box ml="1rem">
|
||||||
|
{renderSectionOfFields(section.name, section.fieldNames, tableMetaData, false, record, undefined, {label: {fontWeight: "500"}})}
|
||||||
|
</Box>
|
||||||
|
</Box>);
|
||||||
|
}
|
||||||
|
else if (section.widgetName)
|
||||||
|
{
|
||||||
|
const widget = qInstance.widgets.get(section.widgetName);
|
||||||
|
if (widget)
|
||||||
|
{
|
||||||
|
let data: ChildRecordListData = null;
|
||||||
|
if (associationPreviewsByWidgetName[section.widgetName])
|
||||||
|
{
|
||||||
|
const associationPreview = associationPreviewsByWidgetName[section.widgetName];
|
||||||
|
const associationRecords = record.associatedRecords?.get(associationPreview.associationName) ?? [];
|
||||||
|
data = {
|
||||||
|
canAddChildRecord: false,
|
||||||
|
childTableMetaData: childTableMetaData[associationPreview.tableName],
|
||||||
|
defaultValuesForNewChildRecords: {},
|
||||||
|
disabledFieldsForNewChildRecords: {},
|
||||||
|
queryOutput: {records: associationRecords},
|
||||||
|
totalRows: associationRecords.length,
|
||||||
|
tablePath: "",
|
||||||
|
title: "",
|
||||||
|
viewAllLink: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
renderedSections.push(<Box mb="1rem">
|
||||||
|
{
|
||||||
|
data && <Box>
|
||||||
|
<Box mb="0.5rem"><h4>{section.label}</h4></Box>
|
||||||
|
<Box pl="1rem">
|
||||||
|
<RecordGridWidget key={index} data={data} widgetMetaData={widget} disableRowClick gridOnly={true} gridDensity={"compact"} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</Box>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{renderedSections}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default ValidationReview;
|
export default ValidationReview;
|
||||||
|
@ -19,6 +19,10 @@
|
|||||||
* 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 {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
import {FormControl, InputLabel, Select, SelectChangeEvent} from "@mui/material";
|
import {FormControl, InputLabel, Select, SelectChangeEvent} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
@ -28,20 +32,26 @@ import Modal from "@mui/material/Modal";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import {GridFilterItem} from "@mui/x-data-grid-pro";
|
|
||||||
import React, {useEffect, useState} from "react";
|
|
||||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
|
|
||||||
import ChipTextField from "qqq/components/forms/ChipTextField";
|
import ChipTextField from "qqq/components/forms/ChipTextField";
|
||||||
|
import HelpContent from "qqq/components/misc/HelpContent";
|
||||||
|
import {LoadingState} from "qqq/models/LoadingState";
|
||||||
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import React, {useEffect, useReducer, useState} from "react";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
type: string;
|
type: string;
|
||||||
onSave: (newValues: any[]) => void;
|
onSave: (newValues: any[]) => void;
|
||||||
|
table?: QTableMetaData;
|
||||||
|
field?: QFieldMetaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterCriteriaPaster.defaultProps = {};
|
FilterCriteriaPaster.defaultProps = {};
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
enum Delimiter
|
enum Delimiter
|
||||||
{
|
{
|
||||||
@ -68,6 +78,12 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
mainCardStyles.width = "60%";
|
mainCardStyles.width = "60%";
|
||||||
mainCardStyles.minWidth = "500px";
|
mainCardStyles.minWidth = "500px";
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// add a LoadingState object, in case the initial loads (of meta data and view) are slow //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
const [pageLoadingState, _] = useState(new LoadingState(forceUpdate));
|
||||||
|
|
||||||
//x const [gridFilterItem, setGridFilterItem] = useState(props.item);
|
//x const [gridFilterItem, setGridFilterItem] = useState(props.item);
|
||||||
const [pasteModalIsOpen, setPasteModalIsOpen] = useState(false);
|
const [pasteModalIsOpen, setPasteModalIsOpen] = useState(false);
|
||||||
const [inputText, setInputText] = useState("");
|
const [inputText, setInputText] = useState("");
|
||||||
@ -75,8 +91,13 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
const [delimiterCharacter, setDelimiterCharacter] = useState("");
|
const [delimiterCharacter, setDelimiterCharacter] = useState("");
|
||||||
const [customDelimiterValue, setCustomDelimiterValue] = useState("");
|
const [customDelimiterValue, setCustomDelimiterValue] = useState("");
|
||||||
const [chipData, setChipData] = useState(undefined);
|
const [chipData, setChipData] = useState(undefined);
|
||||||
|
const [uniqueCount, setUniqueCount] = useState(undefined);
|
||||||
|
const [chipValidity, setChipValidity] = useState([] as boolean[]);
|
||||||
|
const [chipPVSIds, setChipPVSIds] = useState([] as any[]);
|
||||||
const [detectedText, setDetectedText] = useState("");
|
const [detectedText, setDetectedText] = useState("");
|
||||||
const [errorText, setErrorText] = useState("");
|
const [errorText, setErrorText] = useState("");
|
||||||
|
const [saveDisabled, setSaveDisabled] = useState(true);
|
||||||
|
const [metaData, setMetaData] = useState(null as QInstance);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
// handler for when paste icon is clicked in 'any' operator //
|
// handler for when paste icon is clicked in 'any' operator //
|
||||||
@ -92,6 +113,7 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
setDelimiter("");
|
setDelimiter("");
|
||||||
setDelimiterCharacter("");
|
setDelimiterCharacter("");
|
||||||
setChipData([]);
|
setChipData([]);
|
||||||
|
setChipValidity([]);
|
||||||
setInputText("");
|
setInputText("");
|
||||||
setDetectedText("");
|
setDetectedText("");
|
||||||
setCustomDelimiterValue("");
|
setCustomDelimiterValue("");
|
||||||
@ -106,17 +128,42 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
|
|
||||||
const handleSaveClicked = () =>
|
const handleSaveClicked = () =>
|
||||||
{
|
{
|
||||||
////////////////////////////////////////
|
///////////////////////////////////////////////////////////////
|
||||||
// if numeric remove any non-numerics //
|
// if numeric remove any non-numerics, or invalid pvs values //
|
||||||
////////////////////////////////////////
|
///////////////////////////////////////////////////////////////
|
||||||
let saveData = [];
|
let saveData = [];
|
||||||
|
let usedLabels = new Map<any, boolean>();
|
||||||
for (let i = 0; i < chipData.length; i++)
|
for (let i = 0; i < chipData.length; i++)
|
||||||
{
|
{
|
||||||
if (type !== "number" || !Number.isNaN(Number(chipData[i])))
|
if (chipValidity[i] === true)
|
||||||
|
{
|
||||||
|
if (type === "pvs")
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// if already used this PVS label, skip it //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
if (usedLabels.get(chipData[i]) != null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData.push(new QPossibleValue({id: chipPVSIds[i], label: chipData[i]}));
|
||||||
|
usedLabels.set(chipData[i], true);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
saveData.push(chipData[i]);
|
saveData.push(chipData[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// for pvs, sort by label before saving //
|
||||||
|
//////////////////////////////////////////
|
||||||
|
if (type === "pvs")
|
||||||
|
{
|
||||||
|
saveData.sort((a: QPossibleValue, b: QPossibleValue) => b.label.localeCompare(a.label));
|
||||||
|
}
|
||||||
|
|
||||||
onSave(saveData);
|
onSave(saveData);
|
||||||
|
|
||||||
@ -214,6 +261,12 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
const metaData = await qController.loadMetaData();
|
||||||
|
setMetaData(metaData);
|
||||||
|
})();
|
||||||
|
|
||||||
let currentDelimiter = delimiter;
|
let currentDelimiter = delimiter;
|
||||||
let currentDelimiterCharacter = delimiterCharacter;
|
let currentDelimiterCharacter = delimiterCharacter;
|
||||||
|
|
||||||
@ -246,10 +299,16 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
let parts = inputText.split(regex);
|
let parts = inputText.split(regex);
|
||||||
let chipData = [] as string[];
|
let chipData = [] as string[];
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
// use a map to keep track of the counts for each unique value //
|
||||||
|
/////////////////////////////////////////////////////////////////
|
||||||
|
const uniqueValuesMap: { [key: string]: number } = {};
|
||||||
|
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
// if delimiter is empty string, dont split anything //
|
// if delimiter is empty string, dont split anything //
|
||||||
///////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////
|
||||||
setErrorText("");
|
setErrorText("");
|
||||||
|
let invalidCount = 0;
|
||||||
if (currentDelimiterCharacter !== "")
|
if (currentDelimiterCharacter !== "")
|
||||||
{
|
{
|
||||||
for (let i = 0; i < parts.length; i++)
|
for (let i = 0; i < parts.length; i++)
|
||||||
@ -259,20 +318,47 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
chipData.push(part);
|
chipData.push(part);
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
// if numeric, check that first before pushing as a chip //
|
// if numeric or pvs, check validity and add to invalid count //
|
||||||
///////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
if (type === "number" && Number.isNaN(Number(part)))
|
if (chipValidity[i] != null && chipValidity[i] !== true)
|
||||||
{
|
{
|
||||||
setErrorText("Some values are not numbers");
|
if ((type === "number" && Number.isNaN(Number(part))) || type === "pvs")
|
||||||
|
{
|
||||||
|
invalidCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let count = uniqueValuesMap[part] == null ? 0 : uniqueValuesMap[part];
|
||||||
|
uniqueValuesMap[part] = count + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (invalidCount > 0)
|
||||||
|
{
|
||||||
|
if (type === "number")
|
||||||
|
{
|
||||||
|
let suffix = invalidCount === 1 ? " value is not a number" : " values are not numbers";
|
||||||
|
setErrorText(invalidCount + suffix + "numbers and will not be added to the filter");
|
||||||
|
}
|
||||||
|
else if (type === "pvs")
|
||||||
|
{
|
||||||
|
let suffix = invalidCount === 1 ? " value was" : " values were";
|
||||||
|
setErrorText(invalidCount + suffix + " not found and will not be added to the filter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUniqueCount(Object.keys(uniqueValuesMap).length);
|
||||||
setChipData(chipData);
|
setChipData(chipData);
|
||||||
|
|
||||||
}, [inputText, delimiterCharacter, customDelimiterValue, detectedText]);
|
}, [inputText, delimiterCharacter, customDelimiterValue, detectedText, chipValidity]);
|
||||||
|
|
||||||
|
const slotName = type === "pvs" ? "bulkAddFilterValuesPossibleValueSource" : "bulkAddFilterValues";
|
||||||
|
const helpRoles = ["QUERY_SCREEN"];
|
||||||
|
const formattedHelpContent = <HelpContent helpContents={metaData?.helpContent?.get(slotName)} roles={helpRoles} heading={null} helpContentKey={`instanceLevel:true;slot:${slotName}`} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
@ -283,6 +369,7 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
pasteModalIsOpen &&
|
pasteModalIsOpen &&
|
||||||
(
|
(
|
||||||
<Modal open={pasteModalIsOpen}>
|
<Modal open={pasteModalIsOpen}>
|
||||||
|
<Box>
|
||||||
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
|
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
|
||||||
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
|
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
|
||||||
<Card sx={mainCardStyles}>
|
<Card sx={mainCardStyles}>
|
||||||
@ -290,11 +377,13 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item pr={3} xs={12} lg={12}>
|
<Grid item pr={3} xs={12} lg={12}>
|
||||||
<Typography variant="h5">Bulk Add Filter Values</Typography>
|
<Typography variant="h5">Bulk Add Filter Values</Typography>
|
||||||
|
{
|
||||||
|
formattedHelpContent && <Box sx={{display: "flex", lineHeight: "1.7", textTransform: "none"}}>
|
||||||
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
|
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
|
||||||
Paste into the box on the left.
|
{formattedHelpContent}
|
||||||
Review the filter values in the box on the right.
|
|
||||||
If the filter values are not what are expected, try changing the separator using the dropdown below.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@ -314,10 +403,25 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
<Grid item xs={6} lg={6} sx={{display: "flex", flexGrow: 1}}>
|
<Grid item xs={6} lg={6} sx={{display: "flex", flexGrow: 1}}>
|
||||||
<FormControl sx={{m: 1, width: "100%"}}>
|
<FormControl sx={{m: 1, width: "100%"}}>
|
||||||
<ChipTextField
|
<ChipTextField
|
||||||
handleChipChange={() =>
|
handleChipChange={(isMakingRequest: boolean, chipValidity: boolean[], chipPVSIds: any[]) =>
|
||||||
{
|
{
|
||||||
|
setErrorText("");
|
||||||
|
if (isMakingRequest)
|
||||||
|
{
|
||||||
|
pageLoadingState.setLoading();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pageLoadingState.setNotLoading();
|
||||||
|
}
|
||||||
|
setSaveDisabled(isMakingRequest);
|
||||||
|
setChipPVSIds(chipPVSIds);
|
||||||
|
setChipValidity(chipValidity);
|
||||||
}}
|
}}
|
||||||
|
table={table}
|
||||||
|
field={field}
|
||||||
chipData={chipData}
|
chipData={chipData}
|
||||||
|
chipValidity={chipValidity}
|
||||||
chipType={type}
|
chipType={type}
|
||||||
multiline
|
multiline
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -377,7 +481,7 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}} item pl={1} xs={3} lg={3}>
|
<Grid sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}} item pl={1} xs={4} lg={4}>
|
||||||
{
|
{
|
||||||
errorText && chipData.length > 0 && (
|
errorText && chipData.length > 0 && (
|
||||||
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}>
|
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}>
|
||||||
@ -386,11 +490,19 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
pageLoadingState.isLoadingSlow() && (
|
||||||
|
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}>
|
||||||
|
<Icon color="warning">warning</Icon>
|
||||||
|
<Typography sx={{paddingLeft: "4px", textTransform: "revert"}} variant="button">Loading...</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid sx={{display: "flex", justifyContent: "flex-end", alignItems: "flex-start"}} item pr={1} xs={3} lg={3}>
|
<Grid sx={{display: "flex", justifyContent: "flex-end", alignItems: "flex-start"}} item pr={1} xs={2} lg={2}>
|
||||||
{
|
{
|
||||||
chipData && chipData.length > 0 && (
|
chipData && chipData.length > 0 && (
|
||||||
<Typography sx={{textTransform: "revert"}} variant="button">{chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"}</Typography>
|
<Typography sx={{textTransform: "revert"}} variant="button">{chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"} {uniqueCount && `(${uniqueCount} unique)`}</Typography>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -401,12 +513,13 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
|
|||||||
onClickHandler={handleCancelClicked}
|
onClickHandler={handleCancelClicked}
|
||||||
iconName="cancel"
|
iconName="cancel"
|
||||||
disabled={false} />
|
disabled={false} />
|
||||||
<QSaveButton onClickHandler={handleSaveClicked} label="Add Filters" disabled={false} />
|
<QSaveButton onClickHandler={handleSaveClicked} label="Add Values" disabled={saveDisabled} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
)
|
)
|
||||||
|
@ -109,6 +109,7 @@ export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: str
|
|||||||
{
|
{
|
||||||
case QFieldType.DECIMAL:
|
case QFieldType.DECIMAL:
|
||||||
case QFieldType.INTEGER:
|
case QFieldType.INTEGER:
|
||||||
|
case QFieldType.LONG:
|
||||||
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
|
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
|
||||||
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
|
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
|
||||||
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
|
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
|
||||||
|
@ -398,11 +398,12 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
initialValues = criteria.values;
|
initialValues = criteria.values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <Box>
|
return <Box display="flex" alignItems="flex-end" className="multiValue">
|
||||||
|
<Box width={"100%"}>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
|
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
|
||||||
overrideId={field.name + "-multi-" + criteria.id}
|
overrideId={field.name + "-multi-" + criteria.id}
|
||||||
key={field.name + "-multi-" + criteria.id}
|
key={field.name + "-multi-" + criteria.id + "-" + criteria.values.length}
|
||||||
isMultiple
|
isMultiple
|
||||||
fieldLabel="Values"
|
fieldLabel="Values"
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
@ -412,6 +413,10 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
variant="standard"
|
variant="standard"
|
||||||
useCase="filter"
|
useCase="filter"
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<FilterCriteriaPaster table={table} field={field} type="pvs" onSave={(newValues: any[]) => saveNewPasterValues(newValues)} />
|
||||||
|
</Box>
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ import XIcon from "qqq/components/query/XIcon";
|
|||||||
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
|
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
|
||||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
||||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||||
import React, {SyntheticEvent, useContext, useReducer, useState} from "react";
|
import React, {SyntheticEvent, useContext, useEffect, useReducer, useState} from "react";
|
||||||
|
|
||||||
export type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex";
|
export type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex";
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ const doesOperatorOptionEqualCriteria = (operatorOption: OperatorOption, criteri
|
|||||||
** autocomplete), given an array of options, the query's active criteria in this
|
** autocomplete), given an array of options, the query's active criteria in this
|
||||||
** field, and the default operator to use for this field
|
** field, and the default operator to use for this field
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: QFilterCriteriaWithId, defaultOperator: QCriteriaOperator): OperatorOption =>
|
const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: QFilterCriteriaWithId, defaultOperator: QCriteriaOperator, return0thOptionInsteadOfNull: boolean = false): OperatorOption =>
|
||||||
{
|
{
|
||||||
if (criteria)
|
if (criteria)
|
||||||
{
|
{
|
||||||
@ -135,6 +135,23 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
|
|||||||
return (filteredOptions[0]);
|
return (filteredOptions[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (return0thOptionInsteadOfNull)
|
||||||
|
{
|
||||||
|
console.log("Returning 0th operator instead of null - this isn't expected, but has been seen to happen - so here's some additional debugging:");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
console.log("Operator options: " + JSON.stringify(operatorOptions));
|
||||||
|
console.log("Criteria: " + JSON.stringify(criteria));
|
||||||
|
console.log("Default Operator: " + JSON.stringify(defaultOperator));
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log(`Error in debug output: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return operatorOptions[0];
|
||||||
|
}
|
||||||
|
|
||||||
return (null);
|
return (null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,7 +174,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? Object.assign({}, criteriaParam) as QFilterCriteriaWithId : null);
|
const [criteria, setCriteria] = useState(criteriaParamIsCriteria(criteriaParam) ? Object.assign({}, criteriaParam) as QFilterCriteriaWithId : null);
|
||||||
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
|
const [id, setId] = useState(criteriaParamIsCriteria(criteriaParam) ? (criteriaParam as QFilterCriteriaWithId).id : ++seedId);
|
||||||
|
|
||||||
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator));
|
const [operatorSelectedValue, setOperatorSelectedValue] = useState(getOperatorSelectedValue(operatorOptions, criteria, defaultOperator, true));
|
||||||
const [operatorInputValue, setOperatorInputValue] = useState(operatorSelectedValue?.label);
|
const [operatorInputValue, setOperatorInputValue] = useState(operatorSelectedValue?.label);
|
||||||
|
|
||||||
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
|
const {criteriaIsValid, criteriaStatusTooltip} = validateCriteria(criteria, operatorSelectedValue);
|
||||||
@ -169,6 +186,13 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
|||||||
//////////////////////
|
//////////////////////
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// was not seeing criteria changes take place until watching it stringified //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
setCriteria(criteria);
|
||||||
|
}, [JSON.stringify(criteria)]);
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
|
@ -49,7 +49,7 @@ function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.El
|
|||||||
<Card sx={{width: "100%", height: "100%"}}>
|
<Card sx={{width: "100%", height: "100%"}}>
|
||||||
<Typography variant="h6" p={2} pb={1}>{heading}</Typography>
|
<Typography variant="h6" p={2} pb={1}>{heading}</Typography>
|
||||||
<Box className="devDocumentation" height="100%">
|
<Box className="devDocumentation" height="100%">
|
||||||
<Typography variant="body2" sx={{maxWidth: "1200px", margin: "auto", height: "100%"}}>
|
<Typography variant="body2" sx={{maxWidth: "1200px", margin: "auto", height: "calc(100% - 0.5rem)"}}>
|
||||||
<AceEditor
|
<AceEditor
|
||||||
mode={mode}
|
mode={mode}
|
||||||
theme="github"
|
theme="github"
|
||||||
@ -62,7 +62,7 @@ function ScriptDocsForm({helpText, exampleCode, aceEditorHeight}: Props): JSX.El
|
|||||||
width="100%"
|
width="100%"
|
||||||
showPrintMargin={false}
|
showPrintMargin={false}
|
||||||
height="100%"
|
height="100%"
|
||||||
style={{borderBottomRightRadius: "1rem", borderBottomLeftRadius: "1rem"}}
|
style={{borderBottomRightRadius: "0.75rem", borderBottomLeftRadius: "0.75rem"}}
|
||||||
/>
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -313,6 +313,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
function deleteChildRecord(name: string, widgetIndex: number, rowIndex: number)
|
function deleteChildRecord(name: string, widgetIndex: number, rowIndex: number)
|
||||||
{
|
{
|
||||||
updateChildRecordList(name, "delete", rowIndex);
|
updateChildRecordList(name, "delete", rowIndex);
|
||||||
|
forceUpdate();
|
||||||
actionCallback(widgetData[widgetIndex]);
|
actionCallback(widgetData[widgetIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -368,7 +369,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function submitEditChildForm(values: any)
|
function submitEditChildForm(values: any, tableName: string)
|
||||||
{
|
{
|
||||||
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
|
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
|
||||||
let widgetIndex = determineChildRecordListIndex(showEditChildForm.widgetName);
|
let widgetIndex = determineChildRecordListIndex(showEditChildForm.widgetName);
|
||||||
@ -718,6 +719,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
addNewRecordCallback={widgetData[i]?.isInProcess ? () => openAddChildRecord(widgetMetaData.name, widgetData[i]) : null}
|
addNewRecordCallback={widgetData[i]?.isInProcess ? () => openAddChildRecord(widgetMetaData.name, widgetData[i]) : null}
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
data={widgetData[i]}
|
data={widgetData[i]}
|
||||||
|
parentRecord={record}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -46,11 +46,12 @@ import React, {useContext, useEffect, useRef, useState} from "react";
|
|||||||
|
|
||||||
interface FilterAndColumnsSetupWidgetProps
|
interface FilterAndColumnsSetupWidgetProps
|
||||||
{
|
{
|
||||||
isEditable: boolean;
|
isEditable: boolean,
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData,
|
||||||
widgetData: any;
|
widgetData: any,
|
||||||
recordValues: { [name: string]: any };
|
recordValues: { [name: string]: any },
|
||||||
onSaveCallback?: (values: { [name: string]: any }) => void;
|
onSaveCallback?: (values: { [name: string]: any }) => void,
|
||||||
|
label?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterAndColumnsSetupWidget.defaultProps = {
|
FilterAndColumnsSetupWidget.defaultProps = {
|
||||||
@ -83,13 +84,16 @@ 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 FilterAndColumnsSetupWidget({isEditable, widgetMetaData, widgetData, recordValues, onSaveCallback}: FilterAndColumnsSetupWidgetProps): JSX.Element
|
export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData, widgetData, recordValues, onSaveCallback, label}: FilterAndColumnsSetupWidgetProps): JSX.Element
|
||||||
{
|
{
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
const [hideColumns] = useState(widgetData?.hideColumns);
|
||||||
const [hidePreview, setHidePreview] = useState(widgetData?.hidePreview);
|
const [hidePreview] = useState(widgetData?.hidePreview);
|
||||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
|
|
||||||
|
const [filterFieldName] = useState(widgetData?.filterFieldName ?? "queryFilterJson");
|
||||||
|
const [columnsFieldName] = useState(widgetData?.columnsFieldName ?? "columnsJson");
|
||||||
|
|
||||||
const [alertContent, setAlertContent] = useState(null as string);
|
const [alertContent, setAlertContent] = useState(null as string);
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -108,7 +112,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
let columns: QQueryColumns = null;
|
let columns: QQueryColumns = null;
|
||||||
let usingDefaultEmptyFilter = false;
|
let usingDefaultEmptyFilter = false;
|
||||||
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
|
let queryFilter = recordValues[filterFieldName] && JSON.parse(recordValues[filterFieldName]) as QQueryFilter;
|
||||||
const defaultFilterFields = widgetData?.filterDefaultFieldNames;
|
const defaultFilterFields = widgetData?.filterDefaultFieldNames;
|
||||||
if (!queryFilter)
|
if (!queryFilter)
|
||||||
{
|
{
|
||||||
@ -142,9 +146,9 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recordValues["columnsJson"])
|
if (recordValues[columnsFieldName])
|
||||||
{
|
{
|
||||||
columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]);
|
columns = QQueryColumns.buildFromJSON(recordValues[columnsFieldName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
@ -230,7 +234,10 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
setFrontendQueryFilter(view.queryFilter);
|
setFrontendQueryFilter(view.queryFilter);
|
||||||
const filter = FilterUtils.prepQueryFilterForBackend(tableMetaData, view.queryFilter);
|
const filter = FilterUtils.prepQueryFilterForBackend(tableMetaData, view.queryFilter);
|
||||||
|
|
||||||
onSaveCallback({queryFilterJson: JSON.stringify(filter), columnsJson: JSON.stringify(view.queryColumns)});
|
const rs: { [key: string]: any } = {};
|
||||||
|
rs[filterFieldName] = JSON.stringify(filter);
|
||||||
|
rs[columnsFieldName] = JSON.stringify(view.queryColumns);
|
||||||
|
onSaveCallback(rs);
|
||||||
|
|
||||||
closeEditor();
|
closeEditor();
|
||||||
}
|
}
|
||||||
@ -356,7 +363,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
</Collapse>
|
</Collapse>
|
||||||
<Box pt="0.5rem">
|
<Box pt="0.5rem">
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
<h5>Query Filter</h5>
|
<h5>{label ?? "Query Filter"}</h5>
|
||||||
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{
|
{
|
||||||
|
@ -40,7 +40,7 @@ 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, displayValues?: any } [] };
|
||||||
childTableMetaData?: QTableMetaData;
|
childTableMetaData?: QTableMetaData;
|
||||||
tablePath?: string;
|
tablePath?: string;
|
||||||
viewAllLink?: string;
|
viewAllLink?: string;
|
||||||
@ -48,20 +48,22 @@ export interface ChildRecordListData extends WidgetData
|
|||||||
canAddChildRecord?: boolean;
|
canAddChildRecord?: boolean;
|
||||||
defaultValuesForNewChildRecords?: { [fieldName: string]: any };
|
defaultValuesForNewChildRecords?: { [fieldName: string]: any };
|
||||||
disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
|
disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
|
||||||
|
defaultValuesForNewChildRecordsFromParentFields?: { [fieldName: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData,
|
||||||
data: ChildRecordListData;
|
data: ChildRecordListData,
|
||||||
addNewRecordCallback?: () => void;
|
addNewRecordCallback?: () => void,
|
||||||
disableRowClick: boolean;
|
disableRowClick: boolean,
|
||||||
allowRecordEdit: boolean;
|
allowRecordEdit: boolean,
|
||||||
editRecordCallback?: (rowIndex: number) => void;
|
editRecordCallback?: (rowIndex: number) => void,
|
||||||
allowRecordDelete: boolean;
|
allowRecordDelete: boolean,
|
||||||
deleteRecordCallback?: (rowIndex: number) => void;
|
deleteRecordCallback?: (rowIndex: number) => void,
|
||||||
gridOnly?: boolean;
|
gridOnly?: boolean,
|
||||||
gridDensity?: GridDensity;
|
gridDensity?: GridDensity,
|
||||||
|
parentRecord?: QRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordGridWidget.defaultProps =
|
RecordGridWidget.defaultProps =
|
||||||
@ -74,7 +76,7 @@ RecordGridWidget.defaultProps =
|
|||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRowClick, allowRecordEdit, editRecordCallback, allowRecordDelete, deleteRecordCallback, gridOnly, gridDensity}: Props): JSX.Element
|
function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRowClick, allowRecordEdit, editRecordCallback, allowRecordDelete, deleteRecordCallback, gridOnly, gridDensity, parentRecord}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const instance = useRef({timer: null});
|
const instance = useRef({timer: null});
|
||||||
const [rows, setRows] = useState([]);
|
const [rows, setRows] = useState([]);
|
||||||
@ -97,7 +99,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
{
|
{
|
||||||
for (let i = 0; i < queryOutputRecords.length; i++)
|
for (let i = 0; i < queryOutputRecords.length; i++)
|
||||||
{
|
{
|
||||||
if(queryOutputRecords[i] instanceof QRecord)
|
if (queryOutputRecords[i] instanceof QRecord)
|
||||||
{
|
{
|
||||||
records.push(queryOutputRecords[i] as QRecord);
|
records.push(queryOutputRecords[i] as QRecord);
|
||||||
}
|
}
|
||||||
@ -109,7 +111,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tableMetaData = data.childTableMetaData instanceof QTableMetaData ? data.childTableMetaData as QTableMetaData : new QTableMetaData(data.childTableMetaData);
|
const tableMetaData = data.childTableMetaData instanceof QTableMetaData ? data.childTableMetaData as QTableMetaData : new QTableMetaData(data.childTableMetaData);
|
||||||
const rows = DataGridUtils.makeRows(records, tableMetaData, true);
|
const rows = DataGridUtils.makeRows(records, tableMetaData, undefined, true);
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
// note - tablePath may be null, if the user doesn't have access to the table. //
|
// note - tablePath may be null, if the user doesn't have access to the table. //
|
||||||
@ -252,7 +254,22 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
{
|
{
|
||||||
disabledFields = data.defaultValuesForNewChildRecords;
|
disabledFields = data.defaultValuesForNewChildRecords;
|
||||||
}
|
}
|
||||||
labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, data.defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback));
|
|
||||||
|
const defaultValuesForNewChildRecords = data.defaultValuesForNewChildRecords || {};
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// copy values from specified fields in the parent record down into the child record //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (data.defaultValuesForNewChildRecordsFromParentFields)
|
||||||
|
{
|
||||||
|
for (let childField in data.defaultValuesForNewChildRecordsFromParentFields)
|
||||||
|
{
|
||||||
|
const parentField = data.defaultValuesForNewChildRecordsFromParentFields[childField];
|
||||||
|
defaultValuesForNewChildRecords[childField] = parentRecord?.values?.get(parentField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labelAdditionalComponentsRight.push(new AddNewRecordButton(data.childTableMetaData, defaultValuesForNewChildRecords, "Add new", disabledFields, addNewRecordCallback));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -357,7 +374,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if(gridOnly)
|
if (gridOnly)
|
||||||
{
|
{
|
||||||
return (grid);
|
return (grid);
|
||||||
}
|
}
|
||||||
|
@ -393,7 +393,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container className="scriptViewer" m={-2} pt={4} width={"calc(100% + 2rem)"}>
|
<Grid container className="scriptViewer" my={-3} mx={-3} pt={4} width={"calc(100% + 3rem)"}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
@ -530,7 +530,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel index={2} value={selectedTab}>
|
<TabPanel index={2} value={selectedTab}>
|
||||||
<Box sx={{height: "455px"}} px={2} pb={1}>
|
<Box sx={{height: "455px"}} px={2} pt={1}>
|
||||||
<ScriptTestForm scriptId={scriptId}
|
<ScriptTestForm scriptId={scriptId}
|
||||||
scriptType={scriptTypeRecord}
|
scriptType={scriptTypeRecord}
|
||||||
tableName={associatedScriptTableName}
|
tableName={associatedScriptTableName}
|
||||||
@ -543,7 +543,7 @@ export default function ScriptViewer({scriptId, associatedScriptTableName, assoc
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel index={3} value={selectedTab}>
|
<TabPanel index={3} value={selectedTab}>
|
||||||
<Box sx={{height: "455px"}} px={2} pb={1}>
|
<Box sx={{height: "455px"}} px={2} pt={1}>
|
||||||
<ScriptDocsForm helpText={scriptTypeRecord?.values.get("helpText")} exampleCode={scriptTypeRecord?.values.get("sampleCode")} />
|
<ScriptDocsForm helpText={scriptTypeRecord?.values.get("helpText")} exampleCode={scriptTypeRecord?.values.get("sampleCode")} />
|
||||||
</Box>
|
</Box>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
@ -35,7 +35,7 @@ export interface ModalEditFormData
|
|||||||
defaultValues?: { [key: string]: string };
|
defaultValues?: { [key: string]: string };
|
||||||
disabledFields?: { [key: string]: boolean } | string[];
|
disabledFields?: { [key: string]: boolean } | string[];
|
||||||
overrideHeading?: string;
|
overrideHeading?: string;
|
||||||
onSubmitCallback?: (values: any) => void;
|
onSubmitCallback?: (values: any, tableName: String) => void;
|
||||||
initialShowModalValue?: boolean;
|
initialShowModalValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,11 +21,12 @@
|
|||||||
|
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import {ReactNode, useEffect, useState} from "react";
|
|
||||||
import Footer from "qqq/components/horseshoe/Footer";
|
import Footer from "qqq/components/horseshoe/Footer";
|
||||||
import NavBar from "qqq/components/horseshoe/NavBar";
|
import NavBar from "qqq/components/horseshoe/NavBar";
|
||||||
|
import {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||||
import DashboardLayout from "qqq/layouts/DashboardLayout";
|
import DashboardLayout from "qqq/layouts/DashboardLayout";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import {ReactNode, useEffect, useState} from "react";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
@ -80,12 +81,34 @@ function BaseLayout({stickyNavbar, children}: Props): JSX.Element
|
|||||||
return () => window.removeEventListener("resize", handleTabsOrientation);
|
return () => window.removeEventListener("resize", handleTabsOrientation);
|
||||||
}, [tabsOrientation]);
|
}, [tabsOrientation]);
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function banner(): JSX.Element | null
|
||||||
|
{
|
||||||
|
const banner = getBanner(metaData?.branding, "QFMD_TOP_OF_BODY");
|
||||||
|
|
||||||
|
if (!banner)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<Box className={getBannerClassName(banner)} sx={{display: "flex", justifyContent: "center", padding: "0.5rem", margin: "-20px", marginBottom: "20px", ...getBannerStyles(banner)}}>
|
||||||
|
{makeBannerContent(banner)}
|
||||||
|
</Box>);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
|
{banner()}
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<Box>{children}</Box>
|
<Box>{children}</Box>
|
||||||
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
|
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
|
|
||||||
export type ValueType = "defaultValue" | "column";
|
export type ValueType = "defaultValue" | "column";
|
||||||
@ -42,6 +43,7 @@ export class BulkLoadField
|
|||||||
wideLayoutIndexPath: number[] = [];
|
wideLayoutIndexPath: number[] = [];
|
||||||
|
|
||||||
error: string = null;
|
error: string = null;
|
||||||
|
warning: string = null;
|
||||||
|
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ export class BulkLoadField
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [])
|
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null)
|
||||||
{
|
{
|
||||||
this.field = field;
|
this.field = field;
|
||||||
this.tableStructure = tableStructure;
|
this.tableStructure = tableStructure;
|
||||||
@ -59,6 +61,8 @@ export class BulkLoadField
|
|||||||
this.defaultValue = defaultValue;
|
this.defaultValue = defaultValue;
|
||||||
this.doValueMapping = doValueMapping;
|
this.doValueMapping = doValueMapping;
|
||||||
this.wideLayoutIndexPath = wideLayoutIndexPath;
|
this.wideLayoutIndexPath = wideLayoutIndexPath;
|
||||||
|
this.error = error;
|
||||||
|
this.warning = warning;
|
||||||
this.key = new Date().getTime().toString();
|
this.key = new Date().getTime().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +72,7 @@ export class BulkLoadField
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
public static clone(source: BulkLoadField): BulkLoadField
|
public static clone(source: BulkLoadField): BulkLoadField
|
||||||
{
|
{
|
||||||
return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath));
|
return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -422,18 +426,23 @@ export class BulkLoadMapping
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
index = 0;
|
///////////////////////////////////////////////
|
||||||
///////////////////////////////////////////////////////////
|
// find the max index for this field already //
|
||||||
// count how many copies of this field there are already //
|
///////////////////////////////////////////////
|
||||||
///////////////////////////////////////////////////////////
|
let maxIndex = -1;
|
||||||
for (let existingField of [...this.requiredFields, ...this.additionalFields])
|
for (let existingField of [...this.requiredFields, ...this.additionalFields])
|
||||||
{
|
{
|
||||||
if (existingField.getQualifiedName() == bulkLoadField.getQualifiedName())
|
if (existingField.getQualifiedName() == bulkLoadField.getQualifiedName())
|
||||||
{
|
{
|
||||||
index++;
|
const thisIndex = existingField.wideLayoutIndexPath[0];
|
||||||
|
if (thisIndex != null && thisIndex != undefined && thisIndex > maxIndex)
|
||||||
|
{
|
||||||
|
maxIndex = thisIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
index = maxIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
const cloneField = BulkLoadField.clone(bulkLoadField);
|
const cloneField = BulkLoadField.clone(bulkLoadField);
|
||||||
cloneField.wideLayoutIndexPath = [index];
|
cloneField.wideLayoutIndexPath = [index];
|
||||||
@ -455,7 +464,7 @@ export class BulkLoadMapping
|
|||||||
const newAdditionalFields: BulkLoadField[] = [];
|
const newAdditionalFields: BulkLoadField[] = [];
|
||||||
for (let bulkLoadField of this.additionalFields)
|
for (let bulkLoadField of this.additionalFields)
|
||||||
{
|
{
|
||||||
if (bulkLoadField.getQualifiedName() != toRemove.getQualifiedName())
|
if (bulkLoadField.getQualifiedNameWithWideSuffix() != toRemove.getQualifiedNameWithWideSuffix())
|
||||||
{
|
{
|
||||||
newAdditionalFields.push(bulkLoadField);
|
newAdditionalFields.push(bulkLoadField);
|
||||||
}
|
}
|
||||||
@ -463,6 +472,171 @@ export class BulkLoadMapping
|
|||||||
|
|
||||||
this.additionalFields = newAdditionalFields;
|
this.additionalFields = newAdditionalFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public switchLayout(newLayout: string): void
|
||||||
|
{
|
||||||
|
const newAdditionalFields: BulkLoadField[] = [];
|
||||||
|
let anyChanges = false;
|
||||||
|
|
||||||
|
if ("WIDE" != newLayout)
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if going to a layout other than WIDE, make sure there aren't any fields with a wideLayoutIndexPath //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const namesWhereOneWideLayoutIndexHasBeenFound: { [name: string]: boolean } = {};
|
||||||
|
for (let existingField of this.additionalFields)
|
||||||
|
{
|
||||||
|
if (existingField.wideLayoutIndexPath.length > 0)
|
||||||
|
{
|
||||||
|
const name = existingField.getQualifiedName();
|
||||||
|
if (namesWhereOneWideLayoutIndexHasBeenFound[name])
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// in this case, we're on like the 2nd or 3rd instance of, say, Line Item: SKU - so - just discard it. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
anyChanges = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// else, this is the 1st instance of, say, Line Item: SKU - so mark that we've found it - and keep this field //
|
||||||
|
// (that is, put it in the new array), but with no index path //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
namesWhereOneWideLayoutIndexHasBeenFound[name] = true;
|
||||||
|
const newField = BulkLoadField.clone(existingField);
|
||||||
|
newField.wideLayoutIndexPath = [];
|
||||||
|
newAdditionalFields.push(newField);
|
||||||
|
anyChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// else, non-wide-path fields, just get added as-is //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
newAdditionalFields.push(existingField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if going to WIDE layout, then any field from a child table needs a wide-layout-index-path //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
for (let existingField of this.additionalFields)
|
||||||
|
{
|
||||||
|
if (existingField.tableStructure.isMain)
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// fields from main table come over as-is //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
newAdditionalFields.push(existingField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// fields from child tables get a wideLayoutIndexPath (and we're assuming just 1 for each) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const newField = BulkLoadField.clone(existingField);
|
||||||
|
newField.wideLayoutIndexPath = [0];
|
||||||
|
newAdditionalFields.push(newField);
|
||||||
|
anyChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyChanges)
|
||||||
|
{
|
||||||
|
this.additionalFields = newAdditionalFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.layout = newLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public getFieldsForColumnIndex(i: number): BulkLoadField[]
|
||||||
|
{
|
||||||
|
const rs: BulkLoadField[] = [];
|
||||||
|
|
||||||
|
for (let field of [...this.requiredFields, ...this.additionalFields])
|
||||||
|
{
|
||||||
|
if (field.valueType == "column" && field.columnIndex == i)
|
||||||
|
{
|
||||||
|
rs.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public handleChangeToHasHeaderRow(newValue: any, fileDescription: FileDescription)
|
||||||
|
{
|
||||||
|
const newRequiredFields: BulkLoadField[] = [];
|
||||||
|
let anyChangesToRequiredFields = false;
|
||||||
|
|
||||||
|
const newAdditionalFields: BulkLoadField[] = [];
|
||||||
|
let anyChangesToAdditionalFields = false;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if we're switching to have header-rows enabled, then make sure that no columns w/ duplicated headers are selected //
|
||||||
|
// strategy to do this: build new lists of both required & additional fields - and track if we had to change any //
|
||||||
|
// column indexes (set to null) - add a warning to them, and only replace the arrays if there were changes. //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (newValue)
|
||||||
|
{
|
||||||
|
for (let field of this.requiredFields)
|
||||||
|
{
|
||||||
|
if (field.valueType == "column" && fileDescription.duplicateHeaderIndexes[field.columnIndex])
|
||||||
|
{
|
||||||
|
const newField = BulkLoadField.clone(field);
|
||||||
|
newField.columnIndex = null;
|
||||||
|
newField.warning = "This field was assigned to a column with a duplicated header"
|
||||||
|
newRequiredFields.push(newField);
|
||||||
|
anyChangesToRequiredFields = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newRequiredFields.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let field of this.additionalFields)
|
||||||
|
{
|
||||||
|
if (field.valueType == "column" && fileDescription.duplicateHeaderIndexes[field.columnIndex])
|
||||||
|
{
|
||||||
|
const newField = BulkLoadField.clone(field);
|
||||||
|
newField.columnIndex = null;
|
||||||
|
newField.warning = "This field was assigned to a column with a duplicated header"
|
||||||
|
newAdditionalFields.push(newField);
|
||||||
|
anyChangesToAdditionalFields = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newAdditionalFields.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyChangesToRequiredFields)
|
||||||
|
{
|
||||||
|
this.requiredFields = newRequiredFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyChangesToAdditionalFields)
|
||||||
|
{
|
||||||
|
this.additionalFields = newAdditionalFields;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -475,6 +649,8 @@ export class FileDescription
|
|||||||
headerLetters: string[];
|
headerLetters: string[];
|
||||||
bodyValuesPreview: string[][];
|
bodyValuesPreview: string[][];
|
||||||
|
|
||||||
|
duplicateHeaderIndexes: boolean[];
|
||||||
|
|
||||||
// todo - just get this from the profile always - it's not part of the file per-se
|
// todo - just get this from the profile always - it's not part of the file per-se
|
||||||
hasHeaderRow: boolean = true;
|
hasHeaderRow: boolean = true;
|
||||||
|
|
||||||
@ -486,6 +662,18 @@ export class FileDescription
|
|||||||
this.headerValues = headerValues;
|
this.headerValues = headerValues;
|
||||||
this.headerLetters = headerLetters;
|
this.headerLetters = headerLetters;
|
||||||
this.bodyValuesPreview = bodyValuesPreview;
|
this.bodyValuesPreview = bodyValuesPreview;
|
||||||
|
|
||||||
|
this.duplicateHeaderIndexes = [];
|
||||||
|
const usedLabels: { [label: string]: boolean } = {};
|
||||||
|
for (let i = 0; i < headerValues.length; i++)
|
||||||
|
{
|
||||||
|
const label = headerValues[i];
|
||||||
|
if (usedLabels[label])
|
||||||
|
{
|
||||||
|
this.duplicateHeaderIndexes[i] = true;
|
||||||
|
}
|
||||||
|
usedLabels[label] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -517,21 +705,85 @@ export class FileDescription
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
public getPreviewValues(columnIndex: number): string[]
|
public getPreviewValues(columnIndex: number, fieldType?: QFieldType): string[]
|
||||||
{
|
{
|
||||||
if (columnIndex == undefined)
|
if (columnIndex == undefined)
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasHeaderRow)
|
function getTypedValue(value: any): string
|
||||||
{
|
{
|
||||||
return (this.bodyValuesPreview[columnIndex]);
|
if (value == null)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
return ([this.headerValues[columnIndex], ...this.bodyValuesPreview[columnIndex]]);
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// this was useful at one point in time when we had an object coming back for xlsx files with many different data types //
|
||||||
|
// we'd see a .string attribute, which would have the value we'd want to show. not using it now, but keep in case //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (value && value.string)
|
||||||
|
{
|
||||||
|
switch (fieldType)
|
||||||
|
{
|
||||||
|
case QFieldType.BOOLEAN:
|
||||||
|
{
|
||||||
|
return value.bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
case QFieldType.STRING:
|
||||||
|
case QFieldType.TEXT:
|
||||||
|
case QFieldType.HTML:
|
||||||
|
case QFieldType.PASSWORD:
|
||||||
|
{
|
||||||
|
return value.string;
|
||||||
|
}
|
||||||
|
|
||||||
|
case QFieldType.INTEGER:
|
||||||
|
case QFieldType.LONG:
|
||||||
|
{
|
||||||
|
return value.integer;
|
||||||
|
}
|
||||||
|
case QFieldType.DECIMAL:
|
||||||
|
{
|
||||||
|
return value.decimal;
|
||||||
|
}
|
||||||
|
case QFieldType.DATE:
|
||||||
|
{
|
||||||
|
return value.date;
|
||||||
|
}
|
||||||
|
case QFieldType.TIME:
|
||||||
|
{
|
||||||
|
return value.time;
|
||||||
|
}
|
||||||
|
case QFieldType.DATE_TIME:
|
||||||
|
{
|
||||||
|
return value.dateTime;
|
||||||
|
}
|
||||||
|
case QFieldType.BLOB:
|
||||||
|
return ""; // !!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (`${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueArray: string[] = [];
|
||||||
|
|
||||||
|
if (!this.hasHeaderRow)
|
||||||
|
{
|
||||||
|
const typedValue = getTypedValue(this.headerValues[columnIndex]);
|
||||||
|
valueArray.push(typedValue == null ? "" : `${typedValue}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let value of this.bodyValuesPreview[columnIndex])
|
||||||
|
{
|
||||||
|
const typedValue = getTypedValue(value);
|
||||||
|
valueArray.push(typedValue == null ? "" : `${typedValue}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (valueArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1032,9 +1032,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
<BulkLoadFileMappingForm
|
<BulkLoadFileMappingForm
|
||||||
processValues={processValues}
|
processValues={processValues}
|
||||||
tableMetaData={tableMetaData}
|
tableMetaData={tableMetaData}
|
||||||
|
processMetaData={processMetaData}
|
||||||
metaData={qInstance}
|
metaData={qInstance}
|
||||||
ref={bulkLoadFileMappingFormRef}
|
ref={bulkLoadFileMappingFormRef}
|
||||||
setActiveStepLabel={setActiveStepLabel}
|
setActiveStepLabel={setActiveStepLabel}
|
||||||
|
frontendStep={activeStep}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1836,6 +1838,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(urlSearchParams.get("defaultProcessValues"))
|
||||||
|
{
|
||||||
|
if(!defaultProcessValues)
|
||||||
|
{
|
||||||
|
defaultProcessValues = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = JSON.parse(urlSearchParams.get("defaultProcessValues"));
|
||||||
|
for (let key in values)
|
||||||
|
{
|
||||||
|
defaultProcessValues[key] = values[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (defaultProcessValues)
|
if (defaultProcessValues)
|
||||||
{
|
{
|
||||||
for (let key in defaultProcessValues)
|
for (let key in defaultProcessValues)
|
||||||
@ -2220,7 +2236,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
if (isModal)
|
if (isModal)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
|
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}} id="modalProcessScrollContainer">
|
||||||
{body}
|
{body}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -1103,7 +1103,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// make the rows for the grid //
|
// make the rows for the grid //
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
const rows = DataGridUtils.makeRows(results, tableMetaData);
|
const rows = DataGridUtils.makeRows(results, tableMetaData, tableVariant);
|
||||||
setRows(rows);
|
setRows(rows);
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -1612,6 +1612,22 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const processClicked = (process: QProcessMetaData) =>
|
const processClicked = (process: QProcessMetaData) =>
|
||||||
{
|
{
|
||||||
|
if (process.minInputRecords != null && process.minInputRecords > 0 && getNoOfSelectedRecords() === 0)
|
||||||
|
{
|
||||||
|
setAlertContent(`No records were selected for the process: ${process.label}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (process.minInputRecords != null && getNoOfSelectedRecords() < process.minInputRecords)
|
||||||
|
{
|
||||||
|
setAlertContent(`Too few records were selected for the process: ${process.label}. A minimum of ${process.minInputRecords} is required.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (process.maxInputRecords != null && getNoOfSelectedRecords() > process.maxInputRecords)
|
||||||
|
{
|
||||||
|
setAlertContent(`Too many records were selected for the process: ${process.label}. A maximum of ${process.maxInputRecords} is allowed.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// todo - let the process specify that it needs initial rows - err if none selected.
|
// todo - let the process specify that it needs initial rows - err if none selected.
|
||||||
// alternatively, let a process itself have an initial screen to select rows...
|
// alternatively, let a process itself have an initial screen to select rows...
|
||||||
openModalProcess(process);
|
openModalProcess(process);
|
||||||
|
@ -191,7 +191,7 @@ function RecordDeveloperView({table}: Props): JSX.Element
|
|||||||
<Card sx={{mb: 3}}>
|
<Card sx={{mb: 3}}>
|
||||||
<Typography variant="h6" p={2} pl={3} pb={3}>{field?.label}</Typography>
|
<Typography variant="h6" p={2} pl={3} pb={3}>{field?.label}</Typography>
|
||||||
|
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2}>
|
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} mx={3} mb={3} mt={0}>
|
||||||
{scriptId ?
|
{scriptId ?
|
||||||
<ScriptViewer
|
<ScriptViewer
|
||||||
scriptId={scriptId}
|
scriptId={scriptId}
|
||||||
|
@ -92,9 +92,9 @@ const TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT = "qqq.tableVariant";
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: {label?: SxProps, value?: SxProps})
|
export function renderSectionOfFields(key: string, fieldNames: string[], tableMetaData: QTableMetaData, helpHelpActive: boolean, record: QRecord, fieldMap?: { [name: string]: QFieldMetaData }, styleOverrides?: {label?: SxProps, value?: SxProps}, tableVariant?: QTableVariant)
|
||||||
{
|
{
|
||||||
return <Box key={key} display="flex" flexDirection="column" py={1} pr={2}>
|
return <Grid container lg={12} key={key} display="flex" py={1} pr={2}>
|
||||||
{
|
{
|
||||||
fieldNames.map((fieldName: string) =>
|
fieldNames.map((fieldName: string) =>
|
||||||
{
|
{
|
||||||
@ -103,6 +103,7 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
|
|||||||
if (field != null)
|
if (field != null)
|
||||||
{
|
{
|
||||||
let label = field.label;
|
let label = field.label;
|
||||||
|
let gridColumns = (field.gridColumns && field.gridColumns > 0) ? field.gridColumns : 12;
|
||||||
|
|
||||||
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
|
const helpRoles = ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
|
||||||
const showHelp = helpHelpActive || hasHelpContent(field.helpContents, helpRoles);
|
const showHelp = helpHelpActive || hasHelpContent(field.helpContents, helpRoles);
|
||||||
@ -111,22 +112,22 @@ export function renderSectionOfFields(key: string, fieldNames: string[], tableMe
|
|||||||
const labelElement = <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)" sx={{cursor: "default", ...(styleOverrides?.label ?? {})}}>{label}:</Typography>;
|
const labelElement = <Typography variant="button" textTransform="none" fontWeight="bold" pr={1} color="rgb(52, 71, 103)" sx={{cursor: "default", ...(styleOverrides?.label ?? {})}}>{label}:</Typography>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box key={fieldName} flexDirection="row" pr={2}>
|
<Grid item key={fieldName} lg={gridColumns} flexDirection="column" pr={2}>
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
showHelp && formattedHelpContent ? <Tooltip title={formattedHelpContent}>{labelElement}</Tooltip> : labelElement
|
showHelp && formattedHelpContent ? <Tooltip title={formattedHelpContent}>{labelElement}</Tooltip> : labelElement
|
||||||
}
|
}
|
||||||
<div style={{display: "inline-block", width: 0}}> </div>
|
<div style={{display: "inline-block", width: 0}}> </div>
|
||||||
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)" sx={{...(styleOverrides?.value ?? {})}}>
|
<Typography variant="button" textTransform="none" fontWeight="regular" color="rgb(123, 128, 154)" sx={{...(styleOverrides?.value ?? {})}}>
|
||||||
{ValueUtils.getDisplayValue(field, record, "view", fieldName)}
|
{ValueUtils.getDisplayValue(field, record, "view", fieldName, tableVariant)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</>
|
</>
|
||||||
</Box>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</Box>;
|
</Grid>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -597,7 +598,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
|
|||||||
// for a section with field names, render the field values. //
|
// for a section with field names, render the field values. //
|
||||||
// for the T1 section, the "wrapper" will come out below - but for other sections, produce a wrapper too. //
|
// for the T1 section, the "wrapper" will come out below - but for other sections, produce a wrapper too. //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record);
|
const fields = renderSectionOfFields(section.name, section.fieldNames, tableMetaData, helpHelpActive, record, undefined, undefined, tableVariant);
|
||||||
|
|
||||||
if (section.tier === "T1")
|
if (section.tier === "T1")
|
||||||
{
|
{
|
||||||
|
@ -748,35 +748,54 @@ input[type="search"]::-webkit-search-results-decoration
|
|||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpContentAlert.success
|
.helpContentAlert.info,
|
||||||
|
.banner.info
|
||||||
|
{
|
||||||
|
background-color: rgb(234, 242, 255);
|
||||||
|
color: rgb(20, 51, 102);
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert.info .MuiAlert-icon .material-icons-round,
|
||||||
|
.banner.info .MuiAlert-icon .material-icons-round
|
||||||
|
{
|
||||||
|
color: #0062FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpContentAlert.success,
|
||||||
|
.banner.success
|
||||||
{
|
{
|
||||||
background-color: rgb(240, 248, 241);
|
background-color: rgb(240, 248, 241);
|
||||||
color: rgb(44, 76, 46);
|
color: rgb(44, 76, 46);
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpContentAlert.success .MuiAlert-icon .material-icons-round
|
.helpContentAlert.success .MuiAlert-icon .material-icons-round,
|
||||||
|
.banner.success .MuiAlert-icon .material-icons-round
|
||||||
{
|
{
|
||||||
color: #4CAF50;
|
color: #4CAF50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpContentAlert.warning
|
.helpContentAlert.warning,
|
||||||
|
.banner.warning
|
||||||
{
|
{
|
||||||
background-color: rgb(254, 245, 234);
|
background-color: rgb(254, 245, 234);
|
||||||
color: rgb(100, 65, 20);
|
color: rgb(100, 65, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpContentAlert.warning .MuiAlert-icon .material-icons-round
|
.helpContentAlert.warning .MuiAlert-icon .material-icons-round,
|
||||||
|
.banner.warning .MuiAlert-icon .material-icons-round
|
||||||
{
|
{
|
||||||
color: #fb8c00;
|
color: #fb8c00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpContentAlert.error
|
.helpContentAlert.error,
|
||||||
|
.banner.error
|
||||||
{
|
{
|
||||||
background-color: rgb(254, 239, 238);
|
background-color: rgb(254, 239, 238);
|
||||||
color: rgb(98, 41, 37);
|
color: rgb(98, 41, 37);
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpContentAlert.error .MuiAlert-icon .material-icons-round
|
.helpContentAlert.error .MuiAlert-icon .material-icons-round,
|
||||||
|
.banner.error .MuiAlert-icon .material-icons-round
|
||||||
{
|
{
|
||||||
color: #F44335;
|
color: #F44335;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QF
|
|||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
|
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
import {GridColDef, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
|
import {GridColDef, GridRowsProp, MuiEvent} from "@mui/x-data-grid-pro";
|
||||||
@ -70,7 +71,7 @@ export default class DataGridUtils
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, allowEmptyId = false): GridRowsProp[] =>
|
public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData, tableVariant?: QTableVariant, allowEmptyId = false): GridRowsProp[] =>
|
||||||
{
|
{
|
||||||
const fields = [...tableMetaData.fields.values()];
|
const fields = [...tableMetaData.fields.values()];
|
||||||
const rows = [] as any[];
|
const rows = [] as any[];
|
||||||
@ -82,7 +83,7 @@ export default class DataGridUtils
|
|||||||
|
|
||||||
fields.forEach((field) =>
|
fields.forEach((field) =>
|
||||||
{
|
{
|
||||||
row[field.name] = ValueUtils.getDisplayValue(field, record, "query");
|
row[field.name] = ValueUtils.getDisplayValue(field, record, "query", undefined, tableVariant);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tableMetaData.exposedJoins)
|
if (tableMetaData.exposedJoins)
|
||||||
@ -97,7 +98,7 @@ export default class DataGridUtils
|
|||||||
fields.forEach((field) =>
|
fields.forEach((field) =>
|
||||||
{
|
{
|
||||||
let fieldName = join.joinTable.name + "." + field.name;
|
let fieldName = join.joinTable.name + "." + field.name;
|
||||||
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName);
|
row[fieldName] = ValueUtils.getDisplayValue(field, record, "query", fieldName, tableVariant);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ export default class GoogleAnalyticsUtils
|
|||||||
console.log("Error reading session values from localStorage: " + e);
|
console.log("Error reading session values from localStorage: " + e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.metaData.environmentValues.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues.get("GOOGLE_ANALYTICS_TRACKING_ID"))
|
if (this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_ENABLED") == "true" && this.metaData.environmentValues?.get("GOOGLE_ANALYTICS_TRACKING_ID"))
|
||||||
{
|
{
|
||||||
this.active = true;
|
this.active = true;
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ class FilterUtils
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values, undefined, "filter");
|
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values, undefined, undefined, "filter");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export class SavedBulkLoadProfileUtils
|
|||||||
|
|
||||||
for (let bulkLoadField of orderedFieldArray)
|
for (let bulkLoadField of orderedFieldArray)
|
||||||
{
|
{
|
||||||
const fieldName = bulkLoadField.field.name;
|
const fieldName = bulkLoadField.getQualifiedName()
|
||||||
const compareField = compareFieldsMap[fieldName];
|
const compareField = compareFieldsMap[fieldName];
|
||||||
const baseField = baseFieldsMap[fieldName];
|
const baseField = baseFieldsMap[fieldName];
|
||||||
if(!compareField)
|
if(!compareField)
|
||||||
@ -55,12 +55,13 @@ export class SavedBulkLoadProfileUtils
|
|||||||
if (compareField.valueType == "column")
|
if (compareField.valueType == "column")
|
||||||
{
|
{
|
||||||
const column = fileDescription.getColumnNames()[compareField.columnIndex];
|
const column = fileDescription.getColumnNames()[compareField.columnIndex];
|
||||||
rs.push(`Changed ${compareField.getQualifiedLabel()} from using a default value (${baseField.defaultValue}) to using a file column (${column})`);
|
rs.push(`Changed ${compareField.getQualifiedLabel()} from using a default value (${baseField.defaultValue}) to using a file column ${column ? `(${column})` : ""}`);
|
||||||
}
|
}
|
||||||
else if (compareField.valueType == "defaultValue")
|
else if (compareField.valueType == "defaultValue")
|
||||||
{
|
{
|
||||||
const column = fileDescription.getColumnNames()[baseField.columnIndex];
|
const column = fileDescription.getColumnNames()[baseField.columnIndex];
|
||||||
rs.push(`Changed ${compareField.getQualifiedLabel()} from using a file column (${column}) to using a default value (${compareField.defaultValue})`);
|
const value = compareField.defaultValue;
|
||||||
|
rs.push(`Changed ${compareField.getQualifiedLabel()} from using a file column (${column}) to using a default value ${value === undefined ? "" : `(${value})`}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (baseField.valueType == compareField.valueType && baseField.valueType == "defaultValue")
|
else if (baseField.valueType == compareField.valueType && baseField.valueType == "defaultValue")
|
||||||
@ -70,7 +71,8 @@ export class SavedBulkLoadProfileUtils
|
|||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
if (baseField.defaultValue != compareField.defaultValue)
|
if (baseField.defaultValue != compareField.defaultValue)
|
||||||
{
|
{
|
||||||
rs.push(`Changed ${compareField.getQualifiedLabel()} default value from (${baseField.defaultValue}) to (${compareField.defaultValue})`);
|
const value = compareField.defaultValue;
|
||||||
|
rs.push(`Changed ${compareField.getQualifiedLabel()} default value from (${baseField.defaultValue}) to ${value === undefined ? "" : `(${value})`}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (baseField.valueType == compareField.valueType && baseField.valueType == "column")
|
else if (baseField.valueType == compareField.valueType && baseField.valueType == "column")
|
||||||
@ -78,23 +80,27 @@ export class SavedBulkLoadProfileUtils
|
|||||||
///////////////////////////////////////////
|
///////////////////////////////////////////
|
||||||
// if we changed the column, report that //
|
// if we changed the column, report that //
|
||||||
///////////////////////////////////////////
|
///////////////////////////////////////////
|
||||||
|
let isDiff = false;
|
||||||
if (fileDescription.hasHeaderRow)
|
if (fileDescription.hasHeaderRow)
|
||||||
{
|
{
|
||||||
if (baseField.headerName != compareField.headerName)
|
if (baseField.headerName != compareField.headerName)
|
||||||
{
|
{
|
||||||
const baseColumn = fileDescription.getColumnNames()[baseField.columnIndex];
|
isDiff = true;
|
||||||
const compareColumn = fileDescription.getColumnNames()[compareField.columnIndex];
|
|
||||||
rs.push(`Changed ${compareField.getQualifiedLabel()} file column from (${baseColumn}) to (${compareColumn})`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (baseField.columnIndex != compareField.columnIndex)
|
if (baseField.columnIndex != compareField.columnIndex)
|
||||||
|
{
|
||||||
|
isDiff = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isDiff)
|
||||||
{
|
{
|
||||||
const baseColumn = fileDescription.getColumnNames()[baseField.columnIndex];
|
const baseColumn = fileDescription.getColumnNames()[baseField.columnIndex];
|
||||||
const compareColumn = fileDescription.getColumnNames()[compareField.columnIndex];
|
const compareColumn = fileDescription.getColumnNames()[compareField.columnIndex];
|
||||||
rs.push(`Changed ${compareField.getQualifiedLabel()} file column from (${baseColumn}) to (${compareColumn})`);
|
rs.push(`Changed ${compareField.getQualifiedLabel()} file column from ${baseColumn ? `(${baseColumn})` : "--"} to ${compareColumn ? `(${compareColumn})` : "--"}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -120,7 +126,7 @@ export class SavedBulkLoadProfileUtils
|
|||||||
|
|
||||||
for (let bulkLoadField of orderedFieldArray)
|
for (let bulkLoadField of orderedFieldArray)
|
||||||
{
|
{
|
||||||
const fieldName = bulkLoadField.field.name;
|
const fieldName = bulkLoadField.getQualifiedName()
|
||||||
const compareField = compareFieldsMap[fieldName];
|
const compareField = compareFieldsMap[fieldName];
|
||||||
if(!compareField)
|
if(!compareField)
|
||||||
{
|
{
|
||||||
@ -292,7 +298,7 @@ export class SavedBulkLoadProfileUtils
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const fieldName = bulkLoadField.field.name;
|
const fieldName = bulkLoadField.getQualifiedName() // todo - does this (and the others calls to this) need suffix?
|
||||||
|
|
||||||
const valueMappingDiff = this.diffFieldValueMappings(bulkLoadField, baseMapping.valueMappings[fieldName] ?? {}, activeMapping.valueMappings[fieldName] ?? {});
|
const valueMappingDiff = this.diffFieldValueMappings(bulkLoadField, baseMapping.valueMappings[fieldName] ?? {}, activeMapping.valueMappings[fieldName] ?? {});
|
||||||
if(valueMappingDiff)
|
if(valueMappingDiff)
|
||||||
|
@ -23,6 +23,7 @@ import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Ado
|
|||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
|
||||||
|
import {QTableVariant} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableVariant";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import "datejs"; // https://github.com/datejs/Datejs
|
import "datejs"; // https://github.com/datejs/Datejs
|
||||||
import {Chip, ClickAwayListener, Icon} from "@mui/material";
|
import {Chip, ClickAwayListener, Icon} from "@mui/material";
|
||||||
@ -76,14 +77,14 @@ class ValueUtils
|
|||||||
** When you have a field, and a record - call this method to get a string or
|
** When you have a field, and a record - call this method to get a string or
|
||||||
** element back to display the field's value.
|
** element back to display the field's value.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string): string | JSX.Element | JSX.Element[]
|
public static getDisplayValue(field: QFieldMetaData, record: QRecord, usage: "view" | "query" = "view", overrideFieldName?: string, tableVariant?: QTableVariant): string | JSX.Element | JSX.Element[]
|
||||||
{
|
{
|
||||||
const fieldName = overrideFieldName ?? field.name;
|
const fieldName = overrideFieldName ?? field.name;
|
||||||
|
|
||||||
const displayValue = record.displayValues ? record.displayValues.get(fieldName) : undefined;
|
const displayValue = record.displayValues ? record.displayValues.get(fieldName) : undefined;
|
||||||
const rawValue = record.values ? record.values.get(fieldName) : undefined;
|
const rawValue = record.values ? record.values.get(fieldName) : undefined;
|
||||||
|
|
||||||
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage);
|
return ValueUtils.getValueForDisplay(field, rawValue, displayValue, usage, tableVariant, record, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -91,14 +92,35 @@ class ValueUtils
|
|||||||
** When you have a field and a value (either just a raw value, or a raw and
|
** When you have a field and a value (either just a raw value, or a raw and
|
||||||
** display value), call this method to get a string Element to display.
|
** display value), call this method to get a string Element to display.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view"): string | JSX.Element | JSX.Element[]
|
public static getValueForDisplay(field: QFieldMetaData, rawValue: any, displayValue: any = rawValue, usage: "view" | "query" = "view", tableVariant?: QTableVariant, record?: QRecord, fieldName?: string): string | JSX.Element | JSX.Element[]
|
||||||
{
|
{
|
||||||
if (field.hasAdornment(AdornmentType.LINK))
|
if (field.hasAdornment(AdornmentType.LINK))
|
||||||
{
|
{
|
||||||
const adornment = field.getAdornment(AdornmentType.LINK);
|
const adornment = field.getAdornment(AdornmentType.LINK);
|
||||||
let href = rawValue;
|
let href = String(rawValue);
|
||||||
|
|
||||||
|
let toRecordFromTable = adornment.getValue("toRecordFromTable");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the link adornment has a 'toRecordFromTableDynamic', then look for a display //
|
||||||
|
// value named `fieldName`:toRecordFromTableDynamic for the table name. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(adornment.getValue("toRecordFromTableDynamic"))
|
||||||
|
{
|
||||||
|
const toRecordFromTableDynamic = record?.displayValues?.get(fieldName + ":toRecordFromTableDynamic");
|
||||||
|
if(toRecordFromTableDynamic)
|
||||||
|
{
|
||||||
|
toRecordFromTable = toRecordFromTableDynamic;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
// if the table name isn't known, then return w/o the adornment. //
|
||||||
|
///////////////////////////////////////////////////////////////////
|
||||||
|
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toRecordFromTable = adornment.getValue("toRecordFromTable");
|
|
||||||
if (toRecordFromTable)
|
if (toRecordFromTable)
|
||||||
{
|
{
|
||||||
if (ValueUtils.getQInstance())
|
if (ValueUtils.getQInstance())
|
||||||
@ -107,7 +129,7 @@ class ValueUtils
|
|||||||
if (!tablePath)
|
if (!tablePath)
|
||||||
{
|
{
|
||||||
console.log("Couldn't find path for table: " + toRecordFromTable);
|
console.log("Couldn't find path for table: " + toRecordFromTable);
|
||||||
return (displayValue ?? rawValue);
|
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tablePath.endsWith("/"))
|
if (!tablePath.endsWith("/"))
|
||||||
@ -199,12 +221,44 @@ class ValueUtils
|
|||||||
|
|
||||||
if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
|
if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
|
||||||
{
|
{
|
||||||
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
|
let url = rawValue;
|
||||||
|
if(tableVariant)
|
||||||
|
{
|
||||||
|
url += "?tableVariant=" + encodeURIComponent(JSON.stringify(tableVariant));
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the field has the download adornment with a downloadUrlDynamic value, //
|
||||||
|
// then get the url from a displayValue of `fieldName`:downloadUrlDynamic. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
|
||||||
|
{
|
||||||
|
const adornment = field.getAdornment(AdornmentType.FILE_DOWNLOAD);
|
||||||
|
let downloadUrlDynamicAdornmentValue = adornment.getValue("downloadUrlDynamic");
|
||||||
|
if(downloadUrlDynamicAdornmentValue)
|
||||||
|
{
|
||||||
|
const downloadUrlDynamicValue = record?.displayValues?.get(fieldName + ":downloadUrlDynamic");
|
||||||
|
if (downloadUrlDynamicValue)
|
||||||
|
{
|
||||||
|
url = downloadUrlDynamicValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// if the url isn't available, then return w/o the adornment. //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<BlobComponent field={field} url={url} filename={displayValue} usage={usage} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
return (ValueUtils.getUnadornedValueForDisplay(field, rawValue, displayValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** After we know there's no element to be returned (e.g., because no adornment),
|
** After we know there's no element to be returned (e.g., because no adornment),
|
||||||
** this method does the string formatting.
|
** this method does the string formatting.
|
||||||
@ -213,7 +267,13 @@ class ValueUtils
|
|||||||
{
|
{
|
||||||
if (!displayValue && field.defaultValue)
|
if (!displayValue && field.defaultValue)
|
||||||
{
|
{
|
||||||
displayValue = field.defaultValue;
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// note, at one point in time, we used a field's default value here if no displayValue... but that feels 100% wrong, //
|
||||||
|
// e.g., a null field would show up (on a query or view screen) has having some value! //
|
||||||
|
// not sure if this was maybe supposed to be displayValue = rawValue, but, keep that in mind, and keep this block here //
|
||||||
|
// in case we run into issues and need to revisit/rethink //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// displayValue = field.defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === QFieldType.DATE_TIME)
|
if (field.type === QFieldType.DATE_TIME)
|
||||||
|
Reference in New Issue
Block a user