mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
29 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
7c7b9a3dbf | |||
1dce760934 | |||
ff4683af1f | |||
ab4be1d5af | |||
0d7e76df6c | |||
d41f5f8339 | |||
4d30eb3060 | |||
d4a675e952 | |||
633c97b710 | |||
c70ef3dae8 | |||
5c69ae666c | |||
2e5aba6c16 | |||
185775ca4d | |||
cbcb3b505e | |||
ce91f68088 | |||
81da1a4627 | |||
b279a04b43 | |||
1f2e57d688 | |||
52bb7ba411 | |||
34c6f650b5 | |||
d792c23035 | |||
e3d30633f1 | |||
a6ee682671 | |||
c62252075f | |||
debc6f3ebf | |||
679375ba63 | |||
fb10dad803 | |||
c9a618c7f6 | |||
13ce684d23 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@
|
|||||||
.yalc*
|
.yalc*
|
||||||
yalc.lock
|
yalc.lock
|
||||||
.env
|
.env
|
||||||
|
/certs
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
23618
package-lock.json
generated
23618
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@auth0/auth0-react": "1.10.2",
|
||||||
"@emotion/react": "11.7.1",
|
"@emotion/react": "11.7.1",
|
||||||
"@emotion/styled": "11.6.0",
|
"@emotion/styled": "11.6.0",
|
||||||
"@kingsrook/qqq-frontend-core": "1.0.118",
|
"@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",
|
||||||
@ -35,9 +35,9 @@
|
|||||||
"html-react-parser": "1.4.8",
|
"html-react-parser": "1.4.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",
|
||||||
|
"lodash": "4.17.21",
|
||||||
"jwt-decode": "3.1.2",
|
"jwt-decode": "3.1.2",
|
||||||
"oidc-client-ts": "2.4.1",
|
"oidc-client-ts": "2.4.1",
|
||||||
"react-oidc-context": "2.3.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",
|
||||||
@ -51,6 +51,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",
|
||||||
|
4
pom.xml
4
pom.xml
@ -29,7 +29,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.25.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.25.0-integration-sprint-62-20250307-205536</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
|
42
src/App.tsx
42
src/App.tsx
@ -28,6 +28,7 @@ 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";
|
||||||
@ -39,6 +40,7 @@ import useAuth0AuthenticationModule from "qqq/authorization/auth0/useAuth0Authen
|
|||||||
import useOAuth2AuthenticationModule from "qqq/authorization/oauth2/useOAuth2AuthenticationModule";
|
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 {getBannerClassName, getBannerStyles, getBanner, makeBannerContent} from "qqq/components/misc/Banners";
|
||||||
import {setMiniSidenav, useMaterialUIController} from "qqq/context";
|
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";
|
||||||
@ -63,9 +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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,11 +81,10 @@ 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 [authenticationMetaData, setAuthenticationMetaData] = useState(null as QAuthenticationMetaData | null);
|
|
||||||
const [earlyReturnForAuth, setEarlyReturnForAuth] = useState(null as JSX.Element);
|
const [earlyReturnForAuth, setEarlyReturnForAuth] = useState(null as JSX.Element);
|
||||||
|
|
||||||
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
const {setupSession: auth0SetupSession, logout: auth0Logout} = useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||||
const {setupSession: oauth2SetupSession, logout: oauth2Logout} = useOAuth2AuthenticationModule({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});
|
const {setupSession: anonymousSetupSession, logout: anonymousLogout} = useAnonymousAuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth});
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////
|
||||||
@ -99,9 +105,6 @@ export default function App()
|
|||||||
|
|
||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
const authenticationMetaData: QAuthenticationMetaData = await qController.getAuthenticationMetaData();
|
|
||||||
setAuthenticationMetaData(authenticationMetaData);
|
|
||||||
|
|
||||||
if (authenticationMetaData.type === "AUTH_0")
|
if (authenticationMetaData.type === "AUTH_0")
|
||||||
{
|
{
|
||||||
await auth0SetupSession();
|
await auth0SetupSession();
|
||||||
@ -134,7 +137,7 @@ export default function App()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** call approprite logout function based on authentication meta data type
|
** call appropriate logout function based on authentication meta data type
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function doLogout()
|
function doLogout()
|
||||||
{
|
{
|
||||||
@ -157,7 +160,7 @@ export default function App()
|
|||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
@ -450,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",
|
||||||
@ -630,6 +632,23 @@ export default function App()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
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 (
|
||||||
|
|
||||||
appRoutes && (
|
appRoutes && (
|
||||||
@ -657,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}
|
||||||
|
@ -52,7 +52,7 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
|
|||||||
function Auth0RouterBody()
|
function Auth0RouterBody()
|
||||||
{
|
{
|
||||||
const {renderAppWrapper} = useAuth0AuthenticationModule({});
|
const {renderAppWrapper} = useAuth0AuthenticationModule({});
|
||||||
return (renderAppWrapper(authenticationMetaData, null));
|
return (renderAppWrapper(authenticationMetaData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -61,10 +61,10 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function OAuth2RouterBody()
|
function OAuth2RouterBody()
|
||||||
{
|
{
|
||||||
const {renderAppWrapper} = useOAuth2AuthenticationModule({});
|
const {renderAppWrapper} = useOAuth2AuthenticationModule({inOAuthContext: false});
|
||||||
return (renderAppWrapper(authenticationMetaData, (
|
return (renderAppWrapper(authenticationMetaData, (
|
||||||
<MaterialUIControllerProvider>
|
<MaterialUIControllerProvider>
|
||||||
<App />
|
<App authenticationMetaData={authenticationMetaData} />
|
||||||
</MaterialUIControllerProvider>
|
</MaterialUIControllerProvider>
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
|
|||||||
const {renderAppWrapper} = useAnonymousAuthenticationModule({});
|
const {renderAppWrapper} = useAnonymousAuthenticationModule({});
|
||||||
return (renderAppWrapper(authenticationMetaData, (
|
return (renderAppWrapper(authenticationMetaData, (
|
||||||
<MaterialUIControllerProvider>
|
<MaterialUIControllerProvider>
|
||||||
<App />
|
<App authenticationMetaData={authenticationMetaData} />
|
||||||
</MaterialUIControllerProvider>
|
</MaterialUIControllerProvider>
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.frontend.materialdashboard.model.metadata;
|
||||||
|
|
||||||
|
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.branding.BannerSlot;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public enum MaterialDashboardBannerSlots implements BannerSlot
|
||||||
|
{
|
||||||
|
QFMD_TOP_OF_SITE,
|
||||||
|
QFMD_TOP_OF_BODY,
|
||||||
|
QFMD_SIDE_NAV_UNDER_LOGO
|
||||||
|
}
|
@ -41,11 +41,11 @@ interface Props
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** hook for working with the Auth0 authentication module
|
** hook for working with the Auth0 authentication module
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
|
export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser}: Props)
|
||||||
{
|
{
|
||||||
const {user: auth0User, getAccessTokenSilently: auth0GetAccessTokenSilently, logout: useAuth0Logout} = useAuth0();
|
const {user: auth0User, getAccessTokenSilently: auth0GetAccessTokenSilently, logout: useAuth0Logout} = useAuth0();
|
||||||
|
|
||||||
const [cookies, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
const [cookies, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
@ -119,12 +119,7 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
|
|||||||
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
if (shouldStoreNewToken(accessToken, lsAccessToken))
|
||||||
{
|
{
|
||||||
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
console.log("Sending accessToken to backend, requesting a sessionUUID...");
|
||||||
const {uuid: newSessionUuid, values} = await qController.manageSession(accessToken, null);
|
const {uuid: 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("accessToken", accessToken);
|
||||||
localStorage.setItem("sessionValues", JSON.stringify(values));
|
localStorage.setItem("sessionValues", JSON.stringify(values));
|
||||||
@ -199,7 +194,7 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData, children: JSX.Element): JSX.Element =>
|
const renderAppWrapper = (authenticationMetaData: QAuthenticationMetaData): JSX.Element =>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let domain: string = authenticationMetaData.data.baseUrl;
|
let domain: string = authenticationMetaData.data.baseUrl;
|
||||||
@ -225,6 +220,15 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
|
|||||||
domain = domain.replace(/\/$/, "");
|
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 (
|
return (
|
||||||
<Auth0ProviderWithRedirectCallback
|
<Auth0ProviderWithRedirectCallback
|
||||||
domain={domain}
|
domain={domain}
|
||||||
@ -232,7 +236,7 @@ export default function useAuth0AuthenticationModule({setIsFullyAuthenticated, s
|
|||||||
audience={audience}
|
audience={audience}
|
||||||
redirectUri={`${window.location.origin}/`}>
|
redirectUri={`${window.location.origin}/`}>
|
||||||
<MaterialUIControllerProvider>
|
<MaterialUIControllerProvider>
|
||||||
<ProtectedRoute component={App} />
|
<ProtectedRoute component={WrappedApp} />
|
||||||
</MaterialUIControllerProvider>
|
</MaterialUIControllerProvider>
|
||||||
</Auth0ProviderWithRedirectCallback>
|
</Auth0ProviderWithRedirectCallback>
|
||||||
);
|
);
|
||||||
|
@ -23,7 +23,7 @@ import {QAuthenticationMetaData} from "@kingsrook/qqq-frontend-core/lib/model/me
|
|||||||
import {SESSION_UUID_COOKIE_NAME} from "App";
|
import {SESSION_UUID_COOKIE_NAME} from "App";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import {useCookies} from "react-cookie";
|
import {useCookies} from "react-cookie";
|
||||||
import {AuthProvider, useAuth} from "react-oidc-context";
|
import {AuthContextProps, AuthProvider, useAuth} from "react-oidc-context";
|
||||||
import {useNavigate, useSearchParams} from "react-router-dom";
|
import {useNavigate, useSearchParams} from "react-router-dom";
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
@ -33,16 +33,22 @@ interface Props
|
|||||||
setIsFullyAuthenticated?: (is: boolean) => void;
|
setIsFullyAuthenticated?: (is: boolean) => void;
|
||||||
setLoggedInUser?: (user: any) => void;
|
setLoggedInUser?: (user: any) => void;
|
||||||
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
setEarlyReturnForAuth?: (element: JSX.Element | null) => void;
|
||||||
|
inOAuthContext: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** hook for working with the OAuth2 authentication module
|
** hook for working with the OAuth2 authentication module
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth}: Props)
|
export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated, setLoggedInUser, setEarlyReturnForAuth, inOAuthContext}: Props)
|
||||||
{
|
{
|
||||||
const authOidc = useAuth();
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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, setCookie, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
const [cookies, removeCookie] = useCookies([SESSION_UUID_COOKIE_NAME]);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -56,6 +62,12 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
|
|||||||
const preSigninRedirectPathnameKey = "oauth2.preSigninRedirect.pathname";
|
const preSigninRedirectPathnameKey = "oauth2.preSigninRedirect.pathname";
|
||||||
if (window.location.pathname == "/token")
|
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 code = searchParams.get("code");
|
||||||
const state = searchParams.get("state");
|
const state = searchParams.get("state");
|
||||||
const oidcString = localStorage.getItem(`oidc.${state}`);
|
const oidcString = localStorage.getItem(`oidc.${state}`);
|
||||||
@ -77,14 +89,24 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
|
|||||||
localStorage.removeItem(preSigninRedirectPathname);
|
localStorage.removeItem(preSigninRedirectPathname);
|
||||||
navigate(preSigninRedirectPathname ?? "/", {replace: true});
|
navigate(preSigninRedirectPathname ?? "/", {replace: true});
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// if unrecognized state, render an error //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
setEarlyReturnForAuth(<div>Login error: Unrecognized state. Refresh to try again.</div>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// if we have a sessionUUID cookie, try to validate it with the backend //
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
const sessionUuid = cookies[SESSION_UUID_COOKIE_NAME];
|
const sessionUuid = cookies[SESSION_UUID_COOKIE_NAME];
|
||||||
if (sessionUuid)
|
if (sessionUuid)
|
||||||
{
|
{
|
||||||
console.log(`we have session UUID: ${sessionUuid} - validating it...`);
|
console.log(`we have session UUID: ${sessionUuid} - validating it...`);
|
||||||
const {uuid: newSessionUuid, values} = await qController.manageSession(null, sessionUuid, null);
|
const {values} = await qController.manageSession(null, sessionUuid, null);
|
||||||
|
|
||||||
setIsFullyAuthenticated(true);
|
setIsFullyAuthenticated(true);
|
||||||
qController.setGotAuthentication();
|
qController.setGotAuthentication();
|
||||||
@ -94,45 +116,16 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
|
|||||||
}
|
}
|
||||||
else
|
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("Loading token from OAuth2 provider...");
|
||||||
console.log(authOidc);
|
console.log(authOidc);
|
||||||
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
|
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
|
||||||
setEarlyReturnForAuth(<div>Signing in...</div>);
|
setEarlyReturnForAuth(<div>Signing in...</div>);
|
||||||
authOidc.signinRedirect();
|
authOidc?.signinRedirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// this is what's in the docs, but, it sure doesn't seem to ever hit any case other than the signinRedirect block //
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
/*
|
|
||||||
if (authOidc.isLoading)
|
|
||||||
{
|
|
||||||
setLoadingToken(false); //? so we can come back in? but i'm missing something here.
|
|
||||||
setEarlyReturnForAuth(<div>
|
|
||||||
<div>Loading...</div>
|
|
||||||
<button onClick={() => incrementCheckLoadingCounter()}>check again?</button>
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
else if (authOidc.error)
|
|
||||||
{
|
|
||||||
setEarlyReturnForAuth(<div>Error: {authOidc.error.message}</div>);
|
|
||||||
}
|
|
||||||
else if (authOidc.isAuthenticated)
|
|
||||||
{
|
|
||||||
setEarlyReturnForAuth(
|
|
||||||
<div>
|
|
||||||
Welcome, {authOidc.user?.profile.name}!
|
|
||||||
<button onClick={() => authOidc.signoutRedirect()}>Log out</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
localStorage.setItem(preSigninRedirectPathnameKey, window.location.pathname);
|
|
||||||
setEarlyReturnForAuth(<div>Signing in...</div>);
|
|
||||||
authOidc.signinRedirect();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
@ -151,7 +144,7 @@ export default function useOAuth2AuthenticationModule({setIsFullyAuthenticated,
|
|||||||
{
|
{
|
||||||
qController.clearAuthenticationMetaDataLocalStorage();
|
qController.clearAuthenticationMetaDataLocalStorage();
|
||||||
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
removeCookie(SESSION_UUID_COOKIE_NAME, {path: "/"});
|
||||||
authOidc.signoutRedirect();
|
authOidc?.signoutRedirect();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,107 @@ 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();
|
||||||
|
|
||||||
|
if (chipType !== "pvs")
|
||||||
|
{
|
||||||
|
const currentChipValidity = chips.map((chip, i) =>
|
||||||
|
(chipType !== "number" || !Number.isNaN(Number(chips[i])))
|
||||||
|
);
|
||||||
|
setChipValidity(currentChipValidity);
|
||||||
|
}
|
||||||
|
}, [JSON.stringify(chipData), chips]);
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
handleChipChange(chips);
|
handleChipChange(isMakingRequest, chipValidity, chipPVSIds);
|
||||||
}, [chips, handleChipChange]);
|
}, [chipValidity, chipPVSIds, isMakingRequest]);
|
||||||
|
|
||||||
function handleKeyDown(event: any)
|
function handleKeyDown(event: any)
|
||||||
{
|
{
|
||||||
@ -64,13 +152,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 +178,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 +215,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 +224,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 +257,7 @@ function ChipTextField({...props})
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ChipTextField.defaultProps = {
|
ChipTextField.defaultProps = {
|
||||||
chipData: []
|
chipData: []
|
||||||
};
|
};
|
||||||
@ -166,4 +266,4 @@ ChipTextField.propTypes = {
|
|||||||
chipData: arrayOf(string)
|
chipData: arrayOf(string)
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChipTextField
|
export default ChipTextField;
|
||||||
|
@ -96,6 +96,7 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
|||||||
if (width == "full")
|
if (width == "full")
|
||||||
{
|
{
|
||||||
itemSM = 12;
|
itemSM = 12;
|
||||||
|
itemLG = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -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,7 @@ function QDynamicFormField({
|
|||||||
newValue = newValue.toLowerCase();
|
newValue = newValue.toLowerCase();
|
||||||
}
|
}
|
||||||
setFieldValue(name, newValue);
|
setFieldValue(name, newValue);
|
||||||
if(onChangeCallback)
|
if (onChangeCallback)
|
||||||
{
|
{
|
||||||
onChangeCallback(newValue);
|
onChangeCallback(newValue);
|
||||||
}
|
}
|
||||||
@ -153,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);
|
||||||
}
|
}
|
||||||
@ -165,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}
|
||||||
@ -186,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")
|
||||||
{
|
{
|
||||||
@ -220,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);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
|
@ -184,9 +184,9 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
// copy values from specified fields in the parent record down into the child record //
|
// copy values from specified fields in the parent record down into the child record //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
if(widgetData.defaultValuesForNewChildRecordsFromParentFields)
|
if (widgetData.defaultValuesForNewChildRecordsFromParentFields)
|
||||||
{
|
{
|
||||||
for(let childField in widgetData.defaultValuesForNewChildRecordsFromParentFields)
|
for (let childField in widgetData.defaultValuesForNewChildRecordsFromParentFields)
|
||||||
{
|
{
|
||||||
const parentField = widgetData.defaultValuesForNewChildRecordsFromParentFields[childField];
|
const parentField = widgetData.defaultValuesForNewChildRecordsFromParentFields[childField];
|
||||||
defaultValues[childField] = formValues[parentField];
|
defaultValues[childField] = formValues[parentField];
|
||||||
@ -278,21 +278,21 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// build a map of display values for the new record, specifically, for any possible-values that need translated. //
|
// build a map of display values for the new record, specifically, for any possible-values that need translated. //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const displayValues: {[fieldName: string]: string} = {};
|
const displayValues: { [fieldName: string]: string } = {};
|
||||||
if(childTableName && values)
|
if (childTableName && values)
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// this function internally memoizes, so, we could potentially avoid an await here, but, seems ok... //
|
// this function internally memoizes, so, we could potentially avoid an await here, but, seems ok... //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const childTableMetaData = await qController.loadTableMetaData(childTableName)
|
const childTableMetaData = await qController.loadTableMetaData(childTableName);
|
||||||
for (let key in values)
|
for (let key in values)
|
||||||
{
|
{
|
||||||
const value = values[key];
|
const value = values[key];
|
||||||
const field = childTableMetaData.fields.get(key);
|
const field = childTableMetaData.fields.get(key);
|
||||||
if(field.possibleValueSourceName)
|
if (field.possibleValueSourceName)
|
||||||
{
|
{
|
||||||
const possibleValues = await qController.possibleValues(childTableName, null, field.name, null, [value], objectToMap(values), "form")
|
const possibleValues = await qController.possibleValues(childTableName, null, field.name, null, [value], null, objectToMap(values), "form");
|
||||||
if(possibleValues && possibleValues.length > 0)
|
if (possibleValues && possibleValues.length > 0)
|
||||||
{
|
{
|
||||||
displayValues[key] = possibleValues[0].label;
|
displayValues[key] = possibleValues[0].label;
|
||||||
}
|
}
|
||||||
@ -516,13 +516,12 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function objectToMap(object: { [key: string]: any }): Map<string, any>
|
function objectToMap(object: { [key: string]: any }): Map<string, any>
|
||||||
{
|
{
|
||||||
if(object == null)
|
if (object == null)
|
||||||
{
|
{
|
||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
@ -532,7 +531,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
rs.set(key, object[key]);
|
rs.set(key, object[key]);
|
||||||
}
|
}
|
||||||
return rs
|
return rs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -667,7 +666,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
|
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
|
||||||
if (defaultValue && fieldMetaData.possibleValueSourceName)
|
if (defaultValue && fieldMetaData.possibleValueSourceName)
|
||||||
{
|
{
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], objectToMap(initialValues), "form");
|
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);
|
||||||
|
@ -32,6 +32,7 @@ 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 {ReactNode, useEffect, useReducer, useState} from "react";
|
||||||
import {NavLink, useLocation} from "react-router-dom";
|
import {NavLink, useLocation} from "react-router-dom";
|
||||||
@ -301,6 +302,30 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
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}
|
||||||
@ -331,12 +356,7 @@ function Sidenav({color, icon, logo, appName, branding, routes, logout, ...rest}
|
|||||||
</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={
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
@ -162,8 +162,8 @@ function GotoRecordDialog(props: Props): JSX.Element
|
|||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** event handler for close button
|
** event handler for close button
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
const closeRequested = () =>
|
const closeRequested = () =>
|
||||||
{
|
{
|
||||||
if (props.mayClose)
|
if (props.mayClose)
|
||||||
@ -182,23 +182,23 @@ 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);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** event handler for clicking an 'option's go/submit button
|
** event handler for clicking an 'option's go/submit button
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
const optionGoClicked = async (optionIndex: number) =>
|
const optionGoClicked = async (optionIndex: number) =>
|
||||||
{
|
{
|
||||||
setError("");
|
setError("");
|
||||||
@ -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 //
|
||||||
|
@ -59,7 +59,8 @@ interface Props
|
|||||||
bulkLoadProfileOnChangeCallback?: (record: QRecord | null) => void,
|
bulkLoadProfileOnChangeCallback?: (record: QRecord | null) => void,
|
||||||
allowSelectingProfile?: boolean,
|
allowSelectingProfile?: boolean,
|
||||||
fileDescription?: FileDescription,
|
fileDescription?: FileDescription,
|
||||||
bulkLoadProfileResetToSuggestedMappingCallback?: () => void
|
bulkLoadProfileResetToSuggestedMappingCallback?: () => void,
|
||||||
|
isBulkEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
SavedBulkLoadProfiles.defaultProps = {
|
SavedBulkLoadProfiles.defaultProps = {
|
||||||
@ -72,7 +73,7 @@ const qController = Client.getInstance();
|
|||||||
** menu-button, text elements, and modal(s) that let you work with saved
|
** menu-button, text elements, and modal(s) that let you work with saved
|
||||||
** bulk-load profiles.
|
** bulk-load profiles.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, currentSavedBulkLoadProfileRecord, bulkLoadProfileOnChangeCallback, currentMapping, allowSelectingProfile, fileDescription, bulkLoadProfileResetToSuggestedMappingCallback}: Props): JSX.Element
|
function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, currentSavedBulkLoadProfileRecord, bulkLoadProfileOnChangeCallback, currentMapping, allowSelectingProfile, fileDescription, bulkLoadProfileResetToSuggestedMappingCallback, isBulkEdit}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [yourSavedBulkLoadProfiles, setYourSavedBulkLoadProfiles] = useState([] as QRecord[]);
|
const [yourSavedBulkLoadProfiles, setYourSavedBulkLoadProfiles] = useState([] as QRecord[]);
|
||||||
const [bulkLoadProfilesSharedWithYou, setBulkLoadProfilesSharedWithYou] = useState([] as QRecord[]);
|
const [bulkLoadProfilesSharedWithYou, setBulkLoadProfilesSharedWithYou] = useState([] as QRecord[]);
|
||||||
@ -142,6 +143,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("tableName", tableMetaData.name);
|
formData.append("tableName", tableMetaData.name);
|
||||||
|
formData.append("isBulkEdit", isBulkEdit.toString());
|
||||||
|
|
||||||
const savedBulkLoadProfiles = await makeSavedBulkLoadProfileRequest("querySavedBulkLoadProfile", formData);
|
const savedBulkLoadProfiles = await makeSavedBulkLoadProfileRequest("querySavedBulkLoadProfile", formData);
|
||||||
const yourSavedBulkLoadProfiles: QRecord[] = [];
|
const yourSavedBulkLoadProfiles: QRecord[] = [];
|
||||||
@ -212,7 +214,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
break;
|
break;
|
||||||
case RESET_TO_SUGGESTION:
|
case RESET_TO_SUGGESTION:
|
||||||
setSavePopupOpen(false);
|
setSavePopupOpen(false);
|
||||||
if(bulkLoadProfileResetToSuggestedMappingCallback)
|
if (bulkLoadProfileResetToSuggestedMappingCallback)
|
||||||
{
|
{
|
||||||
bulkLoadProfileResetToSuggestedMappingCallback();
|
bulkLoadProfileResetToSuggestedMappingCallback();
|
||||||
}
|
}
|
||||||
@ -265,6 +267,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
const bulkLoadProfile = currentMapping.toProfile();
|
const bulkLoadProfile = currentMapping.toProfile();
|
||||||
const mappingJson = JSON.stringify(bulkLoadProfile.profile);
|
const mappingJson = JSON.stringify(bulkLoadProfile.profile);
|
||||||
formData.append("mappingJson", mappingJson);
|
formData.append("mappingJson", mappingJson);
|
||||||
|
formData.append("isBulkEdit", isBulkEdit.toString());
|
||||||
|
|
||||||
if (isSaveAsAction || isRenameAction || currentSavedBulkLoadProfileRecord == null)
|
if (isSaveAsAction || isRenameAction || currentSavedBulkLoadProfileRecord == null)
|
||||||
{
|
{
|
||||||
@ -389,6 +392,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
return (savedBulkLoadProfiles);
|
return (savedBulkLoadProfiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bulkAction = isBulkEdit ? "Edit" : "Load";
|
||||||
const hasStorePermission = metaData?.processes.has("storeSavedBulkLoadProfile");
|
const hasStorePermission = metaData?.processes.has("storeSavedBulkLoadProfile");
|
||||||
const hasDeletePermission = metaData?.processes.has("deleteSavedBulkLoadProfile");
|
const hasDeletePermission = metaData?.processes.has("deleteSavedBulkLoadProfile");
|
||||||
const hasQueryPermission = metaData?.processes.has("querySavedBulkLoadProfile");
|
const hasQueryPermission = metaData?.processes.has("querySavedBulkLoadProfile");
|
||||||
@ -428,15 +432,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
PaperProps={{style: {maxHeight: "calc(100vh - 200px)", minWidth: menuWidth}}}
|
PaperProps={{style: {maxHeight: "calc(100vh - 200px)", minWidth: menuWidth}}}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial"}}><b>Bulk Load Profile Actions</b></MenuItem>
|
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial"}}><b>Bulk {bulkAction} Profile Actions</b></MenuItem>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!allowSelectingProfile &&
|
!allowSelectingProfile &&
|
||||||
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial", whiteSpace: "wrap", display: "block"}}>
|
<MenuItem sx={{width: menuWidth}} disabled style={{opacity: "initial", whiteSpace: "wrap", display: "block"}}>
|
||||||
{
|
{
|
||||||
currentSavedBulkLoadProfileRecord ?
|
currentSavedBulkLoadProfileRecord ?
|
||||||
<span>You are using the bulk load profile:<br /><b style={{paddingLeft: "1rem"}}>{currentSavedBulkLoadProfileRecord.values.get("label")}</b><br /><br />You can manage this profile on this screen.</span>
|
<span>You are using the bulk {bulkAction.toLowerCase()} profile:<br /><b style={{paddingLeft: "1rem"}}>{currentSavedBulkLoadProfileRecord.values.get("label")}</b><br /><br />You can manage this profile on this screen.</span>
|
||||||
: <span>You are not using a saved bulk load profile.<br /><br />You can save your profile on this screen.</span>
|
: <span>You are not using a saved bulk {bulkAction.toLowerCase()} profile.<br /><br />You can save your profile on this screen.</span>
|
||||||
}
|
}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
}
|
}
|
||||||
@ -456,7 +460,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
|
hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
|
||||||
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved bulk load profile."}>
|
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Change the name for this saved bulk {bulkAction.toLowerCase()} profile."}>
|
||||||
<span>
|
<span>
|
||||||
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
|
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(RENAME_OPTION)}>
|
||||||
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
|
<ListItemIcon><Icon>edit</Icon></ListItemIcon>
|
||||||
@ -467,7 +471,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
|
hasStorePermission && currentSavedBulkLoadProfileRecord != null &&
|
||||||
<Tooltip {...menuTooltipAttribs} title="Save a new copy this bulk load profile, with a different name, separate from the original.">
|
<Tooltip {...menuTooltipAttribs} title="Save a new copy this bulk {bulkAction.toLowerCase()} profile, with a different name, separate from the original.">
|
||||||
<span>
|
<span>
|
||||||
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
|
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null} onClick={() => handleDropdownOptionClick(DUPLICATE_OPTION)}>
|
||||||
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
|
<ListItemIcon><Icon>content_copy</Icon></ListItemIcon>
|
||||||
@ -478,7 +482,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
hasDeletePermission && currentSavedBulkLoadProfileRecord != null &&
|
hasDeletePermission && currentSavedBulkLoadProfileRecord != null &&
|
||||||
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved bulk load profile."}>
|
<Tooltip {...menuTooltipAttribs} title={notOwnerTooltipText ?? "Delete this saved bulk {bulkAction.toLowerCase()} profile."}>
|
||||||
<span>
|
<span>
|
||||||
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
|
<MenuItem disabled={currentSavedBulkLoadProfileRecord === null || disabledBecauseNotOwner} onClick={() => handleDropdownOptionClick(DELETE_OPTION)}>
|
||||||
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
|
<ListItemIcon><Icon>delete</Icon></ListItemIcon>
|
||||||
@ -489,11 +493,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
allowSelectingProfile &&
|
allowSelectingProfile &&
|
||||||
<Tooltip {...menuTooltipAttribs} title="Create a new blank bulk load profile for this table, removing all mappings.">
|
<Tooltip {...menuTooltipAttribs} title="Create a new blank bulk {bulkAction.toLowerCase()} profile for this table, removing all mappings.">
|
||||||
<span>
|
<span>
|
||||||
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
|
<MenuItem onClick={() => handleDropdownOptionClick(CLEAR_OPTION)}>
|
||||||
<ListItemIcon><Icon>monitor</Icon></ListItemIcon>
|
<ListItemIcon><Icon>monitor</Icon></ListItemIcon>
|
||||||
New Bulk Load Profile
|
New Bulk {bulkAction} Profile
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -504,7 +508,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
{
|
{
|
||||||
<Divider />
|
<Divider />
|
||||||
}
|
}
|
||||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Bulk Load Profiles</b></MenuItem>
|
<MenuItem disabled style={{"opacity": "initial"}}><b>Your Saved Bulk {bulkAction} Profiles</b></MenuItem>
|
||||||
{
|
{
|
||||||
yourSavedBulkLoadProfiles && yourSavedBulkLoadProfiles.length > 0 ? (
|
yourSavedBulkLoadProfiles && yourSavedBulkLoadProfiles.length > 0 ? (
|
||||||
yourSavedBulkLoadProfiles.map((record: QRecord, index: number) =>
|
yourSavedBulkLoadProfiles.map((record: QRecord, index: number) =>
|
||||||
@ -514,11 +518,11 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<MenuItem disabled sx={{opacity: "1 !important"}}>
|
<MenuItem disabled sx={{opacity: "1 !important"}}>
|
||||||
<i>You do not have any saved bulk load profiles for this table.</i>
|
<i>You do not have any saved bulk {bulkAction.toLowerCase()} profiles for this table.</i>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<MenuItem disabled style={{"opacity": "initial"}}><b>Bulk Load Profiles Shared with you</b></MenuItem>
|
<MenuItem disabled style={{"opacity": "initial"}}><b>Bulk {bulkAction} Profiles Shared with you</b></MenuItem>
|
||||||
{
|
{
|
||||||
bulkLoadProfilesSharedWithYou && bulkLoadProfilesSharedWithYou.length > 0 ? (
|
bulkLoadProfilesSharedWithYou && bulkLoadProfilesSharedWithYou.length > 0 ? (
|
||||||
bulkLoadProfilesSharedWithYou.map((record: QRecord, index: number) =>
|
bulkLoadProfilesSharedWithYou.map((record: QRecord, index: number) =>
|
||||||
@ -528,7 +532,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<MenuItem disabled sx={{opacity: "1 !important"}}>
|
<MenuItem disabled sx={{opacity: "1 !important"}}>
|
||||||
<i>You do not have any bulk load profiles shared with you for this table.</i>
|
<i>You do not have any bulk {bulkAction.toLowerCase()} profiles shared with you for this table.</i>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -537,7 +541,7 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
||||||
let buttonText = "Saved Bulk Load Profiles";
|
let buttonText = `Saved Bulk ${bulkAction} Profiles`;
|
||||||
let buttonBackground = "none";
|
let buttonBackground = "none";
|
||||||
let buttonBorder = colors.grayLines.main;
|
let buttonBorder = colors.grayLines.main;
|
||||||
let buttonColor = colors.gray.main;
|
let buttonColor = colors.gray.main;
|
||||||
@ -639,13 +643,13 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
<Tooltip {...tooltipMaxWidth("24rem")} sx={{cursor: "pointer"}} title={<>
|
<Tooltip {...tooltipMaxWidth("24rem")} sx={{cursor: "pointer"}} title={<>
|
||||||
<b>Unsaved Mapping</b>
|
<b>Unsaved Mapping</b>
|
||||||
<ul style={{padding: "0.5rem 1rem"}}>
|
<ul style={{padding: "0.5rem 1rem"}}>
|
||||||
<li>You are not using a saved bulk load profile.</li>
|
<li>You are not using a saved bulk {bulkAction.toLowerCase()} profile.</li>
|
||||||
{
|
{
|
||||||
/*bulkLoadProfileDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)*/
|
/*bulkLoadProfileDiffs.map((s: string, i: number) => <li key={i}>{s}</li>)*/
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</>}>
|
</>}>
|
||||||
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save Bulk Load Profile As…</Button>
|
<Button disableRipple={true} sx={linkButtonStyle} onClick={() => handleDropdownOptionClick(SAVE_OPTION)}>Save Bulk {bulkAction} Profile As…</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* vertical rule */}
|
{/* vertical rule */}
|
||||||
@ -716,20 +720,20 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
{
|
{
|
||||||
currentSavedBulkLoadProfileRecord ? (
|
currentSavedBulkLoadProfileRecord ? (
|
||||||
isDeleteAction ? (
|
isDeleteAction ? (
|
||||||
<DialogTitle id="alert-dialog-title">Delete Bulk Load Profile</DialogTitle>
|
<DialogTitle id="alert-dialog-title">Delete Bulk {bulkAction} Profile</DialogTitle>
|
||||||
) : (
|
) : (
|
||||||
isSaveAsAction ? (
|
isSaveAsAction ? (
|
||||||
<DialogTitle id="alert-dialog-title">Save Bulk Load Profile As</DialogTitle>
|
<DialogTitle id="alert-dialog-title">Save Bulk {bulkAction} Profile As</DialogTitle>
|
||||||
) : (
|
) : (
|
||||||
isRenameAction ? (
|
isRenameAction ? (
|
||||||
<DialogTitle id="alert-dialog-title">Rename Bulk Load Profile</DialogTitle>
|
<DialogTitle id="alert-dialog-title">Rename Bulk {bulkAction} Profile</DialogTitle>
|
||||||
) : (
|
) : (
|
||||||
<DialogTitle id="alert-dialog-title">Update Existing Bulk Load Profile</DialogTitle>
|
<DialogTitle id="alert-dialog-title">Update Existing Bulk {bulkAction} Profile</DialogTitle>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<DialogTitle id="alert-dialog-title">Save New Bulk Load Profile</DialogTitle>
|
<DialogTitle id="alert-dialog-title">Save New Bulk {bulkAction} Profile</DialogTitle>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<DialogContent sx={{width: "500px"}}>
|
<DialogContent sx={{width: "500px"}}>
|
||||||
@ -743,15 +747,15 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
isSaveAsAction ? (
|
isSaveAsAction ? (
|
||||||
<Box mb={3}>Enter a name for this new saved bulk load profile.</Box>
|
<Box mb={3}>Enter a name for this new saved bulk {bulkAction.toLowerCase()} profile.</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box mb={3}>Enter a new name for this saved bulk load profile.</Box>
|
<Box mb={3}>Enter a new name for this saved bulk {bulkAction.toLowerCase()} profile.</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
name="custom-delimiter-value"
|
name="custom-delimiter-value"
|
||||||
placeholder="Bulk Load Profile Name"
|
placeholder={`Bulk ${bulkAction} Profile Name`}
|
||||||
inputProps={{width: "100%", maxLength: 100}}
|
inputProps={{width: "100%", maxLength: 100}}
|
||||||
value={savedBulkLoadProfileNameInputValue}
|
value={savedBulkLoadProfileNameInputValue}
|
||||||
sx={{width: "100%"}}
|
sx={{width: "100%"}}
|
||||||
@ -764,9 +768,9 @@ function SavedBulkLoadProfiles({metaData, tableMetaData, tableStructure, current
|
|||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
isDeleteAction ? (
|
isDeleteAction ? (
|
||||||
<Box>Are you sure you want to delete the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
|
<Box>Are you sure you want to delete the bulk {bulkAction.toLowerCase()} profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box>Are you sure you want to update the bulk load profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
|
<Box>Are you sure you want to update the bulk {bulkAction.toLowerCase()} profile {`'${currentSavedBulkLoadProfileRecord?.values.get("label")}'`}?</Box>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,6 @@ 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";
|
||||||
@ -44,6 +43,7 @@ interface BulkLoadMappingFieldProps
|
|||||||
removeFieldCallback?: () => void,
|
removeFieldCallback?: () => void,
|
||||||
fileDescription: FileDescription,
|
fileDescription: FileDescription,
|
||||||
forceParentUpdate?: () => void,
|
forceParentUpdate?: () => void,
|
||||||
|
isBulkEdit?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const xIconButtonSX =
|
const xIconButtonSX =
|
||||||
@ -72,7 +72,7 @@ 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.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate}: BulkLoadMappingFieldProps): JSX.Element
|
export default function BulkLoadFileMappingField({bulkLoadField, isRequired, removeFieldCallback, fileDescription, forceParentUpdate, isBulkEdit}: BulkLoadMappingFieldProps): JSX.Element
|
||||||
{
|
{
|
||||||
const columnNames = fileDescription.getColumnNames();
|
const columnNames = fileDescription.getColumnNames();
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
// deal with dynamically loading the initial default value for a possible value... //
|
// deal with dynamically loading the initial default value for a possible value... //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
let actuallyDoingInitialLoadOfPossibleValue = doingInitialLoadOfPossibleValue;
|
let actuallyDoingInitialLoadOfPossibleValue = doingInitialLoadOfPossibleValue;
|
||||||
if(dynamicField.possibleValueProps && bulkLoadField.defaultValue && !doingInitialLoadOfPossibleValue && !everDidInitialLoadOfPossibleValue)
|
if (dynamicField.possibleValueProps && bulkLoadField.defaultValue && !doingInitialLoadOfPossibleValue && !everDidInitialLoadOfPossibleValue)
|
||||||
{
|
{
|
||||||
actuallyDoingInitialLoadOfPossibleValue = true;
|
actuallyDoingInitialLoadOfPossibleValue = true;
|
||||||
setDoingInitialLoadOfPossibleValue(true);
|
setDoingInitialLoadOfPossibleValue(true);
|
||||||
@ -104,7 +104,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const possibleValues = await qController.possibleValues(bulkLoadField.tableStructure.tableName, null, fieldMetaData.name, null, [bulkLoadField.defaultValue], undefined, "filter");
|
const possibleValues = await qController.possibleValues(bulkLoadField.tableStructure.tableName, null, fieldMetaData.name, null, [bulkLoadField.defaultValue], undefined, null, "filter");
|
||||||
if (possibleValues && possibleValues.length > 0)
|
if (possibleValues && possibleValues.length > 0)
|
||||||
{
|
{
|
||||||
setPossibleValueInitialDisplayValue(possibleValues[0].label);
|
setPossibleValueInitialDisplayValue(possibleValues[0].label);
|
||||||
@ -114,9 +114,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
setPossibleValueInitialDisplayValue(null);
|
setPossibleValueInitialDisplayValue(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e)
|
catch (e)
|
||||||
{
|
{
|
||||||
console.log(`Error loading possible value: ${e}`)
|
console.log(`Error loading possible value: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
actuallyDoingInitialLoadOfPossibleValue = false;
|
actuallyDoingInitialLoadOfPossibleValue = false;
|
||||||
@ -124,7 +124,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(dynamicField.possibleValueProps && possibleValueInitialDisplayValue)
|
if (dynamicField.possibleValueProps && possibleValueInitialDisplayValue)
|
||||||
{
|
{
|
||||||
dynamicField.possibleValueProps.initialDisplayValue = possibleValueInitialDisplayValue;
|
dynamicField.possibleValueProps.initialDisplayValue = possibleValueInitialDisplayValue;
|
||||||
}
|
}
|
||||||
@ -134,11 +134,11 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
// don't allow duplicates //
|
// don't allow duplicates //
|
||||||
//////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////
|
||||||
const columnOptions: { value: number, label: string }[] = [];
|
const columnOptions: { value: number, label: string }[] = [];
|
||||||
const usedLabels: {[label: string]: boolean} = {};
|
const usedLabels: { [label: string]: boolean } = {};
|
||||||
for (let i = 0; i < columnNames.length; i++)
|
for (let i = 0; i < columnNames.length; i++)
|
||||||
{
|
{
|
||||||
const label = columnNames[i];
|
const label = columnNames[i];
|
||||||
if(!usedLabels[label])
|
if (!usedLabels[label])
|
||||||
{
|
{
|
||||||
columnOptions.push({label: label, value: i});
|
columnOptions.push({label: label, value: i});
|
||||||
usedLabels[label] = true;
|
usedLabels[label] = true;
|
||||||
@ -148,9 +148,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// 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]);
|
setSelectedColumnInputValue(columnNames[bulkLoadField.columnIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,6 +228,17 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
forceParentUpdate && forceParentUpdate();
|
forceParentUpdate && forceParentUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function clearIfEmptyChanged(value: boolean)
|
||||||
|
{
|
||||||
|
bulkLoadField.clearIfEmpty = value;
|
||||||
|
forceParentUpdate && forceParentUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@ -314,8 +325,11 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
|
|||||||
<Box ml="1rem">
|
<Box ml="1rem">
|
||||||
{
|
{
|
||||||
valueType == "column" && <>
|
valueType == "column" && <>
|
||||||
<Box>
|
<Box display="flex" alignItems="center" sx={{height: "45px"}}>
|
||||||
<FormControlLabel value="mapValues" control={<Checkbox size="small" defaultChecked={bulkLoadField.doValueMapping} onChange={(event, checked) => mapValuesChanged(checked)} />} label={"Map values"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
<FormControlLabel value="mapValues" control={<Checkbox size="small" defaultChecked={bulkLoadField.doValueMapping} onChange={(event, checked) => mapValuesChanged(checked)} />} label={"Map values"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||||
|
{
|
||||||
|
isBulkEdit && !isRequired && <FormControlLabel value="clearIfEmpty" control={<Checkbox size="small" defaultChecked={bulkLoadField.clearIfEmpty} onChange={(event, checked) => clearIfEmptyChanged(checked)} />} label={"Clear if empty"} sx={{minWidth: "140px", whiteSpace: "nowrap"}} />
|
||||||
|
}
|
||||||
</Box>
|
</Box>
|
||||||
<Box fontSize={mainFontSize} mt="0.5rem">
|
<Box fontSize={mainFontSize} mt="0.5rem">
|
||||||
Preview Values: <span style={{color: "gray"}}>{(fileDescription.getPreviewValues(selectedColumn?.value) ?? [""]).join(", ")}</span>
|
Preview Values: <span style={{color: "gray"}}>{(fileDescription.getPreviewValues(selectedColumn?.value) ?? [""]).join(", ")}</span>
|
||||||
|
@ -33,6 +33,7 @@ interface BulkLoadMappingFieldsProps
|
|||||||
bulkLoadMapping: BulkLoadMapping,
|
bulkLoadMapping: BulkLoadMapping,
|
||||||
fileDescription: FileDescription,
|
fileDescription: FileDescription,
|
||||||
forceParentUpdate?: () => void,
|
forceParentUpdate?: () => void,
|
||||||
|
isBulkEdit?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ const ALREADY_ADDED_FIELD_TOOLTIP = "This field has already been added to your m
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** The section of the bulk load mapping screen with all the fields.
|
** The section of the bulk load mapping screen with all the fields.
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate}: BulkLoadMappingFieldsProps): JSX.Element
|
export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescription, forceParentUpdate, isBulkEdit}: BulkLoadMappingFieldsProps): JSX.Element
|
||||||
{
|
{
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
@ -254,11 +255,16 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h5>Required Fields</h5>
|
{isBulkEdit ? <h5>Key Fields</h5> : <h5>Required Fields</h5>}
|
||||||
<Box pl={"1rem"}>
|
<Box pl={"1rem"}>
|
||||||
{
|
{
|
||||||
bulkLoadMapping.requiredFields.length == 0 &&
|
bulkLoadMapping.requiredFields.length == 0 &&
|
||||||
<i style={{fontSize: "0.875rem"}}>There are no required fields in this table.</i>
|
(
|
||||||
|
isBulkEdit ?
|
||||||
|
<i style={{fontSize: "0.875rem"}}>Select table key fields to continue.</i>
|
||||||
|
:
|
||||||
|
<i style={{fontSize: "0.875rem"}}>There are no required fields in this table.</i>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
{bulkLoadMapping.requiredFields.map((bulkLoadField) => (
|
{bulkLoadMapping.requiredFields.map((bulkLoadField) => (
|
||||||
<BulkLoadFileMappingField
|
<BulkLoadFileMappingField
|
||||||
@ -267,12 +273,13 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
|||||||
bulkLoadField={bulkLoadField}
|
bulkLoadField={bulkLoadField}
|
||||||
isRequired={true}
|
isRequired={true}
|
||||||
forceParentUpdate={forceParentUpdate}
|
forceParentUpdate={forceParentUpdate}
|
||||||
|
isBulkEdit={isBulkEdit}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box mt="1rem">
|
<Box mt="1rem">
|
||||||
<h5>Additional Fields</h5>
|
{isBulkEdit ? <h5>Fields To Update</h5> : <h5>Additional Fields</h5>}
|
||||||
<Box pl={"1rem"}>
|
<Box pl={"1rem"}>
|
||||||
{bulkLoadMapping.additionalFields.map((bulkLoadField) => (
|
{bulkLoadMapping.additionalFields.map((bulkLoadField) => (
|
||||||
<BulkLoadFileMappingField
|
<BulkLoadFileMappingField
|
||||||
@ -282,6 +289,7 @@ export default function BulkLoadFileMappingFields({bulkLoadMapping, fileDescript
|
|||||||
isRequired={false}
|
isRequired={false}
|
||||||
removeFieldCallback={() => removeField(bulkLoadField)}
|
removeFieldCallback={() => removeField(bulkLoadField)}
|
||||||
forceParentUpdate={forceParentUpdate}
|
forceParentUpdate={forceParentUpdate}
|
||||||
|
isBulkEdit={isBulkEdit}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
@ -36,15 +36,18 @@ import {useFormikContext} from "formik";
|
|||||||
import colors from "qqq/assets/theme/base/colors";
|
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 DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
import HelpContent from "qqq/components/misc/HelpContent";
|
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, useImperativeHandle, useReducer, useState} from "react";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
import React, {forwardRef, useEffect, useImperativeHandle, useReducer, useState} from "react";
|
||||||
import ProcessViewForm from "./ProcessViewForm";
|
import ProcessViewForm from "./ProcessViewForm";
|
||||||
|
|
||||||
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
interface BulkLoadMappingFormProps
|
interface BulkLoadMappingFormProps
|
||||||
{
|
{
|
||||||
@ -73,13 +76,12 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
|
|
||||||
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);
|
||||||
const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, processValues.bulkLoadProfile));
|
const [bulkLoadMapping, setBulkLoadMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, processValues.bulkLoadProfile, processMetaData.name));
|
||||||
const [wrappedBulkLoadMapping] = useState(new Wrapper<BulkLoadMapping>(bulkLoadMapping));
|
const [wrappedBulkLoadMapping] = useState(new Wrapper<BulkLoadMapping>(bulkLoadMapping));
|
||||||
|
|
||||||
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
|
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
|
||||||
fileDescription.setHasHeaderRow(bulkLoadMapping.hasHeaderRow);
|
fileDescription.setHasHeaderRow(bulkLoadMapping.hasHeaderRow);
|
||||||
|
|
||||||
|
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -114,6 +116,8 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id");
|
values["savedBulkLoadProfileId"] = wrappedCurrentSavedBulkLoadProfile.get()?.values?.get("id");
|
||||||
values["layout"] = wrappedBulkLoadMapping.get().layout;
|
values["layout"] = wrappedBulkLoadMapping.get().layout;
|
||||||
values["hasHeaderRow"] = wrappedBulkLoadMapping.get().hasHeaderRow;
|
values["hasHeaderRow"] = wrappedBulkLoadMapping.get().hasHeaderRow;
|
||||||
|
values["isBulkEdit"] = wrappedBulkLoadMapping.get().isBulkEdit;
|
||||||
|
values["keyFields"] = wrappedBulkLoadMapping.get().keyFields;
|
||||||
|
|
||||||
let haveLocalErrors = false;
|
let haveLocalErrors = false;
|
||||||
const fieldErrors: { [fieldName: string]: string } = {};
|
const fieldErrors: { [fieldName: string]: string } = {};
|
||||||
@ -130,7 +134,14 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
}
|
}
|
||||||
setFieldErrors(fieldErrors);
|
setFieldErrors(fieldErrors);
|
||||||
|
|
||||||
if(wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0)
|
if (values["isBulkEdit"] && (values["keyFields"] == null || values["keyFields"] == undefined))
|
||||||
|
{
|
||||||
|
haveLocalErrors = true;
|
||||||
|
fieldErrors["keyFields"] = "This field is required.";
|
||||||
|
}
|
||||||
|
setFieldErrors(fieldErrors);
|
||||||
|
|
||||||
|
if (wrappedBulkLoadMapping.get().requiredFields.length == 0 && wrappedBulkLoadMapping.get().additionalFields.length == 0)
|
||||||
{
|
{
|
||||||
setNoMappedFieldsError("You must have at least 1 field.");
|
setNoMappedFieldsError("You must have at least 1 field.");
|
||||||
haveLocalErrors = true;
|
haveLocalErrors = true;
|
||||||
@ -141,9 +152,9 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
setNoMappedFieldsError(null);
|
setNoMappedFieldsError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(haveProfileErrors)
|
if (haveProfileErrors)
|
||||||
{
|
{
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
{
|
{
|
||||||
document.querySelector(".bulkLoadFieldError")?.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
|
document.querySelector(".bulkLoadFieldError")?.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
|
||||||
}, 250);
|
}, 250);
|
||||||
@ -182,7 +193,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
function bulkLoadProfileResetToSuggestedMappingCallback()
|
function bulkLoadProfileResetToSuggestedMappingCallback()
|
||||||
{
|
{
|
||||||
handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile));
|
handleNewBulkLoadMapping(BulkLoadMapping.fromBulkLoadProfile(processValues.tableStructure, suggestedBulkLoadProfile, processValues.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -201,6 +212,8 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
setBulkLoadMapping(newBulkLoadMapping);
|
setBulkLoadMapping(newBulkLoadMapping);
|
||||||
wrappedBulkLoadMapping.set(newBulkLoadMapping);
|
wrappedBulkLoadMapping.set(newBulkLoadMapping);
|
||||||
|
|
||||||
|
setFieldValue("isBulkEdit", newBulkLoadMapping.isBulkEdit);
|
||||||
|
setFieldValue("keyFields", newBulkLoadMapping.keyFields);
|
||||||
setFieldValue("hasHeaderRow", newBulkLoadMapping.hasHeaderRow);
|
setFieldValue("hasHeaderRow", newBulkLoadMapping.hasHeaderRow);
|
||||||
setFieldValue("layout", newBulkLoadMapping.layout);
|
setFieldValue("layout", newBulkLoadMapping.layout);
|
||||||
|
|
||||||
@ -228,10 +241,13 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
||||||
bulkLoadProfileResetToSuggestedMappingCallback={bulkLoadProfileResetToSuggestedMappingCallback}
|
bulkLoadProfileResetToSuggestedMappingCallback={bulkLoadProfileResetToSuggestedMappingCallback}
|
||||||
fileDescription={fileDescription}
|
fileDescription={fileDescription}
|
||||||
|
isBulkEdit={processValues.isBulkEdit}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<BulkLoadMappingHeader
|
<BulkLoadMappingHeader
|
||||||
|
tableMetaData={tableMetaData}
|
||||||
|
isBulkEdit={processValues.isBulkEdit}
|
||||||
key={rerenderHeader}
|
key={rerenderHeader}
|
||||||
bulkLoadMapping={bulkLoadMapping}
|
bulkLoadMapping={bulkLoadMapping}
|
||||||
fileDescription={fileDescription}
|
fileDescription={fileDescription}
|
||||||
@ -245,6 +261,7 @@ const BulkLoadFileMappingForm = forwardRef(({processValues, tableMetaData, metaD
|
|||||||
|
|
||||||
<Box mt="2rem">
|
<Box mt="2rem">
|
||||||
<BulkLoadFileMappingFields
|
<BulkLoadFileMappingFields
|
||||||
|
isBulkEdit={processValues.isBulkEdit}
|
||||||
bulkLoadMapping={bulkLoadMapping}
|
bulkLoadMapping={bulkLoadMapping}
|
||||||
fileDescription={fileDescription}
|
fileDescription={fileDescription}
|
||||||
forceParentUpdate={() =>
|
forceParentUpdate={() =>
|
||||||
@ -267,6 +284,7 @@ export default BulkLoadFileMappingForm;
|
|||||||
|
|
||||||
interface BulkLoadMappingHeaderProps
|
interface BulkLoadMappingHeaderProps
|
||||||
{
|
{
|
||||||
|
isBulkEdit?: boolean,
|
||||||
fileDescription: FileDescription,
|
fileDescription: FileDescription,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
bulkLoadMapping?: BulkLoadMapping,
|
bulkLoadMapping?: BulkLoadMapping,
|
||||||
@ -275,13 +293,16 @@ interface BulkLoadMappingHeaderProps
|
|||||||
forceParentUpdate?: () => void,
|
forceParentUpdate?: () => void,
|
||||||
frontendStep: QFrontendStepMetaData,
|
frontendStep: QFrontendStepMetaData,
|
||||||
processMetaData: QProcessMetaData,
|
processMetaData: QProcessMetaData,
|
||||||
|
tableMetaData: QTableMetaData,
|
||||||
}
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
** 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, frontendStep, processMetaData}: BulkLoadMappingHeaderProps): JSX.Element
|
function BulkLoadMappingHeader({isBulkEdit, fileDescription, fileName, bulkLoadMapping, fieldErrors, tableStructure, forceParentUpdate, frontendStep, processMetaData, tableMetaData}: BulkLoadMappingHeaderProps): JSX.Element
|
||||||
{
|
{
|
||||||
|
const [dynamicField, setDynamicField] = useState(null);
|
||||||
|
|
||||||
const viewFields = [
|
const viewFields = [
|
||||||
new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}),
|
new QFieldMetaData({name: "fileName", label: "File Name", type: "STRING"}),
|
||||||
new QFieldMetaData({name: "fileDetails", label: "File Details", type: "STRING"}),
|
new QFieldMetaData({name: "fileDetails", label: "File Details", type: "STRING"}),
|
||||||
@ -307,6 +328,36 @@ 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;
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
(async () =>
|
||||||
|
{
|
||||||
|
if (isBulkEdit)
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
// if doing a bulk edit, the selected keyFields and set as the display //
|
||||||
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
const displayValues = new Map<string, string>;
|
||||||
|
if (bulkLoadMapping.keyFields)
|
||||||
|
{
|
||||||
|
const possibleValues = await qController.possibleValues(null, processMetaData.name, "tableKeyFields", bulkLoadMapping.keyFields, null);
|
||||||
|
console.log("Received possible values of: " + JSON.stringify(possibleValues));
|
||||||
|
displayValues.set("tableKeyFields", possibleValues[0].label);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableKeyFieldsField = processMetaData.frontendSteps.find(s => s.name == "fileMapping")?.formFields.find(f => f.name == "tableKeyFields");
|
||||||
|
const newDynamicField = DynamicFormUtils.getDynamicField(tableKeyFieldsField);
|
||||||
|
const dynamicFieldInObject: any = {};
|
||||||
|
dynamicFieldInObject[tableKeyFieldsField["name"]] = newDynamicField;
|
||||||
|
DynamicFormUtils.addPossibleValueProps(dynamicFieldInObject, [tableKeyFieldsField], null, processMetaData.name, displayValues);
|
||||||
|
|
||||||
|
keyFieldsChanged(bulkLoadMapping.keyFields);
|
||||||
|
setDynamicField(newDynamicField);
|
||||||
|
forceParentUpdate();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [JSON.stringify(bulkLoadMapping)]);
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@ -331,6 +382,61 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
|||||||
forceParentUpdate();
|
forceParentUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
async function keyFieldsChanged(newValue: any)
|
||||||
|
{
|
||||||
|
fieldErrors.keyFields = null;
|
||||||
|
|
||||||
|
if (newValue && newValue.length > 0)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// validate that the fields in the key have been mapped //
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
console.log("Received key fields of: " + newValue);
|
||||||
|
const keyFields = newValue.split("|");
|
||||||
|
const unmappedKeyFields: string[] = [];
|
||||||
|
const requiredFields: BulkLoadField[] = [];
|
||||||
|
const additionalFields: BulkLoadField[] = [];
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// iterate over all fields in the table, when there are key fields found, make them required, //
|
||||||
|
// otherwise add them to addition fields //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
for (let bulkLoadField of [...bulkLoadMapping.requiredFields, ...bulkLoadMapping.additionalFields])
|
||||||
|
{
|
||||||
|
const qualifiedName = bulkLoadField.getQualifiedName();
|
||||||
|
const keyField = keyFields.find((k: string) => k == qualifiedName);
|
||||||
|
if (keyField)
|
||||||
|
{
|
||||||
|
requiredFields.push(bulkLoadField);
|
||||||
|
var fieldsByTablePrefix = bulkLoadMapping.fieldsByTablePrefix[""][keyField];
|
||||||
|
if (!fieldsByTablePrefix || fieldsByTablePrefix.columnIndex == null)
|
||||||
|
{
|
||||||
|
unmappedKeyFields.push(tableMetaData.fields.get(keyField).label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
additionalFields.push(bulkLoadField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkLoadMapping.requiredFields = requiredFields;
|
||||||
|
bulkLoadMapping.additionalFields = additionalFields;
|
||||||
|
|
||||||
|
if (unmappedKeyFields.length > 0)
|
||||||
|
{
|
||||||
|
fieldErrors.keyFields = "The following key fields are not mapped: " + unmappedKeyFields.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkLoadMapping.handleChangeToKeyFields(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
forceParentUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@ -369,27 +475,50 @@ function BulkLoadMappingHeader({fileDescription, fileName, bulkLoadMapping, fiel
|
|||||||
{getFormattedHelpContent("hasHeaderRow")}
|
{getFormattedHelpContent("hasHeaderRow")}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
|
|
||||||
<Autocomplete
|
|
||||||
id={"layout"}
|
|
||||||
renderInput={(params) => (<TextField {...params} label={""} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
|
|
||||||
options={layoutOptions}
|
|
||||||
multiple={false}
|
|
||||||
defaultValue={selectedLayout}
|
|
||||||
onChange={layoutChanged}
|
|
||||||
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
|
||||||
isOptionEqualToValue={(option, value) => option == null && value == null || option.id == value.id}
|
|
||||||
renderOption={(props, option, state) => (<li {...props}>{option?.label ?? ""}</li>)}
|
|
||||||
disableClearable
|
|
||||||
sx={{"& .MuiOutlinedInput-root": {padding: "0"}}}
|
|
||||||
/>
|
|
||||||
{
|
{
|
||||||
fieldErrors.layout &&
|
!isBulkEdit ? (
|
||||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
|
<>
|
||||||
{<div className="fieldErrorMessage">{fieldErrors.layout}</div>}
|
<DynamicFormFieldLabel name={"layout"} label={"File Layout *"} />
|
||||||
</MDTypography>
|
<Autocomplete
|
||||||
|
id={"layout"}
|
||||||
|
renderInput={(params) => (<TextField {...params} label={""} fullWidth variant="outlined" autoComplete="off" type="search" InputProps={{...params.InputProps}} sx={{"& .MuiOutlinedInput-root": {borderRadius: "0.75rem"}}} />)}
|
||||||
|
options={layoutOptions}
|
||||||
|
multiple={false}
|
||||||
|
defaultValue={selectedLayout}
|
||||||
|
onChange={layoutChanged}
|
||||||
|
getOptionLabel={(option) => typeof (option) == "string" ? option : (option?.label ?? "")}
|
||||||
|
isOptionEqualToValue={(option, value) => option == null && value == null || option.id == value.id}
|
||||||
|
renderOption={(props, option, state) => (<li {...props}>{option?.label ?? ""}</li>)}
|
||||||
|
disableClearable
|
||||||
|
sx={{"& .MuiOutlinedInput-root": {padding: "0"}}}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
fieldErrors.layout &&
|
||||||
|
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
|
||||||
|
{<div className="fieldErrorMessage">{fieldErrors.layout}</div>}
|
||||||
|
</MDTypography>
|
||||||
|
}
|
||||||
|
{getFormattedHelpContent("layout")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
dynamicField &&
|
||||||
|
<>
|
||||||
|
<DynamicFormFieldLabel name={dynamicField.name} label={`${dynamicField.label} *`} />
|
||||||
|
<QDynamicFormField name={dynamicField.name} displayFormat={""} label={""} formFieldObject={dynamicField} type={"pvs"} value={bulkLoadMapping.keyFields} onChangeCallback={keyFieldsChanged} />
|
||||||
|
{
|
||||||
|
fieldErrors.keyFields &&
|
||||||
|
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" mt="0.25rem">
|
||||||
|
{<div className="fieldErrorMessage">{fieldErrors.keyFields}</div>}
|
||||||
|
</MDTypography>
|
||||||
|
}
|
||||||
|
{getFormattedHelpContent("tableKeyFields")}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
{getFormattedHelpContent("layout")}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
@ -490,25 +619,25 @@ function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoad
|
|||||||
const fields = bulkLoadMapping.getFieldsForColumnIndex(index);
|
const fields = bulkLoadMapping.getFieldsForColumnIndex(index);
|
||||||
const count = fields.length;
|
const count = fields.length;
|
||||||
|
|
||||||
let dupeWarning = <></>
|
let dupeWarning = <></>;
|
||||||
if(fileDescription.hasHeaderRow && fileDescription.duplicateHeaderIndexes[index])
|
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}>
|
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>
|
<Icon color="warning" sx={{p: "0.125rem", mr: "0.25rem"}}>warning</Icon>
|
||||||
</Tooltip>
|
</Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<td key={letter} style={{textAlign: "center", color: getHeaderColor(count), cursor: getCursor(count)}}>
|
return (<td key={letter} style={{textAlign: "center", color: getHeaderColor(count), cursor: getCursor(count)}}>
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
count > 0 &&
|
count > 0 &&
|
||||||
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}>
|
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}>
|
||||||
<Box>
|
<Box>
|
||||||
{dupeWarning}
|
{dupeWarning}
|
||||||
{letter}
|
{letter}
|
||||||
<Badge badgeContent={count} variant={"standard"} color="secondary" sx={{marginTop: ".75rem"}}><Icon></Icon></Badge>
|
<Badge badgeContent={count} variant={"standard"} color="secondary" sx={{marginTop: ".75rem"}}><Icon></Icon></Badge>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
count == 0 && <Box>{dupeWarning}{letter}</Box>
|
count == 0 && <Box>{dupeWarning}{letter}</Box>
|
||||||
@ -528,24 +657,24 @@ function BulkLoadMappingFilePreview({fileDescription, bulkLoadMapping}: BulkLoad
|
|||||||
const count = fields.length;
|
const count = fields.length;
|
||||||
const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""};
|
const tdStyle = {color: getHeaderColor(count), cursor: getCursor(count), backgroundColor: ""};
|
||||||
|
|
||||||
if(fileDescription.hasHeaderRow)
|
if (fileDescription.hasHeaderRow)
|
||||||
{
|
{
|
||||||
tdStyle.backgroundColor = "#ebebeb";
|
tdStyle.backgroundColor = "#ebebeb";
|
||||||
|
|
||||||
if(count > 0)
|
if (count > 0)
|
||||||
{
|
{
|
||||||
return <td key={value} style={tdStyle}>
|
return <td key={value} style={tdStyle}>
|
||||||
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}><Box>{value}</Box></Tooltip>
|
<Tooltip title={getColumnTooltip(fields)} placement="top" enterDelay={500}><Box>{value}</Box></Tooltip>
|
||||||
</td>
|
</td>;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return <td key={value} style={tdStyle}>{value}</td>
|
return <td key={value} style={tdStyle}>{value}</td>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return <td key={value} style={tdStyle}>{value}</td>
|
return <td key={value} style={tdStyle}>{value}</td>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
@ -43,12 +43,12 @@ interface BulkLoadValueMappingFormProps
|
|||||||
const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}: BulkLoadValueMappingFormProps, ref) =>
|
const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}: BulkLoadValueMappingFormProps, ref) =>
|
||||||
{
|
{
|
||||||
const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord;
|
const savedBulkLoadProfileRecordProcessValue = processValues.savedBulkLoadProfileRecord;
|
||||||
const [savedBulkLoadProfileRecord, setSavedBulkLoadProfileRecord] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue))
|
const [savedBulkLoadProfileRecord, setSavedBulkLoadProfileRecord] = useState(savedBulkLoadProfileRecordProcessValue == null ? null : new QRecord(savedBulkLoadProfileRecordProcessValue));
|
||||||
|
|
||||||
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
|
const [tableStructure] = useState(processValues.tableStructure as BulkLoadTableStructure);
|
||||||
|
|
||||||
const [bulkLoadProfile, setBulkLoadProfile] = useState(processValues.bulkLoadProfile as BulkLoadProfile);
|
const [bulkLoadProfile, setBulkLoadProfile] = useState(processValues.bulkLoadProfile as BulkLoadProfile);
|
||||||
const [currentMapping, setCurrentMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile))
|
const [currentMapping, setCurrentMapping] = useState(BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile));
|
||||||
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(savedBulkLoadProfileRecord));
|
const [wrappedCurrentSavedBulkLoadProfile] = useState(new Wrapper<QRecord>(savedBulkLoadProfileRecord));
|
||||||
|
|
||||||
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
|
const [fileDescription] = useState(new FileDescription(processValues.headerValues, processValues.headerLetters, processValues.bodyValuesPreview));
|
||||||
@ -93,10 +93,11 @@ const BulkLoadProfileForm = forwardRef(({processValues, tableMetaData, metaData}
|
|||||||
allowSelectingProfile={false}
|
allowSelectingProfile={false}
|
||||||
fileDescription={fileDescription}
|
fileDescription={fileDescription}
|
||||||
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
||||||
|
isBulkEdit={processValues.isBulkEdit}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</Box>);
|
</Box>);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default BulkLoadProfileForm;
|
export default BulkLoadProfileForm;
|
||||||
|
@ -75,7 +75,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function initializeCurrentBulkLoadMapping(): BulkLoadMapping
|
function initializeCurrentBulkLoadMapping(): BulkLoadMapping
|
||||||
{
|
{
|
||||||
const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile);
|
const bulkLoadMapping = BulkLoadMapping.fromBulkLoadProfile(tableStructure, bulkLoadProfile, processValues.name);
|
||||||
|
|
||||||
if (!bulkLoadMapping.valueMappings[fieldFullName])
|
if (!bulkLoadMapping.valueMappings[fieldFullName])
|
||||||
{
|
{
|
||||||
@ -155,7 +155,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
|
|||||||
function mappedValueChanged(fileValue: string, newValue: any)
|
function mappedValueChanged(fileValue: string, newValue: any)
|
||||||
{
|
{
|
||||||
valueErrors[fileValue] = null;
|
valueErrors[fileValue] = null;
|
||||||
if(newValue == null)
|
if (newValue == null)
|
||||||
{
|
{
|
||||||
delete currentMapping.valueMappings[fieldFullName][fileValue];
|
delete currentMapping.valueMappings[fieldFullName][fileValue];
|
||||||
}
|
}
|
||||||
@ -195,6 +195,7 @@ const BulkLoadValueMappingForm = forwardRef(({processValues, setActiveStepLabel,
|
|||||||
allowSelectingProfile={false}
|
allowSelectingProfile={false}
|
||||||
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
bulkLoadProfileOnChangeCallback={bulkLoadProfileOnChangeCallback}
|
||||||
fileDescription={fileDescription}
|
fileDescription={fileDescription}
|
||||||
|
isBulkEdit={processValues.isBulkEdit}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
@ -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,18 +128,43 @@ 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)
|
||||||
{
|
{
|
||||||
saveData.push(chipData[i]);
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// 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);
|
||||||
|
|
||||||
clearData();
|
clearData();
|
||||||
@ -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,152 +318,207 @@ 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 + " 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>
|
||||||
<Tooltip title="Quickly add many values to your filter by pasting them from a spreadsheet or any other data source.">
|
<Tooltip title="Quickly add many values to your filter by pasting them from a spreadsheet or any other data source.">
|
||||||
<Icon onClick={handlePasteClick} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}}>paste_content</Icon>
|
<Icon className="criteriaPasterButton" onClick={handlePasteClick} fontSize="small" color="info" sx={{mx: 0.25, cursor: "pointer"}}>paste_content</Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{
|
{
|
||||||
pasteModalIsOpen &&
|
pasteModalIsOpen &&
|
||||||
(
|
(
|
||||||
<Modal open={pasteModalIsOpen}>
|
<Modal open={pasteModalIsOpen}>
|
||||||
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
|
<Box>
|
||||||
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
|
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
|
||||||
<Card sx={mainCardStyles}>
|
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
|
||||||
<Box p={4} pb={2}>
|
<Card sx={mainCardStyles}>
|
||||||
<Grid container>
|
<Box p={4} pb={2}>
|
||||||
<Grid item pr={3} xs={12} lg={12}>
|
<Grid container>
|
||||||
<Typography variant="h5">Bulk Add Filter Values</Typography>
|
<Grid item pr={3} xs={12} lg={12}>
|
||||||
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
|
<Typography variant="h5">Bulk Add Filter Values</Typography>
|
||||||
Paste into the box on the left.
|
{
|
||||||
Review the filter values in the box on the right.
|
formattedHelpContent && <Box sx={{display: "flex", lineHeight: "1.7", textTransform: "none"}}>
|
||||||
If the filter values are not what are expected, try changing the separator using the dropdown below.
|
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
|
||||||
</Typography>
|
{formattedHelpContent}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
|
||||||
|
<Grid item pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
|
||||||
|
<FormControl sx={{m: 1, width: "100%"}}>
|
||||||
|
<TextField
|
||||||
|
className="criteriaPasterTextArea"
|
||||||
|
id="outlined-multiline-static"
|
||||||
|
label="PASTE TEXT"
|
||||||
|
multiline
|
||||||
|
onChange={handleTextChange}
|
||||||
|
rows={16}
|
||||||
|
value={inputText}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6} lg={6} sx={{display: "flex", flexGrow: 1}}>
|
||||||
|
<FormControl sx={{m: 1, width: "100%"}}>
|
||||||
|
<ChipTextField
|
||||||
|
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}
|
||||||
|
chipValidity={chipValidity}
|
||||||
|
chipType={type}
|
||||||
|
multiline
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
id="tags"
|
||||||
|
rows={0}
|
||||||
|
name="tags"
|
||||||
|
label="FILTER VALUES REVIEW"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
|
||||||
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
|
<Grid item pl={1} pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
|
||||||
<Grid item pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
|
<Box sx={{display: "inline-flex", alignItems: "baseline"}}>
|
||||||
<FormControl sx={{m: 1, width: "100%"}}>
|
<FormControl sx={{mt: 2, width: "50%"}}>
|
||||||
<TextField
|
<InputLabel htmlFor="select-native">
|
||||||
id="outlined-multiline-static"
|
SEPARATOR
|
||||||
label="PASTE TEXT"
|
</InputLabel>
|
||||||
multiline
|
<Select
|
||||||
onChange={handleTextChange}
|
multiline
|
||||||
rows={16}
|
native
|
||||||
value={inputText}
|
value={delimiter}
|
||||||
/>
|
onChange={handleDelimiterChange}
|
||||||
</FormControl>
|
label="SEPARATOR"
|
||||||
</Grid>
|
size="medium"
|
||||||
<Grid item xs={6} lg={6} sx={{display: "flex", flexGrow: 1}}>
|
inputProps={{
|
||||||
<FormControl sx={{m: 1, width: "100%"}}>
|
id: "select-native",
|
||||||
<ChipTextField
|
}}
|
||||||
handleChipChange={() =>
|
>
|
||||||
{
|
{delimiterDropdownOptions.map((delimiter) => (
|
||||||
}}
|
<option key={delimiter} value={delimiter}>
|
||||||
chipData={chipData}
|
{delimiter}
|
||||||
chipType={type}
|
</option>
|
||||||
multiline
|
))}
|
||||||
fullWidth
|
</Select>
|
||||||
variant="outlined"
|
|
||||||
id="tags"
|
|
||||||
rows={0}
|
|
||||||
name="tags"
|
|
||||||
label="FILTER VALUES REVIEW"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
|
|
||||||
<Grid item pl={1} pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
|
|
||||||
<Box sx={{display: "inline-flex", alignItems: "baseline"}}>
|
|
||||||
<FormControl sx={{mt: 2, width: "50%"}}>
|
|
||||||
<InputLabel htmlFor="select-native">
|
|
||||||
SEPARATOR
|
|
||||||
</InputLabel>
|
|
||||||
<Select
|
|
||||||
multiline
|
|
||||||
native
|
|
||||||
value={delimiter}
|
|
||||||
onChange={handleDelimiterChange}
|
|
||||||
label="SEPARATOR"
|
|
||||||
size="medium"
|
|
||||||
inputProps={{
|
|
||||||
id: "select-native",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{delimiterDropdownOptions.map((delimiter) => (
|
|
||||||
<option key={delimiter} value={delimiter}>
|
|
||||||
{delimiter}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
{delimiter === Delimiter.CUSTOM.valueOf() && (
|
|
||||||
|
|
||||||
<FormControl sx={{pl: 2, top: 5, width: "50%"}}>
|
|
||||||
<TextField
|
|
||||||
name="custom-delimiter-value"
|
|
||||||
placeholder="Custom Separator"
|
|
||||||
label="Custom Separator"
|
|
||||||
variant="standard"
|
|
||||||
value={customDelimiterValue}
|
|
||||||
onChange={handleCustomDelimiterChange}
|
|
||||||
inputProps={{maxLength: 1}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
{delimiter === Delimiter.CUSTOM.valueOf() && (
|
||||||
{inputText && delimiter === Delimiter.DETECT_AUTOMATICALLY.valueOf() && (
|
|
||||||
|
|
||||||
<Typography pl={2} variant="button" sx={{top: "1px", textTransform: "revert"}}>
|
<FormControl sx={{pl: 2, top: 5, width: "50%"}}>
|
||||||
<i>{detectedText}</i>
|
<TextField
|
||||||
</Typography>
|
name="custom-delimiter-value"
|
||||||
)}
|
placeholder="Custom Separator"
|
||||||
</Box>
|
label="Custom Separator"
|
||||||
|
variant="standard"
|
||||||
|
value={customDelimiterValue}
|
||||||
|
onChange={handleCustomDelimiterChange}
|
||||||
|
inputProps={{maxLength: 1}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
{inputText && delimiter === Delimiter.DETECT_AUTOMATICALLY.valueOf() && (
|
||||||
|
|
||||||
|
<Typography pl={2} variant="button" sx={{top: "1px", textTransform: "revert"}}>
|
||||||
|
<i>{detectedText}</i>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}} item pl={1} xs={4} lg={4}>
|
||||||
|
{
|
||||||
|
errorText && chipData.length > 0 && (
|
||||||
|
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}>
|
||||||
|
<Icon color="error">error</Icon>
|
||||||
|
<Typography sx={{paddingLeft: "4px", textTransform: "revert"}} variant="button">{errorText}</Typography>
|
||||||
|
</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 sx={{display: "flex", justifyContent: "flex-end", alignItems: "flex-start"}} item pr={1} xs={2} lg={2}>
|
||||||
|
{
|
||||||
|
chipData && chipData.length > 0 && (
|
||||||
|
<Typography sx={{textTransform: "revert"}} variant="button">{chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"} {uniqueCount && `(${uniqueCount} unique)`}</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}} item pl={1} xs={3} lg={3}>
|
<Box p={3} pt={0}>
|
||||||
{
|
<Grid container pl={1} pr={1} justifyContent="right" alignItems="stretch" sx={{display: "flex-inline "}}>
|
||||||
errorText && chipData.length > 0 && (
|
<QCancelButton
|
||||||
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}>
|
onClickHandler={handleCancelClicked}
|
||||||
<Icon color="error">error</Icon>
|
iconName="cancel"
|
||||||
<Typography sx={{paddingLeft: "4px", textTransform: "revert"}} variant="button">{errorText}</Typography>
|
disabled={false} />
|
||||||
</Box>
|
<QSaveButton onClickHandler={handleSaveClicked} label="Add Values" disabled={saveDisabled} />
|
||||||
)
|
</Grid>
|
||||||
}
|
</Box>
|
||||||
</Grid>
|
</Card>
|
||||||
<Grid sx={{display: "flex", justifyContent: "flex-end", alignItems: "flex-start"}} item pr={1} xs={3} lg={3}>
|
</Box>
|
||||||
{
|
|
||||||
chipData && chipData.length > 0 && (
|
|
||||||
<Typography sx={{textTransform: "revert"}} variant="button">{chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"}</Typography>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
<Box p={3} pt={0}>
|
|
||||||
<Grid container pl={1} pr={1} justifyContent="right" alignItems="stretch" sx={{display: "flex-inline "}}>
|
|
||||||
<QCancelButton
|
|
||||||
onClickHandler={handleCancelClicked}
|
|
||||||
iconName="cancel"
|
|
||||||
disabled={false} />
|
|
||||||
<QSaveButton onClickHandler={handleSaveClicked} label="Add Filters" disabled={false} />
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -398,20 +398,25 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
initialValues = criteria.values;
|
initialValues = criteria.values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <Box>
|
return <Box display="flex" alignItems="flex-end" className="multiValue">
|
||||||
<DynamicSelect
|
<Box width={"100%"}>
|
||||||
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
|
<DynamicSelect
|
||||||
overrideId={field.name + "-multi-" + criteria.id}
|
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
|
||||||
key={field.name + "-multi-" + criteria.id}
|
overrideId={field.name + "-multi-" + criteria.id}
|
||||||
isMultiple
|
key={field.name + "-multi-" + criteria.id + "-" + criteria.values.length}
|
||||||
fieldLabel="Values"
|
isMultiple
|
||||||
initialValues={initialValues}
|
fieldLabel="Values"
|
||||||
initiallyOpen={false /*initiallyOpenMultiValuePvs*/}
|
initialValues={initialValues}
|
||||||
inForm={false}
|
initiallyOpen={false /*initiallyOpenMultiValuePvs*/}
|
||||||
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
inForm={false}
|
||||||
variant="standard"
|
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
||||||
useCase="filter"
|
variant="standard"
|
||||||
/>
|
useCase="filter"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<FilterCriteriaPaster table={table} field={field} type="pvs" onSave={(newValues: any[]) => saveNewPasterValues(newValues)} />
|
||||||
|
</Box>
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,9 +29,9 @@ import Icon from "@mui/material/Icon";
|
|||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import {QActionsMenuButton} from "qqq/components/buttons/DefaultButtons";
|
|
||||||
|
|
||||||
interface QueryScreenActionMenuProps
|
interface QueryScreenActionMenuProps
|
||||||
{
|
{
|
||||||
@ -40,28 +40,28 @@ interface QueryScreenActionMenuProps
|
|||||||
tableProcesses: QProcessMetaData[];
|
tableProcesses: QProcessMetaData[];
|
||||||
bulkLoadClicked: () => void;
|
bulkLoadClicked: () => void;
|
||||||
bulkEditClicked: () => void;
|
bulkEditClicked: () => void;
|
||||||
|
bulkEditWithFileClicked: () => void;
|
||||||
bulkDeleteClicked: () => void;
|
bulkDeleteClicked: () => void;
|
||||||
processClicked: (process: QProcessMetaData) => void;
|
processClicked: (process: QProcessMetaData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryScreenActionMenu.defaultProps = {
|
QueryScreenActionMenu.defaultProps = {};
|
||||||
};
|
|
||||||
|
|
||||||
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element
|
export default function QueryScreenActionMenu({metaData, tableMetaData, tableProcesses, bulkLoadClicked, bulkEditClicked, bulkEditWithFileClicked, bulkDeleteClicked, processClicked}: QueryScreenActionMenuProps): JSX.Element
|
||||||
{
|
{
|
||||||
const [anchorElement, setAnchorElement] = useState(null)
|
const [anchorElement, setAnchorElement] = useState(null);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const openActionsMenu = (event: any) =>
|
const openActionsMenu = (event: any) =>
|
||||||
{
|
{
|
||||||
setAnchorElement(event.currentTarget);
|
setAnchorElement(event.currentTarget);
|
||||||
}
|
};
|
||||||
|
|
||||||
const closeActionsMenu = () =>
|
const closeActionsMenu = () =>
|
||||||
{
|
{
|
||||||
setAnchorElement(null);
|
setAnchorElement(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
const pushDividerIfNeeded = (menuItems: JSX.Element[]) =>
|
const pushDividerIfNeeded = (menuItems: JSX.Element[]) =>
|
||||||
{
|
{
|
||||||
@ -75,7 +75,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
|||||||
{
|
{
|
||||||
closeActionsMenu();
|
closeActionsMenu();
|
||||||
handler();
|
handler();
|
||||||
}
|
};
|
||||||
|
|
||||||
const menuItems: JSX.Element[] = [];
|
const menuItems: JSX.Element[] = [];
|
||||||
if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission)
|
if (tableMetaData.capabilities.has(Capability.TABLE_INSERT) && tableMetaData.insertPermission)
|
||||||
@ -85,6 +85,7 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
|||||||
if (tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission)
|
if (tableMetaData.capabilities.has(Capability.TABLE_UPDATE) && tableMetaData.editPermission)
|
||||||
{
|
{
|
||||||
menuItems.push(<MenuItem key="bulkEdit" onClick={() => runSomething(bulkEditClicked)}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>);
|
menuItems.push(<MenuItem key="bulkEdit" onClick={() => runSomething(bulkEditClicked)}><ListItemIcon><Icon>edit</Icon></ListItemIcon>Bulk Edit</MenuItem>);
|
||||||
|
menuItems.push(<MenuItem key="bulkEditWithFile" onClick={() => runSomething(bulkEditWithFileClicked)}><ListItemIcon><Icon>edit_note</Icon></ListItemIcon>Bulk Edit With File</MenuItem>);
|
||||||
}
|
}
|
||||||
if (tableMetaData.capabilities.has(Capability.TABLE_DELETE) && tableMetaData.deletePermission)
|
if (tableMetaData.capabilities.has(Capability.TABLE_DELETE) && tableMetaData.deletePermission)
|
||||||
{
|
{
|
||||||
@ -130,5 +131,5 @@ export default function QueryScreenActionMenu({metaData, tableMetaData, tablePro
|
|||||||
{menuItems}
|
{menuItems}
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
|
|||||||
return (filteredOptions[0]);
|
return (filteredOptions[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(return0thOptionInsteadOfNull)
|
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:");
|
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
|
try
|
||||||
@ -144,7 +144,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
|
|||||||
console.log("Criteria: " + JSON.stringify(criteria));
|
console.log("Criteria: " + JSON.stringify(criteria));
|
||||||
console.log("Default Operator: " + JSON.stringify(defaultOperator));
|
console.log("Default Operator: " + JSON.stringify(defaultOperator));
|
||||||
}
|
}
|
||||||
catch(e)
|
catch (e)
|
||||||
{
|
{
|
||||||
console.log(`Error in debug output: ${e}`);
|
console.log(`Error in debug output: ${e}`);
|
||||||
}
|
}
|
||||||
@ -186,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)]);
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
|
@ -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>
|
<>
|
||||||
<NavBar />
|
<DashboardLayout>
|
||||||
<Box>{children}</Box>
|
{banner()}
|
||||||
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
|
<NavBar />
|
||||||
</DashboardLayout>
|
<Box>{children}</Box>
|
||||||
|
<Footer company={{href: metaData?.branding?.companyUrl, name: metaData?.branding?.companyName}} />
|
||||||
|
</DashboardLayout>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ export class BulkLoadField
|
|||||||
headerName?: string = null;
|
headerName?: string = null;
|
||||||
defaultValue?: any = null;
|
defaultValue?: any = null;
|
||||||
doValueMapping: boolean = false;
|
doValueMapping: boolean = false;
|
||||||
|
clearIfEmpty?: boolean = false;
|
||||||
|
|
||||||
wideLayoutIndexPath: number[] = [];
|
wideLayoutIndexPath: number[] = [];
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ export class BulkLoadField
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null)
|
constructor(field: QFieldMetaData, tableStructure: BulkLoadTableStructure, valueType: ValueType = "column", columnIndex?: number, headerName?: string, defaultValue?: any, doValueMapping?: boolean, wideLayoutIndexPath: number[] = [], error: string = null, warning: string = null, clearIfEmpty?: boolean)
|
||||||
{
|
{
|
||||||
this.field = field;
|
this.field = field;
|
||||||
this.tableStructure = tableStructure;
|
this.tableStructure = tableStructure;
|
||||||
@ -64,6 +65,7 @@ export class BulkLoadField
|
|||||||
this.error = error;
|
this.error = error;
|
||||||
this.warning = warning;
|
this.warning = warning;
|
||||||
this.key = new Date().getTime().toString();
|
this.key = new Date().getTime().toString();
|
||||||
|
this.clearIfEmpty = clearIfEmpty ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -72,7 +74,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, source.error, source.warning));
|
return (new BulkLoadField(source.field, source.tableStructure, source.valueType, source.columnIndex, source.headerName, source.defaultValue, source.doValueMapping, source.wideLayoutIndexPath, source.error, source.warning, source.clearIfEmpty));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -173,6 +175,9 @@ export interface BulkLoadTableStructure
|
|||||||
associationPath: string;
|
associationPath: string;
|
||||||
fields: QFieldMetaData[];
|
fields: QFieldMetaData[];
|
||||||
associations: BulkLoadTableStructure[];
|
associations: BulkLoadTableStructure[];
|
||||||
|
isBulkEdit: boolean;
|
||||||
|
possibleKeyFields: string[];
|
||||||
|
keyFields?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -193,6 +198,8 @@ export class BulkLoadMapping
|
|||||||
|
|
||||||
valueMappings: { [fieldName: string]: { [fileValue: string]: any } } = {};
|
valueMappings: { [fieldName: string]: { [fileValue: string]: any } } = {};
|
||||||
|
|
||||||
|
isBulkEdit: boolean;
|
||||||
|
keyFields: string;
|
||||||
hasHeaderRow: boolean;
|
hasHeaderRow: boolean;
|
||||||
layout: string;
|
layout: string;
|
||||||
|
|
||||||
@ -211,6 +218,8 @@ export class BulkLoadMapping
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isBulkEdit = tableStructure.isBulkEdit;
|
||||||
|
this.keyFields = tableStructure.keyFields;
|
||||||
this.hasHeaderRow = true;
|
this.hasHeaderRow = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,11 +227,13 @@ export class BulkLoadMapping
|
|||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
private processTableStructure(tableStructure: BulkLoadTableStructure)
|
public processTableStructure(tableStructure: BulkLoadTableStructure)
|
||||||
{
|
{
|
||||||
const prefix = tableStructure.isMain ? "" : tableStructure.associationPath;
|
const prefix = tableStructure.isMain ? "" : tableStructure.associationPath;
|
||||||
this.fieldsByTablePrefix[prefix] = {};
|
this.fieldsByTablePrefix[prefix] = {};
|
||||||
this.tablesByPath[prefix] = tableStructure;
|
this.tablesByPath[prefix] = tableStructure;
|
||||||
|
this.isBulkEdit = tableStructure.isBulkEdit;
|
||||||
|
this.keyFields = tableStructure.keyFields;
|
||||||
|
|
||||||
for (let field of tableStructure.fields)
|
for (let field of tableStructure.fields)
|
||||||
{
|
{
|
||||||
@ -233,13 +244,35 @@ export class BulkLoadMapping
|
|||||||
this.fields[qualifiedName] = bulkLoadField;
|
this.fields[qualifiedName] = bulkLoadField;
|
||||||
this.fieldsByTablePrefix[prefix][qualifiedName] = bulkLoadField;
|
this.fieldsByTablePrefix[prefix][qualifiedName] = bulkLoadField;
|
||||||
|
|
||||||
if (tableStructure.isMain && field.isRequired)
|
if (this.isBulkEdit)
|
||||||
{
|
{
|
||||||
this.requiredFields.push(bulkLoadField);
|
if (this.keyFields == null)
|
||||||
|
{
|
||||||
|
this.unusedFields.push(bulkLoadField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const keyFields = this.keyFields.split("|");
|
||||||
|
if (keyFields.includes(qualifiedName))
|
||||||
|
{
|
||||||
|
this.requiredFields.push(bulkLoadField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.unusedFields.push(bulkLoadField);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.unusedFields.push(bulkLoadField);
|
if (tableStructure.isMain && field.isRequired)
|
||||||
|
{
|
||||||
|
this.requiredFields.push(bulkLoadField);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.unusedFields.push(bulkLoadField);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,14 +299,16 @@ export class BulkLoadMapping
|
|||||||
** take a saved bulk load profile - and convert it into a working bulkLoadMapping
|
** take a saved bulk load profile - and convert it into a working bulkLoadMapping
|
||||||
** for the frontend to use!
|
** for the frontend to use!
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile): BulkLoadMapping
|
public static fromBulkLoadProfile(tableStructure: BulkLoadTableStructure, bulkLoadProfile: BulkLoadProfile, processName?: string): BulkLoadMapping
|
||||||
{
|
{
|
||||||
const bulkLoadMapping = new BulkLoadMapping(tableStructure);
|
const bulkLoadMapping = new BulkLoadMapping(tableStructure);
|
||||||
|
|
||||||
if (bulkLoadProfile.version == "v1")
|
if (bulkLoadProfile.version == "v1")
|
||||||
{
|
{
|
||||||
|
bulkLoadMapping.isBulkEdit = bulkLoadProfile.isBulkEdit;
|
||||||
bulkLoadMapping.hasHeaderRow = bulkLoadProfile.hasHeaderRow;
|
bulkLoadMapping.hasHeaderRow = bulkLoadProfile.hasHeaderRow;
|
||||||
bulkLoadMapping.layout = bulkLoadProfile.layout;
|
bulkLoadMapping.layout = bulkLoadProfile.layout;
|
||||||
|
bulkLoadMapping.keyFields = bulkLoadProfile.keyFields;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// function to get a bulkLoadMapping field by its (full) name - whether that's in the required fields list, //
|
// function to get a bulkLoadMapping field by its (full) name - whether that's in the required fields list, //
|
||||||
@ -322,6 +357,7 @@ export class BulkLoadMapping
|
|||||||
{
|
{
|
||||||
bulkLoadField.valueType = "column";
|
bulkLoadField.valueType = "column";
|
||||||
bulkLoadField.doValueMapping = bulkLoadProfileField.doValueMapping;
|
bulkLoadField.doValueMapping = bulkLoadProfileField.doValueMapping;
|
||||||
|
bulkLoadField.clearIfEmpty = bulkLoadProfileField.clearIfEmpty;
|
||||||
bulkLoadField.headerName = bulkLoadProfileField.headerName;
|
bulkLoadField.headerName = bulkLoadProfileField.headerName;
|
||||||
bulkLoadField.columnIndex = bulkLoadProfileField.columnIndex;
|
bulkLoadField.columnIndex = bulkLoadProfileField.columnIndex;
|
||||||
|
|
||||||
@ -344,6 +380,29 @@ export class BulkLoadMapping
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!bulkLoadMapping.keyFields && tableStructure.possibleKeyFields?.length > 0)
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// look at each of the possible key fields, compare with the fields in the bulk load profile, //
|
||||||
|
// on the first one that matches, use that as the default bulk load mapping key field //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
for (let keyField of tableStructure.possibleKeyFields)
|
||||||
|
{
|
||||||
|
const parts = keyField.split("|");
|
||||||
|
const allPartsMatch = parts.every(part =>
|
||||||
|
(bulkLoadProfile.fieldList ?? []).some((field: BulkLoadProfileField) =>
|
||||||
|
field.fieldName === part
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allPartsMatch)
|
||||||
|
{
|
||||||
|
bulkLoadMapping.keyFields = keyField;
|
||||||
|
break; // stop after the first valid match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (bulkLoadMapping);
|
return (bulkLoadMapping);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -365,6 +424,8 @@ export class BulkLoadMapping
|
|||||||
profile.version = "v1";
|
profile.version = "v1";
|
||||||
profile.hasHeaderRow = this.hasHeaderRow;
|
profile.hasHeaderRow = this.hasHeaderRow;
|
||||||
profile.layout = this.layout;
|
profile.layout = this.layout;
|
||||||
|
profile.isBulkEdit = this.isBulkEdit;
|
||||||
|
profile.keyFields = this.keyFields;
|
||||||
|
|
||||||
for (let bulkLoadField of [...this.requiredFields, ...this.additionalFields])
|
for (let bulkLoadField of [...this.requiredFields, ...this.additionalFields])
|
||||||
{
|
{
|
||||||
@ -384,7 +445,7 @@ export class BulkLoadMapping
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping};
|
const field: BulkLoadProfileField = {fieldName: fullFieldName, columnIndex: bulkLoadField.columnIndex, headerName: bulkLoadField.headerName, doValueMapping: bulkLoadField.doValueMapping, clearIfEmpty: bulkLoadField.clearIfEmpty};
|
||||||
|
|
||||||
if (this.valueMappings[fullFieldName])
|
if (this.valueMappings[fullFieldName])
|
||||||
{
|
{
|
||||||
@ -576,6 +637,16 @@ export class BulkLoadMapping
|
|||||||
return (rs);
|
return (rs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public handleChangeToKeyFields(newKeyFields: any)
|
||||||
|
{
|
||||||
|
this.keyFields = newKeyFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/***************************************************************************
|
/***************************************************************************
|
||||||
**
|
**
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
@ -600,7 +671,7 @@ export class BulkLoadMapping
|
|||||||
{
|
{
|
||||||
const newField = BulkLoadField.clone(field);
|
const newField = BulkLoadField.clone(field);
|
||||||
newField.columnIndex = null;
|
newField.columnIndex = null;
|
||||||
newField.warning = "This field was assigned to a column with a duplicated header"
|
newField.warning = "This field was assigned to a column with a duplicated header";
|
||||||
newRequiredFields.push(newField);
|
newRequiredFields.push(newField);
|
||||||
anyChangesToRequiredFields = true;
|
anyChangesToRequiredFields = true;
|
||||||
}
|
}
|
||||||
@ -616,7 +687,7 @@ export class BulkLoadMapping
|
|||||||
{
|
{
|
||||||
const newField = BulkLoadField.clone(field);
|
const newField = BulkLoadField.clone(field);
|
||||||
newField.columnIndex = null;
|
newField.columnIndex = null;
|
||||||
newField.warning = "This field was assigned to a column with a duplicated header"
|
newField.warning = "This field was assigned to a column with a duplicated header";
|
||||||
newAdditionalFields.push(newField);
|
newAdditionalFields.push(newField);
|
||||||
anyChangesToAdditionalFields = true;
|
anyChangesToAdditionalFields = true;
|
||||||
}
|
}
|
||||||
@ -798,6 +869,8 @@ export class BulkLoadProfile
|
|||||||
fieldList: BulkLoadProfileField[] = [];
|
fieldList: BulkLoadProfileField[] = [];
|
||||||
hasHeaderRow: boolean;
|
hasHeaderRow: boolean;
|
||||||
layout: string;
|
layout: string;
|
||||||
|
isBulkEdit: boolean;
|
||||||
|
keyFields: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkLoadProfileField =
|
type BulkLoadProfileField =
|
||||||
@ -807,6 +880,7 @@ type BulkLoadProfileField =
|
|||||||
headerName?: string,
|
headerName?: string,
|
||||||
defaultValue?: any,
|
defaultValue?: any,
|
||||||
doValueMapping?: boolean,
|
doValueMapping?: boolean,
|
||||||
|
clearIfEmpty?: boolean,
|
||||||
valueMappings?: { [fileValue: string]: any }
|
valueMappings?: { [fileValue: string]: any }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1838,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)
|
||||||
|
@ -1557,7 +1557,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** function to open one of the bulk (insert/edit/delete) processes.
|
** function to open one of the bulk (insert/edit/delete) processes.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") =>
|
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete" | "EditWithFile", processLabelPart: "Load" | "Edit" | "Delete" | "Edit With File") =>
|
||||||
{
|
{
|
||||||
const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`));
|
const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`));
|
||||||
if (processList.length > 0)
|
if (processList.length > 0)
|
||||||
@ -1593,6 +1593,15 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Event handler for the bulk-edit-with-file process being selected
|
||||||
|
*******************************************************************************/
|
||||||
|
const bulkEditWithFileClicked = () =>
|
||||||
|
{
|
||||||
|
openBulkProcess("EditWithFile", "Edit With File");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Event handler for the bulk-delete process being selected
|
** Event handler for the bulk-delete process being selected
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -1612,6 +1621,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);
|
||||||
@ -2845,6 +2870,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
|
|||||||
tableProcesses={tableProcesses}
|
tableProcesses={tableProcesses}
|
||||||
bulkLoadClicked={bulkLoadClicked}
|
bulkLoadClicked={bulkLoadClicked}
|
||||||
bulkEditClicked={bulkEditClicked}
|
bulkEditClicked={bulkEditClicked}
|
||||||
|
bulkEditWithFileClicked={bulkEditWithFileClicked}
|
||||||
bulkDeleteClicked={bulkDeleteClicked}
|
bulkDeleteClicked={bulkDeleteClicked}
|
||||||
processClicked={processClicked}
|
processClicked={processClicked}
|
||||||
/>
|
/>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,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)
|
||||||
|
@ -103,6 +103,30 @@ public class QueryScreenLib
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void openCriteriaPasterAndPasteValues(String fieldName, List<String> values)
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// open the is any of criteria for given field and click the paster button //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorContaining("BUTTON", fieldName).click();
|
||||||
|
qSeleniumLib.waitForSelector("#criteriaOperator").click();
|
||||||
|
qSeleniumLib.waitForSelectorContaining("LI", "is any of").click();
|
||||||
|
qSeleniumLib.waitForMillis(250);
|
||||||
|
qSeleniumLib.waitForSelector(".criteriaPasterButton").click();
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
// paste the values into the textarea //
|
||||||
|
////////////////////////////////////////
|
||||||
|
qSeleniumLib
|
||||||
|
.waitForSelector(".criteriaPasterTextArea textarea#outlined-multiline-static")
|
||||||
|
.sendKeys(String.join("\n", values));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -22,13 +22,16 @@
|
|||||||
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
|
package com.kingsrook.qqq.frontend.materialdashboard.selenium.tests.query;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QBaseSeleniumTest;
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QQQMaterialDashboardSelectors;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QQQMaterialDashboardSelectors;
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QueryScreenLib;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.QueryScreenLib;
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.CapturedContext;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.CapturedContext;
|
||||||
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.QSeleniumJavalin;
|
import com.kingsrook.qqq.frontend.materialdashboard.selenium.lib.javalin.QSeleniumJavalin;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
|
||||||
@ -200,6 +203,204 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testCriteriaPasterHappyPath()
|
||||||
|
{
|
||||||
|
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||||
|
|
||||||
|
////////////////////////////
|
||||||
|
// go to the person page //
|
||||||
|
////////////////////////////
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||||
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// open the paste values dialog UI //
|
||||||
|
//////////////////////////////////////
|
||||||
|
queryScreenLib.openCriteriaPasterAndPasteValues("id", List.of("1", "2", "3"));
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// wait for chips to appear in the filter values review box //
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
assertFilterPasterChipCounts(3, 0);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// confirm each chip has the blue color class //
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorAll(".MuiChip-root", 3).forEach(chip ->
|
||||||
|
{
|
||||||
|
String classAttr = chip.getAttribute("class");
|
||||||
|
assertThat(classAttr).contains("MuiChip-colorInfo");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testCriteriaPasterInvalidValueValidation()
|
||||||
|
{
|
||||||
|
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||||
|
|
||||||
|
////////////////////////////
|
||||||
|
// go to the person page //
|
||||||
|
////////////////////////////
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||||
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// open the paste values dialog UI //
|
||||||
|
//////////////////////////////////////
|
||||||
|
queryScreenLib.openCriteriaPasterAndPasteValues("id", List.of("1", "a", "3"));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
// check that chips match values and are classified //
|
||||||
|
//////////////////////////////////////////////////////
|
||||||
|
assertFilterPasterChipCounts(2, 1);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
// confirm that an appropriate validation error message is shown //
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
WebElement errorMessage = qSeleniumLib.waitForSelectorContaining("span", "value is not a number");
|
||||||
|
assertThat(errorMessage.getText()).contains("value is not a number");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testCriteriaPasterDuplicateValueValidation()
|
||||||
|
{
|
||||||
|
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||||
|
|
||||||
|
////////////////////////////
|
||||||
|
// go to the person page //
|
||||||
|
////////////////////////////
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||||
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// open the paste values dialog UI //
|
||||||
|
//////////////////////////////////////
|
||||||
|
List<String> pastedValues = List.of("1", "1", "1", "2", "2");
|
||||||
|
queryScreenLib.openCriteriaPasterAndPasteValues("id", pastedValues);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// expected chip & uniqueness calculations //
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
int totalCount = pastedValues.size(); // 5
|
||||||
|
int uniqueCount = new HashSet<>(pastedValues).size(); // 2
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// chips should show dupes //
|
||||||
|
/////////////////////////////
|
||||||
|
assertFilterPasterChipCounts(pastedValues.size(), 0);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// counter text should match “5 values (2 unique)” (or alike) //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
String expectedCounter = totalCount + " values (" + uniqueCount + " unique)";
|
||||||
|
WebElement counterLabel = qSeleniumLib.waitForSelectorContaining("span", "unique");
|
||||||
|
assertThat(counterLabel.getText()).contains(expectedCounter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testCriteriaPasterWithPVSHappyPath()
|
||||||
|
{
|
||||||
|
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||||
|
|
||||||
|
////////////////////////////
|
||||||
|
// go to the person page //
|
||||||
|
////////////////////////////
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||||
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// open the paste values dialog UI //
|
||||||
|
//////////////////////////////////////
|
||||||
|
queryScreenLib.addBasicFilter("home city");
|
||||||
|
queryScreenLib.openCriteriaPasterAndPasteValues("home city", List.of("St. Louis", "chesterfield"));
|
||||||
|
qSeleniumLib.waitForSeconds(1);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// wait for chips to appear in the filter values review box //
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
assertFilterPasterChipCounts(2, 0);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// confirm each chip has the blue color class //
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
qSeleniumLib.waitForSelectorAll(".MuiChip-root", 2).forEach(chip ->
|
||||||
|
{
|
||||||
|
String classAttr = chip.getAttribute("class");
|
||||||
|
assertThat(classAttr).contains("MuiChip-colorInfo");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testCriteriaPasterWithPVSTwoGoodOneBadAndDupes()
|
||||||
|
{
|
||||||
|
QueryScreenLib queryScreenLib = new QueryScreenLib(qSeleniumLib);
|
||||||
|
|
||||||
|
////////////////////////////
|
||||||
|
// go to the person page //
|
||||||
|
////////////////////////////
|
||||||
|
qSeleniumLib.gotoAndWaitForBreadcrumbHeaderToContain("/peopleApp/greetingsApp/person", "Person");
|
||||||
|
queryScreenLib.waitForQueryToHaveRan();
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
// open the paste values dialog UI //
|
||||||
|
//////////////////////////////////////
|
||||||
|
List<String> cities = List.of("St. Louis", "chesterfield", "Maryville", "st. louis", "st. louis", "chesterfield");
|
||||||
|
queryScreenLib.addBasicFilter("home city");
|
||||||
|
queryScreenLib.openCriteriaPasterAndPasteValues("home city", cities);
|
||||||
|
qSeleniumLib.waitForSeconds(1);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// expected chip & uniqueness calculations //
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
int totalCount = cities.size();
|
||||||
|
int uniqueCount = cities.stream().map(String::toLowerCase).collect(Collectors.toSet()).size();
|
||||||
|
|
||||||
|
///////////////////////////////////////////
|
||||||
|
// chips should show dupes and bad chips //
|
||||||
|
///////////////////////////////////////////
|
||||||
|
assertFilterPasterChipCounts(5, 1);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// counter text should match “5 values (2 unique)” (or alike) //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
String expectedCounter = totalCount + " values (" + uniqueCount + " unique)";
|
||||||
|
WebElement counterLabel = qSeleniumLib.waitForSelectorContaining("span", "unique");
|
||||||
|
assertThat(counterLabel.getText()).contains(expectedCounter);
|
||||||
|
|
||||||
|
//////////////////////////////////////////
|
||||||
|
// assert the "value not found" warning //
|
||||||
|
//////////////////////////////////////////
|
||||||
|
WebElement warning = qSeleniumLib.waitForSelectorContaining("span", "was not found");
|
||||||
|
assertThat(warning.getText()).contains("1 value was not found and will not be added to the filter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -273,4 +474,18 @@ public class QueryScreenTest extends QBaseSeleniumTest
|
|||||||
queryScreenLib.clickAdvancedFilterClearIcon();
|
queryScreenLib.clickAdvancedFilterClearIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void assertFilterPasterChipCounts(int expectedValid, int expectedInvalid)
|
||||||
|
{
|
||||||
|
List<WebElement> chips = qSeleniumLib.waitForSelectorAll(".MuiChip-root", expectedValid + expectedInvalid);
|
||||||
|
long validCount = chips.stream().filter(c -> c.getAttribute("class").contains("MuiChip-colorInfo")).count();
|
||||||
|
long errorCount = chips.stream().filter(c -> c.getAttribute("class").contains("MuiChip-colorError")).count();
|
||||||
|
|
||||||
|
assertThat(validCount).isEqualTo(expectedValid);
|
||||||
|
assertThat(errorCount).isEqualTo(expectedInvalid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user